From 32f16408b45d4ec2894a88b31783feafc26555a2 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 22 Aug 2019 15:21:10 -0700 Subject: [PATCH 001/407] Fixes a bug, this undefined in closure --- data/converter/raw-embedded-value-to-object-converter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/converter/raw-embedded-value-to-object-converter.js b/data/converter/raw-embedded-value-to-object-converter.js index 654277428e..47756b5ccc 100644 --- a/data/converter/raw-embedded-value-to-object-converter.js +++ b/data/converter/raw-embedded-value-to-object-converter.js @@ -52,7 +52,7 @@ exports.RawEmbeddedValueToObjectConverter = RawValueToObjectConverter.specialize } else { if(v) { - return this._convertOneValue(v,typeToFetch, service); + return self._convertOneValue(v,typeToFetch, service); } } }); From 0a12cff7394ccdce8b03e01955b90eff62bb2b86 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 22 Aug 2019 15:22:06 -0700 Subject: [PATCH 002/407] Adds a default implementation for mapRawDataToObject beyond calling deprecated mapFromRawData method --- data/service/raw-data-service.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 1f56b52573..7c68f2d60c 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -12,7 +12,8 @@ var DataService = require("data/service/data-service").DataService, parse = require("frb/parse"), Scope = require("frb/scope"), compile = require("frb/compile-evaluator"), - Promise = require("core/promise").Promise; + Promise = require("core/promise").Promise, + RawDataService; /** * Provides data objects of certain types and manages changes to them based on @@ -34,7 +35,7 @@ var DataService = require("data/service/data-service").DataService, * @class * @extends DataService */ -exports.RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { +exports.RawDataService = RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { /*************************************************************************** * Initializing @@ -777,7 +778,22 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot mapRawDataToObject: { value: function (rawData, object, context) { - return this.mapFromRawData(object, rawData, context); + + //Test for backward compatibility, mapFromRawData is deprecated. + if(this.mapFromRawData !== RawDataService.prototype.mapFromRawData) { + return this.mapFromRawData(object, rawData, context); + } + //New default implementation, just move the data over: + else { + //Loop on object keys: + var aKey; + + for (aKey in object) { + if(rawData.hasOwnProperty(aKey)) { + object[aKey] = rawData[aKey]; + } + } + } } }, /** From 683672ed49a62880c924bc8218113eb1569ba2c8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 28 Aug 2019 11:53:34 -0700 Subject: [PATCH 003/407] Revert changes to mapRawDataToObject --- data/service/raw-data-service.js | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 7c68f2d60c..1f56b52573 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -12,8 +12,7 @@ var DataService = require("data/service/data-service").DataService, parse = require("frb/parse"), Scope = require("frb/scope"), compile = require("frb/compile-evaluator"), - Promise = require("core/promise").Promise, - RawDataService; + Promise = require("core/promise").Promise; /** * Provides data objects of certain types and manages changes to them based on @@ -35,7 +34,7 @@ var DataService = require("data/service/data-service").DataService, * @class * @extends DataService */ -exports.RawDataService = RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { +exports.RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { /*************************************************************************** * Initializing @@ -778,22 +777,7 @@ exports.RawDataService = RawDataService = DataService.specialize(/** @lends RawD mapRawDataToObject: { value: function (rawData, object, context) { - - //Test for backward compatibility, mapFromRawData is deprecated. - if(this.mapFromRawData !== RawDataService.prototype.mapFromRawData) { - return this.mapFromRawData(object, rawData, context); - } - //New default implementation, just move the data over: - else { - //Loop on object keys: - var aKey; - - for (aKey in object) { - if(rawData.hasOwnProperty(aKey)) { - object[aKey] = rawData[aKey]; - } - } - } + return this.mapFromRawData(object, rawData, context); } }, /** From be8b7abbfc7115a01688b29ea6f9204bb8a2033c Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 28 Aug 2019 11:54:08 -0700 Subject: [PATCH 004/407] Adds notes for work to do --- data/service/graphql-service.js | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 data/service/graphql-service.js diff --git a/data/service/graphql-service.js b/data/service/graphql-service.js new file mode 100644 index 0000000000..3fdd7e279e --- /dev/null +++ b/data/service/graphql-service.js @@ -0,0 +1,59 @@ +var HttpService = require("data/service/http-service").HttpService, + DataQuery = require("data/model/data-query").DataQuery, + Enumeration = require("data/model/enumeration").Enumeration, + Map = require("collections/map"), + Montage = require("montage").Montage, + parse = require("frb/parse"), + compile = require("frb/compile-evaluator"), + evaluate = require("frb/evaluate"), + Scope = require("frb/scope"), + Promise = require("core/promise").Promise; + + +/** + * Superclass for GraphQLService services communicating using HTTP, but also with push capable API for subscription feature + * + * @class + */ + + /* TODO: + + - How to support the notion of implements / Interfaces / "Categories (in the objective-C sense" + Basically an object descriptor implements a set of property descriptor and needs to be "adopted" + by other objet descriptors such as the interface's property descriptors become available on the adopter. + It should be fairly straightforwards to add an array of "interfaces", and when added we do what's needed. + this exists in Shopify. +- Needs the ability for queries to dynamically specify the properties of objects fetched that should be returned. + Today there's a set of "prerequisite" properties, but we need to move to a system where properties dynamically + requested by the user interface, through binding or programmatically created, can specify what is needed. + For binding, a frequent pattern happens when displaying a list, triggering the same set of properties on each iteration. + We need to be able group and batch the queries to resolve this more efficiently, which GraphQL is capable of. +- We need to be able to automatically transform a query and it's criteria into a GraphQL query +- we should be able to transform an existing GraphQL schema into a montage data one. +- Some of Shopify objects have "methods", for example an Image has transformedSrc, whicj returns a URL and take as argument: + - crop ( CropRegion ) //Crops the image according to the specified region. + - maxHeight ( Int ) //Image height in pixels between 1 and 5760. + - maxWidth ( Int ) //Image width in pixels between 1 and 5760. + - preferredContentType ( ImageContentType ) //Best effort conversion of image into content type (SVG -> PNG, Anything -> JGP, Anything -> WEBP are supported). + - scale ( Int ) //Image size multiplier for high-resolution retina displays. Must be between 1 and 3. Default value: true + + The RemoteProcedureCall operation should be used to model this +- When we receive data back, the shape of the result is related to the shape of the query. One thing we're missing is going from the name of the objectdescriptor to the name used for expressing the query and the same one used for results. +- shopify/graphql-js-client/src/decode.js has an implementation that reads the structure. +- Shopify has 2 levels of API: storefront and admin, and 2 matching different authentication. Some API / properties of one object are only available through one API vs the other. This is going to require some changes? Or should there be 2 different dataServices? + */ + +/* + * + * @extends HttpService + */ +var GraphQLService = exports.GraphQLService = HttpService.specialize(/** @lends GraphQLService.prototype */ { + + deserializeSelf: { + value: function (deserializer) { + var value, result; + // console.log("AirtableService super deserialize #"+deserializeSelfCount); + return this.super(deserializer); + } + } +}); From 4454f1f7defce308650ac85620ef3f8793dab2c3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 29 Aug 2019 15:05:55 -0700 Subject: [PATCH 005/407] Adds a default data mapping if an object doesn't have an obect desccriptor to provide minimum service of carrying raw data to the final object. --- data/service/raw-data-service.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 1f56b52573..8a0cec167a 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -735,6 +735,10 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot }, "mapSelectorToRawDataSelector", "mapSelectorToRawDataQuery"), }, + _defaultDataMapping : { + value: new DataMapping + }, + /** * Retrieve DataMapping for this object. * @@ -747,11 +751,16 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot mapping = objectDescriptor && this.mappingWithType(objectDescriptor); - if (!mapping && objectDescriptor) { - mapping = this._objectDescriptorMappings.get(objectDescriptor); - if (!mapping) { - mapping = DataMapping.withObjectDescriptor(objectDescriptor); - this._objectDescriptorMappings.set(objectDescriptor, mapping); + if (!mapping) { + if(objectDescriptor) { + mapping = this._objectDescriptorMappings.get(objectDescriptor); + if (!mapping) { + mapping = DataMapping.withObjectDescriptor(objectDescriptor); + this._objectDescriptorMappings.set(objectDescriptor, mapping); + } + } + else { + mapping = this._defaultDataMapping; } } From b94eaaec6bba1e9db62cf58c82a0c2a46c035e17 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 31 Aug 2019 09:43:56 -0700 Subject: [PATCH 006/407] Fixed a typo --- data/service/data-operation.js | 2 +- ui/repetition.reel/repetition.js | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 369cc11602..10a0a2cd91 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -52,7 +52,7 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ clientId: { - } + }, /** * A property used to give a meaningful name to an operation diff --git a/ui/repetition.reel/repetition.js b/ui/repetition.reel/repetition.js index debc101b2b..46fa713252 100644 --- a/ui/repetition.reel/repetition.js +++ b/ui/repetition.reel/repetition.js @@ -208,6 +208,8 @@ var Iteration = exports.Iteration = Montage.specialize( /** @lends Iteration.pro return this._selected; }, set: function (value) { + var myObject = this.object, + myValue = value; value = !!value; if (this.object && this.repetition && this.repetition.contentController) { if (value) { @@ -216,12 +218,19 @@ var Iteration = exports.Iteration = Montage.specialize( /** @lends Iteration.pro this.repetition.contentController.selection.delete(this.object); } } - if (this._selected !== value) { - this.dispatchBeforeOwnPropertyChange("selected", this._selected); - this._selected = value; - this._updateRepetitionDirtyClassIteration(); - this.dispatchOwnPropertyChange("selected", value); - } + //sketched idea to workaround issue Javier found that causes re-entant change + //in the middle of this and causes unexpected behavior + //Checking that nothing happened in a re-entrant way + //if(this.object === myObject) { + + if (this._selected !== value) { + this.dispatchBeforeOwnPropertyChange("selected", this._selected); + this._selected = value; + this._updateRepetitionDirtyClassIteration(); + this.dispatchOwnPropertyChange("selected", value); + } + //} + } }, @@ -2098,6 +2107,8 @@ var Repetition = exports.Repetition = Component.specialize(/** @lends Repetition } } + //this.dispatchEvent("press"); + this._ignoreSelection(); } }, From 516f005c08f6ae4aeebdeb735b6969f8e956ad52 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 31 Aug 2019 10:42:44 -0700 Subject: [PATCH 007/407] Fixes another typo --- data/service/data-operation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 10a0a2cd91..35e10b3431 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -51,7 +51,7 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ */ clientId: { - + value: undefined }, /** From 7047086d1790b27626c4cd5f9d8d5a46c7321465 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 2 Sep 2019 14:41:09 -0700 Subject: [PATCH 008/407] Change to deal with a package being used on montage script tag, like data-package="/app" --- montage.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/montage.js b/montage.js index de5df1fe09..a1588b2016 100644 --- a/montage.js +++ b/montage.js @@ -262,11 +262,17 @@ allModulesLoaded(); }; - var montageLocation; + var montageLocation, appLocation; function loadModuleScript(path, callback) { - montageLocation = montageLocation || resolve(global.location, params.montageLocation); + if(!montageLocation) { + montageLocation = montageLocation || resolve(global.location, params.montageLocation); + appLocation = resolve(global.location, params.package); + if(!appLocation.lastIndexOf("/") !== appLocation.length-1) { + appLocation += "/"; + } + } // try loading script relative to app first (npm 3+) - browser.load(resolve(global.location, path), function (err, script) { + browser.load(resolve(appLocation || global.location, path), function (err, script) { if (err) { // if that fails, the app may have been installed with // npm 2 or with --legacy-bundling, in which case the @@ -954,7 +960,7 @@ }); // Will throw error if there is one - }).done(); + }); }); }; From 6d012ed03d1719d4bd284f08336b4521605d0379 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 3 Sep 2019 11:51:53 -0700 Subject: [PATCH 009/407] Fixes regression when app has no package --- montage.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/montage.js b/montage.js index a1588b2016..60391c7e1f 100644 --- a/montage.js +++ b/montage.js @@ -266,9 +266,11 @@ function loadModuleScript(path, callback) { if(!montageLocation) { montageLocation = montageLocation || resolve(global.location, params.montageLocation); - appLocation = resolve(global.location, params.package); - if(!appLocation.lastIndexOf("/") !== appLocation.length-1) { - appLocation += "/"; + if(params.package) { + appLocation = resolve(global.location, params.package); + if(!appLocation.lastIndexOf("/") !== appLocation.length-1) { + appLocation += "/"; + } } } // try loading script relative to app first (npm 3+) From aba79fac100003751d0a022800dff2751ec79eeb Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 22 Aug 2019 15:21:10 -0700 Subject: [PATCH 010/407] Fixes a bug, this undefined in closure --- data/converter/raw-embedded-value-to-object-converter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/converter/raw-embedded-value-to-object-converter.js b/data/converter/raw-embedded-value-to-object-converter.js index 654277428e..47756b5ccc 100644 --- a/data/converter/raw-embedded-value-to-object-converter.js +++ b/data/converter/raw-embedded-value-to-object-converter.js @@ -52,7 +52,7 @@ exports.RawEmbeddedValueToObjectConverter = RawValueToObjectConverter.specialize } else { if(v) { - return this._convertOneValue(v,typeToFetch, service); + return self._convertOneValue(v,typeToFetch, service); } } }); From 8ff8204c76a036a27ee79c8bc6bce3047326f507 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 22 Aug 2019 15:22:06 -0700 Subject: [PATCH 011/407] Adds a default implementation for mapRawDataToObject beyond calling deprecated mapFromRawData method --- data/service/raw-data-service.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 1f56b52573..7c68f2d60c 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -12,7 +12,8 @@ var DataService = require("data/service/data-service").DataService, parse = require("frb/parse"), Scope = require("frb/scope"), compile = require("frb/compile-evaluator"), - Promise = require("core/promise").Promise; + Promise = require("core/promise").Promise, + RawDataService; /** * Provides data objects of certain types and manages changes to them based on @@ -34,7 +35,7 @@ var DataService = require("data/service/data-service").DataService, * @class * @extends DataService */ -exports.RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { +exports.RawDataService = RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { /*************************************************************************** * Initializing @@ -777,7 +778,22 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot mapRawDataToObject: { value: function (rawData, object, context) { - return this.mapFromRawData(object, rawData, context); + + //Test for backward compatibility, mapFromRawData is deprecated. + if(this.mapFromRawData !== RawDataService.prototype.mapFromRawData) { + return this.mapFromRawData(object, rawData, context); + } + //New default implementation, just move the data over: + else { + //Loop on object keys: + var aKey; + + for (aKey in object) { + if(rawData.hasOwnProperty(aKey)) { + object[aKey] = rawData[aKey]; + } + } + } } }, /** From 34e8504c60b183f396ff973ca48fd8889749d539 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 28 Aug 2019 11:53:34 -0700 Subject: [PATCH 012/407] Revert changes to mapRawDataToObject --- data/service/raw-data-service.js | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 7c68f2d60c..1f56b52573 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -12,8 +12,7 @@ var DataService = require("data/service/data-service").DataService, parse = require("frb/parse"), Scope = require("frb/scope"), compile = require("frb/compile-evaluator"), - Promise = require("core/promise").Promise, - RawDataService; + Promise = require("core/promise").Promise; /** * Provides data objects of certain types and manages changes to them based on @@ -35,7 +34,7 @@ var DataService = require("data/service/data-service").DataService, * @class * @extends DataService */ -exports.RawDataService = RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { +exports.RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { /*************************************************************************** * Initializing @@ -778,22 +777,7 @@ exports.RawDataService = RawDataService = DataService.specialize(/** @lends RawD mapRawDataToObject: { value: function (rawData, object, context) { - - //Test for backward compatibility, mapFromRawData is deprecated. - if(this.mapFromRawData !== RawDataService.prototype.mapFromRawData) { - return this.mapFromRawData(object, rawData, context); - } - //New default implementation, just move the data over: - else { - //Loop on object keys: - var aKey; - - for (aKey in object) { - if(rawData.hasOwnProperty(aKey)) { - object[aKey] = rawData[aKey]; - } - } - } + return this.mapFromRawData(object, rawData, context); } }, /** From e19e0e93d86954a96e00d75435fed3412d9e4613 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 28 Aug 2019 11:54:08 -0700 Subject: [PATCH 013/407] Adds notes for work to do --- data/service/graphql-service.js | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 data/service/graphql-service.js diff --git a/data/service/graphql-service.js b/data/service/graphql-service.js new file mode 100644 index 0000000000..3fdd7e279e --- /dev/null +++ b/data/service/graphql-service.js @@ -0,0 +1,59 @@ +var HttpService = require("data/service/http-service").HttpService, + DataQuery = require("data/model/data-query").DataQuery, + Enumeration = require("data/model/enumeration").Enumeration, + Map = require("collections/map"), + Montage = require("montage").Montage, + parse = require("frb/parse"), + compile = require("frb/compile-evaluator"), + evaluate = require("frb/evaluate"), + Scope = require("frb/scope"), + Promise = require("core/promise").Promise; + + +/** + * Superclass for GraphQLService services communicating using HTTP, but also with push capable API for subscription feature + * + * @class + */ + + /* TODO: + + - How to support the notion of implements / Interfaces / "Categories (in the objective-C sense" + Basically an object descriptor implements a set of property descriptor and needs to be "adopted" + by other objet descriptors such as the interface's property descriptors become available on the adopter. + It should be fairly straightforwards to add an array of "interfaces", and when added we do what's needed. + this exists in Shopify. +- Needs the ability for queries to dynamically specify the properties of objects fetched that should be returned. + Today there's a set of "prerequisite" properties, but we need to move to a system where properties dynamically + requested by the user interface, through binding or programmatically created, can specify what is needed. + For binding, a frequent pattern happens when displaying a list, triggering the same set of properties on each iteration. + We need to be able group and batch the queries to resolve this more efficiently, which GraphQL is capable of. +- We need to be able to automatically transform a query and it's criteria into a GraphQL query +- we should be able to transform an existing GraphQL schema into a montage data one. +- Some of Shopify objects have "methods", for example an Image has transformedSrc, whicj returns a URL and take as argument: + - crop ( CropRegion ) //Crops the image according to the specified region. + - maxHeight ( Int ) //Image height in pixels between 1 and 5760. + - maxWidth ( Int ) //Image width in pixels between 1 and 5760. + - preferredContentType ( ImageContentType ) //Best effort conversion of image into content type (SVG -> PNG, Anything -> JGP, Anything -> WEBP are supported). + - scale ( Int ) //Image size multiplier for high-resolution retina displays. Must be between 1 and 3. Default value: true + + The RemoteProcedureCall operation should be used to model this +- When we receive data back, the shape of the result is related to the shape of the query. One thing we're missing is going from the name of the objectdescriptor to the name used for expressing the query and the same one used for results. +- shopify/graphql-js-client/src/decode.js has an implementation that reads the structure. +- Shopify has 2 levels of API: storefront and admin, and 2 matching different authentication. Some API / properties of one object are only available through one API vs the other. This is going to require some changes? Or should there be 2 different dataServices? + */ + +/* + * + * @extends HttpService + */ +var GraphQLService = exports.GraphQLService = HttpService.specialize(/** @lends GraphQLService.prototype */ { + + deserializeSelf: { + value: function (deserializer) { + var value, result; + // console.log("AirtableService super deserialize #"+deserializeSelfCount); + return this.super(deserializer); + } + } +}); From af8e51b5fa923a0b8be0a5ff5677763f6d50314c Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 29 Aug 2019 15:05:55 -0700 Subject: [PATCH 014/407] Adds a default data mapping if an object doesn't have an obect desccriptor to provide minimum service of carrying raw data to the final object. --- data/service/raw-data-service.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 1f56b52573..8a0cec167a 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -735,6 +735,10 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot }, "mapSelectorToRawDataSelector", "mapSelectorToRawDataQuery"), }, + _defaultDataMapping : { + value: new DataMapping + }, + /** * Retrieve DataMapping for this object. * @@ -747,11 +751,16 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot mapping = objectDescriptor && this.mappingWithType(objectDescriptor); - if (!mapping && objectDescriptor) { - mapping = this._objectDescriptorMappings.get(objectDescriptor); - if (!mapping) { - mapping = DataMapping.withObjectDescriptor(objectDescriptor); - this._objectDescriptorMappings.set(objectDescriptor, mapping); + if (!mapping) { + if(objectDescriptor) { + mapping = this._objectDescriptorMappings.get(objectDescriptor); + if (!mapping) { + mapping = DataMapping.withObjectDescriptor(objectDescriptor); + this._objectDescriptorMappings.set(objectDescriptor, mapping); + } + } + else { + mapping = this._defaultDataMapping; } } From 2f4ff45aa71c625e7cb088173661419fb33db5be Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 31 Aug 2019 09:43:56 -0700 Subject: [PATCH 015/407] Fixed a typo --- ui/repetition.reel/repetition.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/ui/repetition.reel/repetition.js b/ui/repetition.reel/repetition.js index debc101b2b..46fa713252 100644 --- a/ui/repetition.reel/repetition.js +++ b/ui/repetition.reel/repetition.js @@ -208,6 +208,8 @@ var Iteration = exports.Iteration = Montage.specialize( /** @lends Iteration.pro return this._selected; }, set: function (value) { + var myObject = this.object, + myValue = value; value = !!value; if (this.object && this.repetition && this.repetition.contentController) { if (value) { @@ -216,12 +218,19 @@ var Iteration = exports.Iteration = Montage.specialize( /** @lends Iteration.pro this.repetition.contentController.selection.delete(this.object); } } - if (this._selected !== value) { - this.dispatchBeforeOwnPropertyChange("selected", this._selected); - this._selected = value; - this._updateRepetitionDirtyClassIteration(); - this.dispatchOwnPropertyChange("selected", value); - } + //sketched idea to workaround issue Javier found that causes re-entant change + //in the middle of this and causes unexpected behavior + //Checking that nothing happened in a re-entrant way + //if(this.object === myObject) { + + if (this._selected !== value) { + this.dispatchBeforeOwnPropertyChange("selected", this._selected); + this._selected = value; + this._updateRepetitionDirtyClassIteration(); + this.dispatchOwnPropertyChange("selected", value); + } + //} + } }, @@ -2098,6 +2107,8 @@ var Repetition = exports.Repetition = Component.specialize(/** @lends Repetition } } + //this.dispatchEvent("press"); + this._ignoreSelection(); } }, From 72e5d2d112753372330fa7ccf0361325a3c2595e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 2 Sep 2019 14:41:09 -0700 Subject: [PATCH 016/407] Change to deal with a package being used on montage script tag, like data-package="/app" --- montage.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/montage.js b/montage.js index de5df1fe09..a1588b2016 100644 --- a/montage.js +++ b/montage.js @@ -262,11 +262,17 @@ allModulesLoaded(); }; - var montageLocation; + var montageLocation, appLocation; function loadModuleScript(path, callback) { - montageLocation = montageLocation || resolve(global.location, params.montageLocation); + if(!montageLocation) { + montageLocation = montageLocation || resolve(global.location, params.montageLocation); + appLocation = resolve(global.location, params.package); + if(!appLocation.lastIndexOf("/") !== appLocation.length-1) { + appLocation += "/"; + } + } // try loading script relative to app first (npm 3+) - browser.load(resolve(global.location, path), function (err, script) { + browser.load(resolve(appLocation || global.location, path), function (err, script) { if (err) { // if that fails, the app may have been installed with // npm 2 or with --legacy-bundling, in which case the @@ -954,7 +960,7 @@ }); // Will throw error if there is one - }).done(); + }); }); }; From e50508cea5d6df7eee8573de7c70ecabe2a5e790 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 3 Sep 2019 11:51:53 -0700 Subject: [PATCH 017/407] Fixes regression when app has no package --- montage.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/montage.js b/montage.js index a1588b2016..60391c7e1f 100644 --- a/montage.js +++ b/montage.js @@ -266,9 +266,11 @@ function loadModuleScript(path, callback) { if(!montageLocation) { montageLocation = montageLocation || resolve(global.location, params.montageLocation); - appLocation = resolve(global.location, params.package); - if(!appLocation.lastIndexOf("/") !== appLocation.length-1) { - appLocation += "/"; + if(params.package) { + appLocation = resolve(global.location, params.package); + if(!appLocation.lastIndexOf("/") !== appLocation.length-1) { + appLocation += "/"; + } } } // try loading script relative to app first (npm 3+) From bc68ae65d15cf7f6ba34f6df23dffd07de0a95c1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 22 Aug 2019 15:21:10 -0700 Subject: [PATCH 018/407] Fixes a bug, this undefined in closure --- data/converter/raw-embedded-value-to-object-converter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/converter/raw-embedded-value-to-object-converter.js b/data/converter/raw-embedded-value-to-object-converter.js index 654277428e..47756b5ccc 100644 --- a/data/converter/raw-embedded-value-to-object-converter.js +++ b/data/converter/raw-embedded-value-to-object-converter.js @@ -52,7 +52,7 @@ exports.RawEmbeddedValueToObjectConverter = RawValueToObjectConverter.specialize } else { if(v) { - return this._convertOneValue(v,typeToFetch, service); + return self._convertOneValue(v,typeToFetch, service); } } }); From 57c0ea77064ab48b2061c13aa8ba72d5755fa42e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 22 Aug 2019 15:22:06 -0700 Subject: [PATCH 019/407] Adds a default implementation for mapRawDataToObject beyond calling deprecated mapFromRawData method --- data/service/raw-data-service.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 1f56b52573..7c68f2d60c 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -12,7 +12,8 @@ var DataService = require("data/service/data-service").DataService, parse = require("frb/parse"), Scope = require("frb/scope"), compile = require("frb/compile-evaluator"), - Promise = require("core/promise").Promise; + Promise = require("core/promise").Promise, + RawDataService; /** * Provides data objects of certain types and manages changes to them based on @@ -34,7 +35,7 @@ var DataService = require("data/service/data-service").DataService, * @class * @extends DataService */ -exports.RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { +exports.RawDataService = RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { /*************************************************************************** * Initializing @@ -777,7 +778,22 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot mapRawDataToObject: { value: function (rawData, object, context) { - return this.mapFromRawData(object, rawData, context); + + //Test for backward compatibility, mapFromRawData is deprecated. + if(this.mapFromRawData !== RawDataService.prototype.mapFromRawData) { + return this.mapFromRawData(object, rawData, context); + } + //New default implementation, just move the data over: + else { + //Loop on object keys: + var aKey; + + for (aKey in object) { + if(rawData.hasOwnProperty(aKey)) { + object[aKey] = rawData[aKey]; + } + } + } } }, /** From 31b9980c72fb866e97b39552111caee4411ad125 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 28 Aug 2019 11:53:34 -0700 Subject: [PATCH 020/407] Revert changes to mapRawDataToObject --- data/service/raw-data-service.js | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 7c68f2d60c..1f56b52573 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -12,8 +12,7 @@ var DataService = require("data/service/data-service").DataService, parse = require("frb/parse"), Scope = require("frb/scope"), compile = require("frb/compile-evaluator"), - Promise = require("core/promise").Promise, - RawDataService; + Promise = require("core/promise").Promise; /** * Provides data objects of certain types and manages changes to them based on @@ -35,7 +34,7 @@ var DataService = require("data/service/data-service").DataService, * @class * @extends DataService */ -exports.RawDataService = RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { +exports.RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { /*************************************************************************** * Initializing @@ -778,22 +777,7 @@ exports.RawDataService = RawDataService = DataService.specialize(/** @lends RawD mapRawDataToObject: { value: function (rawData, object, context) { - - //Test for backward compatibility, mapFromRawData is deprecated. - if(this.mapFromRawData !== RawDataService.prototype.mapFromRawData) { - return this.mapFromRawData(object, rawData, context); - } - //New default implementation, just move the data over: - else { - //Loop on object keys: - var aKey; - - for (aKey in object) { - if(rawData.hasOwnProperty(aKey)) { - object[aKey] = rawData[aKey]; - } - } - } + return this.mapFromRawData(object, rawData, context); } }, /** From e0eb4ac30c9aa687c6784d17bbfb90b9830b9521 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 28 Aug 2019 11:54:08 -0700 Subject: [PATCH 021/407] Adds notes for work to do --- data/service/graphql-service.js | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 data/service/graphql-service.js diff --git a/data/service/graphql-service.js b/data/service/graphql-service.js new file mode 100644 index 0000000000..3fdd7e279e --- /dev/null +++ b/data/service/graphql-service.js @@ -0,0 +1,59 @@ +var HttpService = require("data/service/http-service").HttpService, + DataQuery = require("data/model/data-query").DataQuery, + Enumeration = require("data/model/enumeration").Enumeration, + Map = require("collections/map"), + Montage = require("montage").Montage, + parse = require("frb/parse"), + compile = require("frb/compile-evaluator"), + evaluate = require("frb/evaluate"), + Scope = require("frb/scope"), + Promise = require("core/promise").Promise; + + +/** + * Superclass for GraphQLService services communicating using HTTP, but also with push capable API for subscription feature + * + * @class + */ + + /* TODO: + + - How to support the notion of implements / Interfaces / "Categories (in the objective-C sense" + Basically an object descriptor implements a set of property descriptor and needs to be "adopted" + by other objet descriptors such as the interface's property descriptors become available on the adopter. + It should be fairly straightforwards to add an array of "interfaces", and when added we do what's needed. + this exists in Shopify. +- Needs the ability for queries to dynamically specify the properties of objects fetched that should be returned. + Today there's a set of "prerequisite" properties, but we need to move to a system where properties dynamically + requested by the user interface, through binding or programmatically created, can specify what is needed. + For binding, a frequent pattern happens when displaying a list, triggering the same set of properties on each iteration. + We need to be able group and batch the queries to resolve this more efficiently, which GraphQL is capable of. +- We need to be able to automatically transform a query and it's criteria into a GraphQL query +- we should be able to transform an existing GraphQL schema into a montage data one. +- Some of Shopify objects have "methods", for example an Image has transformedSrc, whicj returns a URL and take as argument: + - crop ( CropRegion ) //Crops the image according to the specified region. + - maxHeight ( Int ) //Image height in pixels between 1 and 5760. + - maxWidth ( Int ) //Image width in pixels between 1 and 5760. + - preferredContentType ( ImageContentType ) //Best effort conversion of image into content type (SVG -> PNG, Anything -> JGP, Anything -> WEBP are supported). + - scale ( Int ) //Image size multiplier for high-resolution retina displays. Must be between 1 and 3. Default value: true + + The RemoteProcedureCall operation should be used to model this +- When we receive data back, the shape of the result is related to the shape of the query. One thing we're missing is going from the name of the objectdescriptor to the name used for expressing the query and the same one used for results. +- shopify/graphql-js-client/src/decode.js has an implementation that reads the structure. +- Shopify has 2 levels of API: storefront and admin, and 2 matching different authentication. Some API / properties of one object are only available through one API vs the other. This is going to require some changes? Or should there be 2 different dataServices? + */ + +/* + * + * @extends HttpService + */ +var GraphQLService = exports.GraphQLService = HttpService.specialize(/** @lends GraphQLService.prototype */ { + + deserializeSelf: { + value: function (deserializer) { + var value, result; + // console.log("AirtableService super deserialize #"+deserializeSelfCount); + return this.super(deserializer); + } + } +}); From 07748a74809981d253c699e3af4fe1df3c79a4ca Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 29 Aug 2019 15:05:55 -0700 Subject: [PATCH 022/407] Adds a default data mapping if an object doesn't have an obect desccriptor to provide minimum service of carrying raw data to the final object. --- data/service/raw-data-service.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 1f56b52573..8a0cec167a 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -735,6 +735,10 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot }, "mapSelectorToRawDataSelector", "mapSelectorToRawDataQuery"), }, + _defaultDataMapping : { + value: new DataMapping + }, + /** * Retrieve DataMapping for this object. * @@ -747,11 +751,16 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot mapping = objectDescriptor && this.mappingWithType(objectDescriptor); - if (!mapping && objectDescriptor) { - mapping = this._objectDescriptorMappings.get(objectDescriptor); - if (!mapping) { - mapping = DataMapping.withObjectDescriptor(objectDescriptor); - this._objectDescriptorMappings.set(objectDescriptor, mapping); + if (!mapping) { + if(objectDescriptor) { + mapping = this._objectDescriptorMappings.get(objectDescriptor); + if (!mapping) { + mapping = DataMapping.withObjectDescriptor(objectDescriptor); + this._objectDescriptorMappings.set(objectDescriptor, mapping); + } + } + else { + mapping = this._defaultDataMapping; } } From 163c0a23e5d36925a8fefb7f133e73f4e6e842c6 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 31 Aug 2019 09:43:56 -0700 Subject: [PATCH 023/407] Fixed a typo --- ui/repetition.reel/repetition.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/ui/repetition.reel/repetition.js b/ui/repetition.reel/repetition.js index debc101b2b..46fa713252 100644 --- a/ui/repetition.reel/repetition.js +++ b/ui/repetition.reel/repetition.js @@ -208,6 +208,8 @@ var Iteration = exports.Iteration = Montage.specialize( /** @lends Iteration.pro return this._selected; }, set: function (value) { + var myObject = this.object, + myValue = value; value = !!value; if (this.object && this.repetition && this.repetition.contentController) { if (value) { @@ -216,12 +218,19 @@ var Iteration = exports.Iteration = Montage.specialize( /** @lends Iteration.pro this.repetition.contentController.selection.delete(this.object); } } - if (this._selected !== value) { - this.dispatchBeforeOwnPropertyChange("selected", this._selected); - this._selected = value; - this._updateRepetitionDirtyClassIteration(); - this.dispatchOwnPropertyChange("selected", value); - } + //sketched idea to workaround issue Javier found that causes re-entant change + //in the middle of this and causes unexpected behavior + //Checking that nothing happened in a re-entrant way + //if(this.object === myObject) { + + if (this._selected !== value) { + this.dispatchBeforeOwnPropertyChange("selected", this._selected); + this._selected = value; + this._updateRepetitionDirtyClassIteration(); + this.dispatchOwnPropertyChange("selected", value); + } + //} + } }, @@ -2098,6 +2107,8 @@ var Repetition = exports.Repetition = Component.specialize(/** @lends Repetition } } + //this.dispatchEvent("press"); + this._ignoreSelection(); } }, From 2ed6cac5882cbd5b89d48a61fd3fe17b8d7d23c8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 2 Sep 2019 14:41:09 -0700 Subject: [PATCH 024/407] Change to deal with a package being used on montage script tag, like data-package="/app" --- montage.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/montage.js b/montage.js index 3781a9ca94..11f66ba84f 100644 --- a/montage.js +++ b/montage.js @@ -262,11 +262,17 @@ allModulesLoaded(); }; - var montageLocation; + var montageLocation, appLocation; function loadModuleScript(path, callback) { - montageLocation = montageLocation || resolve(global.location, params.montageLocation); + if(!montageLocation) { + montageLocation = montageLocation || resolve(global.location, params.montageLocation); + appLocation = resolve(global.location, params.package); + if(!appLocation.lastIndexOf("/") !== appLocation.length-1) { + appLocation += "/"; + } + } // try loading script relative to app first (npm 3+) - browser.load(resolve(global.location, path), function (err, script) { + browser.load(resolve(appLocation || global.location, path), function (err, script) { if (err) { // if that fails, the app may have been installed with // npm 2 or with --legacy-bundling, in which case the @@ -963,7 +969,7 @@ }); // Will throw error if there is one - }).done(); + }); }); }; From f121d5caa31eac115dca68d830d6d624cf8818bd Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 3 Sep 2019 11:51:53 -0700 Subject: [PATCH 025/407] Fixes regression when app has no package --- montage.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/montage.js b/montage.js index 11f66ba84f..384b72ebea 100644 --- a/montage.js +++ b/montage.js @@ -266,9 +266,11 @@ function loadModuleScript(path, callback) { if(!montageLocation) { montageLocation = montageLocation || resolve(global.location, params.montageLocation); - appLocation = resolve(global.location, params.package); - if(!appLocation.lastIndexOf("/") !== appLocation.length-1) { - appLocation += "/"; + if(params.package) { + appLocation = resolve(global.location, params.package); + if(!appLocation.lastIndexOf("/") !== appLocation.length-1) { + appLocation += "/"; + } } } // try loading script relative to app first (npm 3+) From e6a6c40bac7ed9017040b63c254311b8d2c27aeb Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 22 Aug 2019 15:22:06 -0700 Subject: [PATCH 026/407] Adds a default implementation for mapRawDataToObject beyond calling deprecated mapFromRawData method --- data/service/raw-data-service.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 8a0cec167a..77c3e9865e 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -12,7 +12,8 @@ var DataService = require("data/service/data-service").DataService, parse = require("frb/parse"), Scope = require("frb/scope"), compile = require("frb/compile-evaluator"), - Promise = require("core/promise").Promise; + Promise = require("core/promise").Promise, + RawDataService; /** * Provides data objects of certain types and manages changes to them based on @@ -34,7 +35,7 @@ var DataService = require("data/service/data-service").DataService, * @class * @extends DataService */ -exports.RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { +exports.RawDataService = RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { /*************************************************************************** * Initializing @@ -786,7 +787,22 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot mapRawDataToObject: { value: function (rawData, object, context) { - return this.mapFromRawData(object, rawData, context); + + //Test for backward compatibility, mapFromRawData is deprecated. + if(this.mapFromRawData !== RawDataService.prototype.mapFromRawData) { + return this.mapFromRawData(object, rawData, context); + } + //New default implementation, just move the data over: + else { + //Loop on object keys: + var aKey; + + for (aKey in object) { + if(rawData.hasOwnProperty(aKey)) { + object[aKey] = rawData[aKey]; + } + } + } } }, /** From 981fdfe0a3d15ff85f43d461dec7957e5a40ee97 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 28 Aug 2019 11:53:34 -0700 Subject: [PATCH 027/407] Revert changes to mapRawDataToObject --- data/service/raw-data-service.js | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 77c3e9865e..8a0cec167a 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -12,8 +12,7 @@ var DataService = require("data/service/data-service").DataService, parse = require("frb/parse"), Scope = require("frb/scope"), compile = require("frb/compile-evaluator"), - Promise = require("core/promise").Promise, - RawDataService; + Promise = require("core/promise").Promise; /** * Provides data objects of certain types and manages changes to them based on @@ -35,7 +34,7 @@ var DataService = require("data/service/data-service").DataService, * @class * @extends DataService */ -exports.RawDataService = RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { +exports.RawDataService = DataService.specialize(/** @lends RawDataService.prototype */ { /*************************************************************************** * Initializing @@ -787,22 +786,7 @@ exports.RawDataService = RawDataService = DataService.specialize(/** @lends RawD mapRawDataToObject: { value: function (rawData, object, context) { - - //Test for backward compatibility, mapFromRawData is deprecated. - if(this.mapFromRawData !== RawDataService.prototype.mapFromRawData) { - return this.mapFromRawData(object, rawData, context); - } - //New default implementation, just move the data over: - else { - //Loop on object keys: - var aKey; - - for (aKey in object) { - if(rawData.hasOwnProperty(aKey)) { - object[aKey] = rawData[aKey]; - } - } - } + return this.mapFromRawData(object, rawData, context); } }, /** From b55b8e71c747e2209b41d1823b6717dbe28e44e0 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 2 Sep 2019 14:41:09 -0700 Subject: [PATCH 028/407] Change to deal with a package being used on montage script tag, like data-package="/app" --- montage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/montage.js b/montage.js index 384b72ebea..f38b4f1f87 100644 --- a/montage.js +++ b/montage.js @@ -266,6 +266,7 @@ function loadModuleScript(path, callback) { if(!montageLocation) { montageLocation = montageLocation || resolve(global.location, params.montageLocation); + if(params.package) { appLocation = resolve(global.location, params.package); if(!appLocation.lastIndexOf("/") !== appLocation.length-1) { From 08abb7cedba0ad1e1f666d7944e564644413f070 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 19 Sep 2019 18:16:55 -0700 Subject: [PATCH 029/407] Avoids creating meaningless queries to resolve empty arrays of foreign ids --- data/converter/raw-foreign-value-to-object-converter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/converter/raw-foreign-value-to-object-converter.js b/data/converter/raw-foreign-value-to-object-converter.js index f1abfa6afd..defbff61f9 100644 --- a/data/converter/raw-foreign-value-to-object-converter.js +++ b/data/converter/raw-foreign-value-to-object-converter.js @@ -24,7 +24,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( convert: { value: function (v) { - if(v) { + if((v && !(v instanceof Array )) || (v instanceof Array && v.length > 0)) { var self = this, criteria = new Criteria().initWithSyntax(self.convertSyntax, v), query; From 90e9e1818c4991ccda21f069043dd2043b1cc249 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 19 Sep 2019 18:18:02 -0700 Subject: [PATCH 030/407] Improve handing of relationship mapping using property descriptor's cardinality --- data/service/expression-data-mapping.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 35d1bf29b8..54b4835107 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -490,7 +490,12 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData data; return rule.evaluate(scope).then(function (result) { - data = result; + if(propertyDescriptor.cardinality === 1 && result instanceof Array && result.length === 1) { + data = result[0]; + } + else { + data = result; + } return hasInverse ? self._assignInversePropertyValue(data, object, propertyDescriptor, rule) : null; }).then(function () { From e00200bc64c50e6f0c4a08ad115f09b7cd8669a3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 19 Sep 2019 18:19:14 -0700 Subject: [PATCH 031/407] Refactors dataIdentifierForTypeRawData creating a new method dataIdentifierForTypePrimaryKey --- data/service/raw-data-service.js | 45 +++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 8a0cec167a..cc299f45fd 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -538,11 +538,11 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot if(rawDataPrimaryKeys && rawDataPrimaryKeys.length) { - dataIdentifierMap = this._typeIdentifierMap.get(type); + // dataIdentifierMap = this._typeIdentifierMap.get(type); - if(!dataIdentifierMap) { - this._typeIdentifierMap.set(type,(dataIdentifierMap = new Map())); - } + // if(!dataIdentifierMap) { + // this._typeIdentifierMap.set(type,(dataIdentifierMap = new Map())); + // } for(var i=0, expression; (expression = rawDataPrimaryKeys[i]); i++) { rawDataPrimaryKeysValues = rawDataPrimaryKeysValues || []; @@ -550,9 +550,41 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } if(rawDataPrimaryKeysValues) { primaryKey = rawDataPrimaryKeysValues.join("/"); - dataIdentifier = dataIdentifierMap.get(primaryKey); + // dataIdentifier = dataIdentifierMap.get(primaryKey); } + return this.dataIdentifierForTypePrimaryKey(type,primaryKey); + + // if(!dataIdentifier) { + // var typeName = type.typeName /*DataDescriptor*/ || type.name; + // //This should be done by ObjectDescriptor/blueprint using primaryProperties + // //and extract the corresponsing values from rawData + // //For now we know here that MileZero objects have an "id" attribute. + // dataIdentifier = new DataIdentifier(); + // dataIdentifier.objectDescriptor = type; + // dataIdentifier.dataService = this; + // dataIdentifier.typeName = type.name; + // dataIdentifier._identifier = dataIdentifier.primaryKey = primaryKey; + + // dataIdentifierMap.set(primaryKey,dataIdentifier); + // } + // return dataIdentifier; + } + return undefined; + } + }, + + dataIdentifierForTypePrimaryKey: { + value: function (type, primaryKey) { + var dataIdentifierMap = this._typeIdentifierMap.get(type), + dataIdentifier; + + if(!dataIdentifierMap) { + this._typeIdentifierMap.set(type,(dataIdentifierMap = new Map())); + } + + dataIdentifier = dataIdentifierMap.get(primaryKey); + if(!dataIdentifier) { var typeName = type.typeName /*DataDescriptor*/ || type.name; //This should be done by ObjectDescriptor/blueprint using primaryProperties @@ -567,9 +599,8 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot dataIdentifierMap.set(primaryKey,dataIdentifier); } return dataIdentifier; - } - return undefined; } + }, __snapshot: { From c54c979baca9cbaa94ea0af902358c696c574ad8 Mon Sep 17 00:00:00 2001 From: Roman Cortes Date: Fri, 27 Sep 2019 23:58:39 +0200 Subject: [PATCH 032/407] Fix inaccurate comment in Repetition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Behaviour didn’t match the comment. --- ui/repetition.reel/repetition.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/repetition.reel/repetition.js b/ui/repetition.reel/repetition.js index 46fa713252..eba1e7c001 100644 --- a/ui/repetition.reel/repetition.js +++ b/ui/repetition.reel/repetition.js @@ -1135,7 +1135,7 @@ var Repetition = exports.Repetition = Component.specialize(/** @lends Repetition // Where we want to be after the next draw: // --- - // The _boundaries array contains comment nodes that serve as the + // The _boundaries array contains empty text nodes that serve as the // top and bottom boundary of each iteration. There will always be // one more boundary than iteration. this._boundaries = []; From 5de74a051f9fbe62f559da78364b6b1509adc43b Mon Sep 17 00:00:00 2001 From: Roman Cortes Date: Sat, 28 Sep 2019 04:24:37 +0200 Subject: [PATCH 033/407] Fix regression of initWithContent in Repetition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repetition’s initWithContent was changed by error to set object instead of content. --- ui/repetition.reel/repetition.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/repetition.reel/repetition.js b/ui/repetition.reel/repetition.js index eba1e7c001..b4c5356870 100644 --- a/ui/repetition.reel/repetition.js +++ b/ui/repetition.reel/repetition.js @@ -647,7 +647,7 @@ var Repetition = exports.Repetition = Component.specialize(/** @lends Repetition */ initWithContent: { value: function (content) { - this.object = content; + this.content = content; return this; } }, From 636afad892ffda576217866e75ea12a98bbcc24e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 10 Oct 2019 17:49:15 -0700 Subject: [PATCH 034/407] Add localizable property to property-descriptor to support the development of localizable data-driven applications --- core/meta/property-descriptor.js | 30 +++++++++++++++------ core/meta/property-descriptor.mjson | 42 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index c22f891889..d27ac5c841 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -112,7 +112,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._setPropertyWithDefaults(serializer, "helpKey", this.helpKey); this._setPropertyWithDefaults(serializer, "definition", this.definition); this._setPropertyWithDefaults(serializer, "inversePropertyName", this.inversePropertyName); - + this._setPropertyWithDefaults(serializer, "localizable", this.localizable); } }, @@ -147,6 +147,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._overridePropertyWithDefaults(deserializer, "helpKey"); this._overridePropertyWithDefaults(deserializer, "definition"); this._overridePropertyWithDefaults(deserializer, "inversePropertyName"); + this._overridePropertyWithDefaults(deserializer, "localizable"); } }, @@ -338,6 +339,9 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# /** * @type {string} + * + * This property specifies the type of collectinon this property should use. + * Default is an Array, but this could be a Set or other type of collection. */ collectionValueType: { value: Defaults["collectionValueType"] @@ -427,16 +431,26 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# value: true }, + /** + * @type {boolean} + * Express the fact that the value of this property might change to meet the language, cultural and other requirements of a specific target market (a locale). + * @default false + */ + localizable: { + value: false + }, + + /** * Property name on the object on the opposite side of the relationship * to which the value of this property should be assigned. - * - * For example, take the following relationship: - * + * + * For example, take the following relationship: + * * Foo.bars <------->> Bar.foo - * - * Each Bar object in Foo.bars will have Foo assigned to it's Bar.foo property. Therefore, - * the inversePropertyName on the 'bars' propertyDescriptor would be 'foo'. + * + * Each Bar object in Foo.bars will have Foo assigned to it's Bar.foo property. Therefore, + * the inversePropertyName on the 'bars' propertyDescriptor would be 'foo'. */ inversePropertyName: { value: undefined @@ -472,6 +486,6 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# blueprint: require("../core")._objectDescriptorDescriptor, - + }); diff --git a/core/meta/property-descriptor.mjson b/core/meta/property-descriptor.mjson index 329b506ca0..f2d69335ce 100644 --- a/core/meta/property-descriptor.mjson +++ b/core/meta/property-descriptor.mjson @@ -229,6 +229,42 @@ } }, + "property_localizable": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "localizable", + "objectDescriptor": { + "@": "root" + }, + "cardinality": 1, + "mandatory": false, + "denyDelete": false, + "readOnly": false, + "valueType": "boolean", + "enumValues": [], + "helpKey": "", + "serializable": true + } + }, + + "property_valueDescriptor": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "valueDescriptor", + "objectDescriptor": { + "@": "root" + }, + "cardinality": 1, + "mandatory": false, + "denyDelete": false, + "readOnly": false, + "valueType": "object", + "enumValues": [], + "helpKey": "", + "serializable": true + } + }, + "root": { "prototype": "core/meta/module-object-descriptor", "values": { @@ -247,6 +283,9 @@ { "@": "property_valueType" }, + { + "@": "property_valueDescriptor" + }, { "@": "property_collectionValueType" }, @@ -286,6 +325,9 @@ { "@": "property_valueType" }, + { + "@": "property_valueDescriptor" + }, { "@": "property_collectionValueType" }, From 8b9eabb34b34c3b2b1cbe8a1c5e0b78e9c8171d6 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 10 Oct 2019 17:57:59 -0700 Subject: [PATCH 035/407] Fix a test regression --- core/meta/property-descriptor.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index d27ac5c841..9f0e1e3b39 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -19,7 +19,8 @@ var Defaults = { valueDescriptor: void 0, enumValues: [], defaultValue: void 0, - helpKey: "" + helpKey: "", + localizable: false }; From 932fc80cbf921a2642c9c6001bc22bd51d7d84c5 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 10 Oct 2019 18:01:03 -0700 Subject: [PATCH 036/407] Improves performance by changing Defaults[*] to Defaults.* when possible --- core/meta/property-descriptor.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index 9f0e1e3b39..3e17934278 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -67,9 +67,9 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# */ initWithNameObjectDescriptorAndCardinality: { value:function (name, objectDescriptor, cardinality) { - this._name = (name !== null ? name : Defaults["name"]); + this._name = (name !== null ? name : Defaults.name); this._owner = objectDescriptor; - this.cardinality = (cardinality > 0 ? cardinality : Defaults["cardinality"]); + this.cardinality = (cardinality > 0 ? cardinality : Defaults.cardinality); return this; } }, @@ -263,7 +263,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * @default 1 */ cardinality: { - value: Defaults["cardinality"] + value: Defaults.cardinality }, /** @@ -271,7 +271,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * @default false */ mandatory: { - value: Defaults["mandatory"] + value: Defaults.mandatory }, /** @@ -279,7 +279,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * @default false */ denyDelete: { - value: Defaults["denyDelete"] + value: Defaults.denyDelete }, /** @@ -287,7 +287,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * @default false */ readOnly: { - value: Defaults["readOnly"] + value: Defaults.readOnly }, /** @@ -335,7 +335,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * this. */ valueType: { - value: Defaults["valueType"] + value: Defaults.valueType }, /** @@ -345,21 +345,21 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * Default is an Array, but this could be a Set or other type of collection. */ collectionValueType: { - value: Defaults["collectionValueType"] + value: Defaults.collectionValueType }, /** * @type {string} */ valueObjectPrototypeName: { - value: Defaults["valueObjectPrototypeName"] + value: Defaults.valueObjectPrototypeName }, /** * @type {string} */ valueObjectModuleId: { - value: Defaults["valueObjectModuleId"] + value: Defaults.valueObjectModuleId }, /** @@ -413,11 +413,11 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# }, defaultValue: { - value: Defaults["defaultValue"] + value: Defaults.defaultValue }, helpKey:{ - value: Defaults["helpKey"] + value: Defaults.helpKey }, objectDescriptorModuleId:require("../core")._objectDescriptorModuleIdDescriptor, From 9b9da308d67dbda96c1622fb52efd10bc6c3415f Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 12 Oct 2019 22:11:22 -0700 Subject: [PATCH 037/407] Add a new init method for leveraging an existing compiled syntax --- core/criteria.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/core/criteria.js b/core/criteria.js index 55c7c069b6..74c138fef3 100644 --- a/core/criteria.js +++ b/core/criteria.js @@ -62,7 +62,7 @@ var Criteria = exports.Criteria = Montage.specialize({ }, /** - * Initialize a Criteria with a compiled syntax. + * Initialize a Criteria with a syntax, the expression parsed. * * @method * @returns {Criteria} - The Criteria initialized. @@ -75,6 +75,21 @@ var Criteria = exports.Criteria = Montage.specialize({ } }, + /** + * Initialize a Criteria with a syntax, the expression parsed. + * + * @method + * @returns {Criteria} - The Criteria initialized. + */ + initWithCompiledSyntax: { + value: function (compiledSyntax, parameters) { + this._compiledSyntax = compiledSyntax; + this.parameters = parameters; + return this; + } + }, + + /** * Initialize a Criteria with an expression as string representation * From 7ad02c5b4c7cc6f36bfb6221ac1532361cbf5b78 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 12 Oct 2019 22:12:18 -0700 Subject: [PATCH 038/407] Update version to 19.0.0 and q-io working dependency --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e5a7fe9085..ff07cdbe6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "montage", - "version": "18.0.0", + "version": "19.0.0", "description": "Build your next application with a browser based platform that really gets the web.", "license": "BSD-3-Clause", "repository": { @@ -52,7 +52,7 @@ "collections": "~5.1.x", "frb": "~4.0.x", "htmlparser2": "~3.0.5", - "q-io": "^1.13.3", + "q-io": "github:marchant/q-io.git#bluebird-q-io", "mr": "montagejs/mr#master", "weak-map": "^1.0.5", "lodash.kebabcase": "^4.1.1", From 83356d6e4fb3e638f7c428140014f79e8ef3eab6 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 12 Oct 2019 22:48:33 -0700 Subject: [PATCH 039/407] Add thought of using DataOperation for transaction --- data/service/data-operation.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 35e10b3431..1c9904da0b 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -299,6 +299,10 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ * "object": "object2-data-identifier" * } * } + * //Transaction. For a Transaction, data would contain the list of data operations grouped together. + * //If data is muted, and observed, it could be dynamically processed by RawDataServices. + * //The referrer property, which is a pointer to another DatOperaiton would be used by an update/addition + * //to the transaction * * * @type {Object} @@ -390,6 +394,31 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ RemoteProcedureCall: {isRemoteProcedureCall: true}, RemoteProcedureCallCompleted: {isRemoteProcedureCall: true}, RemoteProcedureCallFailed: {isRemoteProcedureCall: true} + + /* Batch models the ability to group multiple operation. If a referrer is provided + to a BeginTransaction operation, then the batch will be executed within that transaction */ + /* + Batch: {isBatch: true}, + BatchCompleted: {isBatch: true}, + BatchFailed: {isBatch: true}, + */ + /* A transaction is a unit of work that is performed against a database. + Transactions are units or sequences of work accomplished in a logical order. + A transactions begins, operations are grouped, then it is either commited or rolled-back*/ + /* Start/End Open/Close, Commit/Save, rollback/cancel + BeginTransaction: {isTransaction: true}, + BeginTransactionCompleted: {isTransaction: true}, + BeginTransactionFailed: {isTransaction: true}, + + CommitTransaction: {isTransaction: true}, + CommitTransactionCompleted: {isTransaction: true}, + CommitTransactionFailed: {isTransaction: true}, + + RollbackTransaction: {isTransaction: true}, + RollbackTransactionCompleted: {isTransaction: true}, + RollbackTransactionFailed: {isTransaction: true}, + + */ } } From 8827f0d0da83677aa4987536503aa81937e5e754 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 13 Oct 2019 12:09:52 -0700 Subject: [PATCH 040/407] Update relative dependencies to be naturally compatible with node require and fix a bug requiring deserializer when runnnig in node in processing mjson serialization --- core/serialization/deserializer/montage-deserializer.js | 2 +- data/service/raw-data-service.js | 2 +- montage.js | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/serialization/deserializer/montage-deserializer.js b/core/serialization/deserializer/montage-deserializer.js index c1b5cc7b3d..f290a48457 100644 --- a/core/serialization/deserializer/montage-deserializer.js +++ b/core/serialization/deserializer/montage-deserializer.js @@ -3,7 +3,7 @@ var Montage = require("../../core").Montage, MontageReviver = require("./montage-reviver").MontageReviver, BindingsModule = require("../bindings"), Map = require("collections/map").Map, - Promise = require("core/promise").Promise, + Promise = require("../../promise").Promise, deprecate = require("../../deprecate"); var MontageDeserializer = exports.MontageDeserializer = Montage.specialize({ diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index cc299f45fd..7c89977253 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -12,7 +12,7 @@ var DataService = require("data/service/data-service").DataService, parse = require("frb/parse"), Scope = require("frb/scope"), compile = require("frb/compile-evaluator"), - Promise = require("core/promise").Promise; + Promise = require("../../core/promise").Promise; /** * Provides data objects of certain types and manages changes to them based on diff --git a/montage.js b/montage.js index 384b72ebea..423f558a68 100644 --- a/montage.js +++ b/montage.js @@ -345,7 +345,7 @@ "core/logger" ]; - var Promise = montageRequire("core/promise").Promise; + var Promise = montageRequire("./core/promise").Promise; var deepLoadPromises = []; var self = this; @@ -424,7 +424,8 @@ if(!module.deserializer) { // var root = Require.delegate.compileMJSONFile(module.text, require.config.requireForId(module.id), module, /*isSync*/ true); if(!montageExports.MontageDeserializer) { - montageExports.MontageDeserializer = require("montage/core/serialization/deserializer/montage-deserializer").MontageDeserializer; + var MontageDeserializerModule = montageExports.config.modules["core/serialization/deserializer/montage-deserializer"]; + montageExports.MontageDeserializer = MontageDeserializerModule.require("./core/serialization/deserializer/montage-deserializer").MontageDeserializer; } var deserializer = new montageExports.MontageDeserializer(), @@ -510,6 +511,9 @@ dotMJSONLoadJs = ".mjson.load.js"; exports.Compiler = function (config, compile) { + if(!exports.config && config.name === "montage") { + exports.config = config; + } return function(module) { if (module.exports || module.factory || (typeof module.text !== "string") || (typeof module.exports === "object")) { From cc70bfe4991a9800f4692756e84ce87f4e59cf15 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 23 Oct 2019 01:56:56 -0700 Subject: [PATCH 041/407] Fix a typo creating a bug --- core/drag/drag-manager.js | 126 +++++++++++++++++++------------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/core/drag/drag-manager.js b/core/drag/drag-manager.js index af30800db4..97a5d5ef47 100644 --- a/core/drag/drag-manager.js +++ b/core/drag/drag-manager.js @@ -121,7 +121,7 @@ var DragManager = exports.DragManager = Montage.specialize({ value: function (component) { this._rootComponent = component; var element = this._rootComponent.element; - + if ("webkitTransform" in element.style) { DragManager.cssTransform = "webkitTransform"; } else if ("MozTransform" in element.style) { @@ -243,7 +243,7 @@ var DragManager = exports.DragManager = Montage.specialize({ /** * @private * @function - * @param {Element} draggedImage - node element that will be used + * @param {Element} draggedImage - node element that will be used * as a dragged image * @param {Object} startPosition - coordinates of the start position * @description set some default css style on the dragged image. @@ -264,7 +264,7 @@ var DragManager = exports.DragManager = Montage.specialize({ /** * @private * @function - * @param {Element} draggedImage - node element that will be used + * @param {Element} draggedImage - node element that will be used * as a dragged image * @description set some default css style on the dragged image. */ @@ -338,7 +338,7 @@ var DragManager = exports.DragManager = Montage.specialize({ /** * @private * @function - * @param {DraggingOperationContext} draggingOperationContext - current dragging + * @param {DraggingOperationContext} draggingOperationContext - current dragging * operation info object * @description Dispatch Drag End Event */ @@ -361,7 +361,7 @@ var DragManager = exports.DragManager = Montage.specialize({ /** * @private * @function - * @param {DraggingOperationContext} draggingOperationContext - current dragging + * @param {DraggingOperationContext} draggingOperationContext - current dragging * operation info object * @description Dispatch Drop Event */ @@ -423,7 +423,7 @@ var DragManager = exports.DragManager = Montage.specialize({ /** * @private * @function - * @param {DraggingOperationContext} draggingOperationContext - current dragging + * @param {DraggingOperationContext} draggingOperationContext - current dragging * operation info object * @description Dispatch Drag Leave Event */ @@ -453,7 +453,7 @@ var DragManager = exports.DragManager = Montage.specialize({ value: function (positionX, positionY) { return this._findRegisteredComponentAtPosistion( positionX, - positionY, + positionY, DRAGGABLE ); } @@ -471,7 +471,7 @@ var DragManager = exports.DragManager = Montage.specialize({ value: function (positionX, positionY) { return this._findRegisteredComponentAtPosistion( positionX, - positionY, + positionY, DROPABBLE ); } @@ -494,7 +494,7 @@ var DragManager = exports.DragManager = Montage.specialize({ registeredComponent = null; if (targetComponent) { - var components = role === DRAGGABLE ? + var components = role === DRAGGABLE ? this._draggables : this._droppables, index; @@ -526,7 +526,7 @@ var DragManager = exports.DragManager = Montage.specialize({ component = null; // that is done at several place in the framework - // we should re-organize that + // we should re-organize that while (element && !(component = element.component)) { element = element.parentElement; } @@ -616,8 +616,8 @@ var DragManager = exports.DragManager = Montage.specialize({ capturePointerdown: { value: function (event) { - if (event.pointerType === TOUCH_POINTER || - (window.MSPointerEvent && + if (event.pointerType === TOUCH_POINTER || + (window.MSPointerEvent && event.pointerType === window.MSPointerEvent.MSPOINTER_TYPE_TOUCH) ) { this.captureTouchstart(event); @@ -638,7 +638,7 @@ var DragManager = exports.DragManager = Montage.specialize({ "pointermove", this, true ); } else if ( - window.MSPointerEvent && + window.MSPointerEvent && window.navigator.msPointerEnabled ) { this._rootComponent.element.addEventListener( @@ -688,7 +688,7 @@ var DragManager = exports.DragManager = Montage.specialize({ this._draggingOperationContext = (draggingOperationContext = ( this._createDraggingOperationContextWithDraggableAndPosition( - null, + null, event ) )); @@ -702,12 +702,12 @@ var DragManager = exports.DragManager = Montage.specialize({ draggingOperationContext.dragEffect = ( dragStartEvent.dataTransfer.dragEffect ); - + this._addDragListeners(); draggingOperationContext.isDragging = true; this._rootComponent.needsDraw = true; } - + this._dragEnterCounter++; } }, @@ -745,7 +745,7 @@ var DragManager = exports.DragManager = Montage.specialize({ value: function (event) { event.preventDefault(); event.stopPropagation(); - + this._draggingOperationContext.deltaX = ( event.pageX - this._draggingOperationContext.startPositionX ); @@ -757,7 +757,7 @@ var DragManager = exports.DragManager = Montage.specialize({ this._draggingOperationContext.dataTransfer = ( DataTransfer.fromDataTransfer(event.dataTransfer) ); - + this._removeDragListeners(); this.handleTranslateEnd(); } @@ -786,13 +786,13 @@ var DragManager = exports.DragManager = Montage.specialize({ this._draggingOperationContext = (draggingOperationContext = ( this._createDraggingOperationContextWithDraggableAndPosition( - draggable, + draggable, startPosition ) )); var dragStartEvent = this._dispatchDragStart(); - + this._draggingOperationContext.dataTransfer = dragStartEvent.dataTransfer; this._draggingOperationContext.dragEffect = dragStartEvent.dataTransfer.dragEffect; this._draggingOperationContext.showPlaceholder = ( @@ -839,7 +839,7 @@ var DragManager = exports.DragManager = Montage.specialize({ handleTranslateCancel: { value: function () { - this.draggingOperationContext.currentDropTarget = null; + this._draggingOperationContext.currentDropTarget = null; this._willTerminateDraggingOperation = true; this._rootComponent.needsDraw = true; } @@ -849,7 +849,7 @@ var DragManager = exports.DragManager = Montage.specialize({ * Draw Cycles Management */ - willDraw: { + willDraw: { value: function () { var draggingOperationContext; @@ -859,14 +859,14 @@ var DragManager = exports.DragManager = Montage.specialize({ ) { var draggable = draggingOperationContext.draggable; - if ( - !this._draggedImageBoundingRect && + if ( + !this._draggedImageBoundingRect && draggingOperationContext.draggable ) { this._draggedImageBoundingRect = ( draggingOperationContext.draggable.element.getBoundingClientRect() ); - + if (draggable.draggableContainer) { this._draggableContainerBoundingRect = ( draggable.draggableContainer.getBoundingClientRect() @@ -878,7 +878,7 @@ var DragManager = exports.DragManager = Montage.specialize({ draggingOperationContext.positionY ); - if (droppable && + if (droppable && !draggingOperationContext.dropTargetCandidates.has( droppable ) @@ -890,13 +890,13 @@ var DragManager = exports.DragManager = Montage.specialize({ this._invalidDroppable.classList.remove('invalid-drop-target'); this._invalidDroppable = null; } - + if (draggable) { draggable.dispatchEvent(this._createDragEvent( "drag", draggingOperationContext )); } - + if (droppable !== draggingOperationContext.currentDropTarget) { if (draggingOperationContext.currentDropTarget) { this._dispatchDragLeave( @@ -959,48 +959,48 @@ var DragManager = exports.DragManager = Montage.specialize({ if (draggingOperationContext.positionX - ( deltaPointerLeft = ( - draggingOperationContext.startPositionX - + draggingOperationContext.startPositionX - this._draggedImageBoundingRect.left ) ) < rect.left) { translateX = ( - rect.left - - draggingOperationContext.startPositionX + + rect.left - + draggingOperationContext.startPositionX + deltaPointerLeft ); } else if (draggingOperationContext.positionX + ( deltaPointerRight = ( - this._draggedImageBoundingRect.right - + this._draggedImageBoundingRect.right - draggingOperationContext.startPositionX ) ) > rect.right) { translateX = ( - rect.right - - draggingOperationContext.startPositionX - + rect.right - + draggingOperationContext.startPositionX - deltaPointerRight ); } - + if (draggingOperationContext.positionY - ( deltaPointerTop = ( - draggingOperationContext.startPositionY - + draggingOperationContext.startPositionY - this._draggedImageBoundingRect.top ) ) < rect.top) { translateY = ( - rect.top - - draggingOperationContext.startPositionY + + rect.top - + draggingOperationContext.startPositionY + deltaPointerTop ); } else if (draggingOperationContext.positionY + ( deltaPointerBottom = ( - this._draggedImageBoundingRect.bottom - + this._draggedImageBoundingRect.bottom - draggingOperationContext.startPositionY ) ) > rect.bottom) { translateY = ( - rect.bottom - - draggingOperationContext.startPositionY - + rect.bottom - + draggingOperationContext.startPositionY - deltaPointerBottom ); } @@ -1015,7 +1015,7 @@ var DragManager = exports.DragManager = Montage.specialize({ draggedImage.style[DragManager.cssTransform] = translate; this._scrollIfNeeded( - draggingOperationContext.positionX, + draggingOperationContext.positionX, draggingOperationContext.positionY ); } @@ -1037,7 +1037,7 @@ var DragManager = exports.DragManager = Montage.specialize({ this._dispatchDrop( draggingOperationContext ); - + this._resetTranslateContext(); draggingOperationContext.currentDropTarget = null; this._dispatchDragEnd(draggingOperationContext); @@ -1049,9 +1049,9 @@ var DragManager = exports.DragManager = Montage.specialize({ this._shouldRemovePlaceholder = true; this._rootComponent.needsDraw = true; // Wait for the next draw cycle to remove the placeholder, - // or in order to be synchronised with the draw cyle when + // or in order to be synchronised with the draw cyle when // the draggable component will become visible again. - // Plus it allows the receiver to perform any necessary clean-up. + // Plus it allows the receiver to perform any necessary clean-up. return void 0; } else { this._draggingOperationContext = null; @@ -1107,7 +1107,7 @@ var DragManager = exports.DragManager = Montage.specialize({ if (draggingOperationContext.dragEffect === "move") { var draggableElement = draggingOperationContext.draggable.element; this._oldDraggableDisplayStyle = draggableElement.style.display; - draggableElement.style.display = 'none'; + draggableElement.style.display = 'none'; if (draggingOperationContext.showPlaceholder) { var placeholderElement = document.createElement('div'), @@ -1122,7 +1122,7 @@ var DragManager = exports.DragManager = Montage.specialize({ ); draggableElement.parentNode.insertBefore( - placeholderElement, + placeholderElement, draggableElement ); @@ -1145,8 +1145,8 @@ var DragManager = exports.DragManager = Montage.specialize({ if (draggingOperationContext && draggingOperationContext.draggable) { var draggableElement = draggingOperationContext.draggable.element; - draggableElement.style.display = this._oldDraggableDisplayStyle; - + draggableElement.style.display = this._oldDraggableDisplayStyle; + if (draggingOperationContext.showPlaceholder) { draggableElement.parentNode.removeChild( this._placeholderElement @@ -1163,7 +1163,7 @@ var DragManager = exports.DragManager = Montage.specialize({ containerBoundingRect = this._draggableContainerBoundingRect, scrollThreshold = this._scrollThreshold, stopSearchingX = false, - stopSearchingY = false, + stopSearchingY = false, rect, height, width, top, bottom, right, left, scrollWidth, scrollLeft, scrollHeight, scrollTop, outsideBoundariesCounter, notScrollable; @@ -1172,13 +1172,13 @@ var DragManager = exports.DragManager = Montage.specialize({ rect = element.getBoundingClientRect(); height = rect.height; width = rect.width; - + if ( - (!height || !width) || - ((notScrollable = (scrollHeight = element.scrollHeight) <= height)) || + (!height || !width) || + ((notScrollable = (scrollHeight = element.scrollHeight) <= height)) || (notScrollable && (scrollWidth = element.scrollWidth) <= width) ) { - // if no height or width + // if no height or width // or not scrollable pass to to the next parent. element = element.parentElement; continue; @@ -1193,7 +1193,7 @@ var DragManager = exports.DragManager = Montage.specialize({ // Check for horizontal scroll up if ( - positionY >= top && + positionY >= top && (!containerBoundingRect || positionY >= containerBoundingRect.top) ) { if (positionY <= top + scrollThreshold) { @@ -1202,7 +1202,7 @@ var DragManager = exports.DragManager = Montage.specialize({ // if not already reached the bottom edge if (scrollTop) { element.scrollTop = ( - scrollTop - + scrollTop - this._getScrollMultiplier(positionY - top) ); @@ -1219,7 +1219,7 @@ var DragManager = exports.DragManager = Montage.specialize({ // Check for horizontal scroll down if ( - !stopSearchingY && positionY <= bottom && + !stopSearchingY && positionY <= bottom && (!containerBoundingRect || positionY <= containerBoundingRect.bottom) ) { if (positionY >= bottom - scrollThreshold) { @@ -1228,7 +1228,7 @@ var DragManager = exports.DragManager = Montage.specialize({ // if not already reached the bottom edge if (scrollTop < scrollHeight) { element.scrollTop = ( - scrollTop + + scrollTop + this._getScrollMultiplier(bottom - positionY) ); this._rootComponent.needsDraw = true; @@ -1244,7 +1244,7 @@ var DragManager = exports.DragManager = Montage.specialize({ // Check for vertical scroll left if ( - positionX >= left && + positionX >= left && (!containerBoundingRect || positionX >= containerBoundingRect.left) ) { if (positionX <= left + scrollThreshold) { @@ -1253,7 +1253,7 @@ var DragManager = exports.DragManager = Montage.specialize({ // if not already reached the left edge if (scrollLeft) { element.scrollLeft = ( - scrollLeft - + scrollLeft - this._getScrollMultiplier(positionX - left) ); @@ -1270,7 +1270,7 @@ var DragManager = exports.DragManager = Montage.specialize({ // Check for horizontal scroll right if ( - !stopSearchingX && positionX <= right && + !stopSearchingX && positionX <= right && (!containerBoundingRect || positionX <= containerBoundingRect.right) ) { if (positionX >= right - scrollThreshold) { @@ -1278,9 +1278,9 @@ var DragManager = exports.DragManager = Montage.specialize({ scrollWidth = scrollWidth || element.scrollWidth; // if not already reached the right edge - if (scrollLeft < scrollWidth) { + if (scrollLeft < scrollWidth) { element.scrollLeft = ( - scrollLeft + + scrollLeft + this._getScrollMultiplier(right - positionX) ); this._rootComponent.needsDraw = true; From 5c464b9b8b3e72ec4308ff5f4172e94f50cfb602 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 14:12:42 -0800 Subject: [PATCH 042/407] add CountedSet data structure --- core/counted-set.js | 66 +++++++++++ test/all.js | 1 + test/spec/core/counted-set-spec.js | 171 +++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 core/counted-set.js create mode 100644 test/spec/core/counted-set-spec.js diff --git a/core/counted-set.js b/core/counted-set.js new file mode 100644 index 0000000000..c0d81355d7 --- /dev/null +++ b/core/counted-set.js @@ -0,0 +1,66 @@ +// var Montage = require("./core").Montage; +Set = require("collections/set"); + +/** + * The CountedSet keeps a counter associated with object inserted into it. It keeps track of the number of times objects are inserted and objects are really removed when they've been removed the same number of times. + * + * @class CountedSet + * @classdesc todo + * @extends Montage + */ +var CountedSet = exports.CountedSet = function CountedSet(iterable) { + // var element; + + + // for (element of iterable) { + // this.add(element); + // } + // return this; + + var inst = new Set(iterable); + inst.__proto__ = CountedSet.prototype; + inst._contentCount = new Map(); + return inst; + +} + +CountedSet.prototype = Object.create(Set.prototype); + +CountedSet.prototype._add = Set.prototype.add; +CountedSet.prototype.add = function(value) { + var currentCount = this._contentCount.get(value) || (this.has(value) ? 1 : 0); + if(currentCount === 0) { + this._add(value); + } + this._contentCount.set(value,++currentCount); + return this; + //console.log("CountedSet add: countFor "+value.identifier.primaryKey+" is ",currentCount); +} +CountedSet.prototype._delete = Set.prototype.delete; +CountedSet.prototype.delete = function(value) { + var currentCount = this._contentCount.get(value) || (this.has(value) ? 1 : 0); + if(currentCount) { + this._contentCount.set(value,--currentCount); + if(currentCount === 0) { + if(!this.has(value)) { + console.error("Attempt to delete object that is actually gone, mismatch in add/delete?"); + } + this._contentCount.delete(value); + this._delete(value); + return true; + } + } + return false; + //console.log("CountedSet delete: countFor "+value.identifier.primaryKey+" is ",currentCount); +} + +CountedSet.prototype._clear = Set.prototype.clear; +CountedSet.prototype.clear = function(value) { + this._contentCount.clear(); + this._clear(); +}; + + +CountedSet.prototype.countFor = function(value) { + return this._contentCount.get(value) || 0; +} diff --git a/test/all.js b/test/all.js index aa8281b168..4b076367ae 100644 --- a/test/all.js +++ b/test/all.js @@ -35,6 +35,7 @@ module.exports = require("montage-testing").run(require, [ "spec/core/extras/function", "spec/core/extras/string", "spec/core/set-spec", + "spec/core/counted-set-spec", {name: "spec/core/dom-spec", node: false, karma: false}, {name: "spec/core/extras/url", node: false}, "spec/core/range-controller-spec", diff --git a/test/spec/core/counted-set-spec.js b/test/spec/core/counted-set-spec.js new file mode 100644 index 0000000000..6b8b972c01 --- /dev/null +++ b/test/spec/core/counted-set-spec.js @@ -0,0 +1,171 @@ + +var CountedSet = require("montage/core/counted-set").CountedSet; + +describe("test/core/counted-set-spec", function () { + + function newSet(values) { + return new CountedSet(values); + } + + describe("forEach", function () { + it("the callback should receive value, index, set", function () { + var set = new CountedSet([1, 2, 3]); + var other = new CountedSet([]); + var i = 0; + set.forEach(function (value, key, object) { + expect(key).toBe(++i); + other.add(value); + expect(object).toBe(set); + }); + expect(other.size).toBe(3); + expect(other.union(set).size).toBe(3); + expect(other.intersection(set).size).toBe(3); + expect(other.difference(set).size).toBe(0); + }); + }); + + it("should be initially empty", function () { + expect(new CountedSet().size).toBe(0); + }); + + it("cleared set should be empty", function () { + var set = new CountedSet([1, 2]); + expect(set.size).toBe(2); + set.delete(1); + expect(set.size).toBe(1); + set.clear(); + expect(set.size).toBe(0); + }); + + it("can add and delete an object", function () { + var set = new CountedSet(); + var object = {}; + set.add(object); + expect(set.has(object)).toBe(true); + set.delete(object); + expect(set.size).toBe(0); + expect(set.has(object)).toBe(false); + }); + + it("can deleteAll", function () { + var set = new CountedSet([0]); + expect(set.deleteAll(0)).toBe(1); + expect(set.deleteAll(0)).toBe(0); + }); + + it("can add and delete objects from the same bucket", function () { + var a = {id: 0}, b = {id: 1}; + var set = new CountedSet(); + set.add(a); + expect(set.has(a)).toBe(true); + set.add(b); + expect(set.has(b)).toBe(true); + set.delete(b); + expect(set.has(b)).toBe(false); + expect(set.has(a)).toBe(true); + set.delete(a); + expect(set.has(a)).toBe(false); + }); + + it("can read a deleted object", function () { + var set = new CountedSet(); + var object = {}; + set.add(object); + expect(set.has(object)).toBe(true); + set.add(object); + expect(set.size).toBe(1); + set.delete(object); + expect(set.size).toBe(1); + expect(set.has(object)).toBe(true); + set.add(object); + expect(set.size).toBe(1); + expect(set.has(object)).toBe(true); + set.delete(object); + set.delete(object); + expect(set.size).toBe(0); + + }); + + it("can be changed to an array", function () { + var set = new CountedSet([0]); + expect(set.toArray()).toEqual([0]); + }); + + it("is a reducible", function () { + var set = new CountedSet([1, 1, 1, 2, 2, 2, 1, 2]); + expect(set.size).toBe(2); + expect(set.min()).toBe(1); + expect(set.max()).toBe(2); + expect(set.sum()).toBe(3); + expect(set.average()).toBe(1.5); + expect(set.map(function (n) { + return n + 1; + }).indexOf(3)).not.toBe(-1); + }); + + it("is concatenatable", function () { + var array = new CountedSet([3, 2, 1]).concat([4, 5, 6]).toArray(); + array.sort(); + expect(array).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it("should compute unions", function () { + expect(new CountedSet([1, 2, 3]).union([2, 3, 4]).sorted()).toEqual([1, 2, 3, 4]); + expect(new CountedSet([1, 2, 3]).union([2, 3, 4]).equals([1, 2, 3, 4])).toBe(true); + }); + + it("should compute intersections", function () { + expect(new CountedSet([1, 2, 3]).intersection([2, 3, 4]).sorted()).toEqual([2, 3]); + }); + + it("should compute differences", function () { + expect(new CountedSet([1, 2, 3]).difference([2, 3, 4]).sorted()).toEqual([1]); + }); + + it("should compute symmetric differences", function () { + expect(new CountedSet([1, 2, 3]).symmetricDifference([2, 3, 4]).sorted()).toEqual([1, 4]); + }); + + it("should dispatch range change on clear", function () { + var set = new CountedSet([1, 2, 3]); + var spy = jasmine.createSpy(); + set.addRangeChangeListener(spy); + set.clear(); + expect(spy).toHaveBeenCalledWith([], [1, 2, 3], 0, set, undefined); + }); + + it("should dispatch range change on add", function () { + var set = new CountedSet([1, 3]); + var spy = jasmine.createSpy(); + set.addRangeChangeListener(spy); + set.add(2); + expect(set.toArray()).toEqual([1, 3, 2]); + expect(spy).toHaveBeenCalledWith([2], [], 2, set, undefined); + }); + + it("should dispatch range change on delete", function () { + var set = new CountedSet([1, 2, 3]); + var spy = jasmine.createSpy(); + set.addRangeChangeListener(spy); + set["delete"](2); + expect(set.toArray()).toEqual([1, 3]); + expect(spy).toHaveBeenCalledWith([], [2], 1, set, undefined); + }); + + it("should delete objects only after it's been deleted the same amount of tine it was added", function () { + var set = new CountedSet(), + a = {}; + set.add(a); + expect(set.countFor(a)).toEqual(1); + set.add(a); + expect(set.countFor(a)).toEqual(2); + set.delete(a); + expect(set.countFor(a)).toEqual(1); + expect(set.has(a)).toEqual(true); + set.delete(a); + expect(set.countFor(a)).toEqual(0); + expect(set.has(a)).toEqual(false); + }); + + +}); From c951f242d1f8e542128d76564be2f5fe50c3fe08 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 14:14:09 -0800 Subject: [PATCH 043/407] add comment about need for optimization and slight string concatenation performance improvement --- core/core.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/core.js b/core/core.js index 46a1a92dc8..5c0b29623a 100644 --- a/core/core.js +++ b/core/core.js @@ -968,6 +968,10 @@ Montage.defineProperty(Montage, "equals", { * This method calls the method named with the identifier prefix if it exists. * Example: If the name parameter is "shouldDoSomething" and the caller's identifier is "bob", then * this method will try and call "bobShouldDoSomething" + * + * TODO: Cache!!!! We're unlikely to remove a delegate method dynamically, so we should avoid checking all + * that and just cache the function found, using a weak map, so don't retain delegates. + * * @function Montage#callDelegateMethod * @param {string} name */ @@ -977,7 +981,9 @@ Montage.defineProperty(Montage.prototype, "callDelegateMethod", { if (delegate) { - var delegateFunctionName = this.identifier + name.toCapitalized(); + var delegateFunctionName = this.identifier; + delegateFunctionName += name.toCapitalized(); + if ( typeof this.identifier === "string" && typeof delegate[delegateFunctionName] === FUNCTION From d63117a09072fd7ce3a2c8f4a97b1587d00b16aa Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 14:14:46 -0800 Subject: [PATCH 044/407] Add comment about possible external enum package to adopt --- core/enum.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/enum.js b/core/enum.js index effd8f0311..f0f2e21896 100644 --- a/core/enum.js +++ b/core/enum.js @@ -6,6 +6,12 @@ var Montage = require("./core").Montage, logger = require("./logger").logger("enum"); + /* + Evaluate https://github.com/adrai/enum + + This might have the potential to replace gate / bitfield? + */ + /** * @class Enum * @extends Montage From 9eb875a04d8e5bfc1cf7b10213e7e612018483c8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 14:16:10 -0800 Subject: [PATCH 045/407] Correct comment --- core/target.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/target.js b/core/target.js index ee9bf84ab2..12314f6e83 100644 --- a/core/target.js +++ b/core/target.js @@ -4,7 +4,7 @@ var Montage = require("./core").Montage, /** * A Target is any object that can be a candidate for dispatching and receiving - * events throughout what is typically considered the "component tree" of a + * events distributed troughout a tree. One such tree is the "component tree" of a * Montage application. * * @class Target From 9fe951aa4ee49e2e53a9681bf353d6193f328488 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 14:17:43 -0800 Subject: [PATCH 046/407] Consolidate, cleanup API + backward compatibilty --- core/converter/expression-converter.js | 143 ++++++++++++++++++++++--- 1 file changed, 128 insertions(+), 15 deletions(-) diff --git a/core/converter/expression-converter.js b/core/converter/expression-converter.js index 5a9e1840db..6390df06ae 100644 --- a/core/converter/expression-converter.js +++ b/core/converter/expression-converter.js @@ -5,7 +5,8 @@ var Converter = require("./converter").Converter, parse = require("frb/parse"), Scope = require("frb/scope"), - compile = require("frb/compile-evaluator"); + compile = require("frb/compile-evaluator") + deprecate = require("../deprecate"); /** * @class ExpressionConverter @@ -22,6 +23,47 @@ var Converter = require("./converter").Converter, */ exports.ExpressionConverter = Converter.specialize( /** @lends TrimConverter# */ { + /********************************************************************* + * Initialization + */ + + /** + * @param {string} convertExpression the expression to be used for building a criteria to obtain the object corresponding to the value to convert. + * @return itself + */ + initWithConvertExpression: { + value: function (convertExpression) { + this.convertExpression = convertExpression; + return this; + } + }, + + /********************************************************************* + * Serialization + */ + + serializeSelf: { + value: function (serializer) { + + serializer.setProperty("convertExpression", this.convertExpression); + + serializer.setProperty("revertExpression", this.revertExpression); + + } + }, + deserializeSelf: { + value: function (deserializer) { + var value = deserializer.getProperty("convertExpression"); + if (value) { + this.convertExpression = value; + } + + value = deserializer.getProperty("revertExpression"); + if (value) { + this.revertExpression = value; + } + } + }, _convertExpression: { value: null @@ -38,22 +80,50 @@ exports.ExpressionConverter = Converter.specialize( /** @lends TrimConverter# */ if(value !== this._convertExpression) { this._convertExpression = value; //Reset parsed & compiled version: - this.__compiledConvertExpression = undefined; + this._convertSyntax = undefined; + this._compiledConvertSyntax = undefined; } } }, - __compiledConvertExpression: { + + _convertSyntax: { value: undefined }, - _compiledConvertExpression: { + + /** + * Object created by parsing .convertExpression using frb/grammar.js that will + * be used to initialize the convert query criteria + * @type {Object} + * */ + + convertSyntax: { get: function() { - return this.__compiledConvertExpression || (this.__compiledConvertExpression = compile(parse(this.convertExpression))); + return (this._convertSyntax || + ((this._convertSyntax === undefined) ? (this._convertSyntax = (this.convertExpression ? parse(this.convertExpression) : null)) + : null)); } }, + _compiledConvertSyntax: { + value: undefined + }, + compiledConvertSyntax: { + get: function() { + return this._compiledConvertSyntax || + (this._compiledConvertSyntax === undefined ? this._compiledConvertSyntax = this.convertSyntax ? compile(this.convertSyntax) + : null + : null); + } + }, + _compiledConvertExpression: { + + get: deprecate.deprecateMethod(void 0, function () { + return this.compiledConvertSyntax; + }, "_compiledConvertExpression", "compiledConvertSyntax", true) + }, _revertExpression: { - value: "service.dataIdentifierForObject($).primaryKey" + value: null }, /** * The expression used to revert a value. @@ -68,38 +138,81 @@ exports.ExpressionConverter = Converter.specialize( /** @lends TrimConverter# */ this._revertExpression = value; //Reset parswd & compiled version: this._revertSyntax = undefined; - this.__compiledRevertExpression = undefined; + this._compiledRevertSyntax = undefined; } } }, - __compiledRevertExpression: { + _revertSyntax: { value: undefined }, - _compiledRevertExpression: { + + /** + * Object created by parsing .revertExpression using frb/grammar.js that will + * be used to revert the modeled value into a raw one + * @type {Object} + * */ + revertSyntax: { get: function() { - return this.__compiledRevertExpression || (this.__compiledRevertExpression = compile(parse(this.revertExpression))); + return this._revertSyntax || + (this._revertSyntax === undefined ? this._revertSyntax = this.revertExpression ? parse(this.revertExpression) + : null + : null); } }, + + _compiledRevertSyntax: { + value: undefined + }, + + compiledRevertSyntax: { + get: function() { + return this._compiledRevertSyntax || + (this._compiledRevertSyntax === undefined ? this._compiledRevertSyntax = this.revertSyntax ? compile(this.revertSyntax) + : null + : null); + } + }, + + _compiledRevertExpression: { + + get: deprecate.deprecateMethod(void 0, function () { + return this.compiledRevertSyntax; + }, "_compiledRevertExpression", "compiledRevertSyntax", true) + }, + __scope: { value: null }, - _scope: { + + /** + * Scope with which convert and revert expressions are evaluated. + * @type {?Scope} + **/ + scope: { get: function() { return this.__scope || (this.__scope = new Scope()); + }, + set: function(value) { + this.__scope = value; } }, + _scope: { + get: deprecate.deprecateMethod(void 0, function () { + return this.scope; + }, "_scope", "scope", true) + }, convert: { value: function (v) { - this._scope.value = v; - return this._compiledConvertExpression(this._scope); + this.scope.value = v; + return this.compiledConvertSyntax(this.scope); } }, revert: { value: function (v) { - this._scope.value = v; - return this._compiledRevertExpression(this._scope); + this.scope.value = v; + return this.compiledRevertSyntax(this.scope); } } From 622a55cb7a6939a42e32511053cc90ec3bc4f32c Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 14:18:42 -0800 Subject: [PATCH 047/407] Suggest method rename --- core/meta/object-descriptor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/meta/object-descriptor.js b/core/meta/object-descriptor.js index 5b6a927b61..7c18b5130e 100644 --- a/core/meta/object-descriptor.js +++ b/core/meta/object-descriptor.js @@ -635,6 +635,7 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.specialize( /** @lends }, /** + * FIXME: Should probably be named propertyDescriptorNamed or propertyDescriptorWithName * @function * @param {string} name * @returns {PropertyDescriptor} From f02229d11b8c21a90c329bfe37eb6a9228c9cff4 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 14:19:21 -0800 Subject: [PATCH 048/407] First draft of a change Event, work in progress --- core/event/change-event.js | 128 +++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 core/event/change-event.js diff --git a/core/event/change-event.js b/core/event/change-event.js new file mode 100644 index 0000000000..3547fc352e --- /dev/null +++ b/core/event/change-event.js @@ -0,0 +1,128 @@ +var Montage = require("core/core").Montage, + RangeChange = require("./range-change").RangeChange; + +/** + * Models changes happening to an Object's properties/keys as well as changes to data structures such as Arrays, + * Set or Map. It synthesizes multiple aspects of Collection's listen features under one event object + * and one familiar pattern: events. + * + * The addEventListener's listener's option object should be used to limit the scope of what properties/keys are listened to, in order to limit the overhead in setting up the raw mechanics of observing and dispatchnng changes. + * + * @class + * @extends Montage + */ + + //MutableEvent is restricted to use on the client only... + +var ChangeEvent = exports.ChangeEvent = Montage.specialize({ + constructor: { + value: function ChangeEvent() { + this.timestamp = Date.now(); + return this; + } + }, + + type: { + value: "change" + }, + cancelable: { + value: false + }, + target: { + value: "change" + }, + _currentTarget: { + value: void 0 + }, + /** + * @type {Property} + * @default {Targer} null + */ + currentTarget: { + get: function () { + return this._currentTarget; + }, + set: function (value) { + this._currentTarget = value; + } + }, + + bubbles: { + value: true + }, + + /** + * The key/property whose value may have changed + * + * @type {String} + * @default null + */ + key: { + value: undefined + }, + + /** + * The value of property before it was changed + * + * @type {Object} + * @default null + */ + previousKeyValue: { + value: undefined + }, + + /** + * The current value of the property + * + * @type {Object} + * @default null + */ + keyValue: { + value: undefined + }, + + /** + * Alternative: group all range changes under 1 typed RangeChange object that + * combines index, added and removed values + * + * @type {RangeChange} + * @default null + */ + rangeChange: { + value: undefined + }, + + /** + * For an ordered property with a cardinality > 1, + * the index where the change happens + * + * @type {Object} + * @default null + */ + index: { + value: undefined + }, + + /** + * For an ordered property with a cardinality > 1, + * the values added to the range starting at rangeIndex + * + * @type {Object} + * @default null + */ + addedValues: { + value: undefined + }, + + /** + * For an ordered property with a cardinality > 1, + * the values removed from the range starting at rangeIndex + * + * @type {Object} + * @default null + */ + removedValues: { + value: undefined + } + +}); From f752c80afd425c7a0b55f48a617e9992cca839da Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 14:40:40 -0800 Subject: [PATCH 049/407] Todo comments --- core/meta/property-descriptor.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index 3e17934278..d0cfbc479c 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -3,8 +3,7 @@ var Montage = require("../core").Montage, deprecate = require("../deprecate"), logger = require("../logger").logger("objectDescriptor"); -// TODO change Defaults[*] to Defaults.* throughout. Needless performance -// degradations. +// TODO: Replace Defaults by leveraging the value set on the prototype which really is the natural default var Defaults = { name: "default", cardinality: 1, @@ -24,6 +23,31 @@ var Defaults = { }; +/* +TODO: + +- if a propertyDescriptor's serializable isn't false, it should be persisted, + however propertyDescriptors with a definition expression, should not. + +- "valueType": "array" is not enough to persist a property in a modern relational DB, + we need the type in the array + Cardinality should be > 1, and also why isn't collectionValueType used as well? + +- Number types need more data in term of storage: signed or not, decimal / integer / precision / scale? + Or is it a mapping issue?. Look at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation + + Intl.NumberFormat: + +- Also, we need to model for a toMany, wether it's unique, and if it is ordered. In most classic + to-many, order is irrelevant, but it sometimes can be. + Unique and ordered makes only sense when cardinality is > 1, or not? +- Unique can also means, like in a database, that the value of that property should be unique throughout a table. + How do we express this as well, which would be helpful to know to set constraints appropriately. + +*/ + + /* TypeDescriptor */ /* DeleteRules */ From 4e4d4e171442c68763c7b9c21264177a782bbaf4 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 14:43:19 -0800 Subject: [PATCH 050/407] Remove copy of values on an internal object. It could behave differently if external code changes it but is it really worth the cost? Passes tests. Leaving comments to attract attention for a while. --- .../deserializer/montage-interpreter.js | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/core/serialization/deserializer/montage-interpreter.js b/core/serialization/deserializer/montage-interpreter.js index 219f066d38..b057a07c46 100644 --- a/core/serialization/deserializer/montage-interpreter.js +++ b/core/serialization/deserializer/montage-interpreter.js @@ -106,13 +106,21 @@ var MontageContext = Montage.specialize({ this._objects = Object.create(null); if (objects) { - this._userObjects = Object.create(null); - - /* jshint forin: true */ - for (var label in objects) { - /* jshint forin: false */ - this._userObjects[label] = objects[label]; - } + this._userObjects = objects; + /* + #PERF + Benoit Performance Improvement: + this._userObjects is used for lookup, it's never changed, + therefore there's no reason to create a new object, + and loop over it to copy the data over, just use it. + */ + // this._userObjects = Object.create(null); + + // /* jshint forin: true */ + // for (var label in objects) { + // /* jshint forin: false */ + // this._userObjects[label] = objects[label]; + // } } this._element = element; From f603e1cbb9b05356f049e9777461575dea5bf2d1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 14:47:19 -0800 Subject: [PATCH 051/407] Fix bug with relative moduleId and performance in convert, add a revert implementation --- .../raw-foreign-value-to-object-converter.js | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/data/converter/raw-foreign-value-to-object-converter.js b/data/converter/raw-foreign-value-to-object-converter.js index defbff61f9..adee66b5d0 100644 --- a/data/converter/raw-foreign-value-to-object-converter.js +++ b/data/converter/raw-foreign-value-to-object-converter.js @@ -1,6 +1,6 @@ var RawValueToObjectConverter = require("./raw-value-to-object-converter").RawValueToObjectConverter, Criteria = require("core/criteria").Criteria, - DataQuery = require("data/model/data-query").DataQuery, + DataQuery = require("data/model/data-query").DataQuery, Promise = require("core/promise").Promise; /** * @class RawForeignValueToObjectConverter @@ -30,16 +30,12 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( query; return this._descriptorToFetch.then(function (typeToFetch) { - var type = typeToFetch.module.id; - - type += "/"; - type += typeToFetch.name; if (self.serviceIdentifier) { criteria.parameters.serviceIdentifier = self.serviceIdentifier; } - query = DataQuery.withTypeAndCriteria(type, criteria); + query = DataQuery.withTypeAndCriteria(typeToFetch, criteria); return self.service ? self.service.then(function (service) { return service.rootService.fetchData(query); @@ -63,12 +59,28 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( revert: { value: function (v) { if (v) { + //No specific instruction, so we return the primary keys using default assumptions. if (!this.compiledRevertSyntax) { - return Promise.resolve(v); + return this.service ? this.service.then(function (service) { + + if(v instanceof Array) { + var result=[]; + //forEach skipps over holes of a sparse array + v.forEach(function(value) { + result.push(service.dataIdentifierForObject(value).primaryKey); + }); + return result; + } + else { + return service.dataIdentifierForObject(v).primaryKey; + } + + }) : Promise.resolve(v); } else { var scope = this.scope; //Parameter is what is accessed as $ in expressions - scope.value = v; + scope.parameters = v; + scope.value = this; return Promise.resolve(this.compiledRevertSyntax(scope)); } From 0aa5a96a71e3e8af7eb390c807d1aa586df9a718 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 14:48:41 -0800 Subject: [PATCH 052/407] Make RawValueToObjectConverter specialize ExpressionConverter as it should --- .../raw-value-to-object-converter.js | 264 +++++++++--------- 1 file changed, 135 insertions(+), 129 deletions(-) diff --git a/data/converter/raw-value-to-object-converter.js b/data/converter/raw-value-to-object-converter.js index e0c893d676..0ed5042264 100644 --- a/data/converter/raw-value-to-object-converter.js +++ b/data/converter/raw-value-to-object-converter.js @@ -1,4 +1,5 @@ -var Converter = require("core/converter/converter").Converter, +//var Converter = require("core/converter/converter").Converter, +var ExpressionConverter = require("core/converter/expression-converter").ExpressionConverter, ObjectDescriptorReference = require("core/meta/object-descriptor-reference").ObjectDescriptorReference, Promise = require("core/promise").Promise, Scope = require("frb/scope"), @@ -10,7 +11,7 @@ var Converter = require("core/converter/converter").Converter, * @classdesc Converts a property value of raw data to the referenced object. * @extends Converter */ -exports.RawValueToObjectConverter = Converter.specialize( /** @lends RawValueToObjectConverter# */ { +exports.RawValueToObjectConverter = ExpressionConverter.specialize( /** @lends RawValueToObjectConverter# */ { /* converter.expression = converter.expression || rule.expression; @@ -28,11 +29,13 @@ exports.RawValueToObjectConverter = Converter.specialize( /** @lends RawValueToO serializeSelf: { value: function (serializer) { - serializer.setProperty("convertExpression", this.convertExpression); + this.super(serializer); + + // serializer.setProperty("convertExpression", this.convertExpression); serializer.setProperty("foreignDescriptor", this._foreignDescriptorReference); - serializer.setProperty("revertExpression", this.revertExpression); + // serializer.setProperty("revertExpression", this.revertExpression); serializer.setProperty("root", this.owner); @@ -44,15 +47,18 @@ exports.RawValueToObjectConverter = Converter.specialize( /** @lends RawValueToO deserializeSelf: { value: function (deserializer) { - var value = deserializer.getProperty("convertExpression"); - if (value) { - this.convertExpression = value; - } - value = deserializer.getProperty("revertExpression"); - if (value) { - this.revertExpression = value; - } + this.super(deserializer); + + // var value = deserializer.getProperty("convertExpression"); + // if (value) { + // this.convertExpression = value; + // } + + // value = deserializer.getProperty("revertExpression"); + // if (value) { + // this.revertExpression = value; + // } value = deserializer.getProperty("foreignDescriptor"); if (value instanceof ObjectDescriptorReference) { @@ -85,116 +91,116 @@ exports.RawValueToObjectConverter = Converter.specialize( /** @lends RawValueToO } }, - /********************************************************************* - * Initialization - */ - - /** - * @param {string} convertExpression the expression to be used for building a criteria to obtain the object corresponding to the value to convert. - * @return itself - */ - initWithConvertExpression: { - value: function (convertExpression) { - this.convertExpression = convertExpression; - return this; - } - }, + // /********************************************************************* + // * Initialization + // */ + + // /** + // * @param {string} convertExpression the expression to be used for building a criteria to obtain the object corresponding to the value to convert. + // * @return itself + // */ + // initWithConvertExpression: { + // value: function (convertExpression) { + // this.convertExpression = convertExpression; + // return this; + // } + // }, /********************************************************************* * Properties */ - _convertExpression: { - value: null - }, - - /** - * The expression used to convert a raw value into a modeled one, for example a foreign property value into the objet it represents. - * @type {string} - * */ - convertExpression: { - get: function() { - return this._convertExpression; - }, - set: function(value) { - if(value !== this._convertExpression) { - this._convertExpression = value; - this._convertSyntax = undefined; - } - } - }, - - _convertSyntax: { - value: undefined - }, - - /** - * Object created by parsing .convertExpression using frb/grammar.js that will - * be used to initialize the convert query criteria - * @type {Object} - * */ - - convertSyntax: { - get: function() { - return (this._convertSyntax || - ((this._convertSyntax === undefined) ? (this._convertSyntax = (this.convertExpression ? parse(this.convertExpression) : null)) - : null)); - } - }, - - _revertExpression: { - value: null - }, - - /** - * The expression used to revert the modeled value into a raw one. For example, - * reverting an object into it's primary key. - * @type {string} - * */ - revertExpression: { - get: function() { - return this._revertExpression; - }, - set: function(value) { - if(value !== this._revertExpression) { - this._revertExpression = value; - this._revertSyntax = undefined; - } - } - }, - - _revertSyntax: { - value: undefined - }, - - /** - * Object created by parsing .revertExpression using frb/grammar.js that will - * be used to revert the modeled value into a raw one - * @type {Object} - * */ - revertSyntax: { - get: function() { - return this._revertSyntax || - (this._revertSyntax === undefined ? this._revertSyntax = this.revertExpression ? parse(this.revertExpression) - : null - : null); - } - }, - - _compiledRevertSyntax: { - value: undefined - }, - - compiledRevertSyntax: { - get: function () { - - return this._compiledRevertSyntax || - (this._compiledRevertSyntax === undefined ? this._compiledRevertSyntax = this.revertSyntax ? compile(this.revertSyntax) - : null - : null); - } - }, + // _convertExpression: { + // value: null + // }, + + // /** + // * The expression used to convert a raw value into a modeled one, for example a foreign property value into the objet it represents. + // * @type {string} + // * */ + // convertExpression: { + // get: function() { + // return this._convertExpression; + // }, + // set: function(value) { + // if(value !== this._convertExpression) { + // this._convertExpression = value; + // this._convertSyntax = undefined; + // } + // } + // }, + + // _convertSyntax: { + // value: undefined + // }, + + // /** + // * Object created by parsing .convertExpression using frb/grammar.js that will + // * be used to initialize the convert query criteria + // * @type {Object} + // * */ + + // convertSyntax: { + // get: function() { + // return (this._convertSyntax || + // ((this._convertSyntax === undefined) ? (this._convertSyntax = (this.convertExpression ? parse(this.convertExpression) : null)) + // : null)); + // } + // }, + + // _revertExpression: { + // value: null + // }, + + // /** + // * The expression used to revert the modeled value into a raw one. For example, + // * reverting an object into it's primary key. + // * @type {string} + // * */ + // revertExpression: { + // get: function() { + // return this._revertExpression; + // }, + // set: function(value) { + // if(value !== this._revertExpression) { + // this._revertExpression = value; + // this._revertSyntax = undefined; + // } + // } + // }, + + // _revertSyntax: { + // value: undefined + // }, + + // /** + // * Object created by parsing .revertExpression using frb/grammar.js that will + // * be used to revert the modeled value into a raw one + // * @type {Object} + // * */ + // revertSyntax: { + // get: function() { + // return this._revertSyntax || + // (this._revertSyntax === undefined ? this._revertSyntax = this.revertExpression ? parse(this.revertExpression) + // : null + // : null); + // } + // }, + + // _compiledRevertSyntax: { + // value: undefined + // }, + + // compiledRevertSyntax: { + // get: function () { + + // return this._compiledRevertSyntax || + // (this._compiledRevertSyntax === undefined ? this._compiledRevertSyntax = this.revertSyntax ? compile(this.revertSyntax) + // : null + // : null); + // } + // }, /** @@ -287,19 +293,19 @@ exports.RawValueToObjectConverter = Converter.specialize( /** @lends RawValueToO } }, - __scope: { - value: null - }, - - /** - * Scope with which convert and revert expressions are evaluated. - * @type {?Scope} - **/ - scope: { - get: function() { - return this.__scope || (this.__scope = new Scope(this)); - } - }, + // __scope: { + // value: null + // }, + + // /** + // * Scope with which convert and revert expressions are evaluated. + // * @type {?Scope} + // **/ + // scope: { + // get: function() { + // return this.__scope || (this.__scope = new Scope(this)); + // } + // }, /** * The service to use to make requests. From 53f380cb68a8d165519e2956fc2e190f71f132f4 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 14:49:50 -0800 Subject: [PATCH 053/407] add properties to support batching data --- data/model/data-query.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/data/model/data-query.js b/data/model/data-query.js index 997ed5bcd2..46c3114d69 100644 --- a/data/model/data-query.js +++ b/data/model/data-query.js @@ -153,7 +153,7 @@ exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { * * For example, if one would want the number of objects fetched, one would do: * aDataQuery.selectBindings = { - * "count": {"<-": "data.length" + * "count": {"<-": "data.length"} * }; * * aDataQuery.selectBindings = { @@ -221,8 +221,29 @@ exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { prefetchExpressions: { value: null + }, + + /** + * A property defining the number of objets to retrieve at once from the result set. + * @type {Number} + */ + + batchSize: { + value: null + }, + + _doesBatchResults: { + value: null + }, + + doesBatchResult: { + get: function() { + return this._doesBatchResults || (typeof this.batchSize === "number"); + } } + + }, /** @lends DataQuery */ { /** From 6a303f8f15dad6dbc4b7ebec81c9e21d9c8a80cb Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 15:01:46 -0800 Subject: [PATCH 054/407] - make _objectDescriptorForType public - add way to know when object is being mapped - add early implementation of tracking changes - add support for fetching in batches --- data/service/data-service.js | 348 +++++++++++++++++++++++++++++-- data/service/data-stream.js | 122 ++++++++++- data/service/data-trigger.js | 132 ++++++++++-- data/service/raw-data-service.js | 258 ++++++++++++++++++++++- 4 files changed, 825 insertions(+), 35 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 992379b932..1285fdc905 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -9,6 +9,7 @@ var Montage = require("core/core").Montage, Promise = require("core/promise").Promise, ObjectDescriptor = require("core/meta/object-descriptor").ObjectDescriptor, Set = require("collections/set"), + CountedSet = require("core/counted-set").CountedSet, WeakMap = require("collections/weak-map"); @@ -534,7 +535,15 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { var prototype = Object.create(constructor.prototype), mapping = childService.mappingWithType(objectDescriptor), requisitePropertyNames = mapping && mapping.requisitePropertyNames || new Set(), - dataTriggers = DataTrigger.addTriggers(this, objectDescriptor, prototype, requisitePropertyNames); + dataTriggers = DataTrigger.addTriggers(this, objectDescriptor, prototype, requisitePropertyNames), + mainService = this.rootService; + + Object.defineProperty(prototype,"identifier", { + enumerable: true, + get: function() { + return mainService.dataIdentifierForObject(this); + } + }); this._dataObjectPrototypes.set(constructor, prototype); this._dataObjectPrototypes.set(objectDescriptor, prototype); @@ -564,7 +573,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - _objectDescriptorForType: { + objectDescriptorForType: { value: function (type) { var descriptor = this._constructorToObjectDescriptorMap.get(type) || typeof type === "string" && this._moduleIdToObjectDescriptorMap[type]; @@ -573,6 +582,12 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, + _objectDescriptorForType: { + value: function (type) { + return this.objectDescriptorForType(type); + } + }, + _constructorToObjectDescriptorMap: { value: new Map() }, @@ -747,7 +762,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { childServiceForType: { value: function (type) { var services; - type = type instanceof ObjectDescriptor ? type : this._objectDescriptorForType(type); + type = type instanceof ObjectDescriptor ? type : this.objectDescriptorForType(type); services = this._childServicesByType.get(type) || this._childServicesByType.get(null); return services && services[0] || null; } @@ -780,7 +795,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { mappingWithType: { value: function (type) { var mapping; - type = this._objectDescriptorForType(type); + type = this.objectDescriptorForType(type); mapping = this._mappingByType.has(type) && this._mappingByType.get(type); return mapping || null; } @@ -1042,7 +1057,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { _getPrototypeForType: { value: function (type) { var info, triggers, prototypeToExtend, prototype; - type = this._objectDescriptorForType(type); + type = this.objectDescriptorForType(type); prototype = this._dataObjectPrototypes.get(type); if (type && !prototype) { //type.objectPrototype is legacy and should be depreated over time @@ -1396,6 +1411,13 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, + /* TODO: Remove when mapping is moved in the webworker. This is used right now to know + when an object is bieng mapped so we can avoid tracking changes happening during that time. This issue will disappear when mapping is done in a web worker and not on the object directly. + */ + _objectsBeingMapped: { + value: new CountedSet() + }, + _fetchObjectPropertyWithPropertyDescriptor: { value: function (object, propertyName, propertyDescriptor) { var self = this, @@ -1420,11 +1442,31 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { All of this said, I don't know this is the right way to solve the problem. The issue at the moment is that this functionality is being used so we cannot remove it without an alternative. */ Object.assign(data, this.snapshotForObject(object)); + + self._objectsBeingMapped.add(object); + result = mapping.mapObjectToCriteriaSourceForProperty(object, data, propertyName); if (this._isAsync(result)) { return result.then(function() { Object.assign(data, self.snapshotForObject(object)); - return mapping.mapRawDataToObjectProperty(data, object, propertyName); + result = mapping.mapRawDataToObjectProperty(data, object, propertyName); + + if (!self._isAsync(result)) { + self._objectsBeingMapped.delete(object); + } + else { + result = result.then(function(resolved) { + + self._objectsBeingMapped.delete(object); + return resolved; + }, function(failed) { + self._objectsBeingMapped.delete(object); + }); + } + return result; + }, function(error) { + self._objectsBeingMapped.delete(object); + throw error; }); } else { //This was already done a few lines up. Why are we re-doing this? @@ -1432,6 +1474,15 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { result = mapping.mapRawDataToObjectProperty(data, object, propertyName); if (!this._isAsync(result)) { result = this.nullPromise; + this._objectsBeingMapped.delete(object); + } + else { + result = result.then(function(resolved) { + self._objectsBeingMapped.delete(object); + return resolved; + }, function(failed) { + self._objectsBeingMapped.delete(object); + }); } return result; } @@ -1593,6 +1644,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ removeDataIdentifierForObject: { value: function(object) { + console.log("removeDataIdentifierForObject(",object); this._dataIdentifierByObject.delete(object); } }, @@ -1679,7 +1731,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ _createDataObject: { value: function (type, dataIdentifier) { - var objectDescriptor = this._objectDescriptorForType(type), + var objectDescriptor = this.objectDescriptorForType(type), object = Object.create(this._getPrototypeForType(objectDescriptor)); if (object) { @@ -1713,6 +1765,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ registerUniqueObjectWithDataIdentifier: { value: function(object, dataIdentifier) { + //Benoit, this is currently relying on a manual turn-on of isUniquing on the MainService, which is really not something people should have to worry about... if (object && dataIdentifier && this.isRootService && this.isUniquing) { this.recordDataIdentifierForObject(dataIdentifier, object); this.recordObjectForDataIdentifier(object, dataIdentifier); @@ -1751,7 +1804,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { }, /** - * A set of the data objects managed by this service or any other descendent + * A Map of the data objects managed by this service or any other descendent * of this service's [root service]{@link DataService#rootService} that have * been changed since that root service's data was last saved, or since the * root service was created if that service's data hasn't been saved yet @@ -1760,16 +1813,18 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { * whose instances will not be root services should override this property * to return their root service's value for it. * - * @type {Set.} + * The key are the objects, the value is a Set containing the property changed + * + * @type {Map.} */ changedDataObjects: { get: function () { if (this.isRootService) { - this._changedDataObjects = this._changedDataObjects || new Set(); + this._changedDataObjects = this._changedDataObjects || new Map(); return this._changedDataObjects; } else { - return this.rootService.changedDataObjects(); + return this.rootService.changedDataObjects; } } }, @@ -1778,6 +1833,132 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { value: undefined }, + /** + * A Map containing the changes for an object. Keys are the property modified, + * values are either a single value, or a map with added/removed keys for properties + * that have a cardinality superior to 1. The underlyinng collection doesn't matter + * at that level. + * + * Retuns undefined if no changes have been registered. + * + * @type {Map.} + */ + + changesForDataObject: { + value: function (dataObject) { + return this.changedDataObjects.get(dataObject); + } + }, + + registerDataObjectChangesFromEvent: { + value: function (changeEvent) { + + var dataObject = changeEvent.target, + key = changeEvent.key, + keyValue = changeEvent.keyValue, + addedValues = changeEvent.addedValues, + removedValues = changeEvent.addedValues, + changesForDataObject = this.changedDataObjects.get(dataObject); + + if(!changesForDataObject) { + changesForDataObject = new Map(); + this._changedDataObjects.set(dataObject,changesForDataObject); + } + + /* + While a single change Event should be able to model both a range change + equivalent of minus/plus and a related length property change at + the same time, a changeEvent from the perspective of tracking data changes + doesn't really care about length, or the array itself. The key of the changeEvent will be one of the target's and the added/removedValues would be from that property's array if it's one. + + Which means that data objects setters should keep track of an array + changing on the object itself, as well as mutation done to the array itself while modeling that object's relatioonship. + + Client side we're going to have partial views of a whole relationship + as we may not want to fetch everything at once if it's big. Which means + that even if we can track add / removes to a property's array, what we + may consider as an add / remove client side, may be a no-op while it reaches the server, and we may want to be able to tell the client about that specific fact. + + + */ + if(keyValue) { + changesForDataObject.set(key,keyValue); + } + else if(addedValues || removedValues) { + //For key that can have add/remove the value of they key is an object + //that itself has two keys: addedValues and removedValues + //which value will be a set; + var manyChanges = changesForDataObject.get(key), + i, countI; + + if(!manyChanges) { + manyChanges = {}; + changesForDataObject.set(key,manyChanges); + } + + //Not sure if we should consider evaluating added values regarded + //removed ones, one could be added and later removed. + //We later need to convert these into dataIdentifers, we could avoid a loop later + //doing so right here. + if(addedValues) { + var registeredAddedValues = manyChanges.addedValues; + if(!registeredAddedValues) { + manyChanges.addedValues = (registeredAddedValues = new Set(addedValues)); + } else { + for(i=0, countI=addedValues.length;i} + */ + deletedDataObjects: { + get: function () { + if (this.isRootService) { + this._deletedDataObjects = this._deletedDataObjects || new Set(); + return this._deletedDataObjects; + } + else { + return this.rootService.deletedDataObjects(); + } + } + }, + + _deletedDataObjects: { + value: undefined + }, + + /*************************************************************************** * Fetching Data */ @@ -1843,7 +2024,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { stream = optionalCriteria instanceof DataStream ? optionalCriteria : optionalStream; // make sure type is an object descriptor or a data object descriptor. - query.type = this._objectDescriptorForType(query.type); + query.type = this.objectDescriptorForType(query.type); // Set up the stream. stream = stream || new DataStream(); @@ -1915,6 +2096,85 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, + + /* + ObjectDescriptors are what should dispatch events, as well as model objec intances + So an imperative method on DataService would internally create the operation/event and dispatch it on the object descriptor. It would internally addEventListener for the "symetric event", for a Read, it would be a ReadUpdated, for a ReadUpdate/ a ReadUpdated as well. + + This would help re-implementing fetchData to be backward compatible. + + What is clear is that we need for a read for example, a stable array that can be relied on by bindings and be mutated over time. + + If we have anObjecDescriptor.dispatchEvent("read"), then someone whose job it to act on that read is an EventListener for it, and that's the RawDataServices running in the DataServiceWorker. This is where we need an intermediary whose job is to send these events over postMesssage so they get dispatched in the DataServiceWorker. + + So as a developer what am I writing if I'm not doing + + mainService.fetchData(query); //? + + mainService.readData(query); //? + mainService.saveData(query); //? + + mainService.performOperation(readOperation); + + + */ + + readData: { + value: function (dataOperation, optionalStream) { + var self = this, + objectDescriptor = dataOperation.objectDescriptor || dataOperation.dataType, + stream = optionalStream, + dataService, dataServicePromise; + + // Set up the stream. + stream = stream || new DataStream(); + stream.operation = dataOperation; + + if(!(dataService = this._dataServiceByDataStream.get(stream))) { + this._dataServiceByDataStream.set(stream, (dataServicePromise = this._childServiceRegistrationPromise.then(function() { + var service; + //This is a workaround, we should clean that up so we don't + //have to go up to answer that question. The difference between + //.TYPE and Objectdescriptor still creeps-in when it comes to + //the service to answer that to itself + if (self.parentService && self.parentService.childServiceForType(objectDescriptor) === self && typeof self.fetchRawData === "function") { + service = self; + service._fetchRawData(stream); + } else { + + // Use a child service to fetch the data. + service = self.childServiceForType(objectDescriptor); + if (service) { + //Here we end up creating an extra stream for nothing because it should be third argument. + self._dataServiceByDataStream.set(stream, service); + } else { + throw new Error("Can't fetch data of unknown type - " + objectDescriptor.name); + } + } + + return service; + }))); + } + else { + dataServicePromise = Promise.resolve(dataService); + } + + dataServicePromise.then(function(dataService) { + try { + //Direct access for now + stream = dataService.handleReadOperation(dataOperation); + } catch (e) { + stream.dataError(e); + } + + }) + + // Return the passed in or created stream. + return stream; + } + }, + + _childServiceForQuery: { value: function (query) { var serviceModuleID = this._serviceIdentifierForQuery(query), @@ -1997,6 +2257,29 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, + + /** + * EventChange handler, begining of tracking objects changes via Triggers right now, + * which are installed on propertyDescriptors. We might need to refine that by adding the + * ability to model wether a property is persisted or not. If it's not meant to be persisted, + * then a DataService most likely doesn't have much to do with it. + * Right now, this is unfortunately called even during the mapRawDataToObject. + * We need a way to ignore this as early as possible + * + * @method + * @argument {ChangeEvent} [changeEvent] - The changeEvent + * + */ + handleChange: { + value: function(changeEvent) { + if(!this._createdDataObjects || (this._createdDataObjects && !this._createdDataObjects.has(changeEvent.target))) { + //Needs to register the change so saving changes / update operations can use it later to decise what to send + //console.log("handleChange:",changeEvent); + this.registerDataObjectChangesFromEvent(changeEvent); + } + } + }, + /*************************************************************************** * Saving Data */ @@ -2012,6 +2295,7 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { deleteDataObject: { value: function (object) { var saved = !this.createdDataObjects.has(object); + this.deletedDataObjects.add(object); return this._updateDataObject(object, saved && "deleteDataObject"); } }, @@ -2070,7 +2354,22 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { else { service = this._getChildServiceForObject(object); if (service) { - return service.saveDataObject(object); + var result = service.saveDataObject(object); + if(result) { + return result.then(function(success) { + self.rootService.createdDataObjects.delete(object); + //Duck test of an operation + // if(success.data) { + // return success.data; + // } + // else { + return success; + // } + + }, function(error) { + console.error(error); + }); + } } else { return promise; @@ -2133,6 +2432,24 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { // } // }, + saveChanges: { + value: function () { + //We need a list of the changes happening (creates, updates, deletes) operations + //to keep their natural order and be able to create a transaction operationn + //when saveChanges is called. + + //createdDataObjects is a set + var createdDataObjects = this.createdDataObjects, + changedDataObjects = this.changedDataObjects, + deletedDataObjects = this.deletedDataObjects; + + //Here we want to create a transaction to make sure everything is sent at the same time. + //Right now, we don't track what object fetched was changed that eventually needs to be included. + } + }, + + + /*************************************************************************** * Offline */ @@ -2673,3 +2990,8 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }); + + + +//WARNING Shouldn't be a problem, but avoiding a potential require-cycle for now: +DataStream.DataService = exports.DataService; diff --git a/data/service/data-stream.js b/data/service/data-stream.js index b8fe8c2087..6ec6a2d8a9 100644 --- a/data/service/data-stream.js +++ b/data/service/data-stream.js @@ -6,7 +6,9 @@ var DataProvider = require("data/service/data-provider").DataProvider, deprecate = require("core/deprecate"), parse = require("frb/parse"), Scope = require("frb/scope"), - compile = require("frb/compile-evaluator"); + compile = require("frb/compile-evaluator"), + DataOperation= require("data/service/data-operation").DataOperation, + DataStream; /** * A [DataProvider]{@link DataProvider} whose data is received sequentially. @@ -41,7 +43,7 @@ var DataProvider = require("data/service/data-provider").DataProvider, * @extends DataProvider * */ -exports.DataStream = DataProvider.specialize(/** @lends DataStream.prototype */ { +DataStream = exports.DataStream = DataProvider.specialize(/** @lends DataStream.prototype */ { /*************************************************************************** * Basic properties @@ -199,6 +201,36 @@ exports.DataStream = DataProvider.specialize(/** @lends DataStream.prototype */ } }, + + thenForEach: { + value: function (onFulfilled, onRejected) { + this._dataBatchPromises = []; + this.query._doesBatchResults = true; + this._forEachFulilledBatch = onFulfilled; + //return this._promise.then(onFulfilled, onRejected); + return this._promise; + + // var self = this; + // if (typeof this.__forEachPromise === "function") { + // this.__forEachPromise= this.__forEachPromise(); + // } else if (!this.__forEachPromise) { + // this.__forEachPromise = new Promise(function(resolve, reject) { + // self._forEachPromiseResolve = resolve; + // self._forEachPromiseReject = reject; + // }); + // } + + // this._thenForEachFulfilled = onFulfilled; + // this._thenForEachRejected = onRejected; + + + // return this.__promise; + + // return this._forEachPromise.then(onFulfilled, onRejected); + } + }, + + /** * Method of the [Promise]{@linkcode external:Promise} class used to * kick off additional processing when an error has been encountered. @@ -238,6 +270,9 @@ exports.DataStream = DataProvider.specialize(/** @lends DataStream.prototype */ var data = objects; if (this.dataExpression && objects) { + //#PERFORMANCE + //We shpuldn't be creating a Scope every time, but reusing one + //and set its value data = this._compiledDataExpression(new Scope(objects)); } @@ -265,6 +300,89 @@ exports.DataStream = DataProvider.specialize(/** @lends DataStream.prototype */ } }, + //Experimental with Shopify first, _hasNextPage is Shopify specific + //It is set on the stream by the RawDataServie before calling batchDataDone on the stream + hasPendingData: { + get: function() { + return this.hasOwnProperty("_hasNextPage") && this._hasNextPage; + } + }, + + _batchCount: { + value: 0 + }, + + _dataBatchPromises: { + value: 0 + }, + + /** + * To be called when a batch of data expected by this stream has been added + * to its [data]{@link DataStream#data} array. + * + * TODO: an argument should be passed to dataBatchDone, and it should be a ReadUpdated operation + * That operation should have properties like the stream, the results, the batch size, the batch number + * (3rd one since the beginning if we don't know the full size, which could change at any time), etc... + * That read operation would then be passed to thenForEach(function(readUpdatedOperation){...}).then(...) + * + * @method + */ + dataBatchDone: { + value: function (readUpdatedOperation) { + var batchCallResult; + this._batchCount++; + + //This is probably should come from lower layers, the RawDataService in the Worker, but until then: + if(!readUpdatedOperation) { + var readUpdatedOperation = new DataOperation(); + readUpdatedOperation.type = DataOperation.Type.ReadUpdated; + readUpdatedOperation.objectDescriptor = readUpdatedOperation.dataType = this.query.type; + readUpdatedOperation.cursor = this._cursor; + readUpdatedOperation.batchSize = this.query.batchSize; + readUpdatedOperation.batchCount = this._batchCount; + //FIXME, when we get a readUpdatedOperation from bellow, it should have the + //batch in it and we shouldn't have to slice the full array here + readUpdatedOperation.data = this.data.slice(this._lastBatchIndex); + } + + //Kick starts the request for the next batch: + if(this.hasPendingData) { + var readUpdateOperation = new DataOperation(); + readUpdateOperation.type = DataOperation.Type.ReadUpdate; + readUpdateOperation.objectDescriptor = readUpdateOperation.dataType = this.query.type; + readUpdateOperation.cursor = this._cursor; + readUpdateOperation.batchSize = this.query.batchSize; + + + //When we have a first read operation that corresponds to the query, we need to set it as the referrer: + //readUpdateOperation.referrer = this.readOperation + readUpdateOperation.referrer = this; + DataStream.DataService.mainService.readData(readUpdateOperation, this); + } + + //Deliver the current batch + batchCallResult = this._forEachFulilledBatch(readUpdatedOperation); + if(Promise.is(batchCallResult)) { + this._dataBatchPromises.push(batchCallResult); + } + + if(!this.hasPendingData) { + if(this._dataBatchPromises.length) { + var self = this; + Promise.all(this._dataBatchPromises) + .then(function(success) { + self.dataDone(); + },function(error) { + self.dataError(error); + } ); + } + else { + this.dataDone(); + } + } + } + }, + /** * To be called when a problem is encountered while trying to * fetch data for this stream. After this is called all subsequent diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js index c332f70aaa..e2b4554f65 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -2,6 +2,10 @@ var Montage = require("core/core").Montage, DataObjectDescriptor = require("data/model/data-object-descriptor").DataObjectDescriptor, ObjectDescriptor = require("data/model/object-descriptor").ObjectDescriptor, WeakMap = require("collections/weak-map"), + Map = require("collections/map"), + //DataService requires DataTrigger before it sets itself on the exports object... + //DataServiceModule = require("data/service/data-service"), + ChangeEvent = require("../../core/event/change-event").ChangeEvent, DataTrigger; /** @@ -243,6 +247,9 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy } }, + _valueSetter: { + value: undefined + }, /** * Note that if a trigger's property value is set after that values is * requested but before it is obtained from the trigger's service the @@ -258,43 +265,124 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy configurable: true, writable: true, value: function (object, value) { - var status, prototype, descriptor, getter, setter, writable, currentValue, isToMany; + var status, prototype, descriptor, getter, setter = this._valueSetter, writable, currentValue, isToMany, isArray, initialValue; + // Get the value's current status and update that status to indicate // the value has been obtained. This way if the setter called below // requests the property's value it will get the value the property // had before it was set, and it will get that value immediately. status = this._getValueStatus(object); this._setValueStatus(object, null); - // Search the prototype chain for a setter for this trigger's - // property, starting just after the trigger prototype that caused - // this method to be called. - prototype = Object.getPrototypeOf(this._objectPrototype); - while (prototype) { - descriptor = Object.getOwnPropertyDescriptor(prototype, this._propertyName); - getter = descriptor && descriptor.get; - setter = getter && descriptor.set; - writable = !descriptor || setter || descriptor.writable; - prototype = writable && !setter && Object.getPrototypeOf(prototype); + + + //We're not changing inheritance at runtime, no need to wolk the tree everytime... + if(!setter) { + // Search the prototype chain for a setter for this trigger's + // property, starting just after the trigger prototype that caused + // this method to be called. + prototype = Object.getPrototypeOf(this._objectPrototype); + while (prototype) { + descriptor = Object.getOwnPropertyDescriptor(prototype, this._propertyName); + getter = descriptor && descriptor.get; + setter = getter && descriptor.set; + writable = !descriptor || setter || descriptor.writable; + prototype = writable && !setter && Object.getPrototypeOf(prototype); + } + this._valueSetter = setter; } + initialValue = this._getValue(object); + //If Array / to-Many + isToMany = this.propertyDescriptor.cardinality !== 1; + isArray = Array.isArray(initialValue); // Set this trigger's property to the desired value, but only if // that property is writable. if (setter) { setter.call(object, value); + //currentValue = value; } else if (writable) { - //If Array / to-Many - isToMany = this.propertyDescriptor.cardinality !== 1; - currentValue = this._getValue(object); - if (isToMany && Array.isArray(currentValue)) { - object[this._privatePropertyName].splice.apply(currentValue, [0, Infinity].concat(value)); + if (isToMany && isArray) { + object[this._privatePropertyName].splice.apply(initialValue, [0, Infinity].concat(value)); } else { object[this._privatePropertyName] = value; } } + + currentValue = this._getValue(object); + if(currentValue !== initialValue) { + + if(isToMany) { + if(initialValue && isArray) { + var listener = this._collectionListener.get(object); + if(listener) { + initialValue.removeRangeChangeListener(listener); + if(!currentValue) { + this._collectionListener.delete(object); + } + } + } + if(currentValue) { + if(Array.isArray(currentValue)) { + var self = this, + listener = function _triggerCollectionListener(plus, minus, index) { + //If we're not in the middle of a mapping...: + if(!self._service._objectsBeingMapped.has(object)) { + //Dispatch update event + var changeEvent = new ChangeEvent; + changeEvent.target = object; + changeEvent.key = self._propertyName; + changeEvent.index = index; + changeEvent.addedValues = plus; + changeEvent.removedValues = minus; + + //To deal with changes happening to an array value of that property, + //we'll need to add/cancel observing on the array itself + //and dispatch added/removed change in the array's change handler. + + //Bypass EventManager for now + self._service.rootService.handleChange(changeEvent); + } + }; + + this._collectionListener.set(object,listener); + currentValue.addRangeChangeListener(listener); + } + else if(currentValue instanceof Map) { + console.error("DataTrigger misses implementation to track changes on property values that are Map"); + } + else { + console.error("DataTrigger misses implementation to track changes on property values that are neither Array nor Map"); + } + + } + } + } + + +//addRangeChangeListener + + //If we're not in the middle of a mapping...: + if(!this._service._objectsBeingMapped.has(object)) { + //Dispatch update event + var changeEvent = new ChangeEvent; + changeEvent.target = object; + changeEvent.key = this._propertyName; + changeEvent.previousKeyValue = initialValue; + changeEvent.keyValue = currentValue; + + //To deal with changes happening to an array value of that property, + //we'll need to add/cancel observing on the array itself + //and dispatch added/removed change in the array's change handler. + + //Bypass EventManager for now + this._service.rootService.handleChange(changeEvent); + } + + // Resolve any pending promise for this trigger's property value. if (status) { status.resolve(null); @@ -302,6 +390,16 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy } }, + __collectionListener: { + value: undefined + }, + _collectionListener: { + get: function() { + return this.__collectionListener || (this.__collectionListener = new WeakMap); + } + }, + + /** * @todo Rename and document API and implementation. * @@ -370,7 +468,7 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy this._service.fetchObjectProperty(object, this._propertyName).then(function () { return self._fulfillObjectPropertyFetch(object); }).catch(function (error) { - console.error(error); + console.error("DataTrigger Error _fetchObjectProperty for property \""+self._propertyName+"\"",error); return self._fulfillObjectPropertyFetch(object, error); }); } diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 7c89977253..eddf8322ba 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -701,7 +701,41 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot stream.dataDone(); return null; }).catch(function (e) { - console.error(e); + console.error(e,stream); + }); + + } + }, + + /** + * To be called once for each [fetchData()]{@link RawDataService#fetchData} + * or [fetchRawData()]{@link RawDataService#fetchRawData} call received to + * indicate that all the raw data meant for the specified stream has been + * added to that stream. + * + * Subclasses should not override this method. + * + * @method + * @argument {DataStream} stream - The stream to which the data objects + * corresponding to the raw data have been + * added. + * @argument {?} context - An arbitrary value that will be passed to + * [writeOfflineData()]{@link RawDataService#writeOfflineData} + * if it is provided. + */ + rawDataBatchDone: { + value: function (stream, context) { + var self = this, + dataToPersist = this._streamRawData.get(stream), + mappingPromises = this._streamMapDataPromises.get(stream), + dataReadyPromise = mappingPromises ? Promise.all(mappingPromises) : this.nullPromise; + + dataReadyPromise + .then(function () { + stream.dataBatchDone(); + return null; + }).catch(function (e) { + console.error(e,stream); }); } @@ -805,7 +839,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot * Subclasses should override this method to map properties of the raw data * to data objects: * @method - * @argument {Object} record - An object whose properties' values hold + * @argument {Object} rawData - An object whose properties' values hold * the raw data. * @argument {Object} object - An object whose properties must be set or * modified to represent the raw data. @@ -820,6 +854,54 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot return this.mapFromRawData(object, rawData, context); } }, + + + /** + * Called by a mapping before doing it's mapping work, giving the data service. + * an opportunity to intervene. + * + * Subclasses should override this method to influence how are properties of + * the raw mapped data to data objects: + * + * @method + * @argument {Object} mapping - A DataMapping object handing the mapping. + * @argument {Object} rawData - An object whose properties' values hold + * the raw data. + * @argument {Object} object - An object whose properties must be set or + * modified to represent the raw data. + * @argument {?} context - The value that was passed in to the + * [addRawData()]{@link RawDataService#addRawData} + * call that invoked this method. + */ + willMapRawDataToObject: { + value: function (mapping, rawData, object, context) { + return rawData; + } + }, + + /** + * Called by a mapping before doing it's mapping work, giving the data service. + * an opportunity to intervene. + * + * Subclasses should override this method to influence how are properties of + * the raw mapped data to data objects: + * + * @method + * @argument {Object} mapping - A DataMapping object handing the mapping. + * @argument {Object} rawData - An object whose properties' values hold + * the raw data. + * @argument {Object} object - An object whose properties must be set or + * modified to represent the raw data. + * @argument {?} context - The value that was passed in to the + * [addRawData()]{@link RawDataService#addRawData} + * call that invoked this method. + */ + didMapRawDataToObject: { + value: function (mapping, rawData, object, context) { + return rawData; + } + }, + /** * Convert raw data to data objects of an appropriate type. * @@ -857,19 +939,91 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot value: function (record, object, context) { var self = this, mapping = this.mappingForObject(object), + snapshot, result; if (mapping) { + + //Check if this isn't already being done, if it is, it's redundant and we bail + snapshot = this.snapshotForObject(object); + if(snapshot === record && this._objectsBeingMapped.has(object)) { + return result; + } + + + this._objectsBeingMapped.add(object); + result = mapping.mapRawDataToObject(record, object, context); if (result) { result = result.then(function () { - return self.mapRawDataToObject(record, object, context); + result = self.mapRawDataToObject(record, object, context); + if (!self._isAsync(result)) { + self._objectsBeingMapped.delete(object); + + return result; + } + else { + result = result.then(function(resolved) { + + self._objectsBeingMapped.delete(object); + + return resolved; + }, function(failed) { + + self._objectsBeingMapped.delete(object); + + }); + return result; + } + + }, function(error) { + self._objectsBeingMapped.delete(object); + throw error; }); } else { result = this.mapRawDataToObject(record, object, context); + if (!this._isAsync(result)) { + + self._objectsBeingMapped.delete(object); + return result; + } + else { + result = result.then(function(resolved) { + + self._objectsBeingMapped.delete(object); + + return resolved; + }, function(failed) { + self._objectsBeingMapped.delete(object); + + }); + return result; + } } } else { + + + this._objectsBeingMapped.add(object); + result = this.mapRawDataToObject(record, object, context); + + if (!this._isAsync(result)) { + + self._objectsBeingMapped.delete(object); + + return result; + } + else { + result = result.then(function(resolved) { + + self._objectsBeingMapped.delete(object); + + return resolved; + }, function(failed) { + self._objectsBeingMapped.delete(object); + }); + return result; + } } return result; @@ -899,6 +1053,104 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } }, + /** + * Called by a mapping before doing it's mapping work, giving the data service. + * an opportunity to intervene. + * + * Subclasses should override this method to influence how properties of + * data objects are mapped back to raw data: + * + * @method + * @argument {Object} mapping - A DataMapping object handing the mapping. + * @argument {Object} rawData - An object whose properties' values hold + * the raw data. + * @argument {Object} object - An object whose properties must be set or + * modified to represent the raw data. + * @argument {?} context - The value that was passed in to the + * [addRawData()]{@link RawDataService#addRawData} + * call that invoked this method. + */ + willMapObjectToRawData: { + value: function (mapping, object, rawData, context) { + return rawData; + } + }, + /** + * Called by a mapping after doing it's mapping work. + * + * Subclasses should override this method as needed: + * + * @method + * @argument {Object} mapping - A DataMapping object handing the mapping. + * @argument {Object} rawData - An object whose properties' values hold + * the raw data. + * @argument {Object} object - An object whose properties must be set or + * modified to represent the raw data. + * @argument {?} context - The value that was passed in to the + * [addRawData()]{@link RawDataService#addRawData} + * call that invoked this method. + */ + didMapObjectToRawData: { + value: function (mapping, object, rawData, context) { + return rawData; + } + }, + + + /** + * Public method invoked by the framework during the conversion from + * an object to a raw data. + * Designed to be overriden by concrete RawDataServices to allow fine-graine control + * when needed, beyond transformations offered by an ObjectDescriptorDataMapping or + * an ExpressionDataMapping + * + * @method + * @argument {Object} object - An object whose properties must be set or + * modified to represent the raw data. + * @argument {String} propertyName - The name of a property whose values + * should be converted to raw data. + * @argument {Object} data - An object whose properties' values hold + * the raw data. + */ + + mapObjectPropertyToRawData: { + value: function (object, propertyName, data) { + } + }, + + /** + * @todo Document. + * @todo Make this method overridable by type name with methods like + * `mapHazardToRawData()` and `mapProductToRawData()`. + * + * @method + */ + _mapObjectPropertyToRawData: { + value: function (object, propertyName, record, context) { + var mapping = this.mappingForObject(object), + result; + + if (mapping) { + result = mapping.mapObjectPropertyToRawData(object, propertyName, record, context); + } + + if (record) { + if (result) { + var otherResult = this.mapObjectPropertyToRawData(object, propertyName, record, context); + if (this._isAsync(result) && this._isAsync(otherResult)) { + result = Promise.all([result, otherResult]); + } else if (this._isAsync(otherResult)) { + result = otherResult; + } + } else { + result = this.mapObjectPropertyToRawData(object, propertyName, record, context); + } + } + + return result; + } + }, + /** * @todo Document. * @todo Make this method overridable by type name with methods like From f8b0fa66404107c7440c10605cf5e73e4f8db89c Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 15:02:09 -0800 Subject: [PATCH 055/407] comment about design of that object --- data/service/data-operation.js | 68 +++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 1c9904da0b..40b46157ba 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -63,7 +63,7 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ value: undefined }, /** - * BENOIT: we have an overlap in term of semantics between the type of the operation and the type of the data it applies to. So we either keep "type" for the operation itseld as it is, and dataType, or we flip, calling this operationType and the dataType becomes "type". + * The type of operation, (TODO: inherited from Event). We sh * @type {DataOperation.Type.CREATE|DataOperation.Type.READ|DataOperation.Type.UPDATE|DataOperation.Type.DELETE} */ @@ -104,6 +104,19 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ * Expected to be a boolean expression to be applied to data * objects to determine whether they should be impacted by this operation or not. * + * For modifying one object, we need to be able to build a criteria with the identifier + * that can be converted back to the primary key by a RawDataService. + * + * For example, a DataIdentifier: + * "montage-data://environment/type/#12AS7507" + * "m-data://environment/type/#12AS7507" + * "mdata://environment/type/#12AS7507" + * "data-id://environment/type/#12AS7507" + * + * "montage-data://[dataService.identifier]/[dataService.connectionDescriptor.name || default]/[objectDescriptor.name]/[primaryKey] + * "montage-data://[dataService.identifier]/[dataService.connectionDescriptor.name || default]/[objectDescriptor.name]/[primaryKey] + * + * "identifier = $identifier", {"identifier":"montage-data://[dataService.identifier]/[dataService.connectionDescriptor.name || default]/[objectDescriptor.name]/[primaryKey]} * "hazard_ID = #12AS7507" * * @type {Criteria} @@ -231,6 +244,9 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ value: undefined }, + /* + Might be more straightforward to name this objectDescriptor + */ dataType: { value: undefined }, @@ -299,6 +315,46 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ * "object": "object2-data-identifier" * } * } + * + * + * What if we used 2 new different operators on top of <-, <->, =, as in:? + * { + * "root": { + * "prototype": "package/data/main.datareel/model/custom-type", + * "values": { + * "foo": {"=":"Bleh"}, + * "toManyProperty": { + * //We would add the dataService unique objects map in the deserializer's context + * //so that we can reference this objects if they exists. + * //we're missing the index to tell us where the change happens. * + * "+":"[@identifier1-url,@identifier2-url,@newObject]", + * "-":"[@identifier4-url]", + * }, + * //Should/could that be done with one expression that roughtl would look like: + * "toManyProperty2.splice(index,add,remove)": {"=":"[@change.index,@change.add,@change.remove]"} + * } + * }, + * "change": { + * "prototype": "Object", + * "values": { + * "index": "3", + * "add": "[@identifier1-url,@identifier2-url,@newObject]", + * "remove":"[@identifier4-url]", + + * } + * } + * } + + * "newObject": { + * "prototype": "module-id", + * "values": { + * "propA": "A", + * "propB": "B" + * } + * } + * } + + * * //Transaction. For a Transaction, data would contain the list of data operations grouped together. * //If data is muted, and observed, it could be dynamically processed by RawDataServices. * //The referrer property, which is a pointer to another DatOperaiton would be used by an update/addition @@ -351,10 +407,16 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ */ Type: { + + /* + Search: is a read operation + + */ value: { Create: {isCreate: true}, CreateFailed: {isCreate: true}, CreateCompleted: {isCreate: true}, + CreateCancelled: {isCreate: true}, //Additional Copy: {isCreate: true}, CopyFailed: {isCreate: true}, @@ -368,7 +430,8 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ /* ReadProgress / ReadUpdate / ReadSeek is used to instruct server that more data is required for a "live" read / query Need a better name, and a symetric? Or is ReadUpdated enough if it referes to previous operation */ - ReadProgress: {isRead: true}, //ReadUpdated + ReadProgress: {isRead: true}, //ReadUpdate + ReadUpdate: {isRead: true}, //ReadUpdate /* ReadCancel is the operation that instructs baclkend that client isn't interested by a read operastion anymore */ ReadCancel: {isRead: true}, @@ -383,6 +446,7 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ Update: {isUpdate: true}, UpdateCompleted: {isUpdate: true}, UpdateFailed: {isUpdate: true}, + UpdateCanceled: {isUpdate: true}, Delete: {isDelete: true}, DeleteCompleted: {isDelete: true}, DeleteFailed: {isDelete: true}, From ad263cd846f571291ceda03c136c4d690c6f5da3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 15:02:42 -0800 Subject: [PATCH 056/407] add service to scope and avoid re-creating scopes --- data/service/expression-data-mapping.js | 107 ++++++++++++++++++++---- 1 file changed, 92 insertions(+), 15 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 54b4835107..719a41ad42 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -181,7 +181,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData _scope: { get: function() { - return this.__scope || new Scope(); + return this.__scope || new Scope(this.service); } }, @@ -334,6 +334,36 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } }, + _rawRequisitePropertyNames: { + value: undefined + }, + rawRequisitePropertyNames: { + get: function () { + if(!this._rawRequisitePropertyNames) { + this._rawRequisitePropertyNames = new Set(); + + var iterator = this.requisitePropertyNames.values(), + objectRule, rule, objectMappingRules = this.objectMappingRules, + rawDataMappingRules = this.rawDataMappingRules, + promises, propertyName, result = this._rawRequisitePropertyNames; + + + if (this.requisitePropertyNames.size) { + while ((propertyName = iterator.next().value)) { + objectRule = objectMappingRules.get(propertyName), + rule = rawDataMappingRules.get(objectRule.sourcePath); + + if(rule) { + result.add(objectRule.sourcePath); + } + } + } + + } + return this._rawRequisitePropertyNames; + } + }, + /** * Adds a name to the list of properties that will participate in * eager mapping. The requisite property names will be mapped @@ -348,6 +378,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData if (!this._ownRequisitePropertyNames.has(arg)) { this._ownRequisitePropertyNames.add(arg); this._requisitePropertyNames = null; //To ensure all arguments are added to this.requisitePropertyNames + this._rawRequisitePropertyNames = null; } } } @@ -426,14 +457,13 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData * modified to represent the raw data. */ mapRawDataToObject: { - value: function (data, object) { + value: function (data, object, context) { var iterator = this.requisitePropertyNames.values(), promises, propertyName, result; - if (this.requisitePropertyNames.size) { while ((propertyName = iterator.next().value)) { - result = this.mapRawDataToObjectProperty(data, object, propertyName); + result = this.mapRawDataToObjectProperty(data, object, propertyName, context); if (this._isAsync(result)) { (promises || (promises = [])).push(result); } @@ -457,12 +487,13 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData * */ mapRawDataToObjectProperty: { - value: function (data, object, propertyName) { + value: function (data, object, propertyName, context) { var rule = this.objectMappingRules.get(propertyName), propertyDescriptor = rule && this.objectDescriptor.propertyDescriptorForName(propertyName), isRelationship = propertyDescriptor && !propertyDescriptor.definition && propertyDescriptor.valueDescriptor, isDerived = propertyDescriptor && !!propertyDescriptor.definition, scope = this._scope, + propertyScope, debug = DataService.debugProperties.has(propertyName) || (rule && rule.debug === true); @@ -472,19 +503,20 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData console.debug("To debug ExpressionDataMapping.mapRawDataToObjectProperty for " + propertyName + ", set a breakpoint here."); } - scope.value = data; + propertyScope = scope.nest(data); this._prepareRawDataToObjectRule(rule, propertyDescriptor); - return isRelationship ? this._resolveRelationship(object, propertyDescriptor, rule, scope) : - propertyDescriptor && !isDerived ? this._resolveProperty(object, propertyDescriptor, rule, scope) : + return isRelationship ? this._resolveRelationship(object, propertyDescriptor, rule, propertyScope) : + propertyDescriptor && !isDerived ? this._resolveProperty(object, propertyDescriptor, rule, propertyScope) : null; } }, _resolveRelationship: { value: function (object, propertyDescriptor, rule, scope) { + //console.debug("_resolveRelationship "+propertyDescriptor.name+" on ",object); var self = this, hasInverse = !!propertyDescriptor.inversePropertyName || !!rule.inversePropertyName, data; @@ -498,8 +530,14 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } return hasInverse ? self._assignInversePropertyValue(data, object, propertyDescriptor, rule) : null; + }, function(error) { + var message = "Failed to evaluate expression data mapping rule.\n"; + message += error.message; + error.rule = rule; + error.scope = scope; + console.error("failed to evaluate rule:",rule," with scope:",scope); + throw error; }).then(function () { - self._setObjectValueForPropertyDescriptor(object, data, propertyDescriptor); return null; }); @@ -553,7 +591,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData if (this._isAsync(result)) { self = this; result.then(function (value) { - rawData[propertyName] = result; + rawData[propertyName] = value; return null; }); } else { @@ -636,25 +674,57 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData _mapObjectToRawDataProperty: { value: function(object, data, propertyName) { var rule = this.rawDataMappingRules.get(propertyName), - scope = new Scope(object), + //scope = new Scope(object), + scope = this._scope, + propertyScope, propertyDescriptor = rule && rule.propertyDescriptor, isRelationship = propertyDescriptor && propertyDescriptor.valueDescriptor, result; + propertyScope = scope.nest(object); if (isRelationship && rule.converter) { this._prepareObjectToRawDataRule(rule); - result = this._revertRelationshipToRawData(data, propertyDescriptor, rule, scope); + result = this._revertRelationshipToRawData(data, propertyDescriptor, rule, propertyScope); } else if (rule.converter || rule.reverter) { - result = this._revertPropertyToRawData(data, propertyName, rule, scope); + result = this._revertPropertyToRawData(data, propertyName, rule, propertyScope); } else /*if (propertyDescriptor)*/ { //relaxing this for now - data[propertyName] = rule.expression(scope); + data[propertyName] = rule.expression(propertyScope); } return result; } }, + mapObjectPropertyToRawData: { + value: function(object, propertyName, data) { + var objectRule = this.objectMappingRules.get(propertyName), + rule = this.rawDataMappingRules.get(objectRule.sourcePath); + + if(rule) { + return this._mapObjectToRawDataProperty(object,data,objectRule.sourcePath); + } + else { + throw new Error("Can't map property "+propertyName+" of object,", object, "to raw data"); + } + + } + }, + + mapObjectPropertyNameToRawPropertyName: { + value: function(property) { + var objectRule = this.objectMappingRules.get(property), + rule = objectRule && this.rawDataMappingRules.get(objectRule.sourcePath); + + if(rule) { + return objectRule.sourcePath; + } + else { + return property; + } + + } + }, /** * Prefetches any object properties required to map the rawData property * and maps once the fetch is complete. @@ -708,7 +778,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData while ((key = keys.next().value)) { if (rawRequirementsToMap.has(key)) { - result = this.mapObjectToRawDataProperty(object, data, key, propertyName); + result = this.mapObjectToRawDataProperty(object, data, key); if (this._isAsync(result)) { promises = promises || []; promises.push(result); @@ -1054,6 +1124,13 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } else { rule.reverter = this._defaultConverter(rule.sourcePath, rule.targetPath, isObjectMappingRule); } + + if(rule.converter) { + rule.converter.scope = this._scope.nest(undefined); + } + if(rule.reverter) { + rule.reverter.scope = this._scope.nest(undefined); + } return rule; } }, From ad12e56d7499020b8fe5128594c289ef9ec7db73 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 15:04:39 -0800 Subject: [PATCH 057/407] - fix event missing in callback - avoid setting header that has no value --- data/service/http-service.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/data/service/http-service.js b/data/service/http-service.js index b89a0e10c6..c9a616206d 100644 --- a/data/service/http-service.js +++ b/data/service/http-service.js @@ -294,7 +294,7 @@ var HttpService = exports.HttpService = RawDataService.specialize(/** @lends Htt } return new Promise(function (resolve, reject) { - var i, keys, key, + var i, keys, key, iValue, startTime = new Date().getTime(); // Report errors or fetch the requested raw data. @@ -309,7 +309,7 @@ var HttpService = exports.HttpService = RawDataService.specialize(/** @lends Htt // console.log("Completed request for (", parsed.url, ") in (", ((new Date().getTime() - startTime)), ") ms"); } }; - request.onerror = function () { + request.onerror = function (event) { error = HttpError.withRequestAndURL(request, parsed.url); reject(error); }; @@ -319,7 +319,9 @@ var HttpService = exports.HttpService = RawDataService.specialize(/** @lends Htt keys = Object.keys(parsed.headers); for (i = 0; (key = keys[i]); ++i) { - request.setRequestHeader(key, parsed.headers[key]); + if(iValue = parsed.headers[key]) { + request.setRequestHeader(key, iValue); + } } request.withCredentials = parsed.credentials; request.send(parsed.body); From ea7b6b9cbbd86beecbeb30ecffbc099689edc7bd Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 15:05:40 -0800 Subject: [PATCH 058/407] - add missing properties for symetry between convert and revert --- data/service/mapping-rule.js | 46 +++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/data/service/mapping-rule.js b/data/service/mapping-rule.js index 3eb6084eba..4e3e9e5868 100644 --- a/data/service/mapping-rule.js +++ b/data/service/mapping-rule.js @@ -29,6 +29,9 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { * the output of the expression is assigned directly to the destination value. * @type {string} */ + _expression: { + value: undefined + }, expression: { get: function () { if (!this._expression && this.sourcePathSyntax) { @@ -46,7 +49,7 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { * * @type {string} */ - + _inversePropertyName: { value: undefined }, @@ -92,7 +95,7 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { * A converter that takes in the the output of #expression and returns the destination value. * When a reverter is specified the conversion use the revert method when mapping from * right to left. - * + * * @type {Converter} */ reverter: { @@ -172,6 +175,37 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { value: undefined }, + /** + * Object created by parsing .sourcePath using frb/grammar.js that will + * be used to evaluate the data + * @type {Object} + * */ + targetPathSyntax: { + get: function () { + if (!this._targetPathSyntax && this.targetPath) { + this._targetPathSyntax = parse(this.targetPath); + } + return this._targetPathSyntax; + } + }, + + /** + * The expression that defines the input to be passed to .converter's revert. If converter is not provided, + * the output of the expression is assigned directly to the destination value. + * @type {string} + */ + _revertExpression: { + value: undefined + }, + revertExpression: { + get: function () { + if (!this._revertExpression && this.targetPathSyntax) { + this._revertExpression = compile(this.targetPathSyntax); + } + return this._revertExpression; + } + }, + /** * Return the value of the property for this rule @@ -181,7 +215,7 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { value: function (scope) { var value = this.expression(scope); return this.converter ? this.converter.convert(value) : - this.reverter ? + this.reverter ? this.reverter.revert(value) : value; } @@ -195,16 +229,16 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { var rule = new this(), sourcePath = addOneWayBindings ? rawRule[ONE_WAY_BINDING] || rawRule[TWO_WAY_BINDING] : propertyName, targetPath = addOneWayBindings && propertyName || rawRule[TWO_WAY_BINDING]; - + rule.inversePropertyName = rawRule.inversePropertyName; rule.serviceIdentifier = rawRule.serviceIdentifier; rule.sourcePath = sourcePath; rule.targetPath = targetPath; - + return rule; } }, - + }); From 43d2cd302e362a74c29a21dd55030608b07dc7ae Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 15:06:45 -0800 Subject: [PATCH 059/407] Fix spec that passes a string argument to deserialize --- test/spec/serialization/montage-deserializer-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/serialization/montage-deserializer-spec.js b/test/spec/serialization/montage-deserializer-spec.js index 465a8a4039..739c05ec91 100644 --- a/test/spec/serialization/montage-deserializer-spec.js +++ b/test/spec/serialization/montage-deserializer-spec.js @@ -1296,7 +1296,7 @@ describe("serialization/montage-deserializer-spec", function () { serializationString = JSON.stringify(serialization); deserializer.init(serializationString, require); - deserializer.deserialize(serializationString).then(function (objects) { + deserializer.deserialize().then(function (objects) { expect(objects.a).toBe(null); }).catch(function(reason) { fail(reason); From f18f0412461ffe093d326808696b424736ec9c07 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 19:36:29 -0800 Subject: [PATCH 060/407] WIP on change event --- core/event/range-change.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 core/event/range-change.js diff --git a/core/event/range-change.js b/core/event/range-change.js new file mode 100644 index 0000000000..228310eeb4 --- /dev/null +++ b/core/event/range-change.js @@ -0,0 +1,17 @@ + +/** + * Models a change happening to an Array + * + * @class + * @extends Montage + */ + +function RangeChange(index, addedValues, removedValues) { + this.index = index; + this.addedValues = addedValues; + this.removedValues = removedValues; + + return this; +} + +exports.RangeChange = RangeChange; From 7b44f488afc5b653cb029680df0b1de6659c3782 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 22:22:51 -0800 Subject: [PATCH 061/407] add method to find an ObjectDescriptor via it's module.id --- data/service/data-service.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/data/service/data-service.js b/data/service/data-service.js index 1285fdc905..1fd0464a4c 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -981,6 +981,18 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, + objectDescriptorForObjectDescriptorModuleId: { + value: function (objectDescriptorModuleId) { + var types = this.types, i, n; + for (i = 0, n = types.length; i < n; i++) { + if(types[i].module.id === objectDescriptorModuleId) { + return types[i]; + } + } + return null; + } + }, + /** * Get the type of the specified data object. * From 24032ca5e764bb20df8f100f81ca29eca36997fd Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 22:53:10 -0800 Subject: [PATCH 062/407] remove deprecated use of MappingRule inversePropertyName --- data/service/expression-data-mapping.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 719a41ad42..8973cfbad6 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -518,7 +518,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData value: function (object, propertyDescriptor, rule, scope) { //console.debug("_resolveRelationship "+propertyDescriptor.name+" on ",object); var self = this, - hasInverse = !!propertyDescriptor.inversePropertyName || !!rule.inversePropertyName, + hasInverse = !!propertyDescriptor.inversePropertyName, data; return rule.evaluate(scope).then(function (result) { @@ -547,7 +547,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData _assignInversePropertyValue: { value: function (data, object, propertyDescriptor, rule) { var self = this, - inversePropertyName = propertyDescriptor.inversePropertyName || rule.inversePropertyName; + inversePropertyName = propertyDescriptor.inversePropertyName; return propertyDescriptor.valueDescriptor.then(function (objectDescriptor) { var inversePropertyDescriptor = objectDescriptor.propertyDescriptorForName(inversePropertyName); From 9b094add038921f18edd423b504e1ed46a8157c3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 23:01:35 -0800 Subject: [PATCH 063/407] Removes last use of .inversePropertyName --- data/service/expression-data-mapping.js | 2 +- data/service/mapping-rule.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 8973cfbad6..a9cd550271 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -1108,7 +1108,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData value: function (rawRule, propertyName, addOneWayBindings, isObjectMappingRule) { var propertyDescriptorName = !isObjectMappingRule && addOneWayBindings ? rawRule[ONE_WAY_BINDING] || rawRule[TWO_WAY_BINDING] : propertyName, propertyDescriptor = this.objectDescriptor.propertyDescriptorForName(propertyDescriptorName), - rule = MappingRule.withRawRuleAndPropertyName(rawRule, propertyName, addOneWayBindings); + rule = MappingRule.withRawRuleAndPropertyName(rawRule, propertyDescriptor, addOneWayBindings); rule.propertyDescriptor = propertyDescriptor; if (rawRule.converter && addOneWayBindings) { diff --git a/data/service/mapping-rule.js b/data/service/mapping-rule.js index 4e3e9e5868..4ae510cb65 100644 --- a/data/service/mapping-rule.js +++ b/data/service/mapping-rule.js @@ -225,12 +225,13 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { }, { withRawRuleAndPropertyName: { - value: function (rawRule, propertyName, addOneWayBindings) { + value: function (rawRule, propertyDescriptor, addOneWayBindings) { var rule = new this(), + propertyName = propertyDescriptor.propertyName, sourcePath = addOneWayBindings ? rawRule[ONE_WAY_BINDING] || rawRule[TWO_WAY_BINDING] : propertyName, targetPath = addOneWayBindings && propertyName || rawRule[TWO_WAY_BINDING]; - rule.inversePropertyName = rawRule.inversePropertyName; + rule.inversePropertyName = propertyDescriptor.inversePropertyName; rule.serviceIdentifier = rawRule.serviceIdentifier; rule.sourcePath = sourcePath; rule.targetPath = targetPath; From 85879dab0f30e1d41ad58dce31afe88ada0453e1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 23:13:49 -0800 Subject: [PATCH 064/407] Fix a regression deprecating MappingRule inversePropertyName --- data/service/expression-data-mapping.js | 2 +- data/service/mapping-rule.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index a9cd550271..a5ceac8cf9 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -1108,7 +1108,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData value: function (rawRule, propertyName, addOneWayBindings, isObjectMappingRule) { var propertyDescriptorName = !isObjectMappingRule && addOneWayBindings ? rawRule[ONE_WAY_BINDING] || rawRule[TWO_WAY_BINDING] : propertyName, propertyDescriptor = this.objectDescriptor.propertyDescriptorForName(propertyDescriptorName), - rule = MappingRule.withRawRuleAndPropertyName(rawRule, propertyDescriptor, addOneWayBindings); + rule = MappingRule.withRawRuleAndPropertyName(rawRule, propertyName, addOneWayBindings, propertyDescriptor); rule.propertyDescriptor = propertyDescriptor; if (rawRule.converter && addOneWayBindings) { diff --git a/data/service/mapping-rule.js b/data/service/mapping-rule.js index 4ae510cb65..0e6156eb75 100644 --- a/data/service/mapping-rule.js +++ b/data/service/mapping-rule.js @@ -225,13 +225,12 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { }, { withRawRuleAndPropertyName: { - value: function (rawRule, propertyDescriptor, addOneWayBindings) { + value: function (rawRule, propertyName, addOneWayBindings, propertyDescriptor) { var rule = new this(), - propertyName = propertyDescriptor.propertyName, sourcePath = addOneWayBindings ? rawRule[ONE_WAY_BINDING] || rawRule[TWO_WAY_BINDING] : propertyName, targetPath = addOneWayBindings && propertyName || rawRule[TWO_WAY_BINDING]; - rule.inversePropertyName = propertyDescriptor.inversePropertyName; + rule.inversePropertyName = propertyDescriptor ? propertyDescriptor.inversePropertyName : null; rule.serviceIdentifier = rawRule.serviceIdentifier; rule.sourcePath = sourcePath; rule.targetPath = targetPath; From ce486ab6eb31e451587fb143b71f91724a78909b Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Nov 2019 23:35:01 -0800 Subject: [PATCH 065/407] Fully remove MappingRule inversePropertyName --- data/service/expression-data-mapping.js | 2 +- data/service/mapping-rule.js | 25 +------------------------ 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index a5ceac8cf9..8973cfbad6 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -1108,7 +1108,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData value: function (rawRule, propertyName, addOneWayBindings, isObjectMappingRule) { var propertyDescriptorName = !isObjectMappingRule && addOneWayBindings ? rawRule[ONE_WAY_BINDING] || rawRule[TWO_WAY_BINDING] : propertyName, propertyDescriptor = this.objectDescriptor.propertyDescriptorForName(propertyDescriptorName), - rule = MappingRule.withRawRuleAndPropertyName(rawRule, propertyName, addOneWayBindings, propertyDescriptor); + rule = MappingRule.withRawRuleAndPropertyName(rawRule, propertyName, addOneWayBindings); rule.propertyDescriptor = propertyDescriptor; if (rawRule.converter && addOneWayBindings) { diff --git a/data/service/mapping-rule.js b/data/service/mapping-rule.js index 0e6156eb75..d2d890e533 100644 --- a/data/service/mapping-rule.js +++ b/data/service/mapping-rule.js @@ -41,28 +41,6 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { } }, - /** - * The name of the property on the destination value that the destination object represents. - * For example, consider: - * - * The MappingRule for Foo.bars will have inversePropertyName = foo. - * - * @type {string} - */ - - _inversePropertyName: { - value: undefined - }, - - inversePropertyName: { - get: deprecate.deprecateMethod(void 0, function () { - return this._inversePropertyName; - }, "MappingRule.inversePropertyName", "PropertyDescriptor.inversePropertyName", true), - set: deprecate.deprecateMethod(void 0, function (value) { - this._inversePropertyName = value; - }, "MappingRule.inversePropertyName", "PropertyDescriptor.inversePropertyName", true) - }, - /** * The descriptor for the property that this rule applies to @@ -225,12 +203,11 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { }, { withRawRuleAndPropertyName: { - value: function (rawRule, propertyName, addOneWayBindings, propertyDescriptor) { + value: function (rawRule, propertyName, addOneWayBindings) { var rule = new this(), sourcePath = addOneWayBindings ? rawRule[ONE_WAY_BINDING] || rawRule[TWO_WAY_BINDING] : propertyName, targetPath = addOneWayBindings && propertyName || rawRule[TWO_WAY_BINDING]; - rule.inversePropertyName = propertyDescriptor ? propertyDescriptor.inversePropertyName : null; rule.serviceIdentifier = rawRule.serviceIdentifier; rule.sourcePath = sourcePath; rule.targetPath = targetPath; From 505dbd728996a4f6688bcf183493cf2ff69a8c7f Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 15 Nov 2019 00:58:41 -0800 Subject: [PATCH 066/407] Fix a bug when the state of a previous serialization is left over when a serializer is reused --- core/serialization/serializer/montage-serializer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/serialization/serializer/montage-serializer.js b/core/serialization/serializer/montage-serializer.js index 3c7b55f798..e5c811910e 100644 --- a/core/serialization/serializer/montage-serializer.js +++ b/core/serialization/serializer/montage-serializer.js @@ -66,6 +66,7 @@ var MontageSerializer = Montage.specialize({ value: function(objects) { var serializationString; + this._builder.init(); this._labeler.initWithObjects(objects); for (var label in objects) { From 078f66e2364b291dec21e40546ea564ebb1aab0c Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 15 Nov 2019 00:59:29 -0800 Subject: [PATCH 067/407] Rename objectDescriptorForObjectDescriptorModuleId to objectDescriptorWithModuleId and optimize it --- data/service/data-service.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 1fd0464a4c..9b301d4754 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -981,15 +981,22 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { } }, - objectDescriptorForObjectDescriptorModuleId: { + _objectDescriptorByModuleId: { + value:undefined + }, + objectDescriptorWithModuleId: { value: function (objectDescriptorModuleId) { - var types = this.types, i, n; - for (i = 0, n = types.length; i < n; i++) { - if(types[i].module.id === objectDescriptorModuleId) { - return types[i]; + + if(!this._objectDescriptorByModuleId) { + var map = this._objectDescriptorByModuleId = new Map(); + + var types = this.types, i, n; + for (i = 0, n = types.length; i < n; i++) { + map.set(types[i].module.id,types[i]); } + } - return null; + return this._objectDescriptorByModuleId.get(objectDescriptorModuleId); } }, From e71c71cd15cd5fb46fe02d31a01e7a9de8874756 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 15 Nov 2019 01:00:56 -0800 Subject: [PATCH 068/407] -iterate on API - change Operation Type to enum to improve serialization - implement serializeSelf/deserializeSelf --- data/service/data-operation.js | 290 +++++++++++++++++++++++++-------- 1 file changed, 219 insertions(+), 71 deletions(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 40b46157ba..cedc56c93a 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -1,5 +1,98 @@ var Montage = require("core/core").Montage, - Criteria = require("core/criteria").Criteria; + Criteria = require("core/criteria").Criteria, + Enum = require("core/enum").Enum, + uuid = require("core/uuid"), + DataOperationType; + + + +exports.DataOperationType = DataOperationType = new Enum().initWithMembers( + "NoOp", + "Create", + "CreateFailed", + "CreateCompleted", + "CreateCancelled", + //Additional + "Copy", + "CopyFailed", + "CopyCompleted", + /* Read is the first operation that mnodels a query */ + "Read", + + /* ReadUpdated is pushed by server when a query's result changes due to data changes from others */ + "ReadUpdated", + + /* ReadProgress / ReadUpdate / ReadSeek is used to instruct server that more data is required for a "live" read / query + Need a better name, and a symetric? Or is ReadUpdated enough if it referes to previous operation + */ + "ReadProgress", //ReadUpdate + "ReadUpdate", //ReadUpdate + + /* ReadCancel is the operation that instructs baclkend that client isn't interested by a read operastion anymore */ + "ReadCancel", + + /* ReadCanceled is the operation that instructs the client that a read operation is canceled */ + "ReadCanceled", + + /* ReadFailed is the operation that instructs the client that a read operation has failed canceled */ + "ReadFailed", + /* ReadCompleted is the operation that instructs the client that a read operation has returned all available data */ + "ReadCompleted", + /* Request to update data, used either by the client sending the server or vice versa */ + "Update", + /* Confirmation that a Request to update data, used either by the client sending the server or vice versa*, has been completed */ + "UpdateCompleted", + /* Confirmation that a Request to update data, used either by the client sending the server or vice versa*, has failed */ + "UpdateFailed", + /* Request to cancel an update, used either by the client sending the server or vice versa */ + "UpdateCancel", + /* Confirmation that a Request to cancel an update data, used either by the client sending the server or vice versa*, has completed */ + "UpdateCanceled", + "Delete", + "DeleteCompleted", + "DeleteFailed", + + /* Lock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ + "Lock", + "LockCompleted", + "LockFailed", + + /* Unlock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ + "Unlock", + "UnockCompleted", + "UnockFailed", + + /* RemmoteProcedureCall models the ability to invoke code logic on the server-side, being a DB StoredProcedure, or an method/function in a service */ + "RemoteProcedureCall", /* Execute ? */ + "RemoteProcedureCallCompleted", /* ExecuteCompleted ? */ + "RemoteProcedureCallFailed" /* ExecuteFailed ? */ + + /* Batch models the ability to group multiple operation. If a referrer is provided + to a BeginTransaction operation, then the batch will be executed within that transaction */ + /* + "Batch", + "BatchCompleted", + "BatchFailed", + */ + + /* A transaction is a unit of work that is performed against a database. + Transactions are units or sequences of work accomplished in a logical order. + A transactions begins, operations are grouped, then it is either commited or rolled-back*/ + /* Start/End Open/Close, Commit/Save, rollback/cancel + "BeginTransaction", + "BeginTransactionCompleted", + "BeginTransactionFailed", + + "CommitTransaction", + "CommitTransactionCompleted", + "CommitTransactionFailed", + + "RollbackTransaction", + "RollbackTransactionCompleted", + "RollbackTransactionFailed", + + */ +); /** * Represents @@ -16,8 +109,68 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ constructor: { value: function DataOperation() { this.time = Date.now(); - this.creationIndex = exports.DataOperation.prototype._currentIndex + 1 || 0; - exports.DataOperation.prototype._currentIndex = this.creationIndex; + this.id = uuid.generate(); + + //Not sure we need this, it's not used anywhere + //this.creationIndex = exports.DataOperation.prototype._currentIndex + 1 || 0; + //exports.DataOperation.prototype._currentIndex = this.creationIndex; + } + }, + + serializeSelf: { + value:function (serializer) { + serializer.setProperty("id", this.id); + serializer.setProperty("type", this.type); + serializer.setProperty("time", this.time); + serializer.setProperty("dataDescriptor", this.dataDescriptor); + if(this.referrerId) { + serializer.setProperty("referrerId", this.referrerId); + } + serializer.setProperty("criteria", this._criteria); + if(this.data) { + serializer.setProperty("data", this.data); + } + + } + }, + deserializeSelf: { + value:function (deserializer) { + var value; + value = deserializer.getProperty("id"); + if (value !== void 0) { + this.id = value; + } + + value = deserializer.getProperty("type"); + if (value !== void 0) { + this.type = value; + } + + value = deserializer.getProperty("time"); + if (value !== void 0) { + this.time = value; + } + + value = deserializer.getProperty("dataDescriptor"); + if (value !== void 0) { + this.dataDescriptor = value; + } + + value = deserializer.getProperty("referrerId"); + if (value !== void 0) { + this.referrerId = value; + } + + value = deserializer.getProperty("criteria"); + if (value !== void 0) { + this.criteria = value; + } + + value = deserializer.getProperty("data"); + if (value !== void 0) { + this.data = value; + } + } }, @@ -164,6 +317,24 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ value: undefined }, + /** + * An operation that preceded and this one is related to. For a ReadUpdated, it would be the Read operation. + * + * @type {String} + */ + referrerId: { + value: undefined + }, + + /** + * The identifier of an operation that preceded and this one is related to. For a ReadUpdated, it would be the Read operation. + * + * @type {DataOperation} + */ + referrerIdentifier: { + value: undefined + }, + /** * Models the agent that created the operation. * @@ -251,6 +422,10 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ value: undefined }, + dataDescriptor: { + value: undefined + }, + /** * data is designed to carry the "meat" of an operation's specifics. For a create, it would be all properties * of a new object (I'm assuming a create operation is modeling only 1 object's creation). @@ -369,34 +544,6 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ snapshotData: { value: undefined - }, - - /*************************************************************************** - * Deprecated - */ - - /** - * @todo: Deprecate and remove when appropriate. - */ - changes: { - get: function () { - return this.data; - }, - set: function (data) { - this.data = data; - } - }, - - /** - * @todo: Deprecate and remove when appropriate. - */ - lastModified: { - get: function () { - return this.time; - }, - set: function (time) { - this.time = time; - } } }, /** @lends DataOperation */ { @@ -413,74 +560,75 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ */ value: { - Create: {isCreate: true}, - CreateFailed: {isCreate: true}, - CreateCompleted: {isCreate: true}, - CreateCancelled: {isCreate: true}, + Create: DataOperationType.Create, + CreateFailed: DataOperationType.CreateFailed, + CreateCompleted: DataOperationType.CreateCompleted, + CreateCancelled: DataOperationType.CreateCancelled, //Additional - Copy: {isCreate: true}, - CopyFailed: {isCreate: true}, - CopyCompleted: {isCreate: true}, + Copy: DataOperationType.Copy, + CopyFailed: DataOperationType.CopyFailed, + CopyCompleted: DataOperationType.CopyCompleted, /* Read is the first operation that mnodels a query */ - Read: {isRead: true}, + Read: DataOperationType.Read, /* ReadUpdated is pushed by server when a query's result changes due to data changes from others */ - ReadUpdated: {isRead: true}, + ReadUpdated: DataOperationType.ReadUpdated, /* ReadProgress / ReadUpdate / ReadSeek is used to instruct server that more data is required for a "live" read / query Need a better name, and a symetric? Or is ReadUpdated enough if it referes to previous operation */ - ReadProgress: {isRead: true}, //ReadUpdate - ReadUpdate: {isRead: true}, //ReadUpdate + ReadProgress: DataOperationType.ReadProgress, //ReadUpdate + ReadUpdate: DataOperationType.ReadUpdate, //ReadUpdate /* ReadCancel is the operation that instructs baclkend that client isn't interested by a read operastion anymore */ - ReadCancel: {isRead: true}, + ReadCancel: DataOperationType.ReadCancel, /* ReadCanceled is the operation that instructs the client that a read operation is canceled */ - ReadCanceled: {isRead: true}, + ReadCanceled: DataOperationType.ReadCanceled, /* ReadFailed is the operation that instructs the client that a read operation has failed canceled */ - ReadFailed: {isRead: true}, + ReadFailed: DataOperationType.ReadFailed, /* ReadCompleted is the operation that instructs the client that a read operation has returned all available data */ - ReadCompleted: {isRead: true}, - Update: {isUpdate: true}, - UpdateCompleted: {isUpdate: true}, - UpdateFailed: {isUpdate: true}, - UpdateCanceled: {isUpdate: true}, - Delete: {isDelete: true}, - DeleteCompleted: {isDelete: true}, - DeleteFailed: {isDelete: true}, + ReadCompleted: DataOperationType.ReadCompleted, + Update: DataOperationType.Update, + UpdateCompleted: DataOperationType.UpdateCompleted, + UpdateFailed: DataOperationType.UpdateFailed, + UpdateCancel: DataOperationType.UpdateCancel, + UpdateCanceled: DataOperationType.UpdateCanceled, + Delete: DataOperationType.Delete, + DeleteCompleted: DataOperationType.DeleteCompleted, + DeleteFailed: DataOperationType.DeleteFailed, /* Lock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ - Lock: {isLock: true}, - LockCompleted: {isLock: true}, - LockFailed: {isLock: true}, + Lock: DataOperationType.Lock, + LockCompleted: DataOperationType.LockCompleted, + LockFailed: DataOperationType.LockFailed, /* RemmoteProcedureCall models the ability to invoke code logic on the server-side, being a DB StoredProcedure, or an method/function in a service */ - RemoteProcedureCall: {isRemoteProcedureCall: true}, - RemoteProcedureCallCompleted: {isRemoteProcedureCall: true}, - RemoteProcedureCallFailed: {isRemoteProcedureCall: true} + RemoteProcedureCall: DataOperationType.RemoteProcedureCall, + RemoteProcedureCallCompleted: DataOperationType.RemoteProcedureCallCompleted, + RemoteProcedureCallFailed: DataOperationType.RemoteProcedureCallFailed /* Batch models the ability to group multiple operation. If a referrer is provided to a BeginTransaction operation, then the batch will be executed within that transaction */ /* - Batch: {isBatch: true}, - BatchCompleted: {isBatch: true}, - BatchFailed: {isBatch: true}, + Batch: DataOperationType.Batch, + BatchCompleted: DataOperationType.BatchCompleted, + BatchFailed: DataOperationType.BatchFailed, */ /* A transaction is a unit of work that is performed against a database. Transactions are units or sequences of work accomplished in a logical order. A transactions begins, operations are grouped, then it is either commited or rolled-back*/ /* Start/End Open/Close, Commit/Save, rollback/cancel - BeginTransaction: {isTransaction: true}, - BeginTransactionCompleted: {isTransaction: true}, - BeginTransactionFailed: {isTransaction: true}, + BeginTransaction: DataOperationType.BeginTransaction, + BeginTransactionCompleted: DataOperationType.BeginTransactionCompleted, + BeginTransactionFailed: DataOperationType.BeginTransactionFailed, - CommitTransaction: {isTransaction: true}, - CommitTransactionCompleted: {isTransaction: true}, - CommitTransactionFailed: {isTransaction: true}, + CommitTransaction: DataOperationType.CommitTransaction, + CommitTransactionCompleted: DataOperationType.CommitTransactionCompleted, + CommitTransactionFailed: DataOperationType.CommitTransactionFailed, - RollbackTransaction: {isTransaction: true}, - RollbackTransactionCompleted: {isTransaction: true}, - RollbackTransactionFailed: {isTransaction: true}, + RollbackTransaction: DataOperationType.RollbackTransaction, + RollbackTransactionCompleted: DataOperationType.RollbackTransactionCompleted, + RollbackTransactionFailed: DataOperationType.RollbackTransactionFailed, */ } From 2e6d41f17f1733f08d2e0637efb5e95b4a56f09e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 15 Nov 2019 15:10:34 -0800 Subject: [PATCH 069/407] Fix a bug when a relationship foreign key is undefined --- data/converter/raw-foreign-value-to-object-converter.js | 2 +- data/service/mapping-rule.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/converter/raw-foreign-value-to-object-converter.js b/data/converter/raw-foreign-value-to-object-converter.js index adee66b5d0..3bb699e050 100644 --- a/data/converter/raw-foreign-value-to-object-converter.js +++ b/data/converter/raw-foreign-value-to-object-converter.js @@ -43,7 +43,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( }); } else { - return Promise.resolve(); + return Promise.resolve(null); } } }, diff --git a/data/service/mapping-rule.js b/data/service/mapping-rule.js index d2d890e533..5d4b8a24b1 100644 --- a/data/service/mapping-rule.js +++ b/data/service/mapping-rule.js @@ -195,7 +195,7 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { return this.converter ? this.converter.convert(value) : this.reverter ? this.reverter.revert(value) : - value; + Promise.resolve(value); } }, From 708f5b07c5232eda97a2f7ae15c20a0389e28f75 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 19 Nov 2019 00:09:05 -0800 Subject: [PATCH 070/407] - fix bug when re-using a serializer and serializing the same object multiple times - adds the ability for the labeler to deal with an identifier object that isn't a string --- .../serializer/montage-labeler.js | 7 +++--- .../serializer/montage-malker.js | 6 +++++ .../serializer/montage-serializer.js | 10 ++++++-- .../serializer/montage-visitor.js | 23 ++++++++++++------- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/core/serialization/serializer/montage-labeler.js b/core/serialization/serializer/montage-labeler.js index eaeb8933f0..3e1c464e0b 100644 --- a/core/serialization/serializer/montage-labeler.js +++ b/core/serialization/serializer/montage-labeler.js @@ -69,8 +69,9 @@ exports.MontageLabeler = Montage.specialize({ var identifier = object.identifier, objectName; - if (identifier && this._labelRegexp.test(identifier)) { - objectName = object.identifier; + if (identifier && this._labelRegexp.test((identifier = identifier.toString()))) { + //objectName = object.identifier; + objectName = identifier; } else if (object && typeof object === "object" && "getInfoForObject" in object || "getInfoForObject" in object.constructor ) { objectName = Montage.getInfoForObject(object).objectName; objectName = objectName.toLowerCase(); @@ -97,7 +98,7 @@ exports.MontageLabeler = Montage.specialize({ for (var label in labels) { if (labels.hasOwnProperty(label)) { this.setObjectLabel(labels[label], label); - this._userDefinedLabels[label] = true; + this._userDefinedLabels[label] = true; } } } diff --git a/core/serialization/serializer/montage-malker.js b/core/serialization/serializer/montage-malker.js index feabc1dcca..aee6bbf299 100644 --- a/core/serialization/serializer/montage-malker.js +++ b/core/serialization/serializer/montage-malker.js @@ -13,6 +13,12 @@ var MontageWalker = exports.MontageWalker = Montage.specialize({ } }, + cleanup: { + value: function() { + this._enteredObjects = {}; + } + }, + _isObjectEntered: { value: function(object) { return Object.hash(object) in this._enteredObjects; diff --git a/core/serialization/serializer/montage-serializer.js b/core/serialization/serializer/montage-serializer.js index e5c811910e..bde37cd758 100644 --- a/core/serialization/serializer/montage-serializer.js +++ b/core/serialization/serializer/montage-serializer.js @@ -17,7 +17,7 @@ var MontageSerializer = Montage.specialize({ _serializationIndentation: {value: 2}, _malker: { value: null }, legacyMode: { value: false }, - + _hasSerialized: { value: false }, constructor: { value: function (legacyMode) { this.legacyMode = !!legacyMode; @@ -66,9 +66,14 @@ var MontageSerializer = Montage.specialize({ value: function(objects) { var serializationString; - this._builder.init(); this._labeler.initWithObjects(objects); + if(this._hasSerialized) { + this._builder.init(); + this._malker.cleanup(); + this._visitor.cleanup(); + } + for (var label in objects) { if (Object.hasOwnProperty.call(objects, label)) { this._malker.visit(objects[label]); @@ -81,6 +86,7 @@ var MontageSerializer = Montage.specialize({ ) ); + this._hasSerialized = true; return serializationString; } }, diff --git a/core/serialization/serializer/montage-visitor.js b/core/serialization/serializer/montage-visitor.js index 0bafee6774..764cff166d 100644 --- a/core/serialization/serializer/montage-visitor.js +++ b/core/serialization/serializer/montage-visitor.js @@ -29,6 +29,13 @@ var MontageVisitor = Montage.specialize({ } }, + cleanup: { + value: function() { + this._elements = []; + this._objectsSerialization = Object.create(null); + } + }, + getTypeOf: { value: function (object) { // Module and Alias are MontageObject's too so they need to be @@ -280,7 +287,7 @@ var MontageVisitor = Montage.specialize({ var selfSerializer, substituteObject, valuesBuilderObject = this.builder.createObjectLiteral(); - + builderObject.setProperty("prototype", object.constructor.name); builderObject.setProperty("values", valuesBuilderObject); @@ -355,7 +362,7 @@ var MontageVisitor = Montage.specialize({ var valuesObject = this.builder.top.getProperty("values"); this.builder.push(valuesObject); - + /* jshint forin: true */ for (var key in bindings) { /* jshint forin: false */ @@ -381,7 +388,7 @@ var MontageVisitor = Montage.specialize({ value: function (malker, object) { var valuesSerializer, valuesObject = this.builder.top.getProperty("values"); - + this.builder.push(valuesObject); if (typeof object.serializeProperties === "function" || typeof object.serializeValues === "function") { @@ -432,8 +439,8 @@ var MontageVisitor = Montage.specialize({ // a reference to an object but that would be an external reference // the problem here is that the serializable defaults to "reference" // for most cases when in reality we probably just want "value". - return typeof value === "object" && - (value !== null && value !== undefined) && + return typeof value === "object" && + (value !== null && value !== undefined) && !(typeof Element !== "undefined" && Element.isElement(value)); } }, @@ -465,7 +472,7 @@ var MontageVisitor = Montage.specialize({ this.setObjectCustomUnit(malker, object, unitName); } - } + } } }, @@ -675,7 +682,7 @@ var MontageVisitor = Montage.specialize({ } if ( - typeof visitor[methodName] === "function" && + typeof visitor[methodName] === "function" && methodName.substr(0, 5) === "visit" ) { if (typeof customObjectVisitors[methodName] === "undefined") { @@ -683,7 +690,7 @@ var MontageVisitor = Montage.specialize({ } else { return new Error("Visitor '" + methodName + "' is already registered."); } - } + } } } From 5c43e19f0623b438f2544f81483cfa244b079f8d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 19 Nov 2019 00:09:46 -0800 Subject: [PATCH 071/407] improves DataIdentifer handling --- data/model/data-identifier.js | 22 +++++++++++++++++++++- data/service/data-operation.js | 6 ++++++ data/service/raw-data-service.js | 3 ++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/data/model/data-identifier.js b/data/model/data-identifier.js index ff6b280860..98633eef50 100644 --- a/data/model/data-identifier.js +++ b/data/model/data-identifier.js @@ -88,7 +88,27 @@ exports.DataIdentifier = Montage.specialize(/** @lends DataIdentifier.prototype }, _identifier: { - value: false + get: function() { + return this.url; + } + }, + + identifier: { + get: function() { + return this.url; + } + }, + + toString: { + value: function() { + return this.url; + } + }, + + valueOf: { + value: function() { + return this.url; + } }, _url: { diff --git a/data/service/data-operation.js b/data/service/data-operation.js index cedc56c93a..87acdc12cc 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -195,6 +195,12 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ value: undefined }, + identifier: { + get: function() { + return this.id; + } + }, + /** * This is a unique clientId (per tab), that's given by the backend to the * client's OperationService. This clientId needs then to be passed per diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index eddf8322ba..27faf5bcee 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -594,7 +594,8 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot dataIdentifier.objectDescriptor = type; dataIdentifier.dataService = this; dataIdentifier.typeName = type.name; - dataIdentifier._identifier = dataIdentifier.primaryKey = primaryKey; + //dataIdentifier._identifier = dataIdentifier.primaryKey = primaryKey; + dataIdentifier.primaryKey = primaryKey; dataIdentifierMap.set(primaryKey,dataIdentifier); } From 34ec9e168088144bddf913673bd0a1df6e6964b5 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 19 Nov 2019 21:01:16 -0800 Subject: [PATCH 072/407] - WIP: add serializationo/deserialization of objectExpressions to DataOperation, this likely belong to a subclass --- data/service/data-operation.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 87acdc12cc..79e97884ab 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -130,7 +130,9 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ if(this.data) { serializer.setProperty("data", this.data); } - + if(this.objectExpressions) { + serializer.setProperty("objectExpressions", this.objectExpressions); + } } }, deserializeSelf: { @@ -171,6 +173,11 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ this.data = value; } + value = deserializer.getProperty("objectExpressions"); + if (value !== void 0) { + this.objectExpressions = value; + } + } }, From 66fc1153db704802f8969af0ab1960101109c049 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 25 Nov 2019 22:34:20 -0800 Subject: [PATCH 073/407] rename web socket file to load it with the same symbole as the standard --- core/web-socket.js | 240 ++++++++++++++++++++++++++++++++++++++++++++ core/websocket.js | 242 +-------------------------------------------- 2 files changed, 242 insertions(+), 240 deletions(-) create mode 100644 core/web-socket.js diff --git a/core/web-socket.js b/core/web-socket.js new file mode 100644 index 0000000000..ba5585d7a0 --- /dev/null +++ b/core/web-socket.js @@ -0,0 +1,240 @@ + +var Target = require("./target").Target; + +//Todo, to run in node, we'll need to bring in something like the ws npm package. + +/* A WebSocket that offers automatic reconnection and re-send of data that couldn't if closed. */ + +var _WebSocket = global.WebSocket; + +exports.WebSocket = Target.specialize({ + + /* + The constructor can throw exceptions from inside _connect(): + + SECURITY_ERR + The port to which the connection is being attempted is being blocked. + + */ + constructor: { + value: function (url, protocols) { + this._url = url; + this._protocols = protocols; + this._messageQueue = []; + this._webSocket = null; + this._isReconnecting = false; + this._connect(); + return this; + } + }, + + _url: { + value: undefined + }, + _protocols: { + value: undefined + }, + _messageQueue: { + value: undefined + }, + _webSocket: { + value: undefined + }, + reconnectionInterval: { + value: 100 + }, + + _connect: { + value: function () { + this._webSocket = new _WebSocket(this._url, this._protocols); + this._webSocket.addEventListener("error", this, false); + this._webSocket.addEventListener("open", this, false); + } + }, + + send: { + value: function send(data) { + this._messageQueue.push(data); + this._sendNextMessage(); + } + }, + + _sendNextMessage: { + value: function () { + if (this._messageQueue.length) { + switch (this.readyState) { + case WebSocket.CONNECTING: + break; + case WebSocket.CLOSING: + case WebSocket.CLOSED: + this._reconnect(); + break; + case WebSocket.OPEN: + try { + this._webSocket.send(this._messageQueue[0]); + this._messageQueue.shift(); + } catch (e) { + this._reconnect(); + } + break; + } + } + } + }, + + _reconnect: { + value: function () { + var self; + + //if (this._messageQueue.length && !this._isReconnecting) { + if (!this._isReconnecting) { + self = this; + this._webSocket = null; + this._isReconnecting = true; + setTimeout(function () { + self._connect(); + self._isReconnecting = false; + }, Math.random() * this._reconnectionInterval); + this._reconnectionInterval *= 2; + if (this._reconnectionInterval > 30000) { + this._reconnectionInterval = 30000; + } + } + } + }, + + handleEvent: { + value: function (event) { + switch (event.type) { + case "open": + this._reconnectionInterval = 100; + if (this._webSocket) { + this._webSocket.addEventListener("message", this, false); + this._webSocket.addEventListener("close", this, false); + } + this.dispatchEvent(event); + this._sendNextMessage(); + break; + case "message": + this.dispatchEvent(event); + this._sendNextMessage(); + break; + default: + this.dispatchEvent(event); + this._reconnect(); + } + } + }, + close: { + value: function close(code, reason) { + return this._webSocket.close(code, reason); + } + }, + binaryType: { + get: function () { + return this._webSocket.binaryType; + }, + set: function (value) { + this._webSocket.binaryType = value; + } + }, + bufferedAmount: { + get: function () { + return this._webSocket.bufferedAmount; + } + }, + extensions: { + get: function () { + return this._webSocket.extensions; + }, + set: function (value) { + this._webSocket.extensions = value; + } + }, + protocol: { + get: function () { + return this._webSocket.protocol; + } + }, + readyState: { + get: function () { + return this._webSocket ? this._webSocket.readyState : WebSocket.CLOSED; + } + }, + + //Events Handlers + _onclose: { + value: undefined + }, + onclose: { + get: function () { + return this._onclose; + }, + set: function (eventListener) { + if(eventListener !== this._onclose) { + this.removeEventListener("close", this._onclose, false); + this._onclose = eventListener; + } + this.addEventListener("close", eventListener, false); + } + }, + _onerror: { + value: undefined + }, + onerror: { + get: function () { + return this._onerror; + }, + set: function (eventListener) { + if(eventListener !== this._onerror) { + this.removeEventListener("error", this._onerror, false); + this._onerror = eventListener; + } + this.addEventListener("error", eventListener, false); + } + }, + _onmessage: { + value: undefined + }, + onmessage: { + get: function () { + return this._onmessage; + }, + set: function (eventListener) { + if(eventListener !== this._onmessage) { + this.removeEventListener("message", this._onmessage, false); + this._onmessage = eventListener; + } + this.addEventListener("message", eventListener, false); + } + }, + _onopen: { + value: undefined + }, + onopen: { + get: function () { + return this._onopen; + }, + set: function (eventListener) { + if(eventListener !== this._onopen) { + this.removeEventListener("open", this._onopen, false); + this._onopen = eventListener; + } + this.addEventListener("open", eventListener, false); + } + } + +},{ + CONNECTING: { + value: 0 //The connection is not yet open. + }, + OPEN: { + value: 1 //The connection is open and ready to communicate. + }, + CLOSING: { + value: 2 //The connection is in the process of closing. + }, + CLOSED: { + value: 3 //The connection is closed or couldn't be opened. + } +}); diff --git a/core/websocket.js b/core/websocket.js index ba5585d7a0..b311017f74 100644 --- a/core/websocket.js +++ b/core/websocket.js @@ -1,240 +1,2 @@ - -var Target = require("./target").Target; - -//Todo, to run in node, we'll need to bring in something like the ws npm package. - -/* A WebSocket that offers automatic reconnection and re-send of data that couldn't if closed. */ - -var _WebSocket = global.WebSocket; - -exports.WebSocket = Target.specialize({ - - /* - The constructor can throw exceptions from inside _connect(): - - SECURITY_ERR - The port to which the connection is being attempted is being blocked. - - */ - constructor: { - value: function (url, protocols) { - this._url = url; - this._protocols = protocols; - this._messageQueue = []; - this._webSocket = null; - this._isReconnecting = false; - this._connect(); - return this; - } - }, - - _url: { - value: undefined - }, - _protocols: { - value: undefined - }, - _messageQueue: { - value: undefined - }, - _webSocket: { - value: undefined - }, - reconnectionInterval: { - value: 100 - }, - - _connect: { - value: function () { - this._webSocket = new _WebSocket(this._url, this._protocols); - this._webSocket.addEventListener("error", this, false); - this._webSocket.addEventListener("open", this, false); - } - }, - - send: { - value: function send(data) { - this._messageQueue.push(data); - this._sendNextMessage(); - } - }, - - _sendNextMessage: { - value: function () { - if (this._messageQueue.length) { - switch (this.readyState) { - case WebSocket.CONNECTING: - break; - case WebSocket.CLOSING: - case WebSocket.CLOSED: - this._reconnect(); - break; - case WebSocket.OPEN: - try { - this._webSocket.send(this._messageQueue[0]); - this._messageQueue.shift(); - } catch (e) { - this._reconnect(); - } - break; - } - } - } - }, - - _reconnect: { - value: function () { - var self; - - //if (this._messageQueue.length && !this._isReconnecting) { - if (!this._isReconnecting) { - self = this; - this._webSocket = null; - this._isReconnecting = true; - setTimeout(function () { - self._connect(); - self._isReconnecting = false; - }, Math.random() * this._reconnectionInterval); - this._reconnectionInterval *= 2; - if (this._reconnectionInterval > 30000) { - this._reconnectionInterval = 30000; - } - } - } - }, - - handleEvent: { - value: function (event) { - switch (event.type) { - case "open": - this._reconnectionInterval = 100; - if (this._webSocket) { - this._webSocket.addEventListener("message", this, false); - this._webSocket.addEventListener("close", this, false); - } - this.dispatchEvent(event); - this._sendNextMessage(); - break; - case "message": - this.dispatchEvent(event); - this._sendNextMessage(); - break; - default: - this.dispatchEvent(event); - this._reconnect(); - } - } - }, - close: { - value: function close(code, reason) { - return this._webSocket.close(code, reason); - } - }, - binaryType: { - get: function () { - return this._webSocket.binaryType; - }, - set: function (value) { - this._webSocket.binaryType = value; - } - }, - bufferedAmount: { - get: function () { - return this._webSocket.bufferedAmount; - } - }, - extensions: { - get: function () { - return this._webSocket.extensions; - }, - set: function (value) { - this._webSocket.extensions = value; - } - }, - protocol: { - get: function () { - return this._webSocket.protocol; - } - }, - readyState: { - get: function () { - return this._webSocket ? this._webSocket.readyState : WebSocket.CLOSED; - } - }, - - //Events Handlers - _onclose: { - value: undefined - }, - onclose: { - get: function () { - return this._onclose; - }, - set: function (eventListener) { - if(eventListener !== this._onclose) { - this.removeEventListener("close", this._onclose, false); - this._onclose = eventListener; - } - this.addEventListener("close", eventListener, false); - } - }, - _onerror: { - value: undefined - }, - onerror: { - get: function () { - return this._onerror; - }, - set: function (eventListener) { - if(eventListener !== this._onerror) { - this.removeEventListener("error", this._onerror, false); - this._onerror = eventListener; - } - this.addEventListener("error", eventListener, false); - } - }, - _onmessage: { - value: undefined - }, - onmessage: { - get: function () { - return this._onmessage; - }, - set: function (eventListener) { - if(eventListener !== this._onmessage) { - this.removeEventListener("message", this._onmessage, false); - this._onmessage = eventListener; - } - this.addEventListener("message", eventListener, false); - } - }, - _onopen: { - value: undefined - }, - onopen: { - get: function () { - return this._onopen; - }, - set: function (eventListener) { - if(eventListener !== this._onopen) { - this.removeEventListener("open", this._onopen, false); - this._onopen = eventListener; - } - this.addEventListener("open", eventListener, false); - } - } - -},{ - CONNECTING: { - value: 0 //The connection is not yet open. - }, - OPEN: { - value: 1 //The connection is open and ready to communicate. - }, - CLOSING: { - value: 2 //The connection is in the process of closing. - }, - CLOSED: { - value: 3 //The connection is closed or couldn't be opened. - } -}); +var WebSocket = require("./web-socket").WebSocket; +exports.WebSocket = WebSocket; From b4b7a0a2df7e0f6bc8e3276a9c81380c95a68971 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 25 Nov 2019 22:35:01 -0800 Subject: [PATCH 074/407] improves convert method robustness --- core/converter/date-converter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/converter/date-converter.js b/core/converter/date-converter.js index d20fecb5e3..73b99d9cb0 100644 --- a/core/converter/date-converter.js +++ b/core/converter/date-converter.js @@ -1967,8 +1967,8 @@ var Montage = require("../core").Montage, } var expression = !!(this.days && this.days !== null || this.orient || this.operator); - var temp; - + var temp; + var gap, mod, orient; orient = ((this.orient === "past" || this.operator === "subtract") ? -1 : 1); @@ -2561,7 +2561,7 @@ var DateConverter = exports.DateConverter = Converter.specialize(/** @lends Date convert: { value: function (v) { var t = typeof v; - if (isDate(v) || t === "string" || t === "number") { + if (isDate(v) || (t === "string" && v.length) || t === "number") { return formatDate(v, this.pattern); } return v; From f66151aef396f4687fadfa13573b8d4bc8464f8a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 25 Nov 2019 22:37:59 -0800 Subject: [PATCH 075/407] add isSearcheable, isOrdered, hasUniqueValues to property descriptor to support more complex use cases of validation and schema creation --- core/meta/property-descriptor.js | 68 +++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index d0cfbc479c..59585e3798 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -19,7 +19,10 @@ var Defaults = { enumValues: [], defaultValue: void 0, helpKey: "", - localizable: false + localizable: false, + isSearcheable: false, + isOrdered: false, + hasUniqueValues: false, }; @@ -138,6 +141,10 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._setPropertyWithDefaults(serializer, "definition", this.definition); this._setPropertyWithDefaults(serializer, "inversePropertyName", this.inversePropertyName); this._setPropertyWithDefaults(serializer, "localizable", this.localizable); + this._setPropertyWithDefaults(serializer, "isSearcheable", this.isSearcheable); + this._setPropertyWithDefaults(serializer, "isOrdered", this.isOrdered); + this._setPropertyWithDefaults(serializer, "hasUniqueValues", this.hasUniqueValues); + } }, @@ -173,6 +180,9 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._overridePropertyWithDefaults(deserializer, "definition"); this._overridePropertyWithDefaults(deserializer, "inversePropertyName"); this._overridePropertyWithDefaults(deserializer, "localizable"); + this._overridePropertyWithDefaults(deserializer, "isSearcheable"); + this._overridePropertyWithDefaults(deserializer, "isOrdered"); + this._overridePropertyWithDefaults(deserializer, "hasUniqueValues"); } }, @@ -283,6 +293,11 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * stored. Only positive values are legal. A value of infinity means that * any number of values can be stored. * + * Right now with just one property forHandling Cardinality, we can't deal with something like + * minCount and maxCount. minCount and maxCount with equal value would be similar to cardinality, + * or we could make cardinality a Range as well. + * + * * @type {number} * @default 1 */ @@ -336,6 +351,53 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# } }, + /** + * Reflect if this property can be used to search with text and find that + * objetc. It should be used by data storage to provide the ability to offer + * such ability to find objects based on textual content of this property. + * + * Should it be limited to text/string type? + * + * @type {boolean} + * @default false + */ + isSearcheable: { + value: Defaults.readOnly + }, + + /** + * models if the values of a collection / to-many property are ordered or not + * This is relevant for example for relationships where a relational DB would + * use a traditinal join table with 2 foreign keys to implement a many to many + * if isOrdered is false, but if true, that join table should have an index, + * or the relationship could be implemented with an array type in postgresql. + * + * If not ordered, a set could be used to hold data. + * + * @type {boolean} + * @default false + */ + isOrdered: { + value: Defaults.isOrdered + }, + + /** + * models if the values of a collection / to-many property should be unique, + * This is relevant for example for relationships where a relational DB would + * use a traditinal join table with 2 foreign keys to implement a many to many, + * in which naturally there could be only one association and therefore be unique + * by nature. But a relationship implemented with an array type in postgresql for example, + * would naturally offer the ability to hold multiple times the same value at + * multiple indexes. + * + * @type {boolean} + * @default false + */ + hasUniqueValues: { + value: Defaults.hasUniqueValues + }, + + /** * @type {string} * Definition can be used to express a property as the result of evaluating an expression @@ -458,7 +520,9 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# /** * @type {boolean} - * Express the fact that the value of this property might change to meet the language, cultural and other requirements of a specific target market (a locale). + * Express the fact that the value of this property might change to meet the language, + * cultural and other requirements of a specific target market (a locale). + * * @default false */ localizable: { From 78095428920bd2e382e5d7d0550e79f1e084eee2 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 25 Nov 2019 22:42:16 -0800 Subject: [PATCH 076/407] Add a quick fix to be able to map partial/incremental raw data to an existing object which can happen when lazily fetching properties as well as server-pushed updates to objects. this for now involves passing an extra argument to mapRawDataToObject to indicate that it isUpdateToExistingObject --- data/service/expression-data-mapping.js | 39 +++++++++++++++++++++---- data/service/raw-data-service.js | 19 +++++++++--- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 8973cfbad6..dab5bf9bc2 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -457,15 +457,36 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData * modified to represent the raw data. */ mapRawDataToObject: { - value: function (data, object, context) { + value: function (data, object, context, isUpdateToExistingObject) { var iterator = this.requisitePropertyNames.values(), promises, propertyName, result; - if (this.requisitePropertyNames.size) { - while ((propertyName = iterator.next().value)) { - result = this.mapRawDataToObjectProperty(data, object, propertyName, context); - if (this._isAsync(result)) { - (promises || (promises = [])).push(result); + if(isUpdateToExistingObject) { + var dataKeys = Object.keys(data), + i, countI = dataKeys.length, + rawDataMappingRules = this.rawDataMappingRules, + objectMappingRules = this.objectMappingRules, + + iKey, iRule; + for(i=0;i Date: Mon, 25 Nov 2019 22:44:51 -0800 Subject: [PATCH 077/407] Fix a bug in handleOrganizedContentRangeChange that impacts every listeners dispatched after handleOrganizedContentRangeChange because it modifies the passed argument. --- core/range-controller.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/core/range-controller.js b/core/range-controller.js index e59482dc84..93c44ed96f 100644 --- a/core/range-controller.js +++ b/core/range-controller.js @@ -709,13 +709,15 @@ var RangeController = exports.RangeController = Montage.specialize( /** @lends R handleContentRangeChange: { value: function (plus, minus, index) { if (this.selection.length > 0) { - var equals = this.content && this.content.contentEquals || Object.is; + var equals = this.content && this.content.contentEquals || Object.is, + diff = minus.clone(1); + // remove all values from the selection that were removed (but // not added back) - minus.deleteEach(plus, equals); + diff.deleteEach(plus, equals); if (this.selection.length) { - this.selection.deleteEach(minus); + this.selection.deleteEach(diff); // ensure selection always has content if (this.selection.length === 0 && this.content && this.content.length && @@ -772,7 +774,11 @@ var RangeController = exports.RangeController = Montage.specialize( /** @lends R if (this.deselectInvisibleContent && this.selection) { var diff = minus.clone(1); diff.deleteEach(plus); - this.selection.deleteEach(minus); + //Benoit, checking along with fixing a bug with the same pattern in handleContentRangeChange + //This feels wrong we would clone minus to remove what's re-added and still pass minus to + //this.selection.deleteEach instead of diff here. + //this.selection.deleteEach(minus); + this.selection.deleteEach(diff); } } }, From 6e24d4fbd65d37612d93b0f90ba45f4c3a48a4cf Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 19 Dec 2019 16:21:54 -0800 Subject: [PATCH 078/407] add automatic loading of "data/main.datareel/main.mjson" --- montage.js | 54 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/montage.js b/montage.js index 66b145ee9a..c06f7d447b 100644 --- a/montage.js +++ b/montage.js @@ -374,8 +374,10 @@ global.montageWillLoad(); } - // Load the application + + + // Load the application var appProto = applicationRequire.packageDescription.applicationPrototype, applicationLocation, appModulePromise; @@ -392,19 +394,47 @@ defaultEventManager.application = application; application.eventManager = defaultEventManager; - return application._load(applicationRequire, function () { - if (params.module) { - // If a module was specified in the config then we initialize it now - applicationRequire.async(params.module); - } - if (typeof global.montageDidLoad === "function") { - global.montageDidLoad(); - } + // Load main.datareel/main.mjson + var mainDatareel = applicationRequire.packageDescription.mainDatareel, + mainDatareelLocation, mainDatareelModulePromise; + + if (mainDatareel) { + mainDatareelLocation = MontageReviver.parseObjectLocationId(mainDatareel); + mainDatareelModulePromise = applicationRequire.async(mainDatareelLocation.moduleId); + } else { + //mainDatareelModulePromise = applicationRequire.makeRequire("data/main.datareel/main.mjson").async("data/main.datareel/main.mjson"); + mainDatareelLocation = "data/main.datareel/main.mjson"; + mainDatareelModulePromise = applicationRequire.async("data/main.datareel/main.mjson"); + //mainDatareelModulePromise = mrPromise.resolve(); + } + + + return mainDatareelModulePromise.then(function(mainDataServiceExport) { + // fulfillment + application.service = application.dataService = application.mainService = mainDataServiceExport.montageObject; + return application; + }, function(reason) { + // rejection + console.log("App failed to load datareel at location:",mainDatareelLocation,reason); + return application; + }).finally(function() { + + return application._load(applicationRequire, function () { + if (params.module) { + // If a module was specified in the config then we initialize it now + applicationRequire.async(params.module); + } + if (typeof global.montageDidLoad === "function") { + global.montageDidLoad(); + } + + if (window.MontageElement) { + MontageElement.ready(applicationRequire, application, MontageReviver); + } + }); - if (window.MontageElement) { - MontageElement.ready(applicationRequire, application, MontageReviver); - } }); + }); }); } From cb6688c158acc3e02fb1697a64834a2ba2f7e312 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 19 Dec 2019 16:26:10 -0800 Subject: [PATCH 079/407] Add the ability for members with string value to still have an intValue --- core/enum.js | 46 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/core/enum.js b/core/enum.js index f0f2e21896..32d64fe86b 100644 --- a/core/enum.js +++ b/core/enum.js @@ -32,6 +32,38 @@ exports.Enum = Montage.specialize( /** @lends Enum# */ { } }, + __membersByValue: { + value: null + }, + + _membersByValue: { + get: function () { + return this.__membersByValue || (this.__membersByValue = []); + } + }, + + memberWithIntValue: { + value: function(intValue) { + return this[this._membersByValue[intValue]]; + } + }, + + __membersIntValue: { + value: null + }, + + _membersIntValue: { + get: function () { + return this.__membersIntValue || (this.__membersIntValue = new Map()); + } + }, + + intValueForMember: { + value: function(member) { + return this._membersIntValue.get(member); + } + }, + /** * @function * @returns itself @@ -74,7 +106,7 @@ exports.Enum = Montage.specialize( /** @lends Enum# */ { return this; } - this._addMembers(members, values); + this._addMembers(members, values, this._membersByValue, this._membersIntValue); } else { throw new Error("the number of members must equal to the number of values"); @@ -92,14 +124,18 @@ exports.Enum = Montage.specialize( /** @lends Enum# */ { * @param value */ addMember : { - value: function (member, value) { + value: function (member, value, /* private */ _membersByValue, _membersIntValue) { if (typeof this[member] === "undefined") { + var intValue = this._value++; Object.defineProperty(this, member, { writable: false, configurable: false, enumerable: true, - value: value !== void 0 && value !== null ? value : this._value++ + value: value !== void 0 && value !== null ? value : intValue }); + + (_membersByValue || this._membersByValue)[intValue] = member; + (_membersIntValue || this._membersIntValue).set(member, intValue); } } }, @@ -131,7 +167,7 @@ exports.Enum = Montage.specialize( /** @lends Enum# */ { _addMembers: { value: function (members, values) { - var member, i, value; + var member, i, value, membersByValues = this.membersByValues; for (i = 0; typeof (member = members[i]) !== "undefined"; i++) { if (member !== null) { @@ -143,7 +179,7 @@ exports.Enum = Montage.specialize( /** @lends Enum# */ { } } - this.addMember(member, value); + this.addMember(member, value, this._membersByValue, this._membersIntValue); } else { logger.error(this, "Member at index " + i + " is null"); } From 2e3cd7880b096ea11b0c33acb41ac8724fbd3ca4 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 19 Dec 2019 16:27:41 -0800 Subject: [PATCH 080/407] Fix a few bugs and add missing event type info related to wether they bubble or not --- core/event/event-manager.js | 71 +++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/core/event/event-manager.js b/core/event/event-manager.js index e4aef1c155..fdddbf20f6 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -15,7 +15,7 @@ */ var Montage = require("../core").Montage, - MutableEvent = require("./mutable-event").MutableEvent, + MutableEvent = require("./mutable-event").MutableEvent, Serializer = require("../serialization/serializer/montage-serializer").MontageSerializer, Deserializer = require("../serialization/deserializer/montage-deserializer").MontageDeserializer, Map = require("collections/map"), @@ -292,6 +292,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan this._elementEventHandlerByElement = new WeakMap(); this.environment = currentEnvironment; this._trackingTouchTimeoutIDs = new Map(); + this._functionType = "function"; return this; } }, @@ -361,6 +362,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan focusin: {bubbles: true, cancelable: false}, //DOM3 focusout: {bubbles: true, cancelable: false}, //DOM3 input: {bubbles: true, cancelable: false}, // INPUT + invalid: {bubbles: false, cancelable: true}, // INPUT keydown: {bubbles: true, cancelable: false}, //DOM3 keypress: {bubbles: true, cancelable: false}, //DOM3 keyup: {bubbles: true, cancelable: false}, //DOM3 @@ -376,6 +378,9 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan mouseover: {bubbles: true, cancelable: true}, //DOM3 mouseup: {bubbles: true, cancelable: true}, //DOM3 mousewheel: {bubbles: true}, + offline: {bubbles: false, cancelable: false}, //DOM3 + online: {bubbles: false, cancelable: false}, //DOM3 + open: {bubbles: false, cancelable: false}, //DOM3 orientationchange: {bubbles: false}, paste: {bubbles: true, cancelable: true}, //ClipboardEvent progress: {bubbles: false, cancelable: false}, //ProgressEvent @@ -392,6 +397,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan select: {bubbles: true, cancelable: false}, //DOM2, DOM3 submit: {bubbles: true, cancelable: true}, //DOM2 + timeout: {bubbles: false, cancelable: false}, //DOM2 touchcancel: {bubbles: true, cancelable: false}, //TouchEvent touchend: {bubbles: true, cancelable: true}, //TouchEvent touchmove: {bubbles: true, cancelable: true}, //TouchEvent @@ -554,7 +560,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan if (aWindow.EventTarget) { aWindow.EventTarget.prototype.nativeAddEventListener = aWindow.EventTarget.prototype.addEventListener; } - + aWindow.Element.prototype.nativeAddEventListener = aWindow.Element.prototype.addEventListener; Object.defineProperty(aWindow, "nativeAddEventListener", { configurable: true, @@ -759,7 +765,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan if (aWindow.EventTarget) { aWindow.EventTarget.prototype.removeEventListener = aWindow.EventTarget.prototype.nativeRemoveEventListener; } - + aWindow.Element.prototype.removeEventListener = aWindow.Element.prototype.nativeRemoveEventListener; Object.defineProperty(aWindow, "removeEventListener", { configurable: true, @@ -801,7 +807,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan if (aWindow.EventTarget) { delete aWindow.EventTarget.prototype.nativeAddEventListener; } - + delete aWindow.Element.prototype.nativeAddEventListener; delete aWindow.nativeAddEventListener; @@ -1277,10 +1283,18 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan } this._observedTarget_byEventType_[eventType].set(listenerTarget,this); - var isPassiveEventType = this.isPassiveEventType(eventType), - eventOpts = isPassiveEventType ? { - passive: true - } : true; + var eventDefinitions = this.eventDefinitions[eventType], + eventOpts; + + if(eventDefinitions) { + console.debug("Event type "+eventType+" missed definition"); + } + + eventOpts = this.isPassiveEventType(eventType) + ? {passive: true} + : eventDefinitions + ? eventDefinitions.bubbles + : true; //by default listenerTarget.nativeAddEventListener(eventType, this, eventOpts); } @@ -1778,7 +1792,10 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan storeEvent: function (mutableEvent) { var isBrowserSupportPointerEvents = currentEnvironment.isBrowserSupportPointerEvents, event = mutableEvent instanceof MutableEvent ? mutableEvent._event : mutableEvent, - pointerType = event.pointerType; + pointerType = event + ? event.pointerType + : null; + if(!pointerType) return; if ((isBrowserSupportPointerEvents && (pointerType === "mouse" || (window.MSPointerEvent && pointerType === window.MSPointerEvent.MSPOINTER_TYPE_MOUSE))) || @@ -2694,19 +2711,29 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan */ _invokeTargetListenerForEvent: { value: function _invokeTargetListenerForEvent(iTarget, jListener, mutableEvent, identifierSpecificPhaseMethodName, phaseMethodName) { - var functionType = "function"; - if (typeof jListener === functionType) { - jListener.call(iTarget, mutableEvent); - } - else if (identifierSpecificPhaseMethodName && typeof jListener[identifierSpecificPhaseMethodName] === functionType) { - jListener[identifierSpecificPhaseMethodName](mutableEvent); - } - else if (typeof jListener[phaseMethodName] === functionType) { - jListener[phaseMethodName](mutableEvent); - } - else if (typeof jListener.handleEvent === functionType) { - jListener.handleEvent(mutableEvent); - } + // var functionType = "function"; + // if (typeof jListener === functionType) { + // jListener.call(iTarget, mutableEvent); + // } + // else if (identifierSpecificPhaseMethodName && typeof jListener[identifierSpecificPhaseMethodName] === functionType) { + // jListener[identifierSpecificPhaseMethodName](mutableEvent); + // } + // else if (typeof jListener[phaseMethodName] === functionType) { + // jListener[phaseMethodName](mutableEvent); + // } + // else if (typeof jListener.handleEvent === functionType) { + // jListener.handleEvent(mutableEvent); + // } + + (identifierSpecificPhaseMethodName && typeof jListener[identifierSpecificPhaseMethodName] === this._functionType) + ? jListener[identifierSpecificPhaseMethodName](mutableEvent) + : (typeof jListener[phaseMethodName] === this._functionType) + ? jListener[phaseMethodName](mutableEvent) + : (typeof jListener.handleEvent === this._functionType) + ? jListener.handleEvent(mutableEvent) + : (typeof jListener === this._functionType) + ? jListener.call(iTarget, mutableEvent) + : void 0; } }, From cb44f0411acf2f6f512c3bde0331bb57d5207f38 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 19 Dec 2019 16:28:23 -0800 Subject: [PATCH 081/407] Make it work in node and without wrapping an event --- core/event/mutable-event.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/core/event/mutable-event.js b/core/event/mutable-event.js index d11d4a5150..79a3fb2380 100644 --- a/core/event/mutable-event.js +++ b/core/event/mutable-event.js @@ -16,7 +16,7 @@ var wrapPropertyGetter = function (key, storageKey) { // XXX Does not presently function server-side -if (typeof window !== "undefined") { +//if (typeof window !== "undefined") { var _eventConstructorsByType = {}; @@ -86,7 +86,7 @@ if (typeof window !== "undefined") { }, /** - * @function + * @function - deprecated */ getPreventDefault: { value: function () { @@ -97,6 +97,14 @@ if (typeof window !== "undefined") { } }, + defaultPrevented: { + value: function () { + return (typeof this._event.defaultPrevented === "boolean") + ? this._event.defaultPrevented + : this.getPreventDefault(); + } + }, + /** * @function */ @@ -335,4 +343,4 @@ if (typeof window !== "undefined") { }); -} // client-side +//} // client-side From 3a1bd04a361e5b3b80953937f210d457f0aec4f8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 19 Dec 2019 16:28:59 -0800 Subject: [PATCH 082/407] Add toCamelCase polyfill --- core/extras/string.js | 57 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/core/extras/string.js b/core/extras/string.js index 58eb57c0ec..d2765cadde 100644 --- a/core/extras/string.js +++ b/core/extras/string.js @@ -58,3 +58,60 @@ Object.defineProperty(String.prototype, "toCapitalized", { configurable: true }); String.prototype.toCapitalized.cache = new Map(); + + + +if (!String.prototype.toCamelCase) { + function _toCamelCase (sring, cache, isLower) { + var trimmed = sring.trim(), + camelCase = cache[trimmed] || ''; + + if (!camelCase && trimmed.length) { + if ((!isLower && /[^A-Z]/.test(trimmed[0])) || /\.|_|-|\s/.test(trimmed)) { + var data = trimmed.split(/\.|_|-|\s/), + str; + + for (var i = 0, length = data.length; i < length; i++) { + str = data[i]; + + if (str) { + if (isLower && i === 0) { + camelCase += str; + } else { + camelCase += str.toCapitalized(); + } + } + } + + cache[trimmed] = camelCase; + + } else { // already camelCase + camelCase = cache[trimmed] = trimmed; + } + } + + return camelCase; + } + + + Object.defineProperty(String.prototype, 'toCamelCase', { + value: function toCamelCase() { + return _toCamelCase(this, toCamelCase.cache); + }, + writable: true, + configurable: true + }); + + String.prototype.toCamelCase.cache = Object.create(null); + + + Object.defineProperty(String.prototype, 'toLowerCamelCase', { + value: function toLowerCamelCase () { + return _toCamelCase(this, toLowerCamelCase.cache, true); + }, + writable: true, + configurable: true + }); + + String.prototype.toLowerCamelCase.cache = Object.create(null); +} From c32176de83d45d5c871a3da2e0a466d0062decb9 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 19 Dec 2019 16:30:01 -0800 Subject: [PATCH 083/407] Make it specialize Target instead of Montage to be able to dispatch events --- core/meta/object-descriptor.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/meta/object-descriptor.js b/core/meta/object-descriptor.js index 7c18b5130e..04d5918ce3 100644 --- a/core/meta/object-descriptor.js +++ b/core/meta/object-descriptor.js @@ -1,4 +1,5 @@ var Montage = require("../core").Montage, + Target = require("core/target").Target, DerivedDescriptor = require("./derived-descriptor").DerivedDescriptor, EventDescriptor = require("./event-descriptor").EventDescriptor, Map = require("collections/map"), @@ -17,9 +18,9 @@ var Defaults = { /** * @class ObjectDescriptor - * @extends Montage + * @extends Target */ -var ObjectDescriptor = exports.ObjectDescriptor = Montage.specialize( /** @lends ObjectDescriptor.prototype # */ { +var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends ObjectDescriptor.prototype # */ { FileExtension: { value: ".mjson" @@ -252,7 +253,7 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.specialize( /** @lends ); */ } else { - var parentInstancePrototype = (this.parent ? this.parent.newInstancePrototype() : Montage ); + var parentInstancePrototype = (this.parent ? this.parent.newInstancePrototype() : Target ); var newConstructor = parentInstancePrototype.specialize({ // Token class init: { From e30b744c7bc48527052420fe50952efd6af2675a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 19 Dec 2019 16:31:31 -0800 Subject: [PATCH 084/407] add property to carry userIdentity, intented to be set by the framework and meant for RawDataServices via DataOperations --- data/model/data-query.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/data/model/data-query.js b/data/model/data-query.js index 46c3114d69..ca5c51b037 100644 --- a/data/model/data-query.js +++ b/data/model/data-query.js @@ -95,6 +95,20 @@ exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { value: undefined }, + + /** + * A property used to carry the identity of the current user issuing the query + * This is set by the framework based on wethere there's an authenticated user + * or not. RawDataServices use this to communicate to servers the identity of + * who is requiring data so it can be evaluated in term of access contol. + * + * @type {UserIdentity} + */ + userIdentity: { + value: undefined + }, + + /** * An object defining the criteria that must be satisfied by objects for * them to be included in the data set defined by this query. @@ -201,7 +215,7 @@ exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { /** * An expression that is used in memory client side to further refine the set objects retrieves. * by the query's criteria expression. This useful in cases the origin service doesn't know how to handle such criteria. - * That shouldn't be exposed to the end developer, but instead, a RawDataService should be able to analyse a query's criteria + * That shouldn't be exposed to the end developer, but instead, a RawDataService should be able to analyze a query's criteria * and split the apsects that can be executed by the origin service automatically, to filter the rest itself. * @type {Array} */ From 18beb9b79f8d33240bc399f6ebe44c5fa9ce2820 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 19 Dec 2019 16:43:42 -0800 Subject: [PATCH 085/407] - Introduce UserIdentity, UserIdentyService, UserIdentityManager, AuthenticationManagerPanel, AuthenticationPanel - Make DataOperation Events - Generalize RawData mapping to be more data driven --- core/application.js | 118 ++++++- data/service/data-operation.js | 306 +++++++++++------- data/service/data-service.js | 305 ++++++++++++++--- data/service/expression-data-mapping.js | 211 ++++++++++-- data/service/raw-data-service.js | 175 +++++++--- data/service/user-authentication-policy.js | 34 ++ data/service/user-identity-manager.js | 155 +++++++++ data/service/user-identity-service.js | 35 ++ .../authentication-manager-panel.css | 0 .../authentication-manager-panel.html | 37 +++ .../authentication-manager-panel.js | 194 +++++++++++ ui/authentication-panel.js | 55 ++++ 12 files changed, 1382 insertions(+), 243 deletions(-) create mode 100644 data/service/user-authentication-policy.js create mode 100644 data/service/user-identity-manager.js create mode 100644 data/service/user-identity-service.js create mode 100644 ui/authentication-manager-panel.reel/authentication-manager-panel.css create mode 100644 ui/authentication-manager-panel.reel/authentication-manager-panel.html create mode 100644 ui/authentication-manager-panel.reel/authentication-manager-panel.js create mode 100644 ui/authentication-panel.js diff --git a/core/application.js b/core/application.js index 89bdacc76c..a182db57eb 100644 --- a/core/application.js +++ b/core/application.js @@ -13,6 +13,11 @@ var Target = require("./target").Target, Template = require("./template"), MontageWindow = require("../window-loader/montage-window").MontageWindow, + DataStream = require("data/service/data-stream").DataStream, + Criteria = require("core/criteria").Criteria, + DataQuery = require("data/model/data-query").DataQuery, + UserIdentityService = require("data/service/user-identity-service").UserIdentityService, + UserIdentityManager = require("data/service/user-identity-manager").UserIdentityManager, Slot; require("./dom"); @@ -428,6 +433,8 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio ) { this.parentApplication = window.loadInfo.parent.document.application; } + + UserIdentityManager.delegate = this; } }, @@ -443,14 +450,82 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio exports.application = self; return require.async("ui/component").then(function(exports) { + var authorizationPromise; + + self.rootComponent = rootComponent = exports.__root__; + if (typeof document !== "undefined") { + rootComponent.element = document; + } + + + /* + + TODO!!! Modify loading sequence to combine loader -> Authorization Panel -> Main + + While bringing the login panel up when there's an UpfrontAuthorizationPolicy before loading Main + makes sense from both security and performance stand point, we shouldn't be skipping the loader. + + We should deserialize the loader, set the authentication panel as loader's Main, show the AuthenticationManager panel, + and then bring the Main in + */ - rootComponent = exports.__root__; + //URGENT: We need to further test that we don't already have a valid Authorization to use before authenticating. + + + var userIdentityServices = UserIdentityService.userIdentityServices, + userIdentityObjectDescriptors, + authenticationPromise, + // userObjectDescriptor = this. + selfUserCriteria, + userIdentityQuery; + + + if(userIdentityServices && userIdentityServices.length > 0) { + //Shortcut, there could be multiple one we need to flatten. + userIdentityObjectDescriptors = userIdentityServices[0].types; + + if(userIdentityObjectDescriptors.length > 0) { + //selfUserCriteria = new Criteria().initWithExpression("identity == $", "self"); + userIdentityQuery = DataQuery.withTypeAndCriteria(userIdentityObjectDescriptors[0]); + + authenticationPromise = self.mainService.fetchData(userIdentityQuery) + .then(function(userIdenties) { + self.user = userIdenties[0]; + }); + + } + } + else { + authenticationPromise = Promise.resolve(true); + } + + return authenticationPromise.finally(function() { + // if (typeof document !== "undefined") { + // rootComponent.element = document; + // } + + if (typeof document !== "undefined") { + return Template.instantiateDocument(document, applicationRequire); + } + + }); + + +/* if (typeof document !== "undefined") { rootComponent.element = document; - return Template.instantiateDocument(document, applicationRequire); } + return authorizationPromise.then(function(authorization) { + if (typeof document !== "undefined") { + return Template.instantiateDocument(document, applicationRequire); + } + }, function(error) { + console.error(error); + }); + */ + }).then(function (part) { self.callDelegateMethod("willFinishLoading", self); rootComponent.needsDraw = true; @@ -462,6 +537,8 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio } }, + //This should be replaced by a more robust user / session system with opt-in/delegate/configured + //from the outside. _loadApplicationContext: { value: function () { if (this._isFirstLoad === null) { @@ -471,7 +548,7 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio if (typeof localStorage !== "undefined") { localStorage.getItem(alreadyLoadedLocalStorageKey); - + if (hasAlreadyBeenLoaded === null) { try { localStorage.setItem(alreadyLoadedLocalStorageKey, true); @@ -513,15 +590,17 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio /** * @private */ - _createPopupSlot: {value: function (zIndex) { + _createPopupSlot: {value: function (zIndex, className) { var slotEl = document.createElement('div'); document.body.appendChild(slotEl); slotEl.style.zIndex = zIndex; slotEl.style.position = 'absolute'; + slotEl.classList.add(className); var popupSlot = new Slot(); + popupSlot.delegate = this; popupSlot.element = slotEl; - popupSlot.attachToParentComponent(); + //popupSlot.attachToParentComponent(); return popupSlot; }}, @@ -533,19 +612,22 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio .then(function (exports) { Slot = Slot || exports.Slot; type = type || "custom"; - var isSystemPopup = self._isSystemPopup(type), zIndex, popupSlot; + var isSystemPopup = self._isSystemPopup(type), zIndex, popupSlot, className; self.popupSlots = self.popupSlots || {}; if(isSystemPopup) { switch (type) { case "alert": zIndex = 19004; + className = "montage-alert"; break; case "confirm": zIndex = 19003; + className = "montage-confirm"; break; case "notify": zIndex = 19002; + className = "montage-notify"; break; } } else { @@ -556,15 +638,24 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio self._zIndex = self._zIndex + 1; } zIndex = self._zIndex; + className = self.name; + className += "-"; + className += type; } popupSlot = self.popupSlots[type]; if (!popupSlot) { - popupSlot = self.popupSlots[type] = self._createPopupSlot(zIndex); + popupSlot = self.popupSlots[type] = self._createPopupSlot(zIndex, className); + } + + if(!popupSlot._inDocument) { + self.rootComponent.addChildComponent(popupSlot); } + // use the new zIndex for custom popup if(!isSystemPopup) { - popupSlot.element.style.zIndex = zIndex; + //Benoit: Modifying DOM outside of draw loop here, though it's early... + popupSlot.element.style.zIndex = zIndex; } popupSlot.content = content; @@ -585,6 +676,17 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio }}, + slotDidSwitchContent: { + value: function (slot) { + if(slot.content === null) { + slot.detachFromParentComponent(); + //Benoit: can't believe we have to do that in 2 steps.... + slot.element.parentNode.removeChild(slot.element); + + } + } + }, + /** * @private */ diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 79e97884ab..e29f211981 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -1,98 +1,136 @@ var Montage = require("core/core").Montage, + MutableEvent = require("core/event/mutable-event").MutableEvent, Criteria = require("core/criteria").Criteria, Enum = require("core/enum").Enum, uuid = require("core/uuid"), - DataOperationType; + DataOperationType, + dataOperationTypes = [ + "noop", + "create", + "createfailed", + "createcompleted", + "createcancelled", + //Additional + "copy", + "copyfailed", + "copycompleted", + /* Read is the first operation that mnodels a query */ + "read", + + /* ReadUpdated is pushed by server when a query's result changes due to data changes from others */ + "readupdated", + + /* ReadProgress / ReadUpdate / ReadSeek is used to instruct server that more data is required for a "live" read / query + Need a better name, and a symetric? Or is ReadUpdated enough if it referes to previous operation + */ + "readprogress", //ReadUpdate + "readupdate", //ReadUpdate + + /* ReadCancel is the operation that instructs baclkend that client isn't interested by a read operastion anymore */ + "readcancel", + + /* ReadCanceled is the operation that instructs the client that a read operation is canceled */ + "readcanceled", + + /* ReadFailed is the operation that instructs the client that a read operation has failed canceled */ + "readfailed", + /* ReadCompleted is the operation that instructs the client that a read operation has returned all available data */ + "readcompleted", + /* Request to update data, used either by the client sending the server or vice versa */ + "update", + /* Confirmation that a Request to update data, used either by the client sending the server or vice versa*, has been completed */ + "updatecompleted", + /* Confirmation that a Request to update data, used either by the client sending the server or vice versa*, has failed */ + "updatefailed", + /* Request to cancel an update, used either by the client sending the server or vice versa */ + "updatecancel", + /* Confirmation that a Request to cancel an update data, used either by the client sending the server or vice versa*, has completed */ + "updatecanceled", + "delete", + "deletecompleted", + "deletefailed", + + /* Lock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ + "lock", + "kockcompleted", + "lockfailed", + + /* Unlock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ + "unlock", + "unlockcompleted", + "unlockfailed", + + /* RemmoteProcedureCall models the ability to invoke code logic on the server-side, being a DB StoredProcedure, or an method/function in a service */ + "remoteinvocation", /* Execute ? */ + "remoteinvocationcompleted", /* ExecuteCompleted ? */ + "remoteinvocationfailed", /* ExecuteFailed ? */ + + /* Batch models the ability to group multiple operation. If a referrer is provided + to a BeginTransaction operation, then the batch will be executed within that transaction */ + /* + "batch", + "batchcompleted", + "batchfailed", + */ + /* A transaction is a unit of work that is performed against a database. + Transactions are units or sequences of work accomplished in a logical order. + A transactions begins, operations are grouped, then it is either commited or rolled-back*/ + /* Start/End Open/Close, Commit/Save, rollback/cancel + "begintransaction", + "begintransactioncompleted", + "begintransactionafiled", + "committransaction", + "committransactioncompleted", + "committransactionfailed", -exports.DataOperationType = DataOperationType = new Enum().initWithMembers( - "NoOp", - "Create", - "CreateFailed", - "CreateCompleted", - "CreateCancelled", - //Additional - "Copy", - "CopyFailed", - "CopyCompleted", - /* Read is the first operation that mnodels a query */ - "Read", + "rollbacktransaction", + "rollbacktransactioncompleted", + "rollbacktransactionfailed", - /* ReadUpdated is pushed by server when a query's result changes due to data changes from others */ - "ReadUpdated", + */ - /* ReadProgress / ReadUpdate / ReadSeek is used to instruct server that more data is required for a "live" read / query - Need a better name, and a symetric? Or is ReadUpdated enough if it referes to previous operation - */ - "ReadProgress", //ReadUpdate - "ReadUpdate", //ReadUpdate - - /* ReadCancel is the operation that instructs baclkend that client isn't interested by a read operastion anymore */ - "ReadCancel", - - /* ReadCanceled is the operation that instructs the client that a read operation is canceled */ - "ReadCanceled", - - /* ReadFailed is the operation that instructs the client that a read operation has failed canceled */ - "ReadFailed", - /* ReadCompleted is the operation that instructs the client that a read operation has returned all available data */ - "ReadCompleted", - /* Request to update data, used either by the client sending the server or vice versa */ - "Update", - /* Confirmation that a Request to update data, used either by the client sending the server or vice versa*, has been completed */ - "UpdateCompleted", - /* Confirmation that a Request to update data, used either by the client sending the server or vice versa*, has failed */ - "UpdateFailed", - /* Request to cancel an update, used either by the client sending the server or vice versa */ - "UpdateCancel", - /* Confirmation that a Request to cancel an update data, used either by the client sending the server or vice versa*, has completed */ - "UpdateCanceled", - "Delete", - "DeleteCompleted", - "DeleteFailed", - - /* Lock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ - "Lock", - "LockCompleted", - "LockFailed", - - /* Unlock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ - "Unlock", - "UnockCompleted", - "UnockFailed", - - /* RemmoteProcedureCall models the ability to invoke code logic on the server-side, being a DB StoredProcedure, or an method/function in a service */ - "RemoteProcedureCall", /* Execute ? */ - "RemoteProcedureCallCompleted", /* ExecuteCompleted ? */ - "RemoteProcedureCallFailed" /* ExecuteFailed ? */ - - /* Batch models the ability to group multiple operation. If a referrer is provided - to a BeginTransaction operation, then the batch will be executed within that transaction */ - /* - "Batch", - "BatchCompleted", - "BatchFailed", - */ + /* + operations used for the bottom of the stack to get information from a user. + This useful for authenticating a user, refreshing a password, + could be used to coordinate and solve data conflicts if an update realizes + one of the values to change has been changed by someone else in the meantime. + Maybe to communicate data validation, like a field missing, or a value that + isn't correct. Such validations could then be run server side or in a + web/service worker on the client. + + Data components shpuld add themselves as listeners to the data service for events/ + data operations like that they know how to deal with / can help with. + */ + "userauthentication", + "userauthenticationupdate", + "userauthenticationcompleted", + "userauthenticationfailed", + "userauthenticationtimedout", + "userinput", + "userinputcompleted", + "userinputfailed", + "userinputcanceled", + "userinputtimedout", - /* A transaction is a unit of work that is performed against a database. - Transactions are units or sequences of work accomplished in a logical order. - A transactions begins, operations are grouped, then it is either commited or rolled-back*/ - /* Start/End Open/Close, Commit/Save, rollback/cancel - "BeginTransaction", - "BeginTransactionCompleted", - "BeginTransactionFailed", + /* + Modeling validation operation, either executed locally or server-side. + This can be used for expressing that a password value is wrong, that an account + isn't confirmed with the Identity authority + that a mandatory value is missing, etc... - "CommitTransaction", - "CommitTransactionCompleted", - "CommitTransactionFailed", - "RollbackTransaction", - "RollbackTransactionCompleted", - "RollbackTransactionFailed", + */ + "validate", + "validatefailed", + "validatecompleted", + "validatecancelled" + ]; - */ -); + + +exports.DataOperationType = DataOperationType = new Enum().initWithMembersAndValues(dataOperationTypes,dataOperationTypes); /** * Represents @@ -100,7 +138,7 @@ exports.DataOperationType = DataOperationType = new Enum().initWithMembers( * @class * @extends external:Montage */ -exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ { +exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototype */ { /*************************************************************************** * Constructor @@ -117,10 +155,18 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ } }, + bubbles: { + value: true + }, + + defaultPrevented: { + value: false + }, + serializeSelf: { value:function (serializer) { serializer.setProperty("id", this.id); - serializer.setProperty("type", this.type); + serializer.setProperty("type", DataOperationType.intValueForMember(this.type)); serializer.setProperty("time", this.time); serializer.setProperty("dataDescriptor", this.dataDescriptor); if(this.referrerId) { @@ -145,7 +191,7 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ value = deserializer.getProperty("type"); if (value !== void 0) { - this.type = value; + this.type = DataOperationType.memberWithIntValue(value); } value = deserializer.getProperty("time"); @@ -358,14 +404,25 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ }, /** - * The authorization object representing an authenticated user, like a JWToken. + * The userIdentity object representing the authenticated user. * * @type {Object} */ - authorization: { + userIdentity: { value: undefined }, + /** + * a message about the operation meant for the user. + * + * @type {Object} + */ + userMessage: { + value: undefined + }, + + + /** * Deprecate? Make programatic, so that users doesn't have to worry about it. * @@ -573,52 +630,69 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ */ value: { - Create: DataOperationType.Create, - CreateFailed: DataOperationType.CreateFailed, - CreateCompleted: DataOperationType.CreateCompleted, - CreateCancelled: DataOperationType.CreateCancelled, + Create: DataOperationType.create, + CreateFailed: DataOperationType.createfailed, + CreateCompleted: DataOperationType.createcompleted, + CreateCancelled: DataOperationType.createcancelled, //Additional - Copy: DataOperationType.Copy, - CopyFailed: DataOperationType.CopyFailed, - CopyCompleted: DataOperationType.CopyCompleted, + Copy: DataOperationType.copy, + CopyFailed: DataOperationType.copyfailed, + CopyCompleted: DataOperationType.copycompleted, /* Read is the first operation that mnodels a query */ - Read: DataOperationType.Read, + Read: DataOperationType.read, /* ReadUpdated is pushed by server when a query's result changes due to data changes from others */ - ReadUpdated: DataOperationType.ReadUpdated, + ReadUpdated: DataOperationType.readupdated, /* ReadProgress / ReadUpdate / ReadSeek is used to instruct server that more data is required for a "live" read / query Need a better name, and a symetric? Or is ReadUpdated enough if it referes to previous operation */ - ReadProgress: DataOperationType.ReadProgress, //ReadUpdate - ReadUpdate: DataOperationType.ReadUpdate, //ReadUpdate + ReadProgress: DataOperationType.readprogress, //ReadUpdate + ReadUpdate: DataOperationType.readupdate, //ReadUpdate /* ReadCancel is the operation that instructs baclkend that client isn't interested by a read operastion anymore */ - ReadCancel: DataOperationType.ReadCancel, + ReadCancel: DataOperationType.readcancel, /* ReadCanceled is the operation that instructs the client that a read operation is canceled */ - ReadCanceled: DataOperationType.ReadCanceled, + ReadCanceled: DataOperationType.readcanceled, /* ReadFailed is the operation that instructs the client that a read operation has failed canceled */ - ReadFailed: DataOperationType.ReadFailed, + ReadFailed: DataOperationType.readfailed, /* ReadCompleted is the operation that instructs the client that a read operation has returned all available data */ - ReadCompleted: DataOperationType.ReadCompleted, - Update: DataOperationType.Update, - UpdateCompleted: DataOperationType.UpdateCompleted, - UpdateFailed: DataOperationType.UpdateFailed, - UpdateCancel: DataOperationType.UpdateCancel, - UpdateCanceled: DataOperationType.UpdateCanceled, - Delete: DataOperationType.Delete, - DeleteCompleted: DataOperationType.DeleteCompleted, - DeleteFailed: DataOperationType.DeleteFailed, + ReadCompleted: DataOperationType.readcompleted, + Update: DataOperationType.update, + UpdateCompleted: DataOperationType.updatecompleted, + UpdateFailed: DataOperationType.updatefailed, + UpdateCancel: DataOperationType.updatecancel, + UpdateCanceled: DataOperationType.updatecanceled, + Delete: DataOperationType.delete, + DeleteCompleted: DataOperationType.deletecompleted, + DeleteFailed: DataOperationType.deletefailed, /* Lock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ - Lock: DataOperationType.Lock, - LockCompleted: DataOperationType.LockCompleted, - LockFailed: DataOperationType.LockFailed, + Lock: DataOperationType.lock, + LockCompleted: DataOperationType.lockcompleted, + LockFailed: DataOperationType.lockfailed, /* RemmoteProcedureCall models the ability to invoke code logic on the server-side, being a DB StoredProcedure, or an method/function in a service */ - RemoteProcedureCall: DataOperationType.RemoteProcedureCall, - RemoteProcedureCallCompleted: DataOperationType.RemoteProcedureCallCompleted, - RemoteProcedureCallFailed: DataOperationType.RemoteProcedureCallFailed + RemoteProcedureCall: DataOperationType.remoteinvocation, + RemoteProcedureCallCompleted: DataOperationType.remoteinvocationcompleted, + RemoteProcedureCallFailed: DataOperationType.remoteinvocationfailed, + RemoteInvocation: DataOperationType.remoteinvocation, + RemoteInvocationCompleted: DataOperationType.remoteinvocationcompleted, + RemoteInvocationFailed: DataOperationType.remoteinvocationfailed, + UserAuthentication: DataOperationType.userauthentication, + UserAuthenticationUpdate: DataOperationType.userauthenticationupdate, + UserAuthenticationCompleted: DataOperationType.userauthenticationcompleted, + UserAuthenticationFailed: DataOperationType.userauthenticationfailed, + UserAuthenticationTimedout: DataOperationType.userauthenticationtimedout, + UserInput: DataOperationType.userinput, + UserInputCompleted: DataOperationType.userinputcompleted, + UserInputFailed: DataOperationType.userinputfailed, + UserInputCanceled: DataOperationType.userinputcanceled, + UserInputTimedOut: DataOperationType.userinputtimedout, + Validate: DataOperationType.validate, + ValidateFailed: DataOperationType.validatefailed, + validateCompleted: DataOperationType.validatecompleted, + validateCancelled: DataOperationType.validatecancelled, /* Batch models the ability to group multiple operation. If a referrer is provided to a BeginTransaction operation, then the batch will be executed within that transaction */ diff --git a/data/service/data-service.js b/data/service/data-service.js index 9b301d4754..bac14fa601 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -1,6 +1,9 @@ var Montage = require("core/core").Montage, + Target = require("core/target").Target, AuthorizationManager = require("data/service/authorization-manager").defaultAuthorizationManager, AuthorizationPolicy = require("data/service/authorization-policy").AuthorizationPolicy, + UserAuthenticationPolicy = require("data/service/user-authentication-policy").UserAuthenticationPolicy, + UserIdentityManager = require("data/service/user-identity-manager").UserIdentityManager, DataObjectDescriptor = require("data/model/data-object-descriptor").DataObjectDescriptor, DataQuery = require("data/model/data-query").DataQuery, DataStream = require("data/service/data-stream").DataStream, @@ -19,6 +22,11 @@ AuthorizationPolicyType.UpfrontAuthorizationPolicy = AuthorizationPolicy.UP_FRON AuthorizationPolicyType.OnDemandAuthorizationPolicy = AuthorizationPolicy.ON_DEMAND; AuthorizationPolicyType.OnFirstFetchAuthorizationPolicy = AuthorizationPolicy.ON_FIRST_FETCH; +UserAuthenticationPolicy.NoAuthenticationPolicy = UserAuthenticationPolicy.NONE; +UserAuthenticationPolicy.UpfrontAuthenticationPolicy = UserAuthenticationPolicy.UP_FRONT; +UserAuthenticationPolicy.OnDemandAuthenticationPolicy = UserAuthenticationPolicy.ON_DEMAND; +UserAuthenticationPolicy.OnFirstFetchAuthenticationPolicy = UserAuthenticationPolicy.ON_FIRST_FETCH; + /** * Provides data objects and manages changes to them. * @@ -36,7 +44,7 @@ AuthorizationPolicyType.OnFirstFetchAuthorizationPolicy = AuthorizationPolicy.ON * @class * @extends external:Montage */ -exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { +exports.DataService = Target.specialize(/** @lends DataService.prototype */ { /*************************************************************************** * Initializing @@ -45,7 +53,21 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { constructor: { value: function DataService() { exports.DataService.mainService = exports.DataService.mainService || this; - this._initializeAuthorization(); + if(this === DataService.mainService) { + UserIdentityManager.mainService = DataService.mainService; + } + + //Deprecated now + //this._initializeAuthorization(); + + if (this.providesAuthorization) { + exports.DataService.authorizationManager.registerAuthorizationService(this); + } + + if(this.providesUserIdentity === true) { + UserIdentityManager.registerUserIdentityService(this); + } + this._initializeOffline(); } }, @@ -95,6 +117,16 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { this._childServices = value; } + value = deserializer.getProperty("authorizationPolicy"); + if (value) { + this.authorizationPolicy = value; + } + + value = deserializer.getProperty("userAuthenticationPolicy"); + if (value) { + this.userAuthenticationPolicy = value; + } + return this; } }, @@ -106,6 +138,11 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { this._childServices = []; this.addChildServices(childServices); } + + if (this.authorizationPolicy === AuthorizationPolicyType.UpfrontAuthorizationPolicy) { + exports.DataService.authorizationManager.registerServiceWithUpfrontAuthorizationPolicy(this); + } + } }, @@ -265,15 +302,15 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { iChild = childServices[i]; if((types = iChild.types)) { - this._registerTypesByModuleId(types); - - for(j=0, countJ = types.length;(j the rules that matched + loop on that, else build the cache: + + 1. Loop on object rules: + forEach object side rule (they are property names so far): + + if(rule has an raw-property-value-to-object-converter && not in prerequiste && not in object/prefetchExpression) { + skip; + } else { + get requirements (list of raw data properties) + assess if Objec.keys(rawData) contains all of them. + if(yes, that rule can and need to be mapped) + //Cache Object.keys(rawData) -> the rules that matched + } } + */ - } - else { - if (this.requisitePropertyNames.size) { - while ((propertyName = iterator.next().value)) { - result = this.mapRawDataToObjectProperty(data, object, propertyName, context); - if (this._isAsync(result)) { - (promises || (promises = [])).push(result); + // _mappingRulesByRawDataProperty: { + // value: undefined + // }, + + /** + * gather all ObjectMapping rules that require rawDataProperty + * + * @method + * @argument {Set} rawDataProperties - An set of RawData properties. Set vs Array as the order could depend on server + * but we need an order agnostic data struture. + * @returns {DataStream|Promise|?} - Either the value or a "promise" for it + * + */ + // _buildMappingRulesForRawDataProperty: { + // value: function (rawDataProperty) { + // var objectMappingRules = this.objectMappingRules, + // rulesIterator = objectMappingRules.values(), + // allMappingRules = new Set; + // aRule, aRulePropertyRequirements; + + // while ((aRule = iterator.next().value)) { + // aRulePropertyRequirements = aRule.requirements; + // if() + + // allMappingRules.add(aRule); + // } + // this._mappingRulesByRawDataProperty.set(rawDataProperty,allMappingRules); + + // ; + + // } + // }, + + // mappingRulesForRawDataProperty: { + // value: function (rawDataProperty) { + // return this._mappingRulesByRawDataProperty.get(rawDataProperty) || + // this._buildMappingRulesForRawDataProperty(rawDataProperty); + // } + // }, + + //Cache the union of all the object rules relevant to a set of RawDataKeys + __mappingRulesByRawDataProperties: { + value: undefined + }, + + _mappingRulesByRawDataProperties: { + get: function() { + return this.__mappingRulesByRawDataProperties || (this.__mappingRulesByRawDataProperties = new Map()); + } + }, + + _buildMappingRulesForRawDataProperties: { + value: function (rawDataProperties) { + var objectMappingRules = this.objectMappingRules, + rawDataPropertiesSet = new Set(rawDataProperties), + rulesIterator = objectMappingRules.values(), + matchingRules = new Set(), + aRule, aRulePropertyRequirements, iMatch, + i, countI; + + while ((aRule = rulesIterator.next().value)) { + aRulePropertyRequirements = aRule.requirements; + if(aRulePropertyRequirements) { + iMatch = 0; + for(i=0, countI = aRulePropertyRequirements.length;i} - A promise resolving to the mapped object. + * + */ + registerDataIdentifierForTypePrimaryKey: { + value: function (dataIdentifier, type, primaryKey) { + var dataIdentifierMap = this._typeIdentifierMap.get(type); + + if(!dataIdentifierMap) { + this._typeIdentifierMap.set(type,(dataIdentifierMap = new Map())); + } + + dataIdentifierMap.set(primaryKey,dataIdentifier); + } + }, + dataIdentifierForTypePrimaryKey: { value: function (type, primaryKey) { var dataIdentifierMap = this._typeIdentifierMap.get(type), dataIdentifier; - if(!dataIdentifierMap) { - this._typeIdentifierMap.set(type,(dataIdentifierMap = new Map())); - } - - dataIdentifier = dataIdentifierMap.get(primaryKey); + dataIdentifier = dataIdentifierMap + ? dataIdentifierMap.get(primaryKey) + : null; if(!dataIdentifier) { var typeName = type.typeName /*DataDescriptor*/ || type.name; @@ -608,7 +665,8 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot //dataIdentifier._identifier = dataIdentifier.primaryKey = primaryKey; dataIdentifier.primaryKey = primaryKey; - dataIdentifierMap.set(primaryKey,dataIdentifier); + // dataIdentifierMap.set(primaryKey,dataIdentifier); + this.registerDataIdentifierForTypePrimaryKey(dataIdentifier,type, primaryKey); } return dataIdentifier; } @@ -719,6 +777,35 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } }, + rawDataError: { + value: function (stream, error) { + var self = this, + dataToPersist = this._streamRawData.get(stream), + mappingPromises = this._streamMapDataPromises.get(stream), + dataReadyPromise = mappingPromises ? Promise.all(mappingPromises) : this.nullPromise; + + if (mappingPromises) { + this._streamMapDataPromises.delete(stream); + } + + if (dataToPersist) { + this._streamRawData.delete(stream); + } + + dataReadyPromise.then(function (results) { + + //return dataToPersist ? self.writeOfflineData(dataToPersist, stream.query, context) : null; + }).then(function () { + stream.dataError(error); + return null; + }).catch(function (e) { + console.error(e,stream); + }); + + } + }, + + /** * To be called once for each [fetchData()]{@link RawDataService#fetchData} * or [fetchRawData()]{@link RawDataService#fetchRawData} call received to @@ -948,7 +1035,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot * call that invoked this method. */ _mapRawDataToObject: { - value: function (record, object, context, isUpdateToExistingObject) { + value: function (record, object, context, prefetchExpressions) { var self = this, mapping = this.mappingForObject(object), snapshot, @@ -965,10 +1052,10 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot this._objectsBeingMapped.add(object); - result = mapping.mapRawDataToObject(record, object, context, isUpdateToExistingObject); + result = mapping.mapRawDataToObject(record, object, context, prefetchExpressions); if (result) { result = result.then(function () { - result = self.mapRawDataToObject(record, object, context); + result = self.mapRawDataToObject(record, object, context, prefetchExpressions); if (!self._isAsync(result)) { self._objectsBeingMapped.delete(object); @@ -993,7 +1080,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot throw error; }); } else { - result = this.mapRawDataToObject(record, object, context); + result = this.mapRawDataToObject(record, object, context, prefetchExpressions); if (!this._isAsync(result)) { self._objectsBeingMapped.delete(object); @@ -1017,7 +1104,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot this._objectsBeingMapped.add(object); - result = this.mapRawDataToObject(record, object, context); + result = this.mapRawDataToObject(record, object, context, prefetchExpressions); if (!this._isAsync(result)) { diff --git a/data/service/user-authentication-policy.js b/data/service/user-authentication-policy.js new file mode 100644 index 0000000000..c3f8cf472e --- /dev/null +++ b/data/service/user-authentication-policy.js @@ -0,0 +1,34 @@ +var Montage = require("core/core").Montage; + +/** + * AuthorizationPolicyType + * + * UpfrontAuthorizationPolicy + * Authorization is asked upfront, immediately after data service is + * created / launch of an app. + * + * OnDemandAuthorizationPolicy + * Authorization is required when a request fails because of lack of + * authorization. This is likely to be a good strategy for DataServices + * that offer data to both anonymous and authorized users. + * + */ +var UserAuthenticationPolicy = exports.UserAuthenticationPolicy = Montage.specialize({ + + id: { + value: undefined + } + +}, { + withID: { + value: function (id) { + var policy = new this(); + policy.id = id; + return policy; + } + } +}); +UserAuthenticationPolicy.ON_DEMAND = UserAuthenticationPolicy.withID("ON_DEMAND"); +UserAuthenticationPolicy.ON_FIRST_FETCH = UserAuthenticationPolicy.withID("ON_FIRST_FETCH"); +UserAuthenticationPolicy.NONE = UserAuthenticationPolicy.withID("NONE"); +UserAuthenticationPolicy.UP_FRONT = UserAuthenticationPolicy.withID("UP_FRONT"); diff --git a/data/service/user-identity-manager.js b/data/service/user-identity-manager.js new file mode 100644 index 0000000000..e5f28c8289 --- /dev/null +++ b/data/service/user-identity-manager.js @@ -0,0 +1,155 @@ +var Montage = require("core/core").Montage, + DataOperation = require("data/service/data-operation").DataOperation, + UserIdentityManager; + + +/** + * + * @class + * @extends RawDataService + * @deprecated The Authorization API was moved to DataService itself. + */ +UserIdentityManager = Montage.specialize( /** @lends AuthorizationService.prototype */ { + + registerUserIdentityService: { + value: function(aService) { + this.userIdentityServices.push(aService); + } + }, + + _panelsByModuleId: { + value: new Map() + }, + + _panelWithModuleId: { + value: function (moduleId, location, dataServiceModuleId) { + var panel = this._panelsByModuleId.get(location+moduleId); + + return panel ? Promise.resolve(panel) : this._loadPanelWithModuleId(moduleId, location, dataServiceModuleId); + } + }, + + _loadPanelWithModuleId: { + value: function (panelModuleID, location, dataServiceModuleId) { + var self = this, + // providerInfo = Montage.getInfoForObject(provider), + panelPromise; + + if (panelModuleID) { + //panelPromise = providerInfo.require.async(panelModuleID) + var panelRequire = require.packages[location]; + panelPromise = panelRequire.async(panelModuleID) + .then(function (exports) { + var exportNames = Object.keys(exports), + panel, i, n; + + for (i = 0, n = exportNames.length; i < n && !panel; ++i) { + panel = self._panelForConstructorAndProvider(exports[exportNames[i]], dataServiceModuleId); + } + //Cut the cord so it works if they're not in the same thread + //panel.service = provider; + self._panelsByModuleId.set(location+panelModuleID, panel); + self.authenticationManagerPanel.panels.push(panel); + return panel; + }); + } + + return panelPromise; + } + }, + + _panelForConstructorAndProvider: { + value: function (constructor, dataServiceModuleId) { + return this.callDelegateMethod("userIdentityManagerWillInstantiateAuthorizationPanelForDataService", this, constructor, dataServiceModuleId) || new constructor(); + } + }, + + userIdentityServices: { + value: [] + }, + + _mainService: { + value: undefined + }, + /** + * The main data service used by this service to listen to user authentication + * operations. We have a circular dependency pbm, so this is set by DataService + * + * @type {DataService} + */ + + mainService: { + get: function() { + return this._mainService; + }, + set: function(value) { + this._mainService = value; + this._mainService.addEventListener(DataOperation.Type.UserAuthentication, this); + this._mainService.addEventListener(DataOperation.Type.UserAuthenticationCompleted, this); + + } + }, + + /******************************************** + * Panels + */ + + authenticationManagerPanelModule: { + value: "ui/authentication-manager-panel.reel" + }, + + _managerPanel: { + value: function () { + var self = this, + moduleId; + + if (!this._managerPanelPromise && this.authenticationManagerPanel) { + this.authenticationManagerPanel.authorizationManager = this; + this._managerPanelPromise = Promise.resolve(this.authenticationManagerPanel); + } else if (!this._managerPanelPromise) { + moduleId = this.callDelegateMethod("userIdentityManagerWillLoadAuthenticationManagerPanel", this, this.authenticationManagerPanelModule) || this.authenticationManagerPanelModule; + this._managerPanelPromise = require.async(moduleId).bind(this).then(function (exports) { + var panel = new exports.AuthenticationManagerPanel(); + + self.authenticationManagerPanel = panel; + panel.userIdentityManager = self; + return panel; + }).catch(function(error) { + console.log(error); + }); + } + + return this._managerPanelPromise; + } + }, + + + handleUserauthentication: { + value: function(userAuthenticationOperation) { + var self = this; + + console.log("handleUserauthentication:",event); + this._managerPanel().then(function (authManagerPanel) { + var panelModuleId = userAuthenticationOperation.authorizationPanelModuleId; + self._panelWithModuleId(panelModuleId, userAuthenticationOperation.authorizationPanelRequireLocation, + userAuthenticationOperation.dataServiceModuleId) + .then(function (authenticationPanel){ + console.log("authenticationPanel: ",authenticationPanel); + authenticationPanel.userIdentity = userAuthenticationOperation.data; + self.authenticationManagerPanel.runModal(); + }) + }); + } + }, + + handleUserauthenticationcompleted: { + value: function(userAuthenticationCompletedOperation) { + //If a cached user identity still valid, there's no + //authenticationManagerPanel to hide. + if(this.authenticationManagerPanel) this.authenticationManagerPanel.hide(); + } + } +}); + +//Exports the singleton +exports.UserIdentityManager = new UserIdentityManager diff --git a/data/service/user-identity-service.js b/data/service/user-identity-service.js new file mode 100644 index 0000000000..236ed4f5ac --- /dev/null +++ b/data/service/user-identity-service.js @@ -0,0 +1,35 @@ +var RawDataService = require("data/service/raw-data-service").RawDataService, + UserIdentityManager = require("data/service/user-identity-manager").UserIdentityManager, + UserIdentityService; + + +/** + * + * @class + * @extends RawDataService + * @deprecated The Authorization API was moved to DataService itself. + */ +exports.UserIdentityService = UserIdentityService = RawDataService.specialize( /** @lends AuthorizationService.prototype */ { + + + constructor: { + value: function UserIdentityService() { + RawDataService.call(this); + UserIdentityService.userIdentityServices.push(this); + } + }, + providesUserIdentity: { + value: true + } + +}, { + registerUserIdentityService: { + value: function(aService) { + UserIdentityManager.registerUserIdentityService(aService); + } + }, + + userIdentityServices: { + value: UserIdentityManager.userIdentityServices + } +}); diff --git a/ui/authentication-manager-panel.reel/authentication-manager-panel.css b/ui/authentication-manager-panel.reel/authentication-manager-panel.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui/authentication-manager-panel.reel/authentication-manager-panel.html b/ui/authentication-manager-panel.reel/authentication-manager-panel.html new file mode 100644 index 0000000000..9078c2183e --- /dev/null +++ b/ui/authentication-manager-panel.reel/authentication-manager-panel.html @@ -0,0 +1,37 @@ + + + + + + + + +
+
+
+
+
+ + diff --git a/ui/authentication-manager-panel.reel/authentication-manager-panel.js b/ui/authentication-manager-panel.reel/authentication-manager-panel.js new file mode 100644 index 0000000000..7cacb6b097 --- /dev/null +++ b/ui/authentication-manager-panel.reel/authentication-manager-panel.js @@ -0,0 +1,194 @@ +var Component = require("ui/component").Component, + Promise = require("core/promise").Promise, + application = require("core/application").application, + Map = require("collections/map"), + Montage = require("montage").Montage; + +/** + * @class Main + * @extends Component + */ +exports.AuthenticationManagerPanel = Component.specialize({ + + panels: { + get: function () { + if (!this._panels) { + this._panels = []; + } + return this._panels; + }, + set: function (value) { + console.warn("AuthenticationManagerPanel.panels.set", value); + } + }, + + userIdentityManager: { + value: undefined + }, + + // approveAuthorization: { + // value: function (authorization, panel) { + + // if (panel) { + // this._deregisterPanel(panel, authorization); + // } + + // if (!this.panels.length && this._authorizationResolve) { + // this._authorizationResolve(authorization); + // } + // } + // }, + + _deregisterPanel: { + value: function (panel, authorization) { + var index = this.panels.indexOf(panel), + panelInfo = Montage.getInfoForObject(panel); + + if (index !== -1) { + this.panels.splice(index, 1); + } + + if (authorization) { + this._authorizationPromiseByPanel.get(panel).resolve(authorization); + } else { + this._authorizationPromiseByPanel.get(panel).reject(new Error("Authorization Rejected for panel (" + panelInfo.id +")")); + } + this._authorizationPromiseByPanel.delete(panel); + } + }, + + _authorizationPromiseByPanel: { + get: function () { + if (!this.__authorizationPromiseByPanel) { + this.__authorizationPromiseByPanel = new Map(); + } + return this.__authorizationPromiseByPanel; + } + }, + + authorizeWithPanel: { + value: function (panel) { + var self = this, + promise; + + if (!this._authorizationPromiseByPanel.has(panel)) { + promise = new Promise(function (resolve, reject) { + self._authorizationPromiseByPanel.set(panel, { + resolve: resolve, + reject: reject + }); + }); + this.panels.push(panel); + } else { + promise = Promise.resolve(null); + } + + return promise; + } + }, + + _authorizationResolve: { + value: void 0 + }, + + // cancelAuthorization: { + // value: function(panel) { + // if (panel) { + // this._deregisterPanel(panel); + // } + + // if (application.applicationModal) { + // application.applicationModal.hide(this); + // } + // if (this._authorizationResolve) { + // this._authorizationReject("CANCEL"); + // } + // } + // }, + + // _authorizationReject: { + // value: void 0 + // }, + + runModal: { + value: function() { + var self = this; + return new Promise(function(resolve, reject) { + self._authorizationResolve = resolve; + self._authorizationReject = reject; + // FIXME This is temporary shortcut for FreeNAS while we fix Montage's modal. + if (application && application.applicationModal) { + application.applicationModal.show(self); + } + else { + self.show(); + } + }); + } + }, + + show: { + value: function() { + if(this.application) { + var type = this.type, + self = this; + this.application.getPopupSlot(type, this, function(slot) { + self._popupSlot = slot; + self.displayed = true; + self._addEventListeners(); + }); + } + } + }, + + /** + * Hide the popup + */ + hide: { + value: function() { + if(this.application) { + + var type = this.type, + self = this; + + this.application.getPopupSlot(type, this, function(slot) { + self._removeEventListeners(); + self.application.returnPopupSlot(type); + self.displayed = false; + }); + } + } + }, + + type: { + value: "authentication" + }, + + _addEventListeners: { + value: function() { + if (window.Touch) { + this.element.ownerDocument.addEventListener('touchstart', this, false); + } else { + this.element.ownerDocument.addEventListener('mousedown', this, false); + this.element.ownerDocument.addEventListener('keyup', this, false); + } + window.addEventListener('resize', this); + } + }, + + _removeEventListeners: { + value: function() { + if (window.Touch) { + this.element.ownerDocument.removeEventListener('touchstart', this, false); + } else { + this.element.ownerDocument.removeEventListener('mousedown', this, false); + this.element.ownerDocument.removeEventListener('keyup', this, false); + } + window.removeEventListener('resize', this); + } + } + +}); + + +// FIXME: Selection needs to be managed by a selection controller diff --git a/ui/authentication-panel.js b/ui/authentication-panel.js new file mode 100644 index 0000000000..a983677f6d --- /dev/null +++ b/ui/authentication-panel.js @@ -0,0 +1,55 @@ +var Component = require("./component").Component; + +/** + * @class AuthenticationPanel + * @extends Component + */ +exports.AuthenticationPanel = Component.specialize(/** @lends AuthenticationPanel# */ { + + + _userIdentity: { + value: undefined + }, + + userIdentity: { + get: function () { + return this._userIdentity; + }, + set: function(value) { + this._userIdentity = value; + } + }, + + show: { + value: function() { + var type = this.type, + self = this; + this.application.getPopupSlot(type, this, function(slot) { + self._popupSlot = slot; + self.displayed = true; + self._addEventListeners(); + }); + } + }, + + /** + * Hide the popup + */ + hide: { + value: function() { + var type = this.type, + self = this; + + this.application.getPopupSlot(type, this, function(slot) { + self._removeEventListeners(); + //self.application.returnPopupSlot(type); + self.displayed = false; + }); + } + }, + + type: { + value: "authentication" + } + +}); From 1a51a04159cb4bb4cab5c9d244f533ef70f216c1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 19 Dec 2019 16:44:07 -0800 Subject: [PATCH 086/407] Fix some bugs about validity --- ui/text-input.js | 62 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/ui/text-input.js b/ui/text-input.js index ea1f43aa5a..2633a5139e 100644 --- a/ui/text-input.js +++ b/ui/text-input.js @@ -76,6 +76,7 @@ var TextInput = exports.TextInput = Control.specialize(/** @lends module:montag if(this.hasStandardElement || this.element.contentEditable === "true") { this.element.addEventListener('input', this); this.element.addEventListener('change', this); + this.element.addEventListener('invalid', this); } } @@ -114,9 +115,11 @@ var TextInput = exports.TextInput = Control.specialize(/** @lends module:montag if (this.error) { el.classList.add('montage--invalidText'); + el.classList.add('montage--invalid'); el.title = this.error.message || ''; } else { el.classList.remove("montage--invalidText"); + el.classList.remove("montage--invalid"); el.title = ''; } } @@ -142,7 +145,7 @@ var TextInput = exports.TextInput = Control.specialize(/** @lends module:montag handleInput: { enumerable: false, - value: function() { + value: function(event) { if (this.converter) { if (this.converter.allowPartialConversion === true && this.updateOnInput === true) { this.takeValueFromElement(); @@ -150,6 +153,16 @@ var TextInput = exports.TextInput = Control.specialize(/** @lends module:montag } else if(this.updateOnInput === true){ this.takeValueFromElement(); } + + if (this.element.validity.valid) { + //It's a bit unclear if error is/should be a boolean or an error object... + //From a draw's perspective, both work. + this.error = null; + } + else { + this.error = true; + } + } }, handleChange: { @@ -160,6 +173,53 @@ var TextInput = exports.TextInput = Control.specialize(/** @lends module:montag this._hasFocus = false; } }, + + errorMessageFromValidityState: { + value: function(validityState) { + if(!validityState.valid) { + /* + Needs a proper message for each of the following: + + badInput Read only + A Boolean that is true if the user has provided input that the browser is unable to convert. + customError Read only + A Boolean indicating whether the element's custom validity message has been set to a non-empty string by calling the element's setCustomValidity() method. + patternMismatch Read only + A Boolean that is true if the value does not match the specified pattern, and false if it does match. If true, the element matches the :invalid CSS pseudo-class. + rangeOverflow Read only + A Boolean that is true if the value is greater than the maximum specified by the max attribute, or false if it is less than or equal to the maximum. If true, the element matches the :invalid and :out-of-range and CSS pseudo-classes. + rangeUnderflow Read only + A Boolean that is true if the value is less than the minimum specified by the min attribute, or false if it is greater than or equal to the minimum. If true, the element matches the :invalid and :out-of-range CSS pseudo-classes. + stepMismatch Read only + A Boolean that is true if the value does not fit the rules determined by the step attribute (that is, it's not evenly divisible by the step value), or false if it does fit the step rule. If true, the element matches the :invalid and :out-of-range CSS pseudo-classes. + tooLong Read only + A Boolean that is true if the value exceeds the specified maxlength for HTMLInputElement or HTMLTextAreaElement objects, or false if its length is less than or equal to the maximum length. Note: This property is never true in Gecko, because elements' values are prevented from being longer than maxlength. If true, the element matches the the :invalid and :out-of-range CSS pseudo-classes. + tooShort Read only + A Boolean that is true if the value fails to meet the specified minlength for HTMLInputElement or HTMLTextAreaElement objects, or false if its length is greater than or equal to the minimum length. If true, the element matches the :invalid and :out-of-range CSS pseudo-classes. + typeMismatch Read only + A Boolean that is true if the value is not in the required syntax (when type is email or url), or false if the syntax is correct. If true, the element matches the :invalid CSS pseudo-class. + valid Read only + A Boolean that is true if the element meets all its validation constraints, and is therefore considered to be valid, or false if it fails any constraint. If true, the element matches the :valid CSS pseudo-class; the :invalid CSS pseudo-class otherwise. + valueMissing Read only + A Boolean that is true if the element has a required attribute, but no value, or false otherwise. If true, the element matches the :invalid CSS pseudo-class. + */ + + + return "value is invalid"; + } + return null; + + } + }, + + handleInvalid: { + enumerable: false, + value: function(event) { + //To trigger a draw that will add montage--invalid/Text + this.error = true; + this.errorMessage = this.errorMessageFromValidityState(event.target.validity); + } + }, handleBlur: { enumerable: false, value: function(event) { From f23e652f855f52e0befcd2ab800ec4ec1018a8b8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 19 Dec 2019 21:25:21 -0800 Subject: [PATCH 087/407] cache call to super for makePropertyObservable in iteration and repetition --- ui/repetition.reel/repetition.js | 43 +++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/ui/repetition.reel/repetition.js b/ui/repetition.reel/repetition.js index b4c5356870..6c85305f20 100644 --- a/ui/repetition.reel/repetition.js +++ b/ui/repetition.reel/repetition.js @@ -595,10 +595,13 @@ var Iteration = exports.Iteration = Montage.specialize( /** @lends Iteration.pro * for corresponding keys. * @private */ + _superMakePropertyObservable : { + value: PropertyChanges.prototype.makePropertyObservable + }, makePropertyObservable: { value: function(key) { if(key !== "object" && key !== "_childComponents" && key !== "index" && key !== "_noTransition" && key !== "selected") { - PropertyChanges.prototype.makePropertyObservable.call(this, key); + this._superMakePropertyObservable( key); } } } @@ -720,6 +723,16 @@ var Repetition = exports.Repetition = Component.specialize(/** @lends Repetition } }, + /* + Selection TODO: + + selection can't be bound on the outside because it's bound from the inside + - using contentController.selection in the binding isn't really acceptable + - if a binding is set on either selection or contentController.selection, + we shouldn't have to manually set isSelectionEnabled to true. + We should be overriding addOb + */ + /** * When selection is enabled, each element in an iteration responds to * touch and click events such that the iteration is highlighted (with the @@ -737,6 +750,25 @@ var Repetition = exports.Repetition = Component.specialize(/** @lends Repetition isSelectionEnabled: {value: null}, + /** + * As dispatching has been implemented in key setters, limit use of default method + * for corresponding keys. + * @private + */ + _superMakePropertyObservable : { + value: PropertyChanges.prototype.makePropertyObservable + }, + makePropertyObservable: { + value: function(key) { + if(key === "selection") { + this.isSelectionEnabled = true; + } + this._superMakePropertyObservable( key); + } + }, + + + allowsMultipleSelection: { set: function (allowsMultipleSelection) { allowsMultipleSelection = !!allowsMultipleSelection; @@ -2027,6 +2059,15 @@ var Repetition = exports.Repetition = Component.specialize(/** @lends Repetition _enableSelectionTracking: { value: function () { this._pressComposer.addEventListener("pressStart", this, false); + /* + Quick test to eventually listen for press event coming from within. + If iterations component use a press composer, their's will win as deeper + but the repetion should still know about what's going on. + + However, if a button is pressed within an iteration, it might not means + the iteration it belongs to should become the selected one? + */ + // this.addEventListener("pressStart", this, false); } }, From 665c1706d819fa292f2dbcb5ad4b1150c7359449 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 5 Jan 2020 17:46:03 -0800 Subject: [PATCH 088/407] add a Range object using exertnal strange module --- core/range.js | 13 +++++++++++++ package.json | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 core/range.js diff --git a/core/range.js b/core/range.js new file mode 100644 index 0000000000..cff2b75057 --- /dev/null +++ b/core/range.js @@ -0,0 +1,13 @@ +/* + Range using: + - https://github.com/moll/js-strange + - https://www.npmjs.com/package/strange + + a range object for JavaScript. Use it to have a single value type with two endpoints and their boundaries. + Also implements an interval tree for quick lookups. Stringifies itself in the style of [begin,end) and allows you to parse a string back. + Also useful with PostgreSQL. +*/ + +var Range = require("strange"); + +exports.Range = Range; diff --git a/package.json b/package.json index ff07cdbe6c..6f371d0e78 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "lodash.camelcase": "^4.3.0", "lodash.trim": "^4.5.1", "lodash.snakecase": "^4.1.1", - "proxy-polyfill": "~0.1.7" + "proxy-polyfill": "~0.1.7", + "strange": "^1.7.2" }, "devDependencies": { "concurrently": "^3.4.0", From f0553f6269ebd78f675c544db0113d6d42b808c6 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 5 Jan 2020 17:47:07 -0800 Subject: [PATCH 089/407] add draggedObject property documentation --- core/drag/drag-event.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/core/drag/drag-event.js b/core/drag/drag-event.js index 8a8ca4c365..1f855c225d 100644 --- a/core/drag/drag-event.js +++ b/core/drag/drag-event.js @@ -123,6 +123,15 @@ if (typeof window !== "undefined") { value: false }, + /** + * Shortcut to this.data.set('application/object',anObject) + * + * Used to move/copy by reference an application object from + * one place in the application to another. + * + * @property {Object} + */ + draggedObject: { set: function (object) { this._data.set(OBJECT_MIME_TYPE, object); @@ -307,7 +316,7 @@ if (typeof window !== "undefined") { }); exports.DragEvent = MutableEvent.specialize({ - + type: { value: "drag" }, From 8147044ba288432144130dd7fd195c1b34a778b6 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 5 Jan 2020 17:48:05 -0800 Subject: [PATCH 090/407] Fix names of boolean properties --- core/meta/property-descriptor.js | 12 +++++++----- core/meta/property-descriptor.mjson | 26 +++++++++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index 59585e3798..ae757b1885 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -19,7 +19,7 @@ var Defaults = { enumValues: [], defaultValue: void 0, helpKey: "", - localizable: false, + isLocalizable: false, isSearcheable: false, isOrdered: false, hasUniqueValues: false, @@ -140,7 +140,8 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._setPropertyWithDefaults(serializer, "helpKey", this.helpKey); this._setPropertyWithDefaults(serializer, "definition", this.definition); this._setPropertyWithDefaults(serializer, "inversePropertyName", this.inversePropertyName); - this._setPropertyWithDefaults(serializer, "localizable", this.localizable); + this._setPropertyWithDefaults(serializer, "isLocalizable", this.isLocalizable); + this._setPropertyWithDefaults(serializer, "isSerializable", this.isSerializable); this._setPropertyWithDefaults(serializer, "isSearcheable", this.isSearcheable); this._setPropertyWithDefaults(serializer, "isOrdered", this.isOrdered); this._setPropertyWithDefaults(serializer, "hasUniqueValues", this.hasUniqueValues); @@ -179,7 +180,8 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._overridePropertyWithDefaults(deserializer, "helpKey"); this._overridePropertyWithDefaults(deserializer, "definition"); this._overridePropertyWithDefaults(deserializer, "inversePropertyName"); - this._overridePropertyWithDefaults(deserializer, "localizable"); + this._overridePropertyWithDefaults(deserializer, "isLocalizable"); + this._overridePropertyWithDefaults(deserializer, "isSerializable"); this._overridePropertyWithDefaults(deserializer, "isSearcheable"); this._overridePropertyWithDefaults(deserializer, "isOrdered"); this._overridePropertyWithDefaults(deserializer, "hasUniqueValues"); @@ -514,7 +516,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * possible values are: "reference" | "value" | "auto" | true | false, * @default false */ - serializable: { + isSerializable: { value: true }, @@ -525,7 +527,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * * @default false */ - localizable: { + isLocalizable: { value: false }, diff --git a/core/meta/property-descriptor.mjson b/core/meta/property-descriptor.mjson index f2d69335ce..b347777b64 100644 --- a/core/meta/property-descriptor.mjson +++ b/core/meta/property-descriptor.mjson @@ -211,10 +211,10 @@ "helpKey": "" } }, - "property_serializable": { + "property_isSerializable": { "prototype": "core/meta/property-descriptor", "values": { - "name": "serializable", + "name": "isSerializable", "objectDescriptor": { "@": "root" }, @@ -225,14 +225,14 @@ "valueType": "boolean", "enumValues": [], "helpKey": "", - "serializable": true + "isSerializable": true } }, - "property_localizable": { + "property_isLocalizable": { "prototype": "core/meta/property-descriptor", "values": { - "name": "localizable", + "name": "isLocalizable", "objectDescriptor": { "@": "root" }, @@ -243,7 +243,7 @@ "valueType": "boolean", "enumValues": [], "helpKey": "", - "serializable": true + "isSerializable": true } }, @@ -261,7 +261,7 @@ "valueType": "object", "enumValues": [], "helpKey": "", - "serializable": true + "isSerializable": true } }, @@ -307,6 +307,12 @@ { "@": "property_enumValues" }, + { + "@": "property_isSerializable" + }, + { + "@": "property_isLocalizable" + }, { "@": "property_helpKey" } @@ -350,6 +356,12 @@ "@": "property_enumValues" }, { + "@": "property_isSerializable" + }, + { + "@": "property_isLocalizable" + }, + { "@": "property_helpKey" } ] From d1e30ee6b46f83e62ca6665f734b1eb9771a9387 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 5 Jan 2020 17:49:26 -0800 Subject: [PATCH 091/407] add deserializedFromSerialization missing argument --- data/service/data-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index bac14fa601..40132501f6 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -132,7 +132,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { }, deserializedFromSerialization: { - value: function () { + value: function (label) { if(Array.isArray(this._childServices)) { var childServices = this._childServices; this._childServices = []; From 2e2bb62fcc7c2786b525a7b2e94d55e5abcb1678 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 5 Jan 2020 17:50:06 -0800 Subject: [PATCH 092/407] Fix property type documentation --- data/model/data-query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/model/data-query.js b/data/model/data-query.js index ca5c51b037..44384114d0 100644 --- a/data/model/data-query.js +++ b/data/model/data-query.js @@ -79,7 +79,7 @@ exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { /** * The type of the data object to retrieve. * - * @type {DataObjectDescriptor} + * @type {ObjectDescriptor} */ type: { serializable: "value", From 6e653dc854927c28f36f24ac3a75861736aa10d9 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 5 Jan 2020 17:54:57 -0800 Subject: [PATCH 093/407] - add missing deserializedFromSerialization argument - add loadUserInterfaceDescriptor ability to use object.objectDescriptor if available --- ui/component.js | 143 ++++++++++++++++++++++++++++-------------------- 1 file changed, 83 insertions(+), 60 deletions(-) diff --git a/ui/component.js b/ui/component.js index 713877f54f..a68198bc14 100644 --- a/ui/component.js +++ b/ui/component.js @@ -747,7 +747,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto dragManager: { get: function () { - return _defaultDragManager || + return _defaultDragManager || ((_defaultDragManager = new DragManager()).initWithComponent(this.rootComponent)); } }, @@ -1217,7 +1217,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto this.unregisterDroppable(); } - if (this._inDocument && typeof this.exitDocument === "function") { + if (this._inDocument && typeof this.exitDocument === "function") { this.exitDocument(); this._inDocument = false; } @@ -1901,7 +1901,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto }, deserializedFromSerialization: { - value: function () { + value: function (label) { this.attachToParentComponent(); } }, @@ -2384,7 +2384,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto for (i = 0; (component = components[i]); i++) { component.attachToParentComponent(); } - } + } } } } @@ -3389,7 +3389,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto var _name = "_"+attributeName; if ((this[_name] === null) && descriptor !== null && "value" in descriptor) { this[_name] = descriptor.value; - } + } } } } @@ -3586,47 +3586,70 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto false ); - if (typeof object === "object" && - (constructor = object.constructor) && - constructor.objectDescriptorModuleId - ) { - objectDescriptorModuleId = constructor.objectDescriptorModuleId; + + objectDescriptor = object.objectDescriptor; + if(objectDescriptor) { + if(!Promise.is(objectDescriptor)) { + promise = Promise.resolve(objectDescriptor); + } + else { + promise = objectDescriptor; + objectDescriptor = undefined; + } } - objectDescriptorModuleIdCandidate = this.callDelegateMethod( - "componentWillUseObjectDescriptorModuleIdForObject", - this, - objectDescriptorModuleId, - object - ); + if(!promise && this.application.mainService) { + objectDescriptor = this.application.mainService.objectDescriptorForObject(object); - if (objectDescriptorModuleIdCandidate) { - infoDelegate = Montage.getInfoForObject(this.delegate); - objectDescriptorModuleId = objectDescriptorModuleIdCandidate; + if(objectDescriptor) { + promise = Promise.resolve(objectDescriptor); + } } - if (objectDescriptorModuleId) { + if(!promise) { + if (typeof object === "object" && + (constructor = object.constructor) && + constructor.objectDescriptorModuleId + ) { + objectDescriptorModuleId = constructor.objectDescriptorModuleId; + } + + objectDescriptorModuleIdCandidate = this.callDelegateMethod( + "componentWillUseObjectDescriptorModuleIdForObject", + this, + objectDescriptorModuleId, + object + ); + if (objectDescriptorModuleIdCandidate) { - objectDescriptor = getObjectDescriptorWithModuleId( - objectDescriptorModuleId, - infoDelegate ? infoDelegate.require : require - ); - } else { - objectDescriptor = constructor.objectDescriptor; + infoDelegate = Montage.getInfoForObject(this.delegate); + objectDescriptorModuleId = objectDescriptorModuleIdCandidate; } - promise = objectDescriptor; - } else { - promise = Promise.resolve(); + if (objectDescriptorModuleId) { + if (objectDescriptorModuleIdCandidate) { + objectDescriptor = getObjectDescriptorWithModuleId( + objectDescriptorModuleId, + infoDelegate ? infoDelegate.require : require + ); + } else { + objectDescriptor = constructor.objectDescriptor; + } + + promise = objectDescriptor; + } else { + promise = Promise.resolve(objectDescriptor); + } } + promise = promise.then(function (objectDescriptor) { var moduleInfo = Montage.getInfoForObject(self), packageName = moduleInfo.require.packageDescription.name, moduleId = packageName + "/" + moduleInfo.moduleId, userInterfaceDescriptorModuleId, userInterfaceDescriptorModuleIdCandidate; - + if (objectDescriptor && objectDescriptor.userInterfaceDescriptorModules) { userInterfaceDescriptorModuleId = objectDescriptor.userInterfaceDescriptorModules[moduleId]; @@ -3734,7 +3757,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto if (setter) { setter.call(this, value); } else { - this[attributeName] = value; + this[attributeName] = value; } this._elementAttributeValues[name] = value; @@ -4055,7 +4078,7 @@ var RootComponent = Component.specialize( /** @lends RootComponent.prototype */{ * @function */ requestAnimationFrame: { - value: (global.requestAnimationFrame || global.webkitRequestAnimationFrame || + value: (global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.msRequestAnimationFrame), enumerable: false }, @@ -4065,7 +4088,7 @@ var RootComponent = Component.specialize( /** @lends RootComponent.prototype */{ * @function */ cancelAnimationFrame: { - value: (global.cancelAnimationFrame || global.webkitCancelAnimationFrame || + value: (global.cancelAnimationFrame || global.webkitCancelAnimationFrame || global.mozCancelAnimationFrame || global.msCancelAnimationFrame), enumerable: false }, @@ -4091,7 +4114,7 @@ var RootComponent = Component.specialize( /** @lends RootComponent.prototype */{ // Written by John Resig. Used under the Creative Commons Attribution 2.5 License. // http://ejohn.org/projects/javascript-diff-algorithm/ value: function ( o, n ) { - var ns = {}, + var ns = {}, os = {}; function isNullOrUndefined(o) { @@ -4100,9 +4123,9 @@ var RootComponent = Component.specialize( /** @lends RootComponent.prototype */{ for (var i = 0; i < n.length; i++ ) { if (isNullOrUndefined(ns[n[i]])) { - ns[n[i]] = { - rows: [], - o: null + ns[n[i]] = { + rows: [], + o: null }; } ns[n[i]].rows.push( i ); @@ -4110,9 +4133,9 @@ var RootComponent = Component.specialize( /** @lends RootComponent.prototype */{ for (i = 0; i < o.length; i++ ) { if (isNullOrUndefined(os[o[i]])) { - os[o[i]] = { - rows: [], - n: null + os[o[i]] = { + rows: [], + n: null }; } os[o[i]].rows.push(i); @@ -4120,17 +4143,17 @@ var RootComponent = Component.specialize( /** @lends RootComponent.prototype */{ for (i in ns ) { if ( - ns[i].rows.length === 1 && - !isNullOrUndefined(os[i]) && + ns[i].rows.length === 1 && + !isNullOrUndefined(os[i]) && os[i].rows.length === 1 ) { - n[ns[i].rows[0]] = { - text: n[ns[i].rows[0]], - row: os[i].rows[0] + n[ns[i].rows[0]] = { + text: n[ns[i].rows[0]], + row: os[i].rows[0] }; - o[ os[i].rows[0] ] = { - text: o[ os[i].rows[0] ], - row: ns[i].rows[0] + o[ os[i].rows[0] ] = { + text: o[ os[i].rows[0] ], + row: ns[i].rows[0] }; } } @@ -4152,20 +4175,20 @@ var RootComponent = Component.specialize( /** @lends RootComponent.prototype */{ n[i].row > 0 && isNullOrUndefined(o[ n[i].row - 1].text) && n[i - 1] === o[ n[i].row - 1 ] ) { - n[i - 1] = { - text: n[i - 1], - row: n[i].row - 1 + n[i - 1] = { + text: n[i - 1], + row: n[i].row - 1 }; - o[n[i].row-1] = { - text: o[n[i].row-1], - row: i - 1 + o[n[i].row-1] = { + text: o[n[i].row-1], + row: i - 1 }; } } - return { - o: o, - n: n + return { + o: o, + n: n }; } }, @@ -4214,8 +4237,8 @@ var RootComponent = Component.specialize( /** @lends RootComponent.prototype */{ addStyleSheetsFromTemplate: { value: function(template) { if(!this._addedStyleSheetsByTemplate.has(template)) { - var resources = template.getResources(), - ownerDocument = this.element.ownerDocument, + var resources = template.getResources(), + ownerDocument = this.element.ownerDocument, styles = resources.createStylesForDocument(ownerDocument); for (var i = 0, style; (style = styles[i]); i++) { @@ -4574,7 +4597,7 @@ var RootComponent = Component.specialize( /** @lends RootComponent.prototype */{ }); - + exports.__root__ = rootComponent = new RootComponent().init(); //https://github.com/kangax/html-minifier/issues/63 From cb0ab2210889a2621eaf02ede847a6b4cefa9618 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 5 Jan 2020 17:56:36 -0800 Subject: [PATCH 094/407] add query's type objectDescriptor as objectDescriptor property on data array --- data/service/data-stream.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/data/service/data-stream.js b/data/service/data-stream.js index 6ec6a2d8a9..54603a0f23 100644 --- a/data/service/data-stream.js +++ b/data/service/data-stream.js @@ -54,9 +54,23 @@ DataStream = exports.DataStream = DataProvider.specialize(/** @lends DataStream. * * @type {DataQuery} */ - query: { + _query: { value: undefined }, + query: { + get: function () { + return this._query; + }, + set: function (value) { + this._query = value; + + //This will enable a better undertanding of what type of data is coming + //for objects using UserInterfaceDescriptors like the CascadingList + if(value && value.type) { + this.data.objectDescriptor = value.type; + } + } + }, /** * The selector defining the data returned in this stream. @@ -98,6 +112,9 @@ DataStream = exports.DataStream = DataProvider.specialize(/** @lends DataStream. * no effect as data will come in the order in which it is added to the * stream and this order cannot be changed. * + * TODO: this method should be used to fulfill undefined spots in an array + * or an iterative batch. + * * @method * @argument {int} start - See [superclass]{@link DataProvider#requestData}. * @argument {int} length - See [superclass]{@link DataProvider#requestData}. @@ -276,6 +293,7 @@ DataStream = exports.DataStream = DataProvider.specialize(/** @lends DataStream. data = this._compiledDataExpression(new Scope(objects)); } + if (data && Array.isArray(data)) { this.data.push.apply(this.data, data); } else if (data) { From acf63e09e201adf8c2d1ef91055da88ab9f404ce Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 5 Jan 2020 17:57:25 -0800 Subject: [PATCH 095/407] double mock enmployee to exercise scroll in .info sample --- test/mocks/data/services/mock-service.js | 93 +++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/test/mocks/data/services/mock-service.js b/test/mocks/data/services/mock-service.js index 397b7e088d..ce2b8fc005 100644 --- a/test/mocks/data/services/mock-service.js +++ b/test/mocks/data/services/mock-service.js @@ -4,7 +4,7 @@ var Montage = require("montage").Montage, Settings = require('../models/settings').Settings, Check = require('../models/check').Check, Customer = require('../models/customer').Customer, - Department = require('../models/department').Department; + Department = require('../models/department').Department; /** * @class Employee @@ -15,6 +15,96 @@ exports.MockService = Montage.specialize({ fetchEmployees: { value: function () { return [ + new Employee( + 'Annette', + 'Alarcon', + 'Sales' + ), + new Employee( + 'Fredricka', + 'Fuss', + 'Accounting' + ), + new Employee( + 'Moriah', + 'Mcwhorter', + 'Logistics' + ), + new Employee( + 'Emanuel', + 'Sullivan', + 'Management' + ), + new Employee( + 'Gretchen', + 'Chapman', + 'Accounting' + ), + new Employee( + 'Courtney', + 'Simon', + 'Logistics' + ), + new Employee( + 'Brett', + 'Lucas', + 'Management' + ), + new Employee( + 'Mamie', + 'Henderson', + 'Sales' + ), + new Employee( + 'Shaun', + 'Davidson', + 'Accounting' + ), + new Employee( + 'Adam', + 'Owens', + 'Logistics' + ), + new Employee( + 'Jeffrey', + 'Cross', + 'Management' + ), + new Employee( + 'Flora', + 'Adkins', + 'Accounting' + ), + new Employee( + 'Rosemary', + 'Adams', + 'Logistics' + ), + new Employee( + 'Henrietta', + 'Silva', + 'Management' + ), + new Employee( + 'Alvin', + 'Jennings', + 'Management' + ), + new Employee( + 'Casey', + 'Norris', + 'Accounting' + ), + new Employee( + 'Santiago', + 'Peters', + 'Logistics' + ), + new Employee( + 'Johnny', + 'Bailey', + 'Management' + ), new Employee( 'Annette', 'Alarcon', @@ -105,6 +195,7 @@ exports.MockService = Montage.specialize({ 'Bailey', 'Management' ) + ]; } }, From 412ab44edb05df76d0af0f5402463545301db72d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 6 Jan 2020 23:25:23 -0800 Subject: [PATCH 096/407] - change how UserIdentityService is required to avoid circular references --- core/application.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/application.js b/core/application.js index a182db57eb..7307dbc1e1 100644 --- a/core/application.js +++ b/core/application.js @@ -16,7 +16,7 @@ var Target = require("./target").Target, DataStream = require("data/service/data-stream").DataStream, Criteria = require("core/criteria").Criteria, DataQuery = require("data/model/data-query").DataQuery, - UserIdentityService = require("data/service/user-identity-service").UserIdentityService, + UserIdentityService = undefined, UserIdentityManager = require("data/service/user-identity-manager").UserIdentityManager, Slot; @@ -470,7 +470,10 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio */ //URGENT: We need to further test that we don't already have a valid Authorization to use before authenticating. - + return require.async("data/service/user-identity-service"); + }) + .then(function(exports) { + UserIdentityService = exports.UserIdentityService; var userIdentityServices = UserIdentityService.userIdentityServices, userIdentityObjectDescriptors, From 862afea0fa590fc1f696182fb466dc89fc407626 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 7 Jan 2020 00:37:22 -0800 Subject: [PATCH 097/407] change to not expose wrapper if no native implementatioon is available, like in node.js --- core/web-socket.js | 443 +++++++++++++++++++++++---------------------- 1 file changed, 225 insertions(+), 218 deletions(-) diff --git a/core/web-socket.js b/core/web-socket.js index ba5585d7a0..89aa707395 100644 --- a/core/web-socket.js +++ b/core/web-socket.js @@ -7,234 +7,241 @@ var Target = require("./target").Target; var _WebSocket = global.WebSocket; -exports.WebSocket = Target.specialize({ - - /* - The constructor can throw exceptions from inside _connect(): - - SECURITY_ERR - The port to which the connection is being attempted is being blocked. - - */ - constructor: { - value: function (url, protocols) { - this._url = url; - this._protocols = protocols; - this._messageQueue = []; - this._webSocket = null; - this._isReconnecting = false; - this._connect(); - return this; - } - }, - - _url: { - value: undefined - }, - _protocols: { - value: undefined - }, - _messageQueue: { - value: undefined - }, - _webSocket: { - value: undefined - }, - reconnectionInterval: { - value: 100 - }, - - _connect: { - value: function () { - this._webSocket = new _WebSocket(this._url, this._protocols); - this._webSocket.addEventListener("error", this, false); - this._webSocket.addEventListener("open", this, false); - } - }, +if(_WebSocket) { + exports.WebSocket = Target.specialize({ - send: { - value: function send(data) { - this._messageQueue.push(data); - this._sendNextMessage(); - } - }, - - _sendNextMessage: { - value: function () { - if (this._messageQueue.length) { - switch (this.readyState) { - case WebSocket.CONNECTING: - break; - case WebSocket.CLOSING: - case WebSocket.CLOSED: - this._reconnect(); - break; - case WebSocket.OPEN: - try { - this._webSocket.send(this._messageQueue[0]); - this._messageQueue.shift(); - } catch (e) { + /* + The constructor can throw exceptions from inside _connect(): + + SECURITY_ERR + The port to which the connection is being attempted is being blocked. + + */ + constructor: { + value: function (url, protocols) { + this._url = url; + this._protocols = protocols; + this._messageQueue = []; + this._webSocket = null; + this._isReconnecting = false; + this._connect(); + return this; + } + }, + + _url: { + value: undefined + }, + _protocols: { + value: undefined + }, + _messageQueue: { + value: undefined + }, + _webSocket: { + value: undefined + }, + reconnectionInterval: { + value: 100 + }, + + _connect: { + value: function () { + this._webSocket = new _WebSocket(this._url, this._protocols); + this._webSocket.addEventListener("error", this, false); + this._webSocket.addEventListener("open", this, false); + } + }, + + send: { + value: function send(data) { + this._messageQueue.push(data); + this._sendNextMessage(); + } + }, + + _sendNextMessage: { + value: function () { + if (this._messageQueue.length) { + switch (this.readyState) { + case WebSocket.CONNECTING: + break; + case WebSocket.CLOSING: + case WebSocket.CLOSED: this._reconnect(); - } - break; + break; + case WebSocket.OPEN: + try { + this._webSocket.send(this._messageQueue[0]); + this._messageQueue.shift(); + } catch (e) { + this._reconnect(); + } + break; + } } } - } - }, + }, - _reconnect: { - value: function () { - var self; + _reconnect: { + value: function () { + var self; + + //if (this._messageQueue.length && !this._isReconnecting) { + if (!this._isReconnecting) { + self = this; + this._webSocket = null; + this._isReconnecting = true; + setTimeout(function () { + self._connect(); + self._isReconnecting = false; + }, Math.random() * this._reconnectionInterval); + this._reconnectionInterval *= 2; + if (this._reconnectionInterval > 30000) { + this._reconnectionInterval = 30000; + } + } + } + }, - //if (this._messageQueue.length && !this._isReconnecting) { - if (!this._isReconnecting) { - self = this; - this._webSocket = null; - this._isReconnecting = true; - setTimeout(function () { - self._connect(); - self._isReconnecting = false; - }, Math.random() * this._reconnectionInterval); - this._reconnectionInterval *= 2; - if (this._reconnectionInterval > 30000) { - this._reconnectionInterval = 30000; + handleEvent: { + value: function (event) { + switch (event.type) { + case "open": + this._reconnectionInterval = 100; + if (this._webSocket) { + this._webSocket.addEventListener("message", this, false); + this._webSocket.addEventListener("close", this, false); + } + this.dispatchEvent(event); + this._sendNextMessage(); + break; + case "message": + this.dispatchEvent(event); + this._sendNextMessage(); + break; + default: + this.dispatchEvent(event); + this._reconnect(); } } - } - }, - - handleEvent: { - value: function (event) { - switch (event.type) { - case "open": - this._reconnectionInterval = 100; - if (this._webSocket) { - this._webSocket.addEventListener("message", this, false); - this._webSocket.addEventListener("close", this, false); - } - this.dispatchEvent(event); - this._sendNextMessage(); - break; - case "message": - this.dispatchEvent(event); - this._sendNextMessage(); - break; - default: - this.dispatchEvent(event); - this._reconnect(); + }, + close: { + value: function close(code, reason) { + return this._webSocket.close(code, reason); } - } - }, - close: { - value: function close(code, reason) { - return this._webSocket.close(code, reason); - } - }, - binaryType: { - get: function () { - return this._webSocket.binaryType; }, - set: function (value) { - this._webSocket.binaryType = value; - } - }, - bufferedAmount: { - get: function () { - return this._webSocket.bufferedAmount; - } - }, - extensions: { - get: function () { - return this._webSocket.extensions; + binaryType: { + get: function () { + return this._webSocket.binaryType; + }, + set: function (value) { + this._webSocket.binaryType = value; + } }, - set: function (value) { - this._webSocket.extensions = value; - } - }, - protocol: { - get: function () { - return this._webSocket.protocol; - } - }, - readyState: { - get: function () { - return this._webSocket ? this._webSocket.readyState : WebSocket.CLOSED; - } - }, - - //Events Handlers - _onclose: { - value: undefined - }, - onclose: { - get: function () { - return this._onclose; - }, - set: function (eventListener) { - if(eventListener !== this._onclose) { - this.removeEventListener("close", this._onclose, false); - this._onclose = eventListener; - } - this.addEventListener("close", eventListener, false); - } - }, - _onerror: { - value: undefined - }, - onerror: { - get: function () { - return this._onerror; - }, - set: function (eventListener) { - if(eventListener !== this._onerror) { - this.removeEventListener("error", this._onerror, false); - this._onerror = eventListener; - } - this.addEventListener("error", eventListener, false); - } - }, - _onmessage: { - value: undefined - }, - onmessage: { - get: function () { - return this._onmessage; - }, - set: function (eventListener) { - if(eventListener !== this._onmessage) { - this.removeEventListener("message", this._onmessage, false); - this._onmessage = eventListener; - } - this.addEventListener("message", eventListener, false); + bufferedAmount: { + get: function () { + return this._webSocket.bufferedAmount; + } + }, + extensions: { + get: function () { + return this._webSocket.extensions; + }, + set: function (value) { + this._webSocket.extensions = value; + } + }, + protocol: { + get: function () { + return this._webSocket.protocol; + } + }, + readyState: { + get: function () { + return this._webSocket ? this._webSocket.readyState : WebSocket.CLOSED; + } + }, + + //Events Handlers + _onclose: { + value: undefined + }, + onclose: { + get: function () { + return this._onclose; + }, + set: function (eventListener) { + if(eventListener !== this._onclose) { + this.removeEventListener("close", this._onclose, false); + this._onclose = eventListener; + } + this.addEventListener("close", eventListener, false); + } + }, + _onerror: { + value: undefined + }, + onerror: { + get: function () { + return this._onerror; + }, + set: function (eventListener) { + if(eventListener !== this._onerror) { + this.removeEventListener("error", this._onerror, false); + this._onerror = eventListener; + } + this.addEventListener("error", eventListener, false); + } + }, + _onmessage: { + value: undefined + }, + onmessage: { + get: function () { + return this._onmessage; + }, + set: function (eventListener) { + if(eventListener !== this._onmessage) { + this.removeEventListener("message", this._onmessage, false); + this._onmessage = eventListener; + } + this.addEventListener("message", eventListener, false); + } + }, + _onopen: { + value: undefined + }, + onopen: { + get: function () { + return this._onopen; + }, + set: function (eventListener) { + if(eventListener !== this._onopen) { + this.removeEventListener("open", this._onopen, false); + this._onopen = eventListener; + } + this.addEventListener("open", eventListener, false); + } } - }, - _onopen: { - value: undefined - }, - onopen: { - get: function () { - return this._onopen; - }, - set: function (eventListener) { - if(eventListener !== this._onopen) { - this.removeEventListener("open", this._onopen, false); - this._onopen = eventListener; - } - this.addEventListener("open", eventListener, false); + + },{ + CONNECTING: { + value: 0 //The connection is not yet open. + }, + OPEN: { + value: 1 //The connection is open and ready to communicate. + }, + CLOSING: { + value: 2 //The connection is in the process of closing. + }, + CLOSED: { + value: 3 //The connection is closed or couldn't be opened. } - } - -},{ - CONNECTING: { - value: 0 //The connection is not yet open. - }, - OPEN: { - value: 1 //The connection is open and ready to communicate. - }, - CLOSING: { - value: 2 //The connection is in the process of closing. - }, - CLOSED: { - value: 3 //The connection is closed or couldn't be opened. - } -}); + }); + +} +else { + console.error("no native WebSocket implementation available"); +} + From 8de73c30582c8287911e50cb8af1236ffd021382 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 7 Jan 2020 18:49:25 -0800 Subject: [PATCH 098/407] Fix a typo --- data/service/data-operation.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index e29f211981..22b1e74360 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -14,7 +14,7 @@ var Montage = require("core/core").Montage, "copy", "copyfailed", "copycompleted", - /* Read is the first operation that mnodels a query */ + /* Read is the first operation that models a query */ "read", /* ReadUpdated is pushed by server when a query's result changes due to data changes from others */ @@ -52,7 +52,7 @@ var Montage = require("core/core").Montage, /* Lock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ "lock", - "kockcompleted", + "lockcompleted", "lockfailed", /* Unlock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ @@ -103,12 +103,12 @@ var Montage = require("core/core").Montage, Data components shpuld add themselves as listeners to the data service for events/ data operations like that they know how to deal with / can help with. */ - "userauthentication", - "userauthenticationupdate", - "userauthenticationcompleted", - "userauthenticationfailed", - "userauthenticationtimedout", - "userinput", + "userauthentication", + "userauthenticationupdate", + "userauthenticationcompleted", + "userauthenticationfailed", + "userauthenticationtimedout", + "userinput", "userinputcompleted", "userinputfailed", "userinputcanceled", From 05484b644b7e0f37ccb203b86e0d03006c0f48c5 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 27 Jan 2020 22:31:37 +0100 Subject: [PATCH 099/407] Add isBrowser property to environment --- core/environment.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/environment.js b/core/environment.js index 413a248309..f101079708 100644 --- a/core/environment.js +++ b/core/environment.js @@ -16,6 +16,10 @@ var Environment = exports.Environment = Montage.specialize({ } }, + isBrowser: { + value: (typeof window !== "undefined") + }, + _userAgent: { value: null }, From cd381e6b1ca73fe332de365a40e5cc3d31129f10 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 27 Jan 2020 22:32:43 +0100 Subject: [PATCH 100/407] add reusable ObjectPool. --- core/object-pool.js | 65 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 core/object-pool.js diff --git a/core/object-pool.js b/core/object-pool.js new file mode 100644 index 0000000000..e3a965de5b --- /dev/null +++ b/core/object-pool.js @@ -0,0 +1,65 @@ +var EMPTY_SLOT = Object.freeze(Object.create(null)) + defaultObjectFactory = function() { + return {}; + }; + +exports.ObjectPool = function(objectFactory, objectReseter) { + objectFactory = objectFactory || defaultObjectFactory; + var objPool = []; + var nextFreeSlot = null; // pool location to look for a free object to use + + return { + checkout, + checkin, + grow, + size, + }; + + + // ****************************** + + function checkout() { + if (nextFreeSlot == null || nextFreeSlot == objPool.length) { + grow( objPool.length || 5 ); + } + + var objToUse = objPool[nextFreeSlot]; + objPool[nextFreeSlot++] = EMPTY_SLOT; + return objToUse; + } + + function checkin(obj) { + objectReseter ? objectReseter(obj) : void 0; + if (nextFreeSlot == null || nextFreeSlot == -1) { + objPool[objPool.length] = obj; + } + else { + objPool[--nextFreeSlot] = obj; + } + } + + function grow(count) { + if(typeof count !== "number") count = objPool.length; + + if (count > 0 && nextFreeSlot == null) { + nextFreeSlot = 0; + } + + if (count > 0) { + var curLen = objPool.length; + objPool.length += Number(count); + + for (var i = curLen; i < objPool.length; i++) { + // add new obj to pool + objPool[i] = objectFactory(); + } + } + + return objPool.length; + } + + function size() { + return objPool.length; + } + }; + From 84e07a7257bac9b5e88f4160fcfbfbb23969baf1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 27 Jan 2020 22:33:45 +0100 Subject: [PATCH 101/407] change require to be compatible with node require --- core/uuid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/uuid.js b/core/uuid.js index 5f2ddb74b5..3d0da6b9ed 100644 --- a/core/uuid.js +++ b/core/uuid.js @@ -11,7 +11,7 @@ * @class Uuid * @extends Montage */ -var Montage = require("core/core").Montage, +var Montage = require("./core").Montage, CHARS = '0123456789ABCDEF'.split(''), PROTO = "__proto__", VALUE = "value", From ca758455e86b7140ab8a312a9c615df913865664 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 27 Jan 2020 22:35:30 +0100 Subject: [PATCH 102/407] add range ObjectDescriptor --- core/range.mjson | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 core/range.mjson diff --git a/core/range.mjson b/core/range.mjson new file mode 100644 index 0000000000..ddec2930dd --- /dev/null +++ b/core/range.mjson @@ -0,0 +1,55 @@ +{ + "begin_property_descriptor": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "begin", + "objectDescriptor": { "@": "root" }, + "valueType": "object", + "helpKey": "" + } + }, + "end_property_descriptor": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "end", + "objectDescriptor": { "@": "root" }, + "valueType": "object", + "helpKey": "" + } + }, + "bounds_property_descriptor": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "bounds", + "objectDescriptor": { "@": "root" }, + "valueType": "string", + "helpKey": "" + } + }, + "range": { + "object": "core/range" + }, + "root": { + "prototype": "core/meta/module-object-descriptor", + "values": { + "name": "Range", + "propertyDescriptors": [ + { "@": "begin_property_descriptor" }, + { "@": "end_property_descriptor" }, + { "@": "bounds_property_descriptor" } + ], + "propertyDescriptorGroups": { + "all": [ + { "@": "begin_property_descriptor" }, + { "@": "end_property_descriptor" }, + { "@": "bounds_property_descriptor" } + ] + }, + "propertyValidationRules": {}, + "objectDescriptorModule": { "%": "core/range.meta" }, + "exportName": "Range", + "module": { "%": "core/range" }, + "object":{ "@": "range" } + } + } +} From 6a2d21d61e6f9e09aa7b09c6ce66e7f07a1875f8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 27 Jan 2020 23:25:26 +0100 Subject: [PATCH 103/407] add date ISO8601 / RFC 3339 methods and some tests --- core/extras/date.js | 310 ++++++++++++++++++++++++++++++++ test/spec/core/date-spec.js | 340 ++++++++++++++++++++++++++++++++++++ 2 files changed, 650 insertions(+) create mode 100644 test/spec/core/date-spec.js diff --git a/core/extras/date.js b/core/extras/date.js index 3540ef7a3d..07a13234c4 100644 --- a/core/extras/date.js +++ b/core/extras/date.js @@ -23,3 +23,313 @@ Object.defineProperty(Date.prototype, "clone", { configurable: true }); +/** + * Assess if an instance a date is valid + * + * - date checks whether the parameter was not a falsy value (undefined, null, 0, "", etc..) + * - Object.prototype.toString.call(date) returns a native string representation of the given object type - In our case "[object Date]". + * Because date.toString() overrides its parent method, we need to .call or .apply the method from Object.prototype directly which .. + * Bypasses user-defined object type with the same constructor name (e.g.: "Date") + * Works across different JS contexts (e.g. iframes) in contrast to instanceof or Date.prototype.isPrototypeOf. + * - !isNaN(date) finally checks whether the value was not an Invalid Date. + * + * @function external:Date#isValid + * @returns {boolean} - result +*/ + +Object.defineProperty(Date, "isValidDate", { + value: function isValidDate(date) { + return date && Object.prototype.toString.call(date) === "[object Date]" && !isNaN(date); + }, + writable: true, + configurable: true +}); + + +/** + * Assess if a string is a known representation of a date + * + * + * @function external:Date#isValid + * @returns {boolean} - result +*/ + +Object.defineProperty(Date, "isValidDateString", { + value: function isValidDate(date) { + return date && Object.prototype.toString.call(date) === "[object Date]" && !isNaN(date); + }, + writable: true, + configurable: true +}); + + + +/* + * rfc3339date.js version 0.1.3 + * + * from https://github.com/tardate/rfc3339date.js/blob/master/rfc3339date.js + * + * Adds ISO 8601 / RFC 3339 date parsing to the Javascript Date object. + * Usage: + * var d = Date.parseISO8601( "2010-07-20T15:00:00Z" ); + * var d = Date.parse( "2010-07-20T15:00:00Z" ); + * Tested for compatibilty/coexistence with: + * - jQuery [http://jquery.com] + * - datejs [http://www.datejs.com/] + * + * Copyright (c) 2010 Paul GALLAGHER http://tardate.com + * Licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + */ + +/* + * Number.prototype.toPaddedString + * Number instance method used to left-pad numbers to the specified length + * Used by the Date.prototype.toRFC3339XXX methods + */ +Number.prototype.toPaddedString = function(len , fillchar) { + var result = this.toString(); + if(typeof(fillchar) == 'undefined'){ fillchar = '0' }; + while(result.length < len){ result = fillchar + result; }; + return result; + } + + /* + * Date.prototype.toRFC3339UTCString + * Date instance method to format the date as ISO8601 / RFC 3339 string (in UTC format). + * Usage: var d = new Date().toRFC3339UTCString(); + * => "2010-07-25T11:51:31.427Z" + * Parameters: + * supressFormating : if supplied and 'true', will force to remove date/time separators + * supressMillis : if supplied and 'true', will force not to include milliseconds + */ + Date.prototype.toRFC3339UTCString = function(supressFormating , supressMillis){ + var dSep = ( supressFormating ? '' : '-' ); + var tSep = ( supressFormating ? '' : ':' ); + var result = this.getUTCFullYear().toString(); + result += dSep + (this.getUTCMonth() + 1).toPaddedString(2); + result += dSep + this.getUTCDate().toPaddedString(2); + result += 'T' + this.getUTCHours().toPaddedString(2); + result += tSep + this.getUTCMinutes().toPaddedString(2); + result += tSep + this.getUTCSeconds().toPaddedString(2); + if((!supressMillis)&&(this.getUTCMilliseconds()>0)) result += '.' + this.getUTCMilliseconds().toPaddedString(3); + return result + 'Z'; + } + + /* + * Date.prototype.toRFC3339LocaleString + * Date instance method to format the date as ISO8601 / RFC 3339 string (in local timezone format). + * Usage: var d = new Date().toRFC3339LocaleString(); + * => "2010-07-25T19:51:31.427+08:00" + * Parameters: + * supressFormating : if supplied and 'true', will force to remove date/time separators + * supressMillis : if supplied and 'true', will force not to include milliseconds + */ + Date.prototype.toRFC3339LocaleString = function(supressFormating , supressMillis){ + var dSep = ( supressFormating ? '' : '-' ); + var tSep = ( supressFormating ? '' : ':' ); + var result = this.getFullYear().toString(); + result += dSep + (this.getMonth() + 1).toPaddedString(2); + result += dSep + this.getDate().toPaddedString(2); + result += 'T' + this.getHours().toPaddedString(2); + result += tSep + this.getMinutes().toPaddedString(2); + result += tSep + this.getSeconds().toPaddedString(2); + if((!supressMillis)&&(this.getMilliseconds()>0)) result += '.' + this.getMilliseconds().toPaddedString(3); + var tzOffset = -this.getTimezoneOffset(); + result += ( tzOffset<0 ? '-' : '+' ) + result += (tzOffset/60).toPaddedString(2); + result += tSep + (tzOffset%60).toPaddedString(2); + return result; + } + + /* + * Date.parseRFC3339 + * extend Date with a method parsing ISO8601 / RFC 3339 date strings. + * Usage: var d = Date.parseRFC3339( "2010-07-20T15:00:00Z" ); + */ + Date.parseRFC3339_RegExp = /(\d\d\d\d)(-)?(\d\d)(-)?(\d\d)(T)?(\d\d)(:)?(\d\d)?(:)?(\d\d)?([\.,]\d+)?($|Z|([+-])(\d\d)(:)?(\d\d)?)/i; + Date.endsByZ = /Z$/i; + Date.parseRFC3339 = function(dString){ + if (typeof dString != 'string' || !this.endsByZ.test(dString)) return; + var result, + d = dString.match(this.parseRFC3339_RegExp); + + if (d) { + var year = parseInt(d[1],10); + var mon = parseInt(d[3],10) - 1; + var day = parseInt(d[5],10); + var hour = parseInt(d[7],10); + var mins = ( d[9] ? parseInt(d[9],10) : 0 ); + var secs = ( d[11] ? parseInt(d[11],10) : 0 ); + var millis = ( d[12] ? parseFloat(String(1.5).charAt(1) + d[12].slice(1)) * 1000 : 0 ); + if (d[13]) { + result = new Date(0); + result.setUTCFullYear(year); + result.setUTCMonth(mon); + result.setUTCDate(day); + result.setUTCHours(hour); + result.setUTCMinutes(mins); + result.setUTCSeconds(secs); + result.setUTCMilliseconds(millis); + if (d[13] && d[14]) { + var offset = (d[15] * 60) + if (d[17]) offset += parseInt(d[17],10); + offset *= ((d[14] == '-') ? -1 : 1); + result.setTime(result.getTime() - offset * 60 * 1000); + } + } else { + result = new Date(year,mon,day,hour,mins,secs,millis); + } + } + return result; + }; + +/********** + * Part of https://github.com/tardate/rfc3339date.js/. + * But it not only conflicts with a Date.parse override currently done in date-converter.js, + * it's also wrong as JavaScript standard Date.parse doesn't return a date but a number of milliseconds + */ + + /* + * Date.parse + * extend Date with a parse method alias for parseRFC3339. + * If parse is already defined, chain methods to include parseRFC3339 + * Usage: var d = Date.parse( "2010-07-20T15:00:00Z" ); + */ +// if (typeof Date.parse != 'function') { +// Date.parse = Date.parseRFC3339; +// } else { +// var oldparse = Date.parse; +// Date.parse = function(d) { +// var result = Date.parseRFC3339(d); +// if (!result && oldparse) { +// result = oldparse(d); +// } +// return result; +// } +// } + + + +/* + from https://github.com/iamkun/dayjs/blob/dev/src/index.js#L53 + and + https://github.com/iamkun/dayjs/blob/dev/src/constant.js +*/ + +/* +export const SECONDS_A_MINUTE = 60 +export const SECONDS_A_HOUR = SECONDS_A_MINUTE * 60 +export const SECONDS_A_DAY = SECONDS_A_HOUR * 24 +export const SECONDS_A_WEEK = SECONDS_A_DAY * 7 + +export const MILLISECONDS_A_SECOND = 1e3 +export const MILLISECONDS_A_MINUTE = SECONDS_A_MINUTE * MILLISECONDS_A_SECOND +export const MILLISECONDS_A_HOUR = SECONDS_A_HOUR * MILLISECONDS_A_SECOND +export const MILLISECONDS_A_DAY = SECONDS_A_DAY * MILLISECONDS_A_SECOND +export const MILLISECONDS_A_WEEK = SECONDS_A_WEEK * MILLISECONDS_A_SECOND + +// English locales +export const MS = 'millisecond' +export const S = 'second' +export const MIN = 'minute' +export const H = 'hour' +export const D = 'day' +export const W = 'week' +export const M = 'month' +export const Q = 'quarter' +export const Y = 'year' +export const DATE = 'date' + +export const FORMAT_DEFAULT = 'YYYY-MM-DDTHH:mm:ssZ' + +export const INVALID_DATE_STRING = 'Invalid Date' + +// regex +export const REGEX_PARSE = /^(\d{4})-?(\d{1,2})-?(\d{0,2})[^0-9]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?.?(\d{1,3})?$/ +export const REGEX_FORMAT = /\[([^\]]+)]|Y{2,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g +*/ + +/* +const parseDate = (cfg) => { + const { date, utc } = cfg + if (date === null) return new Date(NaN) // null is invalid + if (Utils.u(date)) return new Date() // today + if (date instanceof Date) return new Date(date) + if (typeof date === 'string' && !/Z$/i.test(date)) { + const d = date.match(C.REGEX_PARSE) + if (d) { + if (utc) { + return new Date(Date.UTC(d[1], d[2] - 1, d[3] + || 1, d[4] || 0, d[5] || 0, d[6] || 0, d[7] || 0)) + } + return new Date(d[1], d[2] - 1, d[3] || 1, d[4] || 0, d[5] || 0, d[6] || 0, d[7] || 0) + } + } + + return new Date(date) // everything else + } +*/ + + +/* + other + https://jsfiddle.net/qm9osm4a/ + + from: https://stackoverflow.com/questions/522251/whats-the-difference-between-iso-8601-and-rfc-3339-date-formats +*/ +/* +var dateRegex = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})\.?(\d{3})?(?:(?:([+-]\d{2}):?(\d{2}))|Z)?$/; + + function parseISODate(d) { + var m = dateRegex.exec(d); + //milliseconds are optional. + if( m[7] === undefined ){ + m[7] = 0; + } + + //if timezone is undefined, it must be Z or nothing (otherwise the group would have captured). + if( m[8] === undefined && m[9] === undefined){ + //Use UTC. + m[8] = 0; + m[9] = 0; + } + + var year = +m[1]; + var month = +m[2]; + var day = +m[3]; + var hour = +m[4]; + var minute = +m[5]; + var second = +m[6]; + var msec = +m[7]; + var tzHour = +m[8]; + var tzMin = +m[9]; + var tzOffset = tzHour * 60 + tzMin; + + //console.log(year+', '+(month - 1)+', '+day+', '+hour+', '+(minute - tzOffset)+', '+second+', '+msec); + + return new Date(year, month - 1, day, hour, minute - tzOffset, second, msec); + } + + //Override the browser's default parse function first, but if that fails fall back to parseISODate. + Date.defaultParse = Date.parse; + Date.parse = function(d){ + var defaultVal; + try { + defaultVal = Date.defaultParse(d); + } + catch(err){} + if( defaultVal ){ + return defaultVal; + } + else{ + try { + return parseISODate(d).getTime(); + } + catch(err){ + return NaN; + } + } + } + */ diff --git a/test/spec/core/date-spec.js b/test/spec/core/date-spec.js new file mode 100644 index 0000000000..fb110bf936 --- /dev/null +++ b/test/spec/core/date-spec.js @@ -0,0 +1,340 @@ +require("montage/core/extras/date"); + +describe("core/date-spec", function () { + + it("should fallback on standard parse", function () { + var sourceString = "Jul 8, 2005"; + var actual = new Date(Date.parse( sourceString )); + var expected = new Date(); + expected.setFullYear(2005); + expected.setMonth(7 - 1); + expected.setDate(8); + expected.setHours(0); + expected.setMinutes(0); + expected.setSeconds(0); + expected.setMilliseconds(0); + //Error message: "incorrect conversion of: '" + sourceString + "'"; + expect(actual.toString()).toBe(expected.toString()); + }); + + it("should parse basic UTC", function () { + var sourceString = "2010-07-20T15:00:00Z"; + var resultDate = Date.parseRFC3339( sourceString ); + var expected = new Date(); + expected.setUTCFullYear(2010); + expected.setUTCMonth(7 - 1); + expected.setUTCDate(20); + expected.setUTCHours(15); + expected.setUTCMinutes(0); + expected.setUTCSeconds(0); + expected.setUTCMilliseconds(0); + //Error message: "incorrect conversion of: '" + sourceString + "'"; + expect(resultDate.toUTCString()).toBe(expected.toUTCString()); + }); + + it("should parse basic local time", function () { + var sourceString = "2010-07-20T15:00:00"; + var actual = Date.parse( sourceString ); + var expected = new Date(2010, 7-1 , 20, 15, 0, 0, 0); + //Error message: "incorrect conversion of: '" + sourceString + "'" ; + expect(actual.toString()).toBe(expected.toString()); + }); + + + it("should parse basic local time with zone", function () { + var sourceString = "2010-07-20T15:00:00+08:00"; + var actual = Date.parse( sourceString ); + var expected = new Date(); + expected.setUTCFullYear(2010); + expected.setUTCMonth(7 - 1); + expected.setUTCDate(20); + expected.setUTCHours(7); + expected.setUTCMinutes(0); + expected.setUTCSeconds(0); + expected.setUTCMilliseconds(0); + //Error message: "incorrect conversion of: '" + sourceString + "'" ; + expect(actual.toUTCString()).toBe(expected.toUTCString()); + }); + + it("should parse abbreviate zone with no colon", function () { + var sourceString = "2010-07-20T15:00:00+0800"; + var actual = Date.parse( sourceString ); + var expected = new Date(); + expected.setUTCFullYear(2010); + expected.setUTCMonth(7 - 1); + expected.setUTCDate(20); + expected.setUTCHours(7); + expected.setUTCMinutes(0); + expected.setUTCSeconds(0); + expected.setUTCMilliseconds(0); + //Error message: "incorrect conversion of: '" + sourceString + "'" ; + expect(actual.toUTCString()).toBe(expected.toUTCString()); + }); + + it("should parse abbreviate zone only hours", function () { + var sourceString = "2010-07-20T15:00:00+08"; + var actual = Date.parse( sourceString ); + var expected = new Date(); + expected.setUTCFullYear(2010); + expected.setUTCMonth(7 - 1); + expected.setUTCDate(20); + expected.setUTCHours(7); + expected.setUTCMinutes(0); + expected.setUTCSeconds(0); + expected.setUTCMilliseconds(0); + //Error message: "incorrect conversion of: '" + sourceString + "'" ; + expect(actual.toUTCString()).toBe(expected.toUTCString()); + }); + + it("should parse abbreviate UTC no punctuation", function () { + var sourceString = "20100720T150000Z"; + var resultDate = Date.parseRFC3339( sourceString ); + var expected = new Date(); + expected.setUTCFullYear(2010); + expected.setUTCMonth(7 - 1); + expected.setUTCDate(20); + expected.setUTCHours(15); + expected.setUTCMinutes(0); + expected.setUTCSeconds(0); + expected.setUTCMilliseconds(0); + //Error message: "incorrect conversion of: '" + sourceString + "'" ; + expect(cresultDate.toUTCString()).toBe(expected.toUTCString()); + }); + + it("should parse not case sensitive", function () { + var sourceString = "20100720t150000z"; + var resultDate = Date.parseRFC3339( sourceString ); + var expected = new Date(); + expected.setUTCFullYear(2010); + expected.setUTCMonth(7 - 1); + expected.setUTCDate(20); + expected.setUTCHours(15); + expected.setUTCMinutes(0); + expected.setUTCSeconds(0); + expected.setUTCMilliseconds(0); + //Error message: "incorrect conversion of: '" + sourceString + "'" ; + expect(resultDate.toUTCString()).toBe(expected.toUTCString()); + }); + + it("should parse fractional seconds", function () { + var sourceString = "2010-07-20T15:00:00.559Z"; + var actual = Date.parseRFC3339( sourceString ); + var expected = new Date(); + expected.setUTCFullYear(2010); + expected.setUTCMonth(7 - 1); + expected.setUTCDate(20); + expected.setUTCHours(15); + expected.setUTCMinutes(0); + expected.setUTCSeconds(0); + expected.setUTCMilliseconds(559); + //Error message: "incorrect conversion of: '" + sourceString + "'" ; + expect(actual.toUTCString()).toBe(expected.toUTCString()); + }); + + it("should parse fractional seconds alternate separator", function () { + var sourceString = "2010-07-20T15:00:00,559Z"; + var actual = Date.parseRFC3339( sourceString ); + var expected = new Date(); + expected.setUTCFullYear(2010); + expected.setUTCMonth(7 - 1); + expected.setUTCDate(20); + expected.setUTCHours(15); + expected.setUTCMinutes(0); + expected.setUTCSeconds(0); + expected.setUTCMilliseconds(559); + //Error message: "incorrect conversion of: '" + sourceString + "'" ; + expect(actual.toUTCString()).toBe(expected.toUTCString()); + }); + + it("should parse UTC optional minutes", function () { + var sourceString = "2010-07-20T15Z"; + var actual = Date.parseRFC3339( sourceString ); + var expected = new Date(); + expected.setUTCFullYear(2010); + expected.setUTCMonth(7 - 1); + expected.setUTCDate(20); + expected.setUTCHours(15); + expected.setUTCMinutes(0); + expected.setUTCSeconds(0); + expected.setUTCMilliseconds(0); + //Error message: "incorrect conversion of: '" + sourceString + "'" ; + expect(actual.toUTCString()).toBe(expected.toUTCString()); + }); + + it("should parse UTC optional seconds", function () { + var sourceString = "2010-07-20T15:30Z"; + var actual = Date.parseRFC3339( sourceString ); + var expected = new Date(); + expected.setUTCFullYear(2010); + expected.setUTCMonth(7 - 1); + expected.setUTCDate(20); + expected.setUTCHours(15); + expected.setUTCMinutes(30); + expected.setUTCSeconds(0); + expected.setUTCMilliseconds(0); + //Error message: "incorrect conversion of: "incorrect conversion of: '" + sourceString + "'" ; + expect(actual.toUTCString()).toBe(expected.toUTCString()); + }); + + it("should parse local time optional minutes", function () { + var sourceString = "2010-07-20T15"; + var actual = Date.parse( sourceString ); + var expected = new Date(2010, 7-1 , 20, 15, 0, 0, 0); + //Error message: "incorrect conversion of: '" + sourceString + "'" ); + expect(actual.toUTCString()).toBe(expected.toUTCString()); + }); + + it("should parse local time optional seconds", function () { + var sourceString = "2010-07-20T1530"; + var actual = Date.parse( sourceString ); + var expected = new Date(2010, 7-1 , 20, 15, 30, 0, 0); + //Error message: "incorrect conversion of: '" + sourceString + "'" ; + expect(actual.toUTCString()).toBe(expected.toUTCString()); + }); + + it("should convert basic format UTC to string", function () { + var expected = "2010-07-20T15:00:00Z"; + var actual = new Date(); + actual.setUTCFullYear(2010); + actual.setUTCMonth(7 - 1); + actual.setUTCDate(20); + actual.setUTCHours(15); + actual.setUTCMinutes(0); + actual.setUTCSeconds(0); + actual.setUTCMilliseconds(0); + //Error message: "incorrect toRFC3339UTCString format" ); + expect(actual.toRFC3339UTCString()).toBe(expected); + }); + + it("should convert format UTC supress formating", function () { + var expected = "20100720T150000Z"; + var actual = new Date(); + actual.setUTCFullYear(2010); + actual.setUTCMonth(7 - 1); + actual.setUTCDate(20); + actual.setUTCHours(15); + actual.setUTCMinutes(0); + actual.setUTCSeconds(0); + actual.setUTCMilliseconds(0); + //Error message: "incorrect toRFC3339UTCString format" ); + expect(actual.toRFC3339UTCString(true)).toBe(expected); + }); + + it("should convert format UTC force formating", function () { + var expected = "2010-07-20T15:00:00Z"; + var actual = new Date(); + actual.setUTCFullYear(2010); + actual.setUTCMonth(7 - 1); + actual.setUTCDate(20); + actual.setUTCHours(15); + actual.setUTCMinutes(0); + actual.setUTCSeconds(0); + actual.setUTCMilliseconds(0); + //Error message: "incorrect toRFC3339UTCString format" ); + expect(actual.toRFC3339UTCString(false)).toBe(expected); + }); + + it("should convert format UTC supress formating and millis", function () { + var expected = "20100720T150000Z"; + var actual = new Date(); + actual.setUTCFullYear(2010); + actual.setUTCMonth(7 - 1); + actual.setUTCDate(20); + actual.setUTCHours(15); + actual.setUTCMinutes(0); + actual.setUTCSeconds(0); + actual.setUTCMilliseconds(333); + //Error message: "incorrect toRFC3339UTCString format" ; + expect(actual.toRFC3339UTCString(true, true)).toBe(expected); + }); + + it("should convert format UTC force formating and millis", function () { + var expected = "2010-07-20T15:00:00.333Z"; + var actual = new Date(); + actual.setUTCFullYear(2010); + actual.setUTCMonth(7 - 1); + actual.setUTCDate(20); + actual.setUTCHours(15); + actual.setUTCMinutes(0); + actual.setUTCSeconds(0); + actual.setUTCMilliseconds(333); + //Error message: "incorrect toRFC3339UTCString format" ; + expect(actual.toRFC3339UTCString(false, false)).toBe(expected); + }); + + it("should convert format UTC supress formating and force millis", function () { + var expected = "20100720T150000.333Z"; + var actual = new Date(); + actual.setUTCFullYear(2010); + actual.setUTCMonth(7 - 1); + actual.setUTCDate(20); + actual.setUTCHours(15); + actual.setUTCMinutes(0); + actual.setUTCSeconds(0); + actual.setUTCMilliseconds(333); + //Error message: "incorrect toRFC3339UTCString format" ; + expect(actual.toRFC3339UTCString(true, false)).toBe(expected); + }); + + it("should convert basic format local", function () { + // it is a bit tricky to test this in a way that works in any time zone, + // so we are testing that parsing the formated date should get you back the original date + var expected = new Date(2010, 7-1 , 20, 15, 0, 0, 0); + var resultLocal = expected.toRFC3339LocaleString(); + var resultReconstituted = Date.parse( resultLocal ); + //Error message: "incorrect toRFC3339LocaleString format" ); + expect(resultReconstituted.toUTCString()).toBe(expected.toUTCString()); + }); + + it("should parse day out of bounds", function () { + /* case: + * if today to 31st, the month to be parsed + * doesnt have the 31st day, it rolls over to the next month + */ + + //ref old date + var _Date = window.Date; + + //date mock + window.Date = function(){ + if (arguments.length){ + //pass arguments through + return new _Date(arguments); + } else { + //force today to be... + return new _Date(2012, 0, 31, 15, 0, 0, 0); + } + } + + Date.parseRFC3339 = _Date.parseRFC3339; + Date.parse = _Date.parse; + + //sourceString's month doesnt have a 31st + var sourceString = "2012-04-31T23:30:00Z"; + var actual = Date.parseRFC3339( sourceString ); + + var expected = new Date(0); + var year = 2012; + var mon = 4; + var day = 1; + var hour = 23; + var mins = 30; + var secs = 0; + var millis = 0; + + //set utc + expected.setUTCFullYear(year); + expected.setUTCMonth(mon); + expected.setUTCDate(day); + expected.setUTCHours(hour); + expected.setUTCMinutes(mins); + expected.setUTCSeconds(secs); + expected.setUTCMilliseconds(millis); + + //Error message: "datejs parse method probably not invoked correctly to convert: '" + sourceString + "'" ); + expect(actual.toUTCString()).toBe(expected.toUTCString()); + window.Date = _Date; + }); + +}); + From 5ed1542812248403bc494000cac8720c3446f33e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 27 Jan 2020 23:26:28 +0100 Subject: [PATCH 104/407] defines constants locally to run in node --- core/event/event-manager.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/core/event/event-manager.js b/core/event/event-manager.js index fdddbf20f6..cc2d7bac3e 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -20,9 +20,13 @@ var Montage = require("../core").Montage, Deserializer = require("../serialization/deserializer/montage-deserializer").MontageDeserializer, Map = require("collections/map"), WeakMap = require("collections/weak-map"), - currentEnvironment = require("../environment").currentEnvironment; - -var defaultEventManager; + currentEnvironment = require("../environment").currentEnvironment, + isBrowser = currentEnvironment.isBrowser, + Event_NONE = 0, + Event_CAPTURING_PHASE = 1, + Event_AT_TARGET = 2, + Event_BUBBLING_PHASE = 3, + defaultEventManager; //This is a quick polyfill for IE10 that is not exposing CustomEvent as a function. //From https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill @@ -2521,9 +2525,11 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan handleEvent: { enumerable: false, value: function (event) { - if ((window.MontageElement && event.target instanceof MontageElement) || - (event instanceof UIEvent && !this._shouldDispatchEvent(event))) { - return void 0; + if(isBrowser) { + if ((window.MontageElement && event.target instanceof MontageElement) || + (event instanceof UIEvent && !this._shouldDispatchEvent(event))) { + return void 0; + } } if (this.monitorDOMModificationInEventHandling) { @@ -2572,7 +2578,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan } mutableEventTarget = mutableEvent.target; - if (Element.isElement(mutableEventTarget) || mutableEventTarget instanceof Document || mutableEventTarget === window) { + if (isBrowser && (Element.isElement(mutableEventTarget) || mutableEventTarget instanceof Document || mutableEventTarget === window)) { eventPath = this._eventPathForDomTarget(mutableEventTarget); } else { eventPath = this._eventPathForTarget(mutableEventTarget); @@ -2601,7 +2607,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan } // Capture Phase Distribution - mutableEvent.eventPhase = Event.CAPTURING_PHASE; + mutableEvent.eventPhase = Event_CAPTURING_PHASE; // The event path we generate is from bottom to top, capture needs to traverse this backwards for (i = eventPath.length - 1; !mutableEvent.propagationStopped && (iTarget = eventPath[i]); i--) { mutableEvent.currentTarget = iTarget; @@ -2627,7 +2633,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan // At Target Distribution if (!mutableEvent.propagationStopped) { - mutableEvent.eventPhase = Event.AT_TARGET; + mutableEvent.eventPhase = Event_AT_TARGET; mutableEvent.currentTarget = iTarget = mutableEventTarget; //Capture listenerEntries = this._registeredEventListenersForEventType_onTarget_registeredEventListeners_(eventType, iTarget, registeredCaptureEventListeners); @@ -2666,7 +2672,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan } // Bubble Phase Distribution - mutableEvent.eventPhase = Event.BUBBLING_PHASE; + mutableEvent.eventPhase = Event_BUBBLING_PHASE; for (i = 0; eventBubbles && !mutableEvent.propagationStopped && (iTarget = eventPath[i]); i++) { mutableEvent.currentTarget = iTarget; @@ -2689,7 +2695,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan } } - mutableEvent.eventPhase = Event.NONE; + mutableEvent.eventPhase = Event_NONE; mutableEvent.currentTarget = null; if (this._isStoringPointerEvents) { @@ -2742,6 +2748,8 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan * Ensure that any components associated with DOM elements in the hierarchy between the * original activationEvent target and the window are preparedForActionEvents * + * Benoit todo: Make this more generic and configurable to be applicable to different trees. + * * @function * @private */ From b14a508a80cb74e31a35dbae4279b13fcaa7026a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 27 Jan 2020 23:29:42 +0100 Subject: [PATCH 105/407] support for Date in serialization/deserialization through ISO8601 / RFC 3339 format --- .../deserializer/montage-reviver.js | 27 +++++++++ core/serialization/serializer/montage-ast.js | 17 +++++- .../serializer/montage-builder.js | 8 ++- .../serializer/montage-malker.js | 23 +++++-- .../serializer/montage-visitor.js | 6 ++ .../montage-deserializer-spec.js | 60 +++++++++++++++++++ .../serialization/montage-serializer-spec.js | 7 ++- 7 files changed, 140 insertions(+), 8 deletions(-) diff --git a/core/serialization/deserializer/montage-reviver.js b/core/serialization/deserializer/montage-reviver.js index a81710803f..c7bf6375c3 100644 --- a/core/serialization/deserializer/montage-reviver.js +++ b/core/serialization/deserializer/montage-reviver.js @@ -14,6 +14,7 @@ var Montage = require("../../core").Montage, TWO_WAY = "<->"; require("../../shim/string"); +require("core/extras/date"); var PROXY_ELEMENT_MAP = new WeakMap(); var DATA_ATTRIBUTES_MAP = new Map(); @@ -152,6 +153,11 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont return "null"; } else if (Array.isArray(value)) { return "array"; + } + //TODO: would be great to optimize and not create twice a date as we do now. Once to parse + //and another time later when we need the value + else if(Date.parseRFC3339(value)) { + return "date"; } else if (typeOf === "object" && Object.keys(value).length === 1) { if ("@" in value) { return "reference"; @@ -655,6 +661,12 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont value: function (context) { this._deserializeUnits(context); this._invokeDeserializedFromSerialization(context); + + //We avoided calling deserializedFromSerialization() + //on pre-existing objects that are passed on deserializer. + //We special case that in template.js, calling + //templateDidLoad() on the owner. We could generalize + //this here. } }, @@ -854,6 +866,8 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont if (type === "string" || type === "number" || type === "boolean" || type === "null" || type === "undefined") { revived = this.reviveNativeValue(value, context, label); + } else if (type === "date") { + revived = this.reviveDate(value, context, label); } else if (type === "regexp") { revived = this.reviveRegExp(value, context, label); } else if (type === "reference") { @@ -948,6 +962,19 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont } }, + reviveDate: { + value: function(value, context, label) { + + var date = Date.parseRFC3339(value); + + if (label) { + context.setObjectLabel(date, label); + } + + return date; + } + }, + reviveObjectReference: { value: function(value, context, label) { var valuePath = value["@"], diff --git a/core/serialization/serializer/montage-ast.js b/core/serialization/serializer/montage-ast.js index 9c6a0f410c..8d1c91c086 100644 --- a/core/serialization/serializer/montage-ast.js +++ b/core/serialization/serializer/montage-ast.js @@ -56,7 +56,7 @@ var Root = exports.Root = Montage.specialize({ result[label] = object.toJSON(label, 1); } else { result[label] = object; - } + } } } @@ -301,3 +301,18 @@ var RegExpObject = exports.RegExpObject = ReferenceableValue.specialize( /** @le } } }); + +var DateObject = exports.DateObject = Value.specialize( /** @lends RegExpObject# */ { + + constructor: { + value: function DateObject(root, date) { + Value.call(this, root, date); + } + }, + + _getSerializationValue: { + value: function() { + return this.value.toISOString(); + } + } +}); diff --git a/core/serialization/serializer/montage-builder.js b/core/serialization/serializer/montage-builder.js index 2eea580ac5..7fb3a743ce 100644 --- a/core/serialization/serializer/montage-builder.js +++ b/core/serialization/serializer/montage-builder.js @@ -128,7 +128,7 @@ var MontageBuilder = Montage.specialize(/** @lends MontageBuilder# */ { if (Object.hasOwnProperty.call(references, label)) { if (!root.hasProperty(label)) { root.setProperty(label, this._placeholderProperty); - } + } } } } @@ -200,6 +200,12 @@ var MontageBuilder = Montage.specialize(/** @lends MontageBuilder# */ { } }, + createDate: { + value: function(date) { + return new MontageAst.DateObject(this._root, date); + } + }, + createNumber: { value: function(number) { return new MontageAst.Value(this._root, number); diff --git a/core/serialization/serializer/montage-malker.js b/core/serialization/serializer/montage-malker.js index aee6bbf299..be7a8a2f05 100644 --- a/core/serialization/serializer/montage-malker.js +++ b/core/serialization/serializer/montage-malker.js @@ -1,4 +1,7 @@ -var Montage = require("../../core").Montage; +var Montage = require("../../core").Montage, +Set = require("collections/set"); + +require("../../extras/date"); var MontageWalker = exports.MontageWalker = Montage.specialize({ _visitHandler: {value: null}, @@ -9,25 +12,25 @@ var MontageWalker = exports.MontageWalker = Montage.specialize({ value: function Malker(visitHandler, legacyMode) { this._visitHandler = visitHandler; this.legacyMode = !!legacyMode; - this._enteredObjects = {}; + this._enteredObjects = new Set(); } }, cleanup: { value: function() { - this._enteredObjects = {}; + this._enteredObjects.clear(); } }, _isObjectEntered: { value: function(object) { - return Object.hash(object) in this._enteredObjects; + return this._enteredObjects.has(object); } }, _markObjectAsEntered: { value: function(object) { - this._enteredObjects[Object.hash(object)] = true; + this._enteredObjects.add(object); } }, @@ -46,6 +49,8 @@ var MontageWalker = exports.MontageWalker = Montage.specialize({ return "regexp"; } else if (value === null) { return "null"; + } else if(Date.isValidDate(value)) { + return "date"; } else if (typeof value === "object" || typeof value === "function") { return this._getObjectType(value); } else { @@ -83,6 +88,8 @@ var MontageWalker = exports.MontageWalker = Montage.specialize({ this._visitRegExp(value, name); } else if (type === "number") { this._visitNumber(value, name); + } else if (type === "date") { + this._visitDate(value, name); } else if (type === "string") { this._visitString(value, name); } else if (type === "boolean") { @@ -182,6 +189,12 @@ var MontageWalker = exports.MontageWalker = Montage.specialize({ } }, + _visitDate: { + value: function(date, name) { + this._callVisitorMethod("visitDate", date, name); + } + }, + _visitNumber: { value: function(number, name) { this._callVisitorMethod("visitNumber", number, name); diff --git a/core/serialization/serializer/montage-visitor.js b/core/serialization/serializer/montage-visitor.js index 764cff166d..f5f3070fb4 100644 --- a/core/serialization/serializer/montage-visitor.js +++ b/core/serialization/serializer/montage-visitor.js @@ -614,6 +614,12 @@ var MontageVisitor = Montage.specialize({ } }, + visitDate: { + value: function(malker, date, name) { + this.storeValue(this.builder.createDate(date), date, name); + } + }, + visitNumber: { value: function(malker, number, name) { this.storeValue(this.builder.createNumber(number), number, name); diff --git a/test/spec/serialization/montage-deserializer-spec.js b/test/spec/serialization/montage-deserializer-spec.js index 739c05ec91..1b28dff311 100644 --- a/test/spec/serialization/montage-deserializer-spec.js +++ b/test/spec/serialization/montage-deserializer-spec.js @@ -1341,6 +1341,66 @@ describe("serialization/montage-deserializer-spec", function () { }); describe("deserialization", function() { + it("should deserialize native types", function (done) { + var expectedResult = { + string: "string", + date: new Date('05 October 2011 14:48 UTC'), + number: 42, + regexp: /regexp/gi, + array: [1, 2, 3], + boolean: true, + nil: null + }, + expectedSerialization, + deserializer, + serializationString; + + expectedResult.object = expectedResult; + + serializationString = { + object: { + value: { + string: "string", + date: "2011-10-05T14:48:00.000Z", + number: 42, + regexp: {"/": {source: "regexp", flags: "gi"}}, + array: {"@": "array"}, + boolean: true, + nil: null, + object: {"@": "object"} + } + }, + array: { + value: [1, 2, 3] + }, + string: { + value: "string" + }, + date: { + value: "2011-10-05T14:48:00.000Z" + }, + number: { + value: 42 + }, + regexp: { + value: {"/": {source: "regexp", flags: "gi"}} + }, + boolean: { + value: true + }, + nil: { + value: null + } + }; + + deserializer = new Deserializer().init(serializationString, require); + deserializer.deserialize().then(function(objects) { + expect(objects).toEqual(jasmine.objectContaining(expectedResult)); + }).finally(function () { + done(); + }); + }); + it("should deserialize a serialization string", function(done) { var serialization = { "string": { diff --git a/test/spec/serialization/montage-serializer-spec.js b/test/spec/serialization/montage-serializer-spec.js index dc522326ba..80283e4a18 100644 --- a/test/spec/serialization/montage-serializer-spec.js +++ b/test/spec/serialization/montage-serializer-spec.js @@ -15,7 +15,7 @@ var Montage = require("montage/core/core").Montage, } }); } - + function createFakeModuleReference(id, _require) { return new ModuleReference().initWithIdAndRequire(id, _require || require); } @@ -39,6 +39,7 @@ describe("spec/serialization/montage-serializer-spec", function () { it("should serialize native types", function () { var object = { string: "string", + date: new Date('05 October 2011 14:48 UTC'), number: 42, regexp: /regexp/gi, array: [1, 2, 3], @@ -52,6 +53,7 @@ describe("spec/serialization/montage-serializer-spec", function () { object: { value: { string: "string", + date: "2011-10-05T14:48:00.000Z", number: 42, regexp: {"/": {source: "regexp", flags: "gi"}}, array: {"@": "array"}, @@ -66,6 +68,9 @@ describe("spec/serialization/montage-serializer-spec", function () { string: { value: "string" }, + date: { + value: "2011-10-05T14:48:00.000Z" + }, number: { value: 42 }, From 8a4c59e4b65f9c7f83aed45a84282a45c2cdc745 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 30 Jan 2020 09:45:58 +0100 Subject: [PATCH 106/407] Fixed defaultPrevented when no value and no _event is set first --- core/event/mutable-event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/event/mutable-event.js b/core/event/mutable-event.js index 79a3fb2380..11d796f990 100644 --- a/core/event/mutable-event.js +++ b/core/event/mutable-event.js @@ -279,7 +279,7 @@ var wrapPropertyGetter = function (key, storageKey) { */ defaultPrevented: { get: function () { - return (this._defaultPrevented !== void 0) ? this._defaultPrevented : this._event.defaultPrevented; + return (this._defaultPrevented !== void 0) ? this._defaultPrevented : (this._event ? this._event.defaultPrevented : false); }, set: function (value) { this._defaultPrevented = value; From 3421c32e7c86b3a1a4a9cab4c3a01c0c7aaecbf2 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 30 Jan 2020 09:50:20 +0100 Subject: [PATCH 107/407] add support for dispatching data events and some comments --- core/meta/object-descriptor.js | 38 ++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/core/meta/object-descriptor.js b/core/meta/object-descriptor.js index 04d5918ce3..a7ced19099 100644 --- a/core/meta/object-descriptor.js +++ b/core/meta/object-descriptor.js @@ -305,10 +305,11 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends identifier: { get: function () { // TODO convert UpperCase to lower-case instead of lowercase - return [ - "objectDescriptor", - (this.name || "unnamed").toLowerCase() - ].join("_"); + return this.name; + // return [ + // "objectDescriptor", + // (this.name || "unnamed").toLowerCase() + // ].join("_"); } }, @@ -356,6 +357,19 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends } }, + /** + * An OjectDescriptor's next target is it's parent or in the end the mainService. + * @property {boolean} serializable + * @property {Component} value + */ + nextTarget: { + serializable: false, + get: function() { + return this.parent || ObjectDescriptor.mainService; + } + }, + + /** * Defines whether the object descriptor should use custom prototype for new * instances. @@ -989,6 +1003,22 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends for (name in this._propertyValidationRules) { if (this._propertyValidationRules.hasOwnProperty(name)) { rule = this._propertyValidationRules[name]; + //Benoit: It's likely that some rules might be async + //or we might decide to differ the ones that are async + //to be run on the server instead, but right now the code + //doesn't anticipate any async rule. + + /* + TODO + Also to help reconciliate validation with HTML5 standards + and its ValidityState object (https://developer.mozilla.org/en-US/docs/Web/API/ValidityState), or to know what key/expression failed validation, we need to return a map key/reason, and not just an array, so that each message can end-up being processed/communicated to the user by the component editing it. + + It might even make sense that each component editing a certain property of an object, or any combination of some + would have to "observe/listen" to individual property validations. + + This one is meant to run on all as some rules can involve multiple properties. + */ + if (rule.evaluateRule(objectInstance)) { messages.push(rule.messageKey); } From 2d8fb3badc75f38ef92751cb6279452459b3d534 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 30 Jan 2020 09:51:13 +0100 Subject: [PATCH 108/407] add deleteRule and description properties --- core/meta/property-descriptor.js | 85 +++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 28 deletions(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index ae757b1885..9cbcfdbe5a 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -1,8 +1,37 @@ var Montage = require("../core").Montage, Promise = require("../promise").Promise, deprecate = require("../deprecate"), + Enum = require("core/enum").Enum, logger = require("../logger").logger("objectDescriptor"); +/* TypeDescriptor */ +/* DeleteRules */ + +/* + Deny + If there is at least one object at the relationship destination (employees), do not delete the source object (department). + + For example, if you want to remove a department, you must ensure that all the employees in that department are first transferred elsewhere (or fired!); otherwise, the department cannot be deleted. + + Nullify + Remove the relationship between the objects but do not delete either object. + + This only makes sense if the department relationship for an employee is optional, or if you ensure that you set a new department for each of the employees before the next save operation. + + Cascade + Delete the objects at the destination of the relationship when you delete the source. + + For example, if you delete a department, fire all the employees in that department at the same time. + + No Action -> IGNORE + Do nothing to the object at the destination of the relationship. + + Default + Value that will be assigned ? + + */ +exports.DeleteRule = DeleteRule = new Enum().initWithMembersAndValues(["NULLIFY","CASCADE","DENY","IGNORE"]); + // TODO: Replace Defaults by leveraging the value set on the prototype which really is the natural default var Defaults = { name: "default", @@ -10,6 +39,7 @@ var Defaults = { mandatory: false, readOnly: false, denyDelete: false, + deleteRule: DeleteRule.Nullify, inversePropertyName: void 0, valueType: "string", collectionValueType: "list", @@ -51,32 +81,6 @@ TODO: */ -/* TypeDescriptor */ -/* DeleteRules */ - -/* - Deny - If there is at least one object at the relationship destination (employees), do not delete the source object (department). - - For example, if you want to remove a department, you must ensure that all the employees in that department are first transferred elsewhere (or fired!); otherwise, the department cannot be deleted. - - Nullify - Remove the relationship between the objects but do not delete either object. - - This only makes sense if the department relationship for an employee is optional, or if you ensure that you set a new department for each of the employees before the next save operation. - - Cascade - Delete the objects at the destination of the relationship when you delete the source. - - For example, if you delete a department, fire all the employees in that department at the same time. - - No Action - Do nothing to the object at the destination of the relationship. - - Default - Value that will be assigned ? - - */ /** @@ -128,6 +132,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._setPropertyWithDefaults(serializer, "mandatory", this.mandatory); this._setPropertyWithDefaults(serializer, "readOnly", this.readOnly); this._setPropertyWithDefaults(serializer, "denyDelete", this.denyDelete); + this._setPropertyWithDefaults(serializer, "deleteRule", this.deleteRule); this._setPropertyWithDefaults(serializer, "valueType", this.valueType); this._setPropertyWithDefaults(serializer, "collectionValueType", this.collectionValueType); this._setPropertyWithDefaults(serializer, "valueObjectPrototypeName", this.valueObjectPrototypeName); @@ -145,6 +150,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._setPropertyWithDefaults(serializer, "isSearcheable", this.isSearcheable); this._setPropertyWithDefaults(serializer, "isOrdered", this.isOrdered); this._setPropertyWithDefaults(serializer, "hasUniqueValues", this.hasUniqueValues); + this._setPropertyWithDefaults(serializer, "description", this.description); } }, @@ -170,6 +176,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._overridePropertyWithDefaults(deserializer, "mandatory"); this._overridePropertyWithDefaults(deserializer, "readOnly"); this._overridePropertyWithDefaults(deserializer, "denyDelete"); + this._overridePropertyWithDefaults(deserializer, "deleteRule"); this._overridePropertyWithDefaults(deserializer, "valueType"); this._overridePropertyWithDefaults(deserializer, "collectionValueType"); this._overridePropertyWithDefaults(deserializer, "valueObjectPrototypeName"); @@ -185,6 +192,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._overridePropertyWithDefaults(deserializer, "isSearcheable"); this._overridePropertyWithDefaults(deserializer, "isOrdered"); this._overridePropertyWithDefaults(deserializer, "hasUniqueValues"); + this._overridePropertyWithDefaults(deserializer, "description"); } }, @@ -265,12 +273,23 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * @type {string} */ name: { - serializable:false, + serializable:true, get:function () { return this._name; } }, + /** + * Description of the property descriptor + * object. + * @readonly + * @type {string} + */ + description: { + serializable:true, + value: undefined + }, + /** * The identifier is the name of the descriptor, dot, the name of the * property descriptor, and is used to make the serialization of property @@ -320,7 +339,17 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * @default false */ denyDelete: { - value: Defaults.denyDelete + get: function() { + return this.deleteRule === DeleteRule.DENY; + } + }, + + /** + * @type {boolean} + * @default false + */ + deleteRule: { + value: Defaults.deleteRule }, /** From 44261051ed0156f87c8a92954d08edb08058f233 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 30 Jan 2020 09:52:09 +0100 Subject: [PATCH 109/407] add default mapObjectToRawData, move properties over --- data/service/data-mapping.js | 1 + 1 file changed, 1 insertion(+) diff --git a/data/service/data-mapping.js b/data/service/data-mapping.js index d300995839..3dc8948400 100644 --- a/data/service/data-mapping.js +++ b/data/service/data-mapping.js @@ -65,6 +65,7 @@ exports.DataMapping = Montage.specialize(/** @lends DataMapping.prototype */ { value: function (object, data) { // TO DO: Provide a default mapping based on object.TYPE. // For now, subclasses must override this. + Object.assign(data,object); } }, From 448d8d5f0d1c316bdfe5030c7734058c73c09a52 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 30 Jan 2020 09:55:08 +0100 Subject: [PATCH 110/407] Introduces DataEvents sent by main service, misc changes to improve save, operatioms, changes --- data/model/data-event.js | 89 ++++++++++ data/service/data-operation.js | 118 +++++++------ data/service/data-service.js | 221 ++++++++++++++++++++++-- data/service/data-trigger.js | 15 +- data/service/expression-data-mapping.js | 63 +++++-- data/service/raw-data-service.js | 84 ++++++++- 6 files changed, 500 insertions(+), 90 deletions(-) create mode 100644 data/model/data-event.js diff --git a/data/model/data-event.js b/data/model/data-event.js new file mode 100644 index 0000000000..76927681a2 --- /dev/null +++ b/data/model/data-event.js @@ -0,0 +1,89 @@ +var MutableEvent = require("core/event/mutable-event").MutableEvent, + Montage = require("core/core").Montage; + +/** + * + * DataEvents: + * + * - the target is an objectDescriptor + * + * A DataService facilitates events sent by DataObjects (DOs) at key points in their life cycle: + * - create: A DO has been created. + * - editstart: User has started to edit a DO. + * - edit: A DO has been modifed by the user. Sent when the first change is made from a saved/stable state. + * - change: A DO's property has been changed by the user. This isn't unique to DOs and happing in montage on-demand, and part of biding infrastructure. + * - update: A local DO's property has been changed, and saved, by another user, pushed by the server side. + * - editcancel: User stopped editing a A DataObject (DO) without saving the changes (this would ends up sending a revert as + * an object should never leave an editing state without a clear decision about changes made? What about if offline?) + * - editend: User has stopped editing a DataObject (DO) ? + * - revert: A DataObject's (DO) state has been returned to it's most recent state. / reset? + * - validate + * - validateerror + * - save: A DataObject's (DO) changes have been saved. + * - saveerror: A DataObject's (DO) changes have been saved. + * - delete: A DataObject's (DO) has been deleted. + * - deleteerror: A DataObject's (DO) has been deleted. + * + */ + +exports.DataEvent = MutableEvent.specialize({ + + bubbles: { + value: true + }, + + constructor: { + value: function (type) { + this.type = type; + } + }, + + /** + * the dataService handling the event's target, which is a Data Object (DO) + * + * @type {DataService} + */ + dataService: { + value: true + }, + + /** + * the ObjectDescriptor of the event's target, which is Data Object (DO) + * + * @type {ObjectDescriptor} + */ + + dataObject: { + value: undefined + } + + +}, { + create: { + value: "create" + }, + + DRAG: { + value: "drag" + }, + + DRAGENTER: { + value: "dragenter" + }, + + DRAGEXIT: { + value: "dragexit" + }, + + DRAGLEAVE: { + value: "dragleave" + }, + + DROP: { + value: "drop" + }, + + DRAGEND: { + value: "dragend" + } +}); diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 22b1e74360..b894e0e05c 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -4,6 +4,8 @@ var Montage = require("core/core").Montage, Enum = require("core/enum").Enum, uuid = require("core/uuid"), DataOperationType, + + /* todo: we shpuld add a ...timedout for all operations. */ dataOperationTypes = [ "noop", "create", @@ -60,37 +62,51 @@ var Montage = require("core/core").Montage, "unlockcompleted", "unlockfailed", - /* RemmoteProcedureCall models the ability to invoke code logic on the server-side, being a DB StoredProcedure, or an method/function in a service */ + /* + RemmoteProcedureCall models the ability to invoke code logic on the server-side, being a DB StoredProcedure, or an method/function in a service + */ "remoteinvocation", /* Execute ? */ "remoteinvocationcompleted", /* ExecuteCompleted ? */ "remoteinvocationfailed", /* ExecuteFailed ? */ - /* Batch models the ability to group multiple operation. If a referrer is provided - to a BeginTransaction operation, then the batch will be executed within that transaction */ /* + Batch models the ability to group multiple operation. If a referrer is provided + to a BeginTransaction operation, then the batch will be executed within that transaction + */ "batch", "batchcompleted", "batchfailed", + + /* + A transaction is a unit of work that is performed atomically against a database. + Transactions are units or sequences of work accomplished in a logical order. + A transactions begins, operations are grouped, then it is either commited or rolled-back + */ + /* + begin/commit, Start/End Open/Close, Commit/Save, rollback/cancel + + as a lower-case event name, committransaction is hard to read, perform is equally easy to understand + and less technical. + + so settling on create transaction and perform/rollback transaction */ + "createtransaction", + /* I don't think there's such a thing, keeping for symetry for now */ + "createtransactioncompleted", + + /* Attempting to create a transaction within an existing one will fail */ + "createtransactionfailed", - /* A transaction is a unit of work that is performed against a database. - Transactions are units or sequences of work accomplished in a logical order. - A transactions begins, operations are grouped, then it is either commited or rolled-back*/ - /* Start/End Open/Close, Commit/Save, rollback/cancel - "begintransaction", - "begintransactioncompleted", - "begintransactionafiled", + "createsavepoint", - "committransaction", - "committransactioncompleted", - "committransactionfailed", + "performtransaction", + "performtransactioncompleted", + "performtransactionfailed", "rollbacktransaction", "rollbacktransactioncompleted", "rollbacktransactionfailed", - */ - /* operations used for the bottom of the stack to get information from a user. This useful for authenticating a user, refreshing a password, @@ -179,6 +195,9 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy if(this.objectExpressions) { serializer.setProperty("objectExpressions", this.objectExpressions); } + if(this.snapshot) { + serializer.setProperty("snapshot", this.snapshot); + } } }, deserializeSelf: { @@ -224,6 +243,11 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy this.objectExpressions = value; } + value = deserializer.getProperty("snapshot"); + if (value !== void 0) { + this.snapshot = value; + } + } }, @@ -421,8 +445,6 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy value: undefined }, - - /** * Deprecate? Make programatic, so that users doesn't have to worry about it. * @@ -630,94 +652,80 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy */ value: { + NoOp: DataOperationType.noop, Create: DataOperationType.create, CreateFailed: DataOperationType.createfailed, CreateCompleted: DataOperationType.createcompleted, CreateCancelled: DataOperationType.createcancelled, - //Additional + Copy: DataOperationType.copy, CopyFailed: DataOperationType.copyfailed, CopyCompleted: DataOperationType.copycompleted, - /* Read is the first operation that mnodels a query */ - Read: DataOperationType.read, - /* ReadUpdated is pushed by server when a query's result changes due to data changes from others */ + Read: DataOperationType.read, ReadUpdated: DataOperationType.readupdated, - - /* ReadProgress / ReadUpdate / ReadSeek is used to instruct server that more data is required for a "live" read / query - Need a better name, and a symetric? Or is ReadUpdated enough if it referes to previous operation - */ ReadProgress: DataOperationType.readprogress, //ReadUpdate ReadUpdate: DataOperationType.readupdate, //ReadUpdate - - /* ReadCancel is the operation that instructs baclkend that client isn't interested by a read operastion anymore */ ReadCancel: DataOperationType.readcancel, - - /* ReadCanceled is the operation that instructs the client that a read operation is canceled */ ReadCanceled: DataOperationType.readcanceled, - - /* ReadFailed is the operation that instructs the client that a read operation has failed canceled */ ReadFailed: DataOperationType.readfailed, - /* ReadCompleted is the operation that instructs the client that a read operation has returned all available data */ ReadCompleted: DataOperationType.readcompleted, + Update: DataOperationType.update, UpdateCompleted: DataOperationType.updatecompleted, UpdateFailed: DataOperationType.updatefailed, UpdateCancel: DataOperationType.updatecancel, UpdateCanceled: DataOperationType.updatecanceled, + Delete: DataOperationType.delete, DeleteCompleted: DataOperationType.deletecompleted, DeleteFailed: DataOperationType.deletefailed, - /* Lock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ + Lock: DataOperationType.lock, LockCompleted: DataOperationType.lockcompleted, LockFailed: DataOperationType.lockfailed, - /* RemmoteProcedureCall models the ability to invoke code logic on the server-side, being a DB StoredProcedure, or an method/function in a service */ + RemoteProcedureCall: DataOperationType.remoteinvocation, RemoteProcedureCallCompleted: DataOperationType.remoteinvocationcompleted, RemoteProcedureCallFailed: DataOperationType.remoteinvocationfailed, RemoteInvocation: DataOperationType.remoteinvocation, RemoteInvocationCompleted: DataOperationType.remoteinvocationcompleted, RemoteInvocationFailed: DataOperationType.remoteinvocationfailed, + UserAuthentication: DataOperationType.userauthentication, UserAuthenticationUpdate: DataOperationType.userauthenticationupdate, UserAuthenticationCompleted: DataOperationType.userauthenticationcompleted, UserAuthenticationFailed: DataOperationType.userauthenticationfailed, UserAuthenticationTimedout: DataOperationType.userauthenticationtimedout, + UserInput: DataOperationType.userinput, UserInputCompleted: DataOperationType.userinputcompleted, UserInputFailed: DataOperationType.userinputfailed, UserInputCanceled: DataOperationType.userinputcanceled, UserInputTimedOut: DataOperationType.userinputtimedout, + Validate: DataOperationType.validate, ValidateFailed: DataOperationType.validatefailed, validateCompleted: DataOperationType.validatecompleted, validateCancelled: DataOperationType.validatecancelled, - /* Batch models the ability to group multiple operation. If a referrer is provided - to a BeginTransaction operation, then the batch will be executed within that transaction */ - /* - Batch: DataOperationType.Batch, - BatchCompleted: DataOperationType.BatchCompleted, - BatchFailed: DataOperationType.BatchFailed, - */ - /* A transaction is a unit of work that is performed against a database. - Transactions are units or sequences of work accomplished in a logical order. - A transactions begins, operations are grouped, then it is either commited or rolled-back*/ - /* Start/End Open/Close, Commit/Save, rollback/cancel - BeginTransaction: DataOperationType.BeginTransaction, - BeginTransactionCompleted: DataOperationType.BeginTransactionCompleted, - BeginTransactionFailed: DataOperationType.BeginTransactionFailed, + Batch: DataOperationType.batch, + BatchCompleted: DataOperationType.batchcompleted, + BatchFailed: DataOperationType.batchfailed, + + CreateTransaction: DataOperationType.createtransaction, + CreateTransactionCompleted: DataOperationType.createtransactioncompleted, + CreateTransactionFailed: DataOperationType.createtransactionfailed, - CommitTransaction: DataOperationType.CommitTransaction, - CommitTransactionCompleted: DataOperationType.CommitTransactionCompleted, - CommitTransactionFailed: DataOperationType.CommitTransactionFailed, + CreateSavePoint: DataOperationType.createsavepoint, - RollbackTransaction: DataOperationType.RollbackTransaction, - RollbackTransactionCompleted: DataOperationType.RollbackTransactionCompleted, - RollbackTransactionFailed: DataOperationType.RollbackTransactionFailed, + PerformTransaction: DataOperationType.performtransaction, + PerformTransactionCompleted: DataOperationType.performtransactioncompleted, + PerformTransactionFailed: DataOperationType.performtransactionfailed, - */ + RollbackTransaction: DataOperationType.rollbacktransaction, + RollbackTransactionCompleted: DataOperationType.rollbacktransactioncompleted, + RollbackTransactionFailed: DataOperationType.rollbacktransactionfailed, } } diff --git a/data/service/data-service.js b/data/service/data-service.js index 40132501f6..74275d9e93 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -13,7 +13,12 @@ var Montage = require("core/core").Montage, ObjectDescriptor = require("core/meta/object-descriptor").ObjectDescriptor, Set = require("collections/set"), CountedSet = require("core/counted-set").CountedSet, - WeakMap = require("collections/weak-map"); + WeakMap = require("collections/weak-map"), + ObjectPool = require("core/object-pool").ObjectPool, + defaultEventManager = require("core/event/event-manager").defaultEventManager, + DataEvent = require("data/model/data-event").DataEvent, + PropertyDescriptor = require("core/meta/property-descriptor").PropertyDescriptor, + DeleteRule = require("core/meta/property-descriptor").DeleteRule; var AuthorizationPolicyType = new Montage(); @@ -27,6 +32,12 @@ UserAuthenticationPolicy.UpfrontAuthenticationPolicy = UserAuthenticationPolicy. UserAuthenticationPolicy.OnDemandAuthenticationPolicy = UserAuthenticationPolicy.ON_DEMAND; UserAuthenticationPolicy.OnFirstFetchAuthenticationPolicy = UserAuthenticationPolicy.ON_FIRST_FETCH; + + +/** + * Future: Look at https://www.npmjs.com/package/broadcast-channel to implement cross-tab event distribution? + */ + /** * Provides data objects and manages changes to them. * @@ -41,6 +52,7 @@ UserAuthenticationPolicy.OnFirstFetchAuthenticationPolicy = UserAuthenticationPo * instance of DataService or a DataService subclasses must either be that root * service or be set as a descendent of that root service. * + * * @class * @extends external:Montage */ @@ -52,7 +64,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { constructor: { value: function DataService() { - exports.DataService.mainService = exports.DataService.mainService || this; + exports.DataService.mainService = ObjectDescriptor.mainService = exports.DataService.mainService || this; if(this === DataService.mainService) { UserIdentityManager.mainService = DataService.mainService; } @@ -1409,6 +1421,12 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { expressions.forEach(function (expression) { + /* + This only works for expressions that are pure, chained + property traversals, aka property path. A more general walk + would be needed using the expression syntax to be totally + generic and support any kind of expression. + */ var split = expression.split("."); // if (split.length == 1) { // promises.push(self.getObjectProperties(object, split[0])); @@ -1790,6 +1808,9 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { */ recordDataIdentifierForObject: { value: function(dataIdentifier, object) { + if(this._dataIdentifierByObject.has(object) && this._dataIdentifierByObject.get(object) !== dataIdentifier) { + throw new Error("recordDataIdentifierForObject when one already exists:"+JSON.stringify(object)); + } this._dataIdentifierByObject.set(object, dataIdentifier); } }, @@ -1854,6 +1875,73 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { this._objectByDataIdentifier.delete(dataIdentifier); } }, + _eventPoolFactoryForEventType: { + value: function () { + return new DataEvent(); + } + }, + + __eventPoolByEventType: { + value: null + }, + _eventPoolForEventType: { + value: function (eventType) { + var pool = (this.__eventPoolByEventType || (this.__eventPoolByEventType = new Map())).get(eventType); + if(!pool) { + this.__eventPoolByEventType.set(eventType,(pool = new ObjectPool(this._eventPoolFactoryForEventType))); + } + return pool; + } + }, + __preparedConstructorsForDataEvents: { + value: null + }, + + isConstructorPreparedToHandleDataEvents: { + value: function (objectConstructor) { + return (this.__preparedConstructorsForDataEvents || (this.__preparedConstructorsForDataEvents = new Set())).has(objectConstructor); + } + }, + + prepareConstructorToHandleDataEvents: { + value: function (objectConstructor, event) { + objectConstructor.prepareToHandleDataEvents(event) + //prepareToHandleDataEvent or prepareToHandleCreateEvent + this.__preparedConstructorsForDataEvents.add(objectConstructor); + } + }, + + createDataObject: { + value: function (type) { + + } + }, + + _dispatchEventTypeForObject: { + value: function (eventType, object) { + /* + This needs to be made more generic in EventManager, which has "prepareForActivationEvent, + but it's very specialized for components. Having all prototypes of DO register as eventListeners upfront + would be damaging performance wise. We should do it as things happen. + */ + + var eventPool = this._eventPoolForEventType(eventType), + objectDescriptor = this.objectDescriptorForObject(object), + objectConstructor = object.constructor, + dataEvent = eventPool.checkout(); + + dataEvent.type = eventType; + dataEvent.target = objectDescriptor; + dataEvent.dataService = this; + dataEvent.dataObject = object; + + if(!this.isConstructorPreparedToHandleDataEvents(objectConstructor)) { + this.prepareConstructorToHandleDataEvents(objectConstructor, dataEvent); + } + + objectDescriptor.dispatchEvent(dataEvent); + } + }, /** * Create a new data object of the specified type. @@ -1870,8 +1958,15 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { createDataObject: { value: function (type) { if (this.isRootService) { - var object = this._createDataObject(type); + var service = this.childServiceForType(type), + //Gives a chance to raw data service to provide a primary key for clien-side creation/ + //Especially useful for systems that use uuid as primary keys. + //object = this._createDataObject(type, service.dataIdentifierForNewDataObject(type)); + object = this._createDataObject(type, service.dataIdentifierForNewDataObject(this.objectDescriptorForType(type))); this.createdDataObjects.add(object); + + this._dispatchEventTypeForObject(DataEvent.create, object); + return object; } else { this.rootService.createDataObject(type); @@ -1978,8 +2073,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { changedDataObjects: { get: function () { if (this.isRootService) { - this._changedDataObjects = this._changedDataObjects || new Map(); - return this._changedDataObjects; + return this._changedDataObjects || (this._changedDataObjects = new Map()); } else { return this.rootService.changedDataObjects; @@ -2020,7 +2114,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { if(!changesForDataObject) { changesForDataObject = new Map(); - this._changedDataObjects.set(dataObject,changesForDataObject); + this.changedDataObjects.set(dataObject,changesForDataObject); } /* @@ -2039,10 +2133,13 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { */ - if(keyValue) { + if(changeEvent.hasOwnProperty("key") && changeEvent.hasOwnProperty("keyValue")) { changesForDataObject.set(key,keyValue); } - else if(addedValues || removedValues) { + + //A change event could carry both a key/value change and addedValues/remove, like a splice, where the key would be "length" + + if(addedValues || removedValues) { //For key that can have add/remove the value of they key is an object //that itself has two keys: addedValues and removedValues //which value will be a set; @@ -2077,6 +2174,21 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { registeredRemovedValues.delete(removedValues[i]); } } + + /* + Work on local graph integrity. When objects are disassociated, it could mean some deletions may happen bases on delete rules. + App side goal is to maintain the App graph, server's side is to maintain database integrity. Both needs to act on delete rules: + - get object's descriptor + - get PropertyDescriptor from key + - get PropertyDescriptor's .deleteRule + deleteRule can be: + - DeleteRule.NULLIFY + - DeleteRule.CASCADE + - DeleteRule.DENY + - DeleteRule.IGNORE + */ + + //,,,,,TODO } } } @@ -2505,11 +2617,14 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { */ handleChange: { value: function(changeEvent) { - if(!this._createdDataObjects || (this._createdDataObjects && !this._createdDataObjects.has(changeEvent.target))) { + //Commentting out the restriction to exclude created objects as we might want to + //use it for them as well + + // if(!this._createdDataObjects || (this._createdDataObjects && !this._createdDataObjects.has(changeEvent.target))) { //Needs to register the change so saving changes / update operations can use it later to decise what to send //console.log("handleChange:",changeEvent); this.registerDataObjectChangesFromEvent(changeEvent); - } + //} } }, @@ -2565,6 +2680,13 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { value: function (object) { //return this._updateDataObject(object, "saveDataObject"); + //TODO + //First thing we should be doing here is run validation + //on the object, using objectDescriptor's + //draft: - could/should become async and return a promise. + //var validatity = this.objectDescriptorForObject(object).evaluateRules(object); + + var self = this, service, promise = this.nullPromise, @@ -2666,6 +2788,19 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { // } // }, + /** + * Returns a pomise of a DataOperation to save (create/updates) pending changes to an object + * @method + * @argument {Object} object - The object who will be reset. + * @returns {external:Promise} - A promise fulfilled when the data operation is ready. + */ + saveDataOperationFoObject: { + value: function (object) { + + } + }, + + saveChanges: { value: function () { //We need a list of the changes happening (creates, updates, deletes) operations @@ -2675,10 +2810,19 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { //createdDataObjects is a set var createdDataObjects = this.createdDataObjects, changedDataObjects = this.changedDataObjects, - deletedDataObjects = this.deletedDataObjects; + deletedDataObjects = this.deletedDataObjects, + createTransaction = new DataOperation(); + + createTransaction.type = DataOperation.Type.CreateTransaction; - //Here we want to create a transaction to make sure everything is sent at the same time. - //Right now, we don't track what object fetched was changed that eventually needs to be included. + //Loop, get data operation, discard the no-ops (and clean changes) + + /* + Here we want to create a transaction to make sure everything is sent at the same time. + - We wneed to act on delete rules in relationships on reverse. So an update could lead to a delete operatiom + so we need to process updates before we process deletes. + - we need to check no deleted object is added to a relationoship to-one or to-many while we process updates + */ } }, @@ -3062,6 +3206,57 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } }, + + + /*************************************************************************** + * Access Control methods + */ + + /** + * Answers wether logged-in application user can create a DataObject. + * + * Services overriding the (plural) + * @method + * @argument {ObjectDescriptor} dataObjectDescriptor + * @returns {Promise} - A promise fulfilled with a boolean value. + * + */ + canUserCreateDataObject: { + value: function(dataObjectDescriptor) { + /* + TODO: implement by leveraging Expression Based Access Control mapping/rules if available + */ + return Promise.resolve(true); + } + }, + + /** + * Answers wether logged-in application user can edit/update? a DataObject. + * + * Services overriding the (plural) + * @method + * @argument {Object} dataObject + * @returns {Promise} - A promise fulfilled with a boolean value. + * + */ + canUserEditDataObject: { + value: function(dataObject) { + /* + return this.canPerfomDataOperation(DataOperation.toUpdateDataObject(dataObject)); + */ + return Promise.resolve(true); + } + }, + canUserDeleteDataObject: { + value: function(dataObject) { + /* + return this.canPerfomDataOperation(DataOperation.toDeleteDataObject(dataObject)); + */ + return Promise.resolve(true); + } + }, + + /*************************************************************************** * Utilities */ diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js index e2b4554f65..9948e33682 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -140,11 +140,13 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy // To save memory a separate _isGlobal boolean is not maintained and // the _isGlobal value is derived from the _valueStatus type. return !(this._valueStatus instanceof WeakMap); + //return !(this._valueStatus instanceof Map); }, set: function (global) { global = global ? true : false; if (global !== this._isGlobal) { this._valueStatus = global ? undefined : new WeakMap(); + //this._valueStatus = global ? undefined : new Map(); } } }, @@ -161,11 +163,20 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy * @private * @type {Object|WeakMap} */ - _valueStatus: { + __valueStatus: { configurable: true, writable: true, value: undefined }, + _valueStatus: { + configurable: true, + get: function() { + return this.__valueStatus; + }, + set: function(value) { + this.__valueStatus = value; + } + }, /** * Gets the status of a property value managed by this trigger: @@ -366,7 +377,7 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy //addRangeChangeListener //If we're not in the middle of a mapping...: - if(!this._service._objectsBeingMapped.has(object)) { + if(currentValue !== initialValue && !this._service._objectsBeingMapped.has(object)) { //Dispatch update event var changeEvent = new ChangeEvent; changeEvent.target = object; diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 9f9e17d695..c4296ecb9d 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -731,7 +731,8 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData var inversePropertyDescriptor = objectDescriptor.propertyDescriptorForName(inversePropertyName); if (data) { - self._setObjectsValueForPropertyDescriptor(data, object, inversePropertyDescriptor); + //Adding shouldFlagObjectBeingMapped argument to true. + self._setObjectsValueForPropertyDescriptor(data, object, inversePropertyDescriptor, true); } return null; }); @@ -819,10 +820,14 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData * hold the model data. * @argument {Object} data - An object whose properties must be set or * modified to represent the model data + * @argument {Iterator} keyIterator - an iterator to loop over a subset + * of object's properties that + * must be mapped to raw data. + */ mapObjectToRawData: { - value: function (object, data) { - var keys = this.rawDataMappingRules.keys(), + value: function (object, data, keyIterator) { + var keys = keyIterator || this.rawDataMappingRules.keys(), promises = [], key, result; @@ -877,15 +882,20 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData mapObjectPropertyToRawData: { value: function(object, propertyName, data) { var objectRule = this.objectMappingRules.get(propertyName), - rule = this.rawDataMappingRules.get(objectRule.sourcePath); + rule; - if(rule) { - return this._mapObjectToRawDataProperty(object,data,objectRule.sourcePath); + if(objectRule){ + rule = this.rawDataMappingRules.get(objectRule.sourcePath) + if(rule) { + return this._mapObjectToRawDataProperty(object,data,objectRule.sourcePath); + } + else { + throw new Error("No rawDataMappingRule found to map property "+propertyName+" of object,", object, "to raw data"); + } } else { - throw new Error("Can't map property "+propertyName+" of object,", object, "to raw data"); + throw new Error("No objectMappingRules found to map property "+propertyName+" of object,", object, "to raw data"); } - } }, @@ -903,7 +913,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } }, - /** + /** * Prefetches any object properties required to map the rawData property * and maps once the fetch is complete. * @@ -924,13 +934,24 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData if (this._isAsync(result)) { self = this; - result = result.then(function () { + result = result.then(function (value) { return self._mapObjectToRawDataProperty(object, data, propertyName); }); } else { result = this._mapObjectToRawDataProperty(object, data, propertyName); } - return result; + + //using delegation to allow dataService customization + if (this._isAsync(result)) { + self = this; + return result.then(function (value) { + self.service.mappingDidMapObjectToRawDataProperty(self, object, data, propertyName); + return value; + }); + } else { + this.service.mappingDidMapObjectToRawDataProperty(this, object, data, propertyName); + return result; + } } }, @@ -998,20 +1019,24 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData }, _setObjectsValueForPropertyDescriptor: { - value: function (objects, value, propertyDescriptor) { + value: function (objects, value, propertyDescriptor, shouldFlagObjectBeingMapped) { var i, n; for (i = 0, n = objects.length; i < n; i += 1) { - this._setObjectValueForPropertyDescriptor(objects[i], value, propertyDescriptor); + this._setObjectValueForPropertyDescriptor(objects[i], value, propertyDescriptor, shouldFlagObjectBeingMapped); } } }, _setObjectValueForPropertyDescriptor: { - value: function (object, value, propertyDescriptor) { + value: function (object, value, propertyDescriptor, shouldFlagObjectBeingMapped) { var propertyName = propertyDescriptor.name, isToMany; //Add checks to make sure that data matches expectations of propertyDescriptor.cardinality + if(shouldFlagObjectBeingMapped) { + this.service.rootService._objectsBeingMapped.add(object); + } + if (Array.isArray(value)) { isToMany = propertyDescriptor.cardinality !== 1; if (isToMany) { @@ -1026,6 +1051,11 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } else { object[propertyName] = value; } + + if(shouldFlagObjectBeingMapped) { + this.service.rootService._objectsBeingMapped.delete(object); + } + } }, @@ -1075,6 +1105,9 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } }, + /* + Looks unused + _assignDataToObjectProperty: { value: function (object, propertyDescriptor, data) { var hasData = data && data.length, @@ -1101,6 +1134,8 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } } }, + */ + /*************************************************************************** * Rules */ diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index b36944be5c..b9ab6fbe3a 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -555,7 +555,8 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot //Consolidation, recording snapshot even if we already had an object //Record snapshot before we may create an object - this.recordSnapshot(dataIdentifier, rawData); + //Benoit: commenting out, done twice when fetching now + //this.recordSnapshot(dataIdentifier, rawData); if(!object) { //iDataIdentifier argument should be all we need later on @@ -570,6 +571,45 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot value: undefined }, + /** + * Called by [DataService createDataObject()]{@link DataService#createDataObject} to allow + * RawDataService to provide a primiary key on the client side as soon as an object is created. + * Especially useful for uuid based primary keys that can be generated eithe client or server side. + * + * @method + * @argument {DataStream} stream + * - The stream to which the data objects created + * from the raw data should be added. + * @argument {Object} rawData - An anonymnous object whose properties' + * values hold the raw data. This array + * will be modified by this method. + * @argument {?} context - An arbitrary value that will be passed to + * [getDataObject()]{@link RawDataService#getDataObject} + * and + * [mapRawDataToObject()]{@link RawDataService#mapRawDataToObject} + * if it is provided. + * + * @returns {Promise} - A promise resolving to the mapped object. + * + */ + + primaryKeyForNewDataObject: { + value: function (type) { + return undefined; + } + }, + + dataIdentifierForNewDataObject: { + value: function (type) { + var primaryKey = this.primaryKeyForNewDataObject(type); + + if(primaryKey) { + return this.dataIdentifierForTypePrimaryKey(type,primaryKey); + } + return undefined; + } + }, + primaryKeyForTypeRawData: { value: function (type, rawData) { @@ -693,7 +733,18 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot */ recordSnapshot: { value: function (dataIdentifier, rawData) { - this._snapshot.set(dataIdentifier, rawData); + var snapshot = this._snapshot.get(dataIdentifier); + if(!snapshot) { + this._snapshot.set(dataIdentifier, rawData); + } + else { + var rawDataKeys = Object.keys(rawData), + i, countI; + + for(i=0, countI = rawDataKeys.length;(i Date: Thu, 30 Jan 2020 11:43:10 +0100 Subject: [PATCH 111/407] add date-spec --- test/all.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/all.js b/test/all.js index 4b076367ae..2cd477da99 100644 --- a/test/all.js +++ b/test/all.js @@ -28,6 +28,7 @@ module.exports = require("montage-testing").run(require, [ "spec/core/request-spec", "spec/core/selector-spec", "spec/core/criteria-spec", + "spec/core/date-spec", "spec/core/super-spec", "spec/core/super-for-spec", {name: "spec/core/undo-manager-spec", node: false}, From 13a70072b65c8ef263e96f99b142e6092f84c4b2 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 30 Jan 2020 11:43:50 +0100 Subject: [PATCH 112/407] naming comment about registerDroppable --- ui/component.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/component.js b/ui/component.js index a68198bc14..f64852112d 100644 --- a/ui/component.js +++ b/ui/component.js @@ -2975,6 +2975,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto /** * Register a component for beeing a drag destination. + * droppable is the name of the html5 attribrutes, acceptDrop/acceptsDrop might be better */ registerDroppable: { value: function () { From f8228d8fddec5ac68460080ea34354df26455caf Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 30 Jan 2020 11:45:47 +0100 Subject: [PATCH 113/407] more comments on registerDroppable --- ui/component.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/component.js b/ui/component.js index f64852112d..c6fdc423de 100644 --- a/ui/component.js +++ b/ui/component.js @@ -2975,7 +2975,9 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto /** * Register a component for beeing a drag destination. - * droppable is the name of the html5 attribrutes, acceptDrop/acceptsDrop might be better + * droppable is the name of the html5 attribrutes, + * isDroppable would be a more consistent name while close to standard, + * acceptDrop/acceptsDrop could work too */ registerDroppable: { value: function () { From 31488d0d998cf010cbf06de307c9f40bf6a4cf8b Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 31 Jan 2020 19:51:18 +0100 Subject: [PATCH 114/407] - rename property on Data Objects identifier to dataIdentifier - Changed logic of identifier based callbacks to try method based on currentTarget's identifier when event capture/bubble as that's what a listener listens to. Default to what was before if no match is found. Should most likely be backward compatible with typical usage. Spec updated. - DataService now dispatch events on DOs, which propagate through their ObjectDescriptor's hierarchy before reaching DataService then application. --- core/event/event-manager.js | 45 +++++++++++--------- data/service/data-service.js | 61 ++++++++++++++++----------- test/spec/events/eventmanager-spec.js | 34 ++++++++++++--- 3 files changed, 90 insertions(+), 50 deletions(-) diff --git a/core/event/event-manager.js b/core/event/event-manager.js index cc2d7bac3e..04e0c86e10 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -2552,6 +2552,8 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan bubbleMethodName, identifierSpecificCaptureMethodName, identifierSpecificBubbleMethodName, + currentTargetIdentifierSpecificCaptureMethodName, + currentTargetIdentifierSpecificBubbleMethodName, capitalizedIdentifier, mutableEvent, mutableEventTarget, @@ -2616,17 +2618,19 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan if (!listenerEntries) { continue; } + + currentTargetIdentifierSpecificCaptureMethodName = this.methodNameForCapturePhaseOfEventType(eventType, iTarget.identifier, capitalizedEventType); if (Array.isArray(listenerEntries)) { j=0; _currentDispatchedTargetListeners.set(listenerEntries,null); while ((nextEntry = listenerEntries[j++]) && !mutableEvent.immediatePropagationStopped) { - this._invokeTargetListenerForEvent(iTarget, nextEntry, mutableEvent, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerForEvent(iTarget, nextEntry, mutableEvent, currentTargetIdentifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, true, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, currentTargetIdentifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); } } @@ -2642,13 +2646,13 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan j=0; _currentDispatchedTargetListeners.set(listenerEntries,null); while ((nextEntry = listenerEntries[j++]) && !mutableEvent.immediatePropagationStopped) { - this._invokeTargetListenerForEvent(iTarget, nextEntry, mutableEvent, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerForEvent(iTarget, nextEntry, mutableEvent, identifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, true, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); } } @@ -2659,13 +2663,13 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan j=0; _currentDispatchedTargetListeners.set(listenerEntries,null); while ((nextEntry = listenerEntries[j++]) && !mutableEvent.immediatePropagationStopped) { - this._invokeTargetListenerForEvent(iTarget, nextEntry, mutableEvent, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerForEvent(iTarget, nextEntry, mutableEvent, identifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, false, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); } } @@ -2681,17 +2685,19 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan continue; } + currentTargetIdentifierSpecificBubbleMethodName = this.methodNameForBubblePhaseOfEventType(eventType, iTarget.identifier, capitalizedEventType); + if (Array.isArray(listenerEntries)) { j=0; _currentDispatchedTargetListeners.set(listenerEntries,null); while ((nextEntry = listenerEntries[j++]) && !mutableEvent.immediatePropagationStopped) { - this._invokeTargetListenerForEvent(iTarget, nextEntry, mutableEvent, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerForEvent(iTarget, nextEntry, mutableEvent, currentTargetIdentifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, false, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, currentTargetIdentifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); } } @@ -2716,7 +2722,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan * @private */ _invokeTargetListenerForEvent: { - value: function _invokeTargetListenerForEvent(iTarget, jListener, mutableEvent, identifierSpecificPhaseMethodName, phaseMethodName) { + value: function _invokeTargetListenerForEvent(iTarget, jListener, mutableEvent, currentTargetIdentifierSpecificPhaseMethodName, targetIdentifierSpecificPhaseMethodName, phaseMethodName) { // var functionType = "function"; // if (typeof jListener === functionType) { // jListener.call(iTarget, mutableEvent); @@ -2730,16 +2736,17 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan // else if (typeof jListener.handleEvent === functionType) { // jListener.handleEvent(mutableEvent); // } - - (identifierSpecificPhaseMethodName && typeof jListener[identifierSpecificPhaseMethodName] === this._functionType) - ? jListener[identifierSpecificPhaseMethodName](mutableEvent) - : (typeof jListener[phaseMethodName] === this._functionType) - ? jListener[phaseMethodName](mutableEvent) - : (typeof jListener.handleEvent === this._functionType) - ? jListener.handleEvent(mutableEvent) - : (typeof jListener === this._functionType) - ? jListener.call(iTarget, mutableEvent) - : void 0; + (currentTargetIdentifierSpecificPhaseMethodName && typeof jListener[currentTargetIdentifierSpecificPhaseMethodName] === this._functionType) + ? jListener[currentTargetIdentifierSpecificPhaseMethodName](mutableEvent) + : (targetIdentifierSpecificPhaseMethodName && typeof jListener[targetIdentifierSpecificPhaseMethodName] === this._functionType) + ? jListener[targetIdentifierSpecificPhaseMethodName](mutableEvent) + : (typeof jListener[phaseMethodName] === this._functionType) + ? jListener[phaseMethodName](mutableEvent) + : (typeof jListener.handleEvent === this._functionType) + ? jListener.handleEvent(mutableEvent) + : (typeof jListener === this._functionType) + ? jListener.call(iTarget, mutableEvent) + : void 0; } }, diff --git a/data/service/data-service.js b/data/service/data-service.js index 74275d9e93..52f04fe891 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -609,12 +609,18 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { dataTriggers = DataTrigger.addTriggers(this, objectDescriptor, prototype, requisitePropertyNames), mainService = this.rootService; - Object.defineProperty(prototype,"identifier", { + Object.defineProperty(prototype,"dataIdentifier", { enumerable: true, get: function() { return mainService.dataIdentifierForObject(this); - } - }); + } + }); + Object.defineProperty(prototype,"nextTarget", { + enumerable: true, + get: function() { + return objectDescriptor; + } + }); this._dataObjectPrototypes.set(constructor, prototype); this._dataObjectPrototypes.set(objectDescriptor, prototype); @@ -1905,7 +1911,9 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { prepareConstructorToHandleDataEvents: { value: function (objectConstructor, event) { - objectConstructor.prepareToHandleDataEvents(event) + if(typeof objectConstructor.prepareToHandleDataEvents === "function") { + objectConstructor.prepareToHandleDataEvents(event); + } //prepareToHandleDataEvent or prepareToHandleCreateEvent this.__preparedConstructorsForDataEvents.add(objectConstructor); } @@ -1939,7 +1947,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { this.prepareConstructorToHandleDataEvents(objectConstructor, dataEvent); } - objectDescriptor.dispatchEvent(dataEvent); + object.dispatchEvent(dataEvent); } }, @@ -2801,29 +2809,32 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { }, + /** + * Save all changes made since the last call. This method currently delegates to rawDataServices for the actual work + * Some of it might migrate back up here later when the dust settles. + * + * TEMPORARY implementation assuming a single RawDataService that creates + * operations. We might need to officially make that kind of subclass the + * mainService. + * + * @method + * @returns {external:Promise} - A promise fulfilled when the save operation is complete or failed. + */ + saveChanges: { value: function () { - //We need a list of the changes happening (creates, updates, deletes) operations - //to keep their natural order and be able to create a transaction operationn - //when saveChanges is called. - - //createdDataObjects is a set - var createdDataObjects = this.createdDataObjects, - changedDataObjects = this.changedDataObjects, - deletedDataObjects = this.deletedDataObjects, - createTransaction = new DataOperation(); - - createTransaction.type = DataOperation.Type.CreateTransaction; - - //Loop, get data operation, discard the no-ops (and clean changes) + var self = this, + service, + promise = this.nullPromise, - /* - Here we want to create a transaction to make sure everything is sent at the same time. - - We wneed to act on delete rules in relationships on reverse. So an update could lead to a delete operatiom - so we need to process updates before we process deletes. - - we need to check no deleted object is added to a relationoship to-one or to-many while we process updates - */ - } + service = this.childServices[0]; + if (service && typeof service.saveChanges === "function") { + return service.saveChanges(); + } + else { + return promise; + } + } }, diff --git a/test/spec/events/eventmanager-spec.js b/test/spec/events/eventmanager-spec.js index 0bce5404ee..3685e3c068 100644 --- a/test/spec/events/eventmanager-spec.js +++ b/test/spec/events/eventmanager-spec.js @@ -508,12 +508,15 @@ TestPageLoader.queueTest("eventmanagertest/eventmanagertest", function (testPage var target = testDocument.getElementById("element"); - + target.identifier = "target"; // We install these in the reverse order we expect them to be called in to ensure it's not the order target.addEventListener("mousedown", targetSpy, true); target.parentNode.addEventListener("mousedown", parentSpy, true); + target.parentNode.identifier = "parent"; testDocument.body.addEventListener("mousedown", bodySpy, true); + testDocument.body.identifier = "body"; testDocument.addEventListener("mousedown", documentSpy, true); + testDocument.identifier = "document"; testDocument.defaultView.addEventListener("mousedown", windowSpy, true); testPage.mouseEvent(new EventInfo().initWithElement(target), "mousedown", function () { @@ -922,7 +925,26 @@ TestPageLoader.queueTest("eventmanagertest/eventmanagertest", function (testPage delete testDocument.identifier; }); - it("should handle the event using a less specific handler if the original target has no identifier, even if the currentTarget has an identifier", function () { + it("should handle the event using the identifier from the current target as part of the handler method name, for listeners along the distribution chain", function () { + var eventSpy = { + handleDocumentMousedown: function () { + } + }; + + spyOn(eventSpy, 'handleDocumentMousedown'); + + testDocument.identifier = "document"; + testDocument.addEventListener("mousedown", eventSpy, false); + var target = testDocument.getElementById("element"); + + testPage.mouseEvent(new EventInfo().initWithElement(target), "mousedown", function () { + expect(eventSpy.handleDocumentMousedown).toHaveBeenCalled(); + }); + + delete testDocument.identifier; + }); + + it("should handle the event using the most specific currentTarget identifier handler even if the original target has no identifier", function () { var eventSpy = { @@ -941,14 +963,14 @@ TestPageLoader.queueTest("eventmanagertest/eventmanagertest", function (testPage testDocument.addEventListener("mousedown", eventSpy, false); testPage.mouseEvent(new EventInfo().initWithElement(testDocument.documentElement), "mousedown", function () { - expect(eventSpy.handleMousedown).toHaveBeenCalled(); - expect(eventSpy.handleDocumentMousedown).not.toHaveBeenCalled(); + expect(eventSpy.handleMousedown).not.toHaveBeenCalled(); + expect(eventSpy.handleDocumentMousedown).toHaveBeenCalled(); }); delete testDocument.identifier; }); - it("must not handle the event using an identifier based handler based on the current target, if it is not also the original event target", function () { + it("must handle the event using an identifier based handler based on the current target if present, even if it is not also the original event target", function () { var eventSpy = { handleDocumentMousedown: function () {} }; @@ -959,7 +981,7 @@ TestPageLoader.queueTest("eventmanagertest/eventmanagertest", function (testPage testDocument.addEventListener("mousedown", eventSpy, false); testPage.mouseEvent(new EventInfo().initWithElement(testDocument.documentElement), "mousedown", function () { - expect(eventSpy.handleDocumentMousedown).not.toHaveBeenCalled(); + expect(eventSpy.handleDocumentMousedown).toHaveBeenCalled(); }); delete testDocument.identifier; From 24846505b31e935f8fca90939d069493b168600a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 31 Jan 2020 19:52:09 +0100 Subject: [PATCH 115/407] Attach reusable regex on method rather then Date global --- core/extras/date.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/extras/date.js b/core/extras/date.js index 07a13234c4..b9ab4e6392 100644 --- a/core/extras/date.js +++ b/core/extras/date.js @@ -148,12 +148,10 @@ Number.prototype.toPaddedString = function(len , fillchar) { * extend Date with a method parsing ISO8601 / RFC 3339 date strings. * Usage: var d = Date.parseRFC3339( "2010-07-20T15:00:00Z" ); */ - Date.parseRFC3339_RegExp = /(\d\d\d\d)(-)?(\d\d)(-)?(\d\d)(T)?(\d\d)(:)?(\d\d)?(:)?(\d\d)?([\.,]\d+)?($|Z|([+-])(\d\d)(:)?(\d\d)?)/i; - Date.endsByZ = /Z$/i; - Date.parseRFC3339 = function(dString){ - if (typeof dString != 'string' || !this.endsByZ.test(dString)) return; + function _parseRFC3339(dString) { + if (typeof dString != 'string' || !_parseRFC3339.endsByZ.test(dString)) return; var result, - d = dString.match(this.parseRFC3339_RegExp); + d = dString.match(_parseRFC3339.parseRFC3339_RegExp); if (d) { var year = parseInt(d[1],10); @@ -184,6 +182,9 @@ Number.prototype.toPaddedString = function(len , fillchar) { } return result; }; + _parseRFC3339.parseRFC3339_RegExp = /(\d\d\d\d)(-)?(\d\d)(-)?(\d\d)(T)?(\d\d)(:)?(\d\d)?(:)?(\d\d)?([\.,]\d+)?($|Z|([+-])(\d\d)(:)?(\d\d)?)/i; + _parseRFC3339.endsByZ = /Z$/i; + Date.parseRFC3339 = _parseRFC3339; /********** * Part of https://github.com/tardate/rfc3339date.js/. From a871f9b59f9eb85d6a0e1a990cd2e24f98536db4 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 31 Jan 2020 22:17:26 +0100 Subject: [PATCH 116/407] Fix a regression --- core/meta/property-descriptor.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index 9cbcfdbe5a..25c40762f2 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -341,6 +341,9 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# denyDelete: { get: function() { return this.deleteRule === DeleteRule.DENY; + }, + set: function(value) { + this.deleteRule = DeleteRule.DENY; } }, From 9a9e1ee750971549b0a60a22bc68973ef90ca9f3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 31 Jan 2020 22:18:39 +0100 Subject: [PATCH 117/407] remove test of deprecated interpreter API --- test/all.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/all.js b/test/all.js index 2cd477da99..d2b2ae8404 100644 --- a/test/all.js +++ b/test/all.js @@ -76,7 +76,8 @@ module.exports = require("montage-testing").run(require, [ "spec/serialization/alias-spec", "spec/serialization/labeler-spec", "spec/serialization/reviver-spec", - "spec/serialization/interpreter-spec", + //deprecated + //"spec/serialization/interpreter-spec", "spec/serialization/visitor-spec", "spec/serialization/serialization-extractor-spec", "spec/serialization/bindings-spec", From 4a42d68803092944b1148c6c0a6f72d872a2027a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 4 Feb 2020 13:23:43 +0100 Subject: [PATCH 118/407] fix formating --- data/service/data-stream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/data-stream.js b/data/service/data-stream.js index 54603a0f23..6160c0930c 100644 --- a/data/service/data-stream.js +++ b/data/service/data-stream.js @@ -7,7 +7,7 @@ var DataProvider = require("data/service/data-provider").DataProvider, parse = require("frb/parse"), Scope = require("frb/scope"), compile = require("frb/compile-evaluator"), - DataOperation= require("data/service/data-operation").DataOperation, + DataOperation = require("data/service/data-operation").DataOperation, DataStream; /** From 2d612811435743ab9180add96b08a9a03a14eafd Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 4 Feb 2020 13:24:35 +0100 Subject: [PATCH 119/407] fix method doc --- core/event/mutable-event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/event/mutable-event.js b/core/event/mutable-event.js index 11d796f990..366abf80f6 100644 --- a/core/event/mutable-event.js +++ b/core/event/mutable-event.js @@ -275,7 +275,7 @@ var wrapPropertyGetter = function (key, storageKey) { }, /** * @type {Property} - * @default {Element} null + * @default {boolean} false */ defaultPrevented: { get: function () { From 68feef80e829edf1967c0fc476f3cb52f3689b35 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 4 Feb 2020 13:26:38 +0100 Subject: [PATCH 120/407] - fix event timestamp expected to be high precision - expose performance on global in node - more wip on DataEvent, event types need work --- core/event/change-event.js | 2 +- data/model/data-event.js | 36 +++++++++++++++++++----------------- node.js | 14 ++++++++++++++ 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/core/event/change-event.js b/core/event/change-event.js index 3547fc352e..057f77c4da 100644 --- a/core/event/change-event.js +++ b/core/event/change-event.js @@ -17,7 +17,7 @@ var Montage = require("core/core").Montage, var ChangeEvent = exports.ChangeEvent = Montage.specialize({ constructor: { value: function ChangeEvent() { - this.timestamp = Date.now(); + this.timestamp = performance.now(); return this; } }, diff --git a/data/model/data-event.js b/data/model/data-event.js index 76927681a2..ca12d51caa 100644 --- a/data/model/data-event.js +++ b/data/model/data-event.js @@ -34,7 +34,7 @@ exports.DataEvent = MutableEvent.specialize({ constructor: { value: function (type) { - this.type = type; + this.timestamp = performance.now(); } }, @@ -55,35 +55,37 @@ exports.DataEvent = MutableEvent.specialize({ dataObject: { value: undefined + }, + + detail: { + value: undefined } }, { - create: { - value: "create" + invalid: { + value: "invalid" }, - DRAG: { - value: "drag" - }, - - DRAGENTER: { - value: "dragenter" + create: { + value: "create" }, - DRAGEXIT: { - value: "dragexit" + save: { + value: "save" }, - DRAGLEAVE: { - value: "dragleave" + delete: { + value: "delete" }, - DROP: { - value: "drop" + revert: { + value: "revert" }, - DRAGEND: { - value: "dragend" + /* when a change was made by someone else and it's making it's way to another user. */ + update: { + value: "update" } + }); diff --git a/node.js b/node.js index 47c30b47e5..4a675447e9 100644 --- a/node.js +++ b/node.js @@ -232,6 +232,20 @@ MontageBoot.TemplateLoader = function (config, load) { }; }; + +Object.defineProperties(global, + { + _performance: { + value: undefined + }, + performance: { + get: function() { + return this._performance || (this._performance = require('perf_hooks').performance); + } + } + } +); + // add the TemplateLoader to the middleware chain Require.makeLoader = (function (makeLoader) { return function (config) { From 3c692102fa66a914c370a92dce80b3922fd950af Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 4 Feb 2020 13:27:52 +0100 Subject: [PATCH 121/407] remove serialization of denyDelete as moved to deleteRule --- core/meta/property-descriptor.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index 25c40762f2..c5d6a1ff90 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -131,7 +131,9 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# } this._setPropertyWithDefaults(serializer, "mandatory", this.mandatory); this._setPropertyWithDefaults(serializer, "readOnly", this.readOnly); - this._setPropertyWithDefaults(serializer, "denyDelete", this.denyDelete); + //Not needed anymore as it's now this.deleteRule === DeleteRule.DENY + //and deserializing denyDelete will set the equivallent on value deleteRule + // this._setPropertyWithDefaults(serializer, "denyDelete", this.denyDelete); this._setPropertyWithDefaults(serializer, "deleteRule", this.deleteRule); this._setPropertyWithDefaults(serializer, "valueType", this.valueType); this._setPropertyWithDefaults(serializer, "collectionValueType", this.collectionValueType); From a702f1d12419ab4b69dc661f2a13aa80fc290c3b Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 4 Feb 2020 13:28:47 +0100 Subject: [PATCH 122/407] - start new evaluateObjectValidity API just infrastructure for now, no logic yet --- core/meta/object-descriptor.js | 62 ++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/core/meta/object-descriptor.js b/core/meta/object-descriptor.js index a7ced19099..985e19710f 100644 --- a/core/meta/object-descriptor.js +++ b/core/meta/object-descriptor.js @@ -989,6 +989,64 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends } }, + _emptyValidityMap: { + value: new Map + }, + /** + * Evaluates the validity of objectInstance and collects invalidity into invalidityStates if provides, or in a + * new Map created, which will be the resolved value. This is done using: + * - rules set on ObjectDescriptor + * - propertyDescriptor properties: + * - mandatory: is a value there? + * - readOnly: is the value changed? + * - valueType, collectionValueType, valueDescriptor: Does the current value match? + * - cardinality, is the current value match? + * + * - The result needs to be a promise as some validation might need a round-trip to + * backend? + * - The result will need to be communicated through an "invalid" event that UI componenta + * will use to communicate the validity issues and their corresponding messages. + * - there's a combination of isues matching 1 component's designated property to edit as well + * as rules failing that might concern multiple ones as well. + * @param {Object} object instance to evaluate the rule for + * @returns {Array.} list of message key for rule that fired. Empty + * array otherwise. + */ + evaluateObjectValidity: { + value: function (objectInstance) { + + return Promise.resolve(this._emptyValidityMap); + + var name, rule, + messages = []; + /* bypassing it all for now */ + for (name in this._propertyValidationRules) { + if (this._propertyValidationRules.hasOwnProperty(name)) { + rule = this._propertyValidationRules[name]; + //Benoit: It's likely that some rules might be async + //or we might decide to differ the ones that are async + //to be run on the server instead, but right now the code + //doesn't anticipate any async rule. + + /* + TODO + Also to help reconciliate validation with HTML5 standards + and its ValidityState object (https://developer.mozilla.org/en-US/docs/Web/API/ValidityState), or to know what key/expression failed validation, we need to return a map key/reason, and not just an array, so that each message can end-up being processed/communicated to the user by the component editing it. + + It might even make sense that each component editing a certain property of an object, or any combination of some + would have to "observe/listen" to individual property validations. + + This one is meant to run on all as some rules can involve multiple properties. + */ + + if (rule.evaluateRule(objectInstance)) { + messages.push(rule.messageKey); + } + } + } + return messages; + } + }, /** * Evaluates the rules based on the object and the properties. * @param {Object} object instance to evaluate the rule for @@ -996,7 +1054,7 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends * array otherwise. */ evaluateRules: { - value: function (objectInstance) { + value: deprecate.deprecateMethod(void 0, function (objectInstance) { var name, rule, messages = []; @@ -1025,7 +1083,7 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends } } return messages; - } + }, "addEventBlueprint", "addEventDescriptor") }, objectDescriptorModuleId: require("../core")._objectDescriptorModuleIdDescriptor, From 2fe8493b71ec2fabb54bad05fc79fa6604b596ae Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 4 Feb 2020 13:31:55 +0100 Subject: [PATCH 123/407] - add caching for objectDescriptorForObject - make dispatchDataEventTypeForObject public - add changedDataObjects as Set, parallele to created & deleted - rename Map holding changes per object to dataObjectChanges --- data/service/data-service.js | 67 +++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 52f04fe891..1d20001068 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -1097,6 +1097,10 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { * Returns an object descriptor for the provided object. If this service * does not have an object descriptor for this object it will ask its * parent for one. + * + * TODO: looks like we're looping all the time and not caching a lookup" + * Why isn't objectDescriptorWithModuleId used?? + * * @param {object} * @returns {ObjectDescriptor|null} if an object descriptor is not found this * method will return null. @@ -1108,10 +1112,15 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { moduleId = objectInfo.moduleId, objectName = objectInfo.objectName, module, exportName, objectDescriptor, i, n; + + objectDescriptor = this.objectDescriptorWithModuleId(moduleId); for (i = 0, n = types.length; i < n && !objectDescriptor; i += 1) { module = types[i].module; exportName = module && types[i].exportName; if (module && moduleId === module.id && objectName === exportName) { + if(objectDescriptor !== types[i]) { + console.error("objectDescriptorWithModuleId cached an objectDescriptor and objectDescriptorForObject finds another") + } objectDescriptor = types[i]; } } @@ -1887,14 +1896,14 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } }, - __eventPoolByEventType: { + __dataEventPoolByEventType: { value: null }, - _eventPoolForEventType: { + _dataEventPoolForEventType: { value: function (eventType) { - var pool = (this.__eventPoolByEventType || (this.__eventPoolByEventType = new Map())).get(eventType); + var pool = (this.__dataEventPoolByEventType || (this.__dataEventPoolByEventType = new Map())).get(eventType); if(!pool) { - this.__eventPoolByEventType.set(eventType,(pool = new ObjectPool(this._eventPoolFactoryForEventType))); + this.__dataEventPoolByEventType.set(eventType,(pool = new ObjectPool(this._eventPoolFactoryForEventType))); } return pool; } @@ -1925,15 +1934,15 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } }, - _dispatchEventTypeForObject: { - value: function (eventType, object) { + dispatchDataEventTypeForObject: { + value: function (eventType, object, detail) { /* This needs to be made more generic in EventManager, which has "prepareForActivationEvent, but it's very specialized for components. Having all prototypes of DO register as eventListeners upfront would be damaging performance wise. We should do it as things happen. */ - var eventPool = this._eventPoolForEventType(eventType), + var eventPool = this._dataEventPoolForEventType(eventType), objectDescriptor = this.objectDescriptorForObject(object), objectConstructor = object.constructor, dataEvent = eventPool.checkout(); @@ -1942,6 +1951,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { dataEvent.target = objectDescriptor; dataEvent.dataService = this; dataEvent.dataObject = object; + dataEvent.detail = detail; if(!this.isConstructorPreparedToHandleDataEvents(objectConstructor)) { this.prepareConstructorToHandleDataEvents(objectConstructor, dataEvent); @@ -1973,7 +1983,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { object = this._createDataObject(type, service.dataIdentifierForNewDataObject(this.objectDescriptorForType(type))); this.createdDataObjects.add(object); - this._dispatchEventTypeForObject(DataEvent.create, object); + this.dispatchDataEventTypeForObject(DataEvent.create, object); return object; } else { @@ -2064,6 +2074,25 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } }, + /** + * A set of the data objects moified by the user after they were fetched. + * * + * @type {Set.} + */ + changedDataObjects: { + get: function () { + if (this.isRootService) { + if (!this._changedDataObjects) { + this._changedDataObjects = new Set(); + } + return this._changedDataObjects; + } + else { + return this.rootService.changedDataObjects; + } + } + }, + /** * A Map of the data objects managed by this service or any other descendent * of this service's [root service]{@link DataService#rootService} that have @@ -2078,18 +2107,18 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { * * @type {Map.} */ - changedDataObjects: { + dataObjectChanges: { get: function () { if (this.isRootService) { - return this._changedDataObjects || (this._changedDataObjects = new Map()); + return this._dataObjectChanges || (this._dataObjectChanges = new Map()); } else { - return this.rootService.changedDataObjects; + return this.rootService.dataObjectChanges; } } }, - _changedDataObjects: { + _dataObjectChanges: { value: undefined }, @@ -2106,7 +2135,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { changesForDataObject: { value: function (dataObject) { - return this.changedDataObjects.get(dataObject); + return this.dataObjectChanges.get(dataObject); } }, @@ -2118,11 +2147,15 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { keyValue = changeEvent.keyValue, addedValues = changeEvent.addedValues, removedValues = changeEvent.addedValues, - changesForDataObject = this.changedDataObjects.get(dataObject); + changesForDataObject = this.dataObjectChanges.get(dataObject); + + if(!this.createdDataObjects.has(dataObject)) { + this.changedDataObjects.add(dataObject); + } if(!changesForDataObject) { changesForDataObject = new Map(); - this.changedDataObjects.set(dataObject,changesForDataObject); + this.dataObjectChanges.set(dataObject,changesForDataObject); } /* @@ -2204,7 +2237,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { clearRegisteredChangesForDataObject: { value: function (dataObject) { - this.changedDataObjects.set(dataObject,null); + this.dataObjectChanges.set(dataObject,null); } }, @@ -2227,7 +2260,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { return this._deletedDataObjects; } else { - return this.rootService.deletedDataObjects(); + return this.rootService.deletedDataObjects; } } }, From b24fdc50e1118c3235f3258d0842853da43bc9a7 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 6 Feb 2020 19:58:34 +0100 Subject: [PATCH 124/407] fix a bug when event is missing --- core/event/mutable-event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/event/mutable-event.js b/core/event/mutable-event.js index 366abf80f6..fb12cd9c7a 100644 --- a/core/event/mutable-event.js +++ b/core/event/mutable-event.js @@ -186,7 +186,7 @@ var wrapPropertyGetter = function (key, storageKey) { */ target: { get: function () { - return (this._target !== void 0) ? this._target : this._event.target; + return (this._target !== void 0) ? this._target : this._event ? this._event.target : undefined; }, set: function (value) { this._target = value; From 2edce7ec4a40c90af26291b4cea22bb8598b04e6 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 6 Feb 2020 19:59:57 +0100 Subject: [PATCH 125/407] method name change --- data/service/data-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 1d20001068..0635d49cd0 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -2548,7 +2548,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { dataServicePromise.then(function(dataService) { try { //Direct access for now - stream = dataService.handleReadOperation(dataOperation); + stream = dataService.handleRead(dataOperation); } catch (e) { stream.dataError(e); } From 494fc290654f78c80013e22bdaff257bf83abb64 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 10 Feb 2020 21:49:03 +0100 Subject: [PATCH 126/407] Removes DataStream dependency that creates a loop --- core/application.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/application.js b/core/application.js index 7307dbc1e1..427785b0f3 100644 --- a/core/application.js +++ b/core/application.js @@ -13,7 +13,6 @@ var Target = require("./target").Target, Template = require("./template"), MontageWindow = require("../window-loader/montage-window").MontageWindow, - DataStream = require("data/service/data-stream").DataStream, Criteria = require("core/criteria").Criteria, DataQuery = require("data/model/data-query").DataQuery, UserIdentityService = undefined, @@ -482,7 +481,6 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio selfUserCriteria, userIdentityQuery; - if(userIdentityServices && userIdentityServices.length > 0) { //Shortcut, there could be multiple one we need to flatten. userIdentityObjectDescriptors = userIdentityServices[0].types; From b447ae01ed57bebc84b94ac34ef0180ea7cc33e1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 10 Feb 2020 21:52:20 +0100 Subject: [PATCH 127/407] - add missing require to core/Promise - avoid creating a promise in evaluate for performnce reason as as calling code can already deal with both cases. --- data/service/mapping-rule.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/service/mapping-rule.js b/data/service/mapping-rule.js index 5d4b8a24b1..a5dad4dafe 100644 --- a/data/service/mapping-rule.js +++ b/data/service/mapping-rule.js @@ -1,6 +1,7 @@ var Montage = require("montage").Montage, compile = require("frb/compile-evaluator"), parse = require("frb/parse"), + Promise = require("core/promise").Promise, deprecate = require("core/deprecate"); @@ -195,7 +196,8 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { return this.converter ? this.converter.convert(value) : this.reverter ? this.reverter.revert(value) : - Promise.resolve(value); + //Promise.resolve(value); + value; } }, From d37ae5b3c8790fe58ffa3f7dc763aa6d0ca9f460 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 10 Feb 2020 21:53:34 +0100 Subject: [PATCH 128/407] - replaces deprecated RawPropertyValueToObjectConverter by RawForeignValueToObjectConverter - add better support for many to many in _setObjectValueForPropertyDescriptor --- data/service/expression-data-mapping.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index c4296ecb9d..c22355055d 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -10,7 +10,7 @@ var DataMapping = require("./data-mapping").DataMapping, Scope = require("frb/scope"), Set = require("collections/set"), deprecate = require("core/deprecate"), - RawPropertyValueToObjectConverter = require("data/converter/raw-property-value-to-object-converter").RawPropertyValueToObjectConverter; + RawForeignValueToObjectConverter = require("data/converter/raw-foreign-value-to-object-converter").RawForeignValueToObjectConverter; var Montage = require("montage").Montage; @@ -585,7 +585,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData aRule; while ((aRule = ruleIterator.next().value)) { - if((aRule.converter && (aRule.converter instanceof RawPropertyValueToObjectConverter)) && + if((aRule.converter && (aRule.converter instanceof RawForeignValueToObjectConverter)) && !requisitePropertyNames.has(aRule.sourcePath) && (prefetchExpressions && prefetchExpressions.indexOf(aRule.targetPath) === -1)) { continue; @@ -1030,7 +1030,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData _setObjectValueForPropertyDescriptor: { value: function (object, value, propertyDescriptor, shouldFlagObjectBeingMapped) { var propertyName = propertyDescriptor.name, - isToMany; + isToMany = propertyDescriptor.cardinality !== 1; //Add checks to make sure that data matches expectations of propertyDescriptor.cardinality if(shouldFlagObjectBeingMapped) { @@ -1038,7 +1038,6 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } if (Array.isArray(value)) { - isToMany = propertyDescriptor.cardinality !== 1; if (isToMany) { object[propertyName] = value; } else if (value.length) { @@ -1049,7 +1048,17 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData object[propertyName] = value[0]; } } else { - object[propertyName] = value; + if(isToMany) { + if(!Array.isArray(object[propertyName])) { + value = [value]; + object[propertyName] = value; + } + else { + object[propertyName].push(value); + } + } else { + object[propertyName] = value; + } } if(shouldFlagObjectBeingMapped) { From 62c9f629f0b9e575b4fe9263a8776df8c1fba54c Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 10 Feb 2020 23:20:32 +0100 Subject: [PATCH 129/407] change mr dependency to marchant/mr --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6f371d0e78..c88761156a 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "frb": "~4.0.x", "htmlparser2": "~3.0.5", "q-io": "github:marchant/q-io.git#bluebird-q-io", - "mr": "montagejs/mr#master", + "mr": "marchant/mr#master", "weak-map": "^1.0.5", "lodash.kebabcase": "^4.1.1", "lodash.camelcase": "^4.3.0", From 74ddc592dc16a8b15902168f16fca0343338eef1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 16 Feb 2020 23:19:10 -0800 Subject: [PATCH 130/407] remove dummy criteria creation --- data/model/data-query.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/data/model/data-query.js b/data/model/data-query.js index 44384114d0..11f73437a2 100644 --- a/data/model/data-query.js +++ b/data/model/data-query.js @@ -121,9 +121,10 @@ exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { */ criteria: { get: function () { - if (!this._criteria) { - this._criteria = {}; - } + //Might be breaking, but we shouldn't create an empty object lile that, of the wrong type... + // if (!this._criteria) { + // this._criteria = {}; + // } return this._criteria; }, set: function (criteria) { From 6e2d4dba8413b20772ff589bd52fa81506877fb1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 16 Feb 2020 23:19:43 -0800 Subject: [PATCH 131/407] start exposing frb via montage --- core/frb/language.js | 4 ++++ core/frb/parse.js | 1 + 2 files changed, 5 insertions(+) create mode 100644 core/frb/language.js create mode 100644 core/frb/parse.js diff --git a/core/frb/language.js b/core/frb/language.js new file mode 100644 index 0000000000..46b41b88e6 --- /dev/null +++ b/core/frb/language.js @@ -0,0 +1,4 @@ +exports.precedence = require("frb/language").precedence; +exports.precedenceLevels = require("frb/language").precedenceLevels; +exports.operatorTokens = require("frb/language").operatorTokens; +exports.operatorTypes = require("frb/language").operatorTypes; diff --git a/core/frb/parse.js b/core/frb/parse.js new file mode 100644 index 0000000000..a1471ad4bf --- /dev/null +++ b/core/frb/parse.js @@ -0,0 +1 @@ +exports = require("frb/parse"); From dd43eea9a0f8264569f3dfa8553bd3d3bd6d30b3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 18 Feb 2020 11:08:13 -0800 Subject: [PATCH 132/407] fix typo --- core/meta/property-descriptor.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index c5d6a1ff90..ba94af565f 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -50,7 +50,7 @@ var Defaults = { defaultValue: void 0, helpKey: "", isLocalizable: false, - isSearcheable: false, + isSearchable: false, isOrdered: false, hasUniqueValues: false, }; @@ -149,7 +149,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._setPropertyWithDefaults(serializer, "inversePropertyName", this.inversePropertyName); this._setPropertyWithDefaults(serializer, "isLocalizable", this.isLocalizable); this._setPropertyWithDefaults(serializer, "isSerializable", this.isSerializable); - this._setPropertyWithDefaults(serializer, "isSearcheable", this.isSearcheable); + this._setPropertyWithDefaults(serializer, "isSearchable", this.isSearchable); this._setPropertyWithDefaults(serializer, "isOrdered", this.isOrdered); this._setPropertyWithDefaults(serializer, "hasUniqueValues", this.hasUniqueValues); this._setPropertyWithDefaults(serializer, "description", this.description); @@ -191,7 +191,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._overridePropertyWithDefaults(deserializer, "inversePropertyName"); this._overridePropertyWithDefaults(deserializer, "isLocalizable"); this._overridePropertyWithDefaults(deserializer, "isSerializable"); - this._overridePropertyWithDefaults(deserializer, "isSearcheable"); + this._overridePropertyWithDefaults(deserializer, "isSearchable"); this._overridePropertyWithDefaults(deserializer, "isOrdered"); this._overridePropertyWithDefaults(deserializer, "hasUniqueValues"); this._overridePropertyWithDefaults(deserializer, "description"); @@ -397,7 +397,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * @type {boolean} * @default false */ - isSearcheable: { + isSearchable: { value: Defaults.readOnly }, From 4ab7c5f48dab3a55b4622ce69c95ae934ab100f5 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 18 Feb 2020 14:54:57 -0800 Subject: [PATCH 133/407] add converter matching how serialization serializes dates --- .../RFC3339UTC-string-to-date-converter.js | 59 +++++++++++++++++++ .../RFC3339UTC-string-to-date-converter.mjson | 25 ++++++++ 2 files changed, 84 insertions(+) create mode 100644 core/converter/RFC3339UTC-string-to-date-converter.js create mode 100644 core/converter/RFC3339UTC-string-to-date-converter.mjson diff --git a/core/converter/RFC3339UTC-string-to-date-converter.js b/core/converter/RFC3339UTC-string-to-date-converter.js new file mode 100644 index 0000000000..d6ff9ca73f --- /dev/null +++ b/core/converter/RFC3339UTC-string-to-date-converter.js @@ -0,0 +1,59 @@ +/** + * @module montage/core/converter/RFC3339UTC-string-to-date-converter + * @requires montage/core/converter/converter + */ +var Converter = require("./converter").Converter, + deprecate = require("../extras/date"), + singleton; + +/** + * @class RFC3339UTCStringToDateConverter + * @classdesc Converts an RFC3339 UTC string to a date and reverts it. + */ +var RFC3339UTCStringToDateConverter = exports.RFC3339UTCStringToDateConverter = Converter.specialize({ + + constructor: { + value: function () { + if (this.constructor === RFC3339UTCStringToDateConverter) { + if (!singleton) { + singleton = this; + } + + return singleton; + } + + return this; + } + }, + + /** + * Converts the RFC3339 string to a Date. + * @function + * @param {string} v The string to convert. + * @returns {Date} The Date converted from the string. + */ + convert: {value: function (v) { + return Date.parseRFC3339(v); + }}, + + /** + * Reverts the specified Date to an RFC3339 String. + * @function + * @param {string} v The specified string. + * @returns {string} + */ + revert: {value: function (v) { + return v.toISOString(); + }} + +}); + +Object.defineProperty(exports, 'singleton', { + get: function () { + if (!singleton) { + singleton = new RFC3339UTCStringToDateConverter(); + } + + return singleton; + } +}); diff --git a/core/converter/RFC3339UTC-string-to-date-converter.mjson b/core/converter/RFC3339UTC-string-to-date-converter.mjson new file mode 100644 index 0000000000..170474b0d7 --- /dev/null +++ b/core/converter/RFC3339UTC-string-to-date-converter.mjson @@ -0,0 +1,25 @@ +{ + "converter_descriptor": { + "object": "core/converter/converter.mjson" + }, + "root": { + "prototype": "core/meta/module-object-descriptor", + "values": { + "name": "RFC3339UTCStringToDateConverter", + "customPrototype": false, + "parent": { + "@": "converter_descriptor" + }, + "propertyDescriptors": [], + "propertyDescriptorGroups": {}, + "propertyValidationRules": {}, + "objectDescriptorModule": { + "%": "core/converter/RFC3339UTC-string-to-date-converter.mjson" + }, + "exportName": "RFC3339UTCStringToDateConverter", + "module": { + "%": "core/converter/RFC3339UTC-string-to-date-converter" + } + } + } +} From 725320ea05456518ad1105e970b99c1a1ad12670 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 21 Feb 2020 00:51:19 -0800 Subject: [PATCH 134/407] Improves handling of missing rule --- data/service/expression-data-mapping.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index c22355055d..9f60ff686d 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -352,7 +352,11 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData if (this.requisitePropertyNames.size) { while ((propertyName = iterator.next().value)) { objectRule = objectMappingRules.get(propertyName); - rule = rawDataMappingRules.get(objectRule.sourcePath); + if(objectRule) { + rule = rawDataMappingRules.get(objectRule.sourcePath); + } else { + console.error("expression-dat-mapping.js: - rawRequisitePropertyNames: couldn't find object rule for propertyName -"+propertyName+" of objectDescriptor "+this.objectDescriptor.name); + } if(rule) { result.add(objectRule.sourcePath); From 95c768ef4fc35c1adb3aad66d4233ae6d8251b63 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 21 Feb 2020 16:03:44 -0800 Subject: [PATCH 135/407] fix spec following change in default identifier constructioon --- test/spec/meta/module-object-descriptor-spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/spec/meta/module-object-descriptor-spec.js b/test/spec/meta/module-object-descriptor-spec.js index 186e462b9d..4d72825dd5 100644 --- a/test/spec/meta/module-object-descriptor-spec.js +++ b/test/spec/meta/module-object-descriptor-spec.js @@ -28,7 +28,7 @@ describe("meta/module-object-descriptor-spec", function () { }; var objectDescriptorSerialization = { - "objectDescriptor_one_a": { + "One_a": { "prototype": "montage/core/meta/property-descriptor", "values": { "name": "a", @@ -40,7 +40,7 @@ describe("meta/module-object-descriptor-spec", function () { "values": { "name": "One", "propertyDescriptors": [ - {"@": "objectDescriptor_one_a"} + {"@": "One_a"} ], "maxAge": 240, "module": {"%": "spec/meta/module-object-descriptor-spec"}, From b5f9f3290745390f105fe31893988d12e1082b1f Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 21 Feb 2020 16:05:02 -0800 Subject: [PATCH 136/407] - rename mandatory to isMandatory so it's cleat it's a boolean - improve _overridePropertyWithDefaults to minimize array creation --- core/meta/property-descriptor.js | 39 +++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index ba94af565f..57ee04500e 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -36,7 +36,7 @@ exports.DeleteRule = DeleteRule = new Enum().initWithMembersAndValues(["NULLIFY" var Defaults = { name: "default", cardinality: 1, - mandatory: false, + isMandatory: false, readOnly: false, denyDelete: false, deleteRule: DeleteRule.Nullify, @@ -52,6 +52,8 @@ var Defaults = { isLocalizable: false, isSearchable: false, isOrdered: false, + isUnique: false, + isSerializable: true, hasUniqueValues: false, }; @@ -129,7 +131,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# } else { this._setPropertyWithDefaults(serializer, "cardinality", this.cardinality); } - this._setPropertyWithDefaults(serializer, "mandatory", this.mandatory); + this._setPropertyWithDefaults(serializer, "isMandatory", this.isMandatory); this._setPropertyWithDefaults(serializer, "readOnly", this.readOnly); //Not needed anymore as it's now this.deleteRule === DeleteRule.DENY //and deserializing denyDelete will set the equivallent on value deleteRule @@ -151,6 +153,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._setPropertyWithDefaults(serializer, "isSerializable", this.isSerializable); this._setPropertyWithDefaults(serializer, "isSearchable", this.isSearchable); this._setPropertyWithDefaults(serializer, "isOrdered", this.isOrdered); + this._setPropertyWithDefaults(serializer, "isUnique", this.isUnique); this._setPropertyWithDefaults(serializer, "hasUniqueValues", this.hasUniqueValues); this._setPropertyWithDefaults(serializer, "description", this.description); @@ -175,7 +178,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this.cardinality = Infinity; } - this._overridePropertyWithDefaults(deserializer, "mandatory"); + this._overridePropertyWithDefaults(deserializer, "isMandatory", "mandatory"); this._overridePropertyWithDefaults(deserializer, "readOnly"); this._overridePropertyWithDefaults(deserializer, "denyDelete"); this._overridePropertyWithDefaults(deserializer, "deleteRule"); @@ -193,6 +196,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._overridePropertyWithDefaults(deserializer, "isSerializable"); this._overridePropertyWithDefaults(deserializer, "isSearchable"); this._overridePropertyWithDefaults(deserializer, "isOrdered"); + this._overridePropertyWithDefaults(deserializer, "isUnique"); this._overridePropertyWithDefaults(deserializer, "hasUniqueValues"); this._overridePropertyWithDefaults(deserializer, "description"); } @@ -239,15 +243,16 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# if (arguments.length > 2) { propertyNames = Array.prototype.slice.call(arguments, 2, Infinity); + + for (i = 0, n = propertyNames.length; i < n && !value; i++) { + value = deserializer.getProperty(propertyNames[i]); + } } else { - propertyNames = [objectKey]; - } + value = deserializer.getProperty(objectKey); - for (i = 0, n = propertyNames.length; i < n && !value; i++) { - value = deserializer.getProperty(propertyNames[i]); } - this[objectKey] = value === undefined ? Defaults[propertyNames[0]] : value; + this[objectKey] = value === undefined ? Defaults[propertyNames ? propertyNames[0] : objectKey] : value; } }, @@ -332,8 +337,8 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * @type {boolean} * @default false */ - mandatory: { - value: Defaults.mandatory + isMandatory: { + value: Defaults.isMandatory }, /** @@ -417,6 +422,18 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# value: Defaults.isOrdered }, + /** + * models if the value of the property is unique among all instances described + * by the propertyDescriptor's owner. + * + * @type {boolean} + * @default false + */ + isUnique: { + value: Defaults.isUnique + }, + + /** * models if the values of a collection / to-many property should be unique, * This is relevant for example for relationships where a relational DB would @@ -551,7 +568,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * @default false */ isSerializable: { - value: true + value: Defaults.isSerializable }, /** From 822a82231bb94e8649d9c1c7db811edacbf253c9 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 21 Feb 2020 16:05:59 -0800 Subject: [PATCH 137/407] - fix regression if DataObject (DO) doesn't implement dispatchEvent --- data/service/data-service.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 0635d49cd0..2a2d01c0a9 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -1941,8 +1941,8 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { but it's very specialized for components. Having all prototypes of DO register as eventListeners upfront would be damaging performance wise. We should do it as things happen. */ - - var eventPool = this._dataEventPoolForEventType(eventType), + if(object.dispatchEvent) { + var eventPool = this._dataEventPoolForEventType(eventType), objectDescriptor = this.objectDescriptorForObject(object), objectConstructor = object.constructor, dataEvent = eventPool.checkout(); @@ -1953,11 +1953,12 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { dataEvent.dataObject = object; dataEvent.detail = detail; - if(!this.isConstructorPreparedToHandleDataEvents(objectConstructor)) { - this.prepareConstructorToHandleDataEvents(objectConstructor, dataEvent); - } + if(!this.isConstructorPreparedToHandleDataEvents(objectConstructor)) { + this.prepareConstructorToHandleDataEvents(objectConstructor, dataEvent); + } - object.dispatchEvent(dataEvent); + object.dispatchEvent(dataEvent); + } } }, From 0ea8578d9a03bb9c21e2ad229496cf520bc429d8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 27 Feb 2020 01:28:26 -0800 Subject: [PATCH 138/407] Fix bug if .event is empty --- core/event/mutable-event.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/event/mutable-event.js b/core/event/mutable-event.js index fb12cd9c7a..23855e6e3d 100644 --- a/core/event/mutable-event.js +++ b/core/event/mutable-event.js @@ -201,7 +201,11 @@ var wrapPropertyGetter = function (key, storageKey) { */ currentTarget: { get: function () { - return (this._currentTarget !== void 0) ? this._currentTarget : this._event.currentTarget; + return (this._currentTarget !== void 0) + ? this._currentTarget + : this._event + ? this._event.currentTarget + : undefined; }, set: function (value) { this._currentTarget = value; From 1639c15ebc416361f171024c3f1acfaefc6dcbe1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 28 Feb 2020 14:55:26 -0800 Subject: [PATCH 139/407] fix timeStamp spelling to be consistent with Event's --- data/model/data-event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/model/data-event.js b/data/model/data-event.js index ca12d51caa..1176349711 100644 --- a/data/model/data-event.js +++ b/data/model/data-event.js @@ -34,7 +34,7 @@ exports.DataEvent = MutableEvent.specialize({ constructor: { value: function (type) { - this.timestamp = performance.now(); + this.timeStamp = performance.now(); } }, From d12d7cf42996156baf6b658f685fadd5ed3e46ae Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 28 Feb 2020 14:56:02 -0800 Subject: [PATCH 140/407] Fix timeStamp spelling to be consistent with Event's --- core/event/change-event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/event/change-event.js b/core/event/change-event.js index 057f77c4da..1cf579a9d6 100644 --- a/core/event/change-event.js +++ b/core/event/change-event.js @@ -17,7 +17,7 @@ var Montage = require("core/core").Montage, var ChangeEvent = exports.ChangeEvent = Montage.specialize({ constructor: { value: function ChangeEvent() { - this.timestamp = performance.now(); + this.timeStamp = performance.now(); return this; } }, From e7b4abf831ea1739ab2e38fa135ff98a2ec19b28 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 28 Feb 2020 14:56:25 -0800 Subject: [PATCH 141/407] fix bugs when _evnt is missing --- core/event/mutable-event.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/core/event/mutable-event.js b/core/event/mutable-event.js index 23855e6e3d..3385daf4b3 100644 --- a/core/event/mutable-event.js +++ b/core/event/mutable-event.js @@ -171,7 +171,11 @@ var wrapPropertyGetter = function (key, storageKey) { */ eventPhase: { get: function () { - return (this._eventPhase !== void 0) ? this._eventPhase : this._event.eventPhase; + return (this._eventPhase !== void 0) + ? this._eventPhase + : this._event + ? this._event.eventPhase + : undefined; }, set: function (value) { this._eventPhase = value; @@ -186,7 +190,11 @@ var wrapPropertyGetter = function (key, storageKey) { */ target: { get: function () { - return (this._target !== void 0) ? this._target : this._event ? this._event.target : undefined; + return (this._target !== void 0) + ? this._target + : this._event + ? this._event.target + : undefined; }, set: function (value) { this._target = value; @@ -247,7 +255,7 @@ var wrapPropertyGetter = function (key, storageKey) { */ touches: { get: function () { - return this._event.touches; + return this._event ? this._event.touches : null; }, set: function (value) { this._event.touches = value; @@ -259,7 +267,7 @@ var wrapPropertyGetter = function (key, storageKey) { */ changedTouches: { get: function () { - return this._event.changedTouches; + return this._event ? this._event.changedTouches : null; }, set: function (value) { this._event.changedTouches = value; @@ -271,7 +279,7 @@ var wrapPropertyGetter = function (key, storageKey) { */ targetTouches: { get: function () { - return this._event.targetTouches; + return this._event ? this._event.targetTouches : null; } }, _defaultPrevented: { @@ -298,7 +306,11 @@ var wrapPropertyGetter = function (key, storageKey) { */ timeStamp: { get: function () { - return (this._timeStamp !== void 0) ? this._timeStamp : this._event.timeStamp; + return (this._timeStamp !== void 0) + ? this._timeStamp + : this._event + ? this._event.timeStamp + : undefined; }, set: function (value) { this._timeStamp = value; From 381f64136e1e3cb8a05f16f0767c4c3ac44937f1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 28 Feb 2020 14:58:01 -0800 Subject: [PATCH 142/407] make requiredObjectProperties oly relevant for objects whose mapping has a primacry key, whicj isn't the case if there are just stored embeded in others --- data/service/expression-data-mapping.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 9f60ff686d..e29dffa049 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -921,6 +921,21 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData * Prefetches any object properties required to map the rawData property * and maps once the fetch is complete. * + * This is a bit overeaching. There are two reasons we want to mapObjectToRawDataProperty: + * 1. Create an object + * 1.1 If we create an object, properties that are not relations can't + * be fetched. We need to make sure we don't actually try. + * 1.2 If a property is a relationship and it wasn't set on the object, + * as an object, we can't get it either. + * 1.3 So, for created objects, validation rules should prevent the attempt to save if + * mandory properties are missing. + * + * 2. Update an object. + * 2.1 We know what has changed + * 2.2 A property that is a relation to an object, especially without an object descriptor + * but also with one can be stored inline as json. + * + * * @method * @argument {Object} object - An object whose properties' values * hold the model data. @@ -931,10 +946,15 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData mapObjectToRawDataProperty: { value: function (object, data, propertyName) { var rule = this.rawDataMappingRules.get(propertyName), - requiredObjectProperties = rule ? rule.requirements : [], + //Adding a test for rawDataPrimaryKeys. Types that are meant to be + //embedded in others don't have a rawDataPrimaryKeys + //as they don't exists on their own. + requiredObjectProperties = (rule && this.rawDataPrimaryKeys) ? rule.requirements : null, result, self; - result = this.service.rootService.getObjectPropertyExpressions(object, requiredObjectProperties); + if(requiredObjectProperties) { + result = this.service.rootService.getObjectPropertyExpressions(object, requiredObjectProperties); + } if (this._isAsync(result)) { self = this; From 0e666fef692dd5d707d67fca8974978783a77c7a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 28 Feb 2020 14:59:57 -0800 Subject: [PATCH 143/407] - remove unused _objectDescriptorForObject method - add mappingForObjectDescriptor - refactor mappingForObject on mappingForObjectDescriptor --- data/service/raw-data-service.js | 56 +++++++++++++++++++------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index b9ab6fbe3a..25a45fe73d 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -111,23 +111,24 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } }, - _objectDescriptorForObject: { - value: function (object) { - var types = this.types, - objectInfo = Montage.getInfoForObject(object), - moduleId = objectInfo.moduleId, - objectName = objectInfo.objectName, - module, exportName, objectDescriptor, i, n; - for (i = 0, n = types.length; i < n && !objectDescriptor; i += 1) { - module = types[i].module; - exportName = module && types[i].exportName; - if (module && moduleId === module.id && objectName === exportName) { - objectDescriptor = types[i]; - } - } - return objectDescriptor; - } - }, + //Benoit: 2/25/2020 Doesn't seem to be used anywhere. + // _objectDescriptorForObject: { + // value: function (object) { + // var types = this.types, + // objectInfo = Montage.getInfoForObject(object), + // moduleId = objectInfo.moduleId, + // objectName = objectInfo.objectName, + // module, exportName, objectDescriptor, i, n; + // for (i = 0, n = types.length; i < n && !objectDescriptor; i += 1) { + // module = types[i].module; + // exportName = module && types[i].exportName; + // if (module && moduleId === module.id && objectName === exportName) { + // objectDescriptor = types[i]; + // } + // } + // return objectDescriptor; + // } + // }, _mapObjectPropertyValue: { value: function (object, propertyDescriptor, value) { @@ -955,15 +956,14 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot }, /** - * Retrieve DataMapping for this object. + * Retrieve DataMapping for passed objectDescriptor. * * @method * @argument {Object} object - An object whose object descriptor has a DataMapping */ - mappingForObject: { - value: function (object) { - var objectDescriptor = this.objectDescriptorForObject(object), - mapping = objectDescriptor && this.mappingWithType(objectDescriptor); + mappingForObjectDescriptor: { + value: function (objectDescriptor) { + var mapping = objectDescriptor && this.mappingWithType(objectDescriptor); if (!mapping) { @@ -983,6 +983,18 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } }, + /** + * Retrieve DataMapping for this object. + * + * @method + * @argument {Object} object - An object whose object descriptor has a DataMapping + */ + mappingForObject: { + value: function (object) { + return this.mappingForObjectDescriptor(this.objectDescriptorForObject(object)); + } + }, + /** * Convert raw data to data objects of an appropriate type. * From ecace9a6e8fd73ac77940e35174056b1ede34f3f Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 10:48:54 -0700 Subject: [PATCH 144/407] add lint debug help --- node.js | 23 +++++- package.json | 216 +++++++++++++++++++++++++++------------------------ 2 files changed, 138 insertions(+), 101 deletions(-) diff --git a/node.js b/node.js index 4a675447e9..26aa124047 100644 --- a/node.js +++ b/node.js @@ -1,7 +1,7 @@ /*jshint node:true, browser:false */ +var Require = require("mr"); var FS = require("q-io/fs"); var MontageBoot = require("./montage"); -var Require = require("mr"); var URL = require("url"); var htmlparser = require("htmlparser2"); @@ -64,6 +64,27 @@ MontageBoot.loadPackage = function (location, config) { config.overlays = ["node", "server", "montage"]; config.location = URL.resolve(Require.getLocation(), location); + //The equivalent is done in montage.js for the browser: + //install the linter, which loads on the first error + function lint(module) { + if(!lint.JSHINT) { + lint.JSHINT = require("jshint"); + } + if (!lint.JSHINT.JSHINT(module.text)) { + console.warn("JSHint Error: "+module.location); + lint.JSHINT.JSHINT.errors.forEach(function (error) { + if (error) { + console.warn("Problem at line "+error.line+" character "+error.character+": "+error.reason); + if (error.evidence) { + console.warn(" " + error.evidence); + } + } + }); + } + }; + config.lint = lint; + + return Require.loadPackage(config.location, config); }; diff --git a/package.json b/package.json index c88761156a..ca492f9385 100644 --- a/package.json +++ b/package.json @@ -1,105 +1,121 @@ { - "name": "montage", - "version": "19.0.0", - "description": "Build your next application with a browser based platform that really gets the web.", - "license": "BSD-3-Clause", - "repository": { - "type": "git", - "url": "https://github.com/montagejs/montage.git" - }, - "main": "montage", - "engines": { - "node": ">=10.16.2", - "npm": ">=6.9.0" - }, - "overlay": { - "browser": { - "main": "core/core", - "redirects": { - "montage": "core/core" - }, - "mappings": { - "mr": { - "name": "mr", - "location": "node_modules/mr" - }, - "bluebird": { - "name": "bluebird", - "location": "node_modules/bluebird" - } - } + "name": "montage", + "version": "19.0.0", + "description": "Build your next application with a browser based platform that really gets the web.", + "license": "BSD-3-Clause", + "repository": { + "type": "git", + "url": "https://github.com/montagejs/montage.git" }, - "node": { - "main": "core/core", - "redirects": { - "montage": "core/core" - }, - "mappings": { - "mr": { - "name": "mr", - "location": "node_modules/mr" + "main": "montage", + "engines": { + "node": ">=10.16.2", + "npm": ">=6.9.0" + }, + "overlay": { + "browser": { + "main": "core/core", + "redirects": { + "montage": "core/core" + }, + "mappings": { + "mr": { + "name": "mr", + "location": "core/mr" + }, + "bluebird": { + "name": "bluebird", + "location": "node_modules/bluebird" + } + } }, - "bluebird": { - "name": "bluebird", - "location": "node_modules/bluebird" + "node": { + "main": "core/core", + "redirects": { + "montage": "core/core" + }, + "mappings": { + "mr": { + "name": "mr", + "location": "core/mr" + }, + "bluebird": { + "name": "bluebird", + "location": "node_modules/bluebird" + } + } } - } - } - }, - "production": true, - "dependencies": { - "bluebird": "~3.5.5", - "collections": "~5.1.x", - "frb": "~4.0.x", - "htmlparser2": "~3.0.5", - "q-io": "github:marchant/q-io.git#bluebird-q-io", - "mr": "marchant/mr#master", - "weak-map": "^1.0.5", - "lodash.kebabcase": "^4.1.1", - "lodash.camelcase": "^4.3.0", - "lodash.trim": "^4.5.1", - "lodash.snakecase": "^4.1.1", - "proxy-polyfill": "~0.1.7", - "strange": "^1.7.2" - }, - "devDependencies": { - "concurrently": "^3.4.0", - "http-server": "^0.10.0", - "xhr2": "^0.1.4", - "jasmine-console-reporter": "^1.2.7", - "jasmine-core": "^2.5.2", - "jshint": "^2.9.5", - "karma": "^4.2.0", - "karma-chrome-launcher": "^3.0.0", - "karma-coverage": "^1.1.2", - "karma-firefox-launcher": "^1.2.0", - "karma-ie-launcher": "^1.0.0", - "karma-jasmine": "^2.0.1", - "karma-phantomjs-launcher": "^1.0.4", - "karma-safari-launcher": "^1.0.0", - "montage-testing": "git://github.com/montagejs/montage-testing.git#master", - "mop-integration": "git://github.com/montagejs/mop-integration.git#master", - "open": "0.0.5" - }, - "scripts": { - "test": "node test/run-node.js", - "jsdoc": "jsdoc -c jsdoc.json", - "start:demo": "concurrently \"http-server -p 8084\" \"open http://localhost:8084/demo/\"", - "integration": "MONTAGE_VERSION=${MONTAGE_VERSION:=./} MOP_VERSION=${MOP_VERSION:=#master} node node_modules/mop-integration/integration", - "lint": "jshint .", - "test:karma": "karma start --no-auto-watch --single-run", - "test:karma-travis": "karma start --no-auto-watch --single-run --browsers=Chrome_travis_ci", - "test:karma-firefox": "karma start --no-auto-watch --single-run --browsers=Firefox", - "test:karma-chrome": "karma start --no-auto-watch --single-run --browsers=Chrome", - "test:karma-debug": "karma start --auto-watch --no-single-run --browsers=PhantomJS_debug", - "test:karma-dev": "karma start --auto-watch --no-single-run --capture", - "test:jasmine": "concurrently \"http-server -p 8085\" \"open http://localhost:8085/test/run.html\"" - }, - "exclude": [ - "demo", - "report", - "doc", - "test", - "tools" - ] + }, + "production": true, + "dependencies": { + "mr": "./core/mr", + "collections": "./core/collections", + "q-io": "./core/promise-io", + "frb": "./core/frb", + "bluebird": "~3.5.5", + "htmlparser2": "~3.0.5", + "weak-map": "^1.0.5", + "lodash.kebabcase": "^4.1.1", + "lodash.camelcase": "^4.3.0", + "lodash.trim": "^4.5.1", + "lodash.snakecase": "^4.1.1", + "proxy-polyfill": "~0.1.7", + "strange": "^1.7.2", + "ical.js": "~1.4.0" + }, + "devDependencies": { + "montage-testing": "./testing", + "concurrently": "^3.4.0", + "http-server": "^0.10.0", + "xhr2": "^0.1.4", + "jasmine-console-reporter": "^1.2.7", + "jasmine-core": "^2.5.2", + "jshint": "^2.9.5", + "karma": "^4.2.0", + "karma-chrome-launcher": "^3.0.0", + "karma-coverage": "^1.1.2", + "karma-firefox-launcher": "^1.2.0", + "karma-ie-launcher": "^1.0.0", + "karma-jasmine": "^2.0.1", + "karma-phantomjs-launcher": "^1.0.4", + "karma-safari-launcher": "^1.0.0", + "mop-integration": "git://github.com/montagejs/mop-integration.git#master", + "open": "0.0.5", + "jasmine-node": "montagestudio/jasmine-node#master", + "pegjs": "git://github.com/dmajda/pegjs.git" + }, + "scripts": { + "test-mr": "node core/mr/test/run-node.js", + "lint-mr": "jshint core/mr", + "integration": "MONTAGE_VERSION=${MONTAGE_VERSION:=./} MOP_VERSION=${MOP_VERSION:=#master} node node_modules/mop-integration/integration", + "integration-debug": "node --inspect-brk=9229 node_modules/mop-integration/integration.js", + "test-mr:karma": "karma start --no-auto-watch --single-run", + "test-mr:karma-dev": "karma start --auto-watch --no-single-run", + "test-mr:jasmine": "concurrently \"http-server -p 8081 core/mr\" \"open http://localhost:8081/test/run.html\"", + "test-mr:demo": "concurrently \"http-server -a localhost -p 8082 core/mr\" \"open http://localhost:8082/demo/\"", + "test": "node test/run-node.js", + "jsdoc": "jsdoc -c jsdoc.json", + "start:demo": "concurrently \"http-server -p 8084\" \"open http://localhost:8084/demo/\"", + "lint": "jshint .", + "test:karma": "karma start --no-auto-watch --single-run", + "test:karma-travis": "karma start --no-auto-watch --single-run --browsers=Chrome_travis_ci", + "test:karma-firefox": "karma start --no-auto-watch --single-run --browsers=Firefox", + "test:karma-chrome": "karma start --no-auto-watch --single-run --browsers=Chrome", + "test:karma-debug": "karma start --auto-watch --no-single-run --browsers=PhantomJS_debug", + "test:karma-dev": "karma start --auto-watch --no-single-run --capture", + "test:jasmine": "concurrently \"http-server -p 8085\" \"open http://localhost:8085/test/run.html\"", + "test-collections": "node core/collections/test/run-node.js", + "test-frb": "jasmine-node core/frb/spec", + "build-frb-parser": "pegjs --allowed-start-rules expression,sheet core/frb/grammar.pegjs" + }, + "bin": { + "mr": "core/mr/bin/mr" + }, + "exclude": [ + "demo", + "report", + "doc", + "test", + "tools" + ] } From 1614fc2b0cd64252255c59d06bc9fbe0d38bb76b Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 10:49:41 -0700 Subject: [PATCH 145/407] super invocatiom optimization --- core/core.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/core.js b/core/core.js index 5c0b29623a..2509063588 100644 --- a/core/core.js +++ b/core/core.js @@ -675,9 +675,12 @@ function __super(callerFn, methodPropertyName, isValue, isGetter, isSetter) { */ function _super() { // Figure out which function called us. - var callerFn = ( _super && _super.caller ) ? _super.caller : arguments.callee.caller, - superFn = __super.call(this,callerFn); - return superFn ? superFn.apply(this, arguments) : undefined; + // var callerFn = ( _super && _super.caller ) ? _super.caller : arguments.callee.caller, + // superFn = __super.call(this,callerFn); + // return superFn ? superFn.apply(this, arguments) : undefined; + + return ((__super.call(this, /* callerFn - Figure out which function called us.*/ (( _super && _super.caller ) ? _super.caller : arguments.callee.caller))) || Function.noop).apply(this, arguments); + } function _superForValue(methodName) { From 9cf7a957509a03769575fe6394a615916ce7a166 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 10:54:02 -0700 Subject: [PATCH 146/407] add systemLocaleIdentifier --- core/environment.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/environment.js b/core/environment.js index f101079708..4f7d66fb92 100644 --- a/core/environment.js +++ b/core/environment.js @@ -16,6 +16,12 @@ var Environment = exports.Environment = Montage.specialize({ } }, + systemLocaleIdentifier: { + get: function () { + return (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.userLanguage || navigator.language || navigator.browserLanguage || 'en' + } + }, + isBrowser: { value: (typeof window !== "undefined") }, From 5baf0b4156a0bc3f48cb1109f102802db95b008b Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 14:40:18 -0700 Subject: [PATCH 147/407] add compatibility for Range with Montage --- core/range.js | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/core/range.js b/core/range.js index cff2b75057..160667d76d 100644 --- a/core/range.js +++ b/core/range.js @@ -8,6 +8,89 @@ Also useful with PostgreSQL. */ -var Range = require("strange"); +var Range = require("strange"), + Montage = require("./core").Montage; exports.Range = Range; + +Range.prototype.equals = function(value, equals, memo) { + if(value && this.compareBegin(value.begin) === 0 && this.compareEnd(value.end) === 0) { + return true; + } else { + return false; + } +}; + +Range.empty = new Range(); + + +Range.prototype.intersection = function (other) { + if (this.isEmpty()) return Range.empty; + if (other.isEmpty()) return Range.empty; + + /* Compares the first range's beginning to the second's end. + * Returns `<0` if `a` begins before `b` ends, `0` if one starts where the other + * ends and `>1` if `a` begins after `b` ends. + * + * @example + * Range.compareBeginToEnd(new Range(0, 10), new Range(0, 5)) // => -1 + * Range.compareBeginToEnd(new Range(0, 10), new Range(-10, 0)) // => 0 + * Range.compareBeginToEnd(new Range(0, 10), new Range(-10, -5)) // => 1 + */ + + var begin, + //beginComparison, + //endComparison, + //bounds, + end; + //if((beginComparison = Range.compareBeginToEnd(this, other)) <= 0) { + if(Range.compareBeginToEnd(this, other) <= 0) { + begin = this.begin; + //bounds = beginComparison < 0 ? + } + //if((endComparison = Range.compareBeginToEnd(other, this)) <= 0) { + if(endComparison = Range.compareBeginToEnd(other, this) <= 0) { + end = other.end; + } + + /* + Potgresql does: + intersection: int8range(5,15) * int8range(10,20) ->[10,15) + We'll keep the bounds as the default [] for now + */ + if(begin !== undefined && end !== undefined) { + return new Range(begin,end); + } else { + return Range.empty; + } + +}; + +Range.prototype.overlaps = Range.prototype.intersects; + + +Range.getInfoForObject = function(object) { + return Montage.getInfoForObject(object); +}; + +Range.prototype.serializeSelf = function (serializer) { + serializer.setProperty("begin", this.begin); + serializer.setProperty("end", this.end); + serializer.setProperty("bounds", this.bounds); +}; + +Range.prototype.deserializeSelf = function (deserializer) { + var value; + value = deserializer.getProperty("begin"); + if (value !== void 0) { + this.begin = value; + } + value = deserializer.getProperty("end"); + if (value !== void 0) { + this.end = value; + } + value = deserializer.getProperty("bounds"); + if (value !== void 0) { + this.bounds = value; + } +}; From 45af0130d569773ca6800093a22eefb18c0b51e8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 14:41:02 -0700 Subject: [PATCH 148/407] bring collections in montage --- core/collections/.editorconfig | 11 + core/collections/.gitignore | 9 + core/collections/.jshintignore | 7 + core/collections/.jshintrc | 32 + core/collections/.travis.yml | 17 + core/collections/CHANGES.md | 232 ++++++ core/collections/FUTURE.md | 33 + core/collections/LICENSE.md | 31 + core/collections/README.md | 41 + core/collections/_dict.js | 206 +++++ core/collections/_fast-set.js | 233 ++++++ core/collections/_list.js | 394 +++++++++ core/collections/_map.js | 310 +++++++ core/collections/_set.js | 282 +++++++ core/collections/bench/sorted.js | 52 ++ core/collections/checklist.csv | 97 +++ core/collections/collections.cat.js | 188 +++++ core/collections/collections.js | 28 + core/collections/demo/all.js | 8 + core/collections/demo/array-demo.js | 13 + core/collections/demo/fast-map-demo.js | 48 ++ core/collections/demo/fast-set-demo.js | 57 ++ core/collections/demo/iterator-demo.js | 132 +++ core/collections/demo/list-demo.js | 66 ++ core/collections/demo/log-demo.js | 32 + core/collections/demo/lru-map-demo.js | 17 + core/collections/demo/lru-set-demo.js | 15 + core/collections/demo/map-demo.js | 42 + core/collections/demo/multi-map-demo.js | 20 + core/collections/demo/object-demo.js | 7 + .../collections/demo/observable-array-demo.js | 20 + .../demo/observable-object-demo.js | 12 + core/collections/demo/set-demo.js | 49 ++ core/collections/demo/sorted-array.js | 10 + core/collections/demo/sorted-map-demo.js | 32 + core/collections/demo/sorted-set-demo.js | 59 ++ core/collections/demo/weak-map-demo.js | 17 + core/collections/deque.js | 465 +++++++++++ core/collections/dict.js | 11 + core/collections/fast-map.js | 59 ++ core/collections/fast-set.js | 9 + core/collections/generic-collection.js | 324 ++++++++ core/collections/generic-map.git.js | 201 +++++ core/collections/generic-map.js | 211 +++++ core/collections/generic-order.js | 58 ++ core/collections/generic-set.js | 89 ++ core/collections/heap.js | 245 ++++++ core/collections/iterator.js | 380 +++++++++ core/collections/karma.conf.js | 167 ++++ core/collections/lfu-map.js | 83 ++ core/collections/lfu-set.js | 247 ++++++ core/collections/list.js | 270 ++++++ core/collections/listen/array-changes.js | 429 ++++++++++ core/collections/listen/change-descriptor.js | 141 ++++ core/collections/listen/map-changes.js | 262 ++++++ core/collections/listen/property-changes.js | 481 +++++++++++ core/collections/listen/range-changes.js | 267 ++++++ core/collections/lru-map.js | 83 ++ core/collections/lru-set.js | 149 ++++ core/collections/map.js | 16 + core/collections/minify | 47 ++ core/collections/multi-map.js | 41 + core/collections/package.json | 82 ++ core/collections/set.js | 250 ++++++ core/collections/shim-array.js | 472 +++++++++++ core/collections/shim-function.js | 59 ++ core/collections/shim-object.js | 601 ++++++++++++++ core/collections/shim-regexp.js | 14 + core/collections/shim.js | 6 + core/collections/sorted-array-map.js | 54 ++ core/collections/sorted-array-set.js | 54 ++ core/collections/sorted-array.js | 377 +++++++++ core/collections/sorted-map.js | 68 ++ core/collections/sorted-set.js | 780 ++++++++++++++++++ core/collections/test/all.js | 35 + core/collections/test/min.html | 10 + core/collections/test/package.json | 29 + core/collections/test/run-browser.js | 112 +++ core/collections/test/run-karma.html | 14 + core/collections/test/run-karma.js | 104 +++ core/collections/test/run-node.js | 49 ++ core/collections/test/run.html | 14 + core/collections/test/spec/array-spec.js | 341 ++++++++ core/collections/test/spec/clone-spec.js | 21 + core/collections/test/spec/collection.js | 124 +++ core/collections/test/spec/deque-fuzz.js | 92 +++ core/collections/test/spec/deque-spec.js | 123 +++ core/collections/test/spec/deque.js | 340 ++++++++ core/collections/test/spec/dict-spec.js | 27 + core/collections/test/spec/dict.js | 86 ++ core/collections/test/spec/fast-map-spec.js | 12 + core/collections/test/spec/fast-set-spec.js | 172 ++++ core/collections/test/spec/fuzz.js | 79 ++ core/collections/test/spec/heap-spec.js | 107 +++ core/collections/test/spec/iterator-spec.js | 649 +++++++++++++++ core/collections/test/spec/lfu-map-spec.js | 85 ++ core/collections/test/spec/lfu-set-spec.js | 60 ++ core/collections/test/spec/list-spec.js | 214 +++++ .../test/spec/listen/array-changes-spec.js | 559 +++++++++++++ .../test/spec/listen/map-changes.js | 63 ++ .../test/spec/listen/property-changes-spec.js | 302 +++++++ .../test/spec/listen/range-changes.js | 373 +++++++++ core/collections/test/spec/lru-map-spec.js | 84 ++ core/collections/test/spec/lru-set-spec.js | 55 ++ core/collections/test/spec/map-spec.js | 15 + core/collections/test/spec/map.js | 93 +++ core/collections/test/spec/multi-map-spec.js | 7 + core/collections/test/spec/order.js | 377 +++++++++ core/collections/test/spec/permute.js | 18 + core/collections/test/spec/prng.js | 9 + core/collections/test/spec/regexp-spec.js | 34 + core/collections/test/spec/set-spec.js | 258 ++++++ core/collections/test/spec/set.js | 134 +++ core/collections/test/spec/shim-array-spec.js | 51 ++ .../test/spec/shim-functions-spec.js | 49 ++ .../collections/test/spec/shim-object-spec.js | 593 +++++++++++++ .../test/spec/sorted-array-map-spec.js | 14 + .../test/spec/sorted-array-set-spec.js | 20 + .../test/spec/sorted-array-spec.js | 95 +++ core/collections/test/spec/sorted-map-spec.js | 43 + core/collections/test/spec/sorted-set-spec.js | 457 ++++++++++ core/collections/test/spec/to-json.js | 19 + core/collections/tree-log.js | 40 + core/collections/weak-map.js | 2 + 124 files changed, 17074 insertions(+) create mode 100644 core/collections/.editorconfig create mode 100644 core/collections/.gitignore create mode 100644 core/collections/.jshintignore create mode 100644 core/collections/.jshintrc create mode 100644 core/collections/.travis.yml create mode 100644 core/collections/CHANGES.md create mode 100644 core/collections/FUTURE.md create mode 100644 core/collections/LICENSE.md create mode 100644 core/collections/README.md create mode 100644 core/collections/_dict.js create mode 100644 core/collections/_fast-set.js create mode 100644 core/collections/_list.js create mode 100644 core/collections/_map.js create mode 100644 core/collections/_set.js create mode 100644 core/collections/bench/sorted.js create mode 100644 core/collections/checklist.csv create mode 100644 core/collections/collections.cat.js create mode 100644 core/collections/collections.js create mode 100644 core/collections/demo/all.js create mode 100644 core/collections/demo/array-demo.js create mode 100644 core/collections/demo/fast-map-demo.js create mode 100644 core/collections/demo/fast-set-demo.js create mode 100644 core/collections/demo/iterator-demo.js create mode 100644 core/collections/demo/list-demo.js create mode 100644 core/collections/demo/log-demo.js create mode 100644 core/collections/demo/lru-map-demo.js create mode 100644 core/collections/demo/lru-set-demo.js create mode 100644 core/collections/demo/map-demo.js create mode 100644 core/collections/demo/multi-map-demo.js create mode 100644 core/collections/demo/object-demo.js create mode 100644 core/collections/demo/observable-array-demo.js create mode 100644 core/collections/demo/observable-object-demo.js create mode 100644 core/collections/demo/set-demo.js create mode 100644 core/collections/demo/sorted-array.js create mode 100644 core/collections/demo/sorted-map-demo.js create mode 100644 core/collections/demo/sorted-set-demo.js create mode 100755 core/collections/demo/weak-map-demo.js create mode 100644 core/collections/deque.js create mode 100644 core/collections/dict.js create mode 100644 core/collections/fast-map.js create mode 100644 core/collections/fast-set.js create mode 100644 core/collections/generic-collection.js create mode 100644 core/collections/generic-map.git.js create mode 100644 core/collections/generic-map.js create mode 100644 core/collections/generic-order.js create mode 100644 core/collections/generic-set.js create mode 100644 core/collections/heap.js create mode 100644 core/collections/iterator.js create mode 100644 core/collections/karma.conf.js create mode 100644 core/collections/lfu-map.js create mode 100644 core/collections/lfu-set.js create mode 100644 core/collections/list.js create mode 100644 core/collections/listen/array-changes.js create mode 100644 core/collections/listen/change-descriptor.js create mode 100644 core/collections/listen/map-changes.js create mode 100644 core/collections/listen/property-changes.js create mode 100644 core/collections/listen/range-changes.js create mode 100644 core/collections/lru-map.js create mode 100644 core/collections/lru-set.js create mode 100644 core/collections/map.js create mode 100644 core/collections/minify create mode 100644 core/collections/multi-map.js create mode 100644 core/collections/package.json create mode 100644 core/collections/set.js create mode 100644 core/collections/shim-array.js create mode 100644 core/collections/shim-function.js create mode 100644 core/collections/shim-object.js create mode 100644 core/collections/shim-regexp.js create mode 100644 core/collections/shim.js create mode 100644 core/collections/sorted-array-map.js create mode 100644 core/collections/sorted-array-set.js create mode 100644 core/collections/sorted-array.js create mode 100644 core/collections/sorted-map.js create mode 100644 core/collections/sorted-set.js create mode 100644 core/collections/test/all.js create mode 100644 core/collections/test/min.html create mode 100644 core/collections/test/package.json create mode 100644 core/collections/test/run-browser.js create mode 100644 core/collections/test/run-karma.html create mode 100644 core/collections/test/run-karma.js create mode 100644 core/collections/test/run-node.js create mode 100644 core/collections/test/run.html create mode 100644 core/collections/test/spec/array-spec.js create mode 100644 core/collections/test/spec/clone-spec.js create mode 100644 core/collections/test/spec/collection.js create mode 100644 core/collections/test/spec/deque-fuzz.js create mode 100644 core/collections/test/spec/deque-spec.js create mode 100644 core/collections/test/spec/deque.js create mode 100644 core/collections/test/spec/dict-spec.js create mode 100644 core/collections/test/spec/dict.js create mode 100644 core/collections/test/spec/fast-map-spec.js create mode 100644 core/collections/test/spec/fast-set-spec.js create mode 100644 core/collections/test/spec/fuzz.js create mode 100644 core/collections/test/spec/heap-spec.js create mode 100644 core/collections/test/spec/iterator-spec.js create mode 100644 core/collections/test/spec/lfu-map-spec.js create mode 100644 core/collections/test/spec/lfu-set-spec.js create mode 100644 core/collections/test/spec/list-spec.js create mode 100644 core/collections/test/spec/listen/array-changes-spec.js create mode 100644 core/collections/test/spec/listen/map-changes.js create mode 100644 core/collections/test/spec/listen/property-changes-spec.js create mode 100644 core/collections/test/spec/listen/range-changes.js create mode 100644 core/collections/test/spec/lru-map-spec.js create mode 100644 core/collections/test/spec/lru-set-spec.js create mode 100644 core/collections/test/spec/map-spec.js create mode 100644 core/collections/test/spec/map.js create mode 100644 core/collections/test/spec/multi-map-spec.js create mode 100644 core/collections/test/spec/order.js create mode 100644 core/collections/test/spec/permute.js create mode 100644 core/collections/test/spec/prng.js create mode 100644 core/collections/test/spec/regexp-spec.js create mode 100644 core/collections/test/spec/set-spec.js create mode 100644 core/collections/test/spec/set.js create mode 100644 core/collections/test/spec/shim-array-spec.js create mode 100644 core/collections/test/spec/shim-functions-spec.js create mode 100644 core/collections/test/spec/shim-object-spec.js create mode 100644 core/collections/test/spec/sorted-array-map-spec.js create mode 100644 core/collections/test/spec/sorted-array-set-spec.js create mode 100644 core/collections/test/spec/sorted-array-spec.js create mode 100644 core/collections/test/spec/sorted-map-spec.js create mode 100644 core/collections/test/spec/sorted-set-spec.js create mode 100644 core/collections/test/spec/to-json.js create mode 100644 core/collections/tree-log.js create mode 100644 core/collections/weak-map.js diff --git a/core/collections/.editorconfig b/core/collections/.editorconfig new file mode 100644 index 0000000000..c9721be133 --- /dev/null +++ b/core/collections/.editorconfig @@ -0,0 +1,11 @@ +# http://EditorConfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/core/collections/.gitignore b/core/collections/.gitignore new file mode 100644 index 0000000000..d321659598 --- /dev/null +++ b/core/collections/.gitignore @@ -0,0 +1,9 @@ +.npmignore +.tmp +.idea +.DS_Store +atlassian-ide-plugin.xml +npm-debug.log +report/ +node_modules/ +out/ \ No newline at end of file diff --git a/core/collections/.jshintignore b/core/collections/.jshintignore new file mode 100644 index 0000000000..425a9a5a91 --- /dev/null +++ b/core/collections/.jshintignore @@ -0,0 +1,7 @@ +**/lcov-report/** +**/node_modules/** +**/packages/** +**/spec/** +**/reporter/** +**/support/** +**/report/** \ No newline at end of file diff --git a/core/collections/.jshintrc b/core/collections/.jshintrc new file mode 100644 index 0000000000..ab9705cfc5 --- /dev/null +++ b/core/collections/.jshintrc @@ -0,0 +1,32 @@ +{ + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "forin": true, + "noarg": true, + "noempty": true, + "nonew": true, + "undef": true, + "unused": "paramsignore", + "trailing": true, + "indent": 4, + "boss": true, + "eqnull": true, + "browser": true, + "globals": { + "CustomEvent": true, + "WebSocket": false, + + "require": false, + "exports": false, + "module": false, + "global": false, + + "WeakMap": true, + "Map": true, + "Set": true, + + "console": false + } +} diff --git a/core/collections/.travis.yml b/core/collections/.travis.yml new file mode 100644 index 0000000000..d46ec6348a --- /dev/null +++ b/core/collections/.travis.yml @@ -0,0 +1,17 @@ +language: node_js +node_js: + - "4.8.0" +script: npm test +deploy: + provider: npm + email: "${NPM_EMAIL}" + api_key: "${NPM_API_KEY}" + on: + tags: true +notifications: + irc: + channels: + - "chat.freenode.net#montage" + on_success: false + template: + - "%{author} broke the %{repository} tests on %{branch}: %{build_url}" diff --git a/core/collections/CHANGES.md b/core/collections/CHANGES.md new file mode 100644 index 0000000000..33a0dc97b6 --- /dev/null +++ b/core/collections/CHANGES.md @@ -0,0 +1,232 @@ +## v5.1.5 +- fix object-shim missing Function.identity + +## v5.1.4 +- fix Constructor Map requires 'new' in MultiMap + +## v5.1.3 +- Fix Sorted-Set.reduceRight #206 +- Fix sorted array incomparable values bugs #171 + +## v5.1.2 +- Pass the this.sort arguments to this.sorted. #191 +- Add npm run build via browserify #190 + +## v5.1.1 +- Fix SortedArray handles incomparable values poorly #27 + +## v5.1.0 +- Addresses a bug with Array#find and deprecate find|findLast in favor of findValue|findLastValue API in collections. + +... + +## v5.0.2 +- Addresses a bug in range listening that happened when the number of listeners went beyond 1 and back to 0 + +## v5.0.1 +- Changes to make IE11 use set and map polyfills + +## v5.0.0 +- Some backward compatibility breaking changes: + - Native Maps, WeakMaps and Sets are now used when available + - Aligns Map with latest standard with PR #137 + - Use of a second argument for default value in get() is deprecated + - keys(), values() and entries() now return an iterator per standards, + methods returning an array are now keysArray(), valuesArray(), entriesArray() + - It's not possible to create a Map by passing an anonymous object to the constructor, that feature is now available as Map.from(); + - Introduces .from() method on constructors. +- Fix for issue #149 +- Changes to work in IE10, involving splitting pure implementations as private from the public API that support listening, + +## v3.0.0 + +- Aligns iterations with latest standard with PR #137 + +## v2.0.x + +- This version (v2 branch) does not have issue with Array.#find and expose only findValue|findLastValue API in collections. + +## v1.2.4 + +- Optimise performance of PropertyChanges with PR#126 and avoid unnecessary calls to require in Map Changes with PR #129 + +## v1.2.3 + +- Dict Optimization to remove the need to mangle/unmangle keys. This minimize the amount of string creation and therefore garbage collection + +## v1.2.2 + +- Vlad Alexandru Ionescu fixed a bug in dictionaries with single character keys. +- Optimizations for push to avoid creating unnecessary arrays through splice +- Fixes for a few regressions in listen and impacting Montage + +## v1.2.0 + +- Trevor Dixon fixed bugs in SortedSet find methods. +- Adds toJSON method to all collections. +- Adds deleteAll to some collections. +- Eliminate some extra work in change listener registration and dispatch by + using namespaced properties instead of a weak map, precomputing event + handler method names, and reusing an array to capture a snapshot of active + change listeners during dispatch. +- Fix SortedArrayMap isSorted flag. +- Fix Array find such that the sought value may be a wild card. +- MultiMap provides the key to the bucket maker +- Improve support for strings, maps, and arguments across implementations of + addEach +- Fix a bug in the generic join method +- Dict uses $ in mangled names instead of ~, so names are more frequently + valid identifiers. May have a performance win. +- Ignore property changes in non-configurable objects. + +## v1.1.0 + +- Adds an LfuSet, a set useful as a cache with a least-frequently-used + eviction strategy. +- Fixes array `set` and `swap` for indexes outside the bounds of the existing + array, for both observed and unobserved arrays. + +## v1.0.2 + +- Refinements on `Object.equals` and `Object.compare`. These are not + necessarily backward compatible, but should be a strict improvement: +- `Object.compare` will now return +/- Infinity for inequal strings, + rather than +/- 1 which imply that the distance between any two inequal + strings is always 1. `Object.compare` for numbers is suitable for finding + the magnitude of the difference as well as the direction. +- `Object.compare` and `Object.equals` will now delegate to either non-null, + non-undefined argument if the other argument is null or undefined. + This allows objects to be constructed that will identify themselves + as equivalent to null or undefined, for example `Any` types, useful for + testing. +- `Object.equals` will only compare object literals derrived directly from the + `Object.prototype`. All other objects that do not implement `compare` are + incomparable. +- First attempt at fixing `set`, `swap`, and `splice`, later fixed in v1.0.3. + `splice` must truncate the `start` index to the array length. `swap` and + `set` should not. + +## v1.0.1 + +- Bug fix for filter on map-like collections. + +## v1.0.0 :cake: + +- Adds a Deque type based on a circular buffer of exponential + capacity. (@petkaantonov) +- Implements `peek`, `peekBack`, `poke`, and `pokeBack` on array + shim for Deque “isomorphism”. +- Fixes the cases where a change listener is added or removed during + change dispatch. Neither listener will be informed until the next + change. (@asolove) +- The property change listener system has been altered such that + once a thunk has been installed on an object, it will not be + removed, in order to avoid churn. Once a property has been + observed, it is likely to be observed again. +- Fixes `Object.equals` for comparing NaN to itself, which should + report `true` such that collections that use `Object.equals` to + identify values are able to find `NaN`. Previously, `NaN` could + get stuck in a collection permanently. +- In abstract, Collections previously identified duck types by + looking only at the prototype chain, ignoring owned properties. + Thus, an object could distinguish a property name that was being + used as a key of a record, from the same property name that was + being used as a method name. To improve performance and to face + the reality that oftentimes an owned property is in fact a method, + Collections no longer observe this distinction. That is, if an + object has a function by the appropriate name, either by ownership + or inheritance, it will be recognized as a method of a duck type. + This particularly affects `Object.equals`, which should be much + faster now. +- Fixes `Object.equals` such that property for property comparison + between objects only happens if they both descend directly from + `Object.prototype`. Previously, objects would be thus compared if + they both descended from the same prototype. +- Accommodate *very* large arrays with the `swap` shim. Previously, + the size of an array swap was limited by the size of the + JavaScript run-time stack. (@francoisfrisch) +- Fixes `splice` on an array when given a negative start index. + (@stuk) +- Some methods accept an optional `equals` or `index` argument + that may or may not be supported by certain collections, like + `find` on a `SortedSet` versus a `List`. Collections that do not + support this argument will now throw an error instead of silently + ignoring the argument. +- Fixes `Array#clone` cycle detection. + +## v0.2.2 + +- `one` now returns a consistent value between changes of a sorted + set. +- All collections can now be required using the MontageJS style, as + well as the Node.js style. I reserve the right to withdraw support + for the current MontageJS style if in a future, + backward-incompatible release of Montage migrated to the Node.js + style. + +## v0.2.1 + +- Identify Maps with `isMap` property instead of `keys`, as ES6 + proposes `keys`, `values`, and `entries` methods for arrays. + +## v0.2.0 + +- Fixes the enumerability of dispatchesRangeChanges and + dispatchesMapChanges on observable arrays (and others, + incidentally). +- List and Set now dispatch valid range changes, at the penalty of + making updates linear when they are made observable. +- Adds `join` method to generic collections. +- Fixes a bug in `Object.has(object, value)`, where it would not + delegate polymorphically to `object.has(value)` +- Fixes `Object.addEach(object, undefined)`, such that it tolerates + the case without throwing an error, like `addEach` on other + collections. +- Fixes change dispatch on LruSet (Paul Koppen) such that a single + change event gets dispatched for both augmentation and truncation. +- Fixes change dispatch on Dict, such that the value gets sent on + addition. + +## v0.1.24 + +- Factored out WeakMap into separately maintained package. + +## v0.1.23 + +- Introduces `entries` and deprecates `items` on all map collections. +- Fixes Map clear change dispatch. + +## v0.1.22 + +- Fixes Set clear change dispatch. + +## v0.1.21 + +- Fixes a bug when the `plus` argument of swap is not an array. + +## v0.1.20 + +- Fixes generic map change dispatch on clear. +- Adds map change dispatch to Dict. + +## v0.1.18, v0.1.19 + +- Require total order on SortedSet +- Remove Node v0.6 from supported versions +- Add Node v0.10 to supported versions +- Fixes `hrtime` handling (Alexy Kupershtokh) + +## v0.1.17 + +... + +## v0.0.5 + +- The `observable-array` and `observable-object` modules have been + moved to the Functional Reactive Bindings (`frb`) package as `array` + and `object`. +- `List`, `Set`, and `SortedSet` now support content change + notifications compatibly with `frb`. +- The `observable` module provides generics methods for observables. + New collections need only call the appropriate dispatch functions if + `isObservable` is true. diff --git a/core/collections/FUTURE.md b/core/collections/FUTURE.md new file mode 100644 index 0000000000..44a2dd189c --- /dev/null +++ b/core/collections/FUTURE.md @@ -0,0 +1,33 @@ + +## Future work + +Goals + +- make array dispatch length property changes between range changes to + be consistent with List. +- comprehensive specs and spec coverage tests +- fast list splicing +- revise map changes to use separate handlers for add/delete +- revise tokens for range and map changes to specify complete alternate + delegate methods, particularly for forwarding directly to dispatch +- Make it easier to created a SortedSet with a criterion like + Function.by(Function.get('name')) +- evaluate exposing observeProperty, observeRangeChange, and observeMapChange + instead of the aEL/rEL inspired API FRB exposes today, to minimize + book-keeping and garbage collection +- possibly refactor to make shimming more opt-in + +More possible collections + +- sorted-list (sorted, can contain duplicates, perhaps backed by splay + tree with relaxation on the uniqueness invariant, or a skip list) +- sorted-multi-map (sorted, can contain duplicate entries) +- buffer (backed by a circular array, emits cull events) +- trie-set +- trie-map +- immutable-* (mutation functions return new objects that largely share + the previous version's internal state, some perhaps backed by a hash + trie) +- array set (a set, for fast lookup, backed by an array for meaningful + range changes) + diff --git a/core/collections/LICENSE.md b/core/collections/LICENSE.md new file mode 100644 index 0000000000..b4d1fb99bf --- /dev/null +++ b/core/collections/LICENSE.md @@ -0,0 +1,31 @@ +3-clause BSD license +==================== + +Copyright 2012-2014 Motorola Mobility LLC, Montage Studio Inc, and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Montage nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/core/collections/README.md b/core/collections/README.md new file mode 100644 index 0000000000..2a76101677 --- /dev/null +++ b/core/collections/README.md @@ -0,0 +1,41 @@ + +# Collections + +[![npm version](https://img.shields.io/npm/v/collections.svg?style=flat)](https://www.npmjs.com/package/collections) + +[![Build Status](https://travis-ci.org/montagejs/collections.png?branch=master)](http://travis-ci.org/montagejs/collections) + +[![Analytics](https://ga-beacon.appspot.com/UA-51771141-2/collections/readme)](https://github.com/igrigorik/ga-beacon) + +This package contains JavaScript implementations of common data +structures with idiomatic iterfaces, including extensions for Array and +Object. + +You can use these Node Packaged Modules with Node.js, [Browserify](https://github.com/substack/node-browserify), +[Mr](https://github.com/montagejs/mr), or any compatible CommonJS module loader. Using a module loader +or bundler when using Collections in web browsers has the advantage of +only incorporating the modules you need. However, you can just embed +` + + diff --git a/core/collections/test/package.json b/core/collections/test/package.json new file mode 100644 index 0000000000..8ea633af43 --- /dev/null +++ b/core/collections/test/package.json @@ -0,0 +1,29 @@ +{ + "_comment": [ + "# If you use npm to generate or modify this file, make sure", + "# to restore the file's 4 space indents after the generation", + "# or modification so it matches other source files in the", + "# project. npm only generates files with 2 space indents", + "# (see https://github.com/npm/npm/pull/3180#issuecomment-16336516)." + ], + "name": "run", + "version": "1.0.0-alpha.1", + "license": "BSD-3-Clause", + "author": "Montage Studio, inc. (http://montagestudio.com/)", + "repository": { + "type": "git", + "url": "montagestudio/montage-data-tests" + }, + "scripts": { + "postinstall": "npm link ./.." + }, + "mappings": { + "montage": "../../..", + "montage-testing": "../../../testing", + "collections": "../" + }, + "dependencies": { + "montage": "~16.0.x" + }, + "private": true +} diff --git a/core/collections/test/run-browser.js b/core/collections/test/run-browser.js new file mode 100644 index 0000000000..9fea88e4db --- /dev/null +++ b/core/collections/test/run-browser.js @@ -0,0 +1,112 @@ +/* global global:true, __dirname, jasmineRequire */ + +/*jshint evil:true */ +// reassigning causes eval to not use lexical scope. +var globalEval = eval, + global = globalEval('this'); +/*jshint evil:false */ + +// Init +var jasmine = jasmineRequire.core(jasmineRequire); +var jasmineEnv = jasmine.getEnv(); + +// Export interface +var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); +global.jasmine = jasmine; +for (var property in jasmineInterface) { + if (jasmineInterface.hasOwnProperty(property)) { + global[property] = jasmineInterface[property]; + } +} + +// +// Init Reporter +// + +function queryString(parameter) { + var i, key, value, equalSign; + var loc = location.search.substring(1, location.search.length); + var params = loc.split('&'); + for (i=0; i + + + Montage-Collections - Karma + + + + + + + + + + diff --git a/core/collections/test/run-karma.js b/core/collections/test/run-karma.js new file mode 100644 index 0000000000..72c0e1c8bc --- /dev/null +++ b/core/collections/test/run-karma.js @@ -0,0 +1,104 @@ +/* global global:true, __dirname, jasmineRequire */ + +/*jshint evil:true */ +// reassigning causes eval to not use lexical scope. +var globalEval = eval, + global = globalEval('this'); +/*jshint evil:false */ + +// Bootsrap Karma +if (global.__karma__) { + + //jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000; + + global.__karma__.loaded = function() { + console.log('karma loaded'); + }; + +// Bootstrap Browser fallback +} else { + + // Init + var jasmine = jasmineRequire.core(jasmineRequire); + var jasmineEnv = jasmine.getEnv(); + + // Export interface + var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); + global.jasmine = jasmine; + for (var property in jasmineInterface) { + if (jasmineInterface.hasOwnProperty(property)) { + global[property] = jasmineInterface[property]; + } + } + + // Default reporter + jasmineEnv.addReporter(jasmineInterface.jsApiReporter); + + // Html reporter + jasmineRequire.html(jasmine); + var htmlReporter = new jasmine.HtmlReporter({ + env: jasmineEnv, + getContainer: function() { return document.body; }, + createElement: function() { return document.createElement.apply(document, arguments); }, + createTextNode: function() { return document.createTextNode.apply(document, arguments); }, + timer: new jasmine.Timer() + }); + htmlReporter.initialize(); + + jasmineEnv.addReporter(htmlReporter); +} + +global.queryString = function queryString(parameter) { + var i, key, value, equalSign; + var loc = location.search.substring(1, location.search.length); + var params = loc.split('&'); + for (i=0; i + + + Montage-Collections - Browser + + + + + + + + + + diff --git a/core/collections/test/spec/array-spec.js b/core/collections/test/spec/array-spec.js new file mode 100644 index 0000000000..83979c9123 --- /dev/null +++ b/core/collections/test/spec/array-spec.js @@ -0,0 +1,341 @@ +require("collections/shim"); +require("collections/listen/array-changes"); +var GenericCollection = require("collections/generic-collection"); +var describeDeque = require("./deque"); +var describeCollection = require("./collection"); +var describeOrder = require("./order"); +var describeMapChanges = require("./listen/map-changes"); + +describe("Array-spec", function () { + describeDeque(Array.from); + describeCollection(Array, [1, 2, 3, 4]); + describeCollection(Array, [{id: 0}, {id: 1}, {id: 2}, {id: 3}]); + describeOrder(Array.from); + + function mapAlike(entries) { + var array = []; + if (entries) { + entries.forEach(function (pair) { + array.set(pair[0], pair[1]); + }); + } + return array; + } + + describeMapChanges(mapAlike); + + /* + The following tests are from Montage. + Copyright (c) 2012, Motorola Mobility LLC. + All Rights Reserved. + BSD License. + */ + + // contains 10, 20, 30 + function FakeArray() { + this.length = 3; + } + Object.addEach(FakeArray.prototype, GenericCollection.prototype); + FakeArray.prototype.reduce = function (callback, basis) { + basis = callback(basis, 10, 0, this); + basis = callback(basis, 20, 1, this); + basis = callback(basis, 30, 2, this); + return basis; + }; + var fakeArray = new FakeArray(); + + // should have been almost completely tested by Object.equals and + // describeOrder + + // get does not work the same way as most other ordered collections. it + // behaves like a map of indicies to values. others behave like sets. + describe("get", function () { + + it("should return the value for a given index", function () { + expect([0].get(0)).toEqual(0); + }); + + it("should not return a named property", function () { + expect(function () { + [].get("length"); + }).toThrow(); + }); + + it("should not return a named index", function () { + expect(function () { + [].get("0"); + }).toThrow(); + }); + + }); + + // Since these are brute force sought, they do not need to be comparable + // for arrays, like they would for a SortedArray. These tests would apply + // to lists as well, but lists do not have indicies. + + describe("find", function () { + + it("should find an object in an array by one of its properties", function () { + var inventory = [ + {name: 'apples', quantity: 2}, + {name: 'bananas', quantity: 0}, + {name: 'cherries', quantity: 5} + ]; + + function isCherries(fruit) { + return fruit.name === 'cherries'; + } + + expect(inventory.find(isCherries)).toEqual(inventory[2]); + }); + + describe("find (deprecated support)", function () { + + it("should find equivalent objects", function () { + expect([{a:10}].find({a:10})).toEqual(0); + }); + + it("should allow equality comparison override", function () { + expect([{a:10}].find({a:10}, Object.is)).toEqual(-1); + }); + }); + + }); + + describe("findValue", function () { + + it("should find equivalent objects", function () { + expect([{a:10}].findValue({a:10})).toEqual(0); + }); + + it("should allow equality comparison override", function () { + expect([{a:10}].findValue({a:10}, Object.is)).toEqual(-1); + }); + }); + + describe("findLast (deprecated support)", function () { + + it("should find equivalent objects", function () { + expect([{a:10}].findLast({a:10})).toEqual(0); + }); + + it("should allow equality comparison override", function () { + expect([{a:10}].findLast({a:10}, Object.is)).toEqual(-1); + }); + + it("should find the last of equivalent objects", function () { + var object = {a: 10}; + expect([object, {a: 10}].findLast(object)).toEqual(1); + }); + + }); + + describe("findLastValue", function () { + + it("should find equivalent objects", function () { + expect([{a:10}].findLastValue({a:10})).toEqual(0); + }); + + it("should allow equality comparison override", function () { + expect([{a:10}].findLastValue({a:10}, Object.is)).toEqual(-1); + }); + + it("should find the last of equivalent objects", function () { + var object = {a: 10}; + expect([object, {a: 10}].findLastValue(object)).toEqual(1); + }); + + }); + + describe("has", function () { + + it("should find equivalent objects", function () { + expect([{a: 10}].has({a: 10})).toBe(true); + }); + + it("should not find non-contained values", function () { + expect([].has(-1)).toBe(false); + }); + + it("should allow equality comparison override", function () { + var object = {}; + expect([{}].has(object, Object.is)).toBe(false); + expect([object].has(object, Object.is)).toBe(true); + }); + + }); + + describe("add", function () { + + it("should add values", function () { + var array = [{a: 10}]; + array.add({a: 10}); + expect(array[0]).toEqual({a: 10}); + expect(array[1]).toEqual({a: 10}); + expect(array.has({a: 10})).toBe(true); + }); + + }); + + describe("sorted", function () { + var a = {foo: [1, 4]}, + b = {foo: [2, 3]}, + c = {foo: [2, 3]}, + d = {foo: [3, 2]}, + e = {foo: [4]}, + unsorted = [d, b, c, a, e], // b and c equal, in stable order + sorted = [a, b, c, d, e], + byFoo = Function.by(function (x) { + return x.foo; + }); + + it("should not be an in-place sort", function () { + expect(unsorted.sorted()).not.toBe(unsorted); + }); + + it("should sort objects by a property array", function () { + expect(unsorted.sorted(byFoo)).toEqual(sorted); + unsorted.sorted(byFoo).forEach(function (x, i) { + expect(x).toBe(sorted[i]); + }); + }); + + }); + + describe("clone", function () { + + // should have been adequately covered by Object.clone tests + + it("should clone with indefinite depth", function () { + var array = [[[]]]; + var clone = array.clone(); + expect(clone).toEqual(array); + expect(clone).not.toBe(array); + }); + + it("should clone with depth 0", function () { + var array = []; + expect(array.clone(0)).toBe(array); + }); + + it("should clone with depth 1", function () { + var array = [{}]; + expect(array.clone(1)).not.toBe(array); + expect(array.clone(1)[0]).toBe(array[0]); + }); + + it("should clone with depth 2", function () { + var array = [{a: 10}]; + expect(array.clone(2)).not.toBe(array); + expect(array.clone(2)[0]).not.toBe(array[0]); + expect(array.clone(2)[0]).toEqual(array[0]); + }); + + }); + + describe("zip", function () { + it("should treat holes as undefined", function () { + var a = [0, 1]; + var b = []; + b[1] = 'b'; + expect(a.zip(b)).toEqual([ + [0], + [1, 'b'] + ]); + }); + }); + + describe("group", function () { + it("should make a histogram", function () { + + var groups = [ + {x: 0}, + {x: 1}, + {x: 2}, + {x: 3} + ].group(function (object) { + return Math.floor(object.x / 2); + }) + expect(groups).toEqual([ + [0, [{x: 0}, {x: 1}]], + [1, [{x: 2}, {x: 3}]] + ]); + + }); + }); + + describe("swap", function () { + var array, otherArray; + beforeEach(function () { + array = [1, 2, 3]; + }); + it("should be able to replace content with content of another arraylike", function () { + otherArray = { __proto__ : Array.prototype }; + otherArray[0] = 4; + otherArray[1] = 5; + otherArray.length = 2; + array.swap(0, array.length, otherArray); + expect(array).toEqual([4, 5]); + }); + it("should ignore non array like plus value", function () { + array.swap(0, array.length, 4); + expect(array).toEqual([]); + + }); + it("should ignore extra arguments", function () { + array.swap(0, array.length, 4, 5, 6); + expect(array).toEqual([]); + + }); + it("should work with large arrays", function () { + otherArray = new Array(200000); + expect(function () { + array.swap(0, array.length, otherArray); + }).not.toThrow(); + expect(array.length).toEqual(200000); + }); + it("swaps at an outer index", function () { + array.swap(4, 0, [5]); + expect(array).toEqual([1, 2, 3, , 5]); + }); + }); + + describe("set", function () { + + it("sets an inner index", function () { + var array = [1, 2, 3]; + array.set(1, 10); + expect(array).toEqual([1, 10, 3]); + }); + + it("sets an inner index of an observed array", function () { + var array = [1, 2, 3]; + array.makeObservable(); + array.set(1, 10); + expect(array).toEqual([1, 10, 3]); + }); + + it("sets an outer index", function () { + var array = []; + array.set(4, 10); + expect(array).toEqual([ , , , , 10]); + }); + + it("sets an outer index of an observed array", function () { + var array = []; + array.makeObservable(); + array.set(4, 10); + expect(array).toEqual([ , , , , 10]); + }); + + }); + + describe("deleteAll", function () { + it("should delete a range of equivalent values", function () { + var array = [1, 1, 1, 2, 2, 2, 3, 3, 3]; + expect(array.deleteAll(2)).toBe(3); + expect(array).toEqual([1, 1, 1, 3, 3, 3]); + }); + }); + +}); diff --git a/core/collections/test/spec/clone-spec.js b/core/collections/test/spec/clone-spec.js new file mode 100644 index 0000000000..8192afdba0 --- /dev/null +++ b/core/collections/test/spec/clone-spec.js @@ -0,0 +1,21 @@ + +var Set = require("collections/set"); +var Map = require("collections/map"); + +describe("Clone-spec", function () { + + it("should deeply clone custom collections", function () { + var a = new Set([new Map([["a",{}]])]); + var b = Object.clone(a); + + // equal maps are not consistently hashed + expect(Object.equals(a, b)).toBe(false); + expect(a.equals(b)).toBe(false); + + expect(a.one()).not.toBe(b.one()); + expect(a.one().equals(b.one())).toBe(true); + expect(a.one().get('a')).not.toBe(b.one().get('a')); + expect(a.one().get('a')).toEqual(b.one().get('a')); + }); + +}); diff --git a/core/collections/test/spec/collection.js b/core/collections/test/spec/collection.js new file mode 100644 index 0000000000..dba9b57648 --- /dev/null +++ b/core/collections/test/spec/collection.js @@ -0,0 +1,124 @@ +// Array, List, Set, FastSet, unbounded LruSet. +// SortedSet does not qualify since these objects are incomparable. +// Array#get() behaves like a Map, not a Set, so it is excluded from those +// tests. + +module.exports = describeCollection; +function describeCollection(Collection, values, setLike) { + + var a = values[0]; + var b = values[1]; + var c = values[2]; + var d = values[3]; + + function shouldHaveTheUsualContent(collection) { + expect(collection.has(a)).toBe(true); + expect(collection.has(b)).toBe(true); + expect(collection.has(c)).toBe(true); + expect(collection.has(d)).toBe(false); + if (setLike) { + expect(collection.get(a)).toBe(a); + expect(collection.get(b)).toBe(b); + expect(collection.get(c)).toBe(c); + expect(collection.get(d)).toBe(undefined); + } + expect(collection.length).toBe(3); + } + + it("should be constructable from an array", function () { + var collection = Collection.from([a, b, c]); + shouldHaveTheUsualContent(collection); + }); + + it("should be constructable from an foreachable", function () { + var collection = Collection.from({ + forEach: function (callback, thisp) { + callback.call(thisp, a); + callback.call(thisp, b); + callback.call(thisp, c); + } + }); + shouldHaveTheUsualContent(collection); + }); + + describe("add", function () { + it("should add values to a collection", function () { + var collection = new Collection(); + // expect(collection.add(a)).toBe(true); + // expect(collection.add(b)).toBe(true); + // expect(collection.add(c)).toBe(true); + collection.add(a); + collection.add(b); + collection.add(c); + shouldHaveTheUsualContent(collection); + }); + }); + + describe("delete", function () { + + it("should remove a value from the beginning of a collection", function () { + var collection = Collection.from([d, a, b, c]); + expect(collection.delete(d)).toBe(true); + shouldHaveTheUsualContent(collection); + }); + + it("should remove a value from the middle of a collection", function () { + var collection = Collection.from([a, d, b, c]); + expect(collection.delete(d)).toBe(true); + shouldHaveTheUsualContent(collection); + }); + + it("should remove a value from the end of a collection", function () { + var collection = Collection.from([a, b, c, d]); + expect(collection.delete(d)).toBe(true); + shouldHaveTheUsualContent(collection); + }); + + it("should fail to remove a value not in a collection", function () { + var collection = Collection.from([a, b, c]); + expect(collection.delete(d)).toBe(false); + shouldHaveTheUsualContent(collection); + }); + + }); + + describe("one", function () { + it("should return a value in the collection", function () { + var collection = Collection.from([a, b, c, d]); + expect(collection.has(collection.one())).toBe(true); + }); + + it("should throw an error for an empty collection", function () { + var collection = new Collection(); + expect(collection.one()).toBe(undefined); + }); + }); + + describe("only", function () { + + it("should return a value in the collection", function () { + var collection = Collection.from([a]); + expect(collection.only()).toBe(a); + }); + + it("should be undefined if there are no values in the collection", function () { + expect(new Collection().only()).toBeUndefined(); + }); + + it("should be undefined if there are many values in the collection", function () { + expect(Collection.from([a, b]).only()).toBeUndefined(); + }); + + }); + + describe("clear", function () { + it("should delete all values", function () { + var collection = Collection.from([a, b, c, d]); + expect(collection.length).toBe(4); + collection.clear(); + expect(collection.toArray()).toEqual([]); + expect(collection.length).toBe(0); + }); + }); + +} diff --git a/core/collections/test/spec/deque-fuzz.js b/core/collections/test/spec/deque-fuzz.js new file mode 100644 index 0000000000..fd5b9954bb --- /dev/null +++ b/core/collections/test/spec/deque-fuzz.js @@ -0,0 +1,92 @@ + +var Deque = require("collections/deque"); +require("collections/shim-array"); +var prng = require("./prng"); + +exports.fuzzDeque = fuzzDeque; +function fuzzDeque(Deque) { + describe('fuzz', function () { + it ("should pass deque fuzz", function () { + for (var biasWeight = .3; biasWeight < .8; biasWeight += .2) { + for (var maxAddLength = 1; maxAddLength < 5; maxAddLength += 3) { + for (var seed = 0; seed < 10; seed++) { + var plan = makePlan(100, seed, biasWeight, maxAddLength); + execute(Deque, plan.ops); + } + } + } + }); + }); +} + +exports.makePlan = makePlan; +function makePlan(length, seed, biasWeight, maxAddLength) { + maxAddLength = maxAddLength || 1; + var random = prng(seed); + var ops = []; + while (ops.length < length) { + var bias = ops.length / length; + var choice1 = random() * (1 - biasWeight) + bias * biasWeight; + var choice2 = random(); + if (choice1 < 1 / (maxAddLength + 1)) { + if (choice2 < .5) { + ops.push(["push", makeRandomArray(1 + ~~(random() * maxAddLength - .5))]); + } else { + ops.push(["unshift", makeRandomArray(1 + ~~(random() * maxAddLength - .5))]); + } + } else { + if (choice2 < .5) { + ops.push(["shift", []]); + } else { + ops.push(["pop", []]); + } + } + } + return { + seed: seed, + length: length, + biasWeight: biasWeight, + maxAddLength: maxAddLength, + ops: ops + } +} + +function makeRandomArray(length, random) { + var array = []; + for (var index = 0; index < length; index++) { + array.push(~~(Math.random() * 100)); + } + return array; +} + +exports.execute = execute; +function execute(Collection, ops) { + var oracle = []; + var actual = new Collection(); + ops.forEach(function (op) { + executeOp(oracle, op); + executeOp(actual, op); + if (typeof expect === "function") { + expect(actual.toArray()).toEqual(oracle); + } else if (!actual.toArray().equals(oracle)) { + console.log(actual.front, actual.toArray(), oracle); + throw new Error("Did not match after " + stringifyOp(op)); + } + }); +} + +exports.executeOp = executeOp; +function executeOp(collection, op) { + collection[op[0]].apply(collection, op[1]); +} + +exports.stringify = stringify; +function stringify(ops) { + return ops.map(stringifyOp).join(" "); +} + +exports.stringifyOp = stringifyOp; +function stringifyOp(op) { + return op[0] + "(" + op[1].join(", ") + ")"; +} + diff --git a/core/collections/test/spec/deque-spec.js b/core/collections/test/spec/deque-spec.js new file mode 100644 index 0000000000..c122b563b8 --- /dev/null +++ b/core/collections/test/spec/deque-spec.js @@ -0,0 +1,123 @@ + +var Deque = require("collections/deque"); +var describeDeque = require("./deque"); +var describeOrder = require("./order"); +var describeToJson = require("./to-json"); + +describe("Deque-spec", function () { + + it("just the facts", function () { + var deque = new Deque(); + expect(deque.length).toBe(0); + expect(deque.capacity).toBe(16); + + deque.push(10); + expect(deque.length).toBe(1); + expect(deque.shift()).toBe(10); + expect(deque.length).toBe(0); + + deque.push(20); + expect(deque.length).toBe(1); + deque.push(30); + expect(deque.length).toBe(2); + expect(deque.shift()).toBe(20); + expect(deque.length).toBe(1); + expect(deque.shift()).toBe(30); + expect(deque.length).toBe(0); + + expect(deque.capacity).toBe(16); + + }); + + it("grows", function () { + var deque = Deque(); + + for (var i = 0; i < 16; i++) { + expect(deque.length).toBe(i); + deque.push(i); + expect(deque.capacity).toBe(16); + } + deque.push(i); + expect(deque.capacity).toBe(64); + }); + + it("initializes", function () { + var deque = new Deque([1, 2, 3]); + expect(deque.length).toBe(3); + expect(deque.shift()).toBe(1); + expect(deque.shift()).toBe(2); + expect(deque.shift()).toBe(3); + }); + + it("does not get in a funk", function () { + var deque = Deque(); + expect(deque.shift()).toBe(undefined); + deque.push(4); + expect(deque.shift()).toBe(4); + }); + + it("dispatches range changes", function () { + var spy = jasmine.createSpy(); + var handler = function (plus, minus, value) { + spy(plus, minus, value); // ignore last arg + }; + var deque = Deque(); + deque.addRangeChangeListener(handler); + deque.push(1); + deque.push(2, 3); + deque.pop(); + deque.shift(); + deque.unshift(4, 5); + deque.removeRangeChangeListener(handler); + deque.shift(); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + [[1], [], 0], + [[2, 3], [], 1], + [[], [3], 2], + [[], [1], 0], + [[4, 5], [], 0] + ]); + }); + + // from https://github.com/petkaantonov/deque + + describe('get', function () { + it("should return undefined on nonsensical argument", function() { + var a = new Deque([1,2,3,4]); + expect(a.get(-5)).toBe(void 0); + expect(a.get(-100)).toBe(void 0); + expect(a.get(void 0)).toBe(void 0); + expect(a.get("1")).toBe(void 0); + expect(a.get(NaN)).toBe(void 0); + expect(a.get(Infinity)).toBe(void 0); + expect(a.get(-Infinity)).toBe(void 0); + expect(a.get(1.5)).toBe(void 0); + expect(a.get(4)).toBe(void 0); + }); + + + it("should support positive indexing", function() { + var a = new Deque([1,2,3,4]); + expect(a.get(0)).toBe(1); + expect(a.get(1)).toBe(2); + expect(a.get(2)).toBe(3); + expect(a.get(3)).toBe(4); + }); + + it("should support negative indexing", function() { + var a = new Deque([1,2,3,4]); + expect(a.get(-1)).toBe(4); + expect(a.get(-2)).toBe(3); + expect(a.get(-3)).toBe(2); + expect(a.get(-4)).toBe(1); + }); + }); + + describeDeque(Deque); + describeOrder(Deque); + describeToJson(Deque, [1, 2, 3, 4]); + +}); + diff --git a/core/collections/test/spec/deque.js b/core/collections/test/spec/deque.js new file mode 100644 index 0000000000..89c978c5d0 --- /dev/null +++ b/core/collections/test/spec/deque.js @@ -0,0 +1,340 @@ +// Describe Array, List, and SortedSet, all of which have the interface of a +// double-ended queue. Array and List are proper queues since push and unshift +// put the values at the ends, but for sake of reusing these tests for +// SortedSet, all of these tests maintain the sorted order of the collection. + +var fuzzDeque = require("./deque-fuzz").fuzzDeque; + +module.exports = describeDeque; +function describeDeque(Deque) { + + describe("add(value)", function () { + it("should be an alias for push", function () { + var collection = Deque([1, 2, 3]); + collection.add(4); + expect(collection.toArray()).toEqual([1, 2, 3, 4]); + }); + }); + + describe("push(value)", function () { + it("should add one value to the end", function () { + var collection = Deque([1, 2, 3]); + collection.push(4); + expect(collection.toArray()).toEqual([1, 2, 3, 4]); + }); + }); + + describe("push(...values)", function () { + it("should add many values to the end", function () { + var collection = Deque([1, 2, 3]); + collection.push(4, 5, 6); + expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it("should add many values to the end variadically", function () { + var collection = Deque([1, 2, 3]); + collection.push.apply(collection, [4, 5, 6]); + expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6]); + }); + }); + + describe("unshift(value)", function () { + it("should add a value to the beginning", function () { + var collection = Deque([1, 2, 3]); + collection.unshift(0); + expect(collection.toArray()).toEqual([0, 1, 2, 3]); + }); + }); + + describe("unshift(...values)", function () { + it("should add many values to the beginning", function () { + var collection = Deque([1, 2, 3]); + collection.unshift(-2, -1, 0); + expect(collection.toArray()).toEqual([-2, -1, 0, 1, 2, 3]); + }); + + it("should add many values to the beginning", function () { + var collection = Deque([1, 2, 3]); + collection.unshift.apply(collection, [-2, -1, 0]); + expect(collection.toArray()).toEqual([-2, -1, 0, 1, 2, 3]); + }); + }); + + describe("pop", function () { + it("should remove one value from the end and return it", function () { + var collection = Deque([1, 2, 3]); + expect(collection.pop()).toEqual(3); + expect(collection.toArray()).toEqual([1, 2]); + }); + }); + + describe("shift", function () { + it("should remove one value from the beginning and return it", function () { + var collection = Deque([1, 2, 3]); + expect(collection.shift()).toEqual(1); + expect(collection.toArray()).toEqual([2, 3]); + }); + }); + + describe("concat", function () { + it("should concatenate variadic mixed-type collections", function () { + var collection = Deque([1, 2, 3]).concat( + [4, 5, 6], + Deque([7, 8, 9]) + ); + expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); + }); + }); + + describe("slice", function () { + if (!Deque.prototype.slice) + return; + + var collection = Deque([1, 2, 3, 4]); + + it("should slice all values with no arguments", function () { + expect(collection.slice()).toEqual([1, 2, 3, 4]); + }); + + it("should slice all after an index", function () { + expect(collection.slice(2)).toEqual([3, 4]); + }); + + it("should slice from the middle by indexed positions", function () { + expect(collection.slice(1, 3)).toEqual([2, 3]); + }); + + it("should slice from a negative index", function () { + expect(collection.slice(-2)).toEqual([3, 4]); + }); + + it("should slice from a negative index to a positive", function () { + expect(collection.slice(-2, 3)).toEqual([3]); + }); + + it("should slice from a negative index to a negative", function () { + expect(collection.slice(-2, -1)).toEqual([3]); + }); + + // TODO + /* + it("should slice from a negative index to zero", function () { + expect(collection.slice(-2, 0)).toEqual([]); // Array + expect(collection.slice(-2, 0)).toEqual([3, 4]); // List + }); + */ + + }); + + describe("splice", function () { + if (!Deque.prototype.splice) + return; + + it("should do nothing with no arguments", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.splice()).toEqual([]); + expect(collection.toArray()).toEqual([1, 2, 3, 4]); + }); + + it("should splice to end with only an offset argument", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.splice(2)).toEqual([3, 4]); + expect(collection.toArray()).toEqual([1, 2]); + }); + + it("should splice nothing with no length", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.splice(2, 0)).toEqual([]); + expect(collection.toArray()).toEqual([1, 2, 3, 4]); + }); + + it("should splice all values", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.splice(0, collection.length)).toEqual([1, 2, 3, 4]); + expect(collection.toArray()).toEqual([]); + }); + + it("should splice from negative offset", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.splice(-2)).toEqual([3, 4]); + expect(collection.toArray()).toEqual([1, 2]); + }); + + it("should inject values at a numeric offset", function () { + var collection = Deque([1, 2, 5, 6]); + expect(collection.splice(2, 0, 3, 4)).toEqual([]); + expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it("should replace values at a numeric offset", function () { + var collection = Deque([1, 2, 3, 6]); + expect(collection.splice(1, 2, 4, 5)).toEqual([2, 3]); + expect(collection.toArray()).toEqual([1, 4, 5, 6]); + }); + + it("should inject values with implied position and length", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.splice(null, null, -1, 0)).toEqual([]); + expect(collection.toArray()).toEqual([-1, 0, 1, 2, 3, 4]); + }); + + it("should append values", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.splice(4, 0, 5, 6)).toEqual([]); + expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6]); + }); + + }); + + describe("swap", function () { + if (!Deque.prototype.swap) + return; + + it("should do nothing with no arguments", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.swap()).toEqual([]); + expect(collection.toArray()).toEqual([1, 2, 3, 4]); + }); + + it("should splice to end with only an offset argument", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.swap(2)).toEqual([3, 4]); + expect(collection.toArray()).toEqual([1, 2]); + }); + + it("should splice nothing with no length", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.swap(2, 0)).toEqual([]); + expect(collection.toArray()).toEqual([1, 2, 3, 4]); + }); + + it("should splice all values", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.swap(0, collection.length)).toEqual([1, 2, 3, 4]); + expect(collection.toArray()).toEqual([]); + }); + + it("should splice from negative offset", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.swap(-2)).toEqual([3, 4]); + expect(collection.toArray()).toEqual([1, 2]); + }); + + it("should inject values at a numeric offset", function () { + var collection = Deque([1, 2, 5, 6]); + expect(collection.swap(2, 0, [3, 4])).toEqual([]); + expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it("should replace values at a numeric offset", function () { + var collection = Deque([1, 2, 3, 6]); + expect(collection.swap(1, 2, [4, 5])).toEqual([2, 3]); + expect(collection.toArray()).toEqual([1, 4, 5, 6]); + }); + + it("should inject values with implied position and length", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.swap(null, null, [-1, 0])).toEqual([]); + expect(collection.toArray()).toEqual([-1, 0, 1, 2, 3, 4]); + }); + + it("should append values", function () { + var collection = Deque([1, 2, 3, 4]); + expect(collection.swap(4, 0, [5, 6])).toEqual([]); + expect(collection.toArray()).toEqual([1, 2, 3, 4, 5, 6]); + }); + }); + + + if (!Deque.prototype.isSorted) { + fuzzDeque(Deque); + } + + // TODO peekBack + // TODO pokeBack + + // from https://github.com/petkaantonov/deque + + describe("peek", function () { + + + if (Deque.prototype.poke && Deque.prototype.peek) { + it("peek and poke", function () { + var deque = Deque([1, 2, 3, 4, 5, 6, 7, 8]); + expect(deque.peek()).toBe(1); + expect(deque.poke(2)).toBe(undefined); + expect(deque.shift()).toBe(2); + expect(deque.peek()).toBe(2); + }); + } + + if (Deque.prototype.pokeBack && Deque.prototype.peekBack) { + it("peekBack and pokeBack", function () { + var deque = Deque([1, 2, 3, 4, 5, 6, 7, 8]); + expect(deque.peekBack()).toBe(8); + expect(deque.pokeBack(9)).toBe(undefined); + expect(deque.pop()).toBe(9); + expect(deque.peekBack()).toBe(7); + }); + } + + if (!Deque.prototype.peek) + return; + + it("returns undefined when empty deque", function() { + var a = new Deque(); + expect(a.length).toBe(0); + expect(a.peek()).toBe(undefined); + expect(a.peek()).toBe(undefined); + expect(a.length).toBe(0); + }); + + it("returns the item at the front of the deque", function() { + var a = new Deque(); + a.push(1,2,3,4,5,6,7,8,9); + + expect(a.peek()).toBe(1); + + var l = 5; + while(l--) a.pop(); + + expect(a.toArray()).toEqual([1, 2, 3, 4]); + + expect(a.peek()).toBe(1); + + var l = 2; + while (l--) a.shift(); + + expect(a.peek()).toBe(3); + + expect(a.toArray()).toEqual([3, 4]); + + a.unshift(1,2,3,4,5,6,78,89,12901,10121,0,12, 1,2,3,4,5,6,78,89,12901,10121,0,12); + + expect(a.toArray()).toEqual([1,2,3,4,5,6,78,89,12901,10121,0,12, 1,2,3,4,5,6,78,89,12901,10121,0,12, 3, 4]); + + expect(a.peek()).toBe(1); + + a.push(1,3,4); + + expect(a.peek()).toBe(1); + + a.pop(); + a.shift(); + + expect(a.peek()).toBe(2); + expect(a.toArray()).toEqual([2,3,4,5,6,78,89,12901,10121,0,12, 1,2,3,4,5,6,78,89,12901,10121,0,12, 3, 4, 1, 3]); + + }); + }); + + describe("clear", function () { + it("should clear the deque", function() { + var a = new Deque([1,2,3,4]); + a.clear(); + expect(a.length).toBe(0); + }); + }); + +} + diff --git a/core/collections/test/spec/dict-spec.js b/core/collections/test/spec/dict-spec.js new file mode 100644 index 0000000000..48a2968e62 --- /dev/null +++ b/core/collections/test/spec/dict-spec.js @@ -0,0 +1,27 @@ + +var Dict = require("collections/dict"); +var describeDict = require("./dict"); +var describeToJson = require("./to-json"); + +describe("Dict-spec", function () { + describeDict(Dict); + describeToJson(Dict, {a: 1, b: 2, c: 3}); + + it("should throw errors for non-string keys", function () { + var dict = Dict(); + expect(function () { + dict.get(0); + }).toThrow(); + expect(function () { + dict.set(0, 10); + }).toThrow(); + expect(function () { + dict.has(0); + }).toThrow(); + expect(function () { + dict.delete(0); + }).toThrow(); + }); + +}); + diff --git a/core/collections/test/spec/dict.js b/core/collections/test/spec/dict.js new file mode 100644 index 0000000000..17f1f3ab14 --- /dev/null +++ b/core/collections/test/spec/dict.js @@ -0,0 +1,86 @@ +// tests that are equally applicable to Dict, Map, SortedMap, unbounded LruMap, FastMap + +module.exports = describeDict; +function describeDict(Dict) { + + it("should be constructable from entry duples", function () { + var dict = new Dict([['a', 10], ['b', 20]]); + shouldHaveTheUsualContent(dict); + }); + + it("should be constructable from objects", function () { + var dict = Dict.from({a: 10, b: 20}); + shouldHaveTheUsualContent(dict); + }); + + it("should be constructable from dicts", function () { + var dict = new Dict(Dict.from({a: 10, b: 20})); + shouldHaveTheUsualContent(dict); + }); + + describe("delete", function () { + it("should be able to delete keys", function () { + var dict = Dict.from({a: 10, b: 20, c: 30}); + expect(dict.delete('c')).toBe(true); + expect(dict.delete('c')).toBe(false); + shouldHaveTheUsualContent(dict); + }); + }); + + it("should be able to contain hasOwnProperty", function () { + var dict = new Dict(); + dict.set("hasOwnProperty", 10); + expect(dict.get("hasOwnProperty")).toBe(10); + expect(dict.delete("hasOwnProperty")).toBe(true); + expect(dict.length).toBe(0); + expect(dict.delete("hasOwnProperty")).toBe(false); + }); + + it("should be able to contain __proto__", function () { + var dict = new Dict(); + dict.set("__proto__", 10); + expect(dict.get("__proto__")).toBe(10); + expect(dict.delete("__proto__")).toBe(true); + expect(dict.length).toBe(0); + expect(dict.delete("__proto__")).toBe(false); + }); + + it("should send a value for MapChange events", function () { + var dict = Dict.from({a: 1}); + + var listener = function(value, key) { + expect(value).toBe(2); + }; + dict.addMapChangeListener(listener); + dict.set('a', 2); + }) +} + +function shouldHaveTheUsualContent(dict) { + expect(dict.has('a')).toBe(true); + expect(dict.has('b')).toBe(true); + expect(dict.has('c')).toBe(false); + expect(dict.has('__proto__')).toBe(false); + expect(dict.has('hasOwnProperty')).toBe(false); + + expect(dict.get('a')).toBe(10); + expect(dict.get('b')).toBe(20); + expect(dict.get('c')).toBe(undefined); + + var mapIter = dict.keys(), key, keys = []; + while (key = mapIter.next().value) { + keys.push(key); + } + expect(dict.keysArray()).toEqual(['a', 'b']); + + expect(dict.valuesArray()).toEqual([10, 20]); + expect(dict.entriesArray()).toEqual([['a', 10], ['b', 20]]); + expect(dict.reduce(function (basis, value, key) { + return basis + value; + }, 0)).toEqual(30); + expect(dict.reduce(function (basis, value, key) { + basis.push(key); + return basis; + }, [])).toEqual(['a', 'b']); + expect(dict.length).toBe(2); +} diff --git a/core/collections/test/spec/fast-map-spec.js b/core/collections/test/spec/fast-map-spec.js new file mode 100644 index 0000000000..ae781c1bec --- /dev/null +++ b/core/collections/test/spec/fast-map-spec.js @@ -0,0 +1,12 @@ + +var FastMap = require("collections/fast-map"); +var describeDict = require("./dict"); +var describeMap = require("./map"); +var describeToJson = require("./to-json"); + +describe("FastMap-spec", function () { + describeDict(FastMap); + describeMap(FastMap); + describeToJson(FastMap, [[{a: 1}, 10], [{b: 2}, 20], [{c: 3}, 30]]); +}); + diff --git a/core/collections/test/spec/fast-set-spec.js b/core/collections/test/spec/fast-set-spec.js new file mode 100644 index 0000000000..e0e00c1084 --- /dev/null +++ b/core/collections/test/spec/fast-set-spec.js @@ -0,0 +1,172 @@ +"use strict"; + +var Set = require("collections/fast-set"); +var Iterator = require("collections/iterator"); +var TreeLog = require("collections/tree-log"); + +var describeCollection = require("./collection"); +var describeSet = require("./set"); +var describeToJson = require("./to-json"); + +describe("Set-spec", function () { + // new Set() + // Set() + // Set(values) + // Set(null, equals, hash) + // Set(null, null, null, content) + // Set().has(value) + // Set().get(value) + // Set().delete(value) + // Set().clear() + // Set().add(value) + // Set().reduce(callback, basis, thisp) + // Set().forEach() + // Set().map() + // Set().toArray() + // Set().filter() + // Set().every() + // Set().some() + // Set().all() + // Set().any() + // Set().min() + // Set().max() + + describeCollection(Set, [1, 2, 3, 4], true); + describeCollection(Set, [{id: 0}, {id: 1}, {id: 2}, {id: 3}], true); + describeSet(Set); + describeToJson(Set, [1, 2, 3, 4]); + + it("can use hash delegate methods", function () { + function Item(key, value) { + this.key = key; + this.value = value; + } + + Item.prototype.hash = function () { + return '' + this.key; + }; + + var set = new Set(); + set.add(new Item(1, 'a')); + set.add(new Item(3, 'b')); + set.add(new Item(2, 'c')); + set.add(new Item(2, 'd')); + + expect(set.buckets.keysArray().sort()).toEqual(['1', '2', '3']); + + }); + + it("can iterate with forEach", function () { + var values = [false, null, undefined, 0, 1, {}]; + var set = new Set(values); + set.forEach(function (value) { + var index = values.indexOf(value); + values.splice(index, 1); + }); + expect(values.length).toBe(0); + }); + + it("can iterate with an iterator", function () { + var set = new Set([1, 2, 3, 4, 5, 6]); + var iterator = new Iterator(set); + var array = iterator.toArray(); + expect(array).toEqual(set.toArray()); + }); + + it("should log", function () { + var set = new Set([1, 2, 3]); + var lines = []; + set.log(TreeLog.ascii, null, lines.push, lines); + expect(lines).toEqual([ + "+-+ 1", + "| '-- 1", + "+-+ 2", + "| '-- 2", + "'-+ 3", + " '-- 3" + ]); + }); + + it("should log objects by hash", function () { + function Type(value) { + this.value = value; + } + Type.prototype.hash = function () { + return this.value; + }; + var set = new Set([ + new Type(1), + new Type(1), + new Type(2), + new Type(2) + ]); + var lines = []; + set.log(TreeLog.ascii, function (node, write) { + write(" " + JSON.stringify(node.value)); + }, lines.push, lines); + expect(lines).toEqual([ + "+-+ 1", + "| +-- {\"value\":1}", + "| '-- {\"value\":1}", + "'-+ 2", + " +-- {\"value\":2}", + " '-- {\"value\":2}" + ]); + }); + + it("should log objects by only one hash", function () { + function Type(value) { + this.value = value; + } + Type.prototype.hash = function () { + return this.value; + }; + var set = new Set([ + new Type(1), + new Type(1) + ]); + var lines = []; + set.log(TreeLog.ascii, null, lines.push, lines); + expect(lines).toEqual([ + "'-+ 1", + " +-- {", + " | \"value\": 1", + " | }", + " '-- {", + " \"value\": 1", + " }" + ]); + }); + + describe("should log objects with a custom writer with multiple lines", function () { + function Type(value) { + this.value = value; + } + Type.prototype.hash = function () { + return this.value; + }; + var set = new Set([ + new Type(1), + new Type(1) + ]); + var lines = []; + set.log(TreeLog.ascii, function (node, below, above) { + above(" . "); + below("-+ " + node.value.value); + below(" ' "); + }, lines.push, lines); + [ + "'-+ 1", + " | . ", + " +---+ 1", + " | ' ", + " | . ", + " '---+ 1", + " ' " + ].forEach(function (line, index) { + it("line " + index, function () { + expect(lines[index]).toEqual(line); + }); + }); + }); +}); diff --git a/core/collections/test/spec/fuzz.js b/core/collections/test/spec/fuzz.js new file mode 100644 index 0000000000..eccbc9ee2f --- /dev/null +++ b/core/collections/test/spec/fuzz.js @@ -0,0 +1,79 @@ + +// TODO rename set-fuzz + +var makeRandom = require("./prng"); +exports.makeRandom = makeRandom; + +exports.make = makeFuzz; +function makeFuzz(length, seed, max) { + var random = makeRandom(seed); + var operations = []; + var content = []; + var previous; + var operation; + while (operations.length < length) { + content.sort(function () { + return random() - .5; + }); + var choice = random(); + if (previous !== "delete" && content.length && choice > 2/3) { + operation = {type: 'delete', value: content.shift()}; + } else if (previous !== "get" && content.length && choice > 1/3) { + operation = {type: 'get', value: content[0]}; + } else if (previous !== "add") { + var value = Math.floor(random() * max); + content.push(value); + operation = {type: 'add', value: value}; + } + operations.push(operation); + previous = operation.type; + } + return operations; +} + +exports.stringify = stringifyFuzz; +function stringifyFuzz(operations) { + return operations.map(function (operation) { + if (operation.type === "add") { + return "+" + operation.value; + } else if (operation.type === "delete") { + return "-" + operation.value; + } else if (operation.type === "get") { + return "" + operation.value; + } + }).join(", "); +} + +exports.parse = parseFuzz; +function parseFuzz(fuzz) { + return fuzz.split(", ").map(function (fuzz) { + if (fuzz[0] === "+") { + return {type: "add", value: +fuzz}; + } else if (fuzz[0] === "-") { + return {type: "delete", value: -fuzz}; + } else { + return {type: "get", value: +fuzz}; + } + }); +} + +exports.execute = executeFuzz; +function executeFuzz(set, operations, log) { + operations.forEach(function (operation) { + if (operation.type === "add") { + set.add(operation.value); + } else if (operation.type === "get") { + set.get(operation.value); + } else if (operation.type === "delete") { + set.delete(operation.value); + } + if (log) { + console.log(); + console.log(operation); + set.log(null, function (node, write) { + write(" " + node.value + " length=" + node.length); + }); + } + }); +} + diff --git a/core/collections/test/spec/heap-spec.js b/core/collections/test/spec/heap-spec.js new file mode 100644 index 0000000000..85f762cfdb --- /dev/null +++ b/core/collections/test/spec/heap-spec.js @@ -0,0 +1,107 @@ + +var Heap = require("collections/heap"); +var permute = require("./permute"); +var describeToJson = require("./to-json"); + +describe("Heap-spec", function () { + + describeToJson(Heap, [4, 3, 2, 1]); + + describe("always tracks the max value", function () { + + var commonNumbers = [1, 2, 3, 4, 5]; + permute(commonNumbers).forEach(function (numbers) { + it(JSON.stringify(numbers), function () { + + var heap = Heap(numbers); + var maxes = commonNumbers.slice(); + + while (maxes.length > 0) { + var max = maxes.pop(); + var top = heap.pop(); + expect(top).toEqual(max); + expect(heap.length).toBe(maxes.length); + } + + expect(heap.length).toBe(0); + + }); + }); + + it("[5, 4, 3, 2, 1]", function () { + var stack = [5, 4, 3, 2, 1]; + var heap = Heap(stack); + expect(heap.content).toEqual([5, 4, 3, 2, 1]); + expect(heap.length).toBe(5); + expect(heap.pop()).toBe(5); + expect(heap.content).toEqual([4, 2, 3, 1]); + expect(heap.length).toBe(4); + expect(heap.pop()).toBe(4); + expect(heap.content).toEqual([3, 2, 1]); + expect(heap.length).toBe(3); + expect(heap.pop()).toBe(3); + expect(heap.content).toEqual([2, 1]); + expect(heap.length).toBe(2); + expect(heap.pop()).toBe(2); + expect(heap.content).toEqual([1]); + expect(heap.length).toBe(1); + expect(heap.pop()).toBe(1); + expect(heap.content).toEqual([]); + expect(heap.length).toBe(0); + }); + + }); + + it("should be observable", function () { + + var heap = new Heap([1,2,3,4,5]); + var top; + heap.addMapChangeListener(function (value, key) { + if (key === 0) { + top = value; + } + }); + + heap.push(7); + expect(top).toBe(7); + + heap.pop(); + expect(top).toBe(5); + + heap.pop(); + expect(top).toBe(4); + + heap.pop(); + expect(top).toBe(3); + + }); + + it("should delete properly", function () { + + var heap = new Heap([1, 2, 3, 4, 5, 6]); + expect(heap.length).toEqual(6); + heap.delete(3); + expect(heap.sorted()).toEqual([1, 2, 4, 5, 6]); + expect(heap.length).toEqual(5); + heap.delete(6); + expect(heap.sorted()).toEqual([1, 2, 4, 5]); + expect(heap.length).toEqual(4); + heap.delete(1); + expect(heap.sorted()).toEqual([2, 4, 5]); + expect(heap.length).toEqual(3); + heap.delete(4); + expect(heap.sorted()).toEqual([2, 5]); + expect(heap.length).toEqual(2); + heap.delete(2); + expect(heap.sorted()).toEqual([5]); + expect(heap.length).toEqual(1); + heap.delete(5); + expect(heap.sorted()).toEqual([]); + expect(heap.length).toEqual(0); + expect(heap.delete(null)).toBe(false); + expect(heap.sorted()).toEqual([]); + expect(heap.length).toEqual(0); + + }); + +}); diff --git a/core/collections/test/spec/iterator-spec.js b/core/collections/test/spec/iterator-spec.js new file mode 100644 index 0000000000..9ae34c9ee7 --- /dev/null +++ b/core/collections/test/spec/iterator-spec.js @@ -0,0 +1,649 @@ + +var Iterator = require("collections/iterator"); + +describe("Iterator-spec", function () { + + shouldWorkWithConstructor(function withoutNew(iterable) { + return Iterator(iterable); + }); + + shouldWorkWithConstructor(function withNew(iterable) { + return new Iterator(iterable); + }); + + describe("Iterator.cycle", function () { + + it("should work", function () { + var iterator = Iterator.cycle([1, 2, 3]); + for (var i = 0; i < 10; i++) { + expect(iterator.next().value).toBe(1); + expect(iterator.next().value).toBe(2); + expect(iterator.next().value).toBe(3); + } + }); + + it("should work with specified number of times", function () { + var iterator = Iterator.cycle([1, 2, 3], 2); + for (var i = 0; i < 2; i++) { + expect(iterator.next().value).toBe(1); + expect(iterator.next().value).toBe(2); + expect(iterator.next().value).toBe(3); + } + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + + it("should work with specified 0 times", function () { + var iterator = Iterator.cycle([1, 2, 3], 0); + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + + it("should work with specified -1 times", function () { + var iterator = Iterator.cycle([1, 2, 3], 0); + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + + }); + + describe("Iterator.repeat", function () { + + it("should repeat a value indefinite times by default", function () { + var iterator = Iterator.repeat(1); + for (var i = 0; i < 10; i++) { + expect(iterator.next().value).toEqual(1); + } + }); + + it("should repeat a value specified times", function () { + var iterator = Iterator.repeat(1, 3); + for (var i = 0; i < 3; i++) { + expect(iterator.next().value).toEqual(1); + } + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + + }); + + describe("Iterator.concat", function () { + it("should work", function () { + var iterator = Iterator.concat([ + Iterator([1, 2, 3]), + Iterator([4, 5, 6]), + Iterator([7, 8, 9]) + ]); + for (var i = 0; i < 9; i++) { + expect(iterator.next().value).toEqual(i + 1); + } + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + }); + + describe("Iterator.chain", function () { + it("should work", function () { + var iterator = Iterator.chain( + Iterator([1, 2, 3]), + Iterator([4, 5, 6]), + Iterator([7, 8, 9]) + ); + for (var i = 0; i < 9; i++) { + expect(iterator.next().value).toEqual(i + 1); + } + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + }); + + describe("Iterator.unzip", function () { + it("should work", function () { + var iterator = Iterator.unzip([ + Iterator([0, 'A', 'x']), + Iterator([1, 'B', 'y', 'I']), + Iterator([2, 'C']) + ]); + + expect(iterator.next().value).toEqual([0, 1, 2]); + expect(iterator.next().value).toEqual(['A', 'B', 'C']); + + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + }); + + describe("Iterator.zip", function () { + it("should work", function () { + var iterator = Iterator.zip( + Iterator([0, 'A', 'x']), + Iterator([1, 'B', 'y', 'I']), + Iterator([2, 'C']) + ); + + expect(iterator.next().value).toEqual([0, 1, 2]); + expect(iterator.next().value).toEqual(['A', 'B', 'C']); + + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + }); + + describe("Iterator.range", function () { + }); + + describe("Iterator.count", function () { + }); + +}); + +function shouldWorkWithConstructor(Iterator) { + + function definiteIterator() { + return Iterator([1, 2, 3]); + } + + function indefiniteIterator() { + var n = 0; + return Iterator(function () { + return { + done: false, + value: n++ + }; + }); + } + + it("should iterate an array", function () { + var iterator = Iterator([1, 2, 3]); + expect(iterator.next().value).toEqual(1); + expect(iterator.next().value).toEqual(2); + expect(iterator.next().value).toEqual(3); + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + + it("should iterate an sparse array", function () { + var array = []; + array[0] = 1; + array[100] = 2; + array[1000] = 3; + var iterator = Iterator(array); + expect(iterator.next().value).toEqual(1); + expect(iterator.next().value).toEqual(2); + expect(iterator.next().value).toEqual(3); + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + + it("should iterate a string", function () { + var iterator = Iterator("abc"); + expect(iterator.next().value).toEqual("a"); + expect(iterator.next().value).toEqual("b"); + expect(iterator.next().value).toEqual("c"); + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + + it("should gracefully fail to iterate null", function () { + expect(function () { + Iterator(null); + }).toThrow(); + }); + + it("should gracefully fail to iterate undefined", function () { + expect(function () { + Iterator(); + }).toThrow(); + }); + + it("should gracefully fail to iterate a number", function () { + expect(function () { + Iterator(42); + }).toThrow(); + }); + + it("should gracefully pass an existing iterator through", function () { + var iterator = Iterator([1, 2, 3]); + iterator = Iterator(iterator); + expect(iterator.next().value).toEqual(1); + expect(iterator.next().value).toEqual(2); + expect(iterator.next().value).toEqual(3); + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + + it("should iterate an iterator", function () { + var iterator = Iterator({ + iterate: function () { + return Iterator([1, 2, 3]); + } + }); + iterator = Iterator(iterator); + expect(iterator.next().value).toEqual(1); + expect(iterator.next().value).toEqual(2); + expect(iterator.next().value).toEqual(3); + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + + it("should iterate an iterable", function () { + var n = 0; + var iterator = Iterator({ + next: function next() { + if (++n > 3) { + return {value:void 0,done:true}; + } else { + return {value:n,done:false}; + } + } + }); + expect(iterator.next().value).toEqual(1); + expect(iterator.next().value).toEqual(2); + expect(iterator.next().value).toEqual(3); + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + + it("should create an iterator from a function", function () { + var n = 0; + var iterator = Iterator(function next() { + if (++n > 3) { + return {value:void 0,done:true}; + } else { + return {value:n,done:false}; + } + }); + expect(iterator.next().value).toEqual(1); + expect(iterator.next().value).toEqual(2); + expect(iterator.next().value).toEqual(3); + expect(iterator.next().done).toBe(true); + expect(iterator.next().done).toBe(true); + }); + + describe("reduce", function () { + it("should work", function () { + var iterator = definiteIterator(); + var count = 0; + var result = iterator.reduce(function (result, value, key, object) { + expect(value).toBe(count + 1); + expect(key).toBe(count); + expect(object).toBe(iterator); + count++; + return value + 1; + }, 0); + expect(result).toBe(4); + }); + }); + + describe("forEach", function () { + it("should work", function () { + var iterator = definiteIterator(); + var count = 0; + iterator.forEach(function (value, key, object) { + expect(value).toBe(count + 1); + expect(key).toBe(count); + expect(object).toBe(iterator); + count++; + }); + expect(count).toBe(3); + }); + }); + + describe("map", function () { + it("should work", function () { + var iterator = definiteIterator(); + var count = 0; + var result = iterator.map(function (value, key, object) { + expect(value).toBe(count + 1); + expect(key).toBe(count); + expect(object).toBe(iterator); + count++; + return "abc".charAt(key); + }); + expect(result).toEqual(["a", "b", "c"]); + expect(count).toBe(3); + }); + }); + + describe("filter", function () { + it("should work", function () { + var iterator = definiteIterator(); + var count = 0; + var result = iterator.filter(function (value, key, object) { + expect(value).toBe(count + 1); + expect(key).toBe(count); + expect(object).toBe(iterator); + count++; + return value === 2; + }); + expect(result).toEqual([2]); + expect(count).toBe(3); + }); + }); + + describe("every", function () { + it("should work", function () { + expect(Iterator([1, 2, 3]).every(function (n) { + return n < 10; + })).toBe(true); + expect(Iterator([1, 2, 3]).every(function (n) { + return n > 1; + })).toBe(false); + }); + }); + + describe("some", function () { + it("should work", function () { + expect(Iterator([1, 2, 3]).some(function (n) { + return n === 2; + })).toBe(true); + expect(Iterator([1, 2, 3]).some(function (n) { + return n > 10; + })).toBe(false); + }); + }); + + describe("any", function () { + [ + [[false, false], false], + [[false, true], true], + [[true, false], true], + [[true, true], true] + ].forEach(function (test) { + test = Iterator(test); + var input = test.next().value; + var output = test.next().value; + it("any of " + JSON.stringify(input) + " should be " + output, function () { + expect(Iterator(input).any()).toEqual(output); + }); + }); + }); + + describe("all", function () { + [ + [[false, false], false], + [[false, true], false], + [[true, false], false], + [[true, true], true] + ].forEach(function (test) { + test = Iterator(test); + var input = test.next().value; + var output = test.next().value; + it("all of " + JSON.stringify(input) + " should be " + output, function () { + expect(Iterator(input).all()).toEqual(output); + }); + }); + }); + + describe("min", function () { + it("should work", function () { + expect(definiteIterator().min()).toBe(1); + }); + }); + + describe("max", function () { + it("should work", function () { + expect(definiteIterator().max()).toBe(3); + }); + }); + + describe("sum", function () { + it("should work", function () { + expect(definiteIterator().sum()).toBe(6); + }); + }); + + describe("average", function () { + it("should work", function () { + expect(definiteIterator().average()).toBe(2); + }); + }); + + describe("flatten", function () { + it("should work", function () { + expect( + Iterator([ + definiteIterator(), + definiteIterator(), + definiteIterator() + ]).flatten() + ).toEqual([ + 1, 2, 3, + 1, 2, 3, + 1, 2, 3 + ]); + }); + }); + + describe("zip", function () { + it("should work", function () { + var cardinals = definiteIterator().mapIterator(function (n) { + return n - 1; + }); + var ordinals = definiteIterator(); + expect(cardinals.zip(ordinals)).toEqual([ + [0, 1], + [1, 2], + [2, 3] + ]); + }); + }); + + describe("enumerate", function () { + + it("should work with default start", function () { + var cardinals = definiteIterator(); + expect(cardinals.enumerate()).toEqual([ + [0, 1], + [1, 2], + [2, 3] + ]); + }); + + it("should work with given start", function () { + var cardinals = definiteIterator(); + expect(cardinals.enumerate(1)).toEqual([ + [1, 1], + [2, 2], + [3, 3] + ]); + }); + + }); + + describe("sorted", function () { + it("should work", function () { + expect(Iterator([5, 2, 4, 1, 3]).sorted()).toEqual([1, 2, 3, 4, 5]); + }); + }); + + describe("group", function () { + it("should work", function () { + expect(Iterator([5, 2, 4, 1, 3]).group(function (n) { + return n % 2 === 0; + })).toEqual([ + [false, [5, 1, 3]], + [true, [2, 4]] + ]); + }); + }); + + describe("reversed", function () { + it("should work", function () { + expect(Iterator([5, 2, 4, 1, 3]).reversed()).toEqual([3, 1, 4, 2, 5]); + }); + }); + + describe("toArray", function () { + it("should work", function () { + expect(Iterator([5, 2, 4, 1, 3]).toArray()).toEqual([5, 2, 4, 1, 3]); + }); + }); + + describe("toObject", function () { + it("should work", function () { + expect(Iterator("AB").toObject()).toEqual({ + 0: "A", + 1: "B" + }); + }); + }); + + describe("mapIterator", function () { + + it("should work", function () { + var iterator = indefiniteIterator() + .mapIterator(function (n, i, o) { + return n * 2; + }); + expect(iterator.next().value).toBe(0); + expect(iterator.next().value).toBe(2); + expect(iterator.next().value).toBe(4); + expect(iterator.next().value).toBe(6); + }); + + it("should pass the correct arguments to the callback", function () { + var iterator = indefiniteIterator() + var result = iterator.mapIterator(function (n, i, o) { + expect(i).toBe(n); + expect(o).toBe(iterator); + return n * 2; + }); + result.next(); + result.next(); + result.next(); + result.next(); + }); + + }); + + describe("filterIterator", function () { + + it("should work", function () { + var iterator = indefiniteIterator() + .filterIterator(function (n, i, o) { + expect(i).toBe(n); + //expect(o).toBe(iterator); + return n % 2 === 0; + }); + expect(iterator.next().value).toBe(0); + expect(iterator.next().value).toBe(2); + expect(iterator.next().value).toBe(4); + expect(iterator.next().value).toBe(6); + }); + + it("should pass the correct arguments to the callback", function () { + var iterator = indefiniteIterator() + var result = iterator.filterIterator(function (n, i, o) { + expect(i).toBe(n); + expect(o).toBe(iterator); + return n * 2; + }); + result.next(); + result.next(); + result.next(); + result.next(); + }); + + }); + + describe("concat", function () { + it("should work", function () { + var iterator = definiteIterator().concat(definiteIterator()); + expect(iterator.next().value).toBe(1); + expect(iterator.next().value).toBe(2); + expect(iterator.next().value).toBe(3); + expect(iterator.next().value).toBe(1); + expect(iterator.next().value).toBe(2); + expect(iterator.next().value).toBe(3); + expect(iterator.next().done).toBe(true); + }); + }); + + describe("dropWhile", function () { + + it("should work", function () { + var iterator = indefiniteIterator() + .dropWhile(function (n) { + return n < 10; + }); + expect(iterator.next().value).toBe(10); + expect(iterator.next().value).toBe(11); + expect(iterator.next().value).toBe(12); + }); + + it("should pass the correct arguments to the callback", function () { + var iterator = indefiniteIterator() + var result = iterator.dropWhile(function (n, i, o) { + expect(i).toBe(n); + expect(o).toBe(iterator); + }); + result.next(); + result.next(); + result.next(); + }); + + }); + + describe("takeWhile", function () { + + it("should work", function () { + var iterator = indefiniteIterator() + .takeWhile(function (n) { + return n < 3; + }); + expect(iterator.next().value).toBe(0); + expect(iterator.next().value).toBe(1); + expect(iterator.next().value).toBe(2); + expect(iterator.next().done).toBe(true); + }); + + it("should pass the correct arguments to the callback", function () { + var iterator = indefiniteIterator() + var result = iterator.takeWhile(function (n, i, o) { + expect(i).toBe(n); + expect(o).toBe(iterator); + return n < 3; + }); + result.next(); + result.next(); + result.next(); + }); + + }); + + describe("zipIterator", function () { + + it("should work", function () { + var cardinals = indefiniteIterator(); + var ordinals = indefiniteIterator().mapIterator(function (n) { + return n + 1; + }); + var iterator = cardinals.zipIterator(ordinals); + expect(iterator.next().value).toEqual([0, 1]); + expect(iterator.next().value).toEqual([1, 2]); + expect(iterator.next().value).toEqual([2, 3]); + }); + + it("should work, even for crazy people", function () { + var cardinals = indefiniteIterator(); + var iterator = cardinals.zipIterator(cardinals, cardinals); + expect(iterator.next().value).toEqual([0, 1, 2]); + expect(iterator.next().value).toEqual([3, 4, 5]); + expect(iterator.next().value).toEqual([6, 7, 8]); + }); + }); + + describe("enumerateIterator", function () { + it("should work", function () { + var ordinals = indefiniteIterator().mapIterator(function (n) { + return n + 1; + }); + var iterator = ordinals.enumerateIterator(); + expect(iterator.next().value).toEqual([0, 1]); + expect(iterator.next().value).toEqual([1, 2]); + expect(iterator.next().value).toEqual([2, 3]); + }); + }); + +} diff --git a/core/collections/test/spec/lfu-map-spec.js b/core/collections/test/spec/lfu-map-spec.js new file mode 100644 index 0000000000..c9774abaad --- /dev/null +++ b/core/collections/test/spec/lfu-map-spec.js @@ -0,0 +1,85 @@ + +var LfuMap = require("collections/lfu-map"); +var describeDict = require("./dict"); +var describeMap = require("./map"); +var describeToJson = require("./to-json"); + +describe("LfuMap-spec", function () { + + describeDict(LfuMap); + describeMap(LfuMap); + describeToJson(LfuMap, [[{a: 1}, 10], [{b: 2}, 20], [{c: 3}, 30]]); + + it("should remove stale entries", function () { + var map = LfuMap({a: 10, b: 20, c: 30}, 3); + map.get("a"); + map.get("b"); + map.set("d", 40); + expect(map.keysArray()).toEqual(['d', 'a', 'b']); + expect(map.length).toBe(3); + }); + + it("should not grow when re-adding", function () { + var map = LfuMap({a: 10, b: 20, c: 30}, 3); + + expect(map.keysArray()).toEqual(['a', 'b', 'c']); + expect(map.length).toBe(3); + + map.get("b"); + expect(map.keysArray()).toEqual(['a', 'c', 'b']); + expect(map.length).toBe(3); + + map.set("c", 40); + expect(map.keysArray()).toEqual(['a', 'b', 'c']); + expect(map.length).toBe(3); + }); + + it("should grow when adding new values", function () { + var map = LfuMap({}, 3); + expect(map.length).toBe(0); + + map.set("a", 10); + expect(map.length).toBe(1); + map.set("a", 10); + expect(map.length).toBe(1); + + map.set("b", 20); + expect(map.length).toBe(2); + map.set("b", 20); + expect(map.length).toBe(2); + + map.set("c", 30); + expect(map.length).toBe(3); + map.set("c", 30); + expect(map.length).toBe(3); + + // stops growing + map.set("d", 40); + expect(map.length).toBe(3); + map.set("d", 40); + expect(map.length).toBe(3); + + map.set("e", 50); + expect(map.length).toBe(3); + }); + + it("should dispatch deletion for stale entries", function () { + var map = LfuMap({a: 10, b: 20, c: 30}, 3); + var spy = jasmine.createSpy(); + map.addBeforeMapChangeListener(function (value, key) { + spy('before', key, value); + }); + map.addMapChangeListener(function (value, key) { + spy('after', key, value); + }); + map.set('d', 40); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ['before', 'd', undefined], // d will be added + ['before', 'a', undefined], // then a is pruned (stale) + ['after', 'a', undefined], // afterwards a is still pruned + ['after', 'd', 40] // and now d has a value + ]); + }); +}); diff --git a/core/collections/test/spec/lfu-set-spec.js b/core/collections/test/spec/lfu-set-spec.js new file mode 100644 index 0000000000..313fba458b --- /dev/null +++ b/core/collections/test/spec/lfu-set-spec.js @@ -0,0 +1,60 @@ +var LfuSet = require("collections/lfu-set"); +var describeCollection = require("./collection"); +var describeSet = require("./set"); +var describeToJson = require("./to-json"); + +describe("LfuSet-spec", function () { + + // construction, has, add, get, delete + describeCollection(LfuSet, [1, 2, 3, 4], true); + describeCollection(LfuSet, [{id: 0}, {id: 1}, {id: 2}, {id: 3}], true); + describeSet(LfuSet); + describeToJson(LfuSet, [1, 2, 3, 4]); + + it("should handle many repeated values", function () { + var set = new LfuSet([1, 1, 1, 2, 2, 2, 1, 2]); + expect(set.toArray()).toEqual([1, 2]); + }); + + it("should remove stale entries", function () { + var set = LfuSet([3, 4, 1, 3, 2], 3); + + expect(set.length).toBe(3); + expect(set.toArray()).toEqual([1, 2, 3]); + set.add(4); + expect(set.toArray()).toEqual([2, 4, 3]); + }); + + it("should emit LFU changes as singleton operation", function () { + var a = 1, b = 2, c = 3, d = 4; + var lfuset = LfuSet([d, c, a, b, c], 3); + lfuset.addRangeChangeListener(function(plus, minus) { + expect(plus).toEqual([d]); + expect(minus).toEqual([a]); + }); + expect(lfuset.add(d)).toBe(false); + }); + + it("should dispatch LRU changes as singleton operation", function () { + var set = LfuSet([4, 3, 1, 2, 3], 3); + var spy = jasmine.createSpy(); + set.addBeforeRangeChangeListener(function (plus, minus) { + spy('before-plus', plus); + spy('before-minus', minus); + }); + set.addRangeChangeListener(function (plus, minus) { + spy('after-plus', plus); + spy('after-minus', minus); + }); + expect(set.add(4)).toBe(false); + + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ['before-plus', [4]], + ['before-minus', [1]], + ['after-plus', [4]], + ['after-minus', [1]] + ]); + }) +}); diff --git a/core/collections/test/spec/list-spec.js b/core/collections/test/spec/list-spec.js new file mode 100644 index 0000000000..ec321e3eb7 --- /dev/null +++ b/core/collections/test/spec/list-spec.js @@ -0,0 +1,214 @@ + +var List = require("collections/list"); +var describeDeque = require("./deque"); +var describeCollection = require("./collection"); +var describeRangeChanges = require("./listen/range-changes"); +var describeToJson = require("./to-json"); + +describe("List-spec", function () { + // new List() + // List() + // List(values) + // List(values, equals) + // List(values, null, content) + // List(values).find(value) + // List(values, equals).find(value) + // List(values, equals).find(value, equals) + // List(values).findLast(value) + // List(values, equals).findLast(value) + // List(values, equals).findLast(value, equals) + // List(values).has(value) + // List(values).has(value, equals) + // List(values).get(value) + // List(values, equals).get(value) + // List(values, equals).get(value, equals) + // List(values).delete(value) + // List(values, equals).delete(value) + // List(values, equals).delete(value, equals) + // List(values).cleare() + // List(values).reverse() + // List(values).reduce(callback, basis, thisp) + // List(values).reduceRight(callback, basis, thisp) + // List(values).equals(list) + // List(values).equals(array) + // List([only]).only() + // List([]).only() + // List(many).only() + // List([]).one() + // List([one]).one() + // List(many).one() + // List(values).iterate() + // List(values) node.delete() + // List(values) node.addBefore(node) + // List(values) node.addAfter(node) + + // List(values).{add,remove}RangeChangeListener + // add + // delete + // push + // pop + // shift + // unshift + // splice + // swap + // List(values).{add,remove}BeforeRangeChangeListener + // add + // delete + // push + // pop + // shift + // unshift + // splice + // swap + + // push, pop, shift, unshift, slice, splice with numeric indicies + describeDeque(List); + + // construction, has, add, get, delete + describeCollection(List, [1, 2, 3, 4], true); + describeCollection(List, [{id: 0}, {id: 1}, {id: 2}, {id: 3}], true); + + describe("equals", function () { + var list = List(); + + it("should be reflexive", function () { + expect(list.equals(list)).toBe(true); + }); + + it("should be better than nothing", function () { + expect(list.equals()).toBe(false); + }); + + }); + + describe("compare", function () { + var list = List(); + + it("should be reflexive", function () { + expect(list.compare(list)).toBe(0); + }); + + it("should be better than nothing", function () { + expect(list.compare()).toBe(1); + }); + + }); + + describe("find", function () { + + it("should find every value in a list", function () { + var list = List([1, 2, 3, 4]); + expect(list.find(1)).toBe(list.head.next); + expect(list.find(2)).toBe(list.head.next.next); + expect(list.find(3)).toBe(list.head.next.next.next); + expect(list.find(4)).toBe(list.head.next.next.next.next); + expect(list.find(4)).toBe(list.head.prev); + expect(list.find(3)).toBe(list.head.prev.prev); + expect(list.find(2)).toBe(list.head.prev.prev.prev); + expect(list.find(1)).toBe(list.head.prev.prev.prev.prev); + }); + + it("should the first of equivalent values", function () { + var list = List([0, 1, 1, 0]); + expect(list.find(0)).toBe(list.head.next); + expect(list.find(1)).toBe(list.head.next.next); + }); + + it("should find values before startIndex", function () { + var list = List([2, 3, 2, 3]); + expect(list.find(2, null, 1)).toBe(list.head.next.next.next); + }); + + it("should use startIndex inclusively", function () { + var list = List([2, 3, 2, 3]); + expect(list.find(3, null, 1)).toBe(list.head.next.next); + }); + + }); + + describe("findLast", function () { + + it("should find every value in a list", function () { + var list = List([1, 2, 3, 4]); + expect(list.findLast(1)).toBe(list.head.next); + expect(list.findLast(2)).toBe(list.head.next.next); + expect(list.findLast(3)).toBe(list.head.next.next.next); + expect(list.findLast(4)).toBe(list.head.next.next.next.next); + expect(list.findLast(4)).toBe(list.head.prev); + expect(list.findLast(3)).toBe(list.head.prev.prev); + expect(list.findLast(2)).toBe(list.head.prev.prev.prev); + expect(list.findLast(1)).toBe(list.head.prev.prev.prev.prev); + }); + + it("should prefer later equivalent values", function () { + var list = List([0, 1, 1, 0]); + expect(list.findLast(0)).toBe(list.head.prev); + expect(list.findLast(1)).toBe(list.head.prev.prev); + }); + + it("should find values before endIndex", function () { + var list = List([2, 3, 2, 3]); + expect(list.findLast(2, null, 1)).toBe(list.head.next); + }); + + it("should use endIndex inclusively", function () { + var list = List([2, 3, 2, 3]); + expect(list.findLast(3, null, 1)).toBe(list.head.next.next); + }); + + }); + + // additional constraints on splice with regard to how it behaves when the + // offset is provided as a node instead of a number + describe("splice with nodes", function () { + + it("should splice to end with only an offset argument", function () { + var collection = List([1, 2, 3, 4]); + expect(collection.splice(collection.find(3))).toEqual([3, 4]); + expect(collection.toArray()).toEqual([1, 2]); + }); + + it("should splice nothing with no length", function () { + var collection = List([1, 2, 3, 4]); + expect(collection.splice(collection.find(3), 0)).toEqual([]); + expect(collection.toArray()).toEqual([1, 2, 3, 4]); + }); + + it("should splice one value", function () { + var collection = List([1, 2, 3, 4]); + expect(collection.splice(collection.find(3), 1)).toEqual([3]); + expect(collection.toArray()).toEqual([1, 2, 4]); + }); + + it("should splice all values", function () { + var collection = List([1, 2, 3, 4]); + expect(collection.splice(collection.head.next, collection.length)).toEqual([1, 2, 3, 4]); + expect(collection.toArray()).toEqual([]); + }); + + it("should splice all values with implied length", function () { + var collection = List([1, 2, 3, 4]); + expect(collection.splice(collection.head.next)).toEqual([1, 2, 3, 4]); + expect(collection.toArray()).toEqual([]); + }); + + }); + + describe("deleteAll", function () { + it("deletes all equivalent values", function () { + var anyEven = { + equals: function (that) { + return that % 2 === 0; + } + }; + var collection = List([1, 2, 3, 4, 5]); + expect(collection.deleteAll(anyEven)).toBe(2); + expect(collection.toArray()).toEqual([1, 3, 5]); + expect(collection.length).toBe(3); + }); + }); + + describeRangeChanges(List); + describeToJson(List, [1, 2, 3, 4]); + +}); diff --git a/core/collections/test/spec/listen/array-changes-spec.js b/core/collections/test/spec/listen/array-changes-spec.js new file mode 100644 index 0000000000..2a600cc680 --- /dev/null +++ b/core/collections/test/spec/listen/array-changes-spec.js @@ -0,0 +1,559 @@ + +require("collections/listen/array-changes"); +var describeRangeChanges = require("./range-changes"); + +describe("Array change dispatch", function () { + + // TODO (make consistent with List) + // describeRangeChanges(Array.from); + + var array = [1, 2, 3]; + var spy; + + // the following tests all share the same initial array so they + // are sensitive to changes in order + + it("set up listeners", function () { + + array.addBeforeOwnPropertyChangeListener("length", function (length) { + spy("length change from", length); + }); + + array.addOwnPropertyChangeListener("length", function (length) { + spy("length change to", length); + }); + + array.addBeforeRangeChangeListener(function (plus, minus, index) { + spy("before content change at", index, "to add", plus.slice(), "to remove", minus.slice()); + }); + + array.addRangeChangeListener(function (plus, minus, index) { + spy("content change at", index, "added", plus.slice(), "removed", minus.slice()); + }); + + array.addBeforeMapChangeListener(function (value, key) { + spy("change at", key, "from", value); + }); + + array.addMapChangeListener(function (value, key) { + spy("change at", key, "to", value); + }); + + }); + + it("change dispatch properties should not be enumerable", function () { + // this verifies that dispatchesRangeChanges and dispatchesMapChanges + // are both non-enumerable, and any other properties that might get + // added in the future. + for (var name in array) { + expect(isNaN(+name)).toBe(false); + } + }); + + it("clear initial values", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([1, 2, 3]); + array.clear(); + expect(array).toEqual([]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 3], + ["before content change at", 0, "to add", [], "to remove", [1, 2, 3]], + ["change at", 0, "from", 1], + ["change at", 1, "from", 2], + ["change at", 2, "from", 3], + ["change at", 0, "to", undefined], + ["change at", 1, "to", undefined], + ["change at", 2, "to", undefined], + ["content change at", 0, "added", [], "removed", [1, 2, 3]], + ["length change to", 0] + ]); + }); + + it("push two values on empty array", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([]); // initial + array.push(10, 20); + expect(array).toEqual([10, 20]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 0], + ["before content change at", 0, "to add", [10, 20], "to remove", []], + ["change at", 0, "from", undefined], + ["change at", 1, "from", undefined], + ["change at", 0, "to", 10], + ["change at", 1, "to", 20], + ["content change at", 0, "added", [10, 20], "removed", []], + ["length change to", 2], + ]); + + }); + + it("pop one value", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([10, 20]); + array.pop(); + expect(array).toEqual([10]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 2], + ["before content change at", 1, "to add", [], "to remove", [20]], + ["change at", 1, "from", 20], + ["change at", 1, "to", undefined], + ["content change at", 1, "added", [], "removed", [20]], + ["length change to", 1], + ]); + }); + + it("push two values on top of existing one, with hole open for splice", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([10]); + array.push(40, 50); + expect(array).toEqual([10, 40, 50]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 1], + ["before content change at", 1, "to add", [40, 50], "to remove", []], + ["change at", 1, "from", undefined], + ["change at", 2, "from", undefined], + ["change at", 1, "to", 40], + ["change at", 2, "to", 50], + ["content change at", 1, "added", [40, 50], "removed", []], + ["length change to", 3] + ]); + }); + + it("splices two values into middle", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([10, 40, 50]); + expect(array.splice(1, 0, 20, 30)).toEqual([]); + expect(array).toEqual([10, 20, 30, 40, 50]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 3], + ["before content change at", 1, "to add", [20, 30], "to remove", []], + ["change at", 1, "from", 40], + ["change at", 2, "from", 50], + ["change at", 3, "from", undefined], + ["change at", 4, "from", undefined], + ["change at", 1, "to", 20], + ["change at", 2, "to", 30], + ["change at", 3, "to", 40], + ["change at", 4, "to", 50], + ["content change at", 1, "added", [20, 30], "removed", []], + ["length change to", 5] + ]); + }); + + it("pushes one value to end", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([10, 20, 30, 40, 50]); + array.push(60); + expect(array).toEqual([10, 20, 30, 40, 50, 60]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 5], + ["before content change at", 5, "to add", [60], "to remove", []], + ["change at", 5, "from", undefined], + ["change at", 5, "to", 60], + ["content change at", 5, "added", [60], "removed", []], + ["length change to", 6] + ]); + }); + + it("splices in place", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([10, 20, 30, 40, 50, 60]); + expect(array.splice(2, 2, "A", "B")).toEqual([30, 40]); + expect(array).toEqual([10, 20, "A", "B", 50, 60]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + // no length change + ["before content change at", 2, "to add", ["A", "B"], "to remove", [30, 40]], + ["change at", 2, "from", 30], + ["change at", 3, "from", 40], + ["change at", 2, "to", "A"], + ["change at", 3, "to", "B"], + ["content change at", 2, "added", ["A", "B"], "removed", [30, 40]], + ]); + }); + + // ---- fresh start + + it("shifts one from the beginning", function () { + array.clear(); // start over fresh + array.push(10, 20, 30); + spy = jasmine.createSpy(); + expect(array).toEqual([10, 20, 30]); + expect(array.shift()).toEqual(10); + expect(array).toEqual([20, 30]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 3], + ["before content change at", 0, "to add", [], "to remove", [10]], + ["change at", 0, "from", 10], + ["change at", 1, "from", 20], + ["change at", 2, "from", 30], + ["change at", 0, "to", 20], + ["change at", 1, "to", 30], + ["change at", 2, "to", undefined], + ["content change at", 0, "added", [], "removed", [10]], + ["length change to", 2] + ]); + }); + + it("sets new value at end", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([20, 30]); + expect(array.set(2, 40)).toBe(true); + expect(array).toEqual([20, 30, 40]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 2], + ["before content change at", 2, "to add", [40], "to remove", []], + ["change at", 2, "from", undefined], + ["change at", 2, "to", 40], + ["content change at", 2, "added", [40], "removed", []], + ["length change to", 3] + ]); + }); + + it("sets new value at beginning", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([20, 30, 40]); + expect(array.set(0, 10)).toBe(true); + expect(array).toEqual([10, 30, 40]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 0, "to add", [10], "to remove", [20]], + ["change at", 0, "from", 20], + ["change at", 0, "to", 10], + ["content change at", 0, "added", [10], "removed", [20]] + ]); + }); + + it("splices two values outside the array range", function () { + array.clear(); + array.push(10, 20, 30); + + spy = jasmine.createSpy(); + expect(array).toEqual([10, 20, 30]); + expect(array.splice(4, 0, 50)).toEqual([]); + expect(array).toEqual([10, 20, 30, 50]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 3], + ["before content change at", 3, "to add", [50], "to remove", []], + ["change at", 3, "from", undefined], + ["change at", 3, "to", 50], + ["content change at", 3, "added", [50], "removed", []], + ["length change to", 4] + ]); + }); + + // ---- fresh start + + it("unshifts one to the beginning", function () { + array.clear(); // start over fresh + expect(array).toEqual([]); + spy = jasmine.createSpy(); + array.unshift(30); + expect(array).toEqual([30]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 0], + ["before content change at", 0, "to add", [30], "to remove", []], + ["change at", 0, "from", undefined], + ["change at", 0, "to", 30], + ["content change at", 0, "added", [30], "removed", []], + ["length change to", 1] + ]); + }); + + it("unshifts two values on beginning of already populated array", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([30]); + array.unshift(10, 20); + expect(array).toEqual([10, 20, 30]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 1], + // added and removed values reflect the ending values, not the values at the time of the call + ["before content change at", 0, "to add", [10, 20], "to remove", []], + ["change at", 0, "from", 30], + ["change at", 1, "from", undefined], + ["change at", 2, "from", undefined], + ["change at", 0, "to", 10], + ["change at", 1, "to", 20], + ["change at", 2, "to", 30], + ["content change at", 0, "added", [10, 20], "removed", []], + ["length change to", 3] + ]); + }); + + it("reverses in place", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([10, 20, 30]); + array.reverse(); + expect(array).toEqual([30, 20, 10]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 0, "to add", [30, 20, 10], "to remove", [10, 20, 30]], + ["change at", 0, "from", 10], + ["change at", 1, "from", 20], + ["change at", 2, "from", 30], + ["change at", 0, "to", 30], + ["change at", 1, "to", 20], + ["change at", 2, "to", 10], + ["content change at", 0, "added", [30, 20, 10], "removed", [10, 20, 30]], + ]); + }); + + it("sorts in place", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([30, 20, 10]); + array.sort(); + expect(array).toEqual([10, 20, 30]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + // added and removed values reflect the ending values, not the values at the time of the call + ["before content change at", 0, "to add", [30, 20, 10], "to remove", [30, 20, 10]], + ["change at", 0, "from", 30], + ["change at", 1, "from", 20], + ["change at", 2, "from", 10], + ["change at", 0, "to", 10], + ["change at", 1, "to", 20], + ["change at", 2, "to", 30], + ["content change at", 0, "added", [10, 20, 30], "removed", [10, 20, 30]], + ]); + }); + + it("deletes one value", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([10, 20, 30]); + expect(array.delete(40)).toBe(false); // to exercise deletion of non-existing entry + expect(array.delete(20)).toBe(true); + expect(array).toEqual([10, 30]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 3], + ["before content change at", 1, "to add", [], "to remove", [20]], + ["change at", 1, "from", 20], + ["change at", 2, "from", 30], + ["change at", 1, "to", 30], + ["change at", 2, "to", undefined], + ["content change at", 1, "added", [], "removed", [20]], + ["length change to", 2] + ]); + }); + + it("sets a value outside the existing range", function () { + expect(array).toEqual([10, 30]); + spy = jasmine.createSpy(); + expect(array.set(3, 40)).toBe(true); + expect(array).toEqual([10, 30, , 40]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 2], + ["before content change at", 2, "to add", [ , 40], "to remove", []], + ["change at", 2, "from", undefined], + ["change at", 3, "from", undefined], + ["change at", 2, "to", undefined], + ["change at", 3, "to", 40], + ["content change at", 2, "added", [ , 40], "removed", []], + ["length change to", 4] + ]); + array.pop(); + array.pop(); + }); + + it("clears all values finally", function () { + spy = jasmine.createSpy(); + expect(array).toEqual([10, 30]); + array.clear(); + expect(array).toEqual([]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["length change from", 2], + ["before content change at", 0, "to add", [], "to remove", [10, 30]], + ["change at", 0, "from", 10], + ["change at", 1, "from", 30], + ["change at", 0, "to", undefined], + ["change at", 1, "to", undefined], + ["content change at", 0, "added", [], "removed", [10, 30]], + ["length change to", 0] + ]); + }); + + it("removes content change listeners", function () { + spy = jasmine.createSpy(); + + // mute all listeners + // current is now optimized to be an objet when there's only one listener vs an array when there's more than one. + //This isn't intended to be a public API + var descriptor = array.getOwnPropertyChangeDescriptor('length'), + currentWillChangeListeners = descriptor.willChangeListeners.current, + currentChangeListeners = descriptor.changeListeners.current; + + if(Array.isArray(currentWillChangeListeners)) { + currentWillChangeListeners.forEach(function (listener) { + array.removeBeforeOwnPropertyChangeListener('length', listener); + }); + } + else if(currentWillChangeListeners) { + array.removeBeforeOwnPropertyChangeListener('length', currentWillChangeListeners); + } + + if(Array.isArray(currentChangeListeners)) { + currentChangeListeners.forEach(function (listener) { + array.removeOwnPropertyChangeListener('length', listener); + }); + } + else if(currentChangeListeners){ + array.removeOwnPropertyChangeListener('length', currentChangeListeners); + } + + + // current is now optimized to be an objet when there's only one listener vs an array when there's more than one. + //This isn't intended to be a public API + var descriptor = array.getRangeChangeDescriptor(), + currentWillChangeListeners = descriptor.willChangeListeners.current, + currentChangeListeners = descriptor.changeListeners.current; + + if(Array.isArray(currentWillChangeListeners)) { + currentWillChangeListeners.forEach(function (listener) { + array.removeBeforeRangeChangeListener(listener); + }); + } + else if(currentWillChangeListeners) { + array.removeBeforeRangeChangeListener(currentWillChangeListeners); + } + + if(Array.isArray(currentChangeListeners)) { + currentChangeListeners.forEach(function (listener) { + array.removeRangeChangeListener(listener); + }); + } + else if(currentChangeListeners){ + array.removeRangeChangeListener(currentChangeListeners); + } + + + // current is now optimized to be an objet when there's only one listener vs an array when there's more than one. + //This isn't intended to be a public API + var descriptor = array.getMapChangeDescriptor(), + currentWillChangeListeners = descriptor.willChangeListeners.current, + currentChangeListeners = descriptor.changeListeners.current; + + if(Array.isArray(currentWillChangeListeners)) { + currentWillChangeListeners.forEach(function (listener) { + array.removeBeforeMapChangeListener(listener); + }); + } + else if(currentWillChangeListeners) { + array.removeBeforeMapChangeListener(currentWillChangeListeners); + } + + if(Array.isArray(currentChangeListeners)) { + currentChangeListeners.forEach(function (listener) { + array.removeMapChangeListener(listener); + }); + } + else if(currentChangeListeners){ + array.removeMapChangeListener(currentChangeListeners); + } + + // modify + array.splice(0, 0, 1, 2, 3); + + // note silence + expect(spy).not.toHaveBeenCalled(); + }); + + // --------------- FIN ----------------- + + it("handles cyclic content change listeners", function () { + var foo = []; + var bar = []; + foo.addRangeChangeListener(function (plus, minus, index) { + // if this is a change in response to a change in bar, + // do not send back + if (bar.getRangeChangeDescriptor().isActive) + return; + bar.splice.apply(bar, [index, minus.length].concat(plus)); + }); + bar.addRangeChangeListener(function (plus, minus, index) { + if (foo.getRangeChangeDescriptor().isActive) + return; + foo.splice.apply(foo, [index, minus.length].concat(plus)); + }); + foo.push(10, 20, 30); + expect(bar).toEqual([10, 20, 30]); + bar.pop(); + expect(foo).toEqual([10, 20]); + }); + + it("observes length changes on arrays that are not otherwised observed", function () { + var array = [1, 2, 3]; + var spy = jasmine.createSpy(); + array.addOwnPropertyChangeListener("length", spy); + array.push(4); + expect(spy).toHaveBeenCalled(); + }); + + describe("splice", function () { + var array; + beforeEach(function () { + array = [0, 1, 2]; + array.makeObservable(); + }); + + it("handles a negative start", function () { + var removed = array.splice(-1, 1); + expect(removed).toEqual([2]); + expect(array).toEqual([0, 1]); + }); + + it("handles a negative length", function () { + var removed = array.splice(1, -1); + expect(removed).toEqual([]); + expect(array).toEqual([0, 1, 2]); + }); + + }); + + // Disabled because it takes far too long + xdescribe("swap", function () { + var otherArray; + beforeEach(function () { + array.makeObservable(); + }); + it("should work with large arrays", function () { + otherArray = new Array(200000); + // Should not throw a Maximum call stack size exceeded error. + expect(function () { + array.swap(0, array.length, otherArray); + }).not.toThrow(); + expect(array.length).toEqual(200000); + }); + }); + +}); diff --git a/core/collections/test/spec/listen/map-changes.js b/core/collections/test/spec/listen/map-changes.js new file mode 100644 index 0000000000..2863b1f7e4 --- /dev/null +++ b/core/collections/test/spec/listen/map-changes.js @@ -0,0 +1,63 @@ + +module.exports = describeMapChanges; +function describeMapChanges(Map) { + + it("should dispatch addition", function () { + var map = new Map(); + var spy = jasmine.createSpy(); + map.addBeforeMapChangeListener(function (value, key) { + spy('before', key, value); + }); + map.addMapChangeListener(function (value, key) { + spy('after', key, value); + }); + map.set(0, 10); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ['before', 0, undefined], + ['after', 0, 10] + ]); + }); + + it("should dispatch alteration", function () { + var map = new Map([[0, 10]]); + var spy = jasmine.createSpy(); + map.addBeforeMapChangeListener(function (value, key) { + spy('before', key, value); + }); + map.addMapChangeListener(function (value, key) { + spy('after', key, value); + }); + map.set(0, 20); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ['before', 0, 10], + ['after', 0, 20] + ]); + }); + + xit("should dispatch deletion", function () { + var map = new Map([[0, 20]]); + // Arrays do not behave like maps for deletion. + if (Array.isArray(map)) { + return; + } + var spy = jasmine.createSpy(); + map.addBeforeMapChangeListener(function (value, key) { + spy('before', key, value); + }); + map.addMapChangeListener(function (value, key) { + spy('after', key, value); + }); + map.delete(0); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ['before', 0, 20], + ['after', 0, undefined] + ]); + }); + +} diff --git a/core/collections/test/spec/listen/property-changes-spec.js b/core/collections/test/spec/listen/property-changes-spec.js new file mode 100644 index 0000000000..f7e0df6cdd --- /dev/null +++ b/core/collections/test/spec/listen/property-changes-spec.js @@ -0,0 +1,302 @@ +/* + Based in part on observable arrays from Motorola Mobility’s Montage + Copyright (c) 2012, Motorola Mobility LLC. All Rights Reserved. + 3-Clause BSD License + https://github.com/motorola-mobility/montage/blob/master/LICENSE.md +*/ + +require("collections/shim"); +var PropertyChanges = require("collections/listen/property-changes"); + +describe("PropertyChanges", function () { + + it("observes setter on object", function () { + spy = jasmine.createSpy(); + var object = {}; + PropertyChanges.addBeforeOwnPropertyChangeListener(object, 'x', function (value, key) { + spy('from', value, key); + }); + PropertyChanges.addOwnPropertyChangeListener(object, 'x', function (value, key) { + spy('to', value, key); + }); + object.x = 10; + expect(object.x).toEqual(10); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ['from', undefined, 'x'], + ['to', 10, 'x'], + ]); + }); + + it("observes setter on object with getter/setter", function () { + spy = jasmine.createSpy(); + var value; + var object = Object.create(Object.prototype, { + _x: { + value: 10, + writable: true + }, + x: { + get: function () { + return this._x; + }, + set: function (_value) { + this._x = _value; + }, + enumerable: false, + configurable: true + } + }); + PropertyChanges.addBeforeOwnPropertyChangeListener(object, 'x', function (value, key) { + spy('from', value, key); + }); + PropertyChanges.addOwnPropertyChangeListener(object, 'x', function (value, key) { + spy('to', value, key); + }); + object.x = 20; + expect(object.x).toEqual(20); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ['from', 10, 'x'], + ['to', 20, 'x'], // reports no change + ]); + }); + + it("shouldn't call the listener if the new value is the same after calling the object setter", function () { + spy = jasmine.createSpy(); + var value; + var object = Object.create(Object.prototype, { + x: { + get: function () { + return 20; + }, + set: function (_value) { + // refuse to change internal state + }, + enumerable: false, + configurable: true + } + }); + PropertyChanges.addBeforeOwnPropertyChangeListener(object, 'x', function (value, key) { + spy('from', value, key); + }); + PropertyChanges.addOwnPropertyChangeListener(object, 'x', function (value, key) { + spy('to', value, key); + }); + object.x = 10; + expect(object.x).toEqual(20); + expect(spy).not.toHaveBeenCalled(); + }); + + it("calls setter on object when the new value is the current value", function() { + var object = Object.create(Object.prototype, { + _x: {value: 0, writable: true}, + x: { + get: function() { + return this._x; + }, + set: function(value) { + this._x = value * 2; + }, + configurable: true, + enumerable: true + } + }); + + PropertyChanges.addOwnPropertyChangeListener(object, "x", function() {}); + + object.x = 1; + object.x = 2; + + expect(object.x).toBe(4); + }); + + it("handles cyclic own property change listeners", function () { + var a = {}; + var b = {}; + PropertyChanges.addOwnPropertyChangeListener(a, 'foo', function (value) { + b.bar = value; + }); + PropertyChanges.addOwnPropertyChangeListener(b, 'bar', function (value) { + a.foo = value; + }); + a.foo = 10; + expect(b.bar).toEqual(10); + }); + + it("handles generic own property change listeners", function () { + var object = { + handlePropertyChange: function (value, key) { + expect(value).toBe(10); + expect(key).toBe("foo"); + } + }; + spyOn(object, "handlePropertyChange").and.callThrough(); + PropertyChanges.addOwnPropertyChangeListener(object, "foo", object); + object.foo = 10; + expect(object.handlePropertyChange).toHaveBeenCalled(); + }); + + it("handles generic before own property change listeners", function () { + var object = { + foo: 12, + handlePropertyWillChange: function (value, key) { + expect(value).toBe(12); + expect(key).toBe("foo"); + } + }; + spyOn(object, "handlePropertyWillChange").and.callThrough(); + PropertyChanges.addBeforeOwnPropertyChangeListener(object, "foo", object); + object.foo = 10; + expect(object.handlePropertyWillChange).toHaveBeenCalled(); + }); + + it("handles specific own property change listeners", function () { + var object = { + handleFooChange: function (value) { + expect(value).toBe(10); + } + }; + spyOn(object, "handleFooChange").and.callThrough(); + PropertyChanges.addOwnPropertyChangeListener(object, "foo", object); + object.foo = 10; + expect(object.handleFooChange).toHaveBeenCalled(); + }); + + it("handles specific before own property change listeners", function () { + var object = { + foo: 12, + handleFooWillChange: function (value) { + expect(value).toBe(12); + } + }; + spyOn(object, "handleFooWillChange").and.callThrough(); + PropertyChanges.addBeforeOwnPropertyChangeListener(object, "foo", object); + object.foo = 10; + expect(object.handleFooWillChange).toHaveBeenCalled(); + }); + + it("calls later handlers if earlier ones remove themselves", function () { + var object = { + foo: true + }; + var listener1 = { + handleFooChange: function (value, key, object) { + PropertyChanges.removeOwnPropertyChangeListener(object, key, listener1); + } + }; + var listener2 = jasmine.createSpyObj("listener2", ["handleFooChange"]); + + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener1); + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener2); + + object.foo = false; + expect(listener2.handleFooChange).toHaveBeenCalled(); + }); + + it("calls later handlers if multiple earlier ones remove themselves", function () { + var object = { + foo: true + }; + var listener3 = { + handleFooChange: function (value, key, object) { + PropertyChanges.removeOwnPropertyChangeListener(object, key, listener1); + PropertyChanges.removeOwnPropertyChangeListener(object, key, listener3); + } + }; + var listener1 = jasmine.createSpyObj("listener1", ["handleFooChange"]); + var listener2 = jasmine.createSpyObj("listener2", ["handleFooChange"]); + var listener4 = jasmine.createSpyObj("listener4", ["handleFooChange"]); + + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener1); + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener2); + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener3); + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener4); + + object.foo = false; + expect(listener1.handleFooChange).toHaveBeenCalled(); + expect(listener2.handleFooChange).toHaveBeenCalled(); + expect(listener4.handleFooChange).toHaveBeenCalled(); + }); + + it("doesn't call any handlers if all the listeners are removed during dispatch", function () { + var object = { + foo: true + }; + var listener1 = { + handleFooChange: function (value, key, object) { + PropertyChanges.removeOwnPropertyChangeListener(object, key, listener1); + PropertyChanges.removeOwnPropertyChangeListener(object, key, listener2); + } + }; + var listener2 = jasmine.createSpyObj("listener2", ["handleFooChange"]); + + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener1); + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener2); + + object.foo = false; + expect(listener2.handleFooChange).not.toHaveBeenCalled(); + }); + + it("doesn't call new handlers if listeners are added during dispatch", function () { + var object = { + foo: true + }; + var listener1 = { + handleFooChange: function (value, key, object) { + PropertyChanges.removeOwnPropertyChangeListener(object, key, listener1); + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener3); + } + }; + var listener2 = jasmine.createSpyObj("listener2", ["handleFooChange"]); + var listener3 = jasmine.createSpyObj("listener3", ["handleFooChange"]); + + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener1); + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener2); + + object.foo = false; + expect(listener2.handleFooChange).toHaveBeenCalled(); + expect(listener3.handleFooChange).not.toHaveBeenCalled(); + }); + + it("compact listeners when the ratio of ListenerGhost is too high", function () { + var object = { + foo: true + }; + var listener3 = { + handleFooChange: function (value, key, object) { + PropertyChanges.removeOwnPropertyChangeListener(object, key, listener1); + PropertyChanges.removeOwnPropertyChangeListener(object, key, listener3); + PropertyChanges.removeOwnPropertyChangeListener(object, key, listener5); + } + }; + var listener1 = jasmine.createSpyObj("listener1", ["handleFooChange"]); + var listener2 = jasmine.createSpyObj("listener2", ["handleFooChange"]); + var listener4 = jasmine.createSpyObj("listener4", ["handleFooChange"]); + var listener5 = jasmine.createSpyObj("listener5", ["handleFooChange"]); + + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener1); + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener2); + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener3); + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener4); + PropertyChanges.addOwnPropertyChangeListener(object, "foo", listener5); + + object.foo = false; + expect(listener1.handleFooChange).toHaveBeenCalled(); + expect(listener2.handleFooChange).toHaveBeenCalled(); + expect(listener4.handleFooChange).toHaveBeenCalled(); + + var descriptor = PropertyChanges.getOwnPropertyChangeDescriptor(object, "foo"); + expect(descriptor.changeListeners.ghostCount).toEqual(3); + + object.foo = true; + expect(descriptor.changeListeners.ghostCount).toEqual(0); + + expect(listener2.handleFooChange).toHaveBeenCalled(); + expect(listener4.handleFooChange).toHaveBeenCalled(); + + }); + +}); diff --git a/core/collections/test/spec/listen/range-changes.js b/core/collections/test/spec/listen/range-changes.js new file mode 100644 index 0000000000..261dabf200 --- /dev/null +++ b/core/collections/test/spec/listen/range-changes.js @@ -0,0 +1,373 @@ + +module.exports = describeRangeChanges; +function describeRangeChanges(Collection) { + + var collection = Collection([1, 2, 3]); + var spy; + + // the following tests all share the same initial collection so they + // are sensitive to changes in order + + it("set up listeners", function () { + collection.addBeforeOwnPropertyChangeListener("length", function (length) { + spy("length change from", length); + }); + + collection.addOwnPropertyChangeListener("length", function (length) { + spy("length change to", length); + }); + + collection.addBeforeRangeChangeListener(function (plus, minus, index) { + spy("before content change at", index, "to add", plus.slice(), "to remove", minus.slice()); + }); + + collection.addRangeChangeListener(function (plus, minus, index) { + spy("content change at", index, "added", plus.slice(), "removed", minus.slice()); + }); + }); + + it("clear initial values", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([1, 2, 3]); + collection.clear(); + expect(collection.slice()).toEqual([]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 0, "to add", [], "to remove", [1, 2, 3]], + ["length change from", 3], + ["length change to", 0], + ["content change at", 0, "added", [], "removed", [1, 2, 3]], + ]); + }); + + it("push two values on empty collection", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([]); // initial + collection.push(10, 20); + expect(collection.slice()).toEqual([10, 20]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 0, "to add", [10, 20], "to remove", []], + ["length change from", 0], + ["length change to", 2], + ["content change at", 0, "added", [10, 20], "removed", []], + ]); + + }); + + it("pop one value", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([10, 20]); + collection.pop(); + expect(collection.slice()).toEqual([10]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 1, "to add", [], "to remove", [20]], + ["length change from", 2], + ["length change to", 1], + ["content change at", 1, "added", [], "removed", [20]], + ]); + }); + + it("push two values on top of existing one, with hole open for splice", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([10]); + collection.push(40, 50); + expect(collection.slice()).toEqual([10, 40, 50]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 1, "to add", [40, 50], "to remove", []], + ["length change from", 1], + ["length change to", 3], + ["content change at", 1, "added", [40, 50], "removed", []], + ]); + }); + + it("splices two values into middle", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([10, 40, 50]); + expect(collection.splice(1, 0, 20, 30)).toEqual([]); + expect(collection.slice()).toEqual([10, 20, 30, 40, 50]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 1, "to add", [20, 30], "to remove", []], + ["length change from", 3], + ["length change to", 5], + ["content change at", 1, "added", [20, 30], "removed", []], + ]); + }); + + it("pushes one value to end", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([10, 20, 30, 40, 50]); + collection.push(60); + expect(collection.slice()).toEqual([10, 20, 30, 40, 50, 60]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 5, "to add", [60], "to remove", []], + ["length change from", 5], + ["length change to", 6], + ["content change at", 5, "added", [60], "removed", []], + ]); + }); + + it("splices in place", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([10, 20, 30, 40, 50, 60]); + expect(collection.splice(2, 2, "A", "B")).toEqual([30, 40]); + expect(collection.slice()).toEqual([10, 20, "A", "B", 50, 60]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + // no length change + ["before content change at", 2, "to add", ["A", "B"], "to remove", [30, 40]], + ["content change at", 2, "added", ["A", "B"], "removed", [30, 40]], + ]); + }); + + // ---- fresh start + + it("shifts one from the beginning", function () { + collection.clear(); // start over fresh + collection.push(10, 20, 30); + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([10, 20, 30]); + expect(collection.shift()).toEqual(10); + expect(collection.slice()).toEqual([20, 30]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 0, "to add", [], "to remove", [10]], + ["length change from", 3], + ["length change to", 2], + ["content change at", 0, "added", [], "removed", [10]], + ]); + }); + + it("sets new value at end", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([20, 30]); + expect(collection.splice(2, 0, 40)).toEqual([]); + expect(collection.slice()).toEqual([20, 30, 40]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 2, "to add", [40], "to remove", []], + ["length change from", 2], + ["length change to", 3], + ["content change at", 2, "added", [40], "removed", []], + ]); + }); + + it("sets new value at beginning", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([20, 30, 40]); + expect(collection.splice(0, 1, 10)).toEqual([20]); + expect(collection.slice()).toEqual([10, 30, 40]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 0, "to add", [10], "to remove", [20]], + ["content change at", 0, "added", [10], "removed", [20]], + ]); + }); + + // ---- fresh start + + it("unshifts one to the beginning", function () { + collection.clear(); // start over fresh + expect(collection.slice()).toEqual([]); + spy = jasmine.createSpy(); + collection.unshift(30); + expect(collection.slice()).toEqual([30]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 0, "to add", [30], "to remove", []], + ["length change from", 0], + ["length change to", 1], + ["content change at", 0, "added", [30], "removed", []], + ]); + }); + + it("unshifts two values on beginning of already populated collection", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([30]); + collection.unshift(10, 20); + expect(collection.slice()).toEqual([10, 20, 30]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + // added and removed values reflect the ending values, not the values at the time of the call + ["before content change at", 0, "to add", [10, 20], "to remove", []], + ["length change from", 1], + ["length change to", 3], + ["content change at", 0, "added", [10, 20], "removed", []], + ]); + }); + + it("reverses in place", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([10, 20, 30]); + collection.reverse(); + expect(collection.slice()).toEqual([30, 20, 10]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 0, "to add", [30, 20, 10], "to remove", [10, 20, 30]], + ["content change at", 0, "added", [30, 20, 10], "removed", [10, 20, 30]], + ]); + }); + + it("sorts in place", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([30, 20, 10]); + collection.sort(); + expect(collection.slice()).toEqual([10, 20, 30]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + // added and removed values reflect the ending values, not the values at the time of the call + ["before content change at", 0, "to add", [10, 20, 30], "to remove", [30, 20, 10]], + ["content change at", 0, "added", [10, 20, 30], "removed", [30, 20, 10]], + ]); + }); + + it("deletes one value", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([10, 20, 30]); + expect(collection.delete(40)).toBe(false); // to exercise deletion of non-existing entry + expect(collection.delete(20)).toBe(true); + expect(collection.slice()).toEqual([10, 30]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 1, "to add", [], "to remove", [20]], + ["length change from", 3], + ["length change to", 2], + ["content change at", 1, "added", [], "removed", [20]], + ]); + }); + + it("clears all values finally", function () { + spy = jasmine.createSpy(); + expect(collection.slice()).toEqual([10, 30]); + collection.clear(); + expect(collection.slice()).toEqual([]); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["before content change at", 0, "to add", [], "to remove", [10, 30]], + ["length change from", 2], + ["length change to", 0], + ["content change at", 0, "added", [], "removed", [10, 30]], + ]); + }); + + it("removes content change listeners", function () { + spy = jasmine.createSpy(); + + // mute all listeners + // current is now optimized to be an objet when there's only one listener vs an array when there's more than one. + //This isn't intended to be a public API + var descriptor = collection.getOwnPropertyChangeDescriptor('length'), + currentWillChangeListeners = descriptor.willChangeListeners.current, + currentChangeListeners = descriptor.changeListeners.current; + + if(Array.isArray(currentWillChangeListeners)) { + currentWillChangeListeners.forEach(function (listener) { + collection.removeBeforeOwnPropertyChangeListener('length', listener); + }); + } + else if(currentWillChangeListeners){ + collection.removeBeforeOwnPropertyChangeListener('length', currentWillChangeListeners); + } + + if(Array.isArray(currentChangeListeners)) { + currentChangeListeners.forEach(function (listener) { + collection.removeOwnPropertyChangeListener('length', listener); + }); + } + else if(currentChangeListeners){ + collection.removeOwnPropertyChangeListener('length', currentChangeListeners); + } + + + // current is now optimized to be an objet when there's only one listener vs an array when there's more than one. + //This isn't intended to be a public API + var descriptor = collection.getRangeChangeDescriptor(), + currentWillChangeListeners = descriptor.willChangeListeners.current, + currentChangeListeners = descriptor.changeListeners.current; + + if(Array.isArray(currentWillChangeListeners)) { + currentWillChangeListeners.forEach(function (listener) { + collection.removeBeforeRangeChangeListener(listener); + }); + } + else if(currentWillChangeListeners) { + collection.removeBeforeRangeChangeListener(currentWillChangeListeners); + } + + if(Array.isArray(currentChangeListeners)) { + currentChangeListeners.forEach(function (listener) { + collection.removeRangeChangeListener(listener); + }); + } + else if(currentChangeListeners){ + collection.removeRangeChangeListener(currentChangeListeners); + } + + // modify + collection.splice(0, 0, 1, 2, 3); + + // note silence + expect(spy).not.toHaveBeenCalled(); + }); + + // --------------- FIN ----------------- + + it("handles cyclic content change listeners", function () { + var foo = Collection([]); + var bar = Collection([]); + foo.addRangeChangeListener(function (plus, minus, index) { + // if this is a change in response to a change in bar, + // do not send back + if (bar.getRangeChangeDescriptor().isActive) + return; + bar.splice.apply(bar, [index, minus.length].concat(plus)); + }); + bar.addRangeChangeListener(function (plus, minus, index) { + if (foo.getRangeChangeDescriptor().isActive) + return; + foo.splice.apply(foo, [index, minus.length].concat(plus)); + }); + foo.push(10, 20, 30); + expect(bar.slice()).toEqual([10, 20, 30]); + bar.pop(); + expect(foo.slice()).toEqual([10, 20]); + }); + + it("observes length changes on collections that are not otherwised observed", function () { + var collection = new Collection([1, 2, 3]); + var spy = jasmine.createSpy(); + collection.addOwnPropertyChangeListener("length", spy); + collection.push(4); + expect(spy).toHaveBeenCalled(); + }); + + it("does not throw an error when dispatching a range change on a collection with no range change listeners that previously had more than one listener", function () { + var collection = new Collection([1, 2, 3]); + // Adding two range change listeners to trigger the behavior of change listeners being stored in an array + var cancelRangeChangeListenerA = collection.addRangeChangeListener(Function.noop); + var cancelRangeChangeListenerB = collection.addRangeChangeListener(Function.noop); + cancelRangeChangeListenerA(); + cancelRangeChangeListenerB(); + collection.push(5); + }); +} diff --git a/core/collections/test/spec/lru-map-spec.js b/core/collections/test/spec/lru-map-spec.js new file mode 100644 index 0000000000..78184953a9 --- /dev/null +++ b/core/collections/test/spec/lru-map-spec.js @@ -0,0 +1,84 @@ + +var LruMap = require("collections/lru-map"); +var describeDict = require("./dict"); +var describeMap = require("./map"); +var describeToJson = require("./to-json"); + +describe("LruMap-spec", function () { + + describeDict(LruMap); + describeMap(LruMap); + describeToJson(LruMap, [[{a: 1}, 10], [{b: 2}, 20], [{c: 3}, 30]]); + + it("should remove stale entries", function () { + var map = LruMap({a: 10, b: 20, c: 30}, 3); + map.get("b"); + map.set("d", 40); + expect(map.keysArray()).toEqual(['c', 'b', 'd']); + expect(map.length).toBe(3); + }); + + it("should not grow when re-adding", function () { + var map = LruMap({a: 10, b: 20, c: 30}, 3); + + expect(map.keysArray()).toEqual(['a', 'b', 'c']); + expect(map.length).toBe(3); + + map.get("b"); + expect(map.keysArray()).toEqual(['a', 'c', 'b']); + expect(map.length).toBe(3); + + map.set("c", 40); + expect(map.keysArray()).toEqual(['a', 'b', 'c']); + expect(map.length).toBe(3); + }); + + it("should grow when adding new values", function () { + var map = LruMap({}, 3); + expect(map.length).toBe(0); + + map.set("a", 10); + expect(map.length).toBe(1); + map.set("a", 10); + expect(map.length).toBe(1); + + map.set("b", 20); + expect(map.length).toBe(2); + map.set("b", 20); + expect(map.length).toBe(2); + + map.set("c", 30); + expect(map.length).toBe(3); + map.set("c", 30); + expect(map.length).toBe(3); + + // stops growing + map.set("d", 40); + expect(map.length).toBe(3); + map.set("d", 40); + expect(map.length).toBe(3); + + map.set("e", 50); + expect(map.length).toBe(3); + }); + + it("should dispatch deletion for stale entries", function () { + var map = LruMap({a: 10, b: 20, c: 30}, 3); + var spy = jasmine.createSpy(); + map.addBeforeMapChangeListener(function (value, key) { + spy('before', key, value); + }); + map.addMapChangeListener(function (value, key) { + spy('after', key, value); + }); + map.set('d', 40); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ['before', 'd', undefined], // d will be added + ['before', 'a', undefined], // then a is pruned (stale) + ['after', 'a', undefined], // afterwards a is still pruned + ['after', 'd', 40] // and now d has a value + ]); + }); +}); diff --git a/core/collections/test/spec/lru-set-spec.js b/core/collections/test/spec/lru-set-spec.js new file mode 100644 index 0000000000..2a7127db0f --- /dev/null +++ b/core/collections/test/spec/lru-set-spec.js @@ -0,0 +1,55 @@ + +var LruSet = require("collections/lru-set"); +var describeCollection = require("./collection"); +var describeSet = require("./set"); +var describeToJson = require("./to-json"); + +describe("LruSet-spec", function () { + + // construction, has, add, get, delete + describeCollection(LruSet, [1, 2, 3, 4], true); + describeCollection(LruSet, [{id: 0}, {id: 1}, {id: 2}, {id: 3}], true); + describeSet(LruSet); + describeToJson(LruSet, [1, 2, 3, 4]); + + it("should remove stale entries", function () { + var set = LruSet([4, 3, 1, 2, 3], 3); + expect(set.length).toBe(3); + set.add(3); + expect(set.toArray()).toEqual([1, 2, 3]); + set.add(4); + expect(set.toArray()).toEqual([2, 3, 4]); + }); + + it("should emit LRU changes as singleton operation", function () { + var a = 1, b = 2, c = 3, d = 4; + var lruset = LruSet([d, c, a, b, c], 3); + lruset.addRangeChangeListener(function(plus, minus) { + expect(plus).toEqual([d]); + expect(minus).toEqual([a]); + }); + expect(lruset.add(d)).toBe(false); + }); + + it("should dispatch LRU changes as singleton operation", function () { + var set = LruSet([4, 3, 1, 2, 3], 3); + var spy = jasmine.createSpy(); + set.addBeforeRangeChangeListener(function (plus, minus) { + spy('before-plus', plus); + spy('before-minus', minus); + }); + set.addRangeChangeListener(function (plus, minus) { + spy('after-plus', plus); + spy('after-minus', minus); + }); + expect(set.add(4)).toBe(false); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ['before-plus', [4]], + ['before-minus', [1]], + ['after-plus', [4]], + ['after-minus', [1]] + ]); + }) +}); diff --git a/core/collections/test/spec/map-spec.js b/core/collections/test/spec/map-spec.js new file mode 100644 index 0000000000..37de7b910e --- /dev/null +++ b/core/collections/test/spec/map-spec.js @@ -0,0 +1,15 @@ +// TODO test insertion order + +var Map = require("collections/map"); +var describeDict = require("./dict"); +var describeMap = require("./map"); +var describeMapChanges = require("./listen/map-changes"); +var describeToJson = require("./to-json"); + +describe("Map-spec", function () { + describeDict(Map); + describeMap(Map); + describeMapChanges(Map); + describeToJson(Map, [[{a: 1}, 10], [{b: 2}, 20], [{c: 3}, 30]]); +}); + diff --git a/core/collections/test/spec/map.js b/core/collections/test/spec/map.js new file mode 100644 index 0000000000..4c3d6cb7de --- /dev/null +++ b/core/collections/test/spec/map.js @@ -0,0 +1,93 @@ +// Tests that are equally applicable to Map, unbounded LruMap, FastMap. +// These do not apply to SortedMap since keys are not comparable. + +var describeMapChanges = require("./listen/map-changes"); + +module.exports = describeMap; +function describeMap(Map, values) { + + describeMapChanges(Map); + + values = values || []; + var a = values[0] || {}; + var b = values[1] || {}; + var c = values[2] || {}; + + function shouldHaveTheUsualContent(map) { + expect(map.has(a)).toBe(true); + expect(map.has(b)).toBe(true); + expect(map.has(c)).toBe(false); + expect(map.get(a)).toBe(10); + expect(map.get(b)).toBe(20); + expect(map.get(c)).toBe(undefined); + expect(map.length).toBe(2); + expect(map.keysArray()).toEqual([a, b]); + expect(map.valuesArray()).toEqual([10, 20]); + expect(map.entriesArray()).toEqual([[a, 10], [b, 20]]); + expect(map.reduce(function (basis, value, key) { + basis.push([this, key, value]); + return basis; + }, [], map)).toEqual([ + [map, a, 10], + [map, b, 20] + ]);; + } + + it("should be constructable from entry duples with object keys", function () { + var map = new Map([[a, 10], [b, 20]]); + shouldHaveTheUsualContent(map); + }); + + it("should be constructable from an interable", function () { + var map = Map.from({ + forEach: function (callback, thisp) { + callback.call(thisp, [a, 10]); + callback.call(thisp, [b, 20]); + } + }); + shouldHaveTheUsualContent(map); + }); + + it("should support filter", function () { + var map = Map.from({a: 10, b: 20, c: 30}); + expect(map.filter(function (value, key) { + return key === "a" || value === 30; + }).entriesArray()).toEqual([ + ["a", 10], + ["c", 30] + ]); + }); + + it("should remove one entry", function () { + var map = new Map([[a, 10], [b, 20], [c, 30]]); + expect(map.delete(c)).toBe(true); + shouldHaveTheUsualContent(map); + }); + + it("should be able to delete all content", function () { + var map = Map.from({a: 10, b: 20, c: 30}); + map.clear(); + expect(map.length).toBe(0); + expect(map.keysArray()).toEqual([]); + expect(map.valuesArray()).toEqual([]); + expect(map.entriesArray()).toEqual([]); + }); + + it("should equals", function () { + var map = Map.from({a: 10, b: 20}); + expect(Object.equals(map, map)).toBe(true); + expect(map.equals(map)).toBe(true); + expect(Map.from({a: 10, b: 20}).equals({b: 20, a: 10})).toBe(true); + expect(Object.equals({a: 10, b: 20}, Map.from({b: 20, a: 10}))).toBe(true); + expect(Object.equals(Map.from({b: 20, a: 10}), {a: 10, b: 20})).toBe(true); + expect(Object.equals(Map.from({b: 20, a: 10}), Map.from({a: 10, b: 20}))).toBe(true); + }); + + it("should clone", function () { + var map = Map.from({a: 10, b: 20}); + var clone = Object.clone(map); + expect(map).not.toBe(clone); + expect(map.equals(clone)).toBe(true); + }); + +} diff --git a/core/collections/test/spec/multi-map-spec.js b/core/collections/test/spec/multi-map-spec.js new file mode 100644 index 0000000000..810ee84419 --- /dev/null +++ b/core/collections/test/spec/multi-map-spec.js @@ -0,0 +1,7 @@ + +module.exports = describeMultiMap; +function describeMultiMap(MultiMap, values) { + it("should be constructable", function () { + var accountsMultiMap = new MultiMap() + }); +}; \ No newline at end of file diff --git a/core/collections/test/spec/order.js b/core/collections/test/spec/order.js new file mode 100644 index 0000000000..09db4f19b7 --- /dev/null +++ b/core/collections/test/spec/order.js @@ -0,0 +1,377 @@ + +var GenericCollection = require("collections/generic-collection"); + +module.exports = describeOrder; +function describeOrder(Collection) { + + /* + The following tests are from Montage. + Copyright (c) 2012, Motorola Mobility LLC. + All Rights Reserved. + BSD License. + */ + + // contains 10, 20, 30 + function FakeArray() { + this.length = 3; + } + Object.addEach(FakeArray.prototype, GenericCollection.prototype); + FakeArray.prototype.reduce = function (callback, basis) { + basis = callback(basis, 10, 0, this); + basis = callback(basis, 20, 1, this); + basis = callback(basis, 30, 2, this); + return basis; + }; + var fakeArray = new FakeArray(); + + describe("equals", function () { + + it("identifies itself", function () { + var collection = Collection([1, 2]); + expect(collection.equals(collection)).toBe(true); + }); + + it("distinguishes incomparable objects", function () { + expect(Collection([]).equals(null)).toEqual(false); + }); + + it("compares itself to an array-like collection", function () { + expect(Collection([10, 20, 30]).equals(fakeArray)).toEqual(true); + }); + + }); + + describe("compare", function () { + + it("compares to itself", function () { + var collection = Collection([1, 2]); + expect(collection.compare(collection)).toBe(0); + }); + + // contains 10, 20, 30 + it("a fake array should be equal to collection", function () { + expect(Object.compare(fakeArray, Collection([10, 20, 30]))).toEqual(-0); + }); + + it("a fake array should be less than a collection", function () { + expect(Object.compare(fakeArray, Collection([10, 30]))).toEqual(-10); + }); + + it("a fake array should be greater than a real array because it is longer", function () { + expect(Object.compare(fakeArray, Collection([10, 20]))).toEqual(1); + }); + + it("a fake array should be less than a longer but otherwise equal", function () { + expect(Object.compare(fakeArray, Collection([10, 20, 30, 40]))).toEqual(-1); + }); + + it("an array should be equal to a fake array", function () { + expect(Collection([10, 20, 30]).compare(fakeArray)).toEqual(0); + }); + + it("an array should be greater than a fake array", function () { + expect(Collection([10, 30]).compare(fakeArray)).toEqual(10); + }); + + it("an array should be less than a fake array because it is shorter but otherwise equal", function () { + expect(Collection([10, 20]).compare(fakeArray)).toEqual(-1); + }); + + it("an array should be less than a fake array because it is longer but otherwise equal", function () { + expect(Collection([10, 20, 30, 40]).compare(fakeArray)).toEqual(1); + }); + + }); + + describe("indexOf", function () { + if (!Collection.prototype.indexOf) + return; + + it("finds first value", function () { + var collection = Collection([1, 2, 3]); + expect(collection.indexOf(2)).toBe(1); + }); + + it("finds first identical value", function () { + if (Collection.prototype.isSet) + return; + var collection = Collection([1, 1, 2, 2, 3, 3]); + expect(collection.indexOf(2)).toBe(2); + }); + + it("finds first value after index", function () { + if (Collection.prototype.isSet || Collection.prototype.isSorted) + return; + var collection = Collection([1, 2, 3, 1, 2, 3]); + expect(collection.indexOf(2, 3)).toBe(4); + }); + + it("finds first value after negative index", function () { + if (Collection.prototype.isSet || Collection.prototype.isSorted) + return; + var collection = Collection([1, 2, 3, 1, 2, 3]); + expect(collection.indexOf(2, -3)).toBe(4); + }); + + }); + + describe("lastIndexOf", function () { + if (!Collection.prototype.lastIndexOf) + return; + + it("finds last value", function () { + var collection = Collection([1, 2, 3]); + expect(collection.lastIndexOf(2)).toBe(1); + }); + + it("finds last identical value", function () { + if (Collection.prototype.isSet) + return; + var collection = Collection([1, 1, 2, 2, 3, 3]); + expect(collection.lastIndexOf(2)).toBe(3); + }); + + it("finds the last value before index", function () { + if (Collection.prototype.isSet || Collection.prototype.isSorted) + return; + var collection = Collection([1, 2, 3, 1, 2, 3]); + expect(collection.lastIndexOf(2, 3)).toBe(1); + }); + + it("finds the last value before negative index", function () { + if (Collection.prototype.isSet || Collection.prototype.isSorted) + return; + var collection = Collection([1, 2, 3, 1, 2, 3]); + expect(collection.lastIndexOf(2, -3)).toBe(1); + }); + + }); + + describe("find (deprecated support)", function () { + + it("finds equivalent values", function () { + expect(Collection([10, 10, 10]).find(10)).toEqual(0); + }); + + it("finds equivalent values", function () { + expect(Collection([10, 10, 10]).find(10)).toEqual(0); + }); + + }); + + describe("findValue", function () { + + it("finds equivalent values", function () { + expect(Collection([10, 10, 10]).findValue(10)).toEqual(0); + }); + + it("finds equivalent values", function () { + expect(Collection([10, 10, 10]).findValue(10)).toEqual(0); + }); + + }); + + describe("findLast (deprecated support)", function () { + + it("finds equivalent values", function () { + expect(Collection([10, 10, 10]).findLast(10)).toEqual(2); + }); + }); + + describe("findLastValue", function () { + + it("finds equivalent values", function () { + expect(Collection([10, 10, 10]).findLastValue(10)).toEqual(2); + }); + + }); + + describe("has", function () { + + it("finds equivalent values", function () { + expect(Collection([10]).has(10)).toBe(true); + }); + + it("does not find absent values", function () { + expect(Collection([]).has(-1)).toBe(false); + }); + + }); + + describe("has", function () { + + it("finds a value", function () { + var collection = Collection([1, 2, 3]); + expect(collection.has(2)).toBe(true); + }); + + it("does not find an absent value", function () { + var collection = Collection([1, 2, 3]); + expect(collection.has(4)).toBe(false); + }); + + // TODO + // it("makes use of equality override", function () { + // var collection = Collection([1, 2, 3]); + // expect(collection.has(4, function (a, b) { + // return a - 1 === b; + // })).toBe(true); + // }); + + }); + + + describe("any", function () { + + var tests = [ + [[0, false], false], + [["0"], true], + [[{}], true], + [[{a: 10}], true], + [[0, 1, 0], true], + [[1, 1, 1], true], + [[true, true, true], true], + [[0, 0, 0, true], true], + [[], false], + [[false, false, false], false] + ]; + + tests.forEach(function (test) { + it(JSON.stringify(test[0]) + ".any() should be " + test[1], function () { + expect(Collection(test[0]).any()).toEqual(test[1]); + }); + }); + + }); + + describe("all", function () { + + var tests = [ + [[], true], + [[true], true], + [[1], true], + [[{}], true], + [[false, true, true, true], false] + ]; + + tests.forEach(function (test) { + it(JSON.stringify(test[0]) + ".all() should be " + test[1], function () { + expect(Collection(test[0]).all()).toEqual(test[1]); + }); + }); + + }); + + describe("min", function () { + + it("finds the minimum of numeric values", function () { + expect(Collection([1, 2, 3]).min()).toEqual(1); + }); + + }); + + describe("max", function () { + + it("finds the maximum of numeric values", function () { + expect(Collection([1, 2, 3]).max()).toEqual(3); + }); + + }); + + describe("sum", function () { + + it("computes the sum of numeric values", function () { + expect(Collection([1, 2, 3]).sum()).toEqual(6); + }); + + // sum has deprecated behaviors for implicit flattening and + // property path mapping, not tested here + + }); + + describe("average", function () { + + it("computes the arithmetic mean of values", function () { + expect(Collection([1, 2, 3]).average()).toEqual(2); + }); + + }); + + describe("flatten", function () { + + it("flattens an array one level", function () { + var collection = Collection([ + [[1, 2, 3], [4, 5, 6]], + Collection([[7, 8, 9], [10, 11, 12]]) + ]); + expect(collection.flatten()).toEqual([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12] + ]); + }); + + }); + + describe("one", function () { + + it("gets the first value", function () { + expect(Collection([0]).one()).toEqual(0); + }); + + it("throws if empty", function () { + expect(Collection([]).one()).toBe(undefined); + }); + + }); + + describe("only", function () { + + it("gets the first value", function () { + expect(Collection([0]).only()).toEqual(0); + }); + + it("is undefined if empty", function () { + expect(Collection([]).only()).toBeUndefined(); + }); + + it("is undefined if more than one value", function () { + expect(Collection([1, 2]).only()).toBeUndefined(); + }); + + }); + + describe("clone", function () { + + // should have been adequately covered by Object.clone tests + + it("should clone with indefinite depth", function () { + var collection = Collection([[[]]]); + var clone = collection.clone(); + expect(clone).toEqual(collection); + expect(clone).not.toBe(collection); + }); + + it("should clone with depth 0", function () { + var collection = Collection([]); + expect(collection.clone(0)).toBe(collection); + }); + + it("should clone with depth 1", function () { + var collection = [Collection({})]; + expect(collection.clone(1)).not.toBe(collection); + expect(collection.clone(1).one()).toBe(collection.one()); + }); + + it("should clone with depth 2", function () { + var collection = Collection([{a: 10}]); + expect(collection.clone(2)).not.toBe(collection); + expect(collection.clone(2).one()).not.toBe(collection.one()); + expect(collection.clone(2).one()).toEqual(collection.one()); + }); + + }); + +} + diff --git a/core/collections/test/spec/permute.js b/core/collections/test/spec/permute.js new file mode 100644 index 0000000000..0f7a06279a --- /dev/null +++ b/core/collections/test/spec/permute.js @@ -0,0 +1,18 @@ + +module.exports = permute; +function permute(values) { + if (values.length === 0) + return []; + if (values.length === 1) + return [values]; + var permutations = []; + for (var index = 0; index < values.length; index++) { + var tail = values.slice(); + var head = tail.splice(index, 1); + permute(tail).forEach(function (permutation) { + permutations.push(head.concat(permutation)); + }); + } + return permutations; +} + diff --git a/core/collections/test/spec/prng.js b/core/collections/test/spec/prng.js new file mode 100644 index 0000000000..3894340141 --- /dev/null +++ b/core/collections/test/spec/prng.js @@ -0,0 +1,9 @@ + +module.exports = prng; +function prng(seed) { + return function () { + seed = ((seed * 60271) + 70451) % 99991; + return seed / 99991; + } +} + diff --git a/core/collections/test/spec/regexp-spec.js b/core/collections/test/spec/regexp-spec.js new file mode 100644 index 0000000000..4c0c67fb0f --- /dev/null +++ b/core/collections/test/spec/regexp-spec.js @@ -0,0 +1,34 @@ + +require("collections/shim-regexp"); + +describe("RegExp-spec", function () { + describe("escape", function () { + + [ + "{", + "a-b", + "...", + "\\x", + "[a-b]", + "^foo$", + ".?", + "()", + "1..3", + "[^a-z]" + ].forEach(function (input) { + + it("should escape " + JSON.stringify(input), function () { + var re = new RegExp("^" + RegExp.escape(input) + "$"); + expect(re.test(input)).toBe(true); + }); + + it("should escape case-insensitively " + JSON.stringify(input), function () { + var re = new RegExp("^" + RegExp.escape(input) + "$", "i"); + expect(re.test(input.toUpperCase())).toBe(true); + }); + + }); + + }); +}); + diff --git a/core/collections/test/spec/set-spec.js b/core/collections/test/spec/set-spec.js new file mode 100644 index 0000000000..8fd41352af --- /dev/null +++ b/core/collections/test/spec/set-spec.js @@ -0,0 +1,258 @@ + +var Set = require("collections/set"); +var describeCollection = require("./collection"); +var describeSet = require("./set"); + +if (Set._setupCollectionSet) { + Set._setupCollectionSet(); +} +var CollectionsSet = Set.CollectionsSet || Set; + +describe("CollectionsSet-spec", function () { + var Set = CollectionsSet; + describeCollection(Set, [1, 2, 3, 4], true); + describeCollection(Set, [{id: 0}, {id: 1}, {id: 2}, {id: 3}], true); + describeSet(Set); + + it("should pop and shift", function () { + var a = {i: 2}; + var b = {i: 1}; + var c = {i: 0}; + var set = Set([a, b, c], Object.is); + expect(set.pop()).toBe(c); + expect(set.shift()).toBe(a); + }); + + it("should dispatch range change on clear", function () { + var set = Set([1, 2, 3]); + var spy = jasmine.createSpy(); + set.addRangeChangeListener(spy); + set.clear(); + expect(spy).toHaveBeenCalledWith([], [1, 2, 3], 0, set, undefined); + }); + + it("should dispatch range change on add", function () { + var set = Set([1, 3]); + var spy = jasmine.createSpy(); + set.addRangeChangeListener(spy); + set.add(2); + expect(set.toArray()).toEqual([1, 3, 2]); + expect(spy).toHaveBeenCalledWith([2], [], 2, set, undefined); + }); + + it("should dispatch range change on delete", function () { + var set = Set([1, 2, 3]); + var spy = jasmine.createSpy(); + set.addRangeChangeListener(spy); + set["delete"](2); + expect(set.toArray()).toEqual([1, 3]); + expect(spy).toHaveBeenCalledWith([], [2], 1, set, undefined); + }); + + it("should dispatch range change on pop", function () { + var set = Set([1, 3, 2]); + var spy = jasmine.createSpy(); + set.addRangeChangeListener(spy); + expect(set.pop()).toEqual(2); + expect(set.toArray()).toEqual([1, 3]); + expect(spy).toHaveBeenCalledWith([], [2], 2, set, undefined); + }); + + it("should dispatch range change on shift", function () { + var set = Set([1, 3, 2]); + var spy = jasmine.createSpy(); + set.addRangeChangeListener(spy); + expect(set.shift()).toEqual(1); + expect(set.toArray()).toEqual([3, 2]); + expect(spy).toHaveBeenCalledWith([], [1], 0, set, undefined); + }); + + it("should dispatch range change on shift then pop", function () { + var set = Set([1, 3]); + set.addRangeChangeListener(function (plus, minus, index) { + spy(plus, minus, index); // ignore all others + }); + + var spy = jasmine.createSpy(); + expect(set.add(2)).toEqual(true); + expect(set.toArray()).toEqual([1, 3, 2]); + expect(spy).toHaveBeenCalledWith([2], [], 2); + + var spy = jasmine.createSpy(); + expect(set.shift()).toEqual(1); + expect(set.toArray()).toEqual([3, 2]); + expect(spy).toHaveBeenCalledWith([], [1], 0); + + var spy = jasmine.createSpy(); + expect(set.pop()).toEqual(2); + expect(set.toArray()).toEqual([3]); + expect(spy).toHaveBeenCalledWith([], [2], 1); + + var spy = jasmine.createSpy(); + expect(set.delete(3)).toEqual(true); + expect(set.toArray()).toEqual([]); + expect(spy).toHaveBeenCalledWith([], [3], 0); + }); + +}); + +describe("Set-spec", function () { + describeCollection(Set, [1, 2, 3, 4], false); + describeCollection(Set, [{id: 0}, {id: 1}, {id: 2}, {id: 3}], false); + describeSet(Set); + + it("should pop and shift", function () { + var a = {i: 2}; + var b = {i: 1}; + var c = {i: 0}; + var set = Set.from([a, b, c]); + expect(set.pop()).toBe(c); + expect(set.shift()).toBe(a); + }); + + it("should dispatch range change on clear", function () { + var set = Set.from([1, 2, 3]); + var spy = jasmine.createSpy(); + set.addRangeChangeListener(spy); + set.clear(); + expect(spy).toHaveBeenCalledWith([], [1, 2, 3], 0, set, undefined); + }); + + it("should dispatch range change on add", function () { + var set = Set.from([1, 3]); + var spy = jasmine.createSpy(); + set.addRangeChangeListener(spy); + set.add(2); + expect(set.toArray()).toEqual([1, 3, 2]); + expect(spy).toHaveBeenCalledWith([2], [], 2, set, undefined); + }); + + it("should dispatch range change on delete", function () { + var set = Set.from([1, 2, 3]); + var spy = jasmine.createSpy(); + set.addRangeChangeListener(spy); + set["delete"](2); + expect(set.toArray()).toEqual([1, 3]); + expect(spy).toHaveBeenCalledWith([], [2], 1, set, undefined); + }); + + it("should dispatch range change on pop", function () { + var set = Set.from([1, 3, 2]); + var spy = jasmine.createSpy(); + set.addRangeChangeListener(spy); + expect(set.pop()).toEqual(2); + expect(set.toArray()).toEqual([1, 3]); + expect(spy).toHaveBeenCalledWith([], [2], 2, set, undefined); + }); + + it("should dispatch range change on shift", function () { + var set = Set.from([1, 3, 2]); + var spy = jasmine.createSpy(); + set.addRangeChangeListener(spy); + expect(set.shift()).toEqual(1); + expect(set.toArray()).toEqual([3, 2]); + expect(spy).toHaveBeenCalledWith([], [1], 0, set, undefined); + }); + + it("should dispatch range change on shift then pop", function () { + var set = Set.from([1, 3]); + set.addRangeChangeListener(function (plus, minus, index) { + spy(plus, minus, index); // ignore all others + }); + + var spy = jasmine.createSpy(); + expect(set.add(2)).toEqual(true); + expect(set.toArray()).toEqual([1, 3, 2]); + expect(spy).toHaveBeenCalledWith([2], [], 2); + + var spy = jasmine.createSpy(); + expect(set.shift()).toEqual(1); + expect(set.toArray()).toEqual([3, 2]); + expect(spy).toHaveBeenCalledWith([], [1], 0); + + var spy = jasmine.createSpy(); + expect(set.pop()).toEqual(2); + expect(set.toArray()).toEqual([3]); + expect(spy).toHaveBeenCalledWith([], [2], 1); + + var spy = jasmine.createSpy(); + expect(set.delete(3)).toEqual(true); + expect(set.toArray()).toEqual([]); + expect(spy).toHaveBeenCalledWith([], [3], 0); + }); + + it("should dispatch size property change on clear", function () { + var set = new Set([1, 2, 3]); + // var set = Set.from([1, 2, 3]); + var spy = jasmine.createSpy(); + set.addBeforeOwnPropertyChangeListener("size", function (size) { + spy("size change from", size); + }); + + set.addOwnPropertyChangeListener("size", function (size) { + spy("size change to", size); + }); + + expect(set.size).toBe(3); + expect(set.has(1)).toBe(true); + expect(set.has(2)).toBe(true); + expect(set.has(3)).toBe(true); + set.clear(); + expect(set.size).toBe(0); + expect(set.has(1)).toBe(false); + expect(set.has(2)).toBe(false); + expect(set.has(3)).toBe(false); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["size change from", 3], + ["size change to", 0] + ]); + }); + + it("should dispatch size property change on add", function () { + var set = new Set(); + var spy = jasmine.createSpy(); + set.addBeforeOwnPropertyChangeListener("size", function (size) { + spy("size change from", size); + }); + + set.addOwnPropertyChangeListener("size", function (size) { + spy("size change to", size); + }); + + set.add(10); + set.add(20); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["size change from", 0], + ["size change to", 1], + ["size change from", 1], + ["size change to", 2], + ]); + }); + + it("should dispatch size property change on delete", function () { + var set = new Set([1, 2, 3]); + var spy = jasmine.createSpy(); + set.addBeforeOwnPropertyChangeListener("size", function (size) { + spy("size change from", size); + }); + + set.addOwnPropertyChangeListener("size", function (size) { + spy("size change to", size); + }); + + set.delete(2); + set.delete(1); + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["size change from", 3], + ["size change to", 2], + ["size change from", 2], + ["size change to", 1], + ]); + }); + +}); diff --git a/core/collections/test/spec/set.js b/core/collections/test/spec/set.js new file mode 100644 index 0000000000..da84fd7720 --- /dev/null +++ b/core/collections/test/spec/set.js @@ -0,0 +1,134 @@ + +var Iterator = require("collections/iterator"); + +module.exports = describeSet; +function describeSet(Set, sorted) { + + it("uniqueness", function () { + var set = Set.from([1, 2, 3, 1, 2, 3]); + expect(set.toArray().sort()).toEqual([1, 2, 3]); + }); + + it("the callback should receive value, value, set", function () { + var set = Set.from([1, 2, 3]); + var other = Set.from([]); + var i = 1; + set.forEach(function (value, key, object) { + expect(key).toBe(value); + i++; + other.add(value); + expect(object).toBe(set); + }); + expect(other.length).toBe(3); + expect(other.union(set).length).toBe(3); + expect(other.intersection(set).length).toBe(3); + expect(other.difference(set).length).toBe(0); + }); + + it("should be initially empty", function () { + expect(new Set().length).toBe(0); + }); + + it("cleared set should be empty", function () { + var set = Set.from([1, 2]); + expect(set.length).toBe(2); + set.delete(1); + expect(set.length).toBe(1); + set.clear(); + expect(set.length).toBe(0); + }); + + it("can add and delete an object", function () { + var set = new Set(); + var object = {}; + set.add(object); + expect(set.has(object)).toBe(true); + set.delete(object); + expect(set.length).toBe(0); + expect(set.has(object)).toBe(false); + }); + + it("can deleteAll", function () { + var set = Set.from([0]); + expect(set.deleteAll(0)).toBe(1); + expect(set.deleteAll(0)).toBe(0); + }); + + if (!sorted) { + it("can add and delete objects from the same bucket", function () { + var a = {id: 0}, b = {id: 1}; + var set = new Set(); + set.add(a); + expect(set.has(a)).toBe(true); + set.add(b); + expect(set.has(b)).toBe(true); + set.delete(b); + expect(set.has(b)).toBe(false); + expect(set.has(a)).toBe(true); + set.delete(a); + expect(set.has(a)).toBe(false); + }); + } + + it("can readd a deleted object", function () { + var set = new Set(); + var object = {}; + set.add(object); + expect(set.has(object)).toBe(true); + set.add(object); + expect(set.length).toBe(1); + set.delete(object); + expect(set.length).toBe(0); + expect(set.has(object)).toBe(false); + set.add(object); + expect(set.length).toBe(1); + expect(set.has(object)).toBe(true); + }); + + it("can be changed to an array", function () { + var set = Set.from([0]); + expect(set.toArray()).toEqual([0]); + }); + + it("is a reducible", function () { + var set = Set.from([1, 1, 1, 2, 2, 2, 1, 2]); + expect(set.length).toBe(2); + expect(set.min()).toBe(1); + expect(set.max()).toBe(2); + expect(set.sum()).toBe(3); + expect(set.average()).toBe(1.5); + expect(set.map(function (n) { + return n + 1; + }).indexOf(3)).not.toBe(-1); + }); + + it("is iterable", function () { + var set = Set.from(['c', 'b', 'a']); + var valuesArray = set.valuesArray(); + expect(valuesArray.sort()).toEqual(['a', 'b', 'c']); + }); + + it("is concatenatable", function () { + var array = Set.from([3, 2, 1]).concat([4, 5, 6]).toArray(); + array.sort(); + expect(array).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it("should compute unions", function () { + expect(Set.from([1, 2, 3]).union([2, 3, 4]).sorted()).toEqual([1, 2, 3, 4]); + expect(Set.from([1, 2, 3]).union([2, 3, 4]).equals([1, 2, 3, 4])).toBe(true); + }); + + it("should compute intersections", function () { + expect(Set.from([1, 2, 3]).intersection([2, 3, 4]).sorted()).toEqual([2, 3]); + }); + + it("should compute differences", function () { + expect(Set.from([1, 2, 3]).difference([2, 3, 4]).sorted()).toEqual([1]); + }); + + it("should compute symmetric differences", function () { + expect(Set.from([1, 2, 3]).symmetricDifference([2, 3, 4]).sorted()).toEqual([1, 4]); + }); + +} diff --git a/core/collections/test/spec/shim-array-spec.js b/core/collections/test/spec/shim-array-spec.js new file mode 100644 index 0000000000..cfb1f96758 --- /dev/null +++ b/core/collections/test/spec/shim-array-spec.js @@ -0,0 +1,51 @@ + +require("collections/shim-array"); + +describe("ArrayShim-spec", function () { + + describe("clone", function () { + + it("clones", function () { + expect([1].clone()).toEqual([1]); + }); + + it("clones deeply", function () { + var array = [[1], [2], [3], { + a: 10, + b: 20, + c: [1, 2, 3] + }]; + expect(array.clone()).toEqual(array); + }); + + it("clones cycles", function () { + var array = []; + array[0] = array; + expect(array.clone()).toEqual(array); + }); + + it("clones sparse arrays", function () { + expect([,,].clone()).toEqual([,,]); + }); + + it("clones sparse arrays quickly", function () { + var start = Date.now(); + new Array(Math.pow(2, 30)).clone(); + expect(Date.now() - start < 100).toBe(true); + }); + + it("spliceOne remove", function () { + var array = [1, 2, 3]; + array.spliceOne(1); + expect(array).toEqual([1, 3]); + }); + + it("spliceOne add", function () { + var array = [1, 2, 3]; + array.spliceOne(1, 2.5); + expect(array).toEqual([1, 2.5, 3]); + }); + + }); + +}); diff --git a/core/collections/test/spec/shim-functions-spec.js b/core/collections/test/spec/shim-functions-spec.js new file mode 100644 index 0000000000..8d9c92ad9d --- /dev/null +++ b/core/collections/test/spec/shim-functions-spec.js @@ -0,0 +1,49 @@ + +require("collections/shim-object"); +require("collections/shim-function"); + +describe("FunctionShim-spec", function () { + + describe("identity", function () { + + it("should return the first argument", function () { + expect(Function.identity(1, 2, 3)).toBe(1); + }); + + }); + + describe("noop", function () { + + // should do nothing (not verifiable) + + it("should return nothing", function () { + expect(Function.noop(1, 2, 3)).toBe(undefined); + }); + + }); + + describe("by", function () { + var getA = function (x) { + return x.a; + }; + var wrappedCompare = function (a, b) { + return Object.compare(a, b); + }; + var compare = Function.by(getA, wrappedCompare); + + it("should compare two values", function () { + expect(compare({a: 10}, {a: 20})).toBe(-10); + }); + + it("should have a by property", function () { + expect(compare.by).toBe(getA); + }); + + it("should have a compare property", function () { + expect(compare.compare).toBe(wrappedCompare); + }); + + }); + +}); + diff --git a/core/collections/test/spec/shim-object-spec.js b/core/collections/test/spec/shim-object-spec.js new file mode 100644 index 0000000000..89b0ab9152 --- /dev/null +++ b/core/collections/test/spec/shim-object-spec.js @@ -0,0 +1,593 @@ +"use strict"; + +/* + Based in part on extras from Motorola Mobility’s Montage + Copyright (c) 2012, Motorola Mobility LLC. All Rights Reserved. + 3-Clause BSD License + https://github.com/motorola-mobility/montage/blob/master/LICENSE.md +*/ + +require("collections/shim"); +var Dict = require("collections/dict"); +var Set = require("collections/set"); + +describe("ObjectShim-spec", function () { + + it("should have no enumerable properties", function () { + expect(Object.keys(Object.prototype)).toEqual([]); + }); + + describe("empty", function () { + + it("should own no properties", function () { + expect(Object.getOwnPropertyNames(Object.empty)).toEqual([]); + expect(Object.keys(Object.empty)).toEqual([]); + }); + + it("should have no prototype", function () { + expect(Object.getPrototypeOf(Object.empty)).toBe(null); + }); + + it("should be immutable", function () { + "strict mode"; + expect(function () { + Object.empty.a = 10; // should throw an error in strict mode + if (Object.empty.a !== 10) { + throw new Error("Unchanged"); + } + }).toThrow(); + }); + + }); + + describe("isObject", function () { + + [ + ["null is not an object", null, false], + ["numbers are not objects", 1, false], + ["undefined is not an object", undefined, false], + ["arrays are objects", [], true], + ["object literals are objects", {}, true], + [ + "pure objects (null prototype) are", + Object.create(null), + true + ] + ].forEach(function (test) { + it("should recognize that " + test[0], function () { + expect(Object.isObject(test[1])).toEqual(test[2]); + }); + }); + + }); + + describe("getValueOf", function () { + var fakeNumber = Object.create({ + valueOf: function () { + return 10; + } + }); + + var object = {valueOf: 10}; + var tests = [ + [10, 10, "number"], + [object, object, "object (identical, with misleading owned property)"], + [new Number(20), 20, "boxed number"], + [fakeNumber, 10, "fake number"] + ]; + + tests.forEach(function (test) { + it(test[2], function () { + expect(Object.getValueOf(test[0])).toBe(test[1]); + }); + }); + + }); + + describe("owns", function () { + + it("should recognized an owned property", function () { + expect(Object.owns({a: 0}, "a")).toEqual(true); + }); + + it("should distinguish an inherited property", function () { + expect(Object.owns(Object.prototype, "toString")).toEqual(true); + }); + + }); + + describe("has", function () { + + it("should recognized an owned property", function () { + expect(Object.has({toString: true}, "toString")).toEqual(true); + }); + + it("should recognize an inherited propertry", function () { + var parent = {"a": 10}; + var child = Object.create(parent); + expect(Object.has(child, "a")).toEqual(true); + }); + + it("should distinguish a property from the Object prototype", function () { + expect(Object.has({}, "toString")).toEqual(false); + }); + + it("should recognize a property on a null prototype chain", function () { + var parent = Object.create(null); + parent.a = 10; + var child = Object.create(parent); + expect(Object.has(child, "a")).toEqual(true); + }); + + it("should recognize a falsy property", function () { + expect(Object.has({a:0}, "a")).toEqual(true); + }); + + it("should throw an error if the first argument is not an object", function () { + expect(function () { + Object.has(10, 10); + }).toThrow(); + }); + + it("should delegate to a prototype method", function () { + var Type = Object.create(Object.prototype, { + has: { + value: function (key) { + return key === "a"; + } + } + }); + var instance = Object.create(Type); + expect(Object.has(instance, "a")).toEqual(true); + expect(Object.has(instance, "toString")).toEqual(false); + }); + + it("should delegate to a set", function () { + var set = new Set([1, 2, 3]); + expect(Object.has(set, 2)).toEqual(true); + expect(Object.has(set, 4)).toEqual(false); + expect(Object.has(set, "toString")).toEqual(false); + }); + + }); + + describe("get", function () { + + it("should get an owned property from an object literal", function () { + expect(Object.get({a: 10}, "a")).toEqual(10); + }); + + it("should not get a property from the Object prototype on a literal", function () { + expect(Object.get({}, "toString")).toEqual(undefined); + }); + + it("should delegate to a prototype method", function () { + var Type = Object.create(Object.prototype, { + get: { + value: function (key) { + if (key === "a") + return 10; + } + } + }); + var instance = Object.create(Type); + expect(Object.get(instance, "a")).toEqual(10); + }); + + it("should not delegate to an owned 'get' method", function () { + expect(Object.get({get: 10}, "get")).toEqual(10); + }); + + it("should fallback to a default argument if defined", function () { + expect(Object.get({}, "toString", 10)).toEqual(10); + }); + + }); + + describe("set", function () { + + it("should set a property", function () { + var object = {}; + Object.set(object, "set", 10); + expect(Object.get(object, "set")).toEqual(10); + }); + + it("should delegate to a 'set' method", function () { + var spy = jasmine.createSpy(); + var Type = Object.create(Object.prototype, { + set: { + value: spy + } + }); + var instance = Object.create(Type); + Object.set(instance, "a", 10); + + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + ["a", 10] + ]); + }); + + }); + + describe("forEach", function () { + + it("should iterate the owned properties of an object", function () { + var spy = jasmine.createSpy(); + var object = {a: 10, b: 20, c: 30}; + Object.forEach(object, spy); + var argsForCall = spy.calls.all().map(function (call) { return call.args }); + expect(argsForCall).toEqual([ + [10, "a", object], + [20, "b", object], + [30, "c", object] + ]); + }); + + it("should pass a thisp into the callback", function () { + var thisp = {}; + Object.forEach([1], function (value, key, object) { + expect(this).toBe(thisp); + expect(value).toEqual(1); + expect(key).toEqual("0"); + expect(object).toEqual([1]); + thisp = null; + }, thisp); + expect(thisp).toEqual(null); + }); + + }); + + describe("map", function () { + + it("should iterate the owned properties of an object with a context thisp", function () { + var object = {a: 10, b: 20} + var result = Object.map(object, function (value, key, o) { + expect(o).toBe(object); + return key + this + value; + }, ": ").join(", "); + expect(result).toEqual("a: 10, b: 20"); + }); + + }); + + describe("values", function () { + + it("should produce the values for owned properties", function () { + expect(Object.values({b: 10, a: 20})).toEqual([10, 20]); + }); + + }); + + describe("concat", function () { + + it("should merge objects into a new object", function () { + expect(Object.concat({a: 10}, {b: 20})).toEqual({a: 10, b: 20}); + }); + + it("should prioritize latter objects", function () { + expect(Object.concat({a: 10}, {a: 20})).toEqual({a: 20}); + }); + + it("should delegate to arrays", function () { + expect(Object.concat({a: 10, b: 20}, [['c', 30]])).toEqual({a: 10, b: 20, c: 30}); + }); + + it("should delegate to maps", function () { + expect(Object.concat({a: 10, b: 20}, Dict({c: 30}))).toEqual({a: 10, b: 20, c: 30}); + }); + + }); + + describe("is", function () { + + var distinctValues = { + 'positive zero': 0, + 'negative zero': -0, + 'positive infinity': 1/0, + 'negative infinity': -1/0, + 'one': 1, + 'two': 2, + 'NaN': NaN, + 'objects': {}, + 'other objects': {} + }; + + Object.forEach(distinctValues, function (a, ai) { + Object.forEach(distinctValues, function (b, bi) { + if (ai < bi) + return; + var operation = ai === bi ? "recognizes" : "distinguishes"; + it(operation + " " + ai + " and " + bi, function () { + expect(Object.is(a, b)).toEqual(ai === bi); + }); + }); + }); + + }); + + describe("equals", function () { + var fakeNumber = { + valueOf: function () { + return 10; + } + }; + var equatable = { + value: 10, + clone: function () { + return this; + }, + equals: function (n) { + return n === 10 || typeof n === "object" && n !== null && n.value === 10; + } + }; + + var equivalenceClasses = [ + { + 'unboxed number': 10, + 'boxed number': new Number(10), + 'faked number': fakeNumber, + 'equatable': equatable + }, + { + 'array': [10], + 'other array': [10] + }, + { + 'nested array': [[10, 20], [30, 40]] + }, + { + 'object': {a: 10}, + 'other object': {a: 10} + }, + { + 'now': new Date() + }, + { + 'NaN': NaN + }, + { + 'undefined': undefined + }, + { + 'null': null + } + ]; + + // positives: + // everything should be equal to every other thing in + // its equivalence class + equivalenceClasses.forEach(function (equivalenceClass) { + Object.forEach(equivalenceClass, function (a, ai) { + equivalenceClass[ai + " clone"] = Object.clone(a); + }); + // within each pair of class, test exhaustive combinations to cover + // the commutative property + Object.forEach(equivalenceClass, function (a, ai) { + Object.forEach(equivalenceClass, function (b, bi) { + it(": " + ai + " equals " + bi, function () { + expect(Object.equals(a, b)).toBe(true); + }); + }); + }); + }); + + // negatives + // everything from one equivalence class should not equal + // any other thing from a different equivalence class + equivalenceClasses.forEach(function (aClass, aClassIndex) { + equivalenceClasses.forEach(function (bClass, bClassIndex) { + // only compare each respective class against another once (>), + // and not for equivalence classes to themselves (==). + // This cuts the bottom right triangle below the diagonal out + // of the test matrix of equivalence classes. + if (aClassIndex >= bClassIndex) + return; + // but within each pair of classes, test exhaustive + // combinations to cover the commutative property + Object.forEach(aClass, function (a, ai) { + Object.forEach(bClass, function (b, bi) { + it(ai + " not equals " + bi, function () { + expect(Object.equals(a, b)).toBe(false); + }); + }); + }); + }); + }); + + }); + + describe("compare", function () { + + var fakeOne = Object.create({ + valueOf: function () { + return 1; + } + }); + + var comparable = Object.create({ + create: function (compare) { + var self = Object.create(this); + self._compare = compare; + return self; + }, + compare: function (other) { + return this._compare(other); + } + }); + + var now = new Date(); + + var tests = [ + [0, 0, 0], + [0, 1, -1], + [1, 0, 1], + [[10], [10], 0], + [[10], [20], -10], + [[100, 10], [100, 0], 10], + ["a", "b", -Infinity], + [now, now, 0, "now to itself"], + [ + comparable.create(function () { + return -1; + }), + null, + -1, + "comparable" + ], + [ + null, + comparable.create(function () { + return 1; + }), + -1, + "opposite comparable" + ], + [{b: 10}, {a: 0}, 0, "incomparable to another"], + [new Number(-10), 20, -30, "boxed number to real number"], + [fakeOne, 0, 1, "fake number to real number"] + ]; + + tests.forEach(function (test) { + it( + test[3] || + ( + JSON.stringify(test[0]) + " to " + + JSON.stringify(test[1]) + ), + function () { + expect(Object.compare(test[0], test[1])).toEqual(test[2]); + } + ); + }); + + }); + + describe("clone", function () { + + var graph = { + object: {a: 10}, + array: [1, 2, 3], + string: "hello", + number: 10, + nestedObject: { + a: {a1: 10, a2: 20}, + b: {b1: "a", b2: "c"} + }, + nestedArray: [ + [1, 2, 3], + [4, 5, 6] + ], + mixedObject: { + array: [1, 3, 4], + object: {a: 10, b: 20} + }, + mixedArray: [ + [], + {a: 10, b: 20} + ], + arrayWithHoles: [], + clonable: Object.create({ + clone: function () { + return this; + } + }) + } + + graph.cycle = graph; + graph.arrayWithHoles[10] = 10; + + graph.typedObject = Object.create(null); + graph.typedObject.a = 10; + graph.typedObject.b = 10; + + Object.forEach(graph, function (value, name) { + it(name + " cloned equals self", function () { + expect(Object.clone(value)).toEqual(value); + }); + }); + + it("should clone zero levels of depth", function () { + var clone = Object.clone(graph, 0); + expect(clone).toBe(graph); + }); + + it("should clone object at one level of depth", function () { + var clone = Object.clone(graph, 1); + expect(clone).toEqual(graph); + expect(clone).not.toBe(graph); + }); + + it("should clone object at two levels of depth", function () { + var clone = Object.clone(graph, 2); + expect(clone).toEqual(graph); + expect(clone.object).not.toBe(graph.object); + expect(clone.object).toEqual(graph.object); + expect(clone.nestedObject.a).toBe(graph.nestedObject.a); + }); + + it("should clone array at two levels of depth", function () { + var clone = Object.clone(graph, 2); + expect(clone).toEqual(graph); + expect(clone.array).not.toBe(graph.array); + expect(clone.array).toEqual(graph.array); + }); + + it("should clone identical values at least once", function () { + var clone = Object.clone(graph); + expect(clone.cycle).not.toBe(graph.cycle); + }); + + it("should clone identical values only once", function () { + var clone = Object.clone(graph); + expect(clone.cycle).toBe(clone); + }); + + it("should clone clonable", function () { + var clone = Object.clone(graph); + expect(clone.clonable).toBe(graph.clonable); + }); + + }); + + describe("clone", function () { + var object = {a: {a1: 10, a2: 20}, b: {b1: 10, b2: 20}}; + + it("should clone zero levels", function () { + expect(Object.clone(object, 0)).toBe(object); + }); + + it("should clone one level", function () { + var clone = Object.clone(object, 1); + expect(clone).toEqual(object); + expect(clone).not.toBe(object); + expect(clone.a).toBe(object.a); + }); + + it("should clone two levels", function () { + var clone = Object.clone(object, 2); + expect(clone).toEqual(object); + expect(clone).not.toBe(object); + expect(clone.a).not.toBe(object.a); + }); + + it("should clone with reference cycles", function () { + var cycle = {}; + cycle.cycle = cycle; + var clone = Object.clone(cycle); + expect(clone).toEqual(cycle); + expect(clone).not.toBe(cycle); + expect(clone.cycle).toBe(clone); + }); + + }); + + describe("clear", function () { + + it("should clear all owned properties of the object", function () { + expect(Object.keys(Object.clear({a: 10}))).toEqual([]); + }); + + }); + +}); diff --git a/core/collections/test/spec/sorted-array-map-spec.js b/core/collections/test/spec/sorted-array-map-spec.js new file mode 100644 index 0000000000..9d5897e317 --- /dev/null +++ b/core/collections/test/spec/sorted-array-map-spec.js @@ -0,0 +1,14 @@ + +var SortedArrayMap = require("collections/sorted-array-map"); +var describeDict = require("./dict"); +var describeMap = require("./map"); +var describeMapChanges = require("./listen/map-changes"); +var describeToJson = require("./to-json"); + +describe("SortedArrayMap-spec", function () { + describeDict(SortedArrayMap); + describeMap(SortedArrayMap, [1, 2, 3]); + describeMapChanges(SortedArrayMap); + describeToJson(SortedArrayMap, [[1, 10], [2, 20], [3, 30]]); +}); + diff --git a/core/collections/test/spec/sorted-array-set-spec.js b/core/collections/test/spec/sorted-array-set-spec.js new file mode 100644 index 0000000000..a998373435 --- /dev/null +++ b/core/collections/test/spec/sorted-array-set-spec.js @@ -0,0 +1,20 @@ + +var SortedArraySet = require("collections/sorted-array-set"); +var describeDeque = require("./deque"); +var describeCollection = require("./collection"); +var describeSet = require("./set"); +var describeToJson = require("./to-json"); + +describe("SortedArraySet-spec", function () { + + describeDeque(SortedArraySet); + describeCollection(SortedArraySet, [1, 2, 3, 4]); + describeSet(SortedArraySet); + describeToJson(SortedArraySet, [1, 2, 3, 4]); + + it("uniqueness", function () { + var set = SortedArraySet([1, 2, 3, 1, 2, 3]); + expect(set.slice()).toEqual([1, 2, 3]); + }); + +}); diff --git a/core/collections/test/spec/sorted-array-spec.js b/core/collections/test/spec/sorted-array-spec.js new file mode 100644 index 0000000000..b87c4677b0 --- /dev/null +++ b/core/collections/test/spec/sorted-array-spec.js @@ -0,0 +1,95 @@ + +var SortedArray = require("collections/sorted-array"); +var describeCollection = require("./collection"); +var describeDeque = require("./deque"); +var describeOrder = require("./order"); +var describeToJson = require("./to-json"); + +describe("SortedArray-spec", function () { + + + describeDeque(SortedArray); + describeCollection(SortedArray, [1, 2, 3, 4]); + describeOrder(SortedArray); + describeToJson(SortedArray, [1, 2, 3, 4]); + + describe("non-uniqueness", function () { + it("should retain non-unique values", function () { + var array = SortedArray([1, 2, 3, 1, 2, 3]); + expect(array.slice()).toEqual([1, 1, 2, 2, 3, 3]); + }); + }); + + describe("deleteAll", function () { + it("should delete a range of equivalent values", function () { + var array = SortedArray([1, 1, 1, 2, 2, 2, 3, 3, 3]); + expect(array.deleteAll(2)).toBe(3); + expect(array.toArray()).toEqual([1, 1, 1, 3, 3, 3]); + }); + it("deletes all equivalent values for an alternate relation", function () { + var equivalent = function (a, b) { + return a % 2 === b % 2; + }; + var collection = SortedArray([1, 2, 3, 4, 5]); + expect(collection.deleteAll(2, equivalent)).toBe(2); + expect(collection.toArray()).toEqual([1, 3, 5]); + expect(collection.length).toBe(3); + }); + }); + + describe("incomparable values", function () { + function customEquals(one, two) { + return one.id === two.id; + } + + function customCompare(left, right) { + if (left.position < right.position) { + return -1; + } + if (left.position > right.position) { + return 1; + } + return 0; + } + + var a1 = {id: 'A', position: 1}; + var b1 = {id: 'B', position: 1}; + var c1 = {id: 'C', position: 1}; + + function createCustomArray(backingArray) { + // The ordering of incomparable elements is undefined. + // To control the underlying array it's set directly here. + var array = new SortedArray([], customEquals, customCompare); + array.array = backingArray; + return array; + } + + it("should find the correct incomparable value in a streak", function () { + var array = createCustomArray([a1, b1, c1]); + expect(array.indexOf(a1)).toEqual(0); + expect(array.indexOf(b1)).toEqual(1); + expect(array.indexOf(c1)).toEqual(2); + }); + + it("should respect search direction", function () { + var array = createCustomArray([a1, a1, a1]); + expect(array.indexOf(a1)).toEqual(0); + expect(array.lastIndexOf(a1)).toEqual(2); + }); + + it("should work regardless of array size", function () { + var array = createCustomArray([]); + expect(array.indexOf(a1)).toEqual(-1); + + array = createCustomArray([a1]); + expect(array.indexOf(a1)).toEqual(0); + + array = createCustomArray([a1, b1]); + expect(array.indexOf(a1)).toEqual(0); + expect(array.indexOf(b1)).toEqual(1); + }); + }); + + // TODO test stability + +}); diff --git a/core/collections/test/spec/sorted-map-spec.js b/core/collections/test/spec/sorted-map-spec.js new file mode 100644 index 0000000000..860f50fbaf --- /dev/null +++ b/core/collections/test/spec/sorted-map-spec.js @@ -0,0 +1,43 @@ + +var SortedMap = require("collections/sorted-map"); +var describeDict = require("./dict"); +var describeToJson = require("./to-json"); + +describe("SortedMap-spec", function () { + describeDict(SortedMap); + describeToJson(SortedMap, [[1, 10], [2, 20], [3, 30]]); + + it("should reduceRight", function () { + var map = SortedMap([ + [1, 2], + [2, 4], + [3, 6], + [4, 8] + ]); + expect(map.reduceRight(function (valid, value, key) { + return valid && key * 2 == value; + }, true)).toBe(true); + }); + + it("should iterate", function () { + var map = SortedMap([ + [1, 2], + [2, 4], + [3, 6], + [4, 8] + ]); + var iterator = map.iterator(); + var a = iterator.next().value, + b = iterator.next().value, + c = iterator.next().value, + d = iterator.next().value; + expect(a.key).toEqual(1); + expect(a.value).toEqual(2); + expect(b.key).toEqual(2); + expect(b.value).toEqual(4); + expect(c.key).toEqual(3); + expect(c.value).toEqual(6); + expect(d.key).toEqual(4); + expect(d.value).toEqual(8); + }); +}); diff --git a/core/collections/test/spec/sorted-set-spec.js b/core/collections/test/spec/sorted-set-spec.js new file mode 100644 index 0000000000..7f974dde53 --- /dev/null +++ b/core/collections/test/spec/sorted-set-spec.js @@ -0,0 +1,457 @@ + +require("collections/shim-array"); +var SortedSet = require("collections/sorted-set"); +var TreeLog = require("collections/tree-log"); +var describeDeque = require("./deque"); +var describeCollection = require("./collection"); +var describeSet = require("./set"); +var describeToJson = require("./to-json"); +var Fuzz = require("./fuzz"); + +describe("SortedSet-spec", function () { + + + // TODO SortedSet compare and equals argument overrides + + // construction, has, add, get, delete + describeCollection(SortedSet, [1, 2, 3, 4], true); + + // comparable objects + function Value(value) { + this.value = value; + } + Value.prototype.compare = function (that) { + return Object.compare(this.value, that.value); + } + var a = new Value(1); + var b = new Value(2); + var c = new Value(3); + var d = new Value(4); + var values = [a, b, c, d]; + describeCollection(SortedSet, values, true); + + // Happens to qualify as a deque, since the tests keep the content in + // sorted order. SortedSet has meaningful pop and shift operations, but + // push and unshift just add the arguments into their proper sorted + // positions rather than the ends. + describeDeque(SortedSet); + + describeSet(SortedSet, "sorted"); + describeToJson(SortedSet, [1, 2, 3, 4]); + + describe("splay", function () { + + function draw(set) { + var lines = []; + set.log(TreeLog.ascii, null, lines.push, lines); + return lines; + } + + it("should degenerate for sorted values", function () { + var set = SortedSet([1, 2, 3]); + expect(draw(set)).toEqual([ + " .-- 1", + ".-+ 2", + "+ 3" + ]); + }); + + it("should splay middle value", function () { + var set = SortedSet([1, 2, 3]); + set.get(2); + expect(draw(set)).toEqual([ + ".-- 1", + "+ 2", + "'-- 3" + ]); + }); + + it("should splay middle value", function () { + var set = SortedSet([1, 2, 3]); + set.get(2); + set.delete(1); + expect(draw(set)).toEqual([ + "+ 2", + "'-- 3" + ]); + }); + + }); + + describe("subtree lengths", function () { + + function draw(set) { + var lines = []; + set.log(TreeLog.ascii, function (node, write, writeAbove) { + write(" " + node.value + " length=" + node.length); + }, lines.push, lines); + return lines; + } + + function expectNodeToHaveCorrectSubtreeLengths(node) { + if (!node) + return 0; + var length = 1; + length += expectNodeToHaveCorrectSubtreeLengths(node.left); + length += expectNodeToHaveCorrectSubtreeLengths(node.right); + expect(node.length).toBe(length); + return length; + } + + it("+1", function () { + var set = SortedSet([1]); + expect(draw(set)).toEqual([ + "- 1 length=1" + ]); + expectNodeToHaveCorrectSubtreeLengths(set.root); + }); + + it("+1, +2", function () { + var set = SortedSet([1, 2]); + expect(draw(set)).toEqual([ + ".-- 1 length=1", + "+ 2 length=2" + ]); + expectNodeToHaveCorrectSubtreeLengths(set.root); + }); + + it("+1, +2, 1", function () { + var set = SortedSet([1, 2]); + set.get(1); + expect(draw(set)).toEqual([ + "+ 1 length=2", + "'-- 2 length=1" + ]); + expectNodeToHaveCorrectSubtreeLengths(set.root); + }); + + it("+1, +3, +2", function () { + var set = SortedSet([1, 3, 2]); + expect(draw(set)).toEqual([ + ".-- 1 length=1", + "+ 2 length=3", + "'-- 3 length=1" + ]); + expectNodeToHaveCorrectSubtreeLengths(set.root); + }); + + function makeCase(description, operations, log) { + it(description + " " + operations, function () { + var set = SortedSet(); + Fuzz.execute(set, Fuzz.parse(operations), log); + expectNodeToHaveCorrectSubtreeLengths(set.root); + }); + } + + makeCase("reduction of case with propagation issue", "+2, +4, +3, +1, 4"); + + // 50 fuzz cases + for (var i = 0; i < 50; i++) { + (function () { + var fuzz = Fuzz.make(i * 5, i, Math.max(10, i * 5)); + makeCase("fuzz", Fuzz.stringify(fuzz)); + })(); + } + + }); + + describe("splayIndex", function () { + it("should find the index of every element", function () { + var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + var rand = Fuzz.makeRandom(0); + numbers.sort(function () { + return rand() - .5; + }); + var set = SortedSet(numbers); + numbers.forEach(function (index) { + set.splayIndex(index); + expect(set.root.index).toBe(index); + expect(set.root.value).toBe(index); + }); + }); + }); + + describe("indexOf", function () { + // fuzz cases + for (var seed = 0; seed < 20; seed++) { + (function (seed) { + var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + var rand = Fuzz.makeRandom(seed); + numbers.sort(function () { + return rand() - .5; + }); + it("should discern the position of every value in " + numbers.join(", "), function () { + var set = SortedSet(numbers); + numbers.forEach(function (n, i) { + expect(set.indexOf(n)).toBe(n); + }); + }); + })(seed); + } + }); + + describe("find methods", function () { + var set = new SortedSet([22, 23, 1, 34, 19, 5, 26, 12, 27, 30, 21, + 20, 6, 7, 2, 32, 10, 9, 33, 3, 11, 17, 28, 15]); + + describe("find", function() { + + it("should find the node for existing values", function() { + expect(set.find(1).value).toBe(1); + expect(set.find(5).value).toBe(5); + expect(set.find(9).value).toBe(9); + expect(set.find(30).value).toBe(30); + expect(set.find(34).value).toBe(34); + }); + + it("should return undefined for non-existent values", function() { + expect(set.find(4)).toBe(undefined); + expect(set.find(13)).toBe(undefined); + expect(set.find(31)).toBe(undefined); + }); + + }); + + describe("findGreatest", function () { + + it("should return the highest value in the set", function() { + expect(set.findGreatest().value).toBe(34); + }); + + }); + + describe("findLeast", function () { + + it("should return the lowest value in the set", function() { + expect(set.findLeast().value).toBe(1); + }); + + }); + + describe("findGreatestLessThanOrEqual", function () { + + it("should return values that exist in the set", function() { + expect(set.findGreatestLessThanOrEqual(5).value).toBe(5); + expect(set.findGreatestLessThanOrEqual(7).value).toBe(7); + expect(set.findGreatestLessThanOrEqual(9).value).toBe(9); + }); + + it("should return the next highest value", function() { + expect(set.findGreatestLessThanOrEqual(14).value).toBe(12); + expect(set.findGreatestLessThanOrEqual(24).value).toBe(23); + expect(set.findGreatestLessThanOrEqual(31).value).toBe(30); + expect(set.findGreatestLessThanOrEqual(4).value).toBe(3); + expect(set.findGreatestLessThanOrEqual(29).value).toBe(28); + expect(set.findGreatestLessThanOrEqual(25).value).toBe(23); + }); + + it("should return undefined for values out of range", function() { + expect(set.findGreatestLessThanOrEqual(0)).toBe(undefined); + }); + + }); + + describe("findGreatestLessThan", function () { + + it("should return next highest for values that exist in the set", function() { + expect(set.findGreatestLessThan(5).value).toBe(3); + expect(set.findGreatestLessThan(7).value).toBe(6); + expect(set.findGreatestLessThan(9).value).toBe(7); + expect(set.findGreatestLessThan(26).value).toBe(23); + }); + + it("should return the next highest value", function() { + expect(set.findGreatestLessThan(14).value).toBe(12); + expect(set.findGreatestLessThan(24).value).toBe(23); + expect(set.findGreatestLessThan(31).value).toBe(30); + expect(set.findGreatestLessThan(4).value).toBe(3); + expect(set.findGreatestLessThan(29).value).toBe(28); + expect(set.findGreatestLessThan(25).value).toBe(23); + }); + + + it("should return undefined for value at bottom of range", function() { + expect(set.findGreatestLessThan(1)).toBe(undefined); + }); + + }); + + describe("findLeastGreaterThanOrEqual", function () { + + it("should return values that exist in the set", function() { + expect(set.findLeastGreaterThanOrEqual(5).value).toBe(5); + expect(set.findLeastGreaterThanOrEqual(7).value).toBe(7); + expect(set.findLeastGreaterThanOrEqual(9).value).toBe(9); + }); + + it("should return the next value", function() { + expect(set.findLeastGreaterThanOrEqual(13).value).toBe(15); + expect(set.findLeastGreaterThanOrEqual(24).value).toBe(26); + expect(set.findLeastGreaterThanOrEqual(31).value).toBe(32); + expect(set.findLeastGreaterThanOrEqual(4).value).toBe(5); + expect(set.findLeastGreaterThanOrEqual(29).value).toBe(30); + expect(set.findLeastGreaterThanOrEqual(25).value).toBe(26); + }); + + it("should return undefined for values out of range", function() { + expect(set.findLeastGreaterThanOrEqual(36)).toBe(undefined); + }); + + }); + + describe("findLeastGreaterThan", function () { + + it("should return next value for values that exist in the set", function() { + expect(set.findLeastGreaterThan(5).value).toBe(6); + expect(set.findLeastGreaterThan(7).value).toBe(9); + expect(set.findLeastGreaterThan(9).value).toBe(10); + expect(set.findLeastGreaterThan(26).value).toBe(27); + }); + + it("should return the next value", function() { + expect(set.findLeastGreaterThan(14).value).toBe(15); + expect(set.findLeastGreaterThan(24).value).toBe(26); + expect(set.findLeastGreaterThan(31).value).toBe(32); + expect(set.findLeastGreaterThan(4).value).toBe(5); + expect(set.findLeastGreaterThan(29).value).toBe(30); + expect(set.findLeastGreaterThan(25).value).toBe(26); + }); + + it("should return undefined for value at top of range", function() { + expect(set.findLeastGreaterThan(34)).toBe(undefined); + }); + + }); + + }); + + describe("addRangeChangeListener", function () { + // fuzz cases + for (var seed = 0; seed < 20; seed++) { + (function (seed) { + var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + var rand = Fuzz.makeRandom(seed); + numbers.sort(function () { + return rand() - .5; + }); + it("should bind content changes to an array for " + numbers.join(", "), function () { + var mirror = []; + var set = SortedSet(); + set.addRangeChangeListener(function (plus, minus, index) { + mirror.swap(index, minus.length, plus); + }); + set.addEach(numbers); + expect(mirror.length).toBe(set.length); + mirror.forEach(function (n, i) { + expect(n).toBe(i); + }); + }); + })(seed); + } + }); + + describe("log drawings", function () { + + function draw(set) { + var lines = []; + set.log({ + intersection: "+", + through: "-", + branchUp: "^", + branchDown: "v", + fromBelow: ".", + fromAbove: "'", + fromBoth: "x", + strafe: "|" + }, function (node, write, writeAbove) { + var line = "" + node.value; + var length = line.length; + var rule = Array(length + 1).join("-"); + writeAbove(" +-" + rule + "-+"); + write("-| " + line + " |"); + write(" +-" + rule + "-+"); + }, lines.push, lines); + return lines; + } + + it("should draw a simple box", function () { + var set = SortedSet([1]); + expect(draw(set)).toEqual([ + " +---+", + "--| 1 |", + " +---+" + ]); + }); + + it("should draw a graph of two ascending", function () { + var set = SortedSet([1, 2]); + expect(draw(set)).toEqual([ + " +---+", + ".---| 1 |", + "| +---+", + "| +---+", + "^-| 2 |", + " +---+" + ]); + }); + + it("should draw a graph of two descending", function () { + var set = SortedSet([2, 1]); + expect(draw(set)).toEqual([ + " +---+", + "v-| 1 |", + "| +---+", + "| +---+", + "'---| 2 |", + " +---+" + ]); + }); + + it("should draw a graph of three", function () { + var set = SortedSet([3, 1, 2]); + expect(draw(set)).toEqual([ + " +---+", + ".---| 1 |", + "| +---+", + "| +---+", + "+-| 2 |", + "| +---+", + "| +---+", + "'---| 3 |", + " +---+" + ]); + }); + + it("should draw a complex graph", function () { + var set = SortedSet([8, 6, 5, 3, 7, 2, 1, 4]); + expect(draw(set)).toEqual([ + " +---+", + " .---| 1 |", + " | +---+", + " | +---+", + ".-+-| 2 |", + "| | +---+", + "| | +---+", + "| '---| 3 |", + "| +---+", + "| +---+", + "+-| 4 |", + "| +---+", + "| +---+", + "'-v-| 5 |", + " | +---+", + " | +---+", + " | .---| 6 |", + " | | +---+", + " | | +---+", + " '-+-| 7 |", + " | +---+", + " | +---+", + " '---| 8 |", + " +---+" + ]); + }); + + }); + +}); diff --git a/core/collections/test/spec/to-json.js b/core/collections/test/spec/to-json.js new file mode 100644 index 0000000000..945650c35c --- /dev/null +++ b/core/collections/test/spec/to-json.js @@ -0,0 +1,19 @@ +module.exports = describeToJson; +function describeToJson(Collection, values) { + describe("toJSON", function () { + it("stringifies and parses to a collection with the same data", function () { + var collection = Collection.from(values); + var stringified = JSON.stringify(collection); + + var newCollection = Collection.from(JSON.parse(stringified)); + + expect(stringified).toEqual(JSON.stringify(values)); + + if (collection.entriesArray) { + expect(Object.equals(collection.entriesArray(), newCollection.entriesArray())).toEqual(true); + } else { + expect(Object.equals(collection.toArray(), newCollection.toArray())).toEqual(true); + } + }); + }); +} diff --git a/core/collections/tree-log.js b/core/collections/tree-log.js new file mode 100644 index 0000000000..e5ce1061c5 --- /dev/null +++ b/core/collections/tree-log.js @@ -0,0 +1,40 @@ +"use strict"; + +module.exports = TreeLog; + +function TreeLog() { +} + +TreeLog.ascii = { + intersection: "+", + through: "-", + branchUp: "+", + branchDown: "+", + fromBelow: ".", + fromAbove: "'", + fromBoth: "+", + strafe: "|" +}; + +TreeLog.unicodeRound = { + intersection: "\u254b", + through: "\u2501", + branchUp: "\u253b", + branchDown: "\u2533", + fromBelow: "\u256d", // round corner + fromAbove: "\u2570", // round corner + fromBoth: "\u2523", + strafe: "\u2503" +}; + +TreeLog.unicodeSharp = { + intersection: "\u254b", + through: "\u2501", + branchUp: "\u253b", + branchDown: "\u2533", + fromBelow: "\u250f", // sharp corner + fromAbove: "\u2517", // sharp corner + fromBoth: "\u2523", + strafe: "\u2503" +}; + diff --git a/core/collections/weak-map.js b/core/collections/weak-map.js new file mode 100644 index 0000000000..4e9ad68207 --- /dev/null +++ b/core/collections/weak-map.js @@ -0,0 +1,2 @@ + +module.exports = (typeof WeakMap !== 'undefined') ? WeakMap : require("weak-map"); From bf29fc2b65adcf66941f5ca55aea4de17c5f9553 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 14:43:59 -0700 Subject: [PATCH 149/407] add RFC3339UTC-range-string-to-range-converter --- ...3339UTC-range-string-to-range-converter.js | 70 +++++++++++++++++++ ...9UTC-range-string-to-range-converter.mjson | 25 +++++++ 2 files changed, 95 insertions(+) create mode 100644 core/converter/RFC3339UTC-range-string-to-range-converter.js create mode 100644 core/converter/RFC3339UTC-range-string-to-range-converter.mjson diff --git a/core/converter/RFC3339UTC-range-string-to-range-converter.js b/core/converter/RFC3339UTC-range-string-to-range-converter.js new file mode 100644 index 0000000000..05348e0b09 --- /dev/null +++ b/core/converter/RFC3339UTC-range-string-to-range-converter.js @@ -0,0 +1,70 @@ +/** + * @module montage/core/converter/RFC3339UTC-range-string-to-range-converter + * @requires montage/core/converter/converter + */ +var Converter = require("./converter").Converter, + Range = require("../range"), + Range = require("../extras/date"), + singleton; + +/** + * @class RFC3339UTCRangeStringToRangeConverter + * @classdesc Converts an RFC3339 UTC string to a date and reverts it. + */ +var RFC3339UTCRangeStringToRangeConverter = exports.RFC3339UTCRangeStringToRangeConverter = Converter.specialize({ + + constructor: { + value: function () { + if (this.constructor === RFC3339UTCRangeStringToRangeConverter) { + if (!singleton) { + singleton = this; + } + + return singleton; + } + + return this; + } + }, + + /** + * Converts the RFC3339 string to a Date. + * @function + * @param {string} v The string to convert. + * @returns {Range} The Date converted from the string. + */ + convert: { + value: function (v) { + return Range.parse(v,Date.parseRFC3339); + //return Date.parseRFC3339(v); + } + }, + + /** + * Reverts the specified Date to an RFC3339 String. + * @function + * @param {Range} v The specified string. + * @returns {string} + */ + revert: { + value: function (v) { + //Wish we could just called toString() on v, + //but it's missing the abillity to cutomize the + //stringify of begin/end + return v.bounds[0] + v.begin.toISOString() + "," + v.end.toISOString()+ v.bounds[1] + + return v.toISOString(); + } + } + +}); + +Object.defineProperty(exports, 'singleton', { + get: function () { + if (!singleton) { + singleton = new RFC3339UTCRangeStringToRangeConverter(); + } + + return singleton; + } +}); diff --git a/core/converter/RFC3339UTC-range-string-to-range-converter.mjson b/core/converter/RFC3339UTC-range-string-to-range-converter.mjson new file mode 100644 index 0000000000..96038b03a5 --- /dev/null +++ b/core/converter/RFC3339UTC-range-string-to-range-converter.mjson @@ -0,0 +1,25 @@ +{ + "converter_descriptor": { + "object": "montage/core/converter/converter.mjson" + }, + "root": { + "prototype": "montage/core/meta/module-object-descriptor", + "values": { + "name": "RFC3339UTCRangeStringToRangeConverter", + "customPrototype": false, + "parent": { + "@": "converter_descriptor" + }, + "propertyDescriptors": [], + "propertyDescriptorGroups": {}, + "propertyValidationRules": {}, + "objectDescriptorModule": { + "%": "core/converter/RFC3339UTC-range-string-to-range-converter.mjson" + }, + "exportName": "RFC3339UTCRangeStringToRangeConverter", + "module": { + "%": "core/converter/RFC3339UTC-range-string-to-range-converter" + } + } + } +} From 5f0bdef8d43812dece56ee99dea30256fcb462d1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 14:45:38 -0700 Subject: [PATCH 150/407] add objects to handle time zones and calendar that work with native Date --- core/date/calendar-date.js | 84 + core/date/calendar.js | 224 ++ core/date/locale.js | 283 +++ core/date/time-zone-data/zones.json | 3583 +++++++++++++++++++++++++++ core/date/time-zone.js | 85 + core/date/tools/compile-zones.js | 33 + core/extras/date.js | 23 + 7 files changed, 4315 insertions(+) create mode 100644 core/date/calendar-date.js create mode 100644 core/date/calendar.js create mode 100644 core/date/locale.js create mode 100644 core/date/time-zone-data/zones.json create mode 100644 core/date/time-zone.js create mode 100644 core/date/tools/compile-zones.js diff --git a/core/date/calendar-date.js b/core/date/calendar-date.js new file mode 100644 index 0000000000..a3e80ae55b --- /dev/null +++ b/core/date/calendar-date.js @@ -0,0 +1,84 @@ +/* + +*/ +var Montage = require("./core").Montage, + ICAL = require('ical.js'), + ICAL_Time = ICAL.Time, + ICAL_Time_Prototype = ICAL.Time.prototype, + CalendarDate = exports.CalendarDate = ICAL_Time, + CalendarDatePrototype = CalendarCalendarDatePrototype; + + +CalendarDate.getInfoForObject = function(object) { + return Montage.getInfoForObject(object); +}; + + +//Make CalendarDate behave like JS Date: +CalendarDate.UTC() +CalendarDate.now() +CalendarDate.parse() +CalendarDatePrototype.getDate = function() { + return this.day; +}; + +CalendarDatePrototype.getDay() = function() { + return ; +}; +CalendarDatePrototype.getFullYear() +CalendarDatePrototype.getHours = function() { + return this.hour; +}; +CalendarDatePrototype.getMilliseconds() +CalendarDatePrototype.getMinutes = function() { + return this.minute; +}; +CalendarDatePrototype.getMonth = function() { + return this.month - 1; +}; +CalendarDatePrototype.getSeconds = function() { + return this.second; +}; +CalendarDatePrototype.getTime = function() { + return this.toUnixTime() * 1000; +}; + +CalendarDatePrototype.getTimezoneOffset() +CalendarDatePrototype.getUTCDate() +CalendarDatePrototype.getUTCDay() +CalendarDatePrototype.getUTCFullYear() +CalendarDatePrototype.getUTCHours() +CalendarDatePrototype.getUTCMilliseconds() +CalendarDatePrototype.getUTCMinutes() +CalendarDatePrototype.getUTCMonth() +CalendarDatePrototype.getUTCSeconds() +CalendarDatePrototype.getYear() +CalendarDatePrototype.setDate() +CalendarDatePrototype.setFullYear() +CalendarDatePrototype.setHours() +CalendarDatePrototype.setMilliseconds() +CalendarDatePrototype.setMinutes() +CalendarDatePrototype.setMonth() +CalendarDatePrototype.setSeconds() +CalendarDatePrototype.setTime() +CalendarDatePrototype.setUTCDate() +CalendarDatePrototype.setUTCFullYear() +CalendarDatePrototype.setUTCHours() +CalendarDatePrototype.setUTCMilliseconds() +CalendarDatePrototype.setUTCMinutes() +CalendarDatePrototype.setUTCMonth() +CalendarDatePrototype.setUTCSeconds() +CalendarDatePrototype.setYear() +CalendarDatePrototype.toDateString() +CalendarDatePrototype.toGMTString() +CalendarDatePrototype.toISOString() +CalendarDatePrototype.toJSON() +CalendarDatePrototype.toLocaleDateString() +CalendarDatePrototype.toLocaleFormat() +CalendarDatePrototype.toLocaleString() +CalendarDatePrototype.toLocaleTimeString() +CalendarDatePrototype.toSource() +CalendarDatePrototype.toString() +CalendarDatePrototype.toTimeString() +CalendarDatePrototype.toUTCString() +CalendarDatePrototype.valueOf() diff --git a/core/date/calendar.js b/core/date/calendar.js new file mode 100644 index 0000000000..35c805def8 --- /dev/null +++ b/core/date/calendar.js @@ -0,0 +1,224 @@ +var Montage = require("../core").Montage, + Locale = require("./locale"), + TimeZone = require("./time-zone"), + CalendarDate = require("./calendar-date"), + Range = require("../range").Range, + systemTimeZone = TimeZone.systemTimeZone; //All JS Dates are in this timeZone +/** + Calendar + + Inspired by https://developer.apple.com/documentation/foundation/nscalendar?language=objc + + An object that defines the relationships between calendar units (such as eras, years, and weekdays) and absolute points in time (Date), providing features for calculation and comparison of dates. + + Overview + + Calendar objects encapsulate information about systems of reckoning time in which the beginning, length, and divisions of a year are defined. They provide information about the calendar and support for calendrical computations such as determining the range of a given calendrical unit and adding units to a given absolute time. + + @class module:montage/core/date/calendar.Calendar + @extends module:montage/core/core.Montage + */ + +//Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Locale/calendar +var CalendarIdentifierValues = [ + "buddhist", /* Thai Buddhist calendar */ + "chinese", /* Traditional Chinese calendar */ + "coptic", /* Coptic calendar */ + "dangi", /* Traditional Korean calendar */ + "ethioaa", /* Ethiopic calendar, Amete Alem (epoch approx. 5493 B.C.E) */ + "ethiopic", /* Ethiopic calendar, Amete Mihret (epoch approx, 8 C.E.) */ + "gregory", /* Gregorian calendar */ + "hebrew", /* Traditional Hebrew calendar */ + "indian", /* Indian calendar */ + "islamic", /* Islamic calendar */ + "islamic-umalqura", /* Islamic calendar, Umm al-Qura */ + "islamic-tbla", /* Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - astronomical epoch) */ + "islamic-civil", /* /* Islamic calendar, tabular (intercalary years [2,5,7,10,13,16,18,21,24,26,29] - civil epoch) */ + "islamic-rgsa", /* /* Islamic calendar, Saudi Arabia sighting */ + "iso8601", /* ISO calendar (Gregorian calendar using the ISO 8601 calendar week rules) */ + "japanese", /* Japanese Imperial calendar */ + "persian", /* Persian calendar */ + "roc" /* Republic of China calendar */ +]; +exports.CalendarIdentifier = CalendarIdentifier = new Enum().initWithMembersAndValues(CalendarIdentifierValues,CalendarIdentifierValues); + + +var CalendarUnitNames = ["era","year","yearForWeekOfYear","quarter","month","weekOfYear","weekOfMonth","weekDay","weekDayOrdinal","day","hour","minute","second","millisecond"]; + +//Calendar units may be used as a bit mask to specify a combination of units. +CalendarUnitBitfieldValues = []; +for(var i=0, countI=CalendarUnitNames.length;(i Date + */ + + /** + * Returns an equivalent CalendarDate to aDate (in UTC / local timeZone)in Calendar's timeZone. + * + * @function + * @param {Date} aDate The date for which to perform the calculation. + * @returns {CalendarDate} true if the given date matches the given components, otherwise false. + */ + calendarDateFromDate: { + value: function(aDate) { + + var aCalendarDate = CalendarDate.fromJSDate(aDate, true /*useUTC*/); + TimeZone.convertCalendarDateFromTimeZoneToTimeZone(aCalendarDate,Timezone.utcTimezone,this.timeZone); + return aCalendarDate; + } + }, + + /** + * Returns an UTC equivalent to calendarDate in Calendar's timeZone. + * + * @function + * @param {CalendarDate} calendarDate The date for which to perform the calculation. + * @returns {Date} true if the given date matches the given components, otherwise false. + */ + dateFromCalendarDate: { + value: function(calendarDate) { + return calendarDate.toJSDate(); + } + }, + + /** + * Returns an equivalent CalendarDate Range to aDate Range (in UTC / local timeZone)in Calendar's timeZone. + * + * @function + * @param {Range} aDateRange The date for which to perform the calculation. + * @returns {Range} a Range whose begin/end are CalendarDate in the calendar's timeZone. + */ + calendarDateRangeFromDateRange: { + value: function(aDateRange) { + return new Range(this.calendarDateFromDate(aDateRange.begin), this.calendarDateFromDate(aDateRange.end, aDateRange.bounds)); + } + }, + + /** + * Returns an equivalent date range in Calendar's timeZone. + * + * @function + * @param {CalendarDate} calendarDate The date for which to perform the calculation. + * @returns {Date} true if the given date matches the given components, otherwise false. + */ + dateRangeFromCalendarDateRange: { + value: function(calendarDateRange) { + return new Range(this.dateFromCalendarDate(calendarDateRange.begin), this.dateFromCalendarDate(calendarDateRange.end, calendarDateRange.bounds)); + } + }, + + + /** + * Returns whether a given date matches all of the given date components. + * + * This method is useful for determining whether dates calculated by methods + * like nextDateAfterDate:matchingUnit:value:options: or + * enumerateDatesStartingAfterDate:matchingComponents:options:usingBlock: are exact, + * or required an adjustment due to a nonexistent time. + * + * @function + * @param {Date} date The date for which to perform the calculation. + * @param {CalendarDate} CalendarDate The date components to match.. + * @returns {boolean} true if the given date matches the given components, otherwise false. + */ + doesDateMatchComponents: { + value: function(date, CalendarDate) { + + } + }, + + /** + * Returns the specified date component from a given date. + * + * @function + * @param {Date} date The date for which to perform the calculation. + * @param {CalendarDate} CalendarDate The date components to match.. + * @returns {boolean} true if the given date matches the given components, otherwise false. + */ + + +},{ + /** + * Creates a new calendar specified by a given identifier. + * + * + * @function + * @param {CalendarIdentifier} calendarIdentifier The module id of the HTML page to load. + * @returns {Calendar} a new Calendar instance. + */ + + withIdentifier: { + value: function(calendarIdentifier) { + + } + } +}); + +//To avoid a cycle. +Locale.Calendar = Calendar; diff --git a/core/date/locale.js b/core/date/locale.js new file mode 100644 index 0000000000..f8ca0126a7 --- /dev/null +++ b/core/date/locale.js @@ -0,0 +1,283 @@ +var Montage = require("../core").Montage, +currentEnvironment = require("../environment").currentEnvironment; + +/* + +Finding the users locale +First I need to know the locale of the user. This is usually expressed in the form of culture codes they look like this, en-GB. This code represents English (United Kingdom). It turns out browsers actually expose a few different ways to get this information. + +navigator.language +navigator.languages +navigator.browserLanguage +navigator.userLanguage +Intl.DateTimeFormat().resolvedOptions().locale +Like most things JavaScript not all these options return the same thing or what you might expect. My initial attempt at solving this problem was to use Intl.DateTimeFormat().resolvedOptions().locale. + +This seemed to work fine however I run a Mac and swap between MacOS and Windows 10 running on Parallels. I noticed on MacOS this returned en-GB as I was expecting but on Windows 10 it returned en-US. I checked all my settings and everything appeared to be correctly set to UK culture. This made me slightly concerned that this was not working completely correctly. + +After a bit of Googling and reading I decided to go with a combination of the other options. Most people seem to agree this gives the most accurate result in the majority of situations. It looks like this. + +getBrowserLocale: function () { + return (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.userLanguage || navigator.language || navigator.browserLanguage || 'en'; +} + + +*/ + + +/** + Locale + + - follows Intl.Locale + - offer richer types when we do (Calendar) + - more to add as needed from by https://developer.apple.com/documentation/foundation/nslocale?language=objc + + Information about linguistic, cultural, and technological conventions for use in formatting data for presentation. + + @class module:montage/core/date/locale.Locale + @extends module:montage/core/core.Montage + */ + + + /* + in order to react to locale change at the browser, we can use: + + window.addEventListemer("languagechange",function(event) { + console.log('languagechange event detected!'); + }); + + We shall have our own preferences at some point with direct way to change a locale/language, so + we'll need to use window.addEventListemer("languagechange" ...) as an input to that and globally observe changes on our Locale with our own change events + + */ + +var Locale = exports.Locale = Montage.specialize({ + + /** + * initializes a new calendar locale by a given identifier. + * + * @function + * @param {String} localeIdentifier The module id of the HTML page to load. + * @param {Object} options An object that contains configuration for the Locale. Keys are Unicode locale tags, + * values are valid Unicode tag values. + * + * @returns {Locale} a new Locale instance. + */ + initWithIdentifier: { + value: function(localeIdentifier) { + + } + }, + _localeByIdentifier: { + value: new Map() + } + + /* Instance properties */ + + /** + * The baseName property returns basic, core information about the Locale in the form of a substring + * of the complete data string. Specifically, the property returns the substring containing the language, + * and the script and region if available. baseName returns the language ["-" script] ["-" region] *("-" variant) + * subsequence of the unicode_language_id grammar. + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Locale/baseName + * + * @property {String} + */ + baseName: { + value: undefined + }, + + /** + * Returns the part of the Locale that indicates the Locale's calendar era. + * + * @property {Calendar} + */ + calendar: { + value: undefined + }, + /** + * Returns whether case is taken into account for the locale's collation rules. + * + * @property {boolean} + */ + caseFirst: { + value: undefined + }, + /** + * returns the collation type for the Locale, which is used to order strings according to the locale's rules. + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Locale/collation + * + * Collation is the process of ordering strings of characters. It is used whenever strings must be sorted and + * placed into a certain order, from search query results to ordering records in a database. + * While the idea of placing strings in order might seem trivial, the idea of order can vary from region to + * region and language to language. The collation property helps to make it easier for JavaScript programmers + * to access the collation type used by a particular locale. + * + * Valid collation types + * Collation Type Description + * big5han Pinyin ordering for Latin, big5 charset ordering for CJK characters (used in Chinese) + * compat A previous version of the ordering, for compatibility + * dict Dictionary style ordering (such as in Sinhala) + * + * direct The direct collation type has been deprected. Do not use. + * Binary code point order (used in Hindi) + * + * ducet The default Unicode collation element table order + * emoji Recommended ordering for emoji characters + * eor European ordering rules + * gb2312 Pinyin ordering for Latin, gb2312han charset ordering for CJK characters (used in Chinese) + * phonebk Phonebook style ordering (such as in German) + * phonetic Phonetic ordering (sorting based on pronunciation) + * pinyin Pinyin ordering for Latin and for CJK characters (used in Chinese) + * reformed Reformed ordering (such as in Swedish) + * search Special collation type for string search + * searchjl Special collation type for Korean initial consonant search + * standard Default ordering for each language + * stroke Pinyin ordering for Latin, stroke order for CJK characters (used in Chinese) + * trad Traditional style ordering (such as in Spanish) + * unihan Pinyin ordering for Latin, Unihan radical-stroke ordering for CJK characters (used in Chinese) + * zhuyin Pinyin ordering for Latin, zhuyin order for Bopomofo and CJK characters (used in Chinese) + * + * @property {String} + */ + collation: { + value: undefined + }, + /** + * The Intl.Locale.prototype.hourCycle property is an accessor property that returns the time keeping + * format convention used by the locale. + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Locale/hourCycle + * + * Description + * + * There are 2 main types of time keeping conventions (clocks) used around the world: the 12 hour clock and the 24 hour clock. + * The hourCycle property makes it easier for JavaScript programmers to access the clock type used by a particular locale. + * Like other additional locale data, hour cycle type is an extension subtag, which extends the data contained in a locale string. + * The hour cycle type can have several different values, which are listed in the table below + * + * Valid hour cycle types + * + * Hour cycle type Description + * h12 Hour system using 1–12; corresponds to 'h' in patterns. The 12 hour clock, with midnight starting at 12:00 am. + * h23 Hour system using 0–23; corresponds to 'H' in patterns. The 24 hour clock, with midnight starting at 0:00. + * h11 Hour system using 0–11; corresponds to 'K' in patterns. The 12 hour clock, with midnight starting at 0:00 am. + * h24 Hour system using 1–24; corresponds to 'k' in pattern. The 24 hour clock, with midnight starting at 24:00. + * + * @property {String} + */ + hourCycle: { + value: undefined + }, + /** + * Returns the language associated with the locale. + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Locale/language + * + * @property {String} + */ + language: { + value: undefined + }, + + /** + * returns the numeral system used by the locale. + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Locale/numberingSystem + * + * @property {String} + */ + numberingSystem: { + value: undefined + }, + + /** + * returns whether the locale has special collation handling for numeric characters. + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Locale/numeric + * + * @property {boolean} + */ + numeric: { + value: undefined + }, + + /** + * returns the region of the world (usually a country) associated with the locale. + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Locale/region + * + * @property {String} + */ + region: { + value: undefined + }, + + /** + * Returns the script used for writing the particular language used in the locale. + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Locale/script + * + * @property {String} + */ + script: { + value: undefined + } +},{ + + _systemLocale: { + value: undefined + }, + systemLocale: { + get: function() { + if(!this._systemLocale) { + var systemLocaleIdentifier = currentEnvironment.systemLocaleIdentifier, + //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/resolvedOptions + resolvedOptions = Intl.DateTimeFormat(navigatorLocaleIdentifier).resolvedOptions(), + calendar = resolvedOptions.calendar, /*"gregory"*/ + day = resolvedOptions.day, /*"numeric"*/ + month = resolvedOptions.month, /*"numeric"*/ + year = resolvedOptions.year, /*"year"*/ + locale = resolvedOptions.locale, /* should be equal to navigatorLocaleIdentifier */ + numberingSystem = resolvedOptions.year, /*"latn"*/ + timeZone = resolvedOptions.timeZone, /* "America/Los_Angeles" */ + aLocale; + + //Using timeZone extra data through a TimeZone instance (and extra meta data, we could in certain cases guess the region) + aLocale = this.withIdentifier(locale,{ + calendar: this.Calendar.withIdentifier(calendar), + numberingSystem: numberingSystem + }); + + this._systemLocale = aLocale; + + } + return this._navigatorLocale; + }, + set: function(value) { + this._systemLocale = value; + } + }, + + + + /** + * Creates a new calendar specified by a given identifier. + * + * + * @function + * @param {String} localeIdentifier The module id of the HTML page to load. + * @param {Object} options An object that contains configuration for the Locale. Keys are Unicode locale tags, + * values are valid Unicode tag values. + * + * @returns {Locale} a new Locale instance. + */ + + withIdentifier: { + value: function(localeIdentifier, options) { + return new this().initWithIdentifier(localeIdentifier, options); + } + } +}); diff --git a/core/date/time-zone-data/zones.json b/core/date/time-zone-data/zones.json new file mode 100644 index 0000000000..665ce230a4 --- /dev/null +++ b/core/date/time-zone-data/zones.json @@ -0,0 +1,3583 @@ +{ + "version": "2.2019c", + "aliases": { + "AUS Central Standard Time": { + "aliasTo": "Australia/Darwin" + }, + "AUS Eastern Standard Time": { + "aliasTo": "Australia/Sydney" + }, + "Afghanistan Standard Time": { + "aliasTo": "Asia/Kabul" + }, + "Africa/Asmera": { + "aliasTo": "Africa/Asmara" + }, + "Africa/Timbuktu": { + "aliasTo": "Africa/Bamako" + }, + "Alaskan Standard Time": { + "aliasTo": "America/Anchorage" + }, + "America/Argentina/ComodRivadavia": { + "aliasTo": "America/Argentina/Catamarca" + }, + "America/Buenos_Aires": { + "aliasTo": "America/Argentina/Buenos_Aires" + }, + "America/Louisville": { + "aliasTo": "America/Kentucky/Louisville" + }, + "America/Montreal": { + "aliasTo": "America/Toronto" + }, + "America/Santa_Isabel": { + "aliasTo": "America/Tijuana" + }, + "Arab Standard Time": { + "aliasTo": "Asia/Riyadh" + }, + "Arabian Standard Time": { + "aliasTo": "Asia/Dubai" + }, + "Arabic Standard Time": { + "aliasTo": "Asia/Baghdad" + }, + "Argentina Standard Time": { + "aliasTo": "America/Argentina/Buenos_Aires" + }, + "Asia/Calcutta": { + "aliasTo": "Asia/Kolkata" + }, + "Asia/Katmandu": { + "aliasTo": "Asia/Kathmandu" + }, + "Asia/Rangoon": { + "aliasTo": "Asia/Yangon" + }, + "Asia/Saigon": { + "aliasTo": "Asia/Ho_Chi_Minh" + }, + "Atlantic Standard Time": { + "aliasTo": "America/Halifax" + }, + "Atlantic/Faeroe": { + "aliasTo": "Atlantic/Faroe" + }, + "Atlantic/Jan_Mayen": { + "aliasTo": "Europe/Oslo" + }, + "Azerbaijan Standard Time": { + "aliasTo": "Asia/Baku" + }, + "Azores Standard Time": { + "aliasTo": "Atlantic/Azores" + }, + "Bahia Standard Time": { + "aliasTo": "America/Bahia" + }, + "Bangladesh Standard Time": { + "aliasTo": "Asia/Dhaka" + }, + "Belarus Standard Time": { + "aliasTo": "Europe/Minsk" + }, + "Canada Central Standard Time": { + "aliasTo": "America/Regina" + }, + "Cape Verde Standard Time": { + "aliasTo": "Atlantic/Cape_Verde" + }, + "Caucasus Standard Time": { + "aliasTo": "Asia/Yerevan" + }, + "Cen. Australia Standard Time": { + "aliasTo": "Australia/Adelaide" + }, + "Central America Standard Time": { + "aliasTo": "America/Guatemala" + }, + "Central Asia Standard Time": { + "aliasTo": "Asia/Almaty" + }, + "Central Brazilian Standard Time": { + "aliasTo": "America/Cuiaba" + }, + "Central Europe Standard Time": { + "aliasTo": "Europe/Budapest" + }, + "Central European Standard Time": { + "aliasTo": "Europe/Warsaw" + }, + "Central Pacific Standard Time": { + "aliasTo": "Pacific/Guadalcanal" + }, + "Central Standard Time": { + "aliasTo": "America/Chicago" + }, + "Central Standard Time (Mexico)": { + "aliasTo": "America/Mexico_City" + }, + "China Standard Time": { + "aliasTo": "Asia/Shanghai" + }, + "E. Africa Standard Time": { + "aliasTo": "Africa/Nairobi" + }, + "E. Australia Standard Time": { + "aliasTo": "Australia/Brisbane" + }, + "E. South America Standard Time": { + "aliasTo": "America/Sao_Paulo" + }, + "Eastern Standard Time": { + "aliasTo": "America/New_York" + }, + "Egypt Standard Time": { + "aliasTo": "Africa/Cairo" + }, + "Ekaterinburg Standard Time": { + "aliasTo": "Asia/Yekaterinburg" + }, + "Etc/GMT": { + "aliasTo": "UTC" + }, + "Etc/GMT+0": { + "aliasTo": "UTC" + }, + "Etc/UCT": { + "aliasTo": "UTC" + }, + "Etc/UTC": { + "aliasTo": "UTC" + }, + "Etc/Unversal": { + "aliasTo": "UTC" + }, + "Etc/Zulu": { + "aliasTo": "UTC" + }, + "Europe/Belfast": { + "aliasTo": "Europe/London" + }, + "FLE Standard Time": { + "aliasTo": "Europe/Kiev" + }, + "Fiji Standard Time": { + "aliasTo": "Pacific/Fiji" + }, + "GMT": { + "aliasTo": "UTC" + }, + "GMT Standard Time": { + "aliasTo": "Europe/London" + }, + "GMT+0": { + "aliasTo": "UTC" + }, + "GMT0": { + "aliasTo": "UTC" + }, + "GTB Standard Time": { + "aliasTo": "Europe/Bucharest" + }, + "Georgian Standard Time": { + "aliasTo": "Asia/Tbilisi" + }, + "Greenland Standard Time": { + "aliasTo": "America/Godthab" + }, + "Greenwich": { + "aliasTo": "UTC" + }, + "Greenwich Standard Time": { + "aliasTo": "Atlantic/Reykjavik" + }, + "Hawaiian Standard Time": { + "aliasTo": "Pacific/Honolulu" + }, + "India Standard Time": { + "aliasTo": "Asia/Calcutta" + }, + "Iran Standard Time": { + "aliasTo": "Asia/Tehran" + }, + "Israel Standard Time": { + "aliasTo": "Asia/Jerusalem" + }, + "Jordan Standard Time": { + "aliasTo": "Asia/Amman" + }, + "Kaliningrad Standard Time": { + "aliasTo": "Europe/Kaliningrad" + }, + "Korea Standard Time": { + "aliasTo": "Asia/Seoul" + }, + "Libya Standard Time": { + "aliasTo": "Africa/Tripoli" + }, + "Line Islands Standard Time": { + "aliasTo": "Pacific/Kiritimati" + }, + "Magadan Standard Time": { + "aliasTo": "Asia/Magadan" + }, + "Mauritius Standard Time": { + "aliasTo": "Indian/Mauritius" + }, + "Middle East Standard Time": { + "aliasTo": "Asia/Beirut" + }, + "Montevideo Standard Time": { + "aliasTo": "America/Montevideo" + }, + "Morocco Standard Time": { + "aliasTo": "Africa/Casablanca" + }, + "Mountain Standard Time": { + "aliasTo": "America/Denver" + }, + "Mountain Standard Time (Mexico)": { + "aliasTo": "America/Chihuahua" + }, + "Myanmar Standard Time": { + "aliasTo": "Asia/Rangoon" + }, + "N. Central Asia Standard Time": { + "aliasTo": "Asia/Novosibirsk" + }, + "Namibia Standard Time": { + "aliasTo": "Africa/Windhoek" + }, + "Nepal Standard Time": { + "aliasTo": "Asia/Katmandu" + }, + "New Zealand Standard Time": { + "aliasTo": "Pacific/Auckland" + }, + "Newfoundland Standard Time": { + "aliasTo": "America/St_Johns" + }, + "North Asia East Standard Time": { + "aliasTo": "Asia/Irkutsk" + }, + "North Asia Standard Time": { + "aliasTo": "Asia/Krasnoyarsk" + }, + "Pacific SA Standard Time": { + "aliasTo": "America/Santiago" + }, + "Pacific Standard Time": { + "aliasTo": "America/Los_Angeles" + }, + "Pacific Standard Time (Mexico)": { + "aliasTo": "America/Santa_Isabel" + }, + "Pacific/Johnston": { + "aliasTo": "Pacific/Honolulu" + }, + "Pakistan Standard Time": { + "aliasTo": "Asia/Karachi" + }, + "Paraguay Standard Time": { + "aliasTo": "America/Asuncion" + }, + "Romance Standard Time": { + "aliasTo": "Europe/Paris" + }, + "Russia Time Zone 10": { + "aliasTo": "Asia/Srednekolymsk" + }, + "Russia Time Zone 11": { + "aliasTo": "Asia/Kamchatka" + }, + "Russia Time Zone 3": { + "aliasTo": "Europe/Samara" + }, + "Russian Standard Time": { + "aliasTo": "Europe/Moscow" + }, + "SA Eastern Standard Time": { + "aliasTo": "America/Cayenne" + }, + "SA Pacific Standard Time": { + "aliasTo": "America/Bogota" + }, + "SA Western Standard Time": { + "aliasTo": "America/La_Paz" + }, + "SE Asia Standard Time": { + "aliasTo": "Asia/Bangkok" + }, + "Samoa Standard Time": { + "aliasTo": "Pacific/Apia" + }, + "Singapore Standard Time": { + "aliasTo": "Asia/Singapore" + }, + "South Africa Standard Time": { + "aliasTo": "Africa/Johannesburg" + }, + "Sri Lanka Standard Time": { + "aliasTo": "Asia/Colombo" + }, + "Syria Standard Time": { + "aliasTo": "Asia/Damascus" + }, + "Taipei Standard Time": { + "aliasTo": "Asia/Taipei" + }, + "Tasmania Standard Time": { + "aliasTo": "Australia/Hobart" + }, + "Tokyo Standard Time": { + "aliasTo": "Asia/Tokyo" + }, + "Tonga Standard Time": { + "aliasTo": "Pacific/Tongatapu" + }, + "Turkey Standard Time": { + "aliasTo": "Europe/Istanbul" + }, + "UCT": { + "aliasTo": "UTC" + }, + "US Eastern Standard Time": { + "aliasTo": "America/Indiana/Indianapolis" + }, + "US Mountain Standard Time": { + "aliasTo": "America/Phoenix" + }, + "US/Central": { + "aliasTo": "America/Chicago" + }, + "US/Eastern": { + "aliasTo": "America/New_York" + }, + "US/Mountain": { + "aliasTo": "America/Denver" + }, + "US/Pacific": { + "aliasTo": "America/Los_Angeles" + }, + "US/Pacific-New": { + "aliasTo": "America/Los_Angeles" + }, + "Ulaanbaatar Standard Time": { + "aliasTo": "Asia/Ulaanbaatar" + }, + "Universal": { + "aliasTo": "UTC" + }, + "Venezuela Standard Time": { + "aliasTo": "America/Caracas" + }, + "Vladivostok Standard Time": { + "aliasTo": "Asia/Vladivostok" + }, + "W. Australia Standard Time": { + "aliasTo": "Australia/Perth" + }, + "W. Central Africa Standard Time": { + "aliasTo": "Africa/Lagos" + }, + "W. Europe Standard Time": { + "aliasTo": "Europe/Berlin" + }, + "West Asia Standard Time": { + "aliasTo": "Asia/Tashkent" + }, + "West Pacific Standard Time": { + "aliasTo": "Pacific/Port_Moresby" + }, + "Yakutsk Standard Time": { + "aliasTo": "Asia/Yakutsk" + }, + "Z": { + "aliasTo": "UTC" + }, + "Zulu": { + "aliasTo": "UTC" + }, + "utc": { + "aliasTo": "UTC" + } + }, + "zones": { + "Africa/Abidjan": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0051900", + "longitude": "-0040200" + }, + "Africa/Accra": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0053300", + "longitude": "+0001300" + }, + "Africa/Addis_Ababa": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0090200", + "longitude": "+0384200" + }, + "Africa/Algiers": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0364700", + "longitude": "+0030300" + }, + "Africa/Asmara": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0152000", + "longitude": "+0385300" + }, + "Africa/Bamako": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0123900", + "longitude": "-0080000" + }, + "Africa/Bangui": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0042200", + "longitude": "+0183500" + }, + "Africa/Banjul": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0132800", + "longitude": "-0163900" + }, + "Africa/Bissau": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0115100", + "longitude": "-0153500" + }, + "Africa/Blantyre": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0154700", + "longitude": "+0350000" + }, + "Africa/Brazzaville": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0041600", + "longitude": "+0151700" + }, + "Africa/Bujumbura": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0032300", + "longitude": "+0292200" + }, + "Africa/Cairo": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0300300", + "longitude": "+0311500" + }, + "Africa/Casablanca": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20180325T020000\r\nRDATE:20180325T020000\r\nRDATE:20180617T020000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:20180513T030000\r\nRDATE:20180513T030000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20190609T020000\r\nRDATE:20190609T020000\r\nRDATE:20200524T020000\r\nRDATE:20210516T020000\r\nRDATE:20220508T020000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20181028T030000\r\nRDATE:20181028T030000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:20190505T030000\r\nRDATE:20190505T030000\r\nRDATE:20200419T030000\r\nRDATE:20210411T030000\r\nRDATE:20220327T030000\r\nEND:DAYLIGHT" + ], + "latitude": "+0333900", + "longitude": "-0073500" + }, + "Africa/Ceuta": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0355300", + "longitude": "-0051900" + }, + "Africa/Conakry": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0093100", + "longitude": "-0134300" + }, + "Africa/Dakar": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0144000", + "longitude": "-0172600" + }, + "Africa/Dar_es_Salaam": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0064800", + "longitude": "+0391700" + }, + "Africa/Djibouti": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0113600", + "longitude": "+0430900" + }, + "Africa/Douala": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0040300", + "longitude": "+0094200" + }, + "Africa/El_Aaiun": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20180325T020000\r\nRDATE:20180325T020000\r\nRDATE:20180617T020000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:20180513T030000\r\nRDATE:20180513T030000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20181028T030000\r\nRDATE:20181028T030000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:20190505T030000\r\nRDATE:20190505T030000\r\nRDATE:20200419T030000\r\nRDATE:20210411T030000\r\nRDATE:20220327T030000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20190609T020000\r\nRDATE:20190609T020000\r\nRDATE:20200524T020000\r\nRDATE:20210516T020000\r\nRDATE:20220508T020000\r\nEND:STANDARD" + ], + "latitude": "+0270900", + "longitude": "-0131200" + }, + "Africa/Freetown": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0083000", + "longitude": "-0131500" + }, + "Africa/Gaborone": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0243900", + "longitude": "+0255500" + }, + "Africa/Harare": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0175000", + "longitude": "+0310300" + }, + "Africa/Johannesburg": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:SAST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0261500", + "longitude": "+0280000" + }, + "Africa/Juba": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0045100", + "longitude": "+0313700" + }, + "Africa/Kampala": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0001900", + "longitude": "+0322500" + }, + "Africa/Khartoum": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0153600", + "longitude": "+0323200" + }, + "Africa/Kigali": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0015700", + "longitude": "+0300400" + }, + "Africa/Kinshasa": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0041800", + "longitude": "+0151800" + }, + "Africa/Lagos": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0062700", + "longitude": "+0032400" + }, + "Africa/Libreville": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0002300", + "longitude": "+0092700" + }, + "Africa/Lome": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0060800", + "longitude": "+0011300" + }, + "Africa/Luanda": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0084800", + "longitude": "+0131400" + }, + "Africa/Lubumbashi": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0114000", + "longitude": "+0272800" + }, + "Africa/Lusaka": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0152500", + "longitude": "+0281700" + }, + "Africa/Malabo": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0034500", + "longitude": "+0084700" + }, + "Africa/Maputo": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0255800", + "longitude": "+0323500" + }, + "Africa/Maseru": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:SAST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0292800", + "longitude": "+0273000" + }, + "Africa/Mbabane": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:SAST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0261800", + "longitude": "+0310600" + }, + "Africa/Mogadishu": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0020400", + "longitude": "+0452200" + }, + "Africa/Monrovia": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0061800", + "longitude": "-0104700" + }, + "Africa/Nairobi": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0011700", + "longitude": "+0364900" + }, + "Africa/Ndjamena": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0120700", + "longitude": "+0150300" + }, + "Africa/Niamey": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0133100", + "longitude": "+0020700" + }, + "Africa/Nouakchott": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0180600", + "longitude": "-0155700" + }, + "Africa/Ouagadougou": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0122200", + "longitude": "-0013100" + }, + "Africa/Porto-Novo": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0062900", + "longitude": "+0023700" + }, + "Africa/Sao_Tome": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:20180101T010000\r\nRDATE:20180101T010000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:20190101T020000\r\nRDATE:20190101T020000\r\nEND:STANDARD" + ], + "latitude": "+0002000", + "longitude": "+0064400" + }, + "Africa/Tripoli": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0325400", + "longitude": "+0131100" + }, + "Africa/Tunis": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0364800", + "longitude": "+0101100" + }, + "Africa/Windhoek": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0223400", + "longitude": "+0170600" + }, + "America/Adak": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-1000\r\nTZOFFSETTO:-0900\r\nTZNAME:HDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-1000\r\nTZNAME:HST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0515248", + "longitude": "-1763929" + }, + "America/Anchorage": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0611305", + "longitude": "-1495401" + }, + "America/Anguilla": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0181200", + "longitude": "-0630400" + }, + "America/Antigua": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0170300", + "longitude": "-0614800" + }, + "America/Araguaina": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0071200", + "longitude": "-0481200" + }, + "America/Argentina/Buenos_Aires": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0343600", + "longitude": "-0582700" + }, + "America/Argentina/Catamarca": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0282800", + "longitude": "-0654700" + }, + "America/Argentina/Cordoba": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0312400", + "longitude": "-0641100" + }, + "America/Argentina/Jujuy": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0241100", + "longitude": "-0651800" + }, + "America/Argentina/La_Rioja": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0292600", + "longitude": "-0665100" + }, + "America/Argentina/Mendoza": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0325300", + "longitude": "-0684900" + }, + "America/Argentina/Rio_Gallegos": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0513800", + "longitude": "-0691300" + }, + "America/Argentina/Salta": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0244700", + "longitude": "-0652500" + }, + "America/Argentina/San_Juan": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0313200", + "longitude": "-0683100" + }, + "America/Argentina/San_Luis": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0331900", + "longitude": "-0662100" + }, + "America/Argentina/Tucuman": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0264900", + "longitude": "-0651300" + }, + "America/Argentina/Ushuaia": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0544800", + "longitude": "-0681800" + }, + "America/Aruba": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0123000", + "longitude": "-0695800" + }, + "America/Asuncion": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19701004T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700322T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=4SU\r\nEND:STANDARD" + ], + "latitude": "-0251600", + "longitude": "-0574000" + }, + "America/Atikokan": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0484531", + "longitude": "-0913718" + }, + "America/Bahia": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0125900", + "longitude": "-0383100" + }, + "America/Bahia_Banderas": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT" + ], + "latitude": "+0204800", + "longitude": "-1051500" + }, + "America/Barbados": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0130600", + "longitude": "-0593700" + }, + "America/Belem": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0012700", + "longitude": "-0482900" + }, + "America/Belize": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0173000", + "longitude": "-0881200" + }, + "America/Blanc-Sablon": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0512500", + "longitude": "-0570700" + }, + "America/Boa_Vista": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0024900", + "longitude": "-0604000" + }, + "America/Bogota": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0043600", + "longitude": "-0740500" + }, + "America/Boise": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0433649", + "longitude": "-1161209" + }, + "America/Cambridge_Bay": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0690650", + "longitude": "-1050310" + }, + "America/Campo_Grande": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20181104T000000\r\nRDATE:20181104T000000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20180218T000000\r\nRDATE:20180218T000000\r\nRDATE:20190217T000000\r\nEND:STANDARD" + ], + "latitude": "-0202700", + "longitude": "-0543700" + }, + "America/Cancun": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0210500", + "longitude": "-0864600" + }, + "America/Caracas": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0103000", + "longitude": "-0665600" + }, + "America/Cayenne": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0045600", + "longitude": "-0522000" + }, + "America/Cayman": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0191800", + "longitude": "-0812300" + }, + "America/Chicago": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0415100", + "longitude": "-0873900" + }, + "America/Chihuahua": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0283800", + "longitude": "-1060500" + }, + "America/Costa_Rica": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0095600", + "longitude": "-0840500" + }, + "America/Creston": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0490600", + "longitude": "-1163100" + }, + "America/Cuiaba": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20181104T000000\r\nRDATE:20181104T000000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20180218T000000\r\nRDATE:20180218T000000\r\nRDATE:20190217T000000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0153500", + "longitude": "-0560500" + }, + "America/Curacao": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0121100", + "longitude": "-0690000" + }, + "America/Danmarkshavn": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0764600", + "longitude": "-0184000" + }, + "America/Dawson": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0640400", + "longitude": "-1392500" + }, + "America/Dawson_Creek": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0594600", + "longitude": "-1201400" + }, + "America/Denver": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0394421", + "longitude": "-1045903" + }, + "America/Detroit": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0421953", + "longitude": "-0830245" + }, + "America/Dominica": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0151800", + "longitude": "-0612400" + }, + "America/Edmonton": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0533300", + "longitude": "-1132800" + }, + "America/Eirunepe": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0064000", + "longitude": "-0695200" + }, + "America/El_Salvador": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0134200", + "longitude": "-0891200" + }, + "America/Fort_Nelson": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0584800", + "longitude": "-1224200" + }, + "America/Fortaleza": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0034300", + "longitude": "-0383000" + }, + "America/Glace_Bay": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:ADT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0461200", + "longitude": "-0595700" + }, + "America/Godthab": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:19700328T220000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=24,25,26,27,28,29,30;BYDAY=SA\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19701024T230000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYMONTHDAY=24,25,26,27,28,29,30;BYDAY=SA\r\nEND:STANDARD" + ], + "latitude": "+0641100", + "longitude": "-0514400" + }, + "America/Goose_Bay": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:ADT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT" + ], + "latitude": "+0532000", + "longitude": "-0602500" + }, + "America/Grand_Turk": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:20181104T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:20190310T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:20180311T020000\r\nRDATE:20180311T020000\r\nEND:DAYLIGHT" + ], + "latitude": "+0212800", + "longitude": "-0710800" + }, + "America/Grenada": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0120300", + "longitude": "-0614500" + }, + "America/Guadeloupe": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0161400", + "longitude": "-0613200" + }, + "America/Guatemala": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0143800", + "longitude": "-0903100" + }, + "America/Guayaquil": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0021000", + "longitude": "-0795000" + }, + "America/Guyana": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0064800", + "longitude": "-0581000" + }, + "America/Halifax": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:ADT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0443900", + "longitude": "-0633600" + }, + "America/Havana": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:CST\r\nDTSTART:19701101T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:CDT\r\nDTSTART:19700308T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT" + ], + "latitude": "+0230800", + "longitude": "-0822200" + }, + "America/Hermosillo": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0290400", + "longitude": "-1105800" + }, + "America/Indiana/Indianapolis": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0394606", + "longitude": "-0860929" + }, + "America/Indiana/Knox": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0411745", + "longitude": "-0863730" + }, + "America/Indiana/Marengo": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0382232", + "longitude": "-0862041" + }, + "America/Indiana/Petersburg": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0382931", + "longitude": "-0871643" + }, + "America/Indiana/Tell_City": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0375711", + "longitude": "-0864541" + }, + "America/Indiana/Vevay": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0384452", + "longitude": "-0850402" + }, + "America/Indiana/Vincennes": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0384038", + "longitude": "-0873143" + }, + "America/Indiana/Winamac": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT" + ], + "latitude": "+0410305", + "longitude": "-0863611" + }, + "America/Inuvik": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0682059", + "longitude": "-1334300" + }, + "America/Iqaluit": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0634400", + "longitude": "-0682800" + }, + "America/Jamaica": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0175805", + "longitude": "-0764736" + }, + "America/Juneau": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0581807", + "longitude": "-1342511" + }, + "America/Kentucky/Louisville": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0381515", + "longitude": "-0854534" + }, + "America/Kentucky/Monticello": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0364947", + "longitude": "-0845057" + }, + "America/Kralendijk": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0120903", + "longitude": "-0681636" + }, + "America/La_Paz": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0163000", + "longitude": "-0680900" + }, + "America/Lima": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0120300", + "longitude": "-0770300" + }, + "America/Los_Angeles": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0340308", + "longitude": "-1181434" + }, + "America/Lower_Princes": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0180305", + "longitude": "-0630250" + }, + "America/Maceio": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0094000", + "longitude": "-0354300" + }, + "America/Managua": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0120900", + "longitude": "-0861700" + }, + "America/Manaus": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0030800", + "longitude": "-0600100" + }, + "America/Marigot": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0180400", + "longitude": "-0630500" + }, + "America/Martinique": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0143600", + "longitude": "-0610500" + }, + "America/Matamoros": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0255000", + "longitude": "-0973000" + }, + "America/Mazatlan": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0231300", + "longitude": "-1062500" + }, + "America/Menominee": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0450628", + "longitude": "-0873651" + }, + "America/Merida": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0205800", + "longitude": "-0893700" + }, + "America/Metlakatla": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:20191103T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:20181104T020000\r\nRDATE:20181104T020000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:20190120T020000\r\nRDATE:20190120T020000\r\nEND:STANDARD" + ], + "latitude": "+0550737", + "longitude": "-1313435" + }, + "America/Mexico_City": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0192400", + "longitude": "-0990900" + }, + "America/Miquelon": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0470300", + "longitude": "-0562000" + }, + "America/Moncton": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:ADT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0460600", + "longitude": "-0644700" + }, + "America/Monterrey": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0254000", + "longitude": "-1001900" + }, + "America/Montevideo": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0345433", + "longitude": "-0561245" + }, + "America/Montserrat": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0164300", + "longitude": "-0621300" + }, + "America/Nassau": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0250500", + "longitude": "-0772100" + }, + "America/New_York": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0404251", + "longitude": "-0740023" + }, + "America/Nipigon": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0490100", + "longitude": "-0881600" + }, + "America/Nome": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0643004", + "longitude": "-1652423" + }, + "America/Noronha": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0035100", + "longitude": "-0322500" + }, + "America/North_Dakota/Beulah": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0471551", + "longitude": "-1014640" + }, + "America/North_Dakota/Center": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0470659", + "longitude": "-1011757" + }, + "America/North_Dakota/New_Salem": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0465042", + "longitude": "-1012439" + }, + "America/Ojinaga": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0293400", + "longitude": "-1042500" + }, + "America/Panama": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0085800", + "longitude": "-0793200" + }, + "America/Pangnirtung": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0660800", + "longitude": "-0654400" + }, + "America/Paramaribo": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0055000", + "longitude": "-0551000" + }, + "America/Phoenix": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0332654", + "longitude": "-1120424" + }, + "America/Port-au-Prince": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0183200", + "longitude": "-0722000" + }, + "America/Port_of_Spain": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0103900", + "longitude": "-0613100" + }, + "America/Porto_Velho": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0084600", + "longitude": "-0635400" + }, + "America/Puerto_Rico": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0182806", + "longitude": "-0660622" + }, + "America/Punta_Arenas": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0530900", + "longitude": "-0705500" + }, + "America/Rainy_River": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0484300", + "longitude": "-0943400" + }, + "America/Rankin_Inlet": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0624900", + "longitude": "-0920459" + }, + "America/Recife": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0080300", + "longitude": "-0345400" + }, + "America/Regina": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0502400", + "longitude": "-1043900" + }, + "America/Resolute": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT" + ], + "latitude": "+0744144", + "longitude": "-0944945" + }, + "America/Rio_Branco": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0095800", + "longitude": "-0674800" + }, + "America/Santarem": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0022600", + "longitude": "-0545200" + }, + "America/Santiago": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20190407T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYMONTHDAY=2,3,4,5,6,7,8;BYDAY=SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20190908T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYMONTHDAY=2,3,4,5,6,7,8;BYDAY=SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20180812T000000\r\nRDATE:20180812T000000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20180513T000000\r\nRDATE:20180513T000000\r\nEND:STANDARD" + ], + "latitude": "-0332700", + "longitude": "-0704000" + }, + "America/Santo_Domingo": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0182800", + "longitude": "-0695400" + }, + "America/Sao_Paulo": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:20181104T000000\r\nRDATE:20181104T000000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20180218T000000\r\nRDATE:20180218T000000\r\nRDATE:20190217T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:19700101T000000\r\nEND:DAYLIGHT" + ], + "latitude": "-0233200", + "longitude": "-0463700" + }, + "America/Scoresbysund": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19700329T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:-0100\r\nTZNAME:-01\r\nDTSTART:19701025T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0702900", + "longitude": "-0215800" + }, + "America/Sitka": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0571035", + "longitude": "-1351807" + }, + "America/St_Barthelemy": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0175300", + "longitude": "-0625100" + }, + "America/St_Johns": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0230\r\nTZOFFSETTO:-0330\r\nTZNAME:NST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0330\r\nTZOFFSETTO:-0230\r\nTZNAME:NDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT" + ], + "latitude": "+0473400", + "longitude": "-0524300" + }, + "America/St_Kitts": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0171800", + "longitude": "-0624300" + }, + "America/St_Lucia": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0140100", + "longitude": "-0610000" + }, + "America/St_Thomas": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0182100", + "longitude": "-0645600" + }, + "America/St_Vincent": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0130900", + "longitude": "-0611400" + }, + "America/Swift_Current": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0501700", + "longitude": "-1075000" + }, + "America/Tegucigalpa": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0140600", + "longitude": "-0871300" + }, + "America/Thule": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:ADT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0763400", + "longitude": "-0684700" + }, + "America/Thunder_Bay": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0482300", + "longitude": "-0891500" + }, + "America/Tijuana": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0323200", + "longitude": "-1170100" + }, + "America/Toronto": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0433900", + "longitude": "-0792300" + }, + "America/Tortola": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0182700", + "longitude": "-0643700" + }, + "America/Vancouver": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0491600", + "longitude": "-1230700" + }, + "America/Whitehorse": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0604300", + "longitude": "-1350300" + }, + "America/Winnipeg": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0495300", + "longitude": "-0970900" + }, + "America/Yakutat": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0593249", + "longitude": "-1394338" + }, + "America/Yellowknife": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0622700", + "longitude": "-1142100" + }, + "Antarctica/Casey": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:20180311T040000\r\nRDATE:20180311T040000\r\nEND:STANDARD" + ], + "latitude": "-0661700", + "longitude": "+1103100" + }, + "Antarctica/Davis": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0683500", + "longitude": "+0775800" + }, + "Antarctica/DumontDUrville": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:+10\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0664000", + "longitude": "+1400100" + }, + "Antarctica/Macquarie": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0543000", + "longitude": "+1585700" + }, + "Antarctica/Mawson": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0673600", + "longitude": "+0625300" + }, + "Antarctica/McMurdo": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1300\r\nTZNAME:NZDT\r\nDTSTART:19700927T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1200\r\nTZNAME:NZST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "-0775000", + "longitude": "+1663600" + }, + "Antarctica/Palmer": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0644800", + "longitude": "-0640600" + }, + "Antarctica/Rothera": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0673400", + "longitude": "-0680800" + }, + "Antarctica/Syowa": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0690022", + "longitude": "+0393524" + }, + "Antarctica/Troll": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0200\r\nTZNAME:+02\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "-0720041", + "longitude": "+0023206" + }, + "Antarctica/Vostok": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0782400", + "longitude": "+1065400" + }, + "Arctic/Longyearbyen": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0780000", + "longitude": "+0160000" + }, + "Asia/Aden": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0124500", + "longitude": "+0451200" + }, + "Asia/Almaty": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0431500", + "longitude": "+0765700" + }, + "Asia/Amman": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700326T235959\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1TH\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701030T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1FR\r\nEND:STANDARD" + ], + "latitude": "+0315700", + "longitude": "+0355600" + }, + "Asia/Anadyr": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0644500", + "longitude": "+1772900" + }, + "Asia/Aqtau": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0443100", + "longitude": "+0501600" + }, + "Asia/Aqtobe": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0501700", + "longitude": "+0571000" + }, + "Asia/Ashgabat": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0375700", + "longitude": "+0582300" + }, + "Asia/Atyrau": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0470700", + "longitude": "+0515600" + }, + "Asia/Baghdad": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0332100", + "longitude": "+0442500" + }, + "Asia/Bahrain": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0262300", + "longitude": "+0503500" + }, + "Asia/Baku": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0402300", + "longitude": "+0495100" + }, + "Asia/Bangkok": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0134500", + "longitude": "+1003100" + }, + "Asia/Barnaul": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0532200", + "longitude": "+0834500" + }, + "Asia/Beirut": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0335300", + "longitude": "+0353000" + }, + "Asia/Bishkek": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0425400", + "longitude": "+0743600" + }, + "Asia/Brunei": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0045600", + "longitude": "+1145500" + }, + "Asia/Chita": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:+09\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0520300", + "longitude": "+1132800" + }, + "Asia/Choibalsan": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0480400", + "longitude": "+1143000" + }, + "Asia/Colombo": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0530\r\nTZOFFSETTO:+0530\r\nTZNAME:+0530\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0065600", + "longitude": "+0795100" + }, + "Asia/Damascus": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701030T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1FR\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700327T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1FR\r\nEND:DAYLIGHT" + ], + "latitude": "+0333000", + "longitude": "+0361800" + }, + "Asia/Dhaka": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0234300", + "longitude": "+0902500" + }, + "Asia/Dili": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:+09\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0083300", + "longitude": "+1253500" + }, + "Asia/Dubai": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0251800", + "longitude": "+0551800" + }, + "Asia/Dushanbe": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0383500", + "longitude": "+0684800" + }, + "Asia/Famagusta": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:20180325T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT" + ], + "latitude": "+0350700", + "longitude": "+0335700" + }, + "Asia/Gaza": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701031T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SA\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:20190329T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1FR\r\nEND:DAYLIGHT", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:20180324T010000\r\nRDATE:20180324T010000\r\nEND:DAYLIGHT" + ], + "latitude": "+0313000", + "longitude": "+0342800" + }, + "Asia/Hebron": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701031T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SA\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:20190329T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1FR\r\nEND:DAYLIGHT", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:20180324T010000\r\nRDATE:20180324T010000\r\nEND:DAYLIGHT" + ], + "latitude": "+0313200", + "longitude": "+0350542" + }, + "Asia/Ho_Chi_Minh": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0104500", + "longitude": "+1064000" + }, + "Asia/Hong_Kong": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:HKT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0221700", + "longitude": "+1140900" + }, + "Asia/Hovd": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0480100", + "longitude": "+0913900" + }, + "Asia/Irkutsk": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0521600", + "longitude": "+1042000" + }, + "Asia/Istanbul": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0410100", + "longitude": "+0285800" + }, + "Asia/Jakarta": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:WIB\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0061000", + "longitude": "+1064800" + }, + "Asia/Jayapura": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:WIT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0023200", + "longitude": "+1404200" + }, + "Asia/Jerusalem": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:IDT\r\nDTSTART:19700327T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=23,24,25,26,27,28,29;BYDAY=FR\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:IST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0314650", + "longitude": "+0351326" + }, + "Asia/Kabul": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0430\r\nTZOFFSETTO:+0430\r\nTZNAME:+0430\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0343100", + "longitude": "+0691200" + }, + "Asia/Kamchatka": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0530100", + "longitude": "+1583900" + }, + "Asia/Karachi": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:PKT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0245200", + "longitude": "+0670300" + }, + "Asia/Kathmandu": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0545\r\nTZOFFSETTO:+0545\r\nTZNAME:+0545\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0274300", + "longitude": "+0851900" + }, + "Asia/Khandyga": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:+09\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0623923", + "longitude": "+1353314" + }, + "Asia/Kolkata": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0530\r\nTZOFFSETTO:+0530\r\nTZNAME:IST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0223200", + "longitude": "+0882200" + }, + "Asia/Krasnoyarsk": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0560100", + "longitude": "+0925000" + }, + "Asia/Kuala_Lumpur": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0031000", + "longitude": "+1014200" + }, + "Asia/Kuching": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0013300", + "longitude": "+1102000" + }, + "Asia/Kuwait": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0292000", + "longitude": "+0475900" + }, + "Asia/Macau": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0221150", + "longitude": "+1133230" + }, + "Asia/Magadan": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0593400", + "longitude": "+1504800" + }, + "Asia/Makassar": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:WITA\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0050700", + "longitude": "+1192400" + }, + "Asia/Manila": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:PST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0143500", + "longitude": "+1210000" + }, + "Asia/Muscat": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0233600", + "longitude": "+0583500" + }, + "Asia/Nicosia": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT" + ], + "latitude": "+0351000", + "longitude": "+0332200" + }, + "Asia/Novokuznetsk": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0534500", + "longitude": "+0870700" + }, + "Asia/Novosibirsk": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0550200", + "longitude": "+0825500" + }, + "Asia/Omsk": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0550000", + "longitude": "+0732400" + }, + "Asia/Oral": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0511300", + "longitude": "+0512100" + }, + "Asia/Phnom_Penh": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0113300", + "longitude": "+1045500" + }, + "Asia/Pontianak": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:WIB\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0000200", + "longitude": "+1092000" + }, + "Asia/Pyongyang": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0830\r\nTZNAME:KST\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0830\r\nTZOFFSETTO:+0900\r\nTZNAME:KST\r\nDTSTART:20180504T233000\r\nRDATE:20180504T233000\r\nEND:STANDARD" + ], + "latitude": "+0390100", + "longitude": "+1254500" + }, + "Asia/Qatar": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0251700", + "longitude": "+0513200" + }, + "Asia/Qostanay": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0531200", + "longitude": "+0633700" + }, + "Asia/Qyzylorda": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:20181221T000000\r\nRDATE:20181221T000000\r\nEND:STANDARD" + ], + "latitude": "+0444800", + "longitude": "+0652800" + }, + "Asia/Riyadh": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0243800", + "longitude": "+0464300" + }, + "Asia/Sakhalin": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0465800", + "longitude": "+1424200" + }, + "Asia/Samarkand": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0394000", + "longitude": "+0664800" + }, + "Asia/Seoul": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:KST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0373300", + "longitude": "+1265800" + }, + "Asia/Shanghai": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0311400", + "longitude": "+1212800" + }, + "Asia/Singapore": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0011700", + "longitude": "+1035100" + }, + "Asia/Srednekolymsk": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0672800", + "longitude": "+1534300" + }, + "Asia/Taipei": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0250300", + "longitude": "+1213000" + }, + "Asia/Tashkent": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0412000", + "longitude": "+0691800" + }, + "Asia/Tbilisi": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0414300", + "longitude": "+0444900" + }, + "Asia/Tehran": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0330\r\nTZNAME:+0330\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0330\r\nTZOFFSETTO:+0430\r\nTZNAME:+0430\r\nDTSTART:20180321T235959\r\nRDATE:20180321T235959\r\nRDATE:20190321T235959\r\nRDATE:20200320T235959\r\nRDATE:20210321T235959\r\nRDATE:20220321T235959\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0430\r\nTZOFFSETTO:+0330\r\nTZNAME:+0330\r\nDTSTART:20180921T235959\r\nRDATE:20180921T235959\r\nRDATE:20190921T235959\r\nRDATE:20200920T235959\r\nRDATE:20210921T235959\r\nRDATE:20220921T235959\r\nEND:STANDARD" + ], + "latitude": "+0354000", + "longitude": "+0512600" + }, + "Asia/Thimphu": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0272800", + "longitude": "+0893900" + }, + "Asia/Tokyo": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:JST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0353916", + "longitude": "+1394441" + }, + "Asia/Tomsk": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0563000", + "longitude": "+0845800" + }, + "Asia/Ulaanbaatar": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0475500", + "longitude": "+1065300" + }, + "Asia/Urumqi": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0434800", + "longitude": "+0873500" + }, + "Asia/Ust-Nera": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:+10\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0643337", + "longitude": "+1431336" + }, + "Asia/Vientiane": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0175800", + "longitude": "+1023600" + }, + "Asia/Vladivostok": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:+10\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0431000", + "longitude": "+1315600" + }, + "Asia/Yakutsk": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:+09\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0620000", + "longitude": "+1294000" + }, + "Asia/Yangon": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0630\r\nTZOFFSETTO:+0630\r\nTZNAME:+0630\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0164700", + "longitude": "+0961000" + }, + "Asia/Yekaterinburg": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0565100", + "longitude": "+0603600" + }, + "Asia/Yerevan": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0401100", + "longitude": "+0443000" + }, + "Atlantic/Azores": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19700329T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:-0100\r\nTZNAME:-01\r\nDTSTART:19701025T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0374400", + "longitude": "-0254000" + }, + "Atlantic/Bermuda": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:ADT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "+0321700", + "longitude": "-0644600" + }, + "Atlantic/Canary": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:WEST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:WET\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0280600", + "longitude": "-0152400" + }, + "Atlantic/Cape_Verde": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0100\r\nTZOFFSETTO:-0100\r\nTZNAME:-01\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0145500", + "longitude": "-0233100" + }, + "Atlantic/Faroe": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:WEST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:WET\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0620100", + "longitude": "-0064600" + }, + "Atlantic/Madeira": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:WEST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:WET\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0323800", + "longitude": "-0165400" + }, + "Atlantic/Reykjavik": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0640900", + "longitude": "-0215100" + }, + "Atlantic/South_Georgia": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0541600", + "longitude": "-0363200" + }, + "Atlantic/St_Helena": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0155500", + "longitude": "-0054200" + }, + "Atlantic/Stanley": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0514200", + "longitude": "-0575100" + }, + "Australia/Adelaide": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1030\r\nTZOFFSETTO:+0930\r\nTZNAME:ACST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0930\r\nTZOFFSETTO:+1030\r\nTZNAME:ACDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT" + ], + "latitude": "-0345500", + "longitude": "+1383500" + }, + "Australia/Brisbane": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0272800", + "longitude": "+1530200" + }, + "Australia/Broken_Hill": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1030\r\nTZOFFSETTO:+0930\r\nTZNAME:ACST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0930\r\nTZOFFSETTO:+1030\r\nTZNAME:ACDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT" + ], + "latitude": "-0315700", + "longitude": "+1412700" + }, + "Australia/Currie": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1100\r\nTZNAME:AEDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "-0395600", + "longitude": "+1435200" + }, + "Australia/Darwin": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0930\r\nTZOFFSETTO:+0930\r\nTZNAME:ACST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0122800", + "longitude": "+1305000" + }, + "Australia/Eucla": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0845\r\nTZOFFSETTO:+0845\r\nTZNAME:+0845\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0314300", + "longitude": "+1285200" + }, + "Australia/Hobart": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1100\r\nTZNAME:AEDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "-0425300", + "longitude": "+1471900" + }, + "Australia/Lindeman": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0201600", + "longitude": "+1490000" + }, + "Australia/Lord_Howe": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1030\r\nTZNAME:+1030\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1030\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT" + ], + "latitude": "-0313300", + "longitude": "+1590500" + }, + "Australia/Melbourne": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1100\r\nTZNAME:AEDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT" + ], + "latitude": "-0374900", + "longitude": "+1445800" + }, + "Australia/Perth": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:AWST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0315700", + "longitude": "+1155100" + }, + "Australia/Sydney": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1100\r\nTZNAME:AEDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT" + ], + "latitude": "-0335200", + "longitude": "+1511300" + }, + "Europe/Amsterdam": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0522200", + "longitude": "+0045400" + }, + "Europe/Andorra": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0423000", + "longitude": "+0013100" + }, + "Europe/Astrakhan": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0462100", + "longitude": "+0480300" + }, + "Europe/Athens": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0375800", + "longitude": "+0234300" + }, + "Europe/Belgrade": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0445000", + "longitude": "+0203000" + }, + "Europe/Berlin": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0523000", + "longitude": "+0132200" + }, + "Europe/Bratislava": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0480900", + "longitude": "+0170700" + }, + "Europe/Brussels": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0505000", + "longitude": "+0042000" + }, + "Europe/Bucharest": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0442600", + "longitude": "+0260600" + }, + "Europe/Budapest": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0473000", + "longitude": "+0190500" + }, + "Europe/Busingen": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0474200", + "longitude": "+0084100" + }, + "Europe/Chisinau": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0470000", + "longitude": "+0285000" + }, + "Europe/Copenhagen": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0554000", + "longitude": "+0123500" + }, + "Europe/Dublin": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:IST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:DAYLIGHT" + ], + "latitude": "+0532000", + "longitude": "-0061500" + }, + "Europe/Gibraltar": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0360800", + "longitude": "-0052100" + }, + "Europe/Guernsey": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:BST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0492717", + "longitude": "-0023210" + }, + "Europe/Helsinki": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0601000", + "longitude": "+0245800" + }, + "Europe/Isle_of_Man": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:BST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0540900", + "longitude": "-0042800" + }, + "Europe/Istanbul": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0410100", + "longitude": "+0285800" + }, + "Europe/Jersey": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:BST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0491101", + "longitude": "-0020624" + }, + "Europe/Kaliningrad": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0544300", + "longitude": "+0203000" + }, + "Europe/Kiev": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0502600", + "longitude": "+0303100" + }, + "Europe/Kirov": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0583600", + "longitude": "+0493900" + }, + "Europe/Lisbon": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:WET\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:WEST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT" + ], + "latitude": "+0384300", + "longitude": "-0090800" + }, + "Europe/Ljubljana": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0460300", + "longitude": "+0143100" + }, + "Europe/London": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:BST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0513030", + "longitude": "+0000731" + }, + "Europe/Luxembourg": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0493600", + "longitude": "+0060900" + }, + "Europe/Madrid": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0402400", + "longitude": "-0034100" + }, + "Europe/Malta": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0355400", + "longitude": "+0143100" + }, + "Europe/Mariehamn": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0600600", + "longitude": "+0195700" + }, + "Europe/Minsk": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0535400", + "longitude": "+0273400" + }, + "Europe/Monaco": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0434200", + "longitude": "+0072300" + }, + "Europe/Moscow": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:MSK\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0554521", + "longitude": "+0373704" + }, + "Europe/Nicosia": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT" + ], + "latitude": "+0351000", + "longitude": "+0332200" + }, + "Europe/Oslo": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0595500", + "longitude": "+0104500" + }, + "Europe/Paris": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0485200", + "longitude": "+0022000" + }, + "Europe/Podgorica": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0422600", + "longitude": "+0191600" + }, + "Europe/Prague": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0500500", + "longitude": "+0142600" + }, + "Europe/Riga": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0565700", + "longitude": "+0240600" + }, + "Europe/Rome": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0415400", + "longitude": "+0122900" + }, + "Europe/Samara": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0531200", + "longitude": "+0500900" + }, + "Europe/San_Marino": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0435500", + "longitude": "+0122800" + }, + "Europe/Sarajevo": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0435200", + "longitude": "+0182500" + }, + "Europe/Saratov": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0513400", + "longitude": "+0460200" + }, + "Europe/Simferopol": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:MSK\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0445700", + "longitude": "+0340600" + }, + "Europe/Skopje": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0415900", + "longitude": "+0212600" + }, + "Europe/Sofia": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0424100", + "longitude": "+0231900" + }, + "Europe/Stockholm": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0592000", + "longitude": "+0180300" + }, + "Europe/Tallinn": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0592500", + "longitude": "+0244500" + }, + "Europe/Tirane": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0412000", + "longitude": "+0195000" + }, + "Europe/Ulyanovsk": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0542000", + "longitude": "+0482400" + }, + "Europe/Uzhgorod": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0483700", + "longitude": "+0221800" + }, + "Europe/Vaduz": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0470900", + "longitude": "+0093100" + }, + "Europe/Vatican": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0415408", + "longitude": "+0122711" + }, + "Europe/Vienna": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0481300", + "longitude": "+0162000" + }, + "Europe/Vilnius": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0544100", + "longitude": "+0251900" + }, + "Europe/Volgograd": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:20181028T020000\r\nRDATE:20181028T020000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0484400", + "longitude": "+0442500" + }, + "Europe/Warsaw": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0521500", + "longitude": "+0210000" + }, + "Europe/Zagreb": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0454800", + "longitude": "+0155800" + }, + "Europe/Zaporozhye": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0475000", + "longitude": "+0351000" + }, + "Europe/Zurich": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + ], + "latitude": "+0472300", + "longitude": "+0083200" + }, + "Indian/Antananarivo": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0185500", + "longitude": "+0473100" + }, + "Indian/Chagos": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0072000", + "longitude": "+0722500" + }, + "Indian/Christmas": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0102500", + "longitude": "+1054300" + }, + "Indian/Cocos": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0630\r\nTZOFFSETTO:+0630\r\nTZNAME:+0630\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0121000", + "longitude": "+0965500" + }, + "Indian/Comoro": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0114100", + "longitude": "+0431600" + }, + "Indian/Kerguelen": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0492110", + "longitude": "+0701303" + }, + "Indian/Mahe": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0044000", + "longitude": "+0552800" + }, + "Indian/Maldives": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0041000", + "longitude": "+0733000" + }, + "Indian/Mauritius": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0201000", + "longitude": "+0573000" + }, + "Indian/Mayotte": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0124700", + "longitude": "+0451400" + }, + "Indian/Reunion": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0205200", + "longitude": "+0552800" + }, + "Pacific/Apia": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1400\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:19700405T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1400\r\nTZNAME:+14\r\nDTSTART:19700927T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU\r\nEND:DAYLIGHT" + ], + "latitude": "-0135000", + "longitude": "-1714400" + }, + "Pacific/Auckland": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1300\r\nTZNAME:NZDT\r\nDTSTART:19700927T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1200\r\nTZNAME:NZST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "-0365200", + "longitude": "+1744600" + }, + "Pacific/Bougainville": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0061300", + "longitude": "+1553400" + }, + "Pacific/Chatham": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1245\r\nTZOFFSETTO:+1345\r\nTZNAME:+1345\r\nDTSTART:19700927T024500\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1345\r\nTZOFFSETTO:+1245\r\nTZNAME:+1245\r\nDTSTART:19700405T034500\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD" + ], + "latitude": "-0435700", + "longitude": "-1763300" + }, + "Pacific/Chuuk": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:+10\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0072500", + "longitude": "+1514700" + }, + "Pacific/Easter": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:-06\r\nDTSTART:20190406T220000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SA\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:20190907T220000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=1SA\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:-06\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:20180811T220000\r\nRDATE:20180811T220000\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:-06\r\nDTSTART:20180512T220000\r\nRDATE:20180512T220000\r\nEND:STANDARD" + ], + "latitude": "-0270900", + "longitude": "-1092600" + }, + "Pacific/Efate": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0174000", + "longitude": "+1682500" + }, + "Pacific/Enderbury": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0030800", + "longitude": "-1710500" + }, + "Pacific/Fakaofo": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0092200", + "longitude": "-1711400" + }, + "Pacific/Fiji": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700118T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=12,13,14,15,16,17,18;BYDAY=SU\r\nEND:STANDARD", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:20191110T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=2SU\r\nEND:DAYLIGHT", + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:20181104T020000\r\nRDATE:20181104T020000\r\nEND:DAYLIGHT" + ], + "latitude": "-0180800", + "longitude": "+1782500" + }, + "Pacific/Funafuti": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0083100", + "longitude": "+1791300" + }, + "Pacific/Galapagos": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:-06\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0005400", + "longitude": "-0893600" + }, + "Pacific/Gambier": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0900\r\nTZNAME:-09\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0230800", + "longitude": "-1345700" + }, + "Pacific/Guadalcanal": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0093200", + "longitude": "+1601200" + }, + "Pacific/Guam": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:ChST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0132800", + "longitude": "+1444500" + }, + "Pacific/Honolulu": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-1000\r\nTZOFFSETTO:-1000\r\nTZNAME:HST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0211825", + "longitude": "-1575130" + }, + "Pacific/Kiritimati": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1400\r\nTZOFFSETTO:+1400\r\nTZNAME:+14\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0015200", + "longitude": "-1572000" + }, + "Pacific/Kosrae": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0051900", + "longitude": "+1625900" + }, + "Pacific/Kwajalein": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0090500", + "longitude": "+1672000" + }, + "Pacific/Majuro": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0070900", + "longitude": "+1711200" + }, + "Pacific/Marquesas": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0930\r\nTZOFFSETTO:-0930\r\nTZNAME:-0930\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0090000", + "longitude": "-1393000" + }, + "Pacific/Midway": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-1100\r\nTZOFFSETTO:-1100\r\nTZNAME:SST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0281300", + "longitude": "-1772200" + }, + "Pacific/Nauru": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0003100", + "longitude": "+1665500" + }, + "Pacific/Niue": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-1100\r\nTZOFFSETTO:-1100\r\nTZNAME:-11\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0190100", + "longitude": "-1695500" + }, + "Pacific/Norfolk": { + "ics": [ + "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:20191006T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:20200405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1130\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD", + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:20190701T000000\r\nRDATE:20190701T000000\r\nEND:STANDARD" + ], + "latitude": "-0290300", + "longitude": "+1675800" + }, + "Pacific/Noumea": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0221600", + "longitude": "+1662700" + }, + "Pacific/Pago_Pago": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-1100\r\nTZOFFSETTO:-1100\r\nTZNAME:SST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0141600", + "longitude": "-1704200" + }, + "Pacific/Palau": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:+09\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0072000", + "longitude": "+1342900" + }, + "Pacific/Pitcairn": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0800\r\nTZNAME:-08\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0250400", + "longitude": "-1300500" + }, + "Pacific/Pohnpei": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0065800", + "longitude": "+1581300" + }, + "Pacific/Port_Moresby": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:+10\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0093000", + "longitude": "+1471000" + }, + "Pacific/Rarotonga": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-1000\r\nTZOFFSETTO:-1000\r\nTZNAME:-10\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0211400", + "longitude": "-1594600" + }, + "Pacific/Saipan": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:ChST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0151200", + "longitude": "+1454500" + }, + "Pacific/Tahiti": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:-1000\r\nTZOFFSETTO:-1000\r\nTZNAME:-10\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0173200", + "longitude": "-1493400" + }, + "Pacific/Tarawa": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0012500", + "longitude": "+1730000" + }, + "Pacific/Tongatapu": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0211000", + "longitude": "-1751000" + }, + "Pacific/Wake": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "+0191700", + "longitude": "+1663700" + }, + "Pacific/Wallis": { + "ics": [ + "BEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD" + ], + "latitude": "-0131800", + "longitude": "-1761000" + } + } +} diff --git a/core/date/time-zone.js b/core/date/time-zone.js new file mode 100644 index 0000000000..87fafb5745 --- /dev/null +++ b/core/date/time-zone.js @@ -0,0 +1,85 @@ +/* + Time Zones definitions at: + + https://hg.mozilla.org/comm-central/raw-file/tip/calendar/timezones/zones.json + +*/ +var Montage = require("../core").Montage, + ICAL = require("ical.js"), + ICAL_Timezone = ICAL.Timezone, + ICAL_Timezone_Prototype = ICAL.Timezone.prototype, + ICAL_TimezoneService = ICAL.TimezoneService, + currentEnvironment = require("../environment").currentEnvironment, + + //We really need to find a way to load only the data for the timezone we care about. + //So this file should either be split in individual files or we need to find/build an API that does so + // 1. https://developers.google.com/maps/documentation/timezone/intro //not ics + // 2. https://ipgeolocation.io/documentation/timezone-api.html + // 3. https://www.amdoren.com/time-zone-api/ + // 4. http://worldtimeapi.org + // 5. https://timezoneapi.io/developers/timezone + zonesData = require("./time-zone-data/zones.json"), + TimeZone = exports.TimeZone = ICAL_Timezone, + TimeZonePrototype = TimeZone.prototype; + +(function registerTimezones() { + Object.keys(timezones).forEach(function(key) { + var icsData = timezones[key], + dataToParse = "BEGIN:VCALENDAR\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\nVERSION:2.0\n", + parsed, + // parsed = ICAL.parse(`BEGIN:VCALENDAR\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\nVERSION:2.0\n${icsData}\nEND:VCALENDAR`), + comp, + vtimezone; + + dataToParse += icsData; + dataToParse += "\nEND:VCALENDAR"; + parsed = ICAL.parse(dataToParse); + comp = new ICAL.Component(parsed); + vtimezone = comp.getFirstSubcomponent('vtimezone'); + + + ICAL.TimezoneService.register(key, new ICAL.Timezone(vtimezone)); + }); +})(zonesData); + + +/** + * Returns a TimeZone a given identifier. + * + * + * @function + * @param {CalendarIdentifier} calendarIdentifier The module id of the HTML page to load. + * @returns {Calendar} a new Calendar instance. + */ + +TimeZone.withIdentifier = function(timeZoneIdentifier) { + return ICAL_TimezoneService.get(timeZoneIdentifier); +} + +/** + * Returns a TimeZone a given identifier. + * + * + * @function + * @param {CalendarIdentifier} calendarIdentifier The module id of the HTML page to load. + * @returns {Calendar} a new Calendar instance. + */ + +TimeZone.systemTimeZone = function() { + var systemLocaleIdentifier = currentEnvironment.systemLocaleIdentifier, + resolvedOptions = Intl.DateTimeFormat(navigatorLocaleIdentifier).resolvedOptions(), + timeZone = resolvedOptions.timeZone; /* "America/Los_Angeles" */ + return ICAL_TimezoneService.get(timeZone); +}; + +/** + * Convert a calendarDate from one timeZone zone to another. + * + * @param {CalendarDate} calendarDate The calendarDate to convert + * @param {TimeZone} fromTimeZone The source zone to convert from + * @param {TimeZone} toTimeZone The target zone to convert to + * @return {CalendarDate} The converted calendarDate object + */ + +TimeZone.convertCalendarDateFromTimeZoneToTimeZone = CAL.Timezone.convert_time; + diff --git a/core/date/tools/compile-zones.js b/core/date/tools/compile-zones.js new file mode 100644 index 0000000000..d5b8798700 --- /dev/null +++ b/core/date/tools/compile-zones.js @@ -0,0 +1,33 @@ +'use strict'; + +/* + From https://github.com/mifi/ical-expander/ + + MIT License + + Copyright (c) 2016 Mikael Finstad + +*/ + +/* eslint-disable no-console */ + +const fs = require('fs'); + +const zonesJson = fs.readFileSync('./zones.json'); +const zones = JSON.parse(zonesJson); + +const out = {}; +Object.keys(zones.zones).forEach((z) => { + out[z] = zones.zones[z].ics; +}); + +Object.keys(zones.aliases).forEach((z) => { + const aliasTo = zones.aliases[z].aliasTo; + if (zones.zones[aliasTo]) { + out[z] = zones.zones[aliasTo].ics; + } else { + console.warn(`${aliasTo} (${z}) not found, skipping`); + } +}); + +fs.writeFileSync('./zones-compiled.json', JSON.stringify(out)); diff --git a/core/extras/date.js b/core/extras/date.js index b9ab4e6392..004a1a4502 100644 --- a/core/extras/date.js +++ b/core/extras/date.js @@ -5,6 +5,8 @@ * @see {external:Date} */ +var TimeZone = require("../date/time-zone"); + /** * @external */ @@ -186,6 +188,27 @@ Number.prototype.toPaddedString = function(len , fillchar) { _parseRFC3339.endsByZ = /Z$/i; Date.parseRFC3339 = _parseRFC3339; + + +/** + * Returns an equivalent CalendarDate to aDate (in UTC / local timeZone)in Calendar's timeZone. + * + * @function + * @param {Date} aDate The date for which to perform the calculation. + * @returns {CalendarDate} true if the given date matches the given components, otherwise false. + */ + +Object.defineProperty(Date.prototype, "calendarDateInTimeZone", { + value: function (timeZone) { + var aCalendarDate = CalendarDate.fromJSDate(this, true /*useUTC*/); + TimeZone.convertCalendarDateFromTimeZoneToTimeZone(aCalendarDate,Timezone.utcTimezone,timeZone); + return aCalendarDate; +}, + writable: true, + configurable: true +}); + + /********** * Part of https://github.com/tardate/rfc3339date.js/. * But it not only conflicts with a Date.parse override currently done in date-converter.js, From ce12e9ddfcfe7a77dc81a8836da74f838c5f76e7 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 14:46:43 -0700 Subject: [PATCH 151/407] bring frb in montage --- core/frb/.editorconfig | 11 + core/frb/.gitignore | 2 + core/frb/.travis.yml | 19 + core/frb/.vscode/launch.json | 20 + core/frb/CHANGES.md | 274 + core/frb/CONTRIBUTING.md | 12 + core/frb/LICENSE.md | 31 + core/frb/README.md | 3111 +++++++++++ core/frb/algebra.js | 144 + core/frb/assign.js | 23 + core/frb/bind.js | 279 + core/frb/binders.js | 459 ++ core/frb/bindings.js | 118 + core/frb/checklist.csv | 97 + core/frb/compile-assigner.js | 207 + core/frb/compile-binder.js | 131 + core/frb/compile-evaluator.js | 366 ++ core/frb/compile-observer.js | 126 + core/frb/compute.js | 77 + core/frb/dom.js | 61 + core/frb/evaluate.js | 22 + core/frb/expand.js | 72 + core/frb/frb.png | Bin 0 -> 15047 bytes core/frb/frb.svg | 15 + core/frb/grammar-0.7.0.js | 4776 +++++++++++++++++ core/frb/grammar-maybeValue.pegjs | 542 ++ core/frb/grammar.ebnf | 98 + core/frb/grammar.js | 4776 +++++++++++++++++ core/frb/grammar.pegjs | 540 ++ core/frb/grammar.xhtml | 510 ++ core/frb/language-temp.js | 4 + core/frb/language.js | 72 +- core/frb/lib/l2r-parser.js | 23 + core/frb/lib/parser.js | 70 + core/frb/lib/trie-parser.js | 23 + core/frb/lib/trie.js | 25 + core/frb/merge.js | 187 + core/frb/observe.js | 64 + core/frb/observers.js | 1829 +++++++ core/frb/operators.js | 153 + core/frb/package-lock.json | 3255 +++++++++++ core/frb/package.json | 46 + core/frb/parse-temp.js | 1 + core/frb/parse.js | 37 +- .../temperature-converter/package.json | 10 + .../precision-converter.js | 23 + .../temperature-converter.html | 11 + .../temperature-converter.js | 34 + core/frb/scope.js | 14 + core/frb/signal.js | 26 + core/frb/spec/algebra-spec.js | 56 + core/frb/spec/assign-spec.js | 174 + core/frb/spec/bind-defined-spec.js | 23 + core/frb/spec/bind-null-spec.js | 59 + core/frb/spec/bind-spec.js | 617 +++ core/frb/spec/binders-spec.js | 65 + core/frb/spec/bindings-spec.js | 519 ++ core/frb/spec/complex-spec.js | 71 + core/frb/spec/compute-spec.js | 80 + core/frb/spec/enumerate-spec.js | 79 + core/frb/spec/evaluate-spec.js | 94 + core/frb/spec/evaluate-with-observe-spec.js | 31 + core/frb/spec/evaluate.js | 780 +++ core/frb/spec/expand-spec.js | 107 + core/frb/spec/filter-map-spec.js | 21 + core/frb/spec/filter-spec.js | 44 + core/frb/spec/gate-spec.js | 67 + core/frb/spec/group-spec.js | 149 + core/frb/spec/items-spec.js | 35 + core/frb/spec/language.js | 1193 ++++ core/frb/spec/logic-bindings-spec.js | 219 + core/frb/spec/merge-spec.js | 181 + core/frb/spec/min-max-spec.js | 51 + core/frb/spec/observe-enumeration-spec.js | 56 + core/frb/spec/observe-join-spec.js | 64 + core/frb/spec/observe-sorted-set-spec.js | 39 + core/frb/spec/observe-sorted-spec.js | 30 + core/frb/spec/observe-spec.js | 141 + core/frb/spec/observers-spec.js | 696 +++ core/frb/spec/only-binder-spec.js | 44 + core/frb/spec/override-spec.js | 57 + core/frb/spec/parse-spec.js | 20 + core/frb/spec/path-spec.js | 35 + core/frb/spec/pluck-spec.js | 62 + core/frb/spec/range-content-reflexive-spec.js | 188 + core/frb/spec/range-spec.js | 45 + core/frb/spec/readme-spec.js | 1653 ++++++ core/frb/spec/stringify-spec.js | 21 + core/frb/spec/view-spec.js | 59 + core/frb/stringify.js | 305 ++ core/frb/test.js | 84 + 91 files changed, 31135 insertions(+), 5 deletions(-) create mode 100644 core/frb/.editorconfig create mode 100644 core/frb/.gitignore create mode 100644 core/frb/.travis.yml create mode 100644 core/frb/.vscode/launch.json create mode 100644 core/frb/CHANGES.md create mode 100644 core/frb/CONTRIBUTING.md create mode 100644 core/frb/LICENSE.md create mode 100644 core/frb/README.md create mode 100644 core/frb/algebra.js create mode 100644 core/frb/assign.js create mode 100644 core/frb/bind.js create mode 100644 core/frb/binders.js create mode 100644 core/frb/bindings.js create mode 100644 core/frb/checklist.csv create mode 100644 core/frb/compile-assigner.js create mode 100644 core/frb/compile-binder.js create mode 100644 core/frb/compile-evaluator.js create mode 100644 core/frb/compile-observer.js create mode 100644 core/frb/compute.js create mode 100644 core/frb/dom.js create mode 100644 core/frb/evaluate.js create mode 100644 core/frb/expand.js create mode 100644 core/frb/frb.png create mode 100644 core/frb/frb.svg create mode 100644 core/frb/grammar-0.7.0.js create mode 100644 core/frb/grammar-maybeValue.pegjs create mode 100644 core/frb/grammar.ebnf create mode 100644 core/frb/grammar.js create mode 100644 core/frb/grammar.pegjs create mode 100644 core/frb/grammar.xhtml create mode 100644 core/frb/language-temp.js create mode 100644 core/frb/lib/l2r-parser.js create mode 100644 core/frb/lib/parser.js create mode 100644 core/frb/lib/trie-parser.js create mode 100644 core/frb/lib/trie.js create mode 100644 core/frb/merge.js create mode 100644 core/frb/observe.js create mode 100644 core/frb/observers.js create mode 100644 core/frb/operators.js create mode 100644 core/frb/package-lock.json create mode 100644 core/frb/package.json create mode 100644 core/frb/parse-temp.js create mode 100644 core/frb/samples/temperature-converter/package.json create mode 100644 core/frb/samples/temperature-converter/precision-converter.js create mode 100644 core/frb/samples/temperature-converter/temperature-converter.html create mode 100644 core/frb/samples/temperature-converter/temperature-converter.js create mode 100644 core/frb/scope.js create mode 100644 core/frb/signal.js create mode 100644 core/frb/spec/algebra-spec.js create mode 100644 core/frb/spec/assign-spec.js create mode 100644 core/frb/spec/bind-defined-spec.js create mode 100644 core/frb/spec/bind-null-spec.js create mode 100644 core/frb/spec/bind-spec.js create mode 100644 core/frb/spec/binders-spec.js create mode 100644 core/frb/spec/bindings-spec.js create mode 100644 core/frb/spec/complex-spec.js create mode 100644 core/frb/spec/compute-spec.js create mode 100644 core/frb/spec/enumerate-spec.js create mode 100644 core/frb/spec/evaluate-spec.js create mode 100644 core/frb/spec/evaluate-with-observe-spec.js create mode 100644 core/frb/spec/evaluate.js create mode 100644 core/frb/spec/expand-spec.js create mode 100644 core/frb/spec/filter-map-spec.js create mode 100644 core/frb/spec/filter-spec.js create mode 100644 core/frb/spec/gate-spec.js create mode 100644 core/frb/spec/group-spec.js create mode 100644 core/frb/spec/items-spec.js create mode 100644 core/frb/spec/language.js create mode 100644 core/frb/spec/logic-bindings-spec.js create mode 100644 core/frb/spec/merge-spec.js create mode 100644 core/frb/spec/min-max-spec.js create mode 100644 core/frb/spec/observe-enumeration-spec.js create mode 100644 core/frb/spec/observe-join-spec.js create mode 100644 core/frb/spec/observe-sorted-set-spec.js create mode 100644 core/frb/spec/observe-sorted-spec.js create mode 100644 core/frb/spec/observe-spec.js create mode 100644 core/frb/spec/observers-spec.js create mode 100644 core/frb/spec/only-binder-spec.js create mode 100644 core/frb/spec/override-spec.js create mode 100644 core/frb/spec/parse-spec.js create mode 100644 core/frb/spec/path-spec.js create mode 100644 core/frb/spec/pluck-spec.js create mode 100644 core/frb/spec/range-content-reflexive-spec.js create mode 100644 core/frb/spec/range-spec.js create mode 100644 core/frb/spec/readme-spec.js create mode 100644 core/frb/spec/stringify-spec.js create mode 100644 core/frb/spec/view-spec.js create mode 100644 core/frb/stringify.js create mode 100644 core/frb/test.js diff --git a/core/frb/.editorconfig b/core/frb/.editorconfig new file mode 100644 index 0000000000..c9721be133 --- /dev/null +++ b/core/frb/.editorconfig @@ -0,0 +1,11 @@ +# http://EditorConfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/core/frb/.gitignore b/core/frb/.gitignore new file mode 100644 index 0000000000..b152df746b --- /dev/null +++ b/core/frb/.gitignore @@ -0,0 +1,2 @@ +.tmp +node_modules diff --git a/core/frb/.travis.yml b/core/frb/.travis.yml new file mode 100644 index 0000000000..64c057e46c --- /dev/null +++ b/core/frb/.travis.yml @@ -0,0 +1,19 @@ +language: node_js +node_js: + - "10.16.2" +script: npm run $COMMAND +env: + - CXX=g++-4.8 COMMAND=test +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 +notifications: + irc: + channels: + - "chat.freenode.net#montage" + on_success: false + template: + - "%{author} broke the %{repository} tests on %{branch}: %{build_url}" diff --git a/core/frb/.vscode/launch.json b/core/frb/.vscode/launch.json new file mode 100644 index 0000000000..d74944c245 --- /dev/null +++ b/core/frb/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Run Jasmine Tests", + "program": "${workspaceFolder}/node_modules/jasmine-node/bin/jasmine-node", + "args": [ "spec", "--verbose"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + + } + + ] +} diff --git a/core/frb/CHANGES.md b/core/frb/CHANGES.md new file mode 100644 index 0000000000..9306599b57 --- /dev/null +++ b/core/frb/CHANGES.md @@ -0,0 +1,274 @@ + +# v4.0.3 +- Update to collections ^5.1.3 + +# v4.0.2 +- Update to collections ^5.1.2 + +# v4.0.1 +- Fixes a missing Map require needede for IE10/11 + +# v4.0.0 +- Performance improvements +- backward incompatible update that makes getBindings return a Map instead of an object + + +# v3.0.1 + +- Performance improvements reducing closure lookup and caching constants + +# v3.0.0 + +- Performance improvements from removing forEach, leveraging Collection 5 usr of native Map/Set when available +- Updates to run with Collections 5.x + +# v0.2.19 + +- Small bug fix in cancelling non replacing array observers + +# v0.2.18 + +- Allow white space in more positions in FRB expressions, particularly within + and around records. +- Fixes the cases for null, undefined, and NaN on string operators, + particularly startsWith and endsWith which would previously throw an + exception. +- Various speculative performance related changes, particularly unrolling + swap loops, using undefined instead of noop for default cancelers, + eliminating most uses of `autoCancelPrevious` and all uses of `once`. +- Memoize parse results and require that they be treated as immutable. + This is a break from previous versions because MontageJS previously + modified the syntax tree for reserializing expressions with different + component names. +- Update Collections to v2.1.1, mostly for speculative performance related + changes. +- Fix the `has` operator for map collections. +- Support for contenteditable in the DOM shim (@cesine) + +# v0.2.17 + +- Fix evaluator for ternary conditional operator to match the behavior of the + observer. A null input should result in a null output, not be interpreted as + false and result in the alternate. +- Makes two way range content bindings propagate propertly in both directions. + +# v0.2.16 + +- Add support for `one()` bindings. +- Add support for `min()` and `max()` observers (in addition to the + already-supported `min{}` and `max{}` blocks). +- Fixes `view(start, length)` observers. +- Fixes range change listeners with beforeChange and handlerToken + arguments. + +# v0.2.15 + +- Collections 0.2.1, fixes a bug that impacts FRB on Chrome 30 and + all future browsers that implement ES6 methods on arrays. + +# v0.2.14 + +- Adds support for binding to the `only` operator with sets. +- Implements binder for parent scope operator +- Adds support for polymorphic scope nesting. +- Fixes two bugs in `filter`. +- Restricts the domain of property change observers to numbers and + strings, ignoring all else. +- Collections 0.2.0 + +# v0.2.13 + +- Makes traced binding messages appear less like errors by stripping + the first line. +- Makes `sum` and `average` observers more resilient against + transient, invalid input. + +# v0.2.12 + +- Adds `only` binder and observer. +- Adds `sortedSet` observer. +- Adds `join` observer. +- Makes `join` and `split` algebraic inversions which qualifies these + operators for binders in addition to observers. + +# v0.2.11 + +- Adds the ability to bind to conditionally bind to `null`, to disable + a binding, e.g., `unstopableForce <-> never ? imovableObject : + null`. +- Adds support to bind to `defined()`, e.g., `value.defined() <- + defined`, which will bind `value` to `undefined` when `defined` + is `false`. Takes no action when `defined` becomes `true`. +- Short circuit `has(value)` observers if `value` is null. +- Soort circuit `object.toMap()` if `object` is null or not an object. + +# v0.2.10 + +- Uprev `collections` +- Increase fault-tolerance of map change observers + +# v0.2.9 + +- Tighten the precedence of the `^` (parent scope) operator. This + operator was on the same level as other unary operators, `+`, `-`, + and `!`, but now couples even more tightly. Thus `^foo.bar()` was + equivalent to `^(foo.bar())` but is now equivalent to + `(^foo).bar()`. +- Adds the `last` operator, for observing the last value in a + collection without jitter. +- Finishes the `toMap` operator, which can now coerce and + incrementally update maps, fixed-shape objects (known as records), + and arrays (or other indexed collections) of entries (key value + pairs in duple arrays). +- Throw no errors. It is now clear that FRB should not throw errors + if it encounteres invalid input. It must propagate null instead. + This is because FRB inputs do not necessarily change atomically. + The result is that state must be made consistent by the end of a + turn (not enforced), but may pass through invalid states internally. + As such, throwing an exception would interfere. +- Deprecates `items` in favor of `entries` and makes the terminology + consistent throughout interfaces and documentation. +- Deprecates `asArray` in favor of `toArray`, in keeping with + precedent established in v0.2.7. +- Alters the `toString` operator such that only numbers and strings + can be coerced. All other types propagate `null`. This is intended + to simply the creation of `toString` operators for cross-language + bindings by not entraining JavaScript's string coercion semantics. + +# v0.2.8 + +- In keeping with the new `&&` and `||` bindings, implement binder and + assigner for the default operator, `??`. The binding will apply the + source to the left side of the operator. + +# v0.2.7 + +- Implement logic bindings and assignment. `&&` and `||` can now + meaningfully appear on the left side of a binding, or on either side + of a two-way binding. The binding preserves the expressed predicate + by setting either the left side, right side, or both sides to `true` + or `false`. If setting the left side of the operator is sufficient + to meet the predicate, only that side will be affected. This makes + it possible to contrive bindings that account for check boxes that + should be unchecked when they are disabled. +- Changed `asString` and `asNumber` to `toString` and `toNumber`. The + convention hereafter is to use method names consistent with + precedent in JavaScript, even in the case of `to` methods which do + not look right on the target side of a binding. Since FRB delegates + to JavaScript methods as a fallback, the ship has sailed. +- Makes `flatten` fault-tolerant if any input array is null or + undefined. + +# v0.2.6 + +- Fixes `filter` blocks. The optimization applied in v0.2.4 was + inccorrect. The fix prevents the regression and produces the + originally intended optimization. +- Reintroduces the shortest-possible-transform algorithm and all + charges of bugs have been dropped. + +# v0.2.5 + +- Adds a `concat` operator. +- Removes the shortest-possible-transform algorithm on suspicion of + bugs. + +# v0.2.4 + +- Adds support for `reverters`, which are the same interface as + converters, but the `convert` and `revert` terminals are switched. + This is useful for hooking up a converter against the direction of + the binding arrow. +- Fixes `stringify` when `this` is passed as an argument to a + function. Previously, the argument would be lost. +- Reduces the jitter on the output `filter` blocks. This change + was later found to introduce bugs that were fixed in v0.2.6. +- All observers that produce arrays will now apply the shortest + possible sequence of splices to update the output array. This + feature was removed in v0.2.5, and reintroduced in v0.2.6. +- Improves the debugging experience by providing meaningful names for + all observer and binder functions. + +# v0.2.3 + +- Partially fixes two-way range content bindings. + - Content changes and right to left assignment propagate both + ways. + - Propagation from left to right on assignment is still unsolved. +- Guarantees that rangeContent() bindings will produce a non-replacing + array both from observers and binders. +- Produces better function names in traces. + +# v0.2.2 + +`stringify` can now accept a scope argument, which it will use for the +sole purpose of expanding component labels. + +# v0.2.1 + +Replace the FRB parser with a PEGJS implementation. This extends the +grammar for numbers and string literals (double-quotes are allowed) +but removes support for certain hacks like using `.` for an empty +expression. + +TODO commit 1a3a896464c501f851d1764d219c25bb2e989ab5 + +# v0.2.0 + +This release refactors most of the internals and some of the interface +to introduce a parent scope operator, `^`. As such, bindings now have a +scope chain and the parameters, document object model, component object +model, and options are carried by the scope object. + +The signature of assign has been extended: +assign(target, targetPath, value, parameters) +to +assign(target, targetPath, value, parameters, document, component) + +## Backward-incompatible changes + +### bindings + +The document and component object models are no longer communicated to +bindings through the `document` and `serialization` parameters. + +`Bindings.defineBinding(object, name, descriptor, parameters)` has +changed to `Bindings.defineBinding(object, name, descriptor, +commonDescriptor)` where commonDescriptor is `{parameters, document, +components}` + +In a *future* release, the default parameters will be undefined. The +default parameters are presently the source object which has allowed +us to work-around the lack of a parent scope operator. Please migrate +your code from using `$` (parameters) to `^` (parent scope). You can +verify that your bindings will continue to work in the future by passing +an empty object `{}` as the parameters explicitly. + +### evaluate + +The signature of evaluate functions as returned by compileEvaluator have +changed from `evaluate(value, parameters)` to `evaluate(scope)` such +that `evaluate(new Scope(value, null, parameters))` is equivalent to the +former rendition. + +### assign + +The signature of assign functions as returned from compileAssigner have +changed from `assign(value, target, parameters)` to `assign(value, +scope)` such that `assign(value, new Scope(target, null, parameters)` is +equivalent to the former rendition. + +### expand + +The signature of the `expand` function has changed to `expand(syntax, +scope)`, where `scope` is an object with optional `value`, `parameters` +and `components` properties. The value and parameters must be syntax +nodes to replace value and parameters nodes in place. The `components` +property must be an object with `getObjectLabel(component)` to get the +label for a component, in case the label differs from the one on the +syntax tree. + +### observeGet + +`Observers.observeGet` now delegates to `observeGet` method instead of +`observeKey`. diff --git a/core/frb/CONTRIBUTING.md b/core/frb/CONTRIBUTING.md new file mode 100644 index 0000000000..ae3f0ddd7e --- /dev/null +++ b/core/frb/CONTRIBUTING.md @@ -0,0 +1,12 @@ + +- Make your change. +- Add a spec. Run `npm test` to run the specs. Do not regress. +- Duplicate the first line of LICENSE.md and change the name to your + own. +- Send a pull request on Github. + +For changes to `README.md`. + +- Synchronize your code examples with those in `spec/readme-spec.js` + and run the tests as above. + diff --git a/core/frb/LICENSE.md b/core/frb/LICENSE.md new file mode 100644 index 0000000000..b4d1fb99bf --- /dev/null +++ b/core/frb/LICENSE.md @@ -0,0 +1,31 @@ +3-clause BSD license +==================== + +Copyright 2012-2014 Motorola Mobility LLC, Montage Studio Inc, and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Montage nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/core/frb/README.md b/core/frb/README.md new file mode 100644 index 0000000000..5a5b131fa2 --- /dev/null +++ b/core/frb/README.md @@ -0,0 +1,3111 @@ +FRB Logo + +# Functional Reactive Bindings + +[![npm version](https://img.shields.io/npm/v/frb.svg?style=flat)](https://www.npmjs.com/package/frb) + +[![Build Status](https://travis-ci.org/montagejs/frb.png?branch=master)](http://travis-ci.org/montagejs/frb) + +In their simplest form, bindings provide the illusion that two objects +have the same property. Changing the property on one object causes the +same change in the other. This is useful for coordinating state between +views and models, among other entangled objects. For example, if you +enter text into a text field, the same text might be added to the +corresponding database record. + +```javascript +bind(object, "a.b", {"<->": "c.d"}); +``` + +Functional Reactive Bindings go farther. They can gracefully bind long +property paths and the contents of collections. They can also +incrementally update the results of chains of queries including maps, +flattened arrays, sums, and averages. They can also add and remove +elements from sets based on the changes to a flag. FRB makes it easy to +incrementally ensure consistent state. + +```javascript +bind(company, "payroll", {"<-": "departments.map{employees.sum{salary}}.sum()"}); +bind(document, "body.classList.has('dark')", {"<-": "darkMode", source: viewModel}); +``` + +FRB is built from a combination of powerful functional and generic +building blocks, making it reliable, easy to extend, and easy to +maintain. + + +## Getting Started + +`frb` is a CommonJS package, with JavaScript modules suitable for use +with [Node.js][] on the server side or [Mr][] on the client side. + +``` +❯ npm install frb +``` + + + + +## Tutorial + +In this example, we bind `model.content` to `document.body.innerHTML`. + +```javascript +var bind = require("frb/bind"); +var model = {content: "Hello, World!"}; +var cancelBinding = bind(document, "body.innerHTML", { + "<-": "content", + "source": model +}); +``` + +When a source property is bound to a target property, the target gets +reassigned to the source any time the source changes. + +```javascript +model.content = "Farewell."; +expect(document.body.innerHTML).toBe("Farewell."); +``` + +Bindings can be recursively detached from the objects they observe with +the returned cancel function. + +```javascript +cancelBinding(); +model.content = "Hello again!"; // doesn't take +expect(document.body.innerHTML).toBe("Farewell."); +``` + +### Two-way Bindings + +Bindings can go one way or in both directions. Declare one-way +bindings with the ```<-``` property, and two-way bindings with the +```<->``` property. + +In this example, the "foo" and "bar" properties of an object will be +inexorably intertwined. + +```javascript +var object = {}; +var cancel = bind(object, "foo", {"<->": "bar"}); + +// <- +object.bar = 10; +expect(object.foo).toBe(10); + +// -> +object.foo = 20; +expect(object.bar).toBe(20); +``` + +### Right-to-left + +Note that even with a two-way binding, the right-to-left binding +precedes the left-to-right. In this example, "foo" and "bar" are bound +together, but both have initial values. + +```javascript +var object = {foo: 10, bar: 20}; +var cancel = bind(object, "foo", {"<->": "bar"}); +expect(object.foo).toBe(20); +expect(object.bar).toBe(20); +``` + +The right-to-left assignment of `bar` to `foo` happens first, so the +initial value of `foo` gets lost. + +### Properties + +Bindings can follow deeply nested chains, on both the left and the right +side. + +In this example, we have two object graphs, `foo`, and `bar`, with the +same structure and initial values. This binds `bar.a.b` to `foo.a.b` +and also the other way around. + +```javascript +var foo = {a: {b: 10}}; +var bar = {a: {b: 10}}; +var cancel = bind(foo, "a.b", { + "<->": "a.b", + source: bar +}); +// <- +bar.a.b = 20; +expect(foo.a.b).toBe(20); +// -> +foo.a.b = 30; +expect(bar.a.b).toBe(30); +``` + +### Structure changes + +Changes to the structure of either side of the binding are no matter. +All of the orphaned event listeners will automatically be canceled, and +the binders and observers will reattach to the new object graph. + +Continuing from the previous example, we store and replace the `a` +object from one side of the binding. The old `b` property is now +orphaned, and the old `b` property adopted in its place. + +```javascript +var a = foo.a; +expect(a.b).toBe(30); // from before + +foo.a = {}; // orphan a and replace +foo.a.b = 40; +// -> +expect(bar.a.b).toBe(40); // updated + +bar.a.b = 50; +// <- +expect(foo.a.b).toBe(50); // new one updated +expect(a.b).toBe(30); // from before it was orphaned +``` + +### Strings + +String concatenation is straightforward. + +```javascript +var object = {name: "world"}; +bind(object, "greeting", {"<-": "'hello ' + name + '!'"}); +expect(object.greeting).toBe("hello world!"); +``` + +### Sum + +Some advanced queries are possible with one-way bindings from +collections. FRB updates sums incrementally. When values are added or +removed from the array, the sum of only those values is taken and added +or removed from the last known sum. + +```javascript +var object = {array: [1, 2, 3]}; +bind(object, "sum", {"<-": "array.sum()"}); +expect(object.sum).toEqual(6); +``` + +### Average + +The arithmetic mean of a collection can be updated incrementally. Each +time the array changes, the added and removed values adjust the last +known sum and count of values in the array. + +```javascript +var object = {array: [1, 2, 3]}; +bind(object, "average", {"<-": "array.average()"}); +expect(object.average).toEqual(2); +``` + +### Rounding + +The `round`, `floor`, and `ceil` methods operate on numbers and return +the nearest integer, the nearest integer toward -infinity, and the +nearest integer toward infinity respectively. + +```javascript +var object = {number: -0.5}; +Bindings.defineBindings(object, { + "round": {"<-": "number.round()"}, + "floor": {"<-": "number.floor()"}, + "ceil": {"<-": "number.ceil()"} +}); +expect(object.round).toBe(0); +expect(object.floor).toBe(-1); +expect(object.ceil).toBe(0); +``` + +### Last + +FRB provides an operator for watching the last value in an Array. + +```javascript +var array = [1, 2, 3]; +var object = {array: array, last: null}; +Bindings.defineBinding(object, "last", {"<-": "array.last()"}); +expect(object.last).toBe(3); + +array.push(4); +expect(object.last).toBe(4); +``` + +When the dust settles, `array.last()` is equivalent to +`array[array.length - 1]`, but the `last` observer guarantees that it +will not jitter between the ultimate value and null or the penultimate +value of the collection. With `array[array.length]`, the underlying may +not change its content and length atomically. + +```javascript +var changed = jasmine.createSpy(); +PropertyChanges.addOwnPropertyChangeListener(object, "last", changed); +array.unshift(0); +array.splice(3, 0, 3.5); +expect(object.last).toBe(4); +expect(changed).not.toHaveBeenCalled(); + +array.pop(); +expect(object.last).toBe(3); + +array.clear(); +expect(object.last).toBe(null); +``` + +### Only + +FRB provides an `only` operator, which can either observe or bind the +only element of a collection. The `only` observer watches a collection +for when there is only one value in that collection and emits that +value.. If there are multiple values, it emits null. + +```javascript +var object = {array: [], only: null}; +Bindings.defineBindings(object, { + only: {"<->": "array.only()"} +}); + +object.array = [1]; +expect(object.only).toBe(1); + +object.array.pop(); +expect(object.only).toBe(undefined); + +object.array = [1, 2, 3]; +expect(object.only).toBe(undefined); +``` + +The `only` binder watches a value. When the value is null, it does +nothing. Otherwise, it will update the bound collection such that it +only contains that value. If the collection was empty, it adds the +value. Otherwise, if the collection did not have the value, it replaces +the collection's content with the one value. Otherwise, it removes +everything but the value it already contains. Regardless of the means, +the end result is the same. If the value is non-null, it will be the +only value in the collection. + +```javascript +object.only = 2; +expect(object.array.slice()).toEqual([2]); +// Note that slice() is necessary only because the testing scaffold +// does not consider an observable array equivalent to a plain array +// with the same content + +object.only = null; +object.array.push(3); +expect(object.array.slice()).toEqual([2, 3]); +``` + +### One + +Like the `only` operator, there is also a `one` operator. The `one` +operator will observe one value from a collection, whatever value is +easiest to obtain. For an array, it's the first value; for a sorted +set, it's whatever value was most recently found or added; for a heap, +it's whatever is on top. However, if the collection is null, undefined, +or empty, the result is `undefined`. + +```javascript +var object = {array: [], one: null}; +Bindings.defineBindings(object, { + one: {"<-": "array.one()"} +}); + +expect(object.one).toBe(undefined); + +object.array.push(1); +expect(object.one).toBe(1); + +// Still there... +object.array.push(2); +expect(object.one).toBe(1); +``` + +Unlike `only`, `one` is not bindable. + +### Map + +You can also create mappings from one array to a new array and an +expression to evaluate on each value. The mapped array is bound once, +and all changes to the source array are incrementally updated in the +target array. + +```javascript +var object = {objects: [ + {number: 10}, + {number: 20}, + {number: 30} +]}; +bind(object, "numbers", {"<-": "objects.map{number}"}); +expect(object.numbers).toEqual([10, 20, 30]); +object.objects.push({number: 40}); +expect(object.numbers).toEqual([10, 20, 30, 40]); +``` + +Any function, like `sum` or `average`, can be applied to the result of a +mapping. The straight-forward path would be +`objects.map{number}.sum()`, but you can use a block with any function +as a short hand, `objects.sum{number}`. + +### Filter + +A filter block generates an incrementally updated array filter. The +resulting array will contain only those elements from the source array +that pass the test deescribed in the block. As values of the source +array are added, removed, or changed such that they go from passing to +failing or failing to passing, the filtered array gets incrementally +updated to include or exclude those values in their proper positions, as +if the whole array were regenerated with `array.filter` by brute force. + +```javascript +var object = {numbers: [1, 2, 3, 4, 5, 6]}; +bind(object, "evens", {"<-": "numbers.filter{!(%2)}"}); +expect(object.evens).toEqual([2, 4, 6]); +object.numbers.push(7, 8); +object.numbers.shift(); +object.numbers.shift(); +expect(object.evens).toEqual([4, 6, 8]); +``` + +### Scope + +In a binding, there is always a value in scope. It is the implicit +value for looking up properties and for applying operators, like +methods. The value in scope can be called out explicitly as `this`. On +the left side, the value in scope is called the target, on the right it +is called the source. + +Each scope has a `this` value and may have a parent scope. Inside a +map block, like the `number` in `numbers.map{number}`, the value in +scope is one of the numbers, and the value in the parent scope is an +object with a `numbers` property. To access the value in a parent +scope, use the parent scope operator, `^`. + +Suppose you have an object with `numbers` and `maxNumber` properties. +In this example, we bind a property, `smallNumbers` to an array of all +the `numbers` less than or equal to the `maxNumber`. + +```javascript +var object = Bindings.defineBindings({ + numbers: [1, 2, 3, 4, 5], + maxNumber: 3 +}, { + smallNumbers: { + "<-": "numbers.filter{this <= ^maxNumber}" + } +}); +``` + +Keywords like `this` overlap with the notation normally used for +properties of `this`. If an object has a `this` property, you may use +the notation `.this`, `this.this`, or `this['this']`. `.this` is the +normal form. + +```javascript +var object = Bindings.defineBindings({ + "this": 10 +}, { + that: {"<-": ".this"} +}); +expect(object.that).toBe(object["this"]); +``` + +The only other FRB keywords that collide with propery names are `true`, +`false`, and `null`, and the same technique for disambiguation applies. + +### Some and Every + +A `some` block incrementally tracks whether some of the values in a +collection meet a criterion. + +```javascript +var object = Bindings.defineBindings({ + options: [ + {checked: true}, + {checked: false}, + {checked: false} + ] +}, { + anyChecked: { + "<-": "options.some{checked}" + } +}); +expect(object.anyChecked).toBe(true); +``` + +An `every` block incrementally tracks whether all of the values in a +collection meet a criterion. + +```javascript +var object = Bindings.defineBindings({ + options: [ + {checked: true}, + {checked: false}, + {checked: false} + ] +}, { + allChecked: { + "<-": "options.every{checked}" + } +}); +expect(object.allChecked).toBe(false); +``` + +You can use a two-way binding on `some` and `every` blocks. + +```javascript +var object = Bindings.defineBindings({ + options: [ + {checked: true}, + {checked: false}, + {checked: false} + ] +}, { + allChecked: { + "<->": "options.every{checked}" + }, + noneChecked: { + "<->": "!options.some{checked}" + } +}); + +object.noneChecked = true; +expect(object.options.every(function (option) { + return !option.checked +})); + +object.allChecked = true; +expect(object.noneChecked).toBe(false); +``` + +The caveat of an `equals` binding applies. If the condition for every +element of the collection is set to true, the condition will be bound +incrementally to true on each element. When the condition is set to +false, the binding will simply be canceled. + +```javascript +object.allChecked = false; +expect(object.options.every(function (option) { + return option.checked; // still checked +})); +``` + +### Sorted + +A sorted block generates an incrementally updated sorted array. The +resulting array will contain all of the values from the source except in +sorted order. + +```javascript +var object = {numbers: [5, 2, 7, 3, 8, 1, 6, 4]}; +bind(object, "sorted", {"<-": "numbers.sorted{}"}); +expect(object.sorted).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); +``` + +The block may specify a property or expression by which to compare +values. + +```javascript +var object = {arrays: [[1, 2, 3], [1, 2], [], [1, 2, 3, 4], [1]]}; +bind(object, "sorted", {"<-": "arrays.sorted{-length}"}); +expect(object.sorted.map(function (array) { + return array.slice(); // to clone +})).toEqual([ + [1, 2, 3, 4], + [1, 2, 3], + [1, 2], + [1], + [] +]); +``` + +The sorted binding responds to changes to the sorted property by +removing them at their former place and adding them back at their new +position. + +```javascript +object.arrays[0].push(4, 5); +expect(object.sorted.map(function (array) { + return array.slice(); // to clone +})).toEqual([ + [1, 2, 3, 4, 5], // new + [1, 2, 3, 4], + // old + [1, 2], + [1], + [] +]); +``` + +### Unique and Sorted + +FRB can create a sorted index of unique values using `sortedSet` blocks. + +```javascript +var object = Bindings.defineBindings({ + folks: [ + {id: 4, name: "Bob"}, + {id: 2, name: "Alice"}, + {id: 3, name: "Bob"}, + {id: 1, name: "Alice"}, + {id: 1, name: "Alice"} // redundant + ] +}, { + inOrder: {"<-": "folks.sortedSet{id}"}, + byId: {"<-": "folks.map{[id, this]}.toMap()"}, + byName: {"<-": "inOrder.toArray().group{name}.toMap()"} +}); + +expect(object.inOrder.toArray()).toEqual([ + object.byId.get(1), + object.byId.get(2), + object.byId.get(3), + object.byId.get(4) +]); + +expect(object.byName.get("Alice")).toEqual([ + object.byId.get(1), + object.byId.get(2) +]); +``` + +The outcome is a `SortedSet` data structure, not an `Array`. The sorted +set is useful for fast lookups, inserts, and deletes on sorted, unique +data. If you would prefer a sorted array of unique values, you can +combine other operators to the same effect. + +```javascript +var object = Bindings.defineBindings({ + folks: [ + {id: 4, name: "Bob"}, + {id: 2, name: "Alice"}, + {id: 3, name: "Bob"}, + {id: 1, name: "Alice"}, + {id: 1, name: "Alice"} // redundant + ] +}, { + index: {"<-": "folks.group{id}.sorted{.0}.map{.1.last()}"} +}); + +expect(object.index).toEqual([ + {id: 1, name: "Alice"}, + {id: 2, name: "Alice"}, + {id: 3, name: "Bob"}, + {id: 4, name: "Bob"} +]); +``` + + +### Min and Max + +A binding can observe the minimum or maximum of a collection. FRB uses +a binary heap internally to incrementally track the minimum or maximum +value of the collection. + +```javascript +var object = Bindings.defineBindings({}, { + min: {"<-": "values.min()"}, + max: {"<-": "values.max()"} +}); + +expect(object.min).toBe(undefined); +expect(object.max).toBe(undefined); + +object.values = [2, 3, 2, 1, 2]; +expect(object.min).toBe(1); +expect(object.max).toBe(3); + +object.values.push(4); +expect(object.max).toBe(4); +``` + +Min and max blocks accept an expression on which to compare values from +the collection. + +```javascript +var object = Bindings.defineBindings({}, { + loser: {"<-": "rounds.min{score}.player"}, + winner: {"<-": "rounds.max{score}.player"} +}); + +object.rounds = [ + {score: 0, player: "Luke"}, + {score: 100, player: "Obi Wan"}, + {score: 250, player: "Vader"} +]; +expect(object.loser).toEqual("Luke"); +expect(object.winner).toEqual("Vader"); + +object.rounds[1].score = 300; +expect(object.winner).toEqual("Obi Wan"); +``` + +### Group + +FRB can incrementally track equivalence classes within in a collection. +The group block accepts an expression that determines the equivalence +class for each object in a collection. The result is a nested data +structure: an array of [key, class] pairs, where each class is itself an +array of all members of the collection that have the corresponding key. + +```javascript +var store = Bindings.defineBindings({}, { + "clothingByColor": {"<-": "clothing.group{color}"} +}); +store.clothing = [ + {type: 'shirt', color: 'blue'}, + {type: 'pants', color: 'red'}, + {type: 'blazer', color: 'blue'}, + {type: 'hat', color: 'red'} +]; +expect(store.clothingByColor).toEqual([ + ['blue', [ + {type: 'shirt', color: 'blue'}, + {type: 'blazer', color: 'blue'} + ]], + ['red', [ + {type: 'pants', color: 'red'}, + {type: 'hat', color: 'red'} + ]] +]); +``` + +Tracking the positions of every key and every value in its equivalence +class can be expensive. Internally, `group` blocks are implemented with +a `groupMap` block followed by an `entries()` observer. The `groupMap` +produces a `Map` data structure and does not waste any time, but does +not produce range change events. The `entries()` observer projects the +map of classes into the nested array data structure. + +You can use the `groupMap` block directly. + +```javascript +Bindings.cancelBinding(store, "clothingByColor"); +Bindings.defineBindings(store, { + "clothingByColor": {"<-": "clothing.groupMap{color}"} +}); +var blueClothes = store.clothingByColor.get('blue'); +expect(blueClothes).toEqual([ + {type: 'shirt', color: 'blue'}, + {type: 'blazer', color: 'blue'} +]); + +store.clothing.push({type: 'gloves', color: 'blue'}); +expect(blueClothes).toEqual([ + {type: 'shirt', color: 'blue'}, + {type: 'blazer', color: 'blue'}, + {type: 'gloves', color: 'blue'} +]); +``` + +The `group` and `groupMap` blocks both respect the type of the source +collection. If instead of an array you were to use a `SortedSet`, the +equivalence classes would each be sorted sets. This is useful because +replacing values in a sorted set can be performed with much less waste +than with a large array. + +### View + +Suppose that your source is a large data store, like a `SortedSet` from +the [Collections][] package. You might need to view a sliding window +from that collection as an array. The `view` binding reacts to changes +to the collection and the position and length of the window. + +```javascript +var SortedSet = require("collections/sorted-set"); +var controller = { + index: SortedSet([1, 2, 3, 4, 5, 6, 7, 8]), + start: 2, + length: 4 +}; +var cancel = bind(controller, "view", { + "<-": "index.view(start, length)" +}); + +expect(controller.view).toEqual([3, 4, 5, 6]); + +// change the window length +controller.length = 3; +expect(controller.view).toEqual([3, 4, 5]); + +// change the window position +controller.start = 5; +expect(controller.view).toEqual([6, 7, 8]); + +// add content behind the window +controller.index.add(0); +expect(controller.view).toEqual([5, 6, 7]); +``` + +### Enumerate + +An enumeration observer produces `[index, value]` pairs. You can bind +to the index or the value in subsequent stages. The prefix dot +distinguishes the zeroeth property from the literal zero. + +```javascript +var object = {letters: ['a', 'b', 'c', 'd']}; +bind(object, "lettersAtEvenIndexes", { + "<-": "letters.enumerate().filter{!(.0 % 2)}.map{.1}" +}); +expect(object.lettersAtEvenIndexes).toEqual(['a', 'c']); +object.letters.shift(); +expect(object.lettersAtEvenIndexes).toEqual(['b', 'd']); +``` + +### Range + +A range observes a given length and produces and incrementally updates +an array of consecutive integers starting with zero with that given +length. + +```javascript +var object = Bindings.defineBinding({}, "stack", { + "<-": "&range(length)" +}); +expect(object.stack).toEqual([]); + +object.length = 3; +expect(object.stack).toEqual([0, 1, 2]); + +object.length = 1; +expect(object.stack).toEqual([0]); +``` + +### Flatten + +You can flatten nested arrays. In this example, we have an array of +arrays and bind it to a flat array. + +```javascript +var arrays = [[1, 2, 3], [4, 5, 6]]; +var object = {}; +bind(object, "flat", { + "<-": "flatten()", + source: arrays +}); +expect(object.flat).toEqual([1, 2, 3, 4, 5, 6]); +``` + +Note that changes to the inner and outer arrays are both projected into +the flattened array. + +```javascript +arrays.push([7, 8, 9]); +arrays[0].unshift(0); +expect(object.flat).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); +``` + +Also, as with all other bindings that produce arrays, the flattened +array is never replaced, just incrementally updated. + +```javascript +var flat = object.flat; +arrays.splice(0, arrays.length); +expect(object.flat).toBe(flat); // === same object +``` + +### Concat + +You can observe the concatenation of collection of dynamic arrays. + +```javascript +var object = Bindings.defineBinding({ + head: 10, + tail: [20, 30] +}, "flat", { + "<-": "[head].concat(tail)" +}); +expect(object.flat).toEqual([10, 20, 30]); +``` + +The underlying mechanism is equivalent to `[[head], tail].flatten()`. + +### Reversed + +You can bind the reversal of an array. + +```javascript +var object = {forward: [1, 2, 3]}; +bind(object, "backward", { + "<->": "forward.reversed()" +}); +expect(object.backward.slice()).toEqual([3, 2, 1]); +object.forward.push(4); +expect(object.forward.slice()).toEqual([1, 2, 3, 4]); +expect(object.backward.slice()).toEqual([4, 3, 2, 1]); +``` + +Note that you can do two-way bindings, ```<->``` with reversed arrays. +Changes to either side are updated to the opposite side. + +```javascript +object.backward.pop(); +expect(object.backward.slice()).toEqual([4, 3, 2]); +expect(object.forward.slice()).toEqual([2, 3, 4]); +``` + +### Has + +You can bind a property to always reflect whether a collection contains +a particular value. + +```javascript +var object = { + haystack: [1, 2, 3], + needle: 3 +}; +bind(object, "hasNeedle", {"<-": "haystack.has(needle)"}); +expect(object.hasNeedle).toBe(true); +object.haystack.pop(); // 3 comes off +expect(object.hasNeedle).toBe(false); +``` + +The binding also reacts to changes to the value you seek. + +```javascript +// Continued from above... +object.needle = 2; +expect(object.hasNeedle).toBe(true); +``` + +`has` bindings are not incremental, but with the right data-structure, +updates are cheap. The [Collections][] package contains Lists, Sets, +and OrderedSets that all can send ranged content change notifications and thus +can be bound. + +```javascript +// Continued from above... +var Set = require("collections/set"); +object.haystack = new Set([1, 2, 3]); +expect(object.hasNeedle).toBe(true); +``` + +Likewise, Maps implement `addMapChangeListener`, so you can use a `has` binding +to observe whether an entry exists with the given key. + +```javascript +// Continued from above... +var Map = require("collections/map"); +object.haystack = new Map([[1, "a"], [2, "b"]]); +object.needle = 2; +expect(object.hasNeedle).toBe(true); +object.needle = 3; +expect(object.hasNeedle).toBe(false); +``` + +`has` bindings can also be left-to-right and bi-directional. + +```javascript +bind(object, "hasNeedle", {"<->": "haystack.has(needle)"}); +object.hasNeedle = false; +expect(object.haystack.has(2)).toBe(false); +``` + +The collection on the left-hand-side must implement `has` or `contains`, +`add`, and `delete` or `remove`. FRB shims `Array` to have `has`, +`add`, and `delete`, just like all the collections in [Collections][]. +It happens that the `classList` properties of DOM elements, when they +are supported, implement `add`, `remove`, and `contains`. + +```javascript +var model = {darkMode: false}; +bind(document.body, "classList.has('dark')", { + "<-": "darkMode", + source: model +}); +``` + +The DOM `classList` does not however implement +`addRangeChangeListener` or `removeRangeChangeListener`, so it +cannot be used on the right-hand-side of a binding, and such bindings +cannot be bidirectional. With some DOM [Mutation Observers][], you +might be able to help FRB overcome this limitation in the future. + +### Get + +A binding can observe changes in key-to-value mappings in arrays and map +[Collections][]. + +```javascript +var object = { + array: [1, 2, 3], + second: null +}; +var cancel = bind(object, "second", { + "<->": "array.get(1)" +}); +expect(object.array.slice()).toEqual([1, 2, 3]); +expect(object.second).toBe(2); + +object.array.shift(); +expect(object.array.slice()).toEqual([2, 3]); +expect(object.second).toBe(3); + +object.second = 4; +expect(object.array.slice()).toEqual([2, 4]); + +cancel(); +object.array.shift(); +expect(object.second).toBe(4); // still +``` + +The source collection can be a Map, Dict, MultiMap, SortedMap, +SortedArrayMap, or anything that implements `get` and +`addMapChangeListener` as specified in [Collections][]. The key can +also be a variable. + +```javascript +var Map = require("collections/map"); +var a = {id: 0}, b = {id: 1}; +var object = { + source: new Map([[a, 10], [b, 20]]), + key: null, + selected: null +}; + +var cancel = bind(object, "selected", { + "<-": "source.get(key)" +}); +expect(object.selected).toBe(undefined); + +object.key = a; +expect(object.selected).toBe(10); + +object.key = b; +expect(object.selected).toBe(20); + +object.source.set(b, 30); +expect(object.selected).toBe(30); + +var SortedMap = require("collections/sorted-map"); +object.source = SortedMap(); +expect(object.selected).toBe(undefined); + +object.source.set(b, 40); +expect(object.selected).toBe(40); + +cancel(); +object.key = a; // no effect +expect(object.selected).toBe(40); +``` + +You can also bind the entire content of a map-like collection to the +content of another. Bear in mind that the content of the source +replaces the content of the target initially. + +```javascript +var Map = require("collections/map"); +var object = { + a: new Map({a: 10}), + b: new Map() +}; +var cancel = bind(object, "a.mapContent()", {"<->": "b.mapContent()"}); +expect(object.a.toObject()).toEqual({}); +expect(object.b.toObject()).toEqual({}); + +object.a.set('a', 10); +expect(object.a.toObject()).toEqual({a: 10}); +expect(object.b.toObject()).toEqual({a: 10}); + +object.b.set('b', 20); +expect(object.a.toObject()).toEqual({a: 10, b: 20}); +expect(object.b.toObject()).toEqual({a: 10, b: 20}); +``` + +In this case, the source of the binding is a different object than the +target, so the binding descriptor specifies the alternate source. + +### Keys, Values, Entries + +If the source of a binding is a map, FRB can also translate changes to +the map into changes on an array. The `keys`, `values`, and `entries` +observers produce incrementally updated projections of the +key-value-mappings onto an array. + +```javascript +var Map = require("collections/map"); +var object = Bindings.defineBindings({}, { + keys: {"<-": "map.keysArray()"}, + values: {"<-": "map.valuesArray()"}, + entries: {"<-": "map.entriesArray()"} +}); +object.map = new Map({a: 10, b: 20, c: 30}); +expect(object.keys).toEqual(['a', 'b', 'c']); +expect(object.values).toEqual([10, 20, 30]); +expect(object.entries).toEqual([['a', 10], ['b', 20], ['c', 30]]); + +object.map.set('d', 40); +object.map.delete('a'); +expect(object.keys).toEqual(['b', 'c', 'd']); +expect(object.values).toEqual([20, 30, 40]); +expect(object.entries).toEqual([['b', 20], ['c', 30], ['d', 40]]); +``` + +### Coerce to Map + +Records (Objects with a fixed shape), arrays of entries, and Maps +themselves can be coerced to an incrementally updated `Map` with the +`toMap` operator. + +```javascript +var object = Bindings.defineBindings({}, { + map: {"<-": "entries.toMap()"} +}); + +// map property will persist across changes to entries +var map = object.map; +expect(map).not.toBe(null); + +object.entries = {a: 10}; +expect(map.keysArray()).toEqual(['a']); +expect(map.has('a')).toBe(true); +expect(map.get('a')).toBe(10); +``` + +The `toMap` observer maintains the insertion order of the keys. + +```javascript +// Continued... +object.entries = [['b', 20], ['c', 30]]; +expect(map.keysArray()).toEqual(['b', 'c']); + +object.entries.push(object.entries.shift()); +expect(map.keysArray()).toEqual(['c', 'b']); +``` + +If the entries do not have unique keys, the last entry wins. This is +managed internally by observing, `entries.group{.0}.map{.1.last()}`. + +```javascript +// Continued... +object.entries = [['a', 10], ['a', 20]]; +expect(map.get('a')).toEqual(20); +object.entries.pop(); +expect(map.get('a')).toEqual(10); +``` + +`toMap` binds the content of the output map to the content of the input +map and will clear and repopulate the output map if the input map is +replaced. + +``` +// Continued... +object.entries = new Map({a: 10}); +expect(map.keysArray()).toEqual(['a']); +``` + +### Equals + +You can bind to whether expressions are equal. + +```javascript +var fruit = {apples: 1, oranges: 2}; +bind(fruit, "equal", {"<-": "apples == oranges"}); +expect(fruit.equal).toBe(false); +fruit.orange = 1; +expect(fruit.equal).toBe(true); +``` + +Equality can be bound both directions. In this example, we do a two-way +binding between whether a radio button is checked and a corresponding +value in our model. + +```javascript +var component = { + orangeElement: {checked: false}, + appleElement: {checked: true} +}; +Bindings.defineBindings(component, { + "orangeElement.checked": {"<->": "fruit == 'orange'"}, + "appleElement.checked": {"<->": "fruit == 'apple'"}, +}); + +component.orangeElement.checked = true; +expect(component.fruit).toEqual("orange"); + +component.appleElement.checked = true; +expect(component.fruit).toEqual("apple"); +``` + +Because equality and assignment are interchanged in this language, you +can use either `=` or `==`. + +FRB also supports a comparison operator, `<=>`, which uses +`Object.compare` to determines how two operands should be sorted in +relation to each other. + +### Array and Map Content + +In JavaScript, arrays behave both like objects (in the sense that every +index is a property, but also like a map collection of index-to-value +pairs. The [Collections][] package goes so far as to patch up the +`Array` prototype so arrays can masquerade as maps, with the caveat that +`delete(value)` behaves like a Set instead of a Map. + +This duplicity is reflected in FRB. You can access the values in an +array using the object property notation or the mapped key notation. + +```javascript +var object = { + array: [1, 2, 3] +}; +Bindings.defineBindings(object, { + first: {"<-": "array.0"}, + second: {"<-": "array.get(1)"} +}); +expect(object.first).toBe(1); +expect(object.second).toBe(2); +``` + +To distinguish a numeric property of the source from a number literal, +use a dot. To distingish a mapped index from an array literal, use an +empty expression. + +```javascript +var array = [1, 2, 3]; +var object = {}; +Bindings.defineBindings(object, { + first: { + "<-": ".0", + source: array + }, + second: { + "<-": "get(1)", + source: array + } +}); +expect(object.first).toBe(1); +expect(object.second).toBe(2); +``` + +Unlike property notation, map notation can observe a variable index. + +```javascript +var object = { + array: [1, 2, 3], + index: 0 +}; +Bindings.defineBinding(object, "last", { + "<-": "array.get(array.length - 1)" +}); +expect(object.last).toBe(3); + +object.array.pop(); +expect(object.last).toBe(2); +``` + +You can also bind *all* of the content of an array by range or by +mapping. The notation for binding ranged content is `rangeContent()`. +Every change to an Array or SortedSet dispatches range changes and any +collection that implements `splice` and `swap` can be a target for such +changes. + +```javascript +var SortedSet = require("collections/sorted-set"); +var object = { + set: SortedSet(), + array: [] +}; +Bindings.defineBindings(object, { + "array.rangeContent()": {"<-": "set"} +}); +object.set.addEach([5, 2, 6, 1, 4, 3]); +expect(object.array).toEqual([1, 2, 3, 4, 5, 6]); +``` + +The notation for binding the content of any mapping collection using map +changes is `mapContent()`. On the target of a binding, this will note +when values are added or removed on each key of the source collection +and apply the same change to the target. The target and source can be +arrays or map collections. + +```javascript +var Map = require("collections/map"); +var object = { + map: new Map(), + array: [] +}; +Bindings.defineBinding(object, "map.mapContent()", { + "<-": "array" +}); +object.array.push(1, 2, 3); +expect(object.map.toObject()).toEqual({ + 0: 1, + 1: 2, + 2: 3 +}); +``` + +### Value + +A note about the source value: an empty path implies the source value. +Using empty paths and empty expressions is useful in some situations. + +If a value is ommitted on either side of an operator, it implies the +source value. The expression `sorted{}` indicates a sorted array, where +each value is sorted by its own numeric value. The expression +`filter{!!}` would filter falsy values. The operand is implied. +Similarly, `filter{!(%2)}` produces only even values. + +This is why you can use `.0` to get the zeroth property of an array, to +distingiush the form from `0` which would be a numeric literal, and why +you can use `()[0]` to map the zeroeth key of a map or array, to +distinguish the form from `[0]` which would be an array literal. + +### With Context Value + +Expressions can be evaluated in the context of another value using a +variant of property notation. A parenthesized expression can follow a +path. + +```javascript +var object = { + context: {a: 10, b: 20} +}; +Bindings.defineBinding(object, "sum", { + "<-": "context.(a + b)" +}); +expect(object.sum).toBe(30); + +Bindings.cancelBinding(object, "sum"); +object.context.a = 20; +expect(object.sum).toBe(30); // unchanged +``` + +To observe a constructed array or object literal, the expression does +not need parentheses. + +```javascript +var object = { + context: {a: 10, b: 20} +}; +Bindings.defineBindings(object, { + "duple": {"<-": "context.[a, b]"}, + "pair": {"<-": "context.{key: a, value: b}"} +}); +expect(object.duple).toEqual([10, 20]); +expect(object.pair).toEqual({key: 10, value: 20}); + +Bindings.cancelBindings(object); +``` + +### Operators + +FRB can also recognize many operators. These are in order of precedence +unary `-` negation, `+` numeric coercion, and `!` logical negation and +then binary `**` power, `//` root, `%%` logarithm, `*`, `/`, `%` modulo, +`%%` remainder, `+`, `-`, ```<```, ```>```, ```<=```, ```>=```, `=` or +`==`, `!=`, `&&` and `||`. + +```javascript +var object = {height: 10}; +bind(object, "heightPx", {"<-": "height + 'px'"}); +expect(object.heightPx).toEqual("10px"); +``` + +The unary `+` operator coerces a value to a number. It is handy for +binding a string to a number. + +```javascript +var object = { + number: null, + string: null, +}; +Bindings.defineBinding(object, "+number", { + "<-": "string" +}); +object.string = '10'; +expect(object.number).toBe(10); +``` + +### Functions + +FRB supports some common functions. `startsWith`, `endsWith`, and +`contains` all operate on strings. `join` concatenates an array of +strings with a given delimiter (or empty string). `split` breaks a +string between every delimiter (or just between every character). +`join` and `split` are algebraic and can be bound as well as observed. + +### Conditional + +FRB supports the ternary conditional operator, if `?` then `:` else. + +```javascript +var object = Bindings.defineBindings({ + condition: null, + consequent: 10, + alternate: 20 +}, { + choice: {"<->": "condition ? consequent : alternate"} +}); + +expect(object.choice).toBe(undefined); // no choice made + +object.condition = true; +expect(object.choice).toBe(10); + +object.condition = false; +expect(object.choice).toBe(20); +``` + +The ternary operator can bind in both directions. + +```javascript +object.choice = 30; +expect(object.alternate).toBe(30); + +object.condition = true; +object.choice = 40; +expect(object.consequent).toBe(40); +``` + +### And + +The logical **and** operator, `&&`, observes either the left or right +argument depending on whether the first argument is both defined and +true. If the first argument is null, undefined, or false, it will stand +for the whole expression. Otherwise, the second argument will stand for +the whole expression. + +If we assume that the first and second argument are always defined and +either true or false, the **and** operator serves strictly as a logical +combinator. However, with bindings, it is common for a value to at +least initially be null or undefined. Logical operators are the +exception to the rule that an expression will necessarily terminate if +any operand is null or undefined. + +In this example, the left and right sides are initially undefined. We +set the right operand to `10` and the bound value remains undefined. + +```javascript +var object = Bindings.defineBindings({ + left: undefined, + right: undefined +}, { + and: {"<-": "left && right"} +}); + +object.right = 10; +expect(object.and).toBe(undefined); +``` + +We set the left operand to `20`. The bound value becomes the value of +the right operand, `10`. + +```javascript +// Continued... +object.left = 20; +expect(object.and).toBe(10); +``` + +--- + +Interestingly, logical **and** is bindable. The objective of the +binding is to do whatever is necessary, if possible, to make the logical +expression equal the bound value. + +Supposing that both the left and right operands are false, and the +result is or becomes true, to satisfy the equality `left && right == +true`, both left and right must be set and bound to `true`. + +```javascript +var object = Bindings.defineBindings({}, { + "left && right": { + "<-": "leftAndRight" + } +}); + +object.leftAndRight = true; +expect(object.left).toBe(true); +expect(object.right).toBe(true); +``` + +As with the equals binder, logic bindings will prefer to alter the left +operand if altering either operand would suffice to validate the +expression. So, if the expression then becomes false, it is sufficient +to set the left side to false to satisfy the equality. + +```javascript +// Continued... +object.leftAndRight = false; +expect(object.left).toBe(false); +expect(object.right).toBe(true); +``` + +This can facilitate some interesting, tri-state logic. For example, if +you have a checkbox that can be checked, unchecked, or disabled, and you +want it to be unchecked if it is disabled, you can use logic bindings to +ensure this. + +```javascript +var controller = Bindings.defineBindings({ + checkbox: { + checked: false, + disabled: false + }, + model: { + expanded: false, + children: [1, 2, 3] + } +}, { + "checkbox.checked": {"<->": "model.expanded && expandable"}, + "checkbox.disabled": {"<-": "!expandable"}, + "expandable": {"<-": "model.children.length > 0"} +}); + +expect(controller.checkbox.checked).toBe(false); +expect(controller.checkbox.disabled).toBe(false); + +// check the checkbox +controller.checkbox.checked = true; +expect(controller.model.expanded).toBe(true); + +// alter the model such that the checkbox is unchecked and disabled +controller.model.children.clear(); +expect(controller.checkbox.checked).toBe(false); +expect(controller.checkbox.disabled).toBe(true); +``` + +### Or + +As with the **and** operator, the logical **or** is an exception to the +rule that an expression is null, undefined, or empty if any of the +operands are null or undefined. If both operands are defined and +boolean, **or** expressions behave strictly within the realm of logic. +However, if the values are non-boolean or even non-values, they serve to +select either the left or right side based on whether the left side is +defined and true. + +If the first argument is undefined or false, the aggregate expression +will evaluate to the second argument, even if that argument is null or +undefined. + +Suppose we bind `or` to `left || right` on some object. `or` will be +`undefined` initially, but if we set the `right` to `10`, `or` will +become `10`, bypassing the still undefined left side. + +```javascript +var object = Bindings.defineBindings({ + left: undefined, + right: undefined +}, { + or: {"<-": "left || right"} +}); + +object.right = 10; +expect(object.or).toBe(10); +``` + +However, the left hand side takes precedence over the right if it is +defined and true. + +```javascript +// Continued... +object.left = 20; +expect(object.or).toBe(20); +``` + +And it will remain bound, even if the right hand side becomes undefined. + +```javascript +object.right = undefined; +expect(object.or).toBe(20); +``` + +> Aside: JavaScript’s `delete` operator performs a configuration change, +> and desugars to `Object.defineProperty`, and is not interceptable with +> an ES5 setter. So, don't use it on any property that is involved in a +> binding. Setting to null or undefined should suffice. + +--- + +Logical **or** is bindable. As with logical **and**, the binding +performs the minimum operation necessary to ensure that the expression +is equal. If the expression becomes true, and either of the operands +are true, the nothing needs to change. If the expression becomes false, +however, both operands must be bound to false. If the expression +becomes true again, it is sufficient to bind the left operand to true to +ensure that the expression as a whole is true. Rather than belabor the +point, I leave as an exercise to the reader to apply DeMorgan’s Theorem +to the documentation for logical **and** bindings. + + +### Default + +The **default** operator, `??`, is similar to the **or**, `||` operator, +except that it decides whether to use the left or right solely based on +whether the left is defined. If the left is null or undefined, the +aggregate expression will evaluate to the right expression. If the left +is defined, even if it is false, the result will be the left expression. + +```javascript +var object = Bindings.defineBindings({ + left: undefined, + right: undefined +}, { + or: {"<-": "left ?? right"} +}); + +object.right = 10; +expect(object.or).toBe(10); + +object.left = false; +expect(object.or).toBe(false); +``` + +The default operator is not bindable, but weirder things have happened. + +### Defined + +The `defined()` operator serves a similar role to the default operator. +If the value in scope is null or undefined, it the result will be false, +and otherwise it will be true. This will allow a term that may be +undefined to propagate. + +```javascript +var object = Bindings.defineBindings({}, { + ready: { + "<-": "value.defined()" + } +}); +expect(object.ready).toBe(false); + +object.value = 10; +expect(object.ready).toBe(true); +``` + +The defined operator is also bindable. If the source is or becomes +false, the target will be bound to `null`. If the source is null or +false, the binding has no effect. + +```javascript +var object = Bindings.defineBindings({ + value: 10, + operational: true +}, { + "value.defined()": {"<-": "operational"} +}); +expect(object.value).toBe(10); + +object.operational = false; +expect(object.value).toBe(undefined); +``` + +If the source becomes null or undefined, it will cancel the previous +binding but does not set or restore the bound value. Vaguely becoming +“defined” is not enough information to settle on a particular value. + +```javascript +object.operational = true; +expect(object.value).toBe(undefined); +``` + +However, another binding might settle the issue. + +```javascript +Bindings.defineBindings(object, { + "value == 10": { + "<-": "operational" + } +}); +expect(object.value).toBe(10); +``` + +### Algebra + +FRB can automatically invert algebraic operators as long as they operate +strictly on the left-most expressions on both the source and target are +bindable properties. + +In this example, the primary binding is ```notToBe <- !toBe```, and the +inverse binding is automatically computed ```toBe <- !notToBe```. + +```javascript +var caesar = {toBe: false}; +bind(caesar, "notToBe", {"<->": "!toBe"}); +expect(caesar.toBe).toEqual(false); +expect(caesar.notToBe).toEqual(true); + +caesar.notToBe = false; +expect(caesar.toBe).toEqual(true); +``` + +FRB does algebra by rotating the expressions on one side of a binding to +the other until only one independent property remains (the left most +expression) on the target side of the equation. + +``` +convert: y <- !x +revert: x <- !y +``` + +``` +convert: y <- x + a +revert: x <- y - a +``` + +The left-most independent variable on the right hand side becomes the +dependent variable on the inverted binding. At present, this only works +for numbers and when the left-most expression is a bindable property +because it cannot assign a new value to the literal 10. For example, +FRB cannot yet implicitly revert ```y <-> 10 + x```. + +### Literals + +You may have noticed literals in the previous examples. String literals +take the form of any characters between single quotes. Any character +can be escaped with a back slash. + +```javascript +var object = {}; +bind(object, "greeting", {"<-": "'Hello, World!'"}); +expect(object.greeting).toBe("Hello, World!"); +``` + +Number literals are digits with an optional mantissa. + +```javascript +bind(object, 'four', {"<-": "2 + 2"}); +``` + +### Tuples + +Bindings can produce fixed-length arrays. These are most useful in +conjunction with mappings. Tuples are comma-delimited and +parantheses-enclosed. + +```javascript +var object = {array: [[1, 2, 3], [4, 5]]}; +bind(object, "summary", {"<-": "array.map{[length, sum()]}"}); +expect(object.summary).toEqual([ + [3, 6], + [2, 9] +]); +``` + +### Records + +Bindings can also produce fixed-shape objects. The notation is +comma-delimited, colon-separated entries, enclosed by curly-braces. + +```javascript +var object = {array: [[1, 2, 3], [4, 5]]}; +bind(object, "summary", { + "<-": "array.map{{length: length, sum: sum()}}" +}); +expect(object.summary).toEqual([ + {length: 3, sum: 6}, + {length: 2, sum: 9} +]); +``` + +The left hand side of an entry in a record is any combination of letters +or numbers. The right side is any expression. + +### Parameters + +Bindings can also involve parameters. The source of parameters is by +default the same as the source. The source, in turn, defaults to the +same as the target object. It can be specified on the binding +descriptor. Parameters are declared by any expression following a +dollar sign. + +```javascript +var object = {a: 10, b: 20, c: 30}; +bind(object, "foo", { + "<-": "[$a, $b, $c]"}, + parameters: object +}); +``` + +Bindings also react to changes to the parameters. + +```javascript +object.a = 0; +object.b = 1; +object.c = 2; +expect(object.foo).toEqual([0, 1, 2]); +``` + +The degenerate case of the property language is an empty string. This +is a valid property path that observes the value itself. So, as an +emergent pattern, a `$` expression by itself corresponds to the whole +parameters object. + +```javascript +var object = {}; +bind(object, "ten", {"<-": "$", parameters: 10}); +expect(object.ten).toEqual(10); +``` + +### Elements and Components + +FRB provides a `#` notation for reaching into the DOM for an element. +This is handy for binding views and models on a controller object. + +The `defineBindings` method accepts an optional final argument, +`parameters`, which is shared by all bindings (unless shadowed by a more +specific parameters object on an individual descriptor). + +The `parameters` can include a `document`. The `document` may be any +object that implements `getElementById`. + +Additionally, the `frb/dom` is an experiment that monkey-patches the DOM +to make some properties of DOM elements observable, like the `value` or +`checked` attribute of an `input` or `textarea element`. + +```javascript +var Bindings = require("frb"); +require("frb/dom"); + +var controller = Bindings.defineBindings({}, { + + "fahrenheit": {"<->": "celsius * 1.8 + 32"}, + "celsius": {"<->": "kelvin - 272.15"}, + + "#fahrenheit.value": {"<->": "+fahrenheit"}, + "#celsius.value": {"<->": "+celsius"}, + "#kelvin.value": {"<->": "+kelvin"} + +}, { + document: document +}); + +controller.celsius = 0; +``` + +One caveat of this approach is that it can cause a lot of DOM repaint +and reflow events. The [Montage][] framework uses a synchronized draw +cycle and a component object model to minimize the cost of computing CSS +properties on the DOM and performing repaints and reflows, deferring +such operations to individual animation frames. + +For a future release of Montage, FRB provides an alternate notation for +reaching into the component object model, using its deserializer. The +`@` prefix refers to another component by its label. Instead of +providing a `document`, Montage provides a `serialization`, which in +turn implements `getObjectForLabel`. + +```javascript +var Bindings = require("frb"); + +var controller = Bindings.defineBindings({}, { + + "fahrenheit": {"<->": "celsius * 1.8 + 32"}, + "celsius": {"<->": "kelvin - 272.15"}, + + "@fahrenheit.value": {"<->": "+fahrenheit"}, + "@celsius.value": {"<->": "+celsius"}, + "@kelvin.value": {"<->": "+kelvin"} + +}, { + serializer: serializer +}); + +controller.celsius = 0; +``` + +### Observers + +FRB’s bindings use observers and binders internally. You can create an +observer from a property path with the `observe` function exported by +the `frb/observe` module. + +```javascript +var results = []; +var object = {foo: {bar: 10}}; +var cancel = observe(object, "foo.bar", function (value) { + results.push(value); +}); + +object.foo.bar = 10; +expect(results).toEqual([10]); + +object.foo.bar = 20; +expect(results).toEqual([10, 20]); +``` + +For more complex cases, you can specify a descriptor instead of the +callback. For example, to observe a property’s value *before it +changes*, you can use the `beforeChange` flag. + +```javascript +var results = []; +var object = {foo: {bar: 10}}; +var cancel = observe(object, "foo.bar", { + change: function (value) { + results.push(value); + }, + beforeChange: true +}); + +expect(results).toEqual([10]); + +object.foo.bar = 20; +expect(results).toEqual([10, 10]); + +object.foo.bar = 30; +expect(results).toEqual([10, 10, 20]); +``` + +If the product of an observer is an array, that array is always updated +incrementally. It will only get emitted once. If you want it to get +emitted every time its content changes, you can use the `contentChange` +flag. + +```javascript +var lastResult; +var array = [[1, 2, 3], [4, 5, 6]]; +observe(array, "map{sum()}", { + change: function (sums) { + lastResult = sums.slice(); + // 1. [6, 15] + // 2. [6, 15, 0] + // 3. [10, 15, 0] + }, + contentChange: true +}); + +expect(lastResult).toEqual([6, 15]); + +array.push([0]); +expect(lastResult).toEqual([6, 15, 0]); + +array[0].push(4); +expect(lastResult).toEqual([10, 15, 0]); +``` + +### Nested Observers + +To get the same effect as the previous example, you would have to nest +your own content change observer. + +```javascript +var i = 0; +var array = [[1, 2, 3], [4, 5, 6]]; +var cancel = observe(array, "map{sum()}", function (array) { + function contentChange() { + if (i === 0) { + expect(array.slice()).toEqual([6, 15]); + } else if (i === 1) { + expect(array.slice()).toEqual([6, 15, 0]); + } else if (i === 2) { + expect(array.slice()).toEqual([10, 15, 0]); + } + i++; + } + contentChange(); + array.addRangeChangeListener(contentChange); + return function cancelRangeChange() { + array.removeRangeChangeListener(contentChange); + }; +}); +array.push([0]); +array[0].push(4); +cancel(); +``` + +This illustrates one crucial aspect of the architecture. Observers +return cancelation functions. You can also return a cancelation +function inside a callback observer. That canceler will get called each +time a new value is observed, or when the parent observer is canceled. +This makes it possible to nest observers. + +```javascript +var object = {foo: {bar: 10}}; +var cancel = observe(object, "foo", function (foo) { + return observe(foo, "bar", function (bar) { + expect(bar).toBe(10); + }); +}); +``` + +### Bindings + +FRB provides utilities for declaraing and managing multiple bindings on +objects. The `frb` (`frb/bindings`) module exports this interface. + +```javascript +var Bindings = require("frb"); +``` + +The `Bindings` module provides `defineBindings` and `cancelBindings`, +`defineBinding` and `cancelBinding`, as well as binding inspector +methods `getBindings` and `getBinding`. All of these take a target +object as the first argument. + +The `Bindings.defineBinding(target, descriptors)` method returns the +target object for convenience. + +```javascript +var target = Bindings.defineBindings({}, { + "fahrenheit": {"<->": "celsius * 1.8 + 32"}, + "celsius": {"<->": "kelvin - 272.15"} +}); +target.celsius = 0; +expect(target.fahrenheit).toEqual(32); +expect(target.kelvin).toEqual(272.15); +``` + +`Bindings.getBindings` in that case would return an object with +`fahrenheit` and `celsius` keys. The values would be identical to the +given binding descriptor objects, like `{"<->": "kelvin - 272.15"}`, but +it also gets annotated with a `cancel` function and the default values +for any ommitted properties like `source` (same as `target`), +`parameters` (same as `source`), and others. + +`Bindings.cancelBindings` cancels all bindings attached to an object and +removes them from the bindings descriptors object. + +```javascript +Bindings.cancelBindings(target); +expect(Bindings.getBindings(object)).toEqual({}); +``` + +### Binding Descriptors + +Binding descriptors describe the source of a binding and additional +parameters. `Bindings.defineBindings` can set up bindings (```<-``` or +```<->```), computed (```compute```) properties, and falls back to +defining ES5 properties with permissive defaults (`enumerable`, +`writable`, and `configurable` all on by default). + +If a descriptor has a ```<-``` or ```<->```, it is a binding descriptor. +FRB creates a binding, adds the canceler to the descriptor, and adds the +descriptor to an internal table that tracks all of the bindings defined +on that object. + +```javascript +var object = Bindings.defineBindings({ + darkMode: false, + document: document +}, { + "document.body.classList.has('dark')": { + "<-": "darkMode" + } +}); +``` + +You can get all the binding descriptors with `Bindings.getBindings`, or a +single binding descriptor with `Bindings.getBinding`. `Bindings.cancel` cancels +all the bindings to an object and `Bindings.cancelBinding` will cancel just +one. + +```javascript +// Continued from above... +var bindings = Bindings.getBindings(object); +var descriptor = Bindings.getBinding(object, "document.body.classList.has('dark')"); +Bindings.cancelBinding(object, "document.body.classList.has('dark')"); +Bindings.cancelBindings(object); +expect(Object.keys(bindings)).toEqual([]); +``` + +### Converters + +A binding descriptor can have a `convert` function, a `revert` function, +or alternately a `converter` object. Converters are useful for +transformations that cannot be expressed in the property language, or +are not reversible in the property language. + +In this example, `a` and `b` are synchronized such that `a` is always +half of `b`, regardless of which property gets updated. + +```javascript +var object = Bindings.defineBindings({ + a: 10 +}, { + b: { + "<->": "a", + convert: function (a) { + return a * 2; + }, + revert: function (b) { + return b / 2; + } + } +}); +expect(object.b).toEqual(20); + +object.b = 10; +expect(object.a).toEqual(5); +``` + +Converter objects are useful for reusable or modular converter types and +converters that track additional state. + +```javascript +function Multiplier(factor) { + this.factor = factor; +} +Multiplier.prototype.convert = function (value) { + return value * this.factor; +}; +Multiplier.prototype.revert = function (value) { + return value / this.factor; +}; + +var doubler = new Multiplier(2); + +var object = Bindings.defineBindings({ + a: 10 +}, { + b: { + "<->": "a", + converter: doubler + } +}); +expect(object.b).toEqual(20); + +object.b = 10; +expect(object.a).toEqual(5); +``` + +Reusable converters have an implied direction, from some source type to +a particular target type. Sometimes the types on your binding are the +other way around. For that case, you can use the converter as a +reverter. This merely swaps the `convert` and `revert` methods. + +```javascript +var uriConverter = { + convert: encodeURI, + revert: decodeURI +}; +var model = Bindings.defineBindings({}, { + "title": { + "<->": "location", + reverter: uriConverter + } +}); + +model.title = "Hello, World!"; +expect(model.location).toEqual("Hello,%20World!"); + +model.location = "Hello,%20Dave."; +expect(model.title).toEqual("Hello, Dave."); +``` + +### Computed Properties + +A computed property is one that gets updated with a function call when +one of its arguments changes. Like a converter, it is useful in cases +where a transformation or computation cannot be expressed in the +property language, but can additionally accept multiple arguments as +input. A computed property can be used as the source for another +binding. + +In this example, we create an object as the root of multiple bindings. +The object synchronizes the properties of a "form" object with the +window’s search string, effectively navigating to a new page whenever +the "q" or "charset" values of the form change. + +```javascript +Bindings.defineBindings({ + window: window, + form: { + q: "", + charset: "utf-8" + } +}, { + queryString: { + args: ["form.q", "form.charset"], + compute: function (q, charset) { + return "?" + QS.stringify({ + q: q, + charset: charset + }); + } + }, + "window.location.search": { + "<-": "queryString" + } +}); +``` + +### Debugging with Traces + +A binding can be configured to log when it changes and why. The `trace` +property on a descriptor instructs the binder to log changes to the +console. + +```javascript +Bindings.defineBindings({ + a: 10 +}, { + b: { + "<-": "a + 1", + } +}); +``` + +### Polymorphic Extensibility + +Bindings support three levels of polymorphic extensibility depending on +the needs of a method that FRB does not anticipate. + +If an operator is pure, meaning that all of its operands are value types +that will necessarily need to be replaced outright if they every change, +meaning that they are all effectively stateless, and if all of the +operands must be defined in order for the output to be defined, it is +sufficient to just use a plain JavaScript method. For example, +`string.toUpperCase()` will work fine. + +If an operator responds to state changes of its one and only operand, an +object may implement an observer method. If the operator is `foo` in +FRB, the JavaScript method is `observeFoo(emit)`. The observer must +return a cancel function if it will emit new values after it returns, or +if it uses observers itself. It must stop emitting new values if FRB +calls its canceler. The emitter may return a canceler itself, and the +observer must call that canceler before it emits a new value. + +This is an example of a clock. The `clock.time()` is an observable +operator of the clock in FRB, implemented by `observeTime`. It will +emit a new value once a second. + +```javascript +function Clock() { +} + +Clock.prototype.observeTime = function (emit) { + var cancel, timeoutHandle; + function tick() { + if (cancel) { + cancel(); + } + cancel = emit(Date.now()); + timeoutHandle = setTimeout(tick, 1000); + } + tick(); + return function cancelTimeObserver() { + clearTimeout(timeoutHandle); + if (cancel) { + cancel(); + } + }; +}; + +var object = Bindings.defineBindings({ + clock: new Clock() +}, { + "time": {"<-": "clock.time()"} +}); + +expect(object.time).not.toBe(undefined); + +Bindings.cancelBindings(object); +``` + +If an operator responds to state changes of its operands, you will need +to implement an observer maker. An observer maker is a function that +returns an observer function, and accepts observer functions for all of +the arguments you are expected to observe. The observer must also +handle a scope argument, usually just passing it on at run-time, +`observe(emit, scope)`. Otherwise it is much the same. + +FRB would delegate to `makeTimeObserver(observeResolution)` for a +`clock.time(ms)` FRB expression. + +This is an updated rendition of the clock example except that it will +observe changes to a resolution operand and adjust its tick frequency +accordingly. + +```javascript +function Clock() { +} + +Clock.prototype.observeTime = function (emit, resolution) { + var cancel, timeoutHandle; + function tick() { + if (cancel) { + cancel(); + } + cancel = emit(Date.now()); + timeoutHandle = setTimeout(tick, resolution); + } + tick(); + return function cancelTimeObserver() { + clearTimeout(timeoutHandle); + if (cancel) { + cancel(); + } + }; +}; + +Clock.prototype.makeTimeObserver = function (observeResolution) { + var self = this; + return function observeTime(emit, scope) { + return observeResolution(function replaceResolution(resolution) { + return self.observeTime(emit, resolution); + }, scope); + }; +}; + +var object = Bindings.defineBindings({ + clock: new Clock() +}, { + "time": {"<-": "clock.time(1000)"} +}); + +expect(object.time).not.toBe(undefined); + +Bindings.cancelBindings(object); +``` + +Polymorphic binders are not strictly impossible, but you would be mad to +try them. + + +## Reference + +Functional Reactive Bindings is an implementation of synchronous, +incremental object-property and collection-content bindings for +JavaScript. It was ripped from the heart of the [Montage][] web +application framework and beaten into this new, slightly magical form. +It must prove itself worthy before it can return. + +- **functional**: The implementation uses functional building blocks + to compose observers and binders. +- **generic**: The implementation uses generic methods on collections, + like `addRangeChangeListener`, so any object can implement the + same interface and be used in a binding. +- **reactive**: The values of properties and contents of collections + react to changes in the objects and collections on which they + depend. +- **synchronous**: All bindings are made consistent in the statement + that causes the change. The alternative is asynchronous, where + changes are queued up and consistency is restored in a later event. +- **incremental**: If you update an array, it produces a content + change which contains the values you added, removed, and the + location of the change. Most bindings can be updated using only + these values. For example, a sum is updated by decreasing by the + sum of the values removed, and increasing by the sum of the values + added. FRB can incrementally update `map`, `reversed`, `flatten`, + `sum`, and `average` observers. It can also incrementally update + `has` bindings. +- **unwrapped**: Rather than wrap objects and arrays with observable + containers, FRB modifies existing arrays and objects to make them + dispatch property and content changes. For objects, this involves + installing getters and setters using the ES5 `Object.defineProperty` + method. For arrays, this involves replacing all of the mutation + methods, like `push` and `pop`, with variants that dispatch change + notifications. The methods are either replaced by swapping the + `__proto__` or adding the methods to the instance with + `Object.defineProperties`. These techniques should [work][Define + Property] starting in Internet Explorer 9, Firefox 4, Safari 5, + Chrome 7, and Opera 12. + + +### Architecture + +- [Collections][] provides **property, mapped content, and ranged + content change events** for objects, arrays, and other collections. + For objects, this adds a property descriptor to the observed object. + For arrays, this either swaps the prototype or mixes methods into + the array so that all methods dispatch change events. + Caveats: you have to use a `set` method on Arrays to dispatch + property and content change events. Does not work in older Internet + Explorers since they support neither prototype assignment or ES5 + property setters. +- **observer** functions for watching an entire object graph for + incremental changes, and gracefully rearranging and canceling those + observers as the graph changes. Observers can be constructed + directly or with a very small query language that compiles to a tree + of functions so no parsing occurs while the graph is being watched. +- one- and two-way **bindings** using binder and obserer functions to + incrementally update objects. +- **declarative** interface for creating an object graph with + bindings, properties, and computed properties with dependencies. + + +### Bindings + +The highest level interface for FRB resembles the ES5 Object constructor +and can be used to declare objects and define and cancel bindings on +them with extended property descriptors. + +```javascript +var Bindings = require("frb"); + +// create an object +var object = Bindings.defineBindings({ + foo: 0, + graph: [ + {numbers: [1,2,3]}, + {numbers: [4,5,6]} + ] +}, { + bar: {"<->": "foo", enumerable: false}, + numbers: {"<-": "graph.map{numbers}.flatten()"}, + sum: {"<-": "numbers.sum()"}, + reversed: {"<-": "numbers.reversed()"} +}); + +expect(object.bar).toEqual(object.foo); +object.bar = 10; +expect(object.bar).toEqual(object.foo); +expect.foo = 20; +expect(object.bar).toEqual(object.foo); + +// note that the identity of the bound numbers array never +// changes, because all of the changes to that array are +// incrementally updated +var numbers = object.numbers; + +// first computation +expect(object.sum).toEqual(21); + +// adds an element to graph, +// which pushes [7, 8, 9] to "graph.map{numbers}", +// which splices [7, 8, 9] to the end of +// "graph.map{numbers}.flatten()", +// which increments "sum()" by [7, 8, 9].sum() +object.graph.push({numbers: [7, 8, 9]}); +expect(object.sum).toEqual(45); + +// splices [1] to the beginning of [1, 2, 3], +// which splices [1] to the beginning of "...flatten()" +// which increments "sum()" by [1].sum() +object.graph[0].numbers.unshift(1); +expect(object.sum).toEqual(46); + +// cancels the entire observer hierarchy, then attaches +// listeners to the new one. updates the sum. +object.graph = [{numbers: [1,2,3]}]; +expect(object.sum).toEqual(6); + +expect(object.reversed).toEqual([3, 2, 1]); + +expect(object.numbers).toBe(numbers) // still the same object + +Bindings.cancelBindings(object); // cancels all bindings on this object and +// their transitive observers and event listeners as deep as +// they go +``` + +- `Bindings.defineBindings(object, name, descriptor)` +- `Bindings.defineBinding(object, name, descriptor)` +- `Bindings.getBindings(object)` +- `Bindings.getBinding(object, name)` +- `Bindings.cancelBindings(object)` +- `Bindings.cancelBinding(object, name)` + +A binding descriptor contains: + +- `target`: the +- `targetPath`: the target +- `targetSyntax`: the syntax tree for the target path +- `source`: the source object, which defaults to `target` +- `sourcePath`: the source path, from either ```<-``` or ```<->``` +- `sourceSyntax`: the syntax tree for the source path +- `twoWay`: whether the binding goes in both directions, if ```<->``` + was the source path. +- `parameters`: the parameters, which default to `source`. +- `convert`: a function that converts the source value to the target + value, useful for coercing strings to dates, for example. +- `revert`: a function that converts the target value to the source + value, useful for two-way bindings. +- `converter`: an object with `convert` and optionally also a `revert` + method. The implementation binds these methods to their converter + and stores them in `covert` and `revert`. +- `serializable`: a note from the Montage Deserializer, to the [Montage + Serializer][], indicating that the binding came from a + serialization, and to a serialization it must return. +- `cancel`: a function to cancel the binding + +[Montage Serializer]: https://github.com/montagejs/mousse + +### Bind + +The `bind` module provides direct access to the `bind` function. + +```javascript +var bind = require("frb/bind"); + +var source = [{numbers: [1,2,3]}, {numbers: [4,5,6]}]; +var target = {}; +var cancel = bind(target, "summary", { + "<-": "map{[numbers.sum(), numbers.average()]}", + source: source +}); + +expect(target.summary).toEqual([ + [6, 2], + [15, 5] +]); + +cancel(); +``` + +`bind` is built on top of `parse`, `compileBinder`, and +`compileObserver`. + +### Compute + +The `compute` module provides direct access to the `compute` function, +used by `Bindings` to make computed properties. + +```javascript +var compute = require("frb/compute"); + +var source = {operands: [10, 20]}; +var target = {}; +var cancel = compute(target, "sum", { + source: source, + args: ["operands.0", "operands.1"], + compute: function (a, b) { + return a + b; + } +}); + +expect(target.sum).toEqual(30); + +// change one operand +source.operands.set(1, 30); // needed to dispatch change notification +expect(target.sum).toEqual(40); +``` + +### Observe + +The `observe` modules provides direct access to the `observe` function. +`observe` is built on top of `parse` and `compileObserver`. +`compileObserver` creates a tree of observers using the methods in the +`observers` module. + +```javascript +var observe = require("frb/observe"); + +var source = [1, 2, 3]; +var sum; +var cancel = observe(source, "sum()", function (newSum) { + sum = newSum; +}); + +expect(sum).toBe(6); + +source.push(4); +expect(sum).toBe(10); + +source.unshift(0); // no change +expect(sum).toBe(10); + +cancel(); +source.splice(0, source.length); // would change +expect(sum).toBe(10); +``` + +`observe` produces a cancelation hierarchy. Each time a value is +removed from an array, the underlying observers are canceled. Each time +a property is replaced, the underlying observer is canceled. When new +values are added or replaced, the observer produces a new canceler. The +cancel function returned by `observe` commands the entire underlying +tree. + +Observers also optional accept a descriptor argument in place of a +callback. + +- `set`: the change handler, receives `value` for most observers, but + also `key` and `object` for property changes. +- `parameters`: the value for `$` expressions. +- `beforeChange`: instructs an observer to emit the previous value + before a change occurs. +- `contentChange`: instructs an observer to emit an array every time + its content changes. By default, arrays are only emitted once. + +```javascript +var object = {}; +var cancel = observe(object, "array", { + change: function (value) { + // may return a cancel function for a nested observer + }, + parameters: {}, + beforeChange: false, + contentChange: true +}); + +object.array = []; // emits [] +object.array.push(10); // emits [10] +``` + +### Evaluate + +The `compile-evaluator` module returns a function that accepts a syntax +tree and returns an evaluator function. The evaluator accepts a scope +(which may include a value, parent scope, parameters, a document, and +components) and returns the corresponding value without all the cost or +benefit of setting up incremental observers. + +```javascript +var parse = require("frb/parse"); +var compile = require("frb/compile-evaluator"); +var Scope = require("frb/scope"); + +var syntax = parse("a.b"); +var evaluate = compile(syntax); +var c = evaluate(new Scope({a: {b: 10}})) +expect(c).toBe(10); +``` + +The `evaluate` module returns a function that accepts a path or syntax +tree, a source value, and parameters and returns the corresponding +value. + +```javascript +var evaluate = require("frb/evaluate"); +var c = evaluate("a.b", {a: {b: 10}}) +expect(c).toBe(10); +``` + + +### Stringify + +The `stringify` module returns a function that accepts a syntax tree and +returns the corresponding path in normal form. + +```javascript +var stringify = require("frb/stringify"); + +var syntax = {type: "and", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} +]}; + +var path = stringify(syntax); +expect(path).toBe("a && b"); +``` + + +### Grammar + +The grammar is expressed succinctly in `grammar.pegjs` and is subject to +ammendment. + +### Semantics + +An expression is observed with a source value and emits a target +one or more times. All expressions emit an initial value. Array +targets are always updated incrementally. Numbers and boolean are +emited anew each time their value changes. + +If any operand is `null` or `undefine`, a binding will not emit an +update. Thus, if a binding’s source becomes invalid, it does not +corrupt its target but waits until a valid replacement becomes +available. + +- Literals are interpreted as their corresponding value. +- Value terms provide the source. +- Parameters terms provide the parameters. +- In a path-expression, the first term is evaluated with the source + value. +- Each subsequent term of a path expression uses the target of the + previous as its source. +- A property-expression or variable-property-expression observes the + key of the source object using `Object.addPropertyChangeListener`. +- An element identifier (with the `#` prefix) uses the `document` + property of the `parameters` object and emits + `document.getElementById(id)`, or dies trying. Changes to the + document are not observed. +- A component label (with the `@` prefix) uses the `serialization` + property of `parameters` object and emits + `serialization.getObjectForLable(label)`, or dies trying. Changes + to the serialization are not observed. This syntax exists to + support [Montage][] serializations. +- A "parent" scope operator, `^` observes the given expression in the + context of the current scope's parent. +- A "with" scope operator, e.g., `context.(expression)`, observes the + given expression in a new scope that uses the `context` as its value + and the current scope as its parent. +- A "map" block observes the source array and emits a target array. + The target array is emitted once and all subsequent updates are + reflected as content changes that can be independently observed with + `addRangeChangeListener`. Each element of the target array + corresponds to the observed value of the block expression using the + respective element in the source array as the source value. +- A "map" function call receives a function as its argument rather + than a block. +- A "filter" block observes the source array and emits a target array + containing only those values from the source array that actively + pass the predicate described in the block expression useing the + respective element in the source array as the source value. As with + "map", filters update the target array incrementally. +- A "some" block observes whether any of the values in the source + collection meet the given criterion. +- A "every" block observes whether all of the values in the source + collection meet the given criterion. +- A "sorted" block observes the sorted version of an array, by a + property of each value described in the block, or itself if empty. + Sorted arrays are incrementally updating as values are added and + deleted from the source. +- A "sortedSet" block observes a collection that emits range change + events, by way of a property of each value described in the block, + or itself if empty, emitting a `SortedSet` value exactly once. If + the input is or becomes invalid, the sorted set is cleared, not + replaced. The sorted set will always contain the last of each group + of equivalant values from the input. +- A "min" block observes the which of the values in a given collection + produces the smallest value through the given relation. +- A "max" block observes the which of the values in a given collection + produces the largest value through the given relation. +- A "group" block observes which values belong to corresponding + equivalence classes as determined by the result of a given + expression on each value. The observer is responsible for adding + and removing classes as they are populated and depopulated. Each + class tracks the key (result of the block expression for every + member of a class), and an the values of the corresponding class as + an array. Values are added to the end of each array as they are + discovered. +- Any function call with a "block" implies calling the function on the + result of a "map" block. +- A "flatten" function call observes a source array and produces a + target array. The source array must only contain inner arrays. The + target array is emitted once and all subsequent updates can be + independently observed with `addRangeChangeListener`. The target + array will always contain the concatenation of all of the source + arrays. Changes to the inner and outer source arrays are reflected + with incremental splices to the target array in their corresponding + positions. +- A "concat" function call observes a source array and all of its + argument arrays and effectively flattens all of these arrays. +- A "reversed" function call observes the source array and produces a + target array that contains the elements of the source array in + reverse order. The target is incrementally updated. +- An "enumerate" expression observes [key, value] pairs from an array. + The output array of arrays is incrementally updated with range + changes from the source. +- A "view" function call observes a sliding window from the source, + from a start index (first argument) of a certain length (second + argument). The source can be any collection that dispatches range + changes and the output will be an array of the given length. +- A "sum" function call observes the numeric sum of the source array. + Each alteration to the source array causes a new sum to be emitted, + but the sum is computed incrementally by observing the smaller sums + of the spliced values, added and removed. +- An "average" function call observes the average of the input values, + much like "sum". +- A "last" function call observes the last of the input values, if + there is one. It does this by watching range changes that overlap + the last entry of the collection and emitting the new last value + when necessary, or undefined if the collection becomes empty. +- An "only" function call observes the only value of the input values, + if there is only one such value. If there are none or more than + one, the only function emits undefined. +- A "one" function call observes one of the values from a collection, + if there is one. Otherwise it is undefined. The collection is at + liberty to determine whatever value it can most quickly and sensibly + provide. +- A "round" function call observes the nearest integer to the input + value, rounding `0.5` toward infinity. +- A "floor" function call observes the nearest integer to the input + value toward -infinity; +- A "ceil" function call observes the nearest integer to the input + value toward infinity; +- A "has" function call observes the source collection for whether it + contains an observed value. +- A "tuple" expression observes a source value and emits a single + target array with elements corresponding to the respective + expression in the tuple. Each inner expression is evaluated with + the same source value as the outer expression. +- A "startsWith" function call observes whether the left string + starts with the right string. +- An "endsWith" function call observes whether the right string + ends with the right string. +- A "contains" function call observes whether the left string contains + the right string. +- A "join" function observes the left array joined by the right + delimiter, or an empty string. This is not an incremental + operation. +- A "split" function observes the left string broken into an array + between the right delimiter, or an empty string. This is not an + incremental operation. +- A "range" function call observes an array with the given length + containing sequential numbers starting with zero. The output array + is updated incrementally and will dispatch one range change each + time the size changes by any difference. +- A "keys" function call observes an incrementally updated array of + the keys that a given map contains. The keys are maintained in + insertion order. +- A "values" function call observes an incrementally updated array of + the values that a given map contains. The values are maintained in + insertion order. +- An "entries" function call observes an incrementally updated array + of [key, value] pairs from a given mapping. The entries are + retained in insertion order. + +Unary operators: + +- "number" coerces the value to a number. +- "neg" converts a number to its negative. +- "not" converts a boolean to its logical opposite, treating null or + undefined as false. + +Binary operators: + +- "add" adds the left to the right +- "sub" subtracts the right from the left +- "mul" multiples the left to the right +- "div" divides the left by the right +- "mod" produces the left modula the right. This is proper modula, + meaning a negative number that does not divide evenly into a + positive number will produce the difference between that number and + the next evenly divisible number in direction of negative infinity. +- "rem" produces the remainder of dividing the left by the right. If + the left does not divide evenly into the right it will produce the + difference between that number and the next evenly divisible number + in the direction of zero. That is to say, `rem` can produce + negative numbers. +- "pow" raises the left to the power of the right. +- "root" produces the "righth" root of the left. +- "log" produces the logarithm of the left on the right base. +- "lt" less than, as determined with `Object.compare(left, right) < + 0`. +- "le" less than or equal, as determined with `Object.compare(left, + right) <= 0`. +- "gt" greater than, as determined with `Object.compare(left, right) > + 0`. +- "ge" greater than or equal, as determined with `Object.compare(left, + right) >= 0`. +- "compare" as determined by `Object.compare(left, right)`. +- "equals" whether the left is equal to the right as determined by + `Object.equals(left, right)`. +- *Note: there is no "not equals" syntax node. The `!=` operator gets + converted into a "not" node around an "equals" node. +- "and" logical union, or short circuit on false +- "or" logical intersection, or short circuit on true + +Ternary operator: + +- "if" observes the condition (first argument, expression before the + `?`). If the expression is true, the result observes the consequent + expression (second argument, between the question mark and the + colon), and if it is false, the result observes the alternate (the + third argument, after the colon). If the condition is null or + undefined, the result is null or undefined. + +On the left hand side of a binding, the last term has alternate +semantics. Binders receive a target as well as a source. + +- A "with" binding takes a "context" and "expression" argument from + the target, and a "value" expression from the source. If and when + the context is or becomes defined, the binder creates a child scope + with the context as its value and binds the expression in that scope + to the source in its own. +- A "parent" binding takes an "expression" argument from the target, + and a "value" expression from the source. If and when there is a + parent scope, and if and when there is or becomes a value in that + scope, the binder establishes a binding from the source expression + to the target expression in the parent scope. +- A "property" observes an object and a property name from the target, + and a value from the source. When any of these change, the binder + upates the value for the property name of the object. +- A "get" observes a collection and a key from the target, and a value + from the source. When any of these change, the binder updates the + value for the key on the collection using `collection.set(key, + value)`. This is suitable for arrays and custom map + [Collections][]. +- A "equals" expression observes a boolean value from the source. If + that boolean becomes true, the equality expression is made true by + assigning the right expression to the left property of the equality, + turning the "equals" into an "assign" conceptually. No action is + taken if the boolean becomes false. +- A "reversed" expression observes an indexed collection and maintains + a mirror array of that collection. +- A "has" function call observes a boolean value from the source, and + an collection and a sought value from the target. When the value is + true and the value is absent in the collection, the binder uses the + `add` method of the collection (provided by a shim for arrays) to + make it true that the collection contains the sought value. When + the value is false and the value does appear in the collection one + or more times, the binder uses the `delete` or `remove` method of + the collection to remove all occurrences of the sought value. +- An "only" function call binder observes a boolean value from the + source. If the source value and target collection are both defined, + the binder ensures that the source is the only value in the target + collection. The target collection may have the ranged collection + interface (`has` and `swap`) or it may have the set collection + interface (`has`, `clear`, and `add`), and the binder prefers the + former if both are supported because it results in a single range + change dispatch on the target collection. +- An "if" binding observes the condition and binds the target either + to the consequent or alternate. If the condition is null or + undefined, the target is not bound. +- For an "everyBlock" binding, the first argument of the target + expression is the "collection", the second argument is the "block" + expression, and the source is the "guard". If and when the guard is + or becomes true, the binder maintains a child scope for every value + in the collection and binds the "block" in that scope to be true. + If the guard is or becomes false, all of these bindings are + canceled. When the "guard" is false, the every block produces no + bindings, and when the "guard" becomes false, no state is modified. +- For a "someBlock" binding, the first argument of the target + expression is the "collection", the second argument is the "block" + expression, and the source is the "guard". If and when the guard is + or becomes false, the binder maintains a child scope for every value + in the collection and binds the "block" in that scope to be false. + If the guard is or becomes true, all of these bindings are canceled. + When the "guard" is true, the every block produces no bindings, and + when the "guard" becomes true, no state is modified. +- The "and" operator validates the logical expression by binding the + operands. If the source expression is true, both the left and right + argument expressions are bound to true. If the source expression is + false, and the right operand is false, the binding does nothing. If + the source expression is false and the right operand is true, the + left operand is bound to false. +- The "or" operator validates the logical expression by binding the + operands. If the source expression is false, both the left and + right argument expressions are bout to false. If the source + expression is true, and the right operand is true, the binding does + nothing. If the source expression is true and the right operand is + false, the left operand is bound to false. +- The "rangeContent" binding guarantees that the ranged content (as in + subarrays) of the target will be bound to the content of the source, + if both are defined, but will not replace the target collection. + This is useful for ensuring that a property collection with + important event listeners is never replaced if the bound source is + replaced. The source collection must implement range change + dispatch, like Array, Set, List, and SortedSet. +- The "mapContent" binding guarantees that the map content of the + target will be bound to the content of the source, if both are + defined, but will not replace the target map. This is useful for + ensuring that a map property with important event listeners is never + replaced if the bound source is replaced. The source collection + must implement map change dispatch, like Map, Dict, and SortedMap. + +### Language Interface + +```javascript +var parse = require("frb/parse"); +var compileObserver = require("frb/compile-observer"); +var compileBinder = require("frb/compile-binder"); +``` + +- `parse(text)` returns a syntax tree. +- `compileObserver(syntax)` returns an observer function of the form + `observe(callback, source, parameters)` which in turn returns a + `cancel()` function. `compileObserver` visits the syntax tree and + creates functions for each node, using the `observers` module. +- `compileBinder(syntax)` returns a binder function of the form + `bind(observeValue, source, target, parameters)` which in turn + returns a `cancel()` function. `compileBinder` visits the root node + of the syntax tree and delegates to `compileObserver` for its terms. + The root node must be a `property` at this time, but could + conceivably be any function with a clear inverse operation like + `map` and `reversed`. + +### Syntax Tree + +The syntax tree is JSON serializable and has a "type" property. Nodes +have the following types: + +- `value` corresponds to observing the source value +- `parameters` corresponds to observing the parameters object +- `literal` has a `value` property and observes that value +- `element` has an `id` property and observes an element from the + `parameters.document`, by way of `getElementById`. +- `component` has a `label` property and observes a component from the + `parameters.serialization`, by way of `getObjectForLabel`. This + feature support's [Montage][]’s serialization format. + +All other node types have an "args" property that is an array of syntax +nodes (or an "args" object for `record`). + +- `property`: corresponds to observing a property named by the right + argument of the left argument. +- `get`: corresponds to observing the value for a key (second + argument) in a collection (first argument). +- `with`: corresponds to observing the right expression using the left + expression as the source. +- `parent`: corresponds to observing the given expression (only + argument) in the parent scope. +- `has`: corresponds to whether the key (second argument) exists + within a collection (first argument) +- `mapBlock`: the left is the input, the right is an expression to + observe on each element of the input. +- `filterBlock`: the left is the input, the right is an expression to + determine whether the result is included in the output. +- `someBlock`: the left is the input, the right is a criterion. +- `everyBlock`: the left is the input, the right is a criterion. +- `sortedBlock`: the left is the input, the right is a relation on + each value of the input on which to compare to determine the order. +- `sortedSetBlock`: differs only in semantics from `sortedBlock`. +- `minBlock`: the left is the input, the right is a relation on each + value of the input by which to compare the value to others. +- `maxBlock`: the left is the input, the right is a relation on each + value of the input by which to compare the value to others. +- `groupBlock`: the left is the input, the right is an expression that + provides the key for an equivalence class for each value in the + input. The output is an array of entries, `[key, class]`, with the + shared key of every value in the equivalence class. +- `groupMapBlock`: has the same input semantics as `groupBlock`, but + the output is a `Map` instance instead of an array of entries. +- `tuple`: has any number of arguments, each an expression to observe + in terms of the source value. +- `record`: as an args object. The keys are property names for the + resulting object, and the values are the corresponding syntax nodes + for the values. +- `view`: the arguments are the input, the start position, and the + length of the sliding window to view from the input. The input may + correspond to any ranged content collection, like an array or sorted + set. +- `rangeContent`: corresponds to the content of an ordered collection + that can dispatch indexed range changes like an array or sorted set. + This indicates to a binder that it should replace the content of the + target instead of replacing the target property with the observed + content of the source. A range content node has no effect on the + source. +- `mapContent`: corresponds to the content of a map-like collection + including arrays and all map [Collections][]. These collections + dispatch map changes, which create, read, update, or delete + key-to-value pairs. This indicates to a binder to replace the + content of the target map-like collection with the observed content + of the source, instead of replacing the target collection. A map + change node on the source side just passes the collection forward + without alteration. + +For all operators, the "args" property are operands. The node types for +unary operators are: + +- ```+```: `number`, arithmetic coercion +- ```-```: `neg`, arithmetic negation +- ```!```: `not`, logical negation + +For all binary operators, the node types are: + +- ```**```: `pow`, exponential power +- ```//```: `root`, of 2 square root, of 3 cube root, etc +- ```%%```: `log`, logarithm with base +- ```*```: `mul`, multiplication +- ```/```: `div`, division +- ```%```: `mod`, modulo (toward negative infinity, always positive) +- ```rem```: `rem`, remainder (toward zero, negative if negative) +- ```+```: `add`, addition +- ```-```: `sub`, subtraction +- ```<```: `lt`, less than +- ```<=```: `le`, less than or equal +- ```>```: `gt`, greater than +- ```>=```: `ge`, greater than or equal +- ```<=>```: `compare` +- ```==```: ``equals``, equality comparison and assignment +- ```!=``` produces unary negation and equality comparison or + assignment so does not have a corresponding node type. The + simplification makes it easier to rotate the syntax tree + algebraically. +- ```&&```, `and`, logical and +- ```||```, `or`, logical or +- ```??```, `default` + +For the ternary operator: + +- ```?``` and ```:```: `if`, ternary conditional + +For all function calls, the right hand side is a tuple of arguments. + +- `reversed()` +- `enumerate()` +- `flatten()` +- `sum()` +- `average()` +- `last()` +- `only()` +- `one()` +- `startsWith(other)` +- `endsWith(other)` +- `contains(other)` +- `join(delimiter)` +- `split(delimiter)` +- `concat(...arrays)` +- `range()` +- `keysArray()` +- `valuesArray()` +- `entriesArray()` +- `defined()` +- `round()` +- `floor()` +- `ceil()` + +### Observers and Binders + +The `observers` module contains functions for making all of the +different types of observers, and utilities for creating new ones. +All of these functions are or return an observer function of the form +`observe(emit, value, parameters)` which in turn returns `cancel()`. + +- `observeValue` +- `observeParameters` +- `makeLiteralObserver(value)` +- `makeElementObserver(id)` +- `makeComponentObserver(label)` +- `makeRelationObserver(callback, thisp)` is unavailable through the + property binding language, translates a value through a JavaScript + function. +- `makeComputerObserver(observeArgs, compute, thisp)` applies + arguments to the computation function to get a new value. +- `makeConverterObserver(observeValue, convert, thisp)` calls the + converter function to transform a value to a converted value. +- `makePropertyObserver(observeObject, observeKey)` +- `makeGetObserver(observeCollection, observeKey)` +- `makeMapFunctionObserver(observeArray, observeFunction)` +- `makeMapBlockObserver(observeArray, observeRelation)` +- `makeFilterBlockObserver(observeArray, observePredicate)` +- `makeSortedBlockObserver(observeArray, observeRelation)` +- `makeEnumerationObserver(observeArray)` +- `makeFlattenObserver(observeOuterArray)` +- `makeTupleObserver(...observers)` +- `makeObserversObserver(observers)` +- `makeReversedObserver(observeArrayh)` +- `makeWindowObserver` is not presently available through the language + and is subject to change. It is for watching a length from an array + starting at an observable index. +- `makeSumObserver(observeArray)` +- `makeAverageObserver(observeArray)` +- `makeParentObserver(observeExpression)` +- *etc* + +These are utilities for making observer functions. + +- `makeNonReplacing(observe)` accepts an array observer (the emitted + values must be arrays) and returns an array observer that will only + emit the target once and then incrementally update that target. All + array observers use this decorator to handle the case where the + source value gets replaced. +- `makeArrayObserverMaker(setup)` generates an observer that uses an + array as its source and then incrementally updates a target value, + like `sum` and `average`. The `setup(source, emit)` function must + return an object of the form `{contentChange, cancel}` and arrange + for `emit` to be called with new values when `contentChange(plus, + minus, index)` receives incremental updates. +- `makeUniq(callback)` wraps an emitter callback such that it only + forwards new values. So, if a value is repeated, subsequent calls + are ignored. +- `autoCancelPrevious(callback)` accepts an observer callback and + returns an observer callback. Observer callbacks may return + cancelation functions, so this decorator arranges for the previous + canceler to be called before producing a new one, and arranges for + the last canceler to be called when the whole tree is done. +- `once(callback)` accepts a canceler function and ensures that the + cancelation routine is only called once. + +The `binders` module contains similar functions for binding an observed +value to a bound value. All binders are of the form `bind(observeValue, +source, target, parameters)` and return a `cancel()` function. + +- `makePropertyBinder(observeObject, observeKey)` +- `makeGetBinder(observeCollection, observeKey)` +- `makeHasBinder(observeCollection, observeValue)` +- `makeEqualityBinder(observeLeft, observeRight)` +- `makeRangeContentBinder(observeTarget)` +- `makeMapContentBinder(observeTarget)` +- `makeReversedBinder(observeTarget)` + +This documentation of the internal observer and binder functions is not +exhaustive. + +[Collections]: https://github.com/montagejs/collections +[Define Property]: http://kangax.github.com/es5-compat-table/#define-property-webkit-note +[Montage]: https://github.com/montagejs/montage +[Mr]: https://github.com/montagejs/mr +[Mutation Observers]: https://developer.mozilla.org/en-US/docs/DOM/DOM_Mutation_Observers +[Node.js]: http://nodejs.org/ diff --git a/core/frb/algebra.js b/core/frb/algebra.js new file mode 100644 index 0000000000..bf5a60c3be --- /dev/null +++ b/core/frb/algebra.js @@ -0,0 +1,144 @@ + +// TODO commute literals on the left side of a target operand, when possible + +module.exports = solve; +function solve(target, source) { + return solve.semantics.solve(target, source); +} + +solve.semantics = { + + solve: function (target, source) { + while (true) { + // simplify the target + while (this.simplifiers.hasOwnProperty(target.type)) { + var simplification = this.simplifiers[target.type](target); + if (simplification) { + target = simplification; + } else { + break; + } + } + var canRotateTargetToSource = this.rotateTargetToSource.hasOwnProperty(target.type); + var canRotateSourceToTarget = this.rotateSourceToTarget.hasOwnProperty(source.type); + // solve for bindable target (rotate terms to source) + if (!canRotateTargetToSource && !canRotateSourceToTarget) { + break; + } else if (canRotateTargetToSource) { + source = this.rotateTargetToSource[target.type](target, source); + target = target.args[0]; + } else if (canRotateSourceToTarget) { + target = this.rotateSourceToTarget[source.type](target, source); + source = source.args[0]; + } + } + + return [target, source]; + }, + + simplifiers: { + // !!x -> x + not: function (syntax) { + var left = syntax.args[0]; + if (left.type === "not") { + return left.args[0]; + } + }, + // "" + x -> x.toString() + add: function (syntax) { + var left = syntax.args[0]; + if (left.type === "literal" && left.value === "") { + // "" + x + // toString(x) + // because this can be bound bidirectionally with toNumber(y) + return { + type: "toString", + args: [syntax.args[1]] + }; + } + }, + // DeMorgan's law applied to `some` so we only have to implement + // `every`. + // some{x} -> !every{!x} + someBlock: function (syntax) { + return {type: "not", args: [ + {type: "everyBlock", args: [ + syntax.args[0], + {type: "not", args: [ + syntax.args[1] + ]} + ]} + ]}; + } + }, + + rotateTargetToSource: { + // e.g., + // !y = x + // y = !x + reflect: function (target, source) { + return {type: target.type, args: [source]}; + }, + // e.g., + // y + 1 = x + // y = x - 1 + invert: function (target, source, operator) { + return {type: operator, args: [ + source, + target.args[1] + ]}; + }, + toNumber: function (target, source) { + return this.reflect(target, source); + }, + toString: function (target, source) { + return this.reflect(target, source); + }, + not: function (target, source) { + return this.reflect(target, source); + }, + neg: function (target, source) { + return this.reflect(target, source); + }, + add: function (target, source) { + return this.invert(target, source, 'sub'); + }, + sub: function (target, source) { + return this.invert(target, source, 'add'); + }, + mul: function (target, source) { + return this.invert(target, source, 'div'); + }, + div: function (target, source) { + return this.invert(target, source, 'mul'); + }, + pow: function (target, source) { + return this.invert(target, source, 'root'); + }, + root: function (target, source) { + return this.invert(target, source, 'pow'); + }, + // terms.join(delimiter) <- string + // terms <- string.split(delimiter) + join: function (target, source) { + return this.invert(target, source, 'split'); + }, + split: function (target, source) { + return this.invert(target, source, 'join'); + } + }, + + rotateSourceToTarget: { + // y = x.rangeContent() + // y.rangeContent() = x + rangeContent: function (target, source) { + if (target.type === "rangeContent") { + return target; + } else { + return {type: source.type, args: [target]}; + } + } + } + +}; + diff --git a/core/frb/assign.js b/core/frb/assign.js new file mode 100644 index 0000000000..7156440f04 --- /dev/null +++ b/core/frb/assign.js @@ -0,0 +1,23 @@ + +var parse = require("./parse"); +var compile = require("./compile-assigner"); +var Scope = require("./scope"); + +// TODO deprecate. this is too easy to implement better at other layers, +// depending on scopes. +module.exports = assign; +function assign(target, path, value, parameters, document, components) { + var syntax; + if (typeof path === "string") { + syntax = parse(path); + } else { + syntax = path; + } + var assign = compile(syntax); + var scope = new Scope(target); + scope.parameters = parameters; + scope.document = document; + scope.components = components; + return assign(value, scope); +} + diff --git a/core/frb/bind.js b/core/frb/bind.js new file mode 100644 index 0000000000..18f4bf7770 --- /dev/null +++ b/core/frb/bind.js @@ -0,0 +1,279 @@ + +var parse = require("./parse"), + solve = require("./algebra"), + stringify = require("./stringify"), + compileObserver = require("./compile-observer"), + compileBinder = require("./compile-binder"), + compileAssigner = require("./compile-assigner"), + Observers = require("./observers"), + observeRangeChange = Observers.observeRangeChange, + Binders = require("./binders"), + Scope = require("./scope"), + ONE_WAY = "<-", + ONE_WAY_RIGHT = "->", + TWO_WAY = "<->"; + + +module.exports = bind; +function bind(target, targetPath, descriptor) { + + descriptor.target = target; + descriptor.targetPath = targetPath; + var source = descriptor.source = descriptor.source || target, + twoWay = descriptor.twoWay = TWO_WAY in descriptor, + sourcePath = descriptor.sourcePath = !twoWay ? descriptor[ONE_WAY] : descriptor[TWO_WAY] || "", + parameters = descriptor.parameters = descriptor.parameters || source, + document = descriptor.document, + components = descriptor.components, + trace = descriptor.trace, + + // TODO: consider the possibility that source and target have intrinsic + // scope properties + + sourceScope = /*descriptor.sourceScope =*/ new Scope(source), + targetScope = /*descriptor.targetScope =*/ new Scope(target); + + sourceScope.parameters = parameters; + sourceScope.document = document; + sourceScope.components = components; + targetScope.parameters = parameters; + targetScope.document = document; + targetScope.components = components; + + // promote convert and revert from a converter object up to the descriptor + if (descriptor.converter) { + var converter = descriptor.converter; + if (converter.convert) { + descriptor.convert = converter.convert.bind(converter); + } + if (converter.revert) { + descriptor.revert = converter.revert.bind(converter); + } + } else if (descriptor.reverter) { + var reverter = descriptor.reverter; + if (reverter.convert) { + descriptor.revert = reverter.convert.bind(reverter); + } + if (reverter.revert) { + descriptor.convert = reverter.revert.bind(reverter); + } + } + + var convert = descriptor.convert; + var revert = descriptor.revert; + + var sourceSyntax = descriptor.sourceSyntax = parse(sourcePath); + var targetSyntax = descriptor.targetSyntax = parse(targetPath); + + var solution = solve(targetSyntax, sourceSyntax); + targetSyntax = solution[0]; + sourceSyntax = solution[1]; + + if (twoWay) { + if (targetSyntax.type === "rangeContent") { + return bindRangeContent( + targetScope, + targetSyntax.args[0], + sourceScope, + sourceSyntax, + convert, + revert, + descriptor, + trace ? { + sourcePath: stringify(sourceSyntax), + targetPath: stringify(targetSyntax.args[0]) + } : null + ); + } + } + + // <- source to target + trace && console.log("DEFINE BINDING", targetPath, ONE_WAY, sourcePath, target); + var cancelSourceToTarget = bindOneWay( + targetScope, + targetSyntax, + sourceScope, + sourceSyntax, + convert, + descriptor, + trace + ); + + // flip the arrow + var solution = solve(sourceSyntax, targetSyntax); + sourceSyntax = solution[0]; + targetSyntax = solution[1]; + + // -> target to source + var cancelTargetToSource = Function.noop; + if (twoWay) { + trace && console.log("DEFINE BINDING", targetPath, ONE_WAY_RIGHT, sourcePath, source); + cancelTargetToSource = bindOneWay( + sourceScope, + sourceSyntax, + targetScope, + targetSyntax, + revert, + descriptor, + trace + ); + } + + return function cancel() { + cancelSourceToTarget(); + cancelTargetToSource(); + }; + +} + +function bindOneWay( + targetScope, + targetSyntax, + sourceScope, + sourceSyntax, + convert, + descriptor, + trace +) { + + var observeSource = compileObserver(sourceSyntax); + if (convert) { + observeSource = Observers.makeConverterObserver( + observeSource, + convert, + sourceScope + ); + } + + var bindTarget = compileBinder(targetSyntax); + return bindTarget( + observeSource, + sourceScope, + targetScope, + descriptor, + trace ? { + sourcePath: stringify(sourceSyntax), + targetPath: stringify(targetSyntax) + } : null + ); + +} + +function bindRangeContent( + targetScope, + targetSyntax, + sourceScope, + sourceSyntax, + convert, + revert, + descriptor, + trace +) { + + var observeSource = compileObserver(sourceSyntax); + var observeTarget = compileObserver(targetSyntax); + var assignSource = compileAssigner(sourceSyntax); + var assignTarget = compileAssigner(targetSyntax); + + var cancel = Function.noop; + + var target; + var source; + // We make multiple uses of the isActive variable. + var isActive; + + // We will observe the source and target expressions independently. For + // initialization, if both produce an array, the source will overwrite the + // content of the target. If only the source produces an array, we will + // propagate it to the target, and if only the target produces an array, + // we'll propagate it to the source. If neither produces an array, we will + // assign one. + + // We check the target expression first, but we will use isActive to + // prevent the target from overwriting an existing source. + + isActive = true; + var cancelTargetObserver = observeTarget(function replaceRangeContentTarget(_target) { + cancel(); + cancel = Function.noop; + trace && console.log("RANGE CONTENT TARGET", trace.targetPath, "SET TO", _target); + if (_target && _target.addRangeChangeListener) { + target = _target; + if (source && target) { + trace && console.log("RANGE CONTENT TARGET REPLACES SOURCE", trace.targetPath, ONE_WAY_RIGHT, trace.sourcePath, "WITH", target); + isActive = true; + source.swap(0, source.length, target); + isActive = false; + cancel = establishRangeContentBinding(); + } else if (!source && !isActive) { + trace && console.log("RANGE CONTENT TARGET INITIALIZED TO COPY OF SOURCE", trace.targetPath, ONE_WAY, tarce.sourcePath, "WITH", source); + assignSource(target.clone(), sourceScope); + } + } + }, targetScope); + isActive = false; + + var cancelSourceObserver = observeSource(function replaceRangeContentSource(_source) { + cancel(); + cancel = Function.noop; + trace && console.log("RANGE CONTENT SOURCE", trace.sourcePath, "SET TO", _source); + if (_source && _source.addRangeChangeListener) { + source = _source; + if (target && source) { + trace && console.log("RANGE CONTENT SOURCE REPLACES TARGET", trace.targetPath, ONE_WAY, trace.sourcePath, "WITH", source); + isActive = true; + target.swap(0, target.length, source); + isActive = false; + cancel = establishRangeContentBinding(); + } else if (!target) { + assignTarget(source.clone(), targetScope); + } + } + }, sourceScope); + + // Even if neither the source nor target are provided, we will kick off + // with an empty array. The source will propagate to the target. + if (!target && !source) { + assignSource([], sourceScope); + } + + function sourceRangeChange(plus, minus, index) { + if (!isActive) { + isActive = true; + trace && console.log("RANGE CONTENT PROPAGATED", trace.targetPath, ONE_WAY, trace.sourcePath, "PLUS", plus, "MINUS", minus, "AT", index); + target.swap(index, minus.length, plus); + isActive = false; + } + } + + function targetRangeChange(plus, minus, index) { + if (!isActive) { + isActive = true; + trace && console.log("RANGE CONTENT PROPAGATED", trace.targetPath, ONE_WAY_RIGHT, trace.sourcePath, "PLUS", plus, "MINUS", minus, "AT", index); + source.swap(index, minus.length, plus); + isActive = false; + } + } + + function establishRangeContentBinding() { + if (source === target) { + return; + } + trace && console.log("RANGE CONTENT BOUND", trace.targetPath, TWO_WAY, trace.sourcePath); + isActive = true; + var cancelSourceRangeChangeObserver = observeRangeChange(source, sourceRangeChange, sourceScope); + var cancelTargetRangeChangeObserver = observeRangeChange(target, targetRangeChange, targetScope); + isActive = false; + return function cancelRangeContentBinding() { + trace && console.log("RANGE CONTENT UNBOUND", trace.targetPath, TWO_WAY, trace.sourcePath); + cancelSourceRangeChangeObserver(); + cancelTargetRangeChangeObserver(); + }; + } + + return function cancelRangeContentBinding() { + cancel(); + cancelTargetObserver(); + cancelSourceObserver(); + }; +} diff --git a/core/frb/binders.js b/core/frb/binders.js new file mode 100644 index 0000000000..e61c04b20d --- /dev/null +++ b/core/frb/binders.js @@ -0,0 +1,459 @@ + +var Scope = require("./scope"); +var Observers = require("./observers"); +var autoCancelPrevious = Observers.autoCancelPrevious; +var observeRangeChange = Observers.observeRangeChange; +var cancelEach = Observers.cancelEach; +var makeNotObserver = Observers.makeNotObserver; +var makeOrObserver = Observers.makeOrObserver; +var makeAndObserver = Observers.makeAndObserver; +var observeValue = Observers.observeValue; +var observeUndefined = Observers.makeLiteralObserver(); +var trueScope = new Scope(true); +var falseScope = new Scope(false); + +function getStackTrace() { + return new Error("").stack.replace(/^.*\n.*\n/, "\n"); +} + +exports.bindProperty = bindProperty; +var _bindProperty = bindProperty; // to bypass scope shadowing problems below +function bindProperty(object, key, observeValue, source, descriptor, trace) { + return observeValue(autoCancelPrevious(function replaceBoundPropertyValue(value) { + if (descriptor.isActive) { + return; + } + try { + descriptor.isActive = true; + trace && console.log("SET", trace.targetPath, "TO", value, "ON", object, "BECAUSE", trace.sourcePath, getStackTrace()); + if (Array.isArray(object) && key >>> 0 === key) { + // TODO spec this case + object.set(key, value); + } else { + object[key] = value; + } + } finally { + descriptor.isActive = false; + } + }), source); +} + +exports.makePropertyBinder = makePropertyBinder; +function makePropertyBinder(observeObject, observeKey) { + return function bindProperty(observeValue, source, target, descriptor, trace) { + return observeKey(autoCancelPrevious(function replaceKey(key) { + if (key == null) return; + function replaceObject(object) { + return (object == null) + ? void 0 + : (object.bindProperty) + ? object.bindProperty(key, observeValue, source, descriptor, trace) + : replaceObject.bindProperty(object, key, observeValue, source, descriptor, trace); + }; + replaceObject.bindProperty = _bindProperty; + return observeObject(autoCancelPrevious(replaceObject), target); + }), target); + }; +} + +exports.bindGet = bindGet; +var _bindGet = bindGet; // to bypass scope shadowing below +function bindGet(collection, key, observeValue, source, descriptor, trace) { + return observeValue(autoCancelPrevious(function replaceValue(value) { + if (descriptor.isActive) { + return; + } + try { + descriptor.isActive = true; + trace && console.log("SET FOR KEY", key, "TO", value, "ON", collection, "BECAUSE", trace.sourcePath, getStackTrace()); + collection.set(key, value); + } finally { + descriptor.isActive = false; + } + }), source); +} + +exports.makeGetBinder = makeGetBinder; +function makeGetBinder(observeCollection, observeKey) { + return function bindGet(observeValue, source, target, descriptor, trace) { + return observeCollection(autoCancelPrevious(function replaceCollection(collection) { + if (!collection) return; + + function replaceKey(key) { + return (key == null) + ? void 0 + : replaceKey.bindGet(collection, key, observeValue, source, descriptor, trace); + }; + replaceKey.bindGet = _bindGet; + + return observeKey(autoCancelPrevious(replaceKey), target); + }), target); + }; +} + +exports.makeHasBinder = makeHasBinder; +function makeHasBinder(observeSet, observeValue) { + return function bindHas(observeHas, source, target, descriptor, trace) { + return observeSet(autoCancelPrevious(function replaceHasBindingSet(set) { + if (!set) return; + return observeValue(autoCancelPrevious(function replaceHasBindingValue(value) { + if (value == null) return; + + function changeWhetherSetHas(has) { + // wait for the initial value to be updated by the + // other-way binding + if (has) { // should be in set + if (!changeWhetherSetHas.set.has(value)) { + trace && console.log("ADD", value, "TO", trace.targetPath, "BECAUSE", trace.sourcePath, getStackTrace()); + changeWhetherSetHas.set.add(value); + } + } else { // should not be in set + while (changeWhetherSetHas.set.has(value)) { + trace && console.log("REMOVE", value, "FROM", trace.targetPath, "BECAUSE", trace.sourcePath, getStackTrace()); + changeWhetherSetHas.set.delete(value); + } + } + }; + changeWhetherSetHas.set = set; + return observeHas(autoCancelPrevious(changeWhetherSetHas), source); + }), target); + }), target); + }; +} + +// a == b <-> c +exports.makeEqualityBinder = makeEqualityBinder; +function makeEqualityBinder(bindLeft, observeRight) { + return function bindEquals(observeEquals, source, target, descriptor, trace) { + // c + return observeEquals(autoCancelPrevious(function changeWhetherEquals(equals) { + if (equals) { + trace && console.log("BIND", trace.targetPath, "TO", trace.sourcePath, getStackTrace()); + // a <-> b + var cancel = bindLeft(observeRight, source, source, descriptor, trace); + return function cancelEqualityBinding() { + trace && console.log("UNBIND", trace.targetPath, "FROM", trace.sourcePath, getStackTrace()); + }; + } + }), target); + }; +} + +// collection.every{condition} <- everyCondition +exports.makeEveryBlockBinder = makeEveryBlockBinder; +function makeEveryBlockBinder(observeCollection, bindCondition, observeValue) { + return function bindEveryBlock(observeEveryCondition, source, target, descriptor, trace) { + return observeEveryCondition(autoCancelPrevious(function replaceCondition(condition) { + if (!condition) return; + return observeCollection(autoCancelPrevious(function replaceCollection(collection) { + if (!collection) return; + var cancelers = []; + function rangeChange(plus, minus, index) { + rangeChange.cancelEach(cancelers.slice(index, index + minus.length)); + var plusMapped = []; + for(var i=0, countI = plus.length, scope;i b +exports.makeReversedBinder = makeReversedBinder; +function makeReversedBinder(observeTarget) { + return function bindReversed(observeSource, source, target, descriptor, trace) { + return observeTarget(autoCancelPrevious(function replaceReversedBindingTarget(target) { + if (!target) return; + return observeSource(autoCancelPrevious(function replaceReversedBindingSource(source) { + if (!source) { + target.clear(); + return; + } + + function rangeChange(plus, minus, index) { + if (isActive(target)) + return; + var reflected = target.length - index - minus.length; + target.swap(reflected, minus.length, plus.reversed()); + } + source.addRangeChangeListener(rangeChange); + rangeChange(source, target, 0); + return function cancelReversedBinding() { + source.removeRangeChangeListener(rangeChange); + }; + }), source); + }), target); + }; +} + +exports.makeDefinedBinder = makeDefinedBinder; +function makeDefinedBinder(bindTarget) { + return function bindReversed(observeSource, sourceScope, targetScope, descriptor, trace) { + return observeSource(autoCancelPrevious(function replaceSource(condition) { + if (!condition) { + return bindTarget( + observeUndefined, + sourceScope, + targetScope, + descriptor, + trace + ); + } else { + return Function.noop; + } + }), targetScope); + } +} + +exports.makeParentBinder = makeParentBinder; +function makeParentBinder(bindTarget) { + return function bindParent(observeSource, sourceScope, targetScope, descriptor, trace) { + if (!targetScope.parent) { + return; + } + return bindTarget(observeSource, sourceScope, targetScope.parent, descriptor, trace); + }; +} + +exports.makeWithBinder = makeWithBinder; +function makeWithBinder(observeTarget, bindTarget) { + return function bindWith(observeSource, sourceScope, targetScope, descriptor, trace) { + return observeTarget(autoCancelPrevious(function replaceTarget(target) { + if (target == null) { + return; + } + return bindTarget( + observeSource, + sourceScope, + targetScope.nest(target), + descriptor, + trace + ); + }), targetScope); + }; +} + +function isActive(target) { + return ( + target.getRangeChangeDescriptor && + target.getRangeChangeDescriptor().isActive + ); +} diff --git a/core/frb/bindings.js b/core/frb/bindings.js new file mode 100644 index 0000000000..0cfe48f99c --- /dev/null +++ b/core/frb/bindings.js @@ -0,0 +1,118 @@ + +var Map = require("collections/map"), + bind = require("./bind"), + compute = require("./compute"), + observe = require("./observe"), + stringify = require("./stringify"); + +var bindingsForObject = new Map(), + owns = Object.prototype.hasOwnProperty, + ONE_WAY = "<-", + TWO_WAY = "<->", + COMPUTE = "compute", + GET = "get", + SET = "set", + WRITABLE = "writable", + CONFIGURABLE = "configurable", + ENUMERABLE = "enumerable"; + +exports.count = 0; +exports.bindings = bindingsForObject; + +exports.defineBindings = defineBindings; +function defineBindings(object, descriptors, commonDescriptor) { + if (descriptors) { + for (var name in descriptors) { + defineBinding(object, name, descriptors[name], commonDescriptor); + } + } + return object; +} + +exports.defineBinding = defineBinding; +function defineBinding(object, name, descriptor, commonDescriptor) { + commonDescriptor = commonDescriptor || defineBinding.empty; + var bindingsForName = defineBinding.getBindings(object), + parameters, document; + + if (bindingsForName.has(name)) { + throw new Error("Can't bind to already bound target, " + JSON.stringify(name)); + } + else if (ONE_WAY in descriptor || TWO_WAY in descriptor || COMPUTE in descriptor) { + bindingsForName.set(name,descriptor); + descriptor.target = object; + if((parameters = descriptor.parameters || commonDescriptor.parameters)) + descriptor.parameters = parameters; + if((document = descriptor.document || commonDescriptor.document)) + descriptor.document = document; + descriptor.components = descriptor.components || commonDescriptor.components; + + descriptor.cancel = (COMPUTE in descriptor) + ? defineBinding.compute(object, name, descriptor) + : defineBinding.bind(object, name, descriptor); + + exports.count++; + } else { + if (!(GET in descriptor) && !(SET in descriptor) && !(WRITABLE in descriptor)) { + descriptor.writable = true; + } + if (!(CONFIGURABLE in descriptor)) { + descriptor.configurable = true; + } + if (!(ENUMERABLE in descriptor)) { + descriptor.enumerable = true; + } + Object.defineProperty(object, name, descriptor); + } + return object; +} +defineBinding.empty = Object.empty; +defineBinding.getBindings = getBindings; +defineBinding.compute = compute; +defineBinding.bind = bind; + +exports.getBindings = getBindings; +function getBindings(object) { + var value; + return bindingsForObject.get(object) || (bindingsForObject.set(object, ( value = new Map)) && value); +} + +exports.getBinding = getBinding; +function getBinding(object, name) { + var bindingsForName = getBindings(object); + return bindingsForName.get(name); +} + +exports.cancelBindings = cancelBindings; +function cancelBindings(object) { + var bindings = getBindings(object), + mapIter = bindings.keys(); + + while (name = mapIter.next().value) { + cancelBinding(object, name, bindings); + } + + // for (var name in bindings) { + // cancelBinding(object, name); + // } +} + +exports.cancelBinding = cancelBinding; +function cancelBinding(object, name, bindings/*private argument to call from cancelBindings*/) { + if(!bindings) { + bindings = getBindings(object); + if (!bindings.has(name)) { + throw new Error("Can't cancel non-existent binding to " + JSON.stringify(name)); + } + } + var binding = bindings.get(name); + if (binding && binding.cancel) { + binding.cancel(); + bindings.delete(name); + exports.count--; + + if (bindings.size < 1) { + bindingsForObject.delete(object); + } + } +} diff --git a/core/frb/checklist.csv b/core/frb/checklist.csv new file mode 100644 index 0000000000..3cd5023839 --- /dev/null +++ b/core/frb/checklist.csv @@ -0,0 +1,97 @@ +https://docs.google.com/spreadsheet/ccc?key=0An5phhxDkYDPdFR4eDNFZmNvem5BNUtMdjBXWEhzUlE,,,,,,,,,,, +,implementation and spec,,,,,,,documentation,,, +type,parse,stringify,expand,evaluate,observe,bind,assign,tutorial,observer semantics,binder semantics,syntax +,,,,,,,,,,, +literal,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +value,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +parameters,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +element,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +component,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +tuple,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +record,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +,,,,,,,,,,, +property,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y +get,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y +with,Y,Y,Y,Y,Y,X,X,Y,Y,Y,Y +parent,Y,Y,Y,Y,Y,X,X,Y,Y,Y,Y +has,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y +if,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y +default,Y,Y,Y,Y,Y,X,X,Y,Y,Y,Y +defined,Y,Y,Y,Y,Y,X,X,Y,Y,Y,Y +,,,,,,,,,,, +mapBlock,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +filterBlock,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +sortedBlock,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +sortedSetBlock,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +groupBlock,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +groupMapBlock,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +mapContent,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y +rangeContent,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y +someBlock,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y +everyBlock,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y +,,,,,,,,,,, +entries,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +values,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +keys,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +,,,,,,,,,,, +enumerate,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +reversed,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y +flatten,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +concat,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +view,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +order,N,N,Y,N,N,X,X,N,N,X,N +,,,,,,,,,,, +sum,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +average,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +last,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +one,Y,Y,Y,Y,Y,X,X,Y,N,X,Y +only,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y +minBlock,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +maxBlock,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +,,,,,,,,,,, +not,Y,Y,Y,Y,Y,X,X,Y,Y,Y,Y +neg,Y,Y,Y,Y,Y,X,X,Y,Y,Y,Y +number,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +,,,,,,,,,,, +add,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +sub,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +mul,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +div,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +mod,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +rem,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +pow,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +root,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +log,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +,,,,,,,,,,, +equals,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y +lt,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +le,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +gt,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +ge,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +compare,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +floor,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +ceil,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +round,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +,,,,,,,,,,, +and,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y +or,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y,Y +,,,,,,,,,,, +startsWith,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +endsWith,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +contains,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +join,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +split,Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +,,,,,,,,,,, +range(end),Y,Y,Y,Y,Y,X,X,Y,Y,X,Y +"range(start, end)",Y,Y,Y,N,N,X,X,N,N,X,N +,,,,,,,,,,, +toArray(ranged),Y,Y,Y,Y,Y,X,X,N,N,X,N +toObject(object),N,N,Y,N,N,X,X,N,N,X,N +toObject(map),N,N,Y,N,N,X,X,N,N,X,N +toObject(entries),N,N,Y,N,N,X,X,N,N,X,N +toSet(ranged),Y,Y,Y,Y,Y,X,X,N,N,X,N +toMap(map),Y,Y,Y,Y,Y,X,X,Y,N,X,N +toMap(object),Y,Y,Y,Y,Y,X,X,Y,N,X,N +toMap(entries),Y,Y,Y,Y,N,X,X,Y,N,X,N +,,,,,,,,,,, +evaluate,Y,Y,Y,Y,Y,X,X,N,N,N,N \ No newline at end of file diff --git a/core/frb/compile-assigner.js b/core/frb/compile-assigner.js new file mode 100644 index 0000000000..ab69afac9b --- /dev/null +++ b/core/frb/compile-assigner.js @@ -0,0 +1,207 @@ + +var compileEvaluator = require("./compile-evaluator"); +var solve = require("./algebra"); +var Scope = require("./scope"); +var valueSyntax = {type: "value"}; +var trueScope = {type: "literal", value: true}; +var falseScope = {type: "literal", value: false}; + +module.exports = compile; +function compile(syntax) { + return compile.semantics.compile(syntax); +} + +compile.semantics = { + + compile: function (syntax) { + var compilers = this.compilers; + if (syntax.type === "equals") { + var assignLeft = this.compile(syntax.args[0]); + var evaluateRight = this.compileEvaluator(syntax.args[1]); + return compilers.equals(assignLeft, evaluateRight); + } else if (syntax.type === "if") { + var evaluateCondition = this.compileEvaluator(syntax.args[0]); + var assignConsequent = this.compile(syntax.args[1]); + var assignAlternate = this.compile(syntax.args[2]); + return compilers["if"](evaluateCondition, assignConsequent, assignAlternate); + } else if (syntax.type === "and" || syntax.type === "or") { + var leftArgs = solve(syntax.args[0], valueSyntax); + var rightArgs = solve(syntax.args[1], valueSyntax); + var evaluateLeft = this.compileEvaluator(syntax.args[0]); + var evaluateRight = this.compileEvaluator(syntax.args[1]); + var evaluateLeftAssign = this.compileEvaluator(leftArgs[1]); + var evaluateRightAssign = this.compileEvaluator(rightArgs[1]); + var assignLeft = this.compile(leftArgs[0]); + var assignRight = this.compile(rightArgs[0]); + return compilers[syntax.type]( + assignLeft, + assignRight, + evaluateLeft, + evaluateRight, + evaluateLeftAssign, + evaluateRightAssign + ); + } else if (syntax.type === "everyBlock") { + var evaluateCollection = this.compileEvaluator(syntax.args[0]); + var args = solve(syntax.args[1], {type: "literal", value: true}); + var assignCondition = this.compile(args[0]); + var evaluateValue = this.compileEvaluator(args[1]); + return compilers["everyBlock"](evaluateCollection, assignCondition, evaluateValue); + } else if (syntax.type === "parent") { + var assignParent = this.compile(syntax.args[0]); + return function (value, scope) { + return assignParent(value, scope.parent); + }; + } else if (compilers.hasOwnProperty(syntax.type)) { + var argEvaluators = syntax.args.map(this.compileEvaluator, this.compileEvaluator.semantics); + if(argEvaluators.length === 1) { + return compilers[syntax.type].call(null, argEvaluators[0]); + } + else if(argEvaluators.length === 2) { + return compilers[syntax.type].call(null, argEvaluators[0], argEvaluators[1]); + } + else { + return compilers[syntax.type].apply(null, argEvaluators); + } + + } else { + throw new Error("Can't compile assigner for " + JSON.stringify(syntax.type)); + } + }, + + compileEvaluator: compileEvaluator, + + compilers: { + + property: function (evaluateObject, evaluateKey) { + return function (value, scope) { + var object = evaluateObject(scope); + if (!object) return; + var key = evaluateKey(scope); + if (key == null) return; + if (Array.isArray(object)) { + object.set(key, value); + } else { + object[key] = value; + } + }; + }, + + get: function (evaluateCollection, evaluateKey) { + return function (value, scope) { + var collection = evaluateCollection(scope); + if (!collection) return; + var key = evaluateKey(scope); + if (key == null) return; + collection.set(key, value); + }; + }, + + has: function (evaluateCollection, evaluateValue) { + return function (has, scope) { + var collection = evaluateCollection(scope); + if (!collection) return; + var value = evaluateValue(scope); + if (has == null) return; + if (has) { + if (!(collection.has || collection.contains).call(collection, value)) { + collection.add(value); + } + } else { + if ((collection.has || collection.contains).call(collection, value)) { + (collection.remove || collection["delete"]).call(collection, value); + } + } + }; + }, + + equals: function (assignLeft, evaluateRight) { + return function (value, scope) { + if (value) { + return assignLeft(evaluateRight(scope), scope); + } + }; + }, + + "if": function (evaluateCondition, assignConsequent, assignAlternate) { + return function (value, scope) { + var condition = evaluateCondition(scope); + if (condition == null) return; + if (condition) { + return assignConsequent(value, scope); + } else { + return assignAlternate(value, scope); + } + }; + }, + + and: function (assignLeft, assignRight, evaluateLeft, evaluateRight, evaluateLeftAssign, evaluateRightAssign) { + return function (value, scope) { + if (value == null) return; + if (value) { + assignLeft(evaluateLeftAssign(trueScope), scope); + assignRight(evaluateRightAssign(trueScope), scope); + } else { + assignLeft(evaluateLeft(scope) && !evaluateRight(scope), scope); + } + } + }, + + or: function (assignLeft, assignRight, evaluateLeft, evaluateRight, evaluateLeftAssign, evaluateRightAssign) { + return function (value, scope) { + if (value == null) return; + if (!value) { + assignLeft(evaluateLeftAssign(falseScope), scope); + assignRight(evaluateRightAssign(falseScope), scope); + } else { + assignLeft(evaluateLeft(scope) || !evaluateRight(scope), scope); + } + } + }, + + rangeContent: function (evaluateTarget) { + return function (value, scope) { + var target = evaluateTarget(scope); + if (!target) return; + if (!value) { + target.clear(); + } else { + target.swap(0, target.length, value); + } + }; + }, + + mapContent: function (evaluateTarget) { + return function (value, scope) { + var target = evaluateTarget(scope); + if (!target) return; + target.clear(); + if (scope.value) { + target.addEach(value); + } + }; + }, + + reversed: function (evaluateTarget) { + return function (value, scope) { + var target = evaluateTarget(scope); + if (!target) return; + target.swap(0, target.length, value.reversed()); + }; + }, + + everyBlock: function (evaluateCollection, assignCondition, evaluateEffect) { + return function (value, scope) { + if (value) { + var collection = evaluateCollection(scope); + var effect = evaluateEffect(scope); + collection.forEach(function (content) { + assignCondition(effect, scope.nest(content)); + }); + } + }; + } + + } + +} diff --git a/core/frb/compile-binder.js b/core/frb/compile-binder.js new file mode 100644 index 0000000000..0261b0b102 --- /dev/null +++ b/core/frb/compile-binder.js @@ -0,0 +1,131 @@ + +var compileObserver = require("./compile-observer"); +var Observers = require("./observers"); +var Binders = require("./binders"); +var solve = require("./algebra"); + +var valueSyntax = {type: "value"}; +var trueSyntax = {type: "literal", value: true}; + +module.exports = compile; +function compile(syntax) { + return compile.semantics.compile(syntax); +} + +compile.semantics = { + + compilers: { + property: Binders.makePropertyBinder, + get: Binders.makeGetBinder, + has: Binders.makeHasBinder, + only: Binders.makeOnlyBinder, + one: Binders.makeOneBinder, + rangeContent: Binders.makeRangeContentBinder, + mapContent: Binders.makeMapContentBinder, + reversed: Binders.makeReversedBinder, + and: Binders.makeAndBinder, + or: Binders.makeOrBinder + }, + + syntaxTypeCompile: { + "default": function (syntax) { + return this.compile(syntax.args[0]); + }, + "literal": function (syntax) { + if (syntax.value == null) { + return Function.noop; + } else { + throw new Error("Can't bind to literal: " + syntax.value); + } + }, + "equals": function (syntax) { + return Binders.makeEqualityBinder(/*bindLeft*/ this.compile(syntax.args[0]), /*observeRight*/ compileObserver(syntax.args[1])); + }, + "if": function (syntax) { + return Binders.makeConditionalBinder( + /*observeCondition*/compileObserver(syntax.args[0]), + /*bindConsequent*/this.compile(syntax.args[1]), + /*bindAlternate*/this.compile(syntax.args[2])); + }, + "and_or": function (syntax) { + var leftArgs = solve(syntax.args[0], valueSyntax); + var rightArgs = solve(syntax.args[1], valueSyntax); + var bindLeft = this.compile(leftArgs[0]); + var bindRight = this.compile(rightArgs[0]); + var observeLeftBind = compileObserver(leftArgs[1]); + var observeRightBind = compileObserver(rightArgs[1]); + var observeLeft = compileObserver(syntax.args[0]); + var observeRight = compileObserver(syntax.args[1]); + return this.compilers[syntax.type]( + bindLeft, + bindRight, + observeLeft, + observeRight, + observeLeftBind, + observeRightBind + ); + }, + "everyBlock": function (syntax) { + var observeCollection = compileObserver(syntax.args[0]); + var args = solve(syntax.args[1], trueSyntax); + var bindCondition = this.compile(args[0]); + var observeValue = compileObserver(args[1]); + return Binders.makeEveryBlockBinder(observeCollection, bindCondition, observeValue); + }, + "rangeContent": function (syntax) { + var observeTarget = compileObserver(syntax.args[0]); + var bindTarget; + try { + bindTarget = this.compile(syntax.args[0]); + } catch (exception) { + bindTarget = Function.noop; + } + return Binders.makeRangeContentBinder(observeTarget, bindTarget); + }, + "defined": function (syntax) { + var bindTarget = this.compile(syntax.args[0]); + return Binders.makeDefinedBinder(bindTarget); + }, + "parent": function (syntax) { + var bindTarget = this.compile(syntax.args[0]); + return Binders.makeParentBinder(bindTarget); + }, + "with": function (syntax) { + var observeTarget = compileObserver(syntax.args[0]); + var bindTarget = this.compile(syntax.args[1]); + return Binders.makeWithBinder(observeTarget, bindTarget); + } + }, + + compile: function compile(syntax) { + var compilers = this.compilers, + syntaxTypeCompile; + if(syntaxTypeCompile = this.syntaxTypeCompile[syntax.type]) { + return syntaxTypeCompile.call(this,syntax); + } + else if (compilers.hasOwnProperty(syntax.type)) { + var argObservers = [], + semantics = compileObserver.semantics; + for(var i=0, countI = syntax.args.length;iOL1`( zS#fbv6-PTWOQ0zPgbYe$vYWcv2+q)@*Kss-GVCf?{tC4U_*iE{*-3H3)Phi-EJPsn?GJ7oDLdoix38-A`)m}VlL!Ea1|RgV zo6u$6qNr#P;r$^A&%EslQL#oYaLVLKkXy#AZP!p~5uF(jzy&q^%<#IS!418Z_^tlK$?Tdeh6ois1A2AW)hQ%Ko>K!l`7OS z*-XfjzgSNHl#07`YiBo77;YxPbA=q_mHg6=XP(FTqnCLEw_~orI&QRp`azU5CJQ7k z#BFG7M+W!E@ z(;_e?)!cf*wWiMP(*S{*1;j?J)Fx_g>rE3=8&-EEF2C}?y%~c#gdT-IrZQwI$=^o2 z4x+Y_l2&&|%ZpUh>LYj&7*iEErfFyjRyYEC2=gll68N7gVcq1i{vO?ZJ0tz%-ykB5 zV7`bVRD@2bAn+TZorQ|qe@fq?%nXjS$73p;lT)|B$bt+CzskAHvU1S8kk6yG;CXl{fx&OkSWPbu|6zVh=V7xL2jTz zC?~z}Re%0VR$yy>rv)y4(4?f=43LwANn$A<%Yw@j-9}<6Uu#C!0__EZs24IO#AF-U z9>AlODa;I&5+>gx>mcti(CnzkRYklMXg##veZTd6TWB}jojNYFjCpl0MK>A*ZYNb(8DEW$-l)5$bP0|mX)F>x1l1U%p%jLxla&^ zxBTJr2bwo5QwBQ$X?TARdT(y;bkBUxC<1Zd=Z;c%?iC#p-6^dX9aExpqVFiHMv|4( zec_$C2sf0{20cy#%qXjiW-VimQ$e8rHa8YuuM};QZ1NuDOwu+Vv1>Mu_u{Ng0wyslQHfxVxmeumU@I`is#%DgW;<$irnsE_0-Cp!88Ci+-JE^Kc zv|RQy9`Jot)vNdc>YQi=r`|wOGv|ygjl0GDCVu46MTJN{i#AJ=&&!$5ImG$iImm^o zRo7+exO3Lz`1z>l*k+c$k~?>5{&5!P*lt$1;3KfI=fTvxh5>ZA90bb;O9ty0v<{mE z+ZSRMQX2vjf)WxN^;@b+icTscY9$Ib`aZgZfCW&+(P<~f=D_ti9mr_zT!{2S+EYTZg@qb zu8S^a#d^iS;=-aE@CeWeXaTqYIvN>Q?N^~zsaKcOqDm;0mRXB@M&0;n+K}5iT+z-J z&cx4=&UDW<`Ly}22+Ig<`Pd1A2vc|g{GDl5TpFw{L-TD)3MI!%<(Vs)oxE*4@>!=@ zaC%Ytzw{bg)*Lk4qMeysI`*XOZ=G}xTxZYg+UnZI-@{&WhyldM#Bc)H${ZQgsf}(r zIVE{COHNJZu{f&?Bi1Qeu}uNoOKmH!;j!^(o8~!F90o|6oSQ+N)$eC-g}y`YLN9Hv zEZ24@-7ufvzQ8%dmcb0dYQhSkvLnl(lp*0GyCQ!7vW71PkASp{?igAyw(VfW8$*XF4t-U4X&~j=l)y=B&#{1%}K=xHQ6A=U!rHqJNwVY=)7(Rln``AVOlosrvrJC`|iNBKaiQE@=~q zf?ch>*4FPfa#1uqyH{j07L|;}d}=`4?)`yy5^;ysg$=?=#A-Ld>-tlq(q!*pw29Uu zJdZ_(B^!BBaiiUEEBzLmPr|RoU$xQt;>FhWBp0qP>oKyV`u#YG>%p3SF{(oG+iJ_C z$9l|t|NYK=6Sun^$e>s!*)G*n{w7QJx9*;tmTpKD3p;c8=>E?R3 z6)i7v=zgfdTjt&EIfaq*N=z$prLdVJaMj`9dPl0z!D!bop{BHiH?QSY;wZckJ2rW@ z3-njjX?E+!0$sG)sgjy9PBt-bf}h!MjoFtI;K_1l0^Ni72YOum?ple^PvrNhZVYv!f&wzXEB))JST&I_OS zvuJ+;96Lwb?;Tvbnvdt8RkzFWmN};szsKi}C@;HHSz2VyzFe zYhCeJr>W8n|BV=$8D-+T>zD6;vu|IQ{`QtcmF!R5 zV7=$P3^gttRL8y_-;nli#%LDEAP)_gPiho8CoBBMA}4_M3gF|EN1X zd|ct z9uT#Ej>~Z4CaID5u~RGH;oOgxI|xVW$}8f|(2ZJ=Zm2NiDanVk3;nxbS7LxM;#b1X z5i26Renb&dYRPTi2BU2ytuC>66ocIia^MFoPa zzf%V+Agg!Q?#u5+V0@cpsjls;t*F3fVrRo_^ux~Bl-b?J9t?RQAOzg`z*QSlXCqQ~ z8=$QdpSvLWe<=9C^?%JQ$7v#-}PF z_22B^UxMTo&d&CHEG%wrZp>~R%yy3EENr~IyezEjEbQz|ULpC|vT@&9ms`(I8DcGmyP`G21L zH>Uv0zXAN80sWV?{?iIJmk^==%m0{O2$8U}pacSf8ADb=RNWo&+y^d^a3RfB%f%&K ziGyN-loMl?HaC+Cku(4zzK0ZD^v7q{jw?_Tz?#4bm~-nr{&Kc#PIAdX-M()6n0Bk6 z?amHQ)<;ej7dMQNYe@F|< zb%V=38-HtvN=Q_K`cZe-DsX8Ftp{y^Vh_sI804@Am-Yl(_#)Q+eXy16*#Y1(WEElw zLI;`*(lk{!lP?;LGYUtZ8k<|C@ul$Fhv?Z(~LErsrfN z?i%Fs{u)AHJ3PN}hfr`&zj6MckjnF%yn5k&(Q??@+RASI)x*zlA=ncE9XXf)o5km@ zcF6S%l&5OqA;l}#ww%H}e{3q{F*Xu7F<-mgJ`#cK?9q-Ucms`nRnY-djd$6f@Kt*6 zH_!0_5XYu8Hqd8T8AF0xBtHAJA>L3I- zzvS0YhPwmrCz`ykdfu?x?0M!P=!WY`p_R~s|oNub!B^FXyp#Y%2)qd;XhfpKgA6HDfeqO zhW=Uu$|{T7DtmWtQd#J^|9nwtrT5UgfqpWo?dq5O##+txc+GZKH=zmEt##mF)=+XIFq;E^hCmU-`#jc<3Cu(dGAGwj{0fVD>p3Rq5#z-gn)sIcAUe z$h?q@L~nMU8{r^y>10vbP?UA02MzV#Nanw{FWjwz*nvBwpnA)>`2mJ)(9M0_ z9sm9C_U@?I3)Rxr-bg|c<`=BFjn30#eiAjt-X(Qzooku0JY@p{UWnq5L zdfQj+NbHe7s8L)%dV{{;%#6>c9R8O3bvJ8k>uWqGCyBqSL7OCO#uBzaYsr+FC^nvN zb3VM!R~iO2B|4o^3A}Nr*fPpFe;z-3rpalBz!jEu`7)g zsCTSw+s-qcCcm{h(@7cgR;qtSY#uNjOQtjZ38`&hVCST^y9C#T7d4pp3njR3q@Y*X z_a=c!xA8YM!}2gjC`xLe@lua(M&nd~eDJO=)*P*b#c;j~1|LtF%!b3J0nyqk6`*7b z-H;76sv6Pkr^i@^UFY?c!KPF5SJPmZ^|s4SBz~Ne)BOg}{zD=U&~P)%Ggjz+UX3_b zS}=dKFE;E%ao_TlI&|0YvNE!qtO>!?g0Ys7vn>WVhDlcA233wws_V zT=EM!|NDCL3=gQV4E2-L->DqG$6`3Y0&)akj%GO1Hmwo@`ib6EG>6&v)4E!h-_eG( z`oOQobJpFTJ*HZ|(u&BU0BCbW+$2aQ^9X*A!0bdemHuFVqFxUtiw zi$rm(mhrHi8aDAU5MQxE8c~@Fukz5U>mLN&;*h(c$IGuHNnz5Fym!(?KVH5P0wi87 z?%n#@a}`ASkS-MoGv*M?1_ofvvi( z0)J11>BmTiw42AJ^;ru{c!5!iHG?-D8&H%bD>QV`E~w_K9oDyJ$A>OxaC@C2tLxZI zI*9~dQORIliV4+4>Y|kz1WQ(2nJHs)cu;d5l2(v-&d!g8Jde6daxMf`K>3Mht5=34 z!g$SZH)BYY3^Nv}1o=d#iRz2Y!z7or8`LXH>+?(RJO^*OTnm}DdQKnjSizHvUm1EP z!IMBbo zvVvaN^1(KBy$B6X1Ky&=!N$f0yP?);e>6$>?{zFwS(ZctA%L=#Oe;)wP6PEVC7}j2AyJ_a=!es%keoVK@3 zPabbsE35FYJ}Koa8Vwd)wT+hq>5vdAt$hR{R5?x({`McsFny5Xb`GJC{K_!PGG)@R zIqo~*CIm|iLD;CmT&9_A%xEoP0C_@bEM)ut#C*fQYYc@rRSz6 znq7z|I=$`NwlpZg{PF=saFfia@A+!ewC-`36(ExP1SMba6C-pYg5dAq*AB^|@;jn} z>Sp76P!L>FS%$D*r>gqKRoDB>hx=|c_l;B$l#_4aczGCm!>?XZBFj(U(k1=O70E4hZyUp8VYcQURHe0xk+AoJh&A2)D+Hjlz@o@C1O#dF5bMYzv|V=GtSAa17-L<$3GZWuhfetZFr~*-ThWdpcbr^ zZaIVI2Qk%*b_;8VXq9uWNawg9f;i{JPfmkNYqosc50GG|5zcBNcXMe7szd9s(ujCm_2(E zOk3v46mhDVNL%E>sP2BLGZK%GVpL417cb}R z4w>f}0D`VqvR!Me`b_5`2tHeAv&TNykoAYg9S8rx{4^#M*+bv6UtB;9<{Mp@Jq~e9 z_;hM>MA#XBLRV+h2WPn-64!n{&=;~u3YKIqM_P@Sq-)qaNn(>*Hy#p=TSm`~xqUvd z0O4sXeg>4QGlItChg9<2QTDKg;Fl=?$gmLr44E>qXzhlFD~EbRR#c^s48^UIJ~bK2 zbNYv%)QYB;X5_00?iJQ2E&vN0S|ppT$m11y3vq#PjXczQ!h0zkilr_uMf$rqnnZ7g z{eRuYv1hklYA^}Vhhz_~q7!Wy{S}uTr&&w+Pe-CpOw>Js3)=O0@X-6+1v>3E3GEIY z{B9wGn3m^vYFG0_c~p^Re|RWpfK*K9)eX~O#hTtL`OHKxn!8|JsSuQ8zevjdGOh)M z4>Zxpp9&D1B}1wpaX~FYfxd7O&7Fb9BQninZw)*ctXjOCoSgjp{0hsed`=|IBCMSC zL8AC;xzwKCQu4v{n^2&{k|u=BN7^U!!Le=#eO57k&pV4U7Tp6qg4?)eZ!}X~x$vpX ze!3!jRGMM3Zx9*m0$qcG1wBniJ4WKPcd$o%;3$I=Uy&-x>x~9yO`A5sW~a6$?_mb^ ziwz`jG!6rP^TXs}8?TTM7uNy^9Pbw$D!hG>jv7O*z2wIRd_oE78??VIZ{CA_?8l4h zOq&TOB?LJkHv>E%;)-*&nz{(0c22lY)0p zQeJ*KU#iwJ@@hpBI5mROqz^BJ=eT>6nVPNH0J9;bDY2|qj;;OPJ>9US;0cQcjnR}I~@sbxNg^As8=)ZFk*4%|29{DAn%x@y&kBElH`I9LJ7o&#;@ zjM)dk-*P**g@rn_DF{RQ!n$8G^^?us(*~7a>#R3 zekb;7g#_VOq>pBw5+6e;jVf=e&5K--cHp))RQi?~!E_l{5NC_ACsK3-C;MYPOlMKb z9~`J7C~E(8F1n;O_Sf{^+`qrbGl56)M0y@YTUK#OcBYfhDiVY@jJ z%uP4&3o+$BNbj5YZ6?KOyP{=f^>tYiu#!BzpWiS`&@;(xW1278Vu8&%uEvDxyGRDE!ab4|s(KiQRbm(rQHJVZ(3I z_6t6f@deH_S=-|@W}gY)DTdrK+cE_^sOV-kV6pF?#goIzzSHHK%p>&cp*mBtK zLepyXiScP^YA!A;w5*3Q^eaua30>s7u&O3&P{yt1ZNC!lHglJg7ktzs1E}g4nVg3n zCG23@%8kgfwhem>S>dtOG=iI#C@)a5GyOVdu+KQ!1G zRtuU=Z6I(8eB{D9WmEz4>pmuZmbWuNup^Cb25I)UFJ==mzz}`@;-v>Kjy7cJFRRTa z%&nN$H%-32#KC<&6Ax7N?Z;QL8bhq(Pc%lD8JIm(g&g?rfmFF8zC|_2fNAkYGnhB6 z3Ww;d#PonK^#E3%rjI_mNzpJExmEkYtx@HFYD)hIQhB$=|a-_;6@mW{< z@%Q7TJj877t9`8rlckml%wNbpHA7zUn#<)qXst%cQ;6zBn(pU7?~UUB!8R^xB0CLS zICG$7b{`9xM32)~Sb=r|Tipm3w(whWxvV_YHfx{JA?zl@qdT;Go_c2?Y^3ue3(oh*XO3n5L zwX%7H<3>2?DJ`n;UO_T=w0M?Cn+kA?~v}=wz;+2$)E6ARgr*!+X4}! z=szl(M8Ws=M{?Lbf~Hk=%Slna{&{`i`P2u2tXhVX0Yx7{fQuY!mEn-)SzA1?vwUdP zEHlP=t%CIpfq>sLW7wn~Zttg?ap>oIX5^aZAABH!VpYO5eKcvs)P|L2Y7N zDj<-%=~~~u!RthCa2wAZ4SN=kkrX2w85I@#4nU_H@PpQtbo&M{1L_Yk&pEE5sKo#P zOlB!@2!pHmk29gfrKRC0@X7G(p+z+6K6YFcwbyj{!v#A;>1v$_MuR@f8)1quUUL0r zS&l@}>(VT%oJ(;gu6p0H9g?;~`zU>ZMTPrqHQx&P>`ze6UAuhKo&_(G!S!uhF&<#O#ee}K$D6Sau4_+ zyy8DvaR+d`*Kqdx%8o+sJha&Zxe-+b%l;>B^7sbY!1k7t zTZoH<2oLmAWOb`0?BHN9?4&zM`(rgLCT~KcsppFMnn>kA8OS4mAcEn9_&HuTo7e-pj8#ue#<_n*>C7sXKJq4vmVV?MAh;g z>!rfM?&Gf|G|NP&$pk!H9^d@#LCy&vm2REhwF&)=-v9VgBupnHHfOU9DvdLAh4u z#!A`P!{<(OaEI-BXJp8l1Dpu{neyAJyURJosl6>cd`|&|T4~#E?FAA{bQKYe3H9+R z*IcnwVp_3a_l4YuL4wpVTi?6trhY)`e;I z$8@qmPJ`e}%sn6x1zPI zf0Jt<_#6|AV%nDrL6ARopGh$lVhVXBK4~l-hfMtGnDDrKrB$ip$5N-wWMA9-T%k#x z>lf}gXSwKujsB4Dahj6ztg3uXpw&34TyeJ#!PjDZ-0VD=!_h|rr-vLRf{xipwBaL( zOw-@R0R%^-8OvO*RjLcEp(clb#AgVVn1PyjMNC^wiic+Eslb8BD6t+E!5C*D7xuej z>g3+6O6fI{u2CU%aVKaL?hIFX{$zjZzoxy$Apd*x%s0s6apT2O&MWy?@~m?1E>eFq zBd_*LA(}f^K9CtKgy9!%R@@59EDx9r(t%=Q>mGZiPnE{nJYyXwVPZBnhx{LiQ+F_> zM#{y-H=RL6J2HpjZ49Ws@lC(m1H=la+Kx8~f#yE`<6v94EMf&=nN<`I^oFEs{J4*~ z-v~+O4^iX-kJz6~UQy03u!vmdP(m-*S+lJ7{#kb?T)1u={BGh1hfrj)i$gM!$`%<2 z`D+6_Tbk%{y;TtdDJ%=Wn&9c9a3VL%@;2WRxp|pDy6j2xMOdT}_=iG{(~W%fLTnvk zIm6?kD$x;y2IHcWgG7-4RCI7QQh@j zniR$xLg8d)e@J#^d>uC`FoehJac)N;tfza~*2F6gV0f!)s;|F$crfFXJa2GZCCP%W z2zNrd>Aby+ks*of3uyN@|H6%4T4tT&>0n#r8En`Qy;bSIuIRa7E5WNkZ7eZx%pR`r zx3JZdXAuvXnJIM?i4uDDc{rYe-jcmQ&xR(Dzv)Ma;pRXm^CkaB&xL)>n#k>%wFz-S z=O@Zt1s%oHpJB3v`y)Pi5Cj;TLHik2nPG}tJhL8iNKg+n+$Gg(ITbj%sCE2=;K^pM zm8n<5SEA$tRijwK0rAzU>%Jy0m*Vk^3LGNK@r@$FX$6sI`50W@5N#xAgoOx0y0de4 zw_D?vi8w$LB}|?Yuze3QgsC$$694$Fb!ucHXig5FG_1%Kibi5aILW)i_SE@px&{|h zMq0@2e7QFj*UX^V1$uL1c2Zk_9531bAgI1~G%2>x9*5^|vpt<;q5l}S{=M4QLMJaT zev{xlQYmIRfl2(Ha8imR%=*c zlv@XtUzlVT<;or=7X(agnpg?5$D_x-Yhg4ci(N$BmmcriNal!eIMoDn9q zaU0!lUKv;GvGBw!yA`m%wq*{Ra&T%Meys{4l2t*I$Qk%mpr;W<9l%P^5c+25M=W;L z>cZv{PZqM#t^T^T*264*)zllhuu>^>8u+X1GxWg0_1&8MGD8_kaR}_l4$vLu%BT2E zBIOq5dwns}q;(nvu^fAv??GWrwOf(qPre>%C~f7Uag((c3;Zl)W%{8qhow?ea4dCb z3pHyHdYAF`c)+_ZJ}#t|9M&e{nKZ#m6|A!u6u```#X)Y{%Xe5v0Gg||+S&EN zIoPfJdwWA|BB=yi`^SA3cwb2j-+|RJ|1N|}>q_cRRG4Oi%os5#EB*;kj4}PU{4!1| z*2I}B4!LRec|Aq)iO_O!ccX5=<4%5XC70>93$(qwm&r^9=&B~~PrQU=Ii;b=0jVPV zPjPEvYo1?s$?q7&GYWgxPP!W1Z`HkfeE&k_45x! zeZAJ{O0;l$FjyOPkTI_#F~8-$P+kI?XnL(XUpFC8&8b|@ZVBRd;LoP`efW&+ z^sIm3uMhug1*|0B>!a_H;B|x9Ej@LC+t_7rr>$#f&<-s5cHL@m^*keSvh3~#Y@U@< zBG&g;@#g2a|3n)|Gd*QYEM)i<)~ZIiLv7FBTtlIg3Yr6wd6y(Q$ZW_>;4CvSM2?n! zHtbyBp>$GJbfs(0^4@Jr9XrG2_ju9-_rdlTLr0;){bzHh;aZ<%=63}H#}c~T81e;W z(>OCI28d1WW`KJ;(fgXCWM!)G;dLrvFij%4GntzXwfXx3)Gb9`o?TwD-& zURxzi1lIqdXYSk(bbtj1CX#$w9j`gYK=c_MlV5T)4=tVU8DNjvh22vG9_g+$bceiE zLE^0r-jTMdi+7*^gOJvreS&1nr^2fPkVUvbsmM2-Q7KAu~ z5?_(Q-bx@SUj#)U-l1p+4qHB(=7WBK zwiVHN5(Vomne3?PDvPO21BZ<7S%A_IwSZ{RmtTX*rOobGZRI_c&VLPq=I2H%M9ioY zP|p(|Y)mRX`>NQl`O^ZJTcd03If3+%`n;pG+VLa9-PGy>Jmk_`xJr(Jwd|4QvZ9e$ z&dIGSVkGYfR8eW+J|Fi{(8Gde?D*i7E}E~Y&iAvt(^VVn6KS+$wVy2XVCCxzzFnVW zHn04Ls(64Oqz=#Qb5@E3NKf|ubDPdqtL30U{`?eT1c8N44Idd+Cj`;CO$C1+{6d0y zI|_UyM;bMs_~YL&ANLTjckniDc70$b=1(XyZq9Bjk004X@ctHrVe``%JF`XvAbQ35 z=WUW9%8h0FqXX|W(mu+#s#;&>FD*n}MA-Y~4u9mooF5`3TY5l&5%GcUil`1F_cY*r zR;EU(C{2ZQmDHgbIjFd~59{bo3Ld~$rf>xZc1<&|Ym#R{8vt+X2^VBP2C;Wny7XTT z)CT#snxF^%K~%-a#ou3Ii1I|g^2PF~|72j6qi1f*ZgJVm4dNDtcN1VkggYRYon32j zN}HugsJeEEx%}edkVAT~*-35(HS5(Cvw1A{)4E`WcI+Gc7Lh-}(eZJ>9peg*pqb`l z>T=$$!Jc9TB;X~6=SHqbo|Fu9bU!f1YX#g|;qQUMm9~h(@HO=jA}JSR7_2Q;R~n+t zaD)uQ?fyNsA0a{R4|fkF#j=2I4mdb4yH7j@6^H0TMSBfM5W7l110_h+&#zSQ3y=Gk zLoc5LNLxVX7^>+XsXzjV5PKeEplOe%C>E`)A*=c-VE0e^fbp_9n>GwlN)xSBK=8dq zdVedA#lHiFTV`%Wvo`flhH-d*?NcQUyK%v2bgZI0{~3Hs=SjfV)Qmlk0q!`v4ASNY z?VV?jS&9*qO62CHTN{!e5Hf1J88W5Ba{ELtX~)PUmI`WGRjh_C8{RiP)5qe-(N|zP;+G(3#+SV4n)MG(F zzqZ4$f>)P=?IhkGb zOwjlMBQHk35ZKwkXn-9>G=J01EI|+cEYYBWIwypo{}8EduF1`xzn^ao4bS>v|L~mK zKS{~ef~8In!wPAY<|Uef*g^-0}7QS)8#kUB)1KAOim3?4cz{Jfr@~jE2`@l#P20M_FWMZ{g-%bASd!H{l!x@ za|CUlm?0#?6Jc_Q33xeLjU_5Xz=CX+PLjk$w1j8c|Kws}Kc~+kJm>fNcpEeQ1e9Ly z6%l@aS9t6S@>F}%m6qOy_A2U}yxLYaMz|lXGjUl1{L-(7^qUKe4r#O7wd-12s+s-D z9q#~L1Ifo2Ne*ce<%Kx8@+{3J6jn3?yqkf2$yg*+grXjPMIu1aw{uR=7%hPzuo33%-H0WE>oi>BtjnEfDg-YdjO-4gYlE z<=V4TPfk>{$!{Lhbh?5R#EER~m~1*gjez&SZPfxH z-4tfn*BMxLtBXhHBD2ovTYx0675)1M8eC@L2vJNAcH$UH)YC2h#m0kkgSHklUQ3g# zwenbG2ut>{)lapFn92&R=n|)E92ETFJ=o0njdT{k5A1wP(ez|q8#A!P1h7OtYopx{ z7i3iXP?LHTQ%7hngHAG(2h+u{2Y_wt?{LwHiurLj#~QlXZ%Fn|gR37S<~#==bVBCC z6HAg>J10#SP>W)GR<=ePg5Ru;9yAK`-+2lE!Nm}hs3H=$z|(-RDcMeiJ$8kiIjc5^>m$cNtA4~nQNq#xN_a3 zMAyc(_}!?pz(bp(o?`MX`4RB%D0z`pYg3dvEM7Z` zu^%~@3UXsBF4icEt5>4wsAWwv2-JM_je8OPEO~N2vQ>4n04;wwn1p5R)cRl2o%^05 zHSoK^XPXIL1Vnseu5$Bv6IAM-l6^voS8W0EGpit_aH=1-e4H@nHkrS+(2iAV!zERI zqLh46;DmTD@3jpL$)Hxv_HMIr1jiq^~42`fLd%FT%9MIRKR<4anm^>rsJ2dnXSFd4D z0wwT!NDSGNteeWCZ*~}URir{BxUd15%hCnRoYe_(hoc9h0?DbkI%YCbD(DwpAmT0Yz^`9v#P1K^tCZ5>y)u#XZvjQ z9&|Sp&kO{XshA?sz2kym{dAaO7w2>^_|mhn56|@{EXo)0MoQnK-jEyv6LAo&UQ)p{ z2G5~v%^}@_XOGV7|r>1wAmr6xj8eer-ZE&y;pZ$iB7G*fW%L5sF`3EA%Qo%B% z0v`<|i`_lXbt>uQJP>@7vvNe^33&FoPVln%H}o0NB(1QLjzY zZ(s#Jj5pxSJMA*25)KoG3r=TkS)wkoXky@>n@{nP$V&bcPGBa*q$7Nw`06MMilF5bV_$-o zJ)^^Fy(m7{BH+&fJiY&{6BGhWe;Yq}TcfnAEfMJ1cSjwGYy=s7K#NL_IG}dRowI>5 zs(&y0t68X;qcZrf0!?ciereV_$gBFxX|~iZphB47l%wr;=p379??$FSkC@U?d;WrI zdT(B$AaH;4&=_w{WKBBPc8i!Z`@k0~;N1`HjoZBR=oV*EL?}4RFhSFdd{+2rUz+`= zt8HW!PaF4+srkwfx22!Q;ws-RviX$b^zh|sB(Ycfs4Mq> z2naR%;XILCiOp2It;Wk-X-2Tt$<_|yKkS!i7~LQ-9$bwJZu0x#K@5H)J`_tv1+v>n zQey~h3hf0qCJDsHE!*F2P^EaSczX=8`?YxWa^9X4eO0sUrB-WVkFRmk{X{M1C6^#G z46K;BoISd}!3AzOKAsIcyla1JPnIJASQ1fDK*z6>phA3p#6w5eEbM!ZY=t#kN-%%G zxb!xRPLXeLISC)5Y>YBYST3ktl=6UwV?K%`8rhgj`}(H8=ug^AJ*l9mwWfA}s5F~1 zFmd^ZJy=m^hCTjj;gF7**>g*t9|PgI@yiFf=91@O)@51~;a|IT)C>hK zDkV46GtaKGuumzg942Jmkw2e1tltt9g0I}Dz)2N*OA2?xS~raFLU54r=u|b_R9&en z;zewjhE?QhMjSW@#MKXhqwF-HiUGDHG9fuHL^R|E%Zyb@=sPhQ}XUkTHg!eod+9_xU;}-TaO;rUbUqHR9CY zg`I0{gG!vmsNc)}X%svs_1yDs|NTYE!Al4bEfY7HmYH4!K}Ovy=-C*WQj`m(ff@LM z!-$FCC8PxIya`++1EaF-)9;-$r;Ta86w@1G?Z2tuWDhEz0;!3pjWv zj)JpMzAi?`vx!i4;~{jDcegg{k&9kEt<@Pi3fCWYR&B_Lx-aW+f^%IYtzQnpiQoC| zd1+@Bm@gmGo5lrw0u=2I=d?I}$nVyWt#awHw&HKi$+i0QQBnNs;~)J#n1X+m|G%^Z be4y#i|1px_{p|KHSzlICNuo;3F!28YXO_(F literal 0 HcmV?d00001 diff --git a/core/frb/frb.svg b/core/frb/frb.svg new file mode 100644 index 0000000000..3cef600258 --- /dev/null +++ b/core/frb/frb.svg @@ -0,0 +1,15 @@ + + + + + Layer 1 + FRB + + + image/svg+xml + + + + + + diff --git a/core/frb/grammar-0.7.0.js b/core/frb/grammar-0.7.0.js new file mode 100644 index 0000000000..a80a603868 --- /dev/null +++ b/core/frb/grammar-0.7.0.js @@ -0,0 +1,4776 @@ +module.exports = (function() { + /* + * Generated by PEG.js 0.7.0. + * + * http://pegjs.majda.cz/ + */ + + function peg$subclass(child, parent) { + function ctor() { this.constructor = child; } + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + } + + function SyntaxError(expected, found, offset, line, column) { + function buildMessage(expected, found) { + function stringEscape(s) { + function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); } + + return s + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\x08/g, '\\b') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\f/g, '\\f') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x07\x0B\x0E\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) + .replace(/[\x10-\x1F\x80-\xFF]/g, function(ch) { return '\\x' + hex(ch); }) + .replace(/[\u0180-\u0FFF]/g, function(ch) { return '\\u0' + hex(ch); }) + .replace(/[\u1080-\uFFFF]/g, function(ch) { return '\\u' + hex(ch); }); + } + + var expectedDesc, foundDesc; + + switch (expected.length) { + case 0: + expectedDesc = "end of input"; + break; + + case 1: + expectedDesc = expected[0]; + break; + + default: + expectedDesc = expected.slice(0, -1).join(", ") + + " or " + + expected[expected.length - 1]; + } + + foundDesc = found ? "\"" + stringEscape(found) + "\"" : "end of input"; + + return "Expected " + expectedDesc + " but " + foundDesc + " found."; + } + + this.expected = expected; + this.found = found; + this.offset = offset; + this.line = line; + this.column = column; + + this.name = "SyntaxError"; + this.message = buildMessage(expected, found); + } + + peg$subclass(SyntaxError, Error); + + function parse(input) { + var options = arguments.length > 1 ? arguments[1] : {}, + + peg$startRuleFunctions = { expression: peg$parseexpression, sheet: peg$parsesheet }, + peg$startRuleFunction = peg$parseexpression, + + peg$c0 = "expression", + peg$c1 = null, + peg$c2 = [], + peg$c3 = ",", + peg$c4 = "\",\"", + peg$c5 = function(head, tail) { + var result = [head]; + for (var i = 0; i < tail.length; i++) { + result.push(tail[i][2]); + } + return result; + }, + peg$c6 = "(", + peg$c7 = "\"(\"", + peg$c8 = ")", + peg$c9 = "\")\"", + peg$c10 = function() { + return []; + }, + peg$c11 = function(expressions) { + return expressions; + }, + peg$c12 = "", + peg$c13 = "?", + peg$c14 = "\"?\"", + peg$c15 = ":", + peg$c16 = "\":\"", + peg$c17 = function(condition, tail) { + if (tail) { + var consequent = tail[2]; + var alternate = tail[6]; + return { + type: "if", + args: [condition, consequent, alternate] + }; + } else { + return condition; + } + }, + peg$c18 = "||", + peg$c19 = "\"||\"", + peg$c20 = function(head, tail) { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + }, + peg$c21 = "&&", + peg$c22 = "\"&&\"", + peg$c23 = "<=>", + peg$c24 = "\"<=>\"", + peg$c25 = "<=", + peg$c26 = "\"<=\"", + peg$c27 = ">=", + peg$c28 = "\">=\"", + peg$c29 = "<", + peg$c30 = "\"<\"", + peg$c31 = "-", + peg$c32 = "\"-\"", + peg$c33 = ">", + peg$c34 = "\">\"", + peg$c35 = "==", + peg$c36 = "\"==\"", + peg$c37 = "!=", + peg$c38 = "\"!=\"", + peg$c39 = function(left, tail) { + if (!tail) { + return left; + } else { + var operator = tail[1]; + var right = tail[3]; + if (operator === "!=") { + return {type: "not", args: [{type: "equals", args: [left, right]}]}; + } else { + return {type: BINARY[operator], args: [left, right]}; + } + } + }, + peg$c40 = "+", + peg$c41 = "\"+\"", + peg$c42 = "*", + peg$c43 = "\"*\"", + peg$c44 = "/", + peg$c45 = "\"/\"", + peg$c46 = "%", + peg$c47 = "\"%\"", + peg$c48 = "rem", + peg$c49 = "\"rem\"", + peg$c50 = "**", + peg$c51 = "\"**\"", + peg$c52 = "//", + peg$c53 = "\"//\"", + peg$c54 = "%%", + peg$c55 = "\"%%\"", + peg$c56 = "??", + peg$c57 = "\"??\"", + peg$c58 = "!", + peg$c59 = "\"!\"", + peg$c60 = function(operator, arg) { + return {type: UNARY[operator], args: [arg]}; + }, + peg$c61 = function(head, tail) { + for (var i = 0; i < tail.length; i++) { + head = tail[i](head); + } + return head; + }, + peg$c62 = ".", + peg$c63 = "\".\"", + peg$c64 = function(tail) { + return tail; + }, + peg$c65 = "[", + peg$c66 = "\"[\"", + peg$c67 = "]", + peg$c68 = "\"]\"", + peg$c69 = function(arg) { + return function (previous) { + return { + type: "property", + args: [ + previous, + arg + ] + }; + }; + }, + peg$c70 = "{", + peg$c71 = "\"{\"", + peg$c72 = "}", + peg$c73 = "\"}\"", + peg$c74 = function(name, expression) { + if (BLOCKS[name]) { + return function (previous) { + return { + type: BLOCKS[name], + args: [previous, expression] + }; + } + } else if (expression.type === "value") { + return function (previous) { + return { + type: name, + args: [previous] + }; + }; + } else { + return function (previous) { + return { + type: name, + args: [ + {type: "mapBlock", args: [ + previous, + expression + ]} + ] + }; + }; + } + }, + peg$c75 = function(name, args) { + return function (previous) { + return { + type: name, + args: [previous].concat(args) + }; + }; + }, + peg$c76 = function(index) { + return function (previous) { + return { + type: "property", + args: [ + previous, + {type: "literal", value: +index.join("")} + ] + }; + }; + }, + peg$c77 = function(name) { + return function (previous) { + return { + type: "property", + args: [ + previous, + {type: "literal", value: name} + ] + }; + }; + }, + peg$c78 = function(expression) { + return function (previous) { + return { + type: "with", + args: [ + previous, + expression + ] + }; + }; + }, + peg$c79 = "this", + peg$c80 = "\"this\"", + peg$c81 = function() { return {type: "value"}; }, + peg$c82 = "true", + peg$c83 = "\"true\"", + peg$c84 = function() { return {type: "literal", value: true}; }, + peg$c85 = "false", + peg$c86 = "\"false\"", + peg$c87 = function() { return {type: "literal", value: false}; }, + peg$c88 = "null", + peg$c89 = "\"null\"", + peg$c90 = function() { return {type: "literal", value: null}; }, + peg$c91 = "@", + peg$c92 = "\"@\"", + peg$c93 = function(label) { + return {type: "component", label: label}; + }, + peg$c94 = "$", + peg$c95 = "\"$\"", + peg$c96 = function(name) { + return {type: "property", args: [ + {type: "parameters"}, + {type: "literal", value: name} + ]}; + }, + peg$c97 = function() { + return {type: "parameters"}; + }, + peg$c98 = "#", + peg$c99 = "\"#\"", + peg$c100 = function(name) { + return {type: "element", id: name}; + }, + peg$c101 = "&", + peg$c102 = "\"&\"", + peg$c103 = function(name, args) { + return {type: name, args: args, inline: true}; + }, + peg$c104 = "^", + peg$c105 = "\"^\"", + peg$c106 = function(value) { + return {type: "parent", args: [value]}; + }, + peg$c107 = function(expression) { + return expression; + }, + peg$c108 = function(tail) { + return tail({type: "value"}); + }, + peg$c109 = function() { + return {type: "value"}; + }, + peg$c110 = "word", + peg$c111 = /^[a-zA-Z_0-9\-]/, + peg$c112 = "[a-zA-Z_0-9\\-]", + peg$c113 = "string", + peg$c114 = "'", + peg$c115 = "\"'\"", + peg$c116 = function(chars) { return {type: "literal", value: chars.join("")}; }, + peg$c117 = "\"", + peg$c118 = "\"\\\"\"", + peg$c119 = /^[^'\\\0-\x1F]/, + peg$c120 = "[^'\\\\\\0-\\x1F]", + peg$c121 = "\\'", + peg$c122 = "\"\\\\'\"", + peg$c123 = function() { return "'"; }, + peg$c124 = /^[^"\\\0-\x1F]/, + peg$c125 = "[^\"\\\\\\0-\\x1F]", + peg$c126 = "\\\"", + peg$c127 = "\"\\\\\\\"\"", + peg$c128 = function() { return "\""; }, + peg$c129 = "\\\\", + peg$c130 = "\"\\\\\\\\\"", + peg$c131 = function() { return "\\"; }, + peg$c132 = "\\/", + peg$c133 = "\"\\\\/\"", + peg$c134 = function() { return "/"; }, + peg$c135 = "\\b", + peg$c136 = "\"\\\\b\"", + peg$c137 = function() { return "\b"; }, + peg$c138 = "\\f", + peg$c139 = "\"\\\\f\"", + peg$c140 = function() { return "\f"; }, + peg$c141 = "\\n", + peg$c142 = "\"\\\\n\"", + peg$c143 = function() { return "\n"; }, + peg$c144 = "\\r", + peg$c145 = "\"\\\\r\"", + peg$c146 = function() { return "\r"; }, + peg$c147 = "\\t", + peg$c148 = "\"\\\\t\"", + peg$c149 = function() { return "\t"; }, + peg$c150 = "\\0", + peg$c151 = "\"\\\\0\"", + peg$c152 = function() { return "\0"; }, + peg$c153 = "\\u", + peg$c154 = "\"\\\\u\"", + peg$c155 = function(digits) { + return String.fromCharCode(parseInt(digits, 16)); + }, + peg$c156 = /^[0-9a-fA-F]/, + peg$c157 = "[0-9a-fA-F]", + peg$c158 = function() { + return {type: "tuple", args: []}; + }, + peg$c159 = function(expressions) { + return {type: "tuple", args: expressions}; + }, + peg$c160 = function() { return {type: "record", args: []}; }, + peg$c161 = function(pairs) { return {type: "record", args: pairs}; }, + peg$c162 = function(head, tail) { + var result = {}; + result[head[0]] = head[1]; + for (var i = 0; i < tail.length; i++) { + result[tail[i][2][0]] = tail[i][2][1]; + } + return result; + }, + peg$c163 = function(name, value) { return [name, value]; }, + peg$c164 = "number", + peg$c165 = function(parts) { + return {type: "literal", value: +parts} + }, + peg$c166 = /^[eE]/, + peg$c167 = "[eE]", + peg$c168 = /^[+\-]/, + peg$c169 = "[+\\-]", + peg$c170 = /^[0-9]/, + peg$c171 = "[0-9]", + peg$c172 = /^[1-9]/, + peg$c173 = "[1-9]", + peg$c174 = "whitespace", + peg$c175 = /^[\t\x0B\f \xA0\uFEFF]/, + peg$c176 = "[\\t\\x0B\\f \\xA0\\uFEFF]", + peg$c177 = /^[ \xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000]/, + peg$c178 = "[ \\xA0\\u1680\\u180E\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200A\\u202F\\u205F\\u3000]", + peg$c179 = "line terminator", + peg$c180 = /^[\n\r\u2028\u2029]/, + peg$c181 = "[\\n\\r\\u2028\\u2029]", + peg$c182 = "/*", + peg$c183 = "\"/*\"", + peg$c184 = "*/", + peg$c185 = "\"*/\"", + peg$c186 = "any character", + peg$c187 = function(comment) { + return comment; + }, + peg$c188 = function() { + return null; + }, + peg$c189 = function(blocks) { + return {type: "sheet", blocks: blocks}; + }, + peg$c190 = function(label, annotation, statements) { + return { + type: "block", + connection: annotation.connection, + module: annotation.module, + exports: annotation.exports, + label: label, + statements: statements + }; + }, + peg$c191 = function(connection, module, exports) { + return { + connection: {"<": "prototype", ":": "object"}[connection], + module: module && module.value, + exports: exports !== "" ? exports[1] : undefined + }; + }, + peg$c192 = function() { + return {}; + }, + peg$c193 = /^[a-zA-Z_0-9]/, + peg$c194 = "[a-zA-Z_0-9]", + peg$c195 = ";", + peg$c196 = "\";\"", + peg$c197 = function(statement) { + return [statement]; + }, + peg$c198 = "on", + peg$c199 = "\"on\"", + peg$c200 = "before", + peg$c201 = "\"before\"", + peg$c202 = " ", + peg$c203 = "\" \"", + peg$c204 = "->", + peg$c205 = "\"->\"", + peg$c206 = function(when, type, listener) { + return {type: "event", when: when, event: type, listener: listener}; + }, + peg$c207 = "<->", + peg$c208 = "\"<->\"", + peg$c209 = "<-", + peg$c210 = "\"<-\"", + peg$c211 = function(target, arrow, source, descriptor) { + var result = {type: STATEMENTS[arrow], args: [ + target, + source + ]}; + if (descriptor.length) { + var describe = {}; + for (var i = 0; i < descriptor.length; i++) { + describe[descriptor[i][2]] = descriptor[i][6]; + } + result.descriptor = describe; + } + return result; + }, + peg$c212 = function(name, expression) { + return {type: "unit", name: name, value: expression}; + }, + + peg$currPos = 0, + peg$reportedPos = 0, + peg$cachedPos = 0, + peg$cachedPosDetails = { line: 1, column: 1, seenCR: false }, + peg$maxFailPos = 0, + peg$maxFailExpected = [], + peg$silentFails = 0, + + peg$result; + + if ("startRule" in options) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + + function text() { + return input.substring(peg$reportedPos, peg$currPos); + } + + function offset() { + return peg$reportedPos; + } + + function line() { + return peg$computePosDetails(peg$reportedPos).line; + } + + function column() { + return peg$computePosDetails(peg$reportedPos).column; + } + + function peg$computePosDetails(pos) { + function advance(details, startPos, endPos) { + var p, ch; + + for (p = startPos; p < endPos; p++) { + ch = input.charAt(p); + if (ch === "\n") { + if (!details.seenCR) { details.line++; } + details.column = 1; + details.seenCR = false; + } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") { + details.line++; + details.column = 1; + details.seenCR = true; + } else { + details.column++; + details.seenCR = false; + } + } + } + + if (peg$cachedPos !== pos) { + if (peg$cachedPos > pos) { + peg$cachedPos = 0; + peg$cachedPosDetails = { line: 1, column: 1, seenCR: false }; + } + advance(peg$cachedPosDetails, peg$cachedPos, pos); + peg$cachedPos = pos; + } + + return peg$cachedPosDetails; + } + + function peg$fail(expected) { + if (peg$currPos < peg$maxFailPos) { return; } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos; + peg$maxFailExpected = []; + } + + peg$maxFailExpected.push(expected); + } + + function peg$cleanupExpected(expected) { + var i = 0; + + expected.sort(); + + while (i < expected.length) { + if (expected[i - 1] === expected[i]) { + expected.splice(i, 1); + } else { + i++; + } + } + } + + function peg$parseexpression() { + var s0, s1; + + peg$silentFails++; + s0 = peg$parseif(); + peg$silentFails--; + if (s0 === null) { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + + return s0; + } + + function peg$parseexpressions() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parseexpression(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c3; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + s6 = peg$parseexpression(); + if (s6 !== null) { + s4 = [s4, s5, s6]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c3; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + s6 = peg$parseexpression(); + if (s6 !== null) { + s4 = [s4, s5, s6]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + s3 = peg$parse_(); + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c5(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseargs() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c6; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c7); } + } + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 41) { + s3 = peg$c8; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c9); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c10(); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c6; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c7); } + } + if (s1 !== null) { + s2 = peg$parseexpressions(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 41) { + s3 = peg$c8; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c9); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c11(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + + return s0; + } + + function peg$parseif() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10; + + s0 = peg$currPos; + s1 = peg$parseor(); + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 63) { + s4 = peg$c13; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c14); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + s6 = peg$parseexpression(); + if (s6 !== null) { + s7 = peg$parse_(); + if (s7 !== null) { + if (input.charCodeAt(peg$currPos) === 58) { + s8 = peg$c15; + peg$currPos++; + } else { + s8 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s8 !== null) { + s9 = peg$parse_(); + if (s9 !== null) { + s10 = peg$parseexpression(); + if (s10 !== null) { + s4 = [s4, s5, s6, s7, s8, s9, s10]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + if (s3 === null) { + s3 = peg$c12; + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c17(s1,s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseor() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parseand(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + if (input.substr(peg$currPos, 2) === peg$c18) { + s5 = peg$c18; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parseand(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + if (input.substr(peg$currPos, 2) === peg$c18) { + s5 = peg$c18; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parseand(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c20(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseand() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parsecomparison(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + if (input.substr(peg$currPos, 2) === peg$c21) { + s5 = peg$c21; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c22); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsecomparison(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + if (input.substr(peg$currPos, 2) === peg$c21) { + s5 = peg$c21; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c22); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsecomparison(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c20(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsecomparison() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8; + + s0 = peg$currPos; + s1 = peg$parsearithmetic(); + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$parse_(); + if (s3 !== null) { + s4 = peg$currPos; + if (input.substr(peg$currPos, 3) === peg$c23) { + s5 = peg$c23; + peg$currPos += 3; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c24); } + } + if (s5 === null) { + if (input.substr(peg$currPos, 2) === peg$c25) { + s5 = peg$c25; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c26); } + } + if (s5 === null) { + if (input.substr(peg$currPos, 2) === peg$c27) { + s5 = peg$c27; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c28); } + } + if (s5 === null) { + s5 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 60) { + s6 = peg$c29; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c30); } + } + if (s6 !== null) { + s7 = peg$currPos; + peg$silentFails++; + if (input.charCodeAt(peg$currPos) === 45) { + s8 = peg$c31; + peg$currPos++; + } else { + s8 = null; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + peg$silentFails--; + if (s8 === null) { + s7 = peg$c12; + } else { + peg$currPos = s7; + s7 = peg$c1; + } + if (s7 !== null) { + s6 = [s6, s7]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$c1; + } + } else { + peg$currPos = s5; + s5 = peg$c1; + } + if (s5 === null) { + if (input.charCodeAt(peg$currPos) === 62) { + s5 = peg$c33; + peg$currPos++; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c34); } + } + if (s5 === null) { + if (input.substr(peg$currPos, 2) === peg$c35) { + s5 = peg$c35; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c36); } + } + if (s5 === null) { + if (input.substr(peg$currPos, 2) === peg$c37) { + s5 = peg$c37; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c38); } + } + } + } + } + } + } + } + if (s5 !== null) { + s5 = input.substring(s4, peg$currPos); + } + s4 = s5; + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + s6 = peg$parsearithmetic(); + if (s6 !== null) { + s3 = [s3, s4, s5, s6]; + s2 = s3; + } else { + peg$currPos = s2; + s2 = peg$c1; + } + } else { + peg$currPos = s2; + s2 = peg$c1; + } + } else { + peg$currPos = s2; + s2 = peg$c1; + } + } else { + peg$currPos = s2; + s2 = peg$c1; + } + if (s2 === null) { + s2 = peg$c12; + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c39(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsearithmetic() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parsemultiplicative(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 43) { + s6 = peg$c40; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c41); } + } + if (s6 === null) { + if (input.charCodeAt(peg$currPos) === 45) { + s6 = peg$c31; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + } + if (s6 !== null) { + s6 = input.substring(s5, peg$currPos); + } + s5 = s6; + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsemultiplicative(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 43) { + s6 = peg$c40; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c41); } + } + if (s6 === null) { + if (input.charCodeAt(peg$currPos) === 45) { + s6 = peg$c31; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + } + if (s6 !== null) { + s6 = input.substring(s5, peg$currPos); + } + s5 = s6; + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsemultiplicative(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c20(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsemultiplicative() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parseexponential(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 42) { + s6 = peg$c42; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c43); } + } + if (s6 === null) { + if (input.charCodeAt(peg$currPos) === 47) { + s6 = peg$c44; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c45); } + } + if (s6 === null) { + if (input.charCodeAt(peg$currPos) === 37) { + s6 = peg$c46; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c47); } + } + if (s6 === null) { + if (input.substr(peg$currPos, 3) === peg$c48) { + s6 = peg$c48; + peg$currPos += 3; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c49); } + } + } + } + } + if (s6 !== null) { + s6 = input.substring(s5, peg$currPos); + } + s5 = s6; + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parseexponential(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 42) { + s6 = peg$c42; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c43); } + } + if (s6 === null) { + if (input.charCodeAt(peg$currPos) === 47) { + s6 = peg$c44; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c45); } + } + if (s6 === null) { + if (input.charCodeAt(peg$currPos) === 37) { + s6 = peg$c46; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c47); } + } + if (s6 === null) { + if (input.substr(peg$currPos, 3) === peg$c48) { + s6 = peg$c48; + peg$currPos += 3; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c49); } + } + } + } + } + if (s6 !== null) { + s6 = input.substring(s5, peg$currPos); + } + s5 = s6; + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parseexponential(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c20(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseexponential() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parsedefault(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c50) { + s6 = peg$c50; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c51); } + } + if (s6 === null) { + if (input.substr(peg$currPos, 2) === peg$c52) { + s6 = peg$c52; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c53); } + } + if (s6 === null) { + if (input.substr(peg$currPos, 2) === peg$c54) { + s6 = peg$c54; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c55); } + } + } + } + if (s6 !== null) { + s6 = input.substring(s5, peg$currPos); + } + s5 = s6; + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsedefault(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c50) { + s6 = peg$c50; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c51); } + } + if (s6 === null) { + if (input.substr(peg$currPos, 2) === peg$c52) { + s6 = peg$c52; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c53); } + } + if (s6 === null) { + if (input.substr(peg$currPos, 2) === peg$c54) { + s6 = peg$c54; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c55); } + } + } + } + if (s6 !== null) { + s6 = input.substring(s5, peg$currPos); + } + s5 = s6; + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsedefault(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c20(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsedefault() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parseunary(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + if (input.substr(peg$currPos, 2) === peg$c56) { + s5 = peg$c56; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c57); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parseunary(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + if (input.substr(peg$currPos, 2) === peg$c56) { + s5 = peg$c56; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c57); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parseunary(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c20(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseunary() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 33) { + s2 = peg$c58; + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c59); } + } + if (s2 === null) { + if (input.charCodeAt(peg$currPos) === 43) { + s2 = peg$c40; + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c41); } + } + if (s2 === null) { + if (input.charCodeAt(peg$currPos) === 45) { + s2 = peg$c31; + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + } + } + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + s2 = peg$parseunary(); + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c60(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$parsepipe(); + } + + return s0; + } + + function peg$parsepipe() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parsevalue(); + if (s1 !== null) { + s2 = []; + s3 = peg$parsechain(); + while (s3 !== null) { + s2.push(s3); + s3 = peg$parsechain(); + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c61(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsechain() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 46) { + s1 = peg$c62; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c63); } + } + if (s1 !== null) { + s2 = peg$parsetail(); + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c64(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 91) { + s1 = peg$c65; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c66); } + } + if (s1 !== null) { + s2 = peg$parseexpression(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 93) { + s3 = peg$c67; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c68); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c69(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + + return s0; + } + + function peg$parsetail() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parseword(); + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + if (input.charCodeAt(peg$currPos) === 123) { + s2 = peg$c70; + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c71); } + } + if (s2 !== null) { + s3 = peg$parse_(); + if (s3 !== null) { + s4 = peg$parseexpression(); + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + if (input.charCodeAt(peg$currPos) === 125) { + s6 = peg$c72; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c73); } + } + if (s6 !== null) { + peg$reportedPos = s0; + s1 = peg$c74(s1,s4); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parseword(); + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + s2 = peg$parseargs(); + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c75(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parsedigits(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c76(s1); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parseword(); + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c77(s1); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parsearray(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c78(s1); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parseobject(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c78(s1); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c6; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c7); } + } + if (s1 !== null) { + s2 = peg$parseexpression(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 41) { + s3 = peg$c8; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c9); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c78(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + } + } + } + } + } + + return s0; + } + + function peg$parsevalue() { + var s0, s1, s2, s3; + + s0 = peg$parsearray(); + if (s0 === null) { + s0 = peg$parseobject(); + if (s0 === null) { + s0 = peg$parsestring(); + if (s0 === null) { + s0 = peg$parsenumber(); + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c79) { + s1 = peg$c79; + peg$currPos += 4; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c80); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c81(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c82) { + s1 = peg$c82; + peg$currPos += 4; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c83); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c84(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 5) === peg$c85) { + s1 = peg$c85; + peg$currPos += 5; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c86); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c87(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c88) { + s1 = peg$c88; + peg$currPos += 4; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c89); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c90(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 64) { + s1 = peg$c91; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c92); } + } + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$parselabel(); + if (s3 !== null) { + s3 = input.substring(s2, peg$currPos); + } + s2 = s3; + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c93(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 36) { + s1 = peg$c94; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c95); } + } + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$parseword(); + if (s3 !== null) { + s3 = input.substring(s2, peg$currPos); + } + s2 = s3; + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c96(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 36) { + s1 = peg$c94; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c95); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c97(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 35) { + s1 = peg$c98; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c99); } + } + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$parseword(); + if (s3 !== null) { + s3 = input.substring(s2, peg$currPos); + } + s2 = s3; + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c100(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 38) { + s1 = peg$c101; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c102); } + } + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$parseword(); + if (s3 !== null) { + s3 = input.substring(s2, peg$currPos); + } + s2 = s3; + if (s2 !== null) { + s3 = peg$parseargs(); + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c103(s2,s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 94) { + s1 = peg$c104; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c105); } + } + if (s1 !== null) { + s2 = peg$parsevalue(); + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c106(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c6; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c7); } + } + if (s1 !== null) { + s2 = peg$parseexpression(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 41) { + s3 = peg$c8; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c9); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c107(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parsetail(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c108(s1); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = []; + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c109(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + + return s0; + } + + function peg$parseword() { + var s0, s1; + + peg$silentFails++; + s0 = []; + if (peg$c111.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c112); } + } + if (s1 !== null) { + while (s1 !== null) { + s0.push(s1); + if (peg$c111.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c112); } + } + } + } else { + s0 = peg$c1; + } + peg$silentFails--; + if (s0 === null) { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c110); } + } + + return s0; + } + + function peg$parsestring() { + var s0, s1, s2, s3; + + peg$silentFails++; + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 39) { + s1 = peg$c114; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c115); } + } + if (s1 !== null) { + s2 = []; + s3 = peg$parsetickedChar(); + while (s3 !== null) { + s2.push(s3); + s3 = peg$parsetickedChar(); + } + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 39) { + s3 = peg$c114; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c115); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c116(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 34) { + s1 = peg$c117; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c118); } + } + if (s1 !== null) { + s2 = []; + s3 = peg$parsequotedChar(); + while (s3 !== null) { + s2.push(s3); + s3 = peg$parsequotedChar(); + } + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 34) { + s3 = peg$c117; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c118); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c116(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + peg$silentFails--; + if (s0 === null) { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c113); } + } + + return s0; + } + + function peg$parsetickedChar() { + var s0, s1; + + if (peg$c119.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c120); } + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c121) { + s1 = peg$c121; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c122); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c123(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$parseescape(); + } + } + + return s0; + } + + function peg$parsequotedChar() { + var s0, s1; + + if (peg$c124.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c125); } + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c126) { + s1 = peg$c126; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c127); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c128(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$parseescape(); + } + } + + return s0; + } + + function peg$parseescape() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c129) { + s1 = peg$c129; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c130); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c131(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c132) { + s1 = peg$c132; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c133); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c134(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c135) { + s1 = peg$c135; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c136); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c137(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c138) { + s1 = peg$c138; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c139); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c140(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c141) { + s1 = peg$c141; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c142); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c143(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c144) { + s1 = peg$c144; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c145); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c146(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c147) { + s1 = peg$c147; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c148); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c149(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c150) { + s1 = peg$c150; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c151); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c152(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c153) { + s1 = peg$c153; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c154); } + } + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$currPos; + s4 = peg$parsehexDigit(); + if (s4 !== null) { + s5 = peg$parsehexDigit(); + if (s5 !== null) { + s6 = peg$parsehexDigit(); + if (s6 !== null) { + s7 = peg$parsehexDigit(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + if (s3 !== null) { + s3 = input.substring(s2, peg$currPos); + } + s2 = s3; + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c155(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + } + } + } + } + } + } + } + + return s0; + } + + function peg$parsehexDigit() { + var s0; + + if (peg$c156.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c157); } + } + + return s0; + } + + function peg$parsearray() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 91) { + s1 = peg$c65; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c66); } + } + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 93) { + s3 = peg$c67; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c68); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c158(); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 91) { + s1 = peg$c65; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c66); } + } + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + s3 = peg$parseexpressions(); + if (s3 !== null) { + s4 = peg$parse_(); + if (s4 !== null) { + if (input.charCodeAt(peg$currPos) === 93) { + s5 = peg$c67; + peg$currPos++; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c68); } + } + if (s5 !== null) { + peg$reportedPos = s0; + s1 = peg$c159(s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + + return s0; + } + + function peg$parseobject() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 123) { + s1 = peg$c70; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c71); } + } + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 125) { + s3 = peg$c72; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c73); } + } + if (s3 !== null) { + s4 = peg$parse_(); + if (s4 !== null) { + peg$reportedPos = s0; + s1 = peg$c160(); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 123) { + s1 = peg$c70; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c71); } + } + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + s3 = peg$parsepairs(); + if (s3 !== null) { + if (input.charCodeAt(peg$currPos) === 125) { + s4 = peg$c72; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c73); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + peg$reportedPos = s0; + s1 = peg$c161(s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + + return s0; + } + + function peg$parsepairs() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsepair(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c3; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + s6 = peg$parsepair(); + if (s6 !== null) { + s4 = [s4, s5, s6]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c3; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + s6 = peg$parsepair(); + if (s6 !== null) { + s4 = [s4, s5, s6]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c162(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsepair() { + var s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parseword(); + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + if (input.charCodeAt(peg$currPos) === 58) { + s2 = peg$c15; + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s2 !== null) { + s3 = peg$parse_(); + if (s3 !== null) { + s4 = peg$parseexpression(); + if (s4 !== null) { + peg$reportedPos = s0; + s1 = peg$c163(s1,s4); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsenumber() { + var s0, s1, s2; + + peg$silentFails++; + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parsenumberPattern(); + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c165(s1); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + peg$silentFails--; + if (s0 === null) { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c164); } + } + + return s0; + } + + function peg$parsenumberPattern() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parseint(); + if (s1 !== null) { + s2 = peg$parsefrac(); + if (s2 !== null) { + s3 = peg$parseexp(); + if (s3 !== null) { + s1 = [s1, s2, s3]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parseint(); + if (s1 !== null) { + s2 = peg$parsefrac(); + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parseint(); + if (s1 !== null) { + s2 = peg$parseexp(); + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$parseint(); + } + } + } + + return s0; + } + + function peg$parseint() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parsedigit19(); + if (s1 !== null) { + s2 = peg$parsedigits(); + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$parsedigit(); + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 45) { + s1 = peg$c31; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + if (s1 !== null) { + s2 = peg$parsedigit19(); + if (s2 !== null) { + s3 = peg$parsedigits(); + if (s3 !== null) { + s1 = [s1, s2, s3]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 45) { + s1 = peg$c31; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + if (s1 !== null) { + s2 = peg$parsedigit(); + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + } + } + + return s0; + } + + function peg$parsefrac() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 46) { + s1 = peg$c62; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c63); } + } + if (s1 !== null) { + s2 = peg$parsedigits(); + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseexp() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$parsee(); + if (s1 !== null) { + s2 = peg$parsedigits(); + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsedigits() { + var s0, s1; + + s0 = []; + s1 = peg$parsedigit(); + if (s1 !== null) { + while (s1 !== null) { + s0.push(s1); + s1 = peg$parsedigit(); + } + } else { + s0 = peg$c1; + } + + return s0; + } + + function peg$parsee() { + var s0, s1, s2; + + s0 = peg$currPos; + if (peg$c166.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c167); } + } + if (s1 !== null) { + if (peg$c168.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c169); } + } + if (s2 === null) { + s2 = peg$c12; + } + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsedigit() { + var s0; + + if (peg$c170.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c171); } + } + + return s0; + } + + function peg$parsedigit19() { + var s0; + + if (peg$c172.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c173); } + } + + return s0; + } + + function peg$parse_() { + var s0, s1; + + s0 = []; + s1 = peg$parsewhiteSpace(); + if (s1 === null) { + s1 = peg$parselineTerminator(); + } + while (s1 !== null) { + s0.push(s1); + s1 = peg$parsewhiteSpace(); + if (s1 === null) { + s1 = peg$parselineTerminator(); + } + } + + return s0; + } + + function peg$parsewhiteSpace() { + var s0, s1; + + peg$silentFails++; + if (peg$c175.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c176); } + } + if (s0 === null) { + if (peg$c177.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c178); } + } + } + peg$silentFails--; + if (s0 === null) { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c174); } + } + + return s0; + } + + function peg$parselineTerminator() { + var s0, s1; + + peg$silentFails++; + if (peg$c180.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c181); } + } + peg$silentFails--; + if (s0 === null) { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c179); } + } + + return s0; + } + + function peg$parsecomment() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== null) { + if (input.substr(peg$currPos, 2) === peg$c182) { + s2 = peg$c182; + peg$currPos += 2; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c183); } + } + if (s2 !== null) { + s3 = peg$currPos; + s4 = []; + s5 = peg$currPos; + s6 = peg$currPos; + peg$silentFails++; + if (input.substr(peg$currPos, 2) === peg$c184) { + s7 = peg$c184; + peg$currPos += 2; + } else { + s7 = null; + if (peg$silentFails === 0) { peg$fail(peg$c185); } + } + peg$silentFails--; + if (s7 === null) { + s6 = peg$c12; + } else { + peg$currPos = s6; + s6 = peg$c1; + } + if (s6 !== null) { + if (input.length > peg$currPos) { + s7 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s7 = null; + if (peg$silentFails === 0) { peg$fail(peg$c186); } + } + if (s7 !== null) { + s6 = [s6, s7]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$c1; + } + } else { + peg$currPos = s5; + s5 = peg$c1; + } + while (s5 !== null) { + s4.push(s5); + s5 = peg$currPos; + s6 = peg$currPos; + peg$silentFails++; + if (input.substr(peg$currPos, 2) === peg$c184) { + s7 = peg$c184; + peg$currPos += 2; + } else { + s7 = null; + if (peg$silentFails === 0) { peg$fail(peg$c185); } + } + peg$silentFails--; + if (s7 === null) { + s6 = peg$c12; + } else { + peg$currPos = s6; + s6 = peg$c1; + } + if (s6 !== null) { + if (input.length > peg$currPos) { + s7 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s7 = null; + if (peg$silentFails === 0) { peg$fail(peg$c186); } + } + if (s7 !== null) { + s6 = [s6, s7]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$c1; + } + } else { + peg$currPos = s5; + s5 = peg$c1; + } + } + if (s4 !== null) { + s4 = input.substring(s3, peg$currPos); + } + s3 = s4; + if (s3 !== null) { + if (input.substr(peg$currPos, 2) === peg$c184) { + s4 = peg$c184; + peg$currPos += 2; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c185); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + peg$reportedPos = s0; + s1 = peg$c187(s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c188(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } + + return s0; + } + + function peg$parsesheet() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== null) { + s2 = []; + s3 = peg$parseblock(); + while (s3 !== null) { + s2.push(s3); + s3 = peg$parseblock(); + } + if (s2 !== null) { + s3 = peg$parse_(); + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c189(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseblock() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 64) { + s1 = peg$c91; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c92); } + } + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$parselabel(); + if (s3 !== null) { + s3 = input.substring(s2, peg$currPos); + } + s2 = s3; + if (s2 !== null) { + s3 = peg$parse_(); + if (s3 !== null) { + s4 = peg$parseannotation(); + if (s4 === null) { + s4 = peg$c12; + } + if (s4 !== null) { + if (input.charCodeAt(peg$currPos) === 123) { + s5 = peg$c70; + peg$currPos++; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c71); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsestatements(); + if (s7 !== null) { + if (input.charCodeAt(peg$currPos) === 125) { + s8 = peg$c72; + peg$currPos++; + } else { + s8 = null; + if (peg$silentFails === 0) { peg$fail(peg$c73); } + } + if (s8 !== null) { + s9 = peg$parse_(); + if (s9 !== null) { + peg$reportedPos = s0; + s1 = peg$c190(s2,s4,s7); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseannotation() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 60) { + s1 = peg$c29; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c30); } + } + if (s1 === null) { + if (input.charCodeAt(peg$currPos) === 58) { + s1 = peg$c15; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + } + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + s3 = peg$parsestring(); + if (s3 === null) { + s3 = peg$c12; + } + if (s3 !== null) { + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + s6 = peg$currPos; + peg$silentFails++; + if (input.charCodeAt(peg$currPos) === 123) { + s7 = peg$c70; + peg$currPos++; + } else { + s7 = null; + if (peg$silentFails === 0) { peg$fail(peg$c71); } + } + peg$silentFails--; + if (s7 === null) { + s6 = peg$c12; + } else { + peg$currPos = s6; + s6 = peg$c1; + } + if (s6 !== null) { + s7 = peg$parseexpression(); + if (s7 !== null) { + s6 = [s6, s7]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$c1; + } + } else { + peg$currPos = s5; + s5 = peg$c1; + } + if (s5 === null) { + s5 = peg$c12; + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + peg$reportedPos = s0; + s1 = peg$c191(s1,s3,s5); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c192(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } + + return s0; + } + + function peg$parselabel() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = []; + if (peg$c193.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c194); } + } + if (s2 !== null) { + while (s2 !== null) { + s1.push(s2); + if (peg$c193.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c194); } + } + } + } else { + s1 = peg$c1; + } + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 58) { + s4 = peg$c15; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s4 !== null) { + s5 = []; + if (peg$c193.test(input.charAt(peg$currPos))) { + s6 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c194); } + } + if (s6 !== null) { + while (s6 !== null) { + s5.push(s6); + if (peg$c193.test(input.charAt(peg$currPos))) { + s6 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c194); } + } + } + } else { + s5 = peg$c1; + } + if (s5 !== null) { + s4 = [s4, s5]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 58) { + s4 = peg$c15; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s4 !== null) { + s5 = []; + if (peg$c193.test(input.charAt(peg$currPos))) { + s6 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c194); } + } + if (s6 !== null) { + while (s6 !== null) { + s5.push(s6); + if (peg$c193.test(input.charAt(peg$currPos))) { + s6 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c194); } + } + } + } else { + s5 = peg$c1; + } + if (s5 !== null) { + s4 = [s4, s5]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsestatements() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8; + + s0 = peg$currPos; + s1 = peg$parsestatement(); + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + s3 = []; + s4 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 59) { + s5 = peg$c195; + peg$currPos++; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c196); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsestatement(); + if (s7 !== null) { + s8 = peg$parse_(); + if (s8 !== null) { + s5 = [s5, s6, s7, s8]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } else { + peg$currPos = s4; + s4 = peg$c1; + } + while (s4 !== null) { + s3.push(s4); + s4 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 59) { + s5 = peg$c195; + peg$currPos++; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c196); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsestatement(); + if (s7 !== null) { + s8 = peg$parse_(); + if (s8 !== null) { + s5 = [s5, s6, s7, s8]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } + if (s3 !== null) { + if (input.charCodeAt(peg$currPos) === 59) { + s4 = peg$c195; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c196); } + } + if (s4 === null) { + s4 = peg$c12; + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + peg$reportedPos = s0; + s1 = peg$c5(s1,s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parsestatement(); + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 59) { + s3 = peg$c195; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c196); } + } + if (s3 === null) { + s3 = peg$c12; + } + if (s3 !== null) { + s4 = peg$parse_(); + if (s4 !== null) { + peg$reportedPos = s0; + s1 = peg$c197(s1); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c10(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } + } + + return s0; + } + + function peg$parsestatement() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12, s13, s14, s15, s16; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c198) { + s1 = peg$c198; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c199); } + } + if (s1 === null) { + if (input.substr(peg$currPos, 6) === peg$c200) { + s1 = peg$c200; + peg$currPos += 6; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c201); } + } + } + if (s1 !== null) { + if (input.charCodeAt(peg$currPos) === 32) { + s2 = peg$c202; + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c203); } + } + if (s2 !== null) { + s3 = peg$parse_(); + if (s3 !== null) { + s4 = peg$currPos; + s5 = peg$parseword(); + if (s5 !== null) { + s5 = input.substring(s4, peg$currPos); + } + s4 = s5; + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + if (input.substr(peg$currPos, 2) === peg$c204) { + s6 = peg$c204; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c205); } + } + if (s6 !== null) { + s7 = peg$parse_(); + if (s7 !== null) { + s8 = peg$parseexpression(); + if (s8 !== null) { + s9 = peg$parse_(); + if (s9 !== null) { + peg$reportedPos = s0; + s1 = peg$c206(s1,s4,s8); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parseexpression(); + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c15; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s3 === null) { + if (input.substr(peg$currPos, 3) === peg$c207) { + s3 = peg$c207; + peg$currPos += 3; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c208); } + } + if (s3 === null) { + if (input.substr(peg$currPos, 2) === peg$c209) { + s3 = peg$c209; + peg$currPos += 2; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c210); } + } + } + } + if (s3 !== null) { + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$parseexpression(); + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = []; + s8 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s9 = peg$c3; + peg$currPos++; + } else { + s9 = null; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s9 !== null) { + s10 = peg$parse_(); + if (s10 !== null) { + s11 = peg$currPos; + s12 = peg$parseword(); + if (s12 !== null) { + s12 = input.substring(s11, peg$currPos); + } + s11 = s12; + if (s11 !== null) { + s12 = peg$parse_(); + if (s12 !== null) { + if (input.charCodeAt(peg$currPos) === 58) { + s13 = peg$c15; + peg$currPos++; + } else { + s13 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s13 !== null) { + s14 = peg$parse_(); + if (s14 !== null) { + s15 = peg$parseexpression(); + if (s15 !== null) { + s16 = peg$parse_(); + if (s16 !== null) { + s9 = [s9, s10, s11, s12, s13, s14, s15, s16]; + s8 = s9; + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + while (s8 !== null) { + s7.push(s8); + s8 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s9 = peg$c3; + peg$currPos++; + } else { + s9 = null; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s9 !== null) { + s10 = peg$parse_(); + if (s10 !== null) { + s11 = peg$currPos; + s12 = peg$parseword(); + if (s12 !== null) { + s12 = input.substring(s11, peg$currPos); + } + s11 = s12; + if (s11 !== null) { + s12 = peg$parse_(); + if (s12 !== null) { + if (input.charCodeAt(peg$currPos) === 58) { + s13 = peg$c15; + peg$currPos++; + } else { + s13 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s13 !== null) { + s14 = peg$parse_(); + if (s14 !== null) { + s15 = peg$parseexpression(); + if (s15 !== null) { + s16 = peg$parse_(); + if (s16 !== null) { + s9 = [s9, s10, s11, s12, s13, s14, s15, s16]; + s8 = s9; + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } + if (s7 !== null) { + peg$reportedPos = s0; + s1 = peg$c211(s1,s3,s5,s7); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parseword(); + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + s3 = peg$parseexpression(); + if (s3 !== null) { + s4 = peg$parse_(); + if (s4 !== null) { + peg$reportedPos = s0; + s1 = peg$c212(s1,s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + } + + return s0; + } + + + var BINARY = { + "**": "pow", + "//": "root", + "%%": "log", + "*": "mul", + "/": "div", + "%": "mod", + "rem": "rem", + "+": "add", + "-": "sub", + "<": "lt", + ">": "gt", + "<=": "le", + ">=": "ge", + "==": "equals", + "<=>": "compare", + "??": "default", + "&&": "and", + "||": "or", + "<-": "bind", + "<->": "bind2", + ":": "assign" + }; + + var UNARY = { + "+": "toNumber", + "-": "neg", + "!": "not", + "^": "parent" + }; + + var BLOCKS = { + "map": "mapBlock", + "filter": "filterBlock", + "some": "someBlock", + "every": "everyBlock", + "sorted": "sortedBlock", + "sortedSet": "sortedSetBlock", + "group": "groupBlock", + "groupMap": "groupMapBlock", + "min": "minBlock", + "max": "maxBlock" + }; + + var STATEMENTS = { + ":": "assign", + "<-": "bind", + "<->": "bind2" + }; + + + + peg$result = peg$startRuleFunction(); + + if (peg$result !== null && peg$currPos === input.length) { + return peg$result; + } else { + peg$cleanupExpected(peg$maxFailExpected); + peg$reportedPos = Math.max(peg$currPos, peg$maxFailPos); + + throw new SyntaxError( + peg$maxFailExpected, + peg$reportedPos < input.length ? input.charAt(peg$reportedPos) : null, + peg$reportedPos, + peg$computePosDetails(peg$reportedPos).line, + peg$computePosDetails(peg$reportedPos).column + ); + } + } + + return { + SyntaxError: SyntaxError, + parse : parse + }; +})(); diff --git a/core/frb/grammar-maybeValue.pegjs b/core/frb/grammar-maybeValue.pegjs new file mode 100644 index 0000000000..b5c2acbe6b --- /dev/null +++ b/core/frb/grammar-maybeValue.pegjs @@ -0,0 +1,542 @@ + +{ + var BINARY = { + "**": "pow", + "//": "root", + "%%": "log", + "*": "mul", + "/": "div", + "%": "mod", + "rem": "rem", + "+": "add", + "-": "sub", + "<": "lt", + ">": "gt", + "<=": "le", + ">=": "ge", + "==": "equals", + "<=>": "compare", + "??": "default", + "&&": "and", + "||": "or", + "<-": "bind", + "<->": "bind2", + ":": "assign" + }; + + var UNARY = { + "+": "toNumber", + "-": "neg", + "!": "not", + "^": "parent" + }; + + var BLOCKS = { + "map": "mapBlock", + "filter": "filterBlock", + "some": "someBlock", + "every": "everyBlock", + "sorted": "sortedBlock", + "sortedSet": "sortedSetBlock", + "group": "groupBlock", + "groupMap": "groupMapBlock", + "min": "minBlock", + "max": "maxBlock" + }; + + var STATEMENTS = { + ":": "assign", + "<-": "bind", + "<->": "bind2" + }; + +} + +expression "expression" = if + +expressions + = head:expression tail:("," _ expression)* _ { + var result = [head]; + for (var i = 0; i < tail.length; i++) { + result.push(tail[i][2]); + } + return result; + } + +args + = "(" _ ")" { + return []; + } + / "(" expressions:expressions ")" { + return expressions; + } + +if + = condition:or _ tail:( "?" _ conequent:expression _ ":" _ alternate:expression )? { + if (tail) { + var consequent = tail[2]; + var alternate = tail[6]; + return { + type: "if", + args: [condition, consequent, alternate] + }; + } else { + return condition; + } + } + +or + = head:and tail:( _ "||" _ and)* { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + } + +and + = head:comparison tail:( _ "&&" _ comparison)* { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + } + +comparison + = left:arithmetic tail:( _ operator:$( "<=>" / "<=" / ">=" / "<" !("-") / ">" / "==" / "!=" ) _ right:arithmetic )? { + if (!tail) { + return left; + } else { + var operator = tail[1]; + var right = tail[3]; + if (operator === "!=") { + return {type: "not", args: [{type: "equals", args: [left, right]}]}; + } else { + return {type: BINARY[operator], args: [left, right]}; + } + } + } + +arithmetic + = head:multiplicative tail:( _ $( "+" / "-" ) _ multiplicative)* { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + } + +multiplicative + = head:exponential tail:( _ $( "*" / "/" / "%" / "rem" ) _ exponential)* { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + } + +exponential + = head:default tail:( _ $( "**" / "//" / "%%" ) _ default)* { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + } + +default + = head:unary tail:( _ "??" _ unary)* { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + } + +unary + = operator:$("!" / "+" / "-") arg:unary { + return {type: UNARY[operator], args: [arg]}; + } + / pipe + +pipe + = head:value tail:chain* { + for (var i = 0; i < tail.length; i++) { + head = tail[i](head); + } + return head; + } + +chain + = "." tail:tail { + return tail; + } + / "[" arg:expression "]" { + return function (previous) { + return { + type: "property", + args: [ + previous, + arg + ] + }; + }; + } + +tail + = name:$(word) "{" _ expression:expression _ "}" { + if (BLOCKS[name]) { + return function (previous) { + return { + type: BLOCKS[name], + args: [previous, expression] + }; + } + } else if (expression.type === "value") { + return function (previous) { + return { + type: name, + args: [previous] + }; + }; + } else { + return function (previous) { + return { + type: name, + args: [ + {type: "mapBlock", args: [ + previous, + expression + ]} + ] + }; + }; + } + } + / name:$(word) args:args { + return function (previous) { + return { + type: name, + args: [previous].concat(args) + }; + }; + } + / index:digits { + return function (previous) { + return { + type: "property", + args: [ + previous, + {type: "literal", value: +index.join("")} + ] + }; + }; + } + / name:$(word) { + return function (previous) { + return { + type: "property", + args: [ + previous, + {type: "literal", value: name} + ] + }; + }; + } + / expression:array { + return function (previous) { + return { + type: "with", + args: [ + previous, + expression + ] + }; + }; + } + / expression:object { + return function (previous) { + return { + type: "with", + args: [ + previous, + expression + ] + }; + }; + } + / "(" expression:expression ")" { + return function (previous) { + return { + type: "with", + args: [ + previous, + expression + ] + }; + }; + } + +value + = value:maybeValue? { + return value || {"type": "value"} + } + +maybeValue + = array + / object + / string + / number + / "this" { return {type: "value"}; } + / "true" { return {type: "literal", value: true}; } + / "false" { return {type: "literal", value: false}; } + / "null" { return {type: "literal", value: null}; } + / "@" label:$(label) { + return {type: "component", label: label}; + } + / "$" name:$(word) { + return {type: "property", args: [ + {type: "parameters"}, + {type: "literal", value: name} + ]}; + } + / "$" { + return {type: "parameters"}; + } + / "#" name:$(word) { + return {type: "element", id: name}; + } + / "&" name:$(word) args:args { + return {type: name, args: args, inline: true}; + } + / "^" value:value { + return {type: "parent", args: [value]}; + } + / "(" expression:expression ")" { + return expression; + } + / tail:tail { + return tail({type: "value"}); + } + +word "word" + = [a-zA-Z_0-9-]+ + +string "string" + = "'" chars:tickedChar* "'" { return {type: "literal", value: chars.join("")}; } + / '"' chars:quotedChar* '"' { return {type: "literal", value: chars.join("")}; } + +tickedChar + = [^'\\\0-\x1F\x7f] + / "\\'" { return "'"; } + / escape + +quotedChar + = [^"\\\0-\x1F\x7f] + / "\\\"" { return "\""; } + / escape + +escape + = "\\\\" { return "\\"; } + / "\\/" { return "/"; } + / "\\b" { return "\b"; } + / "\\f" { return "\f"; } + / "\\n" { return "\n"; } + / "\\r" { return "\r"; } + / "\\t" { return "\t"; } + / "\\0" { return "\0"; } + / "\\u" digits:$(hexDigit hexDigit hexDigit hexDigit) { + return String.fromCharCode(parseInt(digits, 16)); + } + +hexDigit + = [0-9a-fA-F] + +array + = "[" _ "]" { + return {type: "tuple", args: []}; + } + / "[" _ expressions:expressions _ "]" { + return {type: "tuple", args: expressions}; + } + +object + = "{" _ "}" _ { return {type: "record", args: []}; } + / "{" _ pairs:pairs "}" _ { return {type: "record", args: pairs}; } + +pairs + = head:pair tail:( "," _ pair )* { + var result = {}; + result[head[0]] = head[1]; + for (var i = 0; i < tail.length; i++) { + result[tail[i][2][0]] = tail[i][2][1]; + } + return result; + } + +pair + = name:$(word) ":" _ value:expression { return [name, value]; } + + +// literals closely modeled after the JSON PEGJS example + +number "number" + = parts:$(numberPattern) { + return {type: "literal", value: +parts} + } + +numberPattern + = int frac exp + / int frac + / int exp + / int + +int + = digit19 digits + / digit + / "-" digit19 digits + / "-" digit + +frac + = "." digits + +exp + = e digits + +digits + = digit+ + +e + = [eE] [+-]? + +digit + = [0-9] + +digit19 + = [1-9] + +// white space and comments defined as in the JavaScript PEGJS example + +_ + = ( whiteSpace / lineTerminator )* + +whiteSpace "whitespace" + = [\t\v\f \u00A0\uFEFF] + / [\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000] + +lineTerminator "line terminator" + = [\n\r\u2028\u2029] + +comment + = _ "/*" comment:$(!"*/" . )* "*/" _ { + return comment; + } + / _ { + return null; + } + +// MCS extensions + +sheet + = _ blocks:block* _ { + return {type: "sheet", blocks: blocks}; + } + +block + = "@" label:$(label) _ annotation:annotation? "{" _ statements:statements "}" _ { + return { + type: "block", + connection: annotation.connection, + module: annotation.module, + exports: annotation.exports, + label: label, + statements: statements + }; + } + +annotation + = connection:("<" / ":") _ module:string? _ exports:( !"{" expression )? _ { + return { + connection: {"<": "prototype", ":": "object"}[connection], + module: module && module.value, + exports: exports !== "" ? exports[1] : undefined + }; + } + / _ { + return {}; + } + +label + = [a-zA-Z_0-9]+ ( ":" [a-zA-Z_0-9]+ )* + +statements + = head:statement _ tail:(";" _ statement _)* ";"? _ { + var result = [head]; + for (var i = 0; i < tail.length; i++) { + result.push(tail[i][2]); + } + return result; + } + / statement:statement _ ";"? _ { + return [statement]; + } + / _ { + return []; + } + +statement + = when:("on" / "before") " " _ type:$(word) _ "->" _ listener:expression _ { + return {type: "event", when: when, event: type, listener: listener}; + } + / target:expression _ arrow:(":" / "<->" / "<-") _ source:expression _ + descriptor:("," _ name:$(word) _ ":" _ expression:expression _)* + { + var result = {type: STATEMENTS[arrow], args: [ + target, + source + ]}; + if (descriptor.length) { + var describe = {}; + for (var i = 0; i < descriptor.length; i++) { + describe[descriptor[i][2]] = descriptor[i][6]; + } + result.descriptor = describe; + } + return result; + } + / name:$(word) _ expression:expression _ { + return {type: "unit", name: name, value: expression}; + } + + diff --git a/core/frb/grammar.ebnf b/core/frb/grammar.ebnf new file mode 100644 index 0000000000..765d1757cb --- /dev/null +++ b/core/frb/grammar.ebnf @@ -0,0 +1,98 @@ + +Pipeline ::= + ( + ( + PropertyName | + "#" ElementId | + "@" ComponentLabel | + ArrayExpression | + ObjectExpression | + "(" Expression ")" + ) + ( + "." Transform | + "[" Expression "]" + )* + ( + ".*" | + "[*]" | + ) + )? + +Transform ::= + ( + PropertyName + ) | + ( + "(" Expression ")" | + ArrayExpression | + ObjectExpression + ) | + ( + ( + "flatten" | + "reversed" | + "enumerate" | + "sum" | + "average" + ) + ( + "()" | + "{" Expression "}" + ) + ) | + ( + ( + "map" | + "filter" | + "some" | + "every" | + "sorted" + ) + ( + "(" Expression ")" | + "{" Expression ")" + ) + ) | + "has(" Expression ")" | + "view(" Expression ", " Expression ")" + +ArrayExpression ::= + "[" ( Expression (", " Expression )* )? "]" + +ObjectExpression ::= + "{" + ( + ( PropertyName ": " Expression ) + ( ", " PropertyName ": " Expression )* + )? + "}" + +UnaryExpression ::= + ( "+" | "-" | "!" )? Pipeline +ExponentialExpression ::= + UnaryExpression ( ( "**" | "//" | "%%" ) UnaryExpression )* +MultiplicativeExpression ::= + ExponentialExpression + ( ( "*" | "/" | "%" | "rem" ) ExponentialExpression )* +ArithmeticExpression ::= + MultiplicativeExpression + ( ( "+" | "-" ) MultiplicativeExpression )* +RelationalExpression ::= + ArithmeticExpression + ( ( "==" | "<" | "<=" | ">" | ">=" ) ArithmeticExpression )? +LogicalIntersectionExpression ::= + RelationalExpression ( "&&" LogicalIntersectionExpression )? +LogicalUnionExpression ::= + LogicalIntersectionExpression ( "||" LogicalUnionExpression )? +ConditionalExpression ::= + LogicalUnionExpression ( "?" Expression ":" Expression )? +Expression ::= ConditionalExpression + +Literal ::= NumberLiteral | StringLiteral +NumberLiteral ::= [0-9][1-9]* ( '.' [0-9]+ )? +StringLiteral ::= "'" ( NonQuoteChar | "\" QuoteChar )* "'" +PropertyName ::= [a-zA-Z0-9]+ +ElementId ::= [a-zA-Z0-9]+ +ComponentLabel ::= [a-zA-Z0-9]+ + diff --git a/core/frb/grammar.js b/core/frb/grammar.js new file mode 100644 index 0000000000..a80a603868 --- /dev/null +++ b/core/frb/grammar.js @@ -0,0 +1,4776 @@ +module.exports = (function() { + /* + * Generated by PEG.js 0.7.0. + * + * http://pegjs.majda.cz/ + */ + + function peg$subclass(child, parent) { + function ctor() { this.constructor = child; } + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + } + + function SyntaxError(expected, found, offset, line, column) { + function buildMessage(expected, found) { + function stringEscape(s) { + function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); } + + return s + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\x08/g, '\\b') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\f/g, '\\f') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x07\x0B\x0E\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) + .replace(/[\x10-\x1F\x80-\xFF]/g, function(ch) { return '\\x' + hex(ch); }) + .replace(/[\u0180-\u0FFF]/g, function(ch) { return '\\u0' + hex(ch); }) + .replace(/[\u1080-\uFFFF]/g, function(ch) { return '\\u' + hex(ch); }); + } + + var expectedDesc, foundDesc; + + switch (expected.length) { + case 0: + expectedDesc = "end of input"; + break; + + case 1: + expectedDesc = expected[0]; + break; + + default: + expectedDesc = expected.slice(0, -1).join(", ") + + " or " + + expected[expected.length - 1]; + } + + foundDesc = found ? "\"" + stringEscape(found) + "\"" : "end of input"; + + return "Expected " + expectedDesc + " but " + foundDesc + " found."; + } + + this.expected = expected; + this.found = found; + this.offset = offset; + this.line = line; + this.column = column; + + this.name = "SyntaxError"; + this.message = buildMessage(expected, found); + } + + peg$subclass(SyntaxError, Error); + + function parse(input) { + var options = arguments.length > 1 ? arguments[1] : {}, + + peg$startRuleFunctions = { expression: peg$parseexpression, sheet: peg$parsesheet }, + peg$startRuleFunction = peg$parseexpression, + + peg$c0 = "expression", + peg$c1 = null, + peg$c2 = [], + peg$c3 = ",", + peg$c4 = "\",\"", + peg$c5 = function(head, tail) { + var result = [head]; + for (var i = 0; i < tail.length; i++) { + result.push(tail[i][2]); + } + return result; + }, + peg$c6 = "(", + peg$c7 = "\"(\"", + peg$c8 = ")", + peg$c9 = "\")\"", + peg$c10 = function() { + return []; + }, + peg$c11 = function(expressions) { + return expressions; + }, + peg$c12 = "", + peg$c13 = "?", + peg$c14 = "\"?\"", + peg$c15 = ":", + peg$c16 = "\":\"", + peg$c17 = function(condition, tail) { + if (tail) { + var consequent = tail[2]; + var alternate = tail[6]; + return { + type: "if", + args: [condition, consequent, alternate] + }; + } else { + return condition; + } + }, + peg$c18 = "||", + peg$c19 = "\"||\"", + peg$c20 = function(head, tail) { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + }, + peg$c21 = "&&", + peg$c22 = "\"&&\"", + peg$c23 = "<=>", + peg$c24 = "\"<=>\"", + peg$c25 = "<=", + peg$c26 = "\"<=\"", + peg$c27 = ">=", + peg$c28 = "\">=\"", + peg$c29 = "<", + peg$c30 = "\"<\"", + peg$c31 = "-", + peg$c32 = "\"-\"", + peg$c33 = ">", + peg$c34 = "\">\"", + peg$c35 = "==", + peg$c36 = "\"==\"", + peg$c37 = "!=", + peg$c38 = "\"!=\"", + peg$c39 = function(left, tail) { + if (!tail) { + return left; + } else { + var operator = tail[1]; + var right = tail[3]; + if (operator === "!=") { + return {type: "not", args: [{type: "equals", args: [left, right]}]}; + } else { + return {type: BINARY[operator], args: [left, right]}; + } + } + }, + peg$c40 = "+", + peg$c41 = "\"+\"", + peg$c42 = "*", + peg$c43 = "\"*\"", + peg$c44 = "/", + peg$c45 = "\"/\"", + peg$c46 = "%", + peg$c47 = "\"%\"", + peg$c48 = "rem", + peg$c49 = "\"rem\"", + peg$c50 = "**", + peg$c51 = "\"**\"", + peg$c52 = "//", + peg$c53 = "\"//\"", + peg$c54 = "%%", + peg$c55 = "\"%%\"", + peg$c56 = "??", + peg$c57 = "\"??\"", + peg$c58 = "!", + peg$c59 = "\"!\"", + peg$c60 = function(operator, arg) { + return {type: UNARY[operator], args: [arg]}; + }, + peg$c61 = function(head, tail) { + for (var i = 0; i < tail.length; i++) { + head = tail[i](head); + } + return head; + }, + peg$c62 = ".", + peg$c63 = "\".\"", + peg$c64 = function(tail) { + return tail; + }, + peg$c65 = "[", + peg$c66 = "\"[\"", + peg$c67 = "]", + peg$c68 = "\"]\"", + peg$c69 = function(arg) { + return function (previous) { + return { + type: "property", + args: [ + previous, + arg + ] + }; + }; + }, + peg$c70 = "{", + peg$c71 = "\"{\"", + peg$c72 = "}", + peg$c73 = "\"}\"", + peg$c74 = function(name, expression) { + if (BLOCKS[name]) { + return function (previous) { + return { + type: BLOCKS[name], + args: [previous, expression] + }; + } + } else if (expression.type === "value") { + return function (previous) { + return { + type: name, + args: [previous] + }; + }; + } else { + return function (previous) { + return { + type: name, + args: [ + {type: "mapBlock", args: [ + previous, + expression + ]} + ] + }; + }; + } + }, + peg$c75 = function(name, args) { + return function (previous) { + return { + type: name, + args: [previous].concat(args) + }; + }; + }, + peg$c76 = function(index) { + return function (previous) { + return { + type: "property", + args: [ + previous, + {type: "literal", value: +index.join("")} + ] + }; + }; + }, + peg$c77 = function(name) { + return function (previous) { + return { + type: "property", + args: [ + previous, + {type: "literal", value: name} + ] + }; + }; + }, + peg$c78 = function(expression) { + return function (previous) { + return { + type: "with", + args: [ + previous, + expression + ] + }; + }; + }, + peg$c79 = "this", + peg$c80 = "\"this\"", + peg$c81 = function() { return {type: "value"}; }, + peg$c82 = "true", + peg$c83 = "\"true\"", + peg$c84 = function() { return {type: "literal", value: true}; }, + peg$c85 = "false", + peg$c86 = "\"false\"", + peg$c87 = function() { return {type: "literal", value: false}; }, + peg$c88 = "null", + peg$c89 = "\"null\"", + peg$c90 = function() { return {type: "literal", value: null}; }, + peg$c91 = "@", + peg$c92 = "\"@\"", + peg$c93 = function(label) { + return {type: "component", label: label}; + }, + peg$c94 = "$", + peg$c95 = "\"$\"", + peg$c96 = function(name) { + return {type: "property", args: [ + {type: "parameters"}, + {type: "literal", value: name} + ]}; + }, + peg$c97 = function() { + return {type: "parameters"}; + }, + peg$c98 = "#", + peg$c99 = "\"#\"", + peg$c100 = function(name) { + return {type: "element", id: name}; + }, + peg$c101 = "&", + peg$c102 = "\"&\"", + peg$c103 = function(name, args) { + return {type: name, args: args, inline: true}; + }, + peg$c104 = "^", + peg$c105 = "\"^\"", + peg$c106 = function(value) { + return {type: "parent", args: [value]}; + }, + peg$c107 = function(expression) { + return expression; + }, + peg$c108 = function(tail) { + return tail({type: "value"}); + }, + peg$c109 = function() { + return {type: "value"}; + }, + peg$c110 = "word", + peg$c111 = /^[a-zA-Z_0-9\-]/, + peg$c112 = "[a-zA-Z_0-9\\-]", + peg$c113 = "string", + peg$c114 = "'", + peg$c115 = "\"'\"", + peg$c116 = function(chars) { return {type: "literal", value: chars.join("")}; }, + peg$c117 = "\"", + peg$c118 = "\"\\\"\"", + peg$c119 = /^[^'\\\0-\x1F]/, + peg$c120 = "[^'\\\\\\0-\\x1F]", + peg$c121 = "\\'", + peg$c122 = "\"\\\\'\"", + peg$c123 = function() { return "'"; }, + peg$c124 = /^[^"\\\0-\x1F]/, + peg$c125 = "[^\"\\\\\\0-\\x1F]", + peg$c126 = "\\\"", + peg$c127 = "\"\\\\\\\"\"", + peg$c128 = function() { return "\""; }, + peg$c129 = "\\\\", + peg$c130 = "\"\\\\\\\\\"", + peg$c131 = function() { return "\\"; }, + peg$c132 = "\\/", + peg$c133 = "\"\\\\/\"", + peg$c134 = function() { return "/"; }, + peg$c135 = "\\b", + peg$c136 = "\"\\\\b\"", + peg$c137 = function() { return "\b"; }, + peg$c138 = "\\f", + peg$c139 = "\"\\\\f\"", + peg$c140 = function() { return "\f"; }, + peg$c141 = "\\n", + peg$c142 = "\"\\\\n\"", + peg$c143 = function() { return "\n"; }, + peg$c144 = "\\r", + peg$c145 = "\"\\\\r\"", + peg$c146 = function() { return "\r"; }, + peg$c147 = "\\t", + peg$c148 = "\"\\\\t\"", + peg$c149 = function() { return "\t"; }, + peg$c150 = "\\0", + peg$c151 = "\"\\\\0\"", + peg$c152 = function() { return "\0"; }, + peg$c153 = "\\u", + peg$c154 = "\"\\\\u\"", + peg$c155 = function(digits) { + return String.fromCharCode(parseInt(digits, 16)); + }, + peg$c156 = /^[0-9a-fA-F]/, + peg$c157 = "[0-9a-fA-F]", + peg$c158 = function() { + return {type: "tuple", args: []}; + }, + peg$c159 = function(expressions) { + return {type: "tuple", args: expressions}; + }, + peg$c160 = function() { return {type: "record", args: []}; }, + peg$c161 = function(pairs) { return {type: "record", args: pairs}; }, + peg$c162 = function(head, tail) { + var result = {}; + result[head[0]] = head[1]; + for (var i = 0; i < tail.length; i++) { + result[tail[i][2][0]] = tail[i][2][1]; + } + return result; + }, + peg$c163 = function(name, value) { return [name, value]; }, + peg$c164 = "number", + peg$c165 = function(parts) { + return {type: "literal", value: +parts} + }, + peg$c166 = /^[eE]/, + peg$c167 = "[eE]", + peg$c168 = /^[+\-]/, + peg$c169 = "[+\\-]", + peg$c170 = /^[0-9]/, + peg$c171 = "[0-9]", + peg$c172 = /^[1-9]/, + peg$c173 = "[1-9]", + peg$c174 = "whitespace", + peg$c175 = /^[\t\x0B\f \xA0\uFEFF]/, + peg$c176 = "[\\t\\x0B\\f \\xA0\\uFEFF]", + peg$c177 = /^[ \xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000]/, + peg$c178 = "[ \\xA0\\u1680\\u180E\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200A\\u202F\\u205F\\u3000]", + peg$c179 = "line terminator", + peg$c180 = /^[\n\r\u2028\u2029]/, + peg$c181 = "[\\n\\r\\u2028\\u2029]", + peg$c182 = "/*", + peg$c183 = "\"/*\"", + peg$c184 = "*/", + peg$c185 = "\"*/\"", + peg$c186 = "any character", + peg$c187 = function(comment) { + return comment; + }, + peg$c188 = function() { + return null; + }, + peg$c189 = function(blocks) { + return {type: "sheet", blocks: blocks}; + }, + peg$c190 = function(label, annotation, statements) { + return { + type: "block", + connection: annotation.connection, + module: annotation.module, + exports: annotation.exports, + label: label, + statements: statements + }; + }, + peg$c191 = function(connection, module, exports) { + return { + connection: {"<": "prototype", ":": "object"}[connection], + module: module && module.value, + exports: exports !== "" ? exports[1] : undefined + }; + }, + peg$c192 = function() { + return {}; + }, + peg$c193 = /^[a-zA-Z_0-9]/, + peg$c194 = "[a-zA-Z_0-9]", + peg$c195 = ";", + peg$c196 = "\";\"", + peg$c197 = function(statement) { + return [statement]; + }, + peg$c198 = "on", + peg$c199 = "\"on\"", + peg$c200 = "before", + peg$c201 = "\"before\"", + peg$c202 = " ", + peg$c203 = "\" \"", + peg$c204 = "->", + peg$c205 = "\"->\"", + peg$c206 = function(when, type, listener) { + return {type: "event", when: when, event: type, listener: listener}; + }, + peg$c207 = "<->", + peg$c208 = "\"<->\"", + peg$c209 = "<-", + peg$c210 = "\"<-\"", + peg$c211 = function(target, arrow, source, descriptor) { + var result = {type: STATEMENTS[arrow], args: [ + target, + source + ]}; + if (descriptor.length) { + var describe = {}; + for (var i = 0; i < descriptor.length; i++) { + describe[descriptor[i][2]] = descriptor[i][6]; + } + result.descriptor = describe; + } + return result; + }, + peg$c212 = function(name, expression) { + return {type: "unit", name: name, value: expression}; + }, + + peg$currPos = 0, + peg$reportedPos = 0, + peg$cachedPos = 0, + peg$cachedPosDetails = { line: 1, column: 1, seenCR: false }, + peg$maxFailPos = 0, + peg$maxFailExpected = [], + peg$silentFails = 0, + + peg$result; + + if ("startRule" in options) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + + function text() { + return input.substring(peg$reportedPos, peg$currPos); + } + + function offset() { + return peg$reportedPos; + } + + function line() { + return peg$computePosDetails(peg$reportedPos).line; + } + + function column() { + return peg$computePosDetails(peg$reportedPos).column; + } + + function peg$computePosDetails(pos) { + function advance(details, startPos, endPos) { + var p, ch; + + for (p = startPos; p < endPos; p++) { + ch = input.charAt(p); + if (ch === "\n") { + if (!details.seenCR) { details.line++; } + details.column = 1; + details.seenCR = false; + } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") { + details.line++; + details.column = 1; + details.seenCR = true; + } else { + details.column++; + details.seenCR = false; + } + } + } + + if (peg$cachedPos !== pos) { + if (peg$cachedPos > pos) { + peg$cachedPos = 0; + peg$cachedPosDetails = { line: 1, column: 1, seenCR: false }; + } + advance(peg$cachedPosDetails, peg$cachedPos, pos); + peg$cachedPos = pos; + } + + return peg$cachedPosDetails; + } + + function peg$fail(expected) { + if (peg$currPos < peg$maxFailPos) { return; } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos; + peg$maxFailExpected = []; + } + + peg$maxFailExpected.push(expected); + } + + function peg$cleanupExpected(expected) { + var i = 0; + + expected.sort(); + + while (i < expected.length) { + if (expected[i - 1] === expected[i]) { + expected.splice(i, 1); + } else { + i++; + } + } + } + + function peg$parseexpression() { + var s0, s1; + + peg$silentFails++; + s0 = peg$parseif(); + peg$silentFails--; + if (s0 === null) { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c0); } + } + + return s0; + } + + function peg$parseexpressions() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parseexpression(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c3; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + s6 = peg$parseexpression(); + if (s6 !== null) { + s4 = [s4, s5, s6]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c3; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + s6 = peg$parseexpression(); + if (s6 !== null) { + s4 = [s4, s5, s6]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + s3 = peg$parse_(); + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c5(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseargs() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c6; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c7); } + } + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 41) { + s3 = peg$c8; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c9); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c10(); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c6; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c7); } + } + if (s1 !== null) { + s2 = peg$parseexpressions(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 41) { + s3 = peg$c8; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c9); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c11(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + + return s0; + } + + function peg$parseif() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10; + + s0 = peg$currPos; + s1 = peg$parseor(); + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 63) { + s4 = peg$c13; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c14); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + s6 = peg$parseexpression(); + if (s6 !== null) { + s7 = peg$parse_(); + if (s7 !== null) { + if (input.charCodeAt(peg$currPos) === 58) { + s8 = peg$c15; + peg$currPos++; + } else { + s8 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s8 !== null) { + s9 = peg$parse_(); + if (s9 !== null) { + s10 = peg$parseexpression(); + if (s10 !== null) { + s4 = [s4, s5, s6, s7, s8, s9, s10]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + if (s3 === null) { + s3 = peg$c12; + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c17(s1,s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseor() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parseand(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + if (input.substr(peg$currPos, 2) === peg$c18) { + s5 = peg$c18; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parseand(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + if (input.substr(peg$currPos, 2) === peg$c18) { + s5 = peg$c18; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parseand(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c20(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseand() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parsecomparison(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + if (input.substr(peg$currPos, 2) === peg$c21) { + s5 = peg$c21; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c22); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsecomparison(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + if (input.substr(peg$currPos, 2) === peg$c21) { + s5 = peg$c21; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c22); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsecomparison(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c20(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsecomparison() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8; + + s0 = peg$currPos; + s1 = peg$parsearithmetic(); + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$parse_(); + if (s3 !== null) { + s4 = peg$currPos; + if (input.substr(peg$currPos, 3) === peg$c23) { + s5 = peg$c23; + peg$currPos += 3; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c24); } + } + if (s5 === null) { + if (input.substr(peg$currPos, 2) === peg$c25) { + s5 = peg$c25; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c26); } + } + if (s5 === null) { + if (input.substr(peg$currPos, 2) === peg$c27) { + s5 = peg$c27; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c28); } + } + if (s5 === null) { + s5 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 60) { + s6 = peg$c29; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c30); } + } + if (s6 !== null) { + s7 = peg$currPos; + peg$silentFails++; + if (input.charCodeAt(peg$currPos) === 45) { + s8 = peg$c31; + peg$currPos++; + } else { + s8 = null; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + peg$silentFails--; + if (s8 === null) { + s7 = peg$c12; + } else { + peg$currPos = s7; + s7 = peg$c1; + } + if (s7 !== null) { + s6 = [s6, s7]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$c1; + } + } else { + peg$currPos = s5; + s5 = peg$c1; + } + if (s5 === null) { + if (input.charCodeAt(peg$currPos) === 62) { + s5 = peg$c33; + peg$currPos++; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c34); } + } + if (s5 === null) { + if (input.substr(peg$currPos, 2) === peg$c35) { + s5 = peg$c35; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c36); } + } + if (s5 === null) { + if (input.substr(peg$currPos, 2) === peg$c37) { + s5 = peg$c37; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c38); } + } + } + } + } + } + } + } + if (s5 !== null) { + s5 = input.substring(s4, peg$currPos); + } + s4 = s5; + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + s6 = peg$parsearithmetic(); + if (s6 !== null) { + s3 = [s3, s4, s5, s6]; + s2 = s3; + } else { + peg$currPos = s2; + s2 = peg$c1; + } + } else { + peg$currPos = s2; + s2 = peg$c1; + } + } else { + peg$currPos = s2; + s2 = peg$c1; + } + } else { + peg$currPos = s2; + s2 = peg$c1; + } + if (s2 === null) { + s2 = peg$c12; + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c39(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsearithmetic() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parsemultiplicative(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 43) { + s6 = peg$c40; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c41); } + } + if (s6 === null) { + if (input.charCodeAt(peg$currPos) === 45) { + s6 = peg$c31; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + } + if (s6 !== null) { + s6 = input.substring(s5, peg$currPos); + } + s5 = s6; + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsemultiplicative(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 43) { + s6 = peg$c40; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c41); } + } + if (s6 === null) { + if (input.charCodeAt(peg$currPos) === 45) { + s6 = peg$c31; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + } + if (s6 !== null) { + s6 = input.substring(s5, peg$currPos); + } + s5 = s6; + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsemultiplicative(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c20(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsemultiplicative() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parseexponential(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 42) { + s6 = peg$c42; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c43); } + } + if (s6 === null) { + if (input.charCodeAt(peg$currPos) === 47) { + s6 = peg$c44; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c45); } + } + if (s6 === null) { + if (input.charCodeAt(peg$currPos) === 37) { + s6 = peg$c46; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c47); } + } + if (s6 === null) { + if (input.substr(peg$currPos, 3) === peg$c48) { + s6 = peg$c48; + peg$currPos += 3; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c49); } + } + } + } + } + if (s6 !== null) { + s6 = input.substring(s5, peg$currPos); + } + s5 = s6; + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parseexponential(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 42) { + s6 = peg$c42; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c43); } + } + if (s6 === null) { + if (input.charCodeAt(peg$currPos) === 47) { + s6 = peg$c44; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c45); } + } + if (s6 === null) { + if (input.charCodeAt(peg$currPos) === 37) { + s6 = peg$c46; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c47); } + } + if (s6 === null) { + if (input.substr(peg$currPos, 3) === peg$c48) { + s6 = peg$c48; + peg$currPos += 3; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c49); } + } + } + } + } + if (s6 !== null) { + s6 = input.substring(s5, peg$currPos); + } + s5 = s6; + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parseexponential(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c20(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseexponential() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parsedefault(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c50) { + s6 = peg$c50; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c51); } + } + if (s6 === null) { + if (input.substr(peg$currPos, 2) === peg$c52) { + s6 = peg$c52; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c53); } + } + if (s6 === null) { + if (input.substr(peg$currPos, 2) === peg$c54) { + s6 = peg$c54; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c55); } + } + } + } + if (s6 !== null) { + s6 = input.substring(s5, peg$currPos); + } + s5 = s6; + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsedefault(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c50) { + s6 = peg$c50; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c51); } + } + if (s6 === null) { + if (input.substr(peg$currPos, 2) === peg$c52) { + s6 = peg$c52; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c53); } + } + if (s6 === null) { + if (input.substr(peg$currPos, 2) === peg$c54) { + s6 = peg$c54; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c55); } + } + } + } + if (s6 !== null) { + s6 = input.substring(s5, peg$currPos); + } + s5 = s6; + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsedefault(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c20(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsedefault() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parseunary(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + if (input.substr(peg$currPos, 2) === peg$c56) { + s5 = peg$c56; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c57); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parseunary(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== null) { + if (input.substr(peg$currPos, 2) === peg$c56) { + s5 = peg$c56; + peg$currPos += 2; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c57); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parseunary(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c20(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseunary() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 33) { + s2 = peg$c58; + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c59); } + } + if (s2 === null) { + if (input.charCodeAt(peg$currPos) === 43) { + s2 = peg$c40; + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c41); } + } + if (s2 === null) { + if (input.charCodeAt(peg$currPos) === 45) { + s2 = peg$c31; + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + } + } + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + s2 = peg$parseunary(); + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c60(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$parsepipe(); + } + + return s0; + } + + function peg$parsepipe() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parsevalue(); + if (s1 !== null) { + s2 = []; + s3 = peg$parsechain(); + while (s3 !== null) { + s2.push(s3); + s3 = peg$parsechain(); + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c61(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsechain() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 46) { + s1 = peg$c62; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c63); } + } + if (s1 !== null) { + s2 = peg$parsetail(); + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c64(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 91) { + s1 = peg$c65; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c66); } + } + if (s1 !== null) { + s2 = peg$parseexpression(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 93) { + s3 = peg$c67; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c68); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c69(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + + return s0; + } + + function peg$parsetail() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parseword(); + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + if (input.charCodeAt(peg$currPos) === 123) { + s2 = peg$c70; + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c71); } + } + if (s2 !== null) { + s3 = peg$parse_(); + if (s3 !== null) { + s4 = peg$parseexpression(); + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + if (input.charCodeAt(peg$currPos) === 125) { + s6 = peg$c72; + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c73); } + } + if (s6 !== null) { + peg$reportedPos = s0; + s1 = peg$c74(s1,s4); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parseword(); + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + s2 = peg$parseargs(); + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c75(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parsedigits(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c76(s1); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parseword(); + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c77(s1); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parsearray(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c78(s1); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parseobject(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c78(s1); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c6; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c7); } + } + if (s1 !== null) { + s2 = peg$parseexpression(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 41) { + s3 = peg$c8; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c9); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c78(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + } + } + } + } + } + + return s0; + } + + function peg$parsevalue() { + var s0, s1, s2, s3; + + s0 = peg$parsearray(); + if (s0 === null) { + s0 = peg$parseobject(); + if (s0 === null) { + s0 = peg$parsestring(); + if (s0 === null) { + s0 = peg$parsenumber(); + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c79) { + s1 = peg$c79; + peg$currPos += 4; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c80); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c81(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c82) { + s1 = peg$c82; + peg$currPos += 4; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c83); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c84(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 5) === peg$c85) { + s1 = peg$c85; + peg$currPos += 5; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c86); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c87(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c88) { + s1 = peg$c88; + peg$currPos += 4; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c89); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c90(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 64) { + s1 = peg$c91; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c92); } + } + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$parselabel(); + if (s3 !== null) { + s3 = input.substring(s2, peg$currPos); + } + s2 = s3; + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c93(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 36) { + s1 = peg$c94; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c95); } + } + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$parseword(); + if (s3 !== null) { + s3 = input.substring(s2, peg$currPos); + } + s2 = s3; + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c96(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 36) { + s1 = peg$c94; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c95); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c97(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 35) { + s1 = peg$c98; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c99); } + } + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$parseword(); + if (s3 !== null) { + s3 = input.substring(s2, peg$currPos); + } + s2 = s3; + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c100(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 38) { + s1 = peg$c101; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c102); } + } + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$parseword(); + if (s3 !== null) { + s3 = input.substring(s2, peg$currPos); + } + s2 = s3; + if (s2 !== null) { + s3 = peg$parseargs(); + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c103(s2,s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 94) { + s1 = peg$c104; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c105); } + } + if (s1 !== null) { + s2 = peg$parsevalue(); + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c106(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c6; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c7); } + } + if (s1 !== null) { + s2 = peg$parseexpression(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 41) { + s3 = peg$c8; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c9); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c107(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parsetail(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c108(s1); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = []; + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c109(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + + return s0; + } + + function peg$parseword() { + var s0, s1; + + peg$silentFails++; + s0 = []; + if (peg$c111.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c112); } + } + if (s1 !== null) { + while (s1 !== null) { + s0.push(s1); + if (peg$c111.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c112); } + } + } + } else { + s0 = peg$c1; + } + peg$silentFails--; + if (s0 === null) { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c110); } + } + + return s0; + } + + function peg$parsestring() { + var s0, s1, s2, s3; + + peg$silentFails++; + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 39) { + s1 = peg$c114; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c115); } + } + if (s1 !== null) { + s2 = []; + s3 = peg$parsetickedChar(); + while (s3 !== null) { + s2.push(s3); + s3 = peg$parsetickedChar(); + } + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 39) { + s3 = peg$c114; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c115); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c116(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 34) { + s1 = peg$c117; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c118); } + } + if (s1 !== null) { + s2 = []; + s3 = peg$parsequotedChar(); + while (s3 !== null) { + s2.push(s3); + s3 = peg$parsequotedChar(); + } + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 34) { + s3 = peg$c117; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c118); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c116(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + peg$silentFails--; + if (s0 === null) { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c113); } + } + + return s0; + } + + function peg$parsetickedChar() { + var s0, s1; + + if (peg$c119.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c120); } + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c121) { + s1 = peg$c121; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c122); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c123(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$parseescape(); + } + } + + return s0; + } + + function peg$parsequotedChar() { + var s0, s1; + + if (peg$c124.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c125); } + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c126) { + s1 = peg$c126; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c127); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c128(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$parseescape(); + } + } + + return s0; + } + + function peg$parseescape() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c129) { + s1 = peg$c129; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c130); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c131(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c132) { + s1 = peg$c132; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c133); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c134(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c135) { + s1 = peg$c135; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c136); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c137(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c138) { + s1 = peg$c138; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c139); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c140(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c141) { + s1 = peg$c141; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c142); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c143(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c144) { + s1 = peg$c144; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c145); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c146(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c147) { + s1 = peg$c147; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c148); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c149(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c150) { + s1 = peg$c150; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c151); } + } + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c152(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c153) { + s1 = peg$c153; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c154); } + } + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$currPos; + s4 = peg$parsehexDigit(); + if (s4 !== null) { + s5 = peg$parsehexDigit(); + if (s5 !== null) { + s6 = peg$parsehexDigit(); + if (s6 !== null) { + s7 = peg$parsehexDigit(); + if (s7 !== null) { + s4 = [s4, s5, s6, s7]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + if (s3 !== null) { + s3 = input.substring(s2, peg$currPos); + } + s2 = s3; + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c155(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + } + } + } + } + } + } + } + + return s0; + } + + function peg$parsehexDigit() { + var s0; + + if (peg$c156.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c157); } + } + + return s0; + } + + function peg$parsearray() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 91) { + s1 = peg$c65; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c66); } + } + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 93) { + s3 = peg$c67; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c68); } + } + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c158(); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 91) { + s1 = peg$c65; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c66); } + } + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + s3 = peg$parseexpressions(); + if (s3 !== null) { + s4 = peg$parse_(); + if (s4 !== null) { + if (input.charCodeAt(peg$currPos) === 93) { + s5 = peg$c67; + peg$currPos++; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c68); } + } + if (s5 !== null) { + peg$reportedPos = s0; + s1 = peg$c159(s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + + return s0; + } + + function peg$parseobject() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 123) { + s1 = peg$c70; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c71); } + } + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 125) { + s3 = peg$c72; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c73); } + } + if (s3 !== null) { + s4 = peg$parse_(); + if (s4 !== null) { + peg$reportedPos = s0; + s1 = peg$c160(); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 123) { + s1 = peg$c70; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c71); } + } + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + s3 = peg$parsepairs(); + if (s3 !== null) { + if (input.charCodeAt(peg$currPos) === 125) { + s4 = peg$c72; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c73); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + peg$reportedPos = s0; + s1 = peg$c161(s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + + return s0; + } + + function peg$parsepairs() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parsepair(); + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c3; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + s6 = peg$parsepair(); + if (s6 !== null) { + s4 = [s4, s5, s6]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c3; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + s6 = peg$parsepair(); + if (s6 !== null) { + s4 = [s4, s5, s6]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + peg$reportedPos = s0; + s1 = peg$c162(s1,s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsepair() { + var s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parseword(); + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + if (input.charCodeAt(peg$currPos) === 58) { + s2 = peg$c15; + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s2 !== null) { + s3 = peg$parse_(); + if (s3 !== null) { + s4 = peg$parseexpression(); + if (s4 !== null) { + peg$reportedPos = s0; + s1 = peg$c163(s1,s4); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsenumber() { + var s0, s1, s2; + + peg$silentFails++; + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parsenumberPattern(); + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c165(s1); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + peg$silentFails--; + if (s0 === null) { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c164); } + } + + return s0; + } + + function peg$parsenumberPattern() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parseint(); + if (s1 !== null) { + s2 = peg$parsefrac(); + if (s2 !== null) { + s3 = peg$parseexp(); + if (s3 !== null) { + s1 = [s1, s2, s3]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parseint(); + if (s1 !== null) { + s2 = peg$parsefrac(); + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parseint(); + if (s1 !== null) { + s2 = peg$parseexp(); + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$parseint(); + } + } + } + + return s0; + } + + function peg$parseint() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parsedigit19(); + if (s1 !== null) { + s2 = peg$parsedigits(); + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$parsedigit(); + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 45) { + s1 = peg$c31; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + if (s1 !== null) { + s2 = peg$parsedigit19(); + if (s2 !== null) { + s3 = peg$parsedigits(); + if (s3 !== null) { + s1 = [s1, s2, s3]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 45) { + s1 = peg$c31; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + if (s1 !== null) { + s2 = peg$parsedigit(); + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + } + } + + return s0; + } + + function peg$parsefrac() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 46) { + s1 = peg$c62; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c63); } + } + if (s1 !== null) { + s2 = peg$parsedigits(); + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseexp() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$parsee(); + if (s1 !== null) { + s2 = peg$parsedigits(); + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsedigits() { + var s0, s1; + + s0 = []; + s1 = peg$parsedigit(); + if (s1 !== null) { + while (s1 !== null) { + s0.push(s1); + s1 = peg$parsedigit(); + } + } else { + s0 = peg$c1; + } + + return s0; + } + + function peg$parsee() { + var s0, s1, s2; + + s0 = peg$currPos; + if (peg$c166.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c167); } + } + if (s1 !== null) { + if (peg$c168.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c169); } + } + if (s2 === null) { + s2 = peg$c12; + } + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsedigit() { + var s0; + + if (peg$c170.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c171); } + } + + return s0; + } + + function peg$parsedigit19() { + var s0; + + if (peg$c172.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c173); } + } + + return s0; + } + + function peg$parse_() { + var s0, s1; + + s0 = []; + s1 = peg$parsewhiteSpace(); + if (s1 === null) { + s1 = peg$parselineTerminator(); + } + while (s1 !== null) { + s0.push(s1); + s1 = peg$parsewhiteSpace(); + if (s1 === null) { + s1 = peg$parselineTerminator(); + } + } + + return s0; + } + + function peg$parsewhiteSpace() { + var s0, s1; + + peg$silentFails++; + if (peg$c175.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c176); } + } + if (s0 === null) { + if (peg$c177.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c178); } + } + } + peg$silentFails--; + if (s0 === null) { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c174); } + } + + return s0; + } + + function peg$parselineTerminator() { + var s0, s1; + + peg$silentFails++; + if (peg$c180.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = null; + if (peg$silentFails === 0) { peg$fail(peg$c181); } + } + peg$silentFails--; + if (s0 === null) { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c179); } + } + + return s0; + } + + function peg$parsecomment() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== null) { + if (input.substr(peg$currPos, 2) === peg$c182) { + s2 = peg$c182; + peg$currPos += 2; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c183); } + } + if (s2 !== null) { + s3 = peg$currPos; + s4 = []; + s5 = peg$currPos; + s6 = peg$currPos; + peg$silentFails++; + if (input.substr(peg$currPos, 2) === peg$c184) { + s7 = peg$c184; + peg$currPos += 2; + } else { + s7 = null; + if (peg$silentFails === 0) { peg$fail(peg$c185); } + } + peg$silentFails--; + if (s7 === null) { + s6 = peg$c12; + } else { + peg$currPos = s6; + s6 = peg$c1; + } + if (s6 !== null) { + if (input.length > peg$currPos) { + s7 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s7 = null; + if (peg$silentFails === 0) { peg$fail(peg$c186); } + } + if (s7 !== null) { + s6 = [s6, s7]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$c1; + } + } else { + peg$currPos = s5; + s5 = peg$c1; + } + while (s5 !== null) { + s4.push(s5); + s5 = peg$currPos; + s6 = peg$currPos; + peg$silentFails++; + if (input.substr(peg$currPos, 2) === peg$c184) { + s7 = peg$c184; + peg$currPos += 2; + } else { + s7 = null; + if (peg$silentFails === 0) { peg$fail(peg$c185); } + } + peg$silentFails--; + if (s7 === null) { + s6 = peg$c12; + } else { + peg$currPos = s6; + s6 = peg$c1; + } + if (s6 !== null) { + if (input.length > peg$currPos) { + s7 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s7 = null; + if (peg$silentFails === 0) { peg$fail(peg$c186); } + } + if (s7 !== null) { + s6 = [s6, s7]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$c1; + } + } else { + peg$currPos = s5; + s5 = peg$c1; + } + } + if (s4 !== null) { + s4 = input.substring(s3, peg$currPos); + } + s3 = s4; + if (s3 !== null) { + if (input.substr(peg$currPos, 2) === peg$c184) { + s4 = peg$c184; + peg$currPos += 2; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c185); } + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + peg$reportedPos = s0; + s1 = peg$c187(s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c188(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } + + return s0; + } + + function peg$parsesheet() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== null) { + s2 = []; + s3 = peg$parseblock(); + while (s3 !== null) { + s2.push(s3); + s3 = peg$parseblock(); + } + if (s2 !== null) { + s3 = peg$parse_(); + if (s3 !== null) { + peg$reportedPos = s0; + s1 = peg$c189(s2); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseblock() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 64) { + s1 = peg$c91; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c92); } + } + if (s1 !== null) { + s2 = peg$currPos; + s3 = peg$parselabel(); + if (s3 !== null) { + s3 = input.substring(s2, peg$currPos); + } + s2 = s3; + if (s2 !== null) { + s3 = peg$parse_(); + if (s3 !== null) { + s4 = peg$parseannotation(); + if (s4 === null) { + s4 = peg$c12; + } + if (s4 !== null) { + if (input.charCodeAt(peg$currPos) === 123) { + s5 = peg$c70; + peg$currPos++; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c71); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsestatements(); + if (s7 !== null) { + if (input.charCodeAt(peg$currPos) === 125) { + s8 = peg$c72; + peg$currPos++; + } else { + s8 = null; + if (peg$silentFails === 0) { peg$fail(peg$c73); } + } + if (s8 !== null) { + s9 = peg$parse_(); + if (s9 !== null) { + peg$reportedPos = s0; + s1 = peg$c190(s2,s4,s7); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parseannotation() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 60) { + s1 = peg$c29; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c30); } + } + if (s1 === null) { + if (input.charCodeAt(peg$currPos) === 58) { + s1 = peg$c15; + peg$currPos++; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + } + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + s3 = peg$parsestring(); + if (s3 === null) { + s3 = peg$c12; + } + if (s3 !== null) { + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$currPos; + s6 = peg$currPos; + peg$silentFails++; + if (input.charCodeAt(peg$currPos) === 123) { + s7 = peg$c70; + peg$currPos++; + } else { + s7 = null; + if (peg$silentFails === 0) { peg$fail(peg$c71); } + } + peg$silentFails--; + if (s7 === null) { + s6 = peg$c12; + } else { + peg$currPos = s6; + s6 = peg$c1; + } + if (s6 !== null) { + s7 = peg$parseexpression(); + if (s7 !== null) { + s6 = [s6, s7]; + s5 = s6; + } else { + peg$currPos = s5; + s5 = peg$c1; + } + } else { + peg$currPos = s5; + s5 = peg$c1; + } + if (s5 === null) { + s5 = peg$c12; + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + peg$reportedPos = s0; + s1 = peg$c191(s1,s3,s5); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c192(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } + + return s0; + } + + function peg$parselabel() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = []; + if (peg$c193.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c194); } + } + if (s2 !== null) { + while (s2 !== null) { + s1.push(s2); + if (peg$c193.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c194); } + } + } + } else { + s1 = peg$c1; + } + if (s1 !== null) { + s2 = []; + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 58) { + s4 = peg$c15; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s4 !== null) { + s5 = []; + if (peg$c193.test(input.charAt(peg$currPos))) { + s6 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c194); } + } + if (s6 !== null) { + while (s6 !== null) { + s5.push(s6); + if (peg$c193.test(input.charAt(peg$currPos))) { + s6 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c194); } + } + } + } else { + s5 = peg$c1; + } + if (s5 !== null) { + s4 = [s4, s5]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + while (s3 !== null) { + s2.push(s3); + s3 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 58) { + s4 = peg$c15; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s4 !== null) { + s5 = []; + if (peg$c193.test(input.charAt(peg$currPos))) { + s6 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c194); } + } + if (s6 !== null) { + while (s6 !== null) { + s5.push(s6); + if (peg$c193.test(input.charAt(peg$currPos))) { + s6 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c194); } + } + } + } else { + s5 = peg$c1; + } + if (s5 !== null) { + s4 = [s4, s5]; + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } else { + peg$currPos = s3; + s3 = peg$c1; + } + } + if (s2 !== null) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + + return s0; + } + + function peg$parsestatements() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8; + + s0 = peg$currPos; + s1 = peg$parsestatement(); + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + s3 = []; + s4 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 59) { + s5 = peg$c195; + peg$currPos++; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c196); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsestatement(); + if (s7 !== null) { + s8 = peg$parse_(); + if (s8 !== null) { + s5 = [s5, s6, s7, s8]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } else { + peg$currPos = s4; + s4 = peg$c1; + } + while (s4 !== null) { + s3.push(s4); + s4 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 59) { + s5 = peg$c195; + peg$currPos++; + } else { + s5 = null; + if (peg$silentFails === 0) { peg$fail(peg$c196); } + } + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = peg$parsestatement(); + if (s7 !== null) { + s8 = peg$parse_(); + if (s8 !== null) { + s5 = [s5, s6, s7, s8]; + s4 = s5; + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } else { + peg$currPos = s4; + s4 = peg$c1; + } + } + if (s3 !== null) { + if (input.charCodeAt(peg$currPos) === 59) { + s4 = peg$c195; + peg$currPos++; + } else { + s4 = null; + if (peg$silentFails === 0) { peg$fail(peg$c196); } + } + if (s4 === null) { + s4 = peg$c12; + } + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + peg$reportedPos = s0; + s1 = peg$c5(s1,s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parsestatement(); + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 59) { + s3 = peg$c195; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c196); } + } + if (s3 === null) { + s3 = peg$c12; + } + if (s3 !== null) { + s4 = peg$parse_(); + if (s4 !== null) { + peg$reportedPos = s0; + s1 = peg$c197(s1); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== null) { + peg$reportedPos = s0; + s1 = peg$c10(); + } + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } + } + + return s0; + } + + function peg$parsestatement() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12, s13, s14, s15, s16; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c198) { + s1 = peg$c198; + peg$currPos += 2; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c199); } + } + if (s1 === null) { + if (input.substr(peg$currPos, 6) === peg$c200) { + s1 = peg$c200; + peg$currPos += 6; + } else { + s1 = null; + if (peg$silentFails === 0) { peg$fail(peg$c201); } + } + } + if (s1 !== null) { + if (input.charCodeAt(peg$currPos) === 32) { + s2 = peg$c202; + peg$currPos++; + } else { + s2 = null; + if (peg$silentFails === 0) { peg$fail(peg$c203); } + } + if (s2 !== null) { + s3 = peg$parse_(); + if (s3 !== null) { + s4 = peg$currPos; + s5 = peg$parseword(); + if (s5 !== null) { + s5 = input.substring(s4, peg$currPos); + } + s4 = s5; + if (s4 !== null) { + s5 = peg$parse_(); + if (s5 !== null) { + if (input.substr(peg$currPos, 2) === peg$c204) { + s6 = peg$c204; + peg$currPos += 2; + } else { + s6 = null; + if (peg$silentFails === 0) { peg$fail(peg$c205); } + } + if (s6 !== null) { + s7 = peg$parse_(); + if (s7 !== null) { + s8 = peg$parseexpression(); + if (s8 !== null) { + s9 = peg$parse_(); + if (s9 !== null) { + peg$reportedPos = s0; + s1 = peg$c206(s1,s4,s8); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$parseexpression(); + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c15; + peg$currPos++; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s3 === null) { + if (input.substr(peg$currPos, 3) === peg$c207) { + s3 = peg$c207; + peg$currPos += 3; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c208); } + } + if (s3 === null) { + if (input.substr(peg$currPos, 2) === peg$c209) { + s3 = peg$c209; + peg$currPos += 2; + } else { + s3 = null; + if (peg$silentFails === 0) { peg$fail(peg$c210); } + } + } + } + if (s3 !== null) { + s4 = peg$parse_(); + if (s4 !== null) { + s5 = peg$parseexpression(); + if (s5 !== null) { + s6 = peg$parse_(); + if (s6 !== null) { + s7 = []; + s8 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s9 = peg$c3; + peg$currPos++; + } else { + s9 = null; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s9 !== null) { + s10 = peg$parse_(); + if (s10 !== null) { + s11 = peg$currPos; + s12 = peg$parseword(); + if (s12 !== null) { + s12 = input.substring(s11, peg$currPos); + } + s11 = s12; + if (s11 !== null) { + s12 = peg$parse_(); + if (s12 !== null) { + if (input.charCodeAt(peg$currPos) === 58) { + s13 = peg$c15; + peg$currPos++; + } else { + s13 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s13 !== null) { + s14 = peg$parse_(); + if (s14 !== null) { + s15 = peg$parseexpression(); + if (s15 !== null) { + s16 = peg$parse_(); + if (s16 !== null) { + s9 = [s9, s10, s11, s12, s13, s14, s15, s16]; + s8 = s9; + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + while (s8 !== null) { + s7.push(s8); + s8 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 44) { + s9 = peg$c3; + peg$currPos++; + } else { + s9 = null; + if (peg$silentFails === 0) { peg$fail(peg$c4); } + } + if (s9 !== null) { + s10 = peg$parse_(); + if (s10 !== null) { + s11 = peg$currPos; + s12 = peg$parseword(); + if (s12 !== null) { + s12 = input.substring(s11, peg$currPos); + } + s11 = s12; + if (s11 !== null) { + s12 = peg$parse_(); + if (s12 !== null) { + if (input.charCodeAt(peg$currPos) === 58) { + s13 = peg$c15; + peg$currPos++; + } else { + s13 = null; + if (peg$silentFails === 0) { peg$fail(peg$c16); } + } + if (s13 !== null) { + s14 = peg$parse_(); + if (s14 !== null) { + s15 = peg$parseexpression(); + if (s15 !== null) { + s16 = peg$parse_(); + if (s16 !== null) { + s9 = [s9, s10, s11, s12, s13, s14, s15, s16]; + s8 = s9; + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } else { + peg$currPos = s8; + s8 = peg$c1; + } + } + if (s7 !== null) { + peg$reportedPos = s0; + s1 = peg$c211(s1,s3,s5,s7); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + if (s0 === null) { + s0 = peg$currPos; + s1 = peg$currPos; + s2 = peg$parseword(); + if (s2 !== null) { + s2 = input.substring(s1, peg$currPos); + } + s1 = s2; + if (s1 !== null) { + s2 = peg$parse_(); + if (s2 !== null) { + s3 = peg$parseexpression(); + if (s3 !== null) { + s4 = peg$parse_(); + if (s4 !== null) { + peg$reportedPos = s0; + s1 = peg$c212(s1,s3); + if (s1 === null) { + peg$currPos = s0; + s0 = s1; + } else { + s0 = s1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } else { + peg$currPos = s0; + s0 = peg$c1; + } + } + } + + return s0; + } + + + var BINARY = { + "**": "pow", + "//": "root", + "%%": "log", + "*": "mul", + "/": "div", + "%": "mod", + "rem": "rem", + "+": "add", + "-": "sub", + "<": "lt", + ">": "gt", + "<=": "le", + ">=": "ge", + "==": "equals", + "<=>": "compare", + "??": "default", + "&&": "and", + "||": "or", + "<-": "bind", + "<->": "bind2", + ":": "assign" + }; + + var UNARY = { + "+": "toNumber", + "-": "neg", + "!": "not", + "^": "parent" + }; + + var BLOCKS = { + "map": "mapBlock", + "filter": "filterBlock", + "some": "someBlock", + "every": "everyBlock", + "sorted": "sortedBlock", + "sortedSet": "sortedSetBlock", + "group": "groupBlock", + "groupMap": "groupMapBlock", + "min": "minBlock", + "max": "maxBlock" + }; + + var STATEMENTS = { + ":": "assign", + "<-": "bind", + "<->": "bind2" + }; + + + + peg$result = peg$startRuleFunction(); + + if (peg$result !== null && peg$currPos === input.length) { + return peg$result; + } else { + peg$cleanupExpected(peg$maxFailExpected); + peg$reportedPos = Math.max(peg$currPos, peg$maxFailPos); + + throw new SyntaxError( + peg$maxFailExpected, + peg$reportedPos < input.length ? input.charAt(peg$reportedPos) : null, + peg$reportedPos, + peg$computePosDetails(peg$reportedPos).line, + peg$computePosDetails(peg$reportedPos).column + ); + } + } + + return { + SyntaxError: SyntaxError, + parse : parse + }; +})(); diff --git a/core/frb/grammar.pegjs b/core/frb/grammar.pegjs new file mode 100644 index 0000000000..3d8f72d1cf --- /dev/null +++ b/core/frb/grammar.pegjs @@ -0,0 +1,540 @@ + +{ + var BINARY = { + "**": "pow", + "//": "root", + "%%": "log", + "*": "mul", + "/": "div", + "%": "mod", + "rem": "rem", + "+": "add", + "-": "sub", + "<": "lt", + ">": "gt", + "<=": "le", + ">=": "ge", + "==": "equals", + "<=>": "compare", + "??": "default", + "&&": "and", + "||": "or", + "<-": "bind", + "<->": "bind2", + ":": "assign" + }; + + var UNARY = { + "+": "toNumber", + "-": "neg", + "!": "not", + "^": "parent" + }; + + var BLOCKS = { + "map": "mapBlock", + "filter": "filterBlock", + "some": "someBlock", + "every": "everyBlock", + "sorted": "sortedBlock", + "sortedSet": "sortedSetBlock", + "group": "groupBlock", + "groupMap": "groupMapBlock", + "min": "minBlock", + "max": "maxBlock" + }; + + var STATEMENTS = { + ":": "assign", + "<-": "bind", + "<->": "bind2" + }; + +} + +expression "expression" = if + +expressions + = head:expression tail:("," _ expression)* _ { + var result = [head]; + for (var i = 0; i < tail.length; i++) { + result.push(tail[i][2]); + } + return result; + } + +args + = "(" _ ")" { + return []; + } + / "(" expressions:expressions ")" { + return expressions; + } + +if + = condition:or _ tail:( "?" _ conequent:expression _ ":" _ alternate:expression )? { + if (tail) { + var consequent = tail[2]; + var alternate = tail[6]; + return { + type: "if", + args: [condition, consequent, alternate] + }; + } else { + return condition; + } + } + +or + = head:and tail:( _ "||" _ and)* { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + } + +and + = head:comparison tail:( _ "&&" _ comparison)* { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + } + +comparison + = left:arithmetic tail:( _ operator:$( "<=>" / "<=" / ">=" / "<" !("-") / ">" / "==" / "!=" ) _ right:arithmetic )? { + if (!tail) { + return left; + } else { + var operator = tail[1]; + var right = tail[3]; + if (operator === "!=") { + return {type: "not", args: [{type: "equals", args: [left, right]}]}; + } else { + return {type: BINARY[operator], args: [left, right]}; + } + } + } + +arithmetic + = head:multiplicative tail:( _ $( "+" / "-" ) _ multiplicative)* { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + } + +multiplicative + = head:exponential tail:( _ $( "*" / "/" / "%" / "rem" ) _ exponential)* { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + } + +exponential + = head:default tail:( _ $( "**" / "//" / "%%" ) _ default)* { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + } + +default + = head:unary tail:( _ "??" _ unary)* { + for (var i = 0; i < tail.length; i++) { + head = { + type: BINARY[tail[i][1]], + args: [ + head, + tail[i][3] + ] + } + } + return head; + } + +unary + = operator:$("!" / "+" / "-") arg:unary { + return {type: UNARY[operator], args: [arg]}; + } + / pipe + +pipe + = head:value tail:chain* { + for (var i = 0; i < tail.length; i++) { + head = tail[i](head); + } + return head; + } + +chain + = "." tail:tail { + return tail; + } + / "[" arg:expression "]" { + return function (previous) { + return { + type: "property", + args: [ + previous, + arg + ] + }; + }; + } + +tail + = name:$(word) "{" _ expression:expression _ "}" { + if (BLOCKS[name]) { + return function (previous) { + return { + type: BLOCKS[name], + args: [previous, expression] + }; + } + } else if (expression.type === "value") { + return function (previous) { + return { + type: name, + args: [previous] + }; + }; + } else { + return function (previous) { + return { + type: name, + args: [ + {type: "mapBlock", args: [ + previous, + expression + ]} + ] + }; + }; + } + } + / name:$(word) args:args { + return function (previous) { + return { + type: name, + args: [previous].concat(args) + }; + }; + } + / index:digits { + return function (previous) { + return { + type: "property", + args: [ + previous, + {type: "literal", value: +index.join("")} + ] + }; + }; + } + / name:$(word) { + return function (previous) { + return { + type: "property", + args: [ + previous, + {type: "literal", value: name} + ] + }; + }; + } + / expression:array { + return function (previous) { + return { + type: "with", + args: [ + previous, + expression + ] + }; + }; + } + / expression:object { + return function (previous) { + return { + type: "with", + args: [ + previous, + expression + ] + }; + }; + } + / "(" expression:expression ")" { + return function (previous) { + return { + type: "with", + args: [ + previous, + expression + ] + }; + }; + } + +value + = array + / object + / string + / number + / "this" { return {type: "value"}; } + / "true" { return {type: "literal", value: true}; } + / "false" { return {type: "literal", value: false}; } + / "null" { return {type: "literal", value: null}; } + / "@" label:$(label) { + return {type: "component", label: label}; + } + / "$" name:$(word) { + return {type: "property", args: [ + {type: "parameters"}, + {type: "literal", value: name} + ]}; + } + / "$" { + return {type: "parameters"}; + } + / "#" name:$(word) { + return {type: "element", id: name}; + } + / "&" name:$(word) args:args { + return {type: name, args: args, inline: true}; + } + / "^" value:value { + return {type: "parent", args: [value]}; + } + / "(" expression:expression ")" { + return expression; + } + / tail:tail { + return tail({type: "value"}); + } + / { + return {type: "value"}; + } + +word "word" + = [a-zA-Z_0-9-]+ + +string "string" + = "'" chars:tickedChar* "'" { return {type: "literal", value: chars.join("")}; } + / '"' chars:quotedChar* '"' { return {type: "literal", value: chars.join("")}; } + +tickedChar + = [^'\\\0-\x1F\x7f] + / "\\'" { return "'"; } + / escape + +quotedChar + = [^"\\\0-\x1F\x7f] + / "\\\"" { return "\""; } + / escape + +escape + = "\\\\" { return "\\"; } + / "\\/" { return "/"; } + / "\\b" { return "\b"; } + / "\\f" { return "\f"; } + / "\\n" { return "\n"; } + / "\\r" { return "\r"; } + / "\\t" { return "\t"; } + / "\\0" { return "\0"; } + / "\\u" digits:$(hexDigit hexDigit hexDigit hexDigit) { + return String.fromCharCode(parseInt(digits, 16)); + } + +hexDigit + = [0-9a-fA-F] + +array + = "[" _ "]" { + return {type: "tuple", args: []}; + } + / "[" _ expressions:expressions _ "]" { + return {type: "tuple", args: expressions}; + } + +object + = "{" _ "}" _ { return {type: "record", args: []}; } + / "{" _ pairs:pairs "}" _ { return {type: "record", args: pairs}; } + +pairs + = head:pair tail:( "," _ pair )* { + var result = {}; + result[head[0]] = head[1]; + for (var i = 0; i < tail.length; i++) { + result[tail[i][2][0]] = tail[i][2][1]; + } + return result; + } + +pair + = name:$(word) ":" _ value:expression { return [name, value]; } + + +// literals closely modeled after the JSON PEGJS example + +number "number" + = parts:$(numberPattern) { + return {type: "literal", value: +parts} + } + +numberPattern + = int frac exp + / int frac + / int exp + / int + +int + = digit19 digits + / digit + / "-" digit19 digits + / "-" digit + +frac + = "." digits + +exp + = e digits + +digits + = digit+ + +e + = [eE] [+-]? + +digit + = [0-9] + +digit19 + = [1-9] + +// white space and comments defined as in the JavaScript PEGJS example + +_ + = ( whiteSpace / lineTerminator )* + +whiteSpace "whitespace" + = [\t\v\f \u00A0\uFEFF] + / [\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000] + +lineTerminator "line terminator" + = [\n\r\u2028\u2029] + +comment + = _ "/*" comment:$(!"*/" . )* "*/" _ { + return comment; + } + / _ { + return null; + } + +// MCS extensions + +sheet + = _ blocks:block* _ { + return {type: "sheet", blocks: blocks}; + } + +block + = "@" label:$(label) _ annotation:annotation? "{" _ statements:statements "}" _ { + return { + type: "block", + connection: annotation.connection, + module: annotation.module, + exports: annotation.exports, + label: label, + statements: statements + }; + } + +annotation + = connection:("<" / ":") _ module:string? _ exports:( !"{" expression )? _ { + return { + connection: {"<": "prototype", ":": "object"}[connection], + module: module && module.value, + exports: exports !== "" ? exports[1] : undefined + }; + } + / _ { + return {}; + } + +label + = [a-zA-Z_0-9]+ ( ":" [a-zA-Z_0-9]+ )* + +statements + = head:statement _ tail:(";" _ statement _)* ";"? _ { + var result = [head]; + for (var i = 0; i < tail.length; i++) { + result.push(tail[i][2]); + } + return result; + } + / statement:statement _ ";"? _ { + return [statement]; + } + / _ { + return []; + } + +statement + = when:("on" / "before") " " _ type:$(word) _ "->" _ listener:expression _ { + return {type: "event", when: when, event: type, listener: listener}; + } + / target:expression _ arrow:(":" / "<->" / "<-") _ source:expression _ + descriptor:("," _ name:$(word) _ ":" _ expression:expression _)* + { + var result = {type: STATEMENTS[arrow], args: [ + target, + source + ]}; + if (descriptor.length) { + var describe = {}; + for (var i = 0; i < descriptor.length; i++) { + describe[descriptor[i][2]] = descriptor[i][6]; + } + result.descriptor = describe; + } + return result; + } + / name:$(word) _ expression:expression _ { + return {type: "unit", name: name, value: expression}; + } + + diff --git a/core/frb/grammar.xhtml b/core/frb/grammar.xhtml new file mode 100644 index 0000000000..7d960b179b --- /dev/null +++ b/core/frb/grammar.xhtml @@ -0,0 +1,510 @@ +Pipeline:PropertyName#ElementId@ComponentLabelArrayExpressionObjectExpression(Expression).Transform[Expression].*[*]Pipeline ::= ( ( PropertyName | '#' ElementId | '@' ComponentLabel | ArrayExpression | ObjectExpression | '(' Expression ')' ) ( '.' Transform | '[' Expression ']' )* ( '.*' | '[*]' | ) )?referenced by:UnaryExpressionTransform:PropertyName(Expression)ArrayExpressionObjectExpressionflattenreversedenumeratesumaverage(){Expression}mapfiltersomeeverysorted(Expression){Expression)has(Expression)view(Expression,Expression)Transform + ::= PropertyName + | ( '(' Expression ')' | ArrayExpression | ObjectExpression ) + | ( 'flatten' | 'reversed' | 'enumerate' | 'sum' | 'average' ) ( '()' | '{' Expression '}' ) + | ( 'map' | 'filter' | 'some' | 'every' | 'sorted' ) ( '(' Expression ')' | '{' Expression ')' ) + | 'has(' Expression ')' + | 'view(' Expression ', ' Expression ')'referenced by:PipelineArrayExpression:[Expression,]ArrayExpression + ::= '[' ( Expression ( ', ' Expression )* )? ']'referenced by:PipelineTransformObjectExpression:{PropertyName:Expression,}ObjectExpression + ::= '{' ( PropertyName ': ' Expression ( ', ' PropertyName ': ' Expression )* )? '}'referenced by:PipelineTransformUnaryExpression:+-!PipelineUnaryExpression + ::= ( '+' | '-' | '!' )? Pipelinereferenced by:ExponentialExpressionExponentialExpression:UnaryExpression**//%%ExponentialExpression + ::= UnaryExpression ( ( '**' | '//' | '%%' ) UnaryExpression )*referenced by:MultiplicativeExpressionMultiplicativeExpression:ExponentialExpression*/%remMultiplicativeExpression + ::= ExponentialExpression ( ( '*' | '/' | '%' | 'rem' ) ExponentialExpression )*referenced by:ArithmeticExpressionArithmeticExpression:MultiplicativeExpression+-ArithmeticExpression + ::= MultiplicativeExpression ( ( '+' | '-' ) MultiplicativeExpression )*referenced by:RelationalExpressionRelationalExpression:ArithmeticExpression==<<=>>=ArithmeticExpressionRelationalExpression + ::= ArithmeticExpression ( ( '==' | '<' | '<=' | '>' | '>=' ) ArithmeticExpression )?referenced by:LogicalIntersectionExpressionLogicalIntersectionExpression:RelationalExpression&&LogicalIntersectionExpressionLogicalIntersectionExpression + ::= RelationalExpression ( '&&' LogicalIntersectionExpression )?referenced by:LogicalIntersectionExpressionLogicalUnionExpressionLogicalUnionExpression:LogicalIntersectionExpression||LogicalUnionExpressionLogicalUnionExpression + ::= LogicalIntersectionExpression ( '||' LogicalUnionExpression )?referenced by:ConditionalExpressionLogicalUnionExpressionConditionalExpression:LogicalUnionExpression?Expression:ExpressionConditionalExpression + ::= LogicalUnionExpression ( '?' Expression ':' Expression )?referenced by:ExpressionExpression:ConditionalExpressionExpression + ::= ConditionalExpressionreferenced by:ArrayExpressionConditionalExpressionObjectExpressionPipelineTransformLiteral:NumberLiteralStringLiteralLiteral ::= NumberLiteral + | StringLiteralno referencesNumberLiteral:[0-9][1-9].[0-9]NumberLiteral + ::= [0-9] [1-9]* ( '.' [0-9]+ )?referenced by:LiteralStringLiteral:'NonQuoteChar\QuoteChar'StringLiteral + ::= "'" ( NonQuoteChar | '\' QuoteChar )* "'"referenced by:LiteralPropertyName:[a-z][A-Z][0-9]PropertyName + ::= [a-zA-Z0-9]+referenced by:ObjectExpressionPipelineTransformElementId:[a-z][A-Z][0-9]ElementId + ::= [a-zA-Z0-9]+referenced by:PipelineComponentLabel:[a-z][A-Z][0-9]ComponentLabel + ::= [a-zA-Z0-9]+referenced by:Pipeline ... generated by Railroad Diagram GeneratorRR \ No newline at end of file diff --git a/core/frb/language-temp.js b/core/frb/language-temp.js new file mode 100644 index 0000000000..46b41b88e6 --- /dev/null +++ b/core/frb/language-temp.js @@ -0,0 +1,4 @@ +exports.precedence = require("frb/language").precedence; +exports.precedenceLevels = require("frb/language").precedenceLevels; +exports.operatorTokens = require("frb/language").operatorTokens; +exports.operatorTypes = require("frb/language").operatorTypes; diff --git a/core/frb/language.js b/core/frb/language.js index 46b41b88e6..176a00eded 100644 --- a/core/frb/language.js +++ b/core/frb/language.js @@ -1,4 +1,68 @@ -exports.precedence = require("frb/language").precedence; -exports.precedenceLevels = require("frb/language").precedenceLevels; -exports.operatorTokens = require("frb/language").operatorTokens; -exports.operatorTypes = require("frb/language").operatorTypes; + +var Set = require("collections/set"); +var Map = require("collections/map"); + +var precedence = exports.precedence = new Map(); +var levels = exports.precedenceLevels = [ + ["tuple", "record"], + [ + "literal", + "value", + "parameters", + "property", + "element", + "component", + "mapBlock", + "filterBlock", + "sortedBlock", + "groupBlock", + "groupMapBlock", + "with" + ], + ["not", "neg", "number", "parent"], + ["scope"], + ["default"], + ["pow", "root", "log"], + ["mul", "div", "mod", "rem"], + ["add", "sub"], + ["equals", "lt", "gt", "le", "ge", "compare"], + ["and"], + ["or"], + ["if"] +]; + +for(var i=0, countI = levels.length, predecessors, level, j, countJ; i": "gt", + "<=": "le", + ">=": "ge", + "==": "equals", + "<=>": "compare", + "!=": "notEquals", + "??": "default", + "&&": "and", + "||": "or", + "?": "then", + ":": "else" +}); + +exports.operatorTypes = new Map(operatorTokens.map(function (type, token) { + return [type, token]; +})); diff --git a/core/frb/lib/l2r-parser.js b/core/frb/lib/l2r-parser.js new file mode 100644 index 0000000000..8be724adc6 --- /dev/null +++ b/core/frb/lib/l2r-parser.js @@ -0,0 +1,23 @@ + +module.exports = makeLeftToRightParser; +function makeLeftToRightParser(parsePrevious, parseOperator, makeSyntax) { + var parseSelf = function (callback, left) { + if (left) { + return parseOperator(function (operator) { + if (operator) { + return parsePrevious(function (right) { + return parseSelf(callback, makeSyntax(operator, left, right)); + }); + } else { + return callback(left); + } + }); + } else { + return parsePrevious(function (left) { + return parseSelf(callback, left); + }); + } + }; + return parseSelf; +} + diff --git a/core/frb/lib/parser.js b/core/frb/lib/parser.js new file mode 100644 index 0000000000..fb4fcc8cd0 --- /dev/null +++ b/core/frb/lib/parser.js @@ -0,0 +1,70 @@ + +exports.makeParser = makeParser; +function makeParser(production) { + return function (text /*, ...args*/) { + var syntax; + var column = 0, line = 1; + var state = production.apply(this, [function (_syntax) { + syntax = _syntax; + return expectEof(); + }].concat(Array.prototype.slice.call(arguments, 1))); + try { + for (var i = 0; i < text.length; i++) { + state = state(text[i], { + index: i, + line: line, + column: column + }); + if (text[i] === "\n") { + line++; + column = 0; + } else { + column++; + } + } + state = state("", { + index: i, + line: line, + column: column + }); + } catch (exception) { + if (exception.loc) { + var loc = exception.loc; + if (text.length > 80) { + exception.message += " at line " + loc.line + ", column " + loc.column; + } else { + exception.message += " in " + JSON.stringify(text) + " at index " + loc.index; + } + } + throw exception; + } + return syntax; + }; +} + +exports.expectEof = expectEof; +function expectEof() { + return function (character, loc) { + if (character !== "") { + var error = new Error("Unexpected " + JSON.stringify(character)); + error.loc = loc; + throw error; + } + return function noop() { + return noop; + }; + }; +} + +exports.makeExpect = makeExpect; +function makeExpect(expected) { + return function (callback, loc) { + return function (character, loc) { + if (character === expected) { + return callback(character, loc); + } else { + return callback(null, loc)(character, loc); + } + }; + }; +} diff --git a/core/frb/lib/trie-parser.js b/core/frb/lib/trie-parser.js new file mode 100644 index 0000000000..5ecc94643f --- /dev/null +++ b/core/frb/lib/trie-parser.js @@ -0,0 +1,23 @@ + +module.exports = makeParserFromTrie; +function makeParserFromTrie(trie) { + var children = {}; + Object.keys(trie.children).forEach(function (character) { + children[character] = makeParserFromTrie(trie.children[character]); + }); + return function (callback, rewind) { + rewind = rewind || identity; + return function (character, loc) { + if (children[character]) { + return children[character](callback, function (callback) { + return rewind(callback)(character, loc); + }); + } else { + return callback(trie.value, rewind)(character, loc); + } + }; + }; +} + +function identity(x) {return x;} + diff --git a/core/frb/lib/trie.js b/core/frb/lib/trie.js new file mode 100644 index 0000000000..35c6e0b25f --- /dev/null +++ b/core/frb/lib/trie.js @@ -0,0 +1,25 @@ + +module.exports = makeTrie; +function makeTrie(table) { + var strings = Object.keys(table); + var trie = {value: void 0, children: {}}; + var tables = {}; + strings.forEach(function (string) { + if (string.length === 0) { + trie.value = table[string]; + } else { + var character = string[0]; + if (!tables[character]) { + tables[character] = {}; + } + var tail = string.slice(1); + tables[character][tail] = table[string]; + } + }); + var characters = Object.keys(tables); + characters.forEach(function (character) { + trie.children[character] = makeTrie(tables[character]); + }); + return trie; +} + diff --git a/core/frb/merge.js b/core/frb/merge.js new file mode 100644 index 0000000000..5bc0f7dd62 --- /dev/null +++ b/core/frb/merge.js @@ -0,0 +1,187 @@ +"use strict"; + +require("collections/shim"); + +/* + * See: Introduction to Algorithms, Longest Common Subsequence, Algorithms, + * Cormen et al, 15.4 + * This is an adaptation of the LCS and Lehvenstein distance algorithm that + * instead of computing the longest subsequence, or the cost of edit, finds the + * shortest operational transform. The cost is the cost of traversal in terms + * of "insert" and "delete" operations, where "retain" is free. + */ +exports.graphOt = graphOt; +function graphOt(target, source) { + var size = (source.length + 1) * (target.length + 1); + + // edges is a 2D linearized array with a height of target.length + 1 and a + // width of source.length + 1. Each cell corresponds to an operational + // transform walking from the top-left to the bottom-right. + var edges = Array(size); + // we only need to know the costs of the previous source column and the + // current source column up to the cell in question. + var theseCosts = Array(source.length + 1); + var prevCosts = Array(source.length + 1); + var tempCosts; + + for (var t = 0; t < target.length + 1; t++) { + for (var s = 0; s < source.length + 1; s++) { + var direction, cost; + if (t === 0 && s === 0) { + direction = " "; + cost = 0; + } else if (t === 0) { + direction = "insert"; + cost = s; + } else if (s === 0) { + direction = "delete"; + cost = t; + } else if (target[t - 1] === source[s - 1]) { + direction = "retain"; + cost = prevCosts[s - 1]; + } else { + var tCost = theseCosts[s - 1]; + var sCost = prevCosts[s]; + // favoring the source tends to produce more removal followed + // by insertion, which packs into a swap transforms better + if (sCost < tCost) { + direction = "delete"; + cost = sCost + 1; + } else { + direction = "insert"; + cost = tCost + 1; + } + } + edges[t + s * (target.length + 1)] = direction; + theseCosts[s] = cost; + + } + // swap columns, reuse and overwrite the previous column as the current + // column for the next iteration. + tempCosts = theseCosts; + theseCosts = prevCosts; + prevCosts = tempCosts; + } + + return { + edges: edges, + cost: prevCosts[source.length], + source: source, + target: target + }; +} + +/** + * Tracks backward through a graph produced by graphOt along the cheapest path + * from the bottom-right to the top-left to produce the cheapest sequence of + * operations. Accumulates adjacent operations of the same type into a single + * operation of greater length. + */ +exports.traceOt = traceOt; +function traceOt(graph) { + var ops = []; + var edges = graph.edges; + var t, tl = t = graph.target.length; + var s = graph.source.length; + var previous; + while (t || s) { + var direction = edges[t + s * (tl + 1)]; + if (direction === "delete") { + if (previous && previous[0] === "delete") { + previous[1]++; + } else { + var op = ["delete", 1]; + previous = op; + ops.push(op); + } + t--; + } else if (direction === "insert") { + if (previous && previous[0] === "insert") { + previous[1]++; + } else { + var op = ["insert", 1]; + previous = op; + ops.push(op); + } + s--; + } else if (direction === "retain") { + var op = ["retain", 1]; + if (previous && previous[0] === "retain") { + previous[1]++; + } else { + previous = op; + ops.push(op); + } + t--; s--; + } + } + ops.reverse(); + return ops; +} + +/** + * Compute the shortest operational transform on the target sequence to become + * the source sequence. + */ +exports.ot = ot; +function ot(target, source) { + return traceOt(graphOt(target, source)); +} + +/** + * Compute the shortest sequence of splice or swap operations on the target + * sequence to become the source sequence. + */ +exports.diff = diff; +function diff(target, source) { + var ops = ot(target, source); + var swops = []; + + // convert ops to splice/swap operations + var t = 0; + var s = 0; + var o = 0; + var previous; + while (o < ops.length) { + var op = ops[o++]; + if (op[0] === "insert") { + swops.push([s, 0, source.slice(s, s + op[1])]); + s += op[1]; + } else if (op[0] === "delete") { + if (o < ops.length && ops[o][0] === "insert") { + var insert = ops[o++]; + swops.push([s, op[1], source.slice(s, s + insert[1])]); + t += op[1]; + s += insert[1]; + } else { + swops.push([s, op[1]]); + t += op[1]; + } + } else if (op[0] == "retain") { + t += op[1]; + s += op[1]; + } + } + + return swops; +} + +/** + * Apply the given sequence of swap operations on the target sequence. + */ +exports.apply = apply; +function apply(target, patch) { + for (var s = 0; s < patch.length; s++) { + target.swap.apply(target, patch[s]); + } +} + +/** + * Apply the shortest sequence of swap operations on the target sequence for it + * to become equivalent to the target sequence. + */ +exports.merge = merge; +function merge(target, source) { + apply(target, diff(target, source)); +} + diff --git a/core/frb/observe.js b/core/frb/observe.js new file mode 100644 index 0000000000..1815258c67 --- /dev/null +++ b/core/frb/observe.js @@ -0,0 +1,64 @@ + +var parse = require("./parse"); +var compile = require("./compile-observer"); +var Observers = require("./observers"); +var autoCancelPrevious = Observers.autoCancelPrevious; +var Scope = require("./scope"); + +module.exports = observe; +function observe(source, expression, descriptorOrFunction) { + var descriptor; + if (typeof descriptorOrFunction === "function") { + descriptor = {change: descriptorOrFunction}; + } else { + descriptor = descriptorOrFunction; + } + + descriptor = descriptor || empty; + descriptor.source = source; + descriptor.sourcePath = expression; + var parameters = descriptor.parameters = descriptor.parameters || source; + var document = descriptor.document; + var components = descriptor.components; + var beforeChange = descriptor.beforeChange; + var contentChange = descriptor.contentChange; + + // TODO consider the possibility that source has an intrinsic scope + // property + var sourceScope = new Scope(source); + sourceScope.parameters = parameters; + sourceScope.document = document; + sourceScope.components = components; + sourceScope.beforeChange = beforeChange; + + var syntax = parse(expression); + var observe = compile(syntax); + + // decorate for content change observations + if (contentChange === true) { + observe = Observers.makeRangeContentObserver(observe); + } + + return observe(autoCancelPrevious(function (value) { + if (!value) { + } else if (typeof contentChange !== "function") { + if(arguments.length === 1) { + return descriptor.change.call(source, arguments[0]); + } + else if(arguments.length === 2) { + return descriptor.change.call(source, arguments[0], arguments[1]); + } + else { + return descriptor.change.apply(source, arguments); + } + + } else if (typeof contentChange === "function") { + value.addRangeChangeListener(contentChange); + return function () { + value.removeRangeChangeListener(contentChange); + }; + } + }), sourceScope); +} + +var empty = {}; diff --git a/core/frb/observers.js b/core/frb/observers.js new file mode 100644 index 0000000000..4a65ce56e1 --- /dev/null +++ b/core/frb/observers.js @@ -0,0 +1,1829 @@ + +require("collections/shim"); // Function.noop +var PropertyChanges = require("collections/listen/property-changes"); +require("collections/listen/array-changes"); +var SortedArray = require("collections/sorted-array"); +var SortedSet = require("collections/sorted-set"); +var Map = require("collections/map"); +var Set = require("collections/set"); +var Heap = require("collections/heap"); +var Scope = require("./scope"); +var Operators = require("./operators"); + +// Simple stuff..."degenerate" even + +exports.makeLiteralObserver = makeLiteralObserver; +function makeLiteralObserver(literal) { + return function observeLiteral(emit) { + return emit(literal); + }; +} + +exports.observeValue = observeValue; +function observeValue(emit, scope) { + return emit(scope.value); +} + +exports.observeParameters = observeParameters; +function observeParameters(emit, scope) { + return emit(scope.parameters); +} + +// This is a concession that in practice FRB may be used in conjunction with a +// browser DOM. +exports.makeElementObserver = makeElementObserver; +function makeElementObserver(id) { + return function observeElement(emit, scope) { + return emit(scope.document.getElementById(id)); + }; +} + +// This is a concession that in practice FRB will probably be used mostly in +// conjunction with MontageJS for its component model. +exports.makeComponentObserver = makeComponentObserver; +function makeComponentObserver(label, syntax) { + return function observeComponent(emit, scope) { + // TODO error if scope.components does not exist or components for + // label does not exist + var components = scope.components; + var method = components.getObjectByLabel || components.getComponentByLabel; + var component = method.call(components, label); + return emit(component); + }; +} + +exports.observeProperty = observeProperty; +var _observeProperty = observeProperty; // to bypass scope shadowing problems below +function observeProperty(object, key, emit, scope) { + if (object == null) return emit(); + var cancel; + function propertyChange(value, key, object) { + if (cancel) cancel(); + cancel = emit(value, key, object); + } + PropertyChanges.addOwnPropertyChangeListener( + object, + key, + propertyChange, + scope.beforeChange + ); + propertyChange(object[key], key, object); + return function cancelPropertyObserver() { + if (cancel) cancel(); + PropertyChanges.removeOwnPropertyChangeListener( + object, + key, + propertyChange, + scope.beforeChange + ); + }; +} + +exports.makePropertyObserver = makePropertyObserver; +function makePropertyObserver(observeObject, observeKey) { + return function observeProperty(emit, scope) { + return observeKey(function replaceKey(key) { + if (typeof key !== "string" && typeof key !== "number") return emit(); + + function replaceObject(object) { + return (object == null) + ? replaceObject.emit() + : object.observeProperty + ? object.observeProperty(key, replaceObject.emit, replaceObject.scope) + : replaceObject.observeProperty(object, key, replaceObject.emit, replaceObject.scope); + }; + replaceObject.observeProperty = _observeProperty; + replaceObject.emit = emit; + replaceObject.scope = scope; + + return observeObject(replaceObject, scope); + }, scope); + }; +} + +exports.observeKey = observeGet; // deprecated +exports.observeGet = observeGet; +var _observeGet = observeGet; // to bypass scope shadowing below +function observeGet(collection, key, emit, scope) { + var cancel; + var equals = collection.contentEquals || Object.equals; + function mapChange(value, mapKey, collection) { + if (equals(key, mapKey)) { + if (cancel) cancel(); + cancel = emit(value, key, collection); + } + } + mapChange(collection.get(key), key, collection); + collection.addMapChangeListener(mapChange, scope.beforeChange); + return function cancelMapObserver() { + if (cancel) cancel(); + collection.removeMapChangeListener(mapChange); + }; +} + +exports.makeGetObserver = makeGetObserver; +function makeGetObserver(observeCollection, observeKey) { + return function observeGet(emit, scope) { + return observeCollection(function replaceCollection(collection) { + if (!collection) return emit(); + + function replaceKey(key) { + if (key == null) return emit(); + if (collection.observeGet) { + // polymorphic override + return collection.observeGet(key, emit, scope); + } else { + // common case + return replaceKey.observeGet(collection, key, emit, scope); + } + }; + replaceKey.observeGet = _observeGet; + + return observeKey(replaceKey, scope); + }, scope); + }; +} + +exports.makeHasObserver = makeHasObserver; +function makeHasObserver(observeSet, observeValue) { + return function observeHas(emit, scope) { + emit = makeUniq(emit); + return observeValue(function replaceValue(sought) { + return observeSet(function replaceSet(collection) { + if (!collection) { + return emit(); + } else if (collection.addRangeChangeListener) { + return observeRangeChange(collection, function rangeChange() { + // This could be done incrementally if there were + // guarantees of uniqueness, but if there are + // guarantees of uniqueness, the data structure can + // probably efficiently check + return emit((collection.has || collection.contains) + .call(collection, sought)); + }, scope); + } else if (collection.addMapChangeListener) { + return observeMapChange(collection, function mapChange() { + return emit(collection.has(sought)); + }, scope); + } else { + return emit(); + } + }, scope); + }, scope); + }; +} + +// Compound Observers + +// accepts an array of observers and emits an array of the corresponding +// values, incrementally updated +exports.makeObserversObserver = makeObserversObserver; +function makeObserversObserver(observers) { + return function observeObservers(emit, scope) { + var output = [], + cancelers = []; + for (var index = 0, indexCount = observers.length; index < indexCount; index++) { + output[index] = void 0; + cancelers[index] = (function captureIndex(observe, index) { + return observe(function replaceValueAtIndex(value) { + output.set(index, value); + }, scope); + })(observers[index], index); + } + var cancel = emit(output); + return function cancelObserversObserver() { + if (cancel) cancel(); + for (var index = 0, indexCount = cancelers.length; index < indexCount; index++) { + cancel = cancelers[index]; + if (cancel) cancel(); + } + }; + }; +} + +// {type: "record", args: {key: observe}} +// {a: 10, b: c + d} +// {type: "record", args: {a: {type: "literal", value: 10 ... +exports.makeRecordObserver = makeObjectObserver; // deprecated +exports.makeObjectObserver = makeObjectObserver; +function makeObjectObserver(observers) { + return function observeObject(emit, scope) { + var cancelers = {}, + output = {}, + names = Object.keys(observers), + i; + + for (i=0;(name=names[i]);i++) { + (function (name, observe) { + // To ensure that the property exists, even if the observer + // fails to emit: + output[name] = void 0; + cancelers[name] = observe(function (value) { + output[name] = value; + }, scope); + })(name, observers[name]); + } + var cancel = emit(output); + return function cancelRecordObserver() { + if (cancel) cancel(); + var names = Object.keys(cancelers),i; + + for (i=0;(name=names[i]);i++) { + cancel = cancelers[name]; + if (cancel) cancel(); + } + }; + }; +} + +exports.makeTupleObserver = makeArrayObserver; // deprecated +exports.makeArrayObserver = makeArrayObserver; +function makeArrayObserver() { + // Unroll Array.prototype.slice.call(arguments) to prevent deopt + var observers = []; + var index = arguments.length; + while (index--) { + observers[index] = arguments[index]; + } + return makeObserversObserver(observers); +} + +// Operators + +exports.makeOperatorObserverMaker = makeOperatorObserverMaker; +function makeOperatorObserverMaker(operator) { + return function makeOperatorObserver(/*...observers*/) { + // Unroll Array.prototype.slice.call(arguments); + var observers = []; + var index = arguments.length; + while (index--) { + observers[index] = arguments[index]; + } + var observeOperands = makeObserversObserver(observers); + var observeOperandChanges = makeRangeContentObserver(observeOperands); + return function observeOperator(emit, scope) { + return observeOperandChanges(function (operands) { + if (!operands.every(Operators.defined)) return emit(); + if(operands.length === 1) { + return emit(operator.call(void 0, operands[0])); + } + else if(operands.length === 2) { + return emit(operator.call(void 0, operands[0], operands[1])); + } + else { + return emit(operator.apply(void 0, operands)); + } + }, scope); + }; + }; +} + +exports.makeMethodObserverMaker = makeMethodObserverMaker; +function makeMethodObserverMaker(name) { + var capitalName = name.slice(0, 1).toUpperCase(); + capitalName += name.slice(1); + var makeObserverName = 'make'; + makeObserverName += capitalName; + makeObserverName += 'Observer'; + var observeName = 'observe'; + observeName += capitalName; + return function makeMethodObserver(/*...observers*/) { + var observeObject = arguments[0]; + var operandObservers = Array.prototype.slice.call(arguments, 1); + var observeOperands = makeObserversObserver(operandObservers); + var observeOperandChanges = makeRangeContentObserver(observeOperands); + return function observeMethod(emit, scope) { + return observeObject(function (object) { + if (!object) return emit(); + if (object[makeObserverName]) { + if(operandObservers.length === 1) { + return object[makeObserverName].call(object, operandObservers[0])(emit, scope); + } + else if(operandObservers.length === 2) { + return object[makeObserverName].call(object, operandObservers[0], operandObservers[1])(emit, scope); + } + else { + return object[makeObserverName].apply(object, operandObservers)(emit, scope); + } + } + if (object[observeName]) + return object[observeName](emit, scope); + return observeOperandChanges(function (operands) { + if (!operands.every(Operators.defined)) return emit(); + if (typeof object[name] === "function") { + if(operands.length === 1) { + return emit(object[name].call(object, operands[0])); + } + else if(operands.length === 2) { + return emit(object[name].call(object, operands[0], operands[1])); + } + else { + return emit(object[name].apply(object, operands)); + } + } else { + return emit(); + } + }, scope); + }, scope); + }; + }; +} + +// The "not" operator coerces null and undefined, so it is not adequate to +// implement it with makeOperatorObserverMaker. + +exports.makeNotObserver = makeNotObserver; +function makeNotObserver(observeValue) { + return function observeNot(emit, scope) { + return observeValue(function replaceValue(value) { + return emit(!value); + }, scope); + }; +} + +// The "and" and "or" operators short-circuit, so it is not adequate to +// implement them with makeOperatorObserverMaker. + +exports.makeAndObserver = makeAndObserver; +function makeAndObserver(observeLeft, observeRight) { + return function observeAnd(emit, scope) { + return observeLeft(function replaceLeft(left) { + return (!left) + ? emit(left) + : observeRight(emit, scope); + }, scope); + }; +} + +exports.makeOrObserver = makeOrObserver; +function makeOrObserver(observeLeft, observeRight) { + return function observeOr(emit, scope) { + return observeLeft(function replaceLeft(left) { + return left + ? emit(left) + : observeRight(emit, scope); + }, scope); + }; +} + +// expression: condition ? consequent : alternate +// syntax: {type: "if", args: [condition, consequent, alternate]} +exports.makeConditionalObserver = makeConditionalObserver; +function makeConditionalObserver(observeCondition, observeConsequent, observeAlternate) { + return function observeConditional(emit, scope) { + return observeCondition(function replaceCondition(condition) { + if (condition == null) { + return emit(); + } else if (condition) { + return observeConsequent(emit, scope); + } else { + return observeAlternate(emit, scope); + } + }, scope); + }; +} + +// This cannot be written in terms of the defined operator because the input +// may be null or undefined and still emit a value. +exports.makeDefinedObserver = makeDefinedObserver; +function makeDefinedObserver(observeValue) { + return function observeDefault(emit, scope) { + return observeValue(function replaceValue(value) { + return emit(value != null); + }, scope); + }; +} + +exports.makeDefaultObserver = makeDefaultObserver; +function makeDefaultObserver(observeValue, observeAlternate) { + return function observeDefault(emit, scope) { + return observeValue(function replaceValue(value) { + if (value == null) { + return observeAlternate(emit, scope); + } else { + return emit(value); + } + }, scope); + }; +} + +// Comprehension Observers + +// The map comprehension +// object.array.map{+1} +// Handles both range content changes and full replacement of the input +// object.array.splice(0, 1, 2); +// object.array = [1, 2, 3] +var makeMapBlockObserver = exports.makeMapBlockObserver = makeNonReplacing(makeReplacingMapBlockObserver); +function makeReplacingMapBlockObserver(observeCollection, observeRelation) { + return function observeMap(emit, scope) { + return observeCollection(function replaceMapInput(input) { + if (!input) return emit(); + + var output = []; + var indexRefs = []; + var cancelers = []; + + function update(index) { + for (var i=0, countI = input.length; i < countI; i++) { + indexRefs[i].index = i; + } + } + + function rangeChange(plus, minus, index) { + var i, + countI, + // plusMapped = plus.map(function (value, offset) { + // return {index: index + offset}; + // })), + plusMapped = []; + + for(i=0, countI = plus.length;i +// names.group{.0} -> [[klass, [...values]], [...], [...]] +// names.groupMap{.0}.entries() +exports.makeGroupBlockObserver = makeGroupBlockObserver; +function makeGroupBlockObserver(observeCollection, observeRelation) { + var observeGroup = makeGroupMapBlockObserver(observeCollection, observeRelation); + return makeEntriesObserver(observeGroup); +} + +exports.makeGroupMapBlockObserver = makeGroupMapBlockObserver; +function makeGroupMapBlockObserver(observeCollection, observeRelation) { + var observeRelationEntry = makeRelationEntryObserver(observeRelation); + var observeRelationEntries = makeReplacingMapBlockObserver(observeCollection, observeRelationEntry); + return function observeGroup(emit, scope) { + return observeRelationEntries(function (input, original) { + if (!input) return emit(); + + var groups = new Map(); + + function rangeChange(plus, minus, index) { + var i, countI, item, group, create; + for(i=0, countI = minus.length; i boundedStart) { // rolling forward + // old: A B C D + output.swap(output.length, 0, input.slice(boundedStart + boundedLength, newBoundedStart + newBoundedLength)); + // mid: A B C D E F + output.splice(0, output.length - newBoundedLength); + // new: C D E F + } else { + // a new window that does not overlap the previous + output.swap(0, output.length, input.slice(newBoundedStart, newBoundedStart + newBoundedLength)); + } + } // otherwise, was inactive, remains inactive + start = newStart; + boundedStart = newBoundedStart; + boundedLength = newBoundedLength; + }, scope); + + function rangeChange(plus, minus, index) { + if (!active) { + return; + } + var diff = plus.length - minus.length; + if (index <= start) { // before window + if (diff > 0) { // grow by diff on leading side, rolling forward + // 0 1 2 3 4 5 + // ----- (start 1, length 3) + // A C D E F + // A>B 0) { + // truncate + output.splice(length, output.length - length); + } else { + // regrow + output.swap(output.length, 0, input.slice(start + output.length, start + length)); + } + } // after the window + } + + var cancel = emit(output); + + return function cancelViewChangeObserver() { + if (cancel) cancel(); + cancelLengthObserver(); + cancelStartObserver(); + cancelRangeChangeObserver(); + }; + }, scope); + }; +} + +var observeZero = makeLiteralObserver(0); + +exports.makeEnumerateObserver = makeNonReplacing(makeReplacingEnumerateObserver); +exports.makeEnumerationObserver = exports.makeEnumerateObserver; // deprecated +function makeReplacingEnumerateObserver(observeArray) { + return function (emit, scope) { + return observeArray(function replaceArray(input) { + if (!input) return emit(); + + var output = []; + function update(index) { + for (; index < output.length; index++) { + output[index].set(0, index); + } + } + function rangeChange(plus, minus, index) { + output.swap(index, minus.length, plus.map(function (value, offset) { + return [index + offset, value]; + })); + update(index + plus.length); + } + var cancelRangeChange = observeRangeChange(input, rangeChange, scope); + var cancel = emit(output); + return function cancelEnumerateObserver() { + if (cancel) cancel(); + cancelRangeChange(); + }; + }, scope); + }; +} + +// length.range() -> [0, 1, 2, ...length] +exports.makeRangeObserver = makeRangeObserver; +function makeRangeObserver(observeLength) { + return function observeRange(emit, scope) { + var output = []; + var cancel = emit(output); + var cancelLengthObserver = observeLength(function (length) { + length = length >>> 0; + if (length == null) { + output.clear(); + } else if (length > output.length) { + // pre-fab the extension so the we only have to propagate one + // range change to the output. + var extension = []; + for (var i = output.length; i < length; i++) { + extension.push(i); + } + output.swap(output.length, 0, extension); + } else if (length < output.length) { + output.splice(length, output.length); + } + }, scope); + return function cancelObserveRange() { + if (cancel) cancel(); + if (cancelLengthObserver) cancelLengthObserver(); + }; + }; +} + + +// String Observers + +exports.makeStartsWithObserver = makeStartsWithObserver; +function makeStartsWithObserver(observeHaystack, observeNeedle) { + return function observeStartsWith(emit, scope) { + return observeNeedle(function (needle) { + if (typeof needle !== "string") + return emit(); + var expression = new RegExp("^" + RegExp.escape(needle)); + return observeHaystack(function (haystack) { + if (typeof haystack !== "string") + return emit(); + return emit(expression.test(haystack)); + }, scope); + }, scope); + }; +} + +exports.makeEndsWithObserver = makeEndsWithObserver; +function makeEndsWithObserver(observeHaystack, observeNeedle) { + return function observeEndsWith(emit, scope) { + return observeNeedle(function (needle) { + if (typeof needle !== "string") + return emit(); + var expression = new RegExp(RegExp.escape(needle) + "$"); + return observeHaystack(function (haystack) { + if (typeof haystack !== "string") + return emit(); + return emit(expression.test(haystack)); + }, scope); + }, scope); + }; +} + +exports.makeContainsObserver = makeContainsObserver; +function makeContainsObserver(observeHaystack, observeNeedle) { + return function observeContains(emit, scope) { + return observeNeedle(function (needle) { + if (typeof needle !== "string") + return emit(); + var expression = new RegExp(RegExp.escape(needle)); + return observeHaystack(function (haystack) { + if (typeof haystack !== "string") + return emit(); + return emit(expression.test(haystack)); + }, scope); + }, scope); + }; +} + +exports.makeJoinObserver = makeJoinObserver; +function makeJoinObserver(observeArray, observeDelimiter) { + observeDelimiter = observeDelimiter || observeNullString; + return function observeJoin(emit, scope) { + return observeArray(function changeJoinArray(array) { + if (!array) return emit(); + return observeDelimiter(function changeJoinDelimiter(delimiter) { + if (typeof delimiter !== "string") return emit(); + var cancel; + function rangeChange() { + if (cancel) cancel(); + cancel = emit(array.join(delimiter)); + } + var cancelRangeChange = observeRangeChange(array, rangeChange, scope); + return function cancelJoinObserver() { + cancelRangeChange(); + if (cancel) cancel(); + }; + }, scope); + }, scope); + }; +} + +var observeNullString = makeLiteralObserver(""); + +// Collection Observers + +exports.observeRangeChange = observeRangeChange; +function observeRangeChange(collection, emit, scope) { + if ((!collection) || (!collection.toArray) || (!collection.addRangeChangeListener)) { + return Function.noop; + } + + var cancel, cancelRangeChange; + + function rangeChange(plus, minus, index) { + if (cancel) cancel(); + cancel = emit(plus, minus, index); + } + rangeChange(Array.isArray(collection) ? collection : collection.toArray(), Array.empty, 0); + + cancelRangeChange = collection.addRangeChangeListener( + rangeChange, + null, + scope.beforeChange + ); + return function cancelRangeObserver() { + if (cancel) cancel(); + cancelRangeChange(); + }; +} + +// array[array.length - 1] +// array.last() +exports.makeLastObserver = makeLastObserver; +function makeLastObserver(observeCollection) { + return function observeLast(emit, scope) { + return observeCollection(function (collection) { + return _observeLast(collection, emit, scope); + }, scope); + }; +} + +// [] +// [1, 2, 3], [], 0 -> [1, 2, 3] grow from start +// [4], [], 3 -> [1, 2, 3, 4] grow +// [], [4], 3 -> [1, 2, 3] +exports.observeLast = observeLast; +var _observeLast = observeLast; +function observeLast(collection, emit, scope) { + var lastIndex = -1; + var cancel; + var prev = null; + function rangeChange(plus, minus, index) { + lastIndex += plus.length - minus.length; + // short circuit if the change does not have the reach to change the + // last value + if ( + index + minus.length < lastIndex && + index + plus.length < lastIndex + ) { + return; + } + var next = lastIndex < 0 ? null : collection.get(lastIndex); + if (cancel) cancel(); + cancel = emit(next); + prev = next; + } + var cancelRangeChange = observeRangeChange(collection, rangeChange, scope); + return function cancelLastObserver() { + if (cancel) cancel(); + if (cancelRangeChange) cancelRangeChange(); + }; +} + +exports.makeOnlyObserver = makeOnlyObserver; +function makeOnlyObserver(observeCollection) { + return function (emit, scope) { + return observeCollection(makeUniq(function replaceCollectionForOnly(collection) { + return observeOnly(collection, emit, scope); + }), scope); + }; +} + +// selectedValues <-> selection.map{value} +// selectedValue <-> selectedValues.only() +exports.observeOnly = observeOnly; +function observeOnly(collection, emit, scope) { + var length = 0; + function rangeChange(plus, minus, index) { + length += plus.length - minus.length; + if (length !== 1) return emit(); + return emit(collection.only()); + } + return observeRangeChange(collection, rangeChange, scope); +} + +exports.makeOneObserver = makeOneObserver; +function makeOneObserver(observeCollection) { + return function (emit, scope) { + return observeCollection(makeUniq(function replaceCollectionForOne(collection) { + return observeOne(collection, emit, scope); + }), scope); + }; +} + +exports.observeOne = observeOne; +function observeOne(collection, emit, scope) { + var length = 0; + function rangeChange(plus, minus, index) { + length += plus.length - minus.length; + if (length === 0) return emit(); + return emit(collection.one()); + } + return observeRangeChange(collection, rangeChange, scope); +} + +// this.values = []; +// this.values.addRangeChangeListener(this, "values") // dispatches handleValuesRangeChange +// this.defineBinding("values.rangeContent()", {"<-": "otherValues"}); +exports.makeRangeContentObserver = makeRangeContentObserver; +function makeRangeContentObserver(observeCollection) { + return function observeContent(emit, scope) { + //Add scope variable closer + //emit and scope are always together? worth inlining them? + return observeCollection(function (collection) { + if (!collection || !collection.addRangeChangeListener) { + return emit(collection); + } else { + return observeRangeChange(collection, function rangeChange() { + return emit(collection); + }, scope); + } + }, scope); + }; +} + +exports.makeMapContentObserver = makeMapContentObserver; +function makeMapContentObserver(observeCollection) { + return function observeContent(emit, scope) { + return observeCollection(function (collection) { + if (!collection || !collection.addMapChangeListener) { + return emit(collection); + } else { + return observeMapChange(collection, function rangeChange() { + return emit(collection); + }, scope); + } + }, scope); + }; +} + +exports.observeMapChange = observeMapChange; +function observeMapChange(collection, emit, scope) { + if (!collection.addMapChangeListener) + return; + var cancelers = new Map(); + function mapChange(value, key, collection) { + var cancel = cancelers.get(key); + cancelers["delete"](key); + if (cancel) cancel(); + cancel = emit(value, key, collection); + if (value === undefined) { + if (cancel) cancel(); + } else { + cancelers.set(key, cancel); + } + } + collection.forEach(mapChange); + var cancelMapChange = collection.addMapChangeListener(mapChange, scope.beforeChange); + return function cancelMapObserver() { + // No point in unrolling this. The underlying data structure is a Map, + // not an array. + cancelers.forEach(function (cancel) { + if (cancel) cancel(); + }); + cancelMapChange(); + }; +} + +var makeEntriesObserver = exports.makeEntriesObserver = makeNonReplacing(makeReplacingEntriesObserver); +function makeReplacingEntriesObserver(observeCollection) { + return function _observeEntries(emit, scope) { + return observeCollection(function (collection) { + if (!collection) return emit(); + return observeEntries(collection, emit, scope); + }, scope); + }; +} + +exports.observeEntries = observeEntries; +function observeEntries(collection, emit, scope) { + var items = []; + var keyToEntry = new Map(); + var cancel = emit(items); + // TODO observe addition and deletion with separate observers + function mapChange(value, key, collection) { + var item, index; + if (!keyToEntry.has(key)) { // add + item = [key, value]; + keyToEntry.set(key, item); + items.push(item); + } else if (value == null) { // delete + item = keyToEntry.get(key); + keyToEntry["delete"](key); + index = items.indexOf(item); + items.splice(index, 1); + } else { // update + item = keyToEntry.get(key); + item.set(1, value); + } + } + var cancelMapChange = observeMapChange(collection, mapChange, scope); + return function cancelObserveEntries() { + if (cancel) cancel(); + if (cancelMapChange) cancelMapChange(); + }; +} + +exports.makeKeysObserver = makeKeysObserver; +function makeKeysObserver(observeCollection) { + var observeEntries = makeEntriesObserver(observeCollection); + return makeMapBlockObserver(observeEntries, observeEntryKey); +} + +exports.observeEntryKey = observeEntryKey; +function observeEntryKey(emit, scope) { + if (!scope.value) return emit(); + return emit(scope.value[0]); +} + +exports.makeValuesObserver = makeValuesObserver; +function makeValuesObserver(observeCollection) { + var observeEntries = makeEntriesObserver(observeCollection); + return makeMapBlockObserver(observeEntries, observeEntryValue); +} + +exports.observeEntryValue = observeEntryValue; +function observeEntryValue(emit, scope) { + if (!scope.value) return emit(); + return emit(scope.value[1]); +} + +exports.makeToMapObserver = makeToMapObserver; +function makeToMapObserver(observeObject) { + return function observeToMap(emit, scope) { + var map = new Map(); + var cancel = emit(map); + + var cancelObjectObserver = observeObject(function replaceObject(object) { + map.clear(); + if (!object || typeof object !== "object") return; + + // Must come first because Arrays also implement map changes, but + // Maps do not implement range changes. + if (object.addRangeChangeListener) { // array/collection of items + return observeUniqueEntries(function (entries) { + function rangeChange(plus, minus) { + + var i, countI; + for(i=0, countI = minus.length; i array.length) { + array.length = start; + } + + if (start + minusLength > array.length) { + // Truncate minus length if it extends beyond the length + minusLength = array.length - start; + } else if (minusLength < 0) { + // It is the JavaScript way. + minusLength = 0; + } + + var diff = plus.length - minusLength; + var oldLength = array.length; + var newLength = array.length + diff; + + if (diff > 0) { + // Head Tail Plus Minus + // H H H H M M T T T T + // H H H H P P P P T T T T + // ^ start + // ^-^ minus.length + // ^ --> diff + // ^-----^ plus.length + // ^------^ tail before + // ^------^ tail after + // ^ start iteration + // ^ start iteration offset + // ^ end iteration + // ^ end iteration offset + // ^ start + minus.length + // ^ length + // ^ length - 1 + for (var index = oldLength - 1; index >= start + minusLength; index--) { + var offset = index + diff; + if (index in array) { + array[offset] = array[index]; + } else { + // Oddly, PhantomJS complains about deleting array + // properties, unless you assign undefined first. + array[offset] = void 0; + delete array[offset]; + } + } + } + for (var index = 0; index < plus.length; index++) { + if (index in plus) { + array[start + index] = plus[index]; + } else { + array[start + index] = void 0; + delete array[start + index]; + } + } + if (diff < 0) { + // Head Tail Plus Minus + // H H H H M M M M T T T T + // H H H H P P T T T T + // ^ start + // ^-----^ length + // ^-^ plus.length + // ^ start iteration + // ^ offset start iteration + // ^ end + // ^ offset end + // ^ start + minus.length - plus.length + // ^ start - diff + // ^------^ tail before + // ^------^ tail after + // ^ length - diff + // ^ newLength + for (var index = start + plus.length; index < oldLength - diff; index++) { + var offset = index - diff; + if (offset in array) { + array[index] = array[offset]; + } else { + array[index] = void 0; + delete array[index]; + } + } + } + array.length = newLength; +} diff --git a/core/frb/operators.js b/core/frb/operators.js new file mode 100644 index 0000000000..2b86edd49d --- /dev/null +++ b/core/frb/operators.js @@ -0,0 +1,153 @@ + +require("collections/shim-object"); // equals, compare +require("collections/shim-regexp"); // escape +var Map = require("collections/map"); +var Set = require("collections/set"); +// from highest to lowest precedence + +exports.toNumber = function (s) { + return +s; +}; + +exports.toString = function (value) { + if (value == null) { + return value; + } else if (typeof value === "string" || typeof value === "number") { + return "" + value; + } else { + return null; + } +}; + +exports.toArray = Array.from; + +exports.toMap = Map.from.bind(Map); + +exports.toSet = Set.from.bind(Set); + +exports.not = function (b) { + return !b; +}; + +exports.neg = function (n) { + return -n; +}; + +exports.pow = function (a, b) { + return Math.pow(a, b); +}; + +exports.root = function (a, b) { + return Math.pow(a, 1 / b); +}; + +exports.log = function (a, b) { + return Math.log(a) / Math.log(b); +}; + +exports.mul = function (a, b) { + return a * b; +}; + +exports.div = function (a, b) { + return a / b; +}; + +exports.mod = function (a, b) { + return ((a % b) + b) % b; +}; + +exports.rem = function (a, b) { + return a % b; +}; + +exports.add = function (a, b) { + return a + b; +}; + +exports.sub = function (a, b) { + return a - b; +}; + +exports.ceil = function (n) { + return Math.ceil(n); +}; + +exports.floor = function (n) { + return Math.floor(n); +}; + +exports.round = function (n) { + return Math.round(n); +}; + +exports.lt = function (a, b) { + return Object.compare(a, b) < 0; +}; + +exports.gt = function (a, b) { + return Object.compare(a, b) > 0; +}; + +exports.le = function (a, b) { + return Object.compare(a, b) <= 0; +}; + +exports.ge = function (a, b) { + return Object.compare(a, b) >= 0; +}; + +exports.equals = Object.equals; + +exports.compare = Object.compare; + +exports.and = function (a, b) { + return a && b; +}; + +exports.or = function (a, b) { + return a || b; +}; + +exports.defined = function (value) { + return value != null; +}; + +// "startsWith", "endsWith", and "contains" are overridden in +// complile-observer so they can precompile the regular expression and reuse it +// in each reaction. + +exports.startsWith = function (a, b) { + var expression = new RegExp("^" + RegExp.escape(b)); + return expression.test(a); +}; + +exports.endsWith = function (a, b) { + var expression = new RegExp(RegExp.escape(b) + "$"); + return expression.test(a); +}; + +exports.contains = function (a, b) { + var expression = new RegExp(RegExp.escape(b)); + return expression.test(a); +}; + +exports.join = function (a, b) { + return a.join(b || ""); +}; + +exports.split = function (a, b) { + return a.split(b || ""); +}; + +exports.range = function (stop) { + var range = []; + for (var start = 0; start < stop; start++) { + range.push(start); + } + return range; +}; + +exports.last = function (collection) { + return collection.get(collection.length - 1); +}; diff --git a/core/frb/package-lock.json b/core/frb/package-lock.json new file mode 100644 index 0000000000..5bc8e391eb --- /dev/null +++ b/core/frb/package-lock.json @@ -0,0 +1,3255 @@ +{ + "name": "frb", + "version": "4.0.3", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.4.tgz", + "integrity": "sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA==", + "dev": true, + "requires": { + "@babel/types": "^7.9.0", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz", + "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", + "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.0.tgz", + "integrity": "sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.9.0", + "@babel/types": "^7.9.0", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.0.tgz", + "integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.0", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "@futagoza/eslint-config": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@futagoza/eslint-config/-/eslint-config-11.2.0.tgz", + "integrity": "sha512-aBZLI4bm7tvnyzxGfMuov1Ue+ppq2rcBrdC/fsSR8VzmfLbVW2xuIGo9jT9aDv81r8SsT6LCoAHbnf34uXPCNg==", + "dev": true, + "requires": { + "@futagoza/eslint-config-dev": "11.2.0", + "@futagoza/eslint-config-html": "11.2.0", + "@futagoza/eslint-config-node": "11.2.0", + "@futagoza/eslint-config-typescript": "11.2.0" + } + }, + "@futagoza/eslint-config-core": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@futagoza/eslint-config-core/-/eslint-config-core-11.2.0.tgz", + "integrity": "sha512-cTvzWJcnq0r4O94sEvKnWJw6Sg2gozfnPZFMekxcc+mJ8IZPjcYt2Aj0JlEXq+Oawy3T3qDv3bD/lv3VMfDhoQ==", + "dev": true + }, + "@futagoza/eslint-config-dev": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@futagoza/eslint-config-dev/-/eslint-config-dev-11.2.0.tgz", + "integrity": "sha512-xnBvFIKGdtLWB54n/F6nDmH64w4EvH0G6K830A8hhYwCVLTzXu/KBsjbPARpjIdnthFG3XNGHQ02gvG489oxHQ==", + "dev": true, + "requires": { + "@futagoza/eslint-config-globals": "11.2.0", + "@futagoza/eslint-config-javascript": "11.2.0", + "@futagoza/eslint-config-node": "11.2.0" + } + }, + "@futagoza/eslint-config-globals": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@futagoza/eslint-config-globals/-/eslint-config-globals-11.2.0.tgz", + "integrity": "sha512-4lfWTHRnvKxjyFXFtJr2aWBXIiJsZKGo0/Hvyr96g1mQn9njqzGFgms9fMSxjPYLaIqqFH8QJyQH/Z2HN5206Q==", + "dev": true, + "requires": { + "globals": "12" + } + }, + "@futagoza/eslint-config-html": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@futagoza/eslint-config-html/-/eslint-config-html-11.2.0.tgz", + "integrity": "sha512-/9XlwRo66qm/vgwIFn8Yt+qzYT4qRr9pFMNBEqnU+vWeZhWKqhwvv7n2pd9do55AMAeFD/rXpF/6HU50sS++cA==", + "dev": true, + "requires": { + "@futagoza/eslint-config-globals": "11.2.0", + "eslint-plugin-html": "6.0.0" + } + }, + "@futagoza/eslint-config-javascript": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@futagoza/eslint-config-javascript/-/eslint-config-javascript-11.2.0.tgz", + "integrity": "sha512-GfqCGLRKTA22oL7JqQUVnq8IORlOSj35po9Pb0bj2eUSDR18ctv6EVzIFx46rUPyDwkStp84ZBGPO5kIx6Vlsg==", + "dev": true, + "requires": { + "@futagoza/eslint-config-core": "11.2.0", + "@futagoza/eslint-config-globals": "11.2.0", + "babel-eslint": "^10.0.3" + } + }, + "@futagoza/eslint-config-node": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@futagoza/eslint-config-node/-/eslint-config-node-11.2.0.tgz", + "integrity": "sha512-kcY0Iq8UIwv7c4NwSpEujimXVAIi9rWytVUOQdvMOyBhiFEqc3bb2ydvrsA+R3QcSSgKUET3TcQ6XdOILHB9Tg==", + "dev": true, + "requires": { + "@futagoza/eslint-config-globals": "11.2.0", + "@futagoza/eslint-config-javascript": "11.2.0", + "eslint-plugin-node": "~10.0.0" + } + }, + "@futagoza/eslint-config-typescript": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@futagoza/eslint-config-typescript/-/eslint-config-typescript-11.2.0.tgz", + "integrity": "sha512-FMDSLylDaGjEkCcrUPQAyeKDI+kP6m4zzctQ59rbcqq+0yobeNhtSdRkMo0RJlzphtLUltIsT+yyh3aM/MnzOA==", + "dev": true, + "requires": { + "@futagoza/eslint-config-core": "11.2.0", + "@futagoza/eslint-config-javascript": "11.2.0", + "@typescript-eslint/eslint-plugin": "2.7.0", + "@typescript-eslint/parser": "2.7.0", + "typescript": "*" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", + "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", + "dev": true + }, + "@types/node": { + "version": "8.10.59", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.59.tgz", + "integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.7.0.tgz", + "integrity": "sha512-H5G7yi0b0FgmqaEUpzyBlVh0d9lq4cWG2ap0RKa6BkF3rpBb6IrAoubt1NWh9R2kRs/f0k6XwRDiDz3X/FqXhQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.7.0", + "eslint-utils": "^1.4.2", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^2.0.1", + "tsutils": "^3.17.1" + }, + "dependencies": { + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.7.0.tgz", + "integrity": "sha512-9/L/OJh2a5G2ltgBWJpHRfGnt61AgDeH6rsdg59BH0naQseSwR7abwHq3D5/op0KYD/zFT4LS5gGvWcMmegTEg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.7.0", + "eslint-scope": "^5.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.7.0.tgz", + "integrity": "sha512-ctC0g0ZvYclxMh/xI+tyqP0EC2fAo6KicN9Wm2EIao+8OppLfxji7KAGJosQHSGBj3TcqUrA96AjgXuKa5ob2g==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.7.0", + "@typescript-eslint/typescript-estree": "2.7.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.7.0.tgz", + "integrity": "sha512-vVCE/DY72N4RiJ/2f10PTyYekX2OLaltuSIBqeHYI44GQ940VCYioInIb8jKMrK9u855OEJdFC+HmWAZTnC+Ag==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "glob": "^7.1.4", + "is-glob": "^4.0.1", + "lodash.unescape": "4.0.1", + "semver": "^6.3.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "acorn": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "dev": true + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "ajv": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "argv": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", + "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "caching-transform": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", + "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==", + "dev": true, + "requires": { + "hasha": "^3.0.0", + "make-dir": "^2.0.0", + "package-hash": "^3.0.0", + "write-file-atomic": "^2.4.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "codecov": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.6.1.tgz", + "integrity": "sha512-IUJB6WG47nWK7o50etF8jBadxdMw7DmoQg05yIljstXFBGB6clOZsIj6iD4P82T2YaIU3qq+FFu8K9pxgkCJDQ==", + "dev": true, + "requires": { + "argv": "^0.0.2", + "ignore-walk": "^3.0.1", + "js-yaml": "^3.13.1", + "teeny-request": "^3.11.3", + "urlgrey": "^0.4.4" + } + }, + "coffee-script": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", + "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", + "dev": true + }, + "collections": { + "version": "github:montagejs/collections#e3a889e6b8766a664fbab95e2b09d1431f39d12b", + "from": "github:montagejs/collections#master", + "requires": { + "weak-map": "~1.0.x" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "cp-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", + "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "make-dir": "^2.0.0", + "nested-error-stacks": "^2.0.0", + "pify": "^4.0.1", + "safe-buffer": "^5.0.1" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", + "dev": true + }, + "entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", + "dev": true + } + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.6.0.tgz", + "integrity": "sha512-PpEBq7b6qY/qrOmpYQ/jTMDYfuQMELR4g4WI1M/NaSDDD/bdcMb+dj4Hgks7p41kW2caXsPsEZAEAyAgjVVC0g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mkdirp": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", + "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + } + } + }, + "eslint-plugin-es": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-2.0.0.tgz", + "integrity": "sha512-f6fceVtg27BR02EYnBhgWLFQfK6bN4Ll0nQFrBHOlCsAyxeZkn0NHns5O0YZOPrV1B3ramd6cgFwaoFLcSkwEQ==", + "dev": true, + "requires": { + "eslint-utils": "^1.4.2", + "regexpp": "^3.0.0" + } + }, + "eslint-plugin-html": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-html/-/eslint-plugin-html-6.0.0.tgz", + "integrity": "sha512-PQcGippOHS+HTbQCStmH5MY1BF2MaU8qW/+Mvo/8xTa/ioeMXdSP+IiaBw2+nh0KEMfYQKuTz1Zo+vHynjwhbg==", + "dev": true, + "requires": { + "htmlparser2": "^3.10.1" + } + }, + "eslint-plugin-node": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-10.0.0.tgz", + "integrity": "sha512-1CSyM/QCjs6PXaT18+zuAXsjXGIGo5Rw630rSKwokSs2jrYURQc4R5JZpoanNCqwNmepg+0eZ9L7YiRUJb8jiQ==", + "dev": true, + "requires": { + "eslint-plugin-es": "^2.0.0", + "eslint-utils": "^1.4.2", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "dependencies": { + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.2.0.tgz", + "integrity": "sha512-weltsSqdeWIX9G2qQZz7KlTRJdkkOCTPgLYJUz1Hacf48R4YOwGPHO3+ORfWedqJKbq5WQmsgK90n+pFLIKt/Q==", + "dev": true, + "requires": { + "estraverse": "^5.0.0" + }, + "dependencies": { + "estraverse": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.0.0.tgz", + "integrity": "sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "fileset": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-0.1.8.tgz", + "integrity": "sha1-UGuRqTluqn4y+0KoQHfHoMc2t0E=", + "dev": true, + "requires": { + "glob": "3.x", + "minimatch": "0.x" + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "foreground-child": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", + "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + } + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gaze": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.3.4.tgz", + "integrity": "sha1-X5S92gr+U7xxCWm81vKCVI1gwnk=", + "dev": true, + "requires": { + "fileset": "~0.1.5", + "minimatch": "~0.2.9" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "requires": { + "inherits": "2", + "minimatch": "0.3" + }, + "dependencies": { + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + }, + "growl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", + "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "hasha": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", + "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", + "dev": true, + "requires": { + "is-stream": "^1.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", + "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", + "dev": true + }, + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + }, + "dependencies": { + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "inquirer": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", + "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "dev": true, + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0" + } + }, + "jasmine-growl-reporter": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/jasmine-growl-reporter/-/jasmine-growl-reporter-0.0.3.tgz", + "integrity": "sha1-uHrlUeNZ0orVIXdl6u9sB7dj9sg=", + "dev": true, + "requires": { + "growl": "~1.7.0" + } + }, + "jasmine-node": { + "version": "github:montagestudio/jasmine-node#f47f43783c80434b30318e43539ded6d26e40696", + "from": "github:montagestudio/jasmine-node#master", + "dev": true, + "requires": { + "coffee-script": ">=1.0.1", + "gaze": "~0.3.2", + "jasmine-growl-reporter": "~0.0.2", + "jasmine-reporters": "~1.0.0", + "mkdirp": "~0.3.5", + "requirejs": ">=0.27.1", + "underscore": ">= 1.3.1", + "walkdir": ">= 0.0.1" + } + }, + "jasmine-reporters": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/jasmine-reporters/-/jasmine-reporters-1.0.2.tgz", + "integrity": "sha1-q2E+1Zd9x0h+hbPBL2qOqNsq3jE=", + "dev": true, + "requires": { + "mkdirp": "~0.3.5" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", + "dev": true + }, + "mocha": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.2.tgz", + "integrity": "sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==", + "dev": true, + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.3.0", + "yargs-parser": "13.1.1", + "yargs-unparser": "1.6.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nested-error-stacks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", + "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "nyc": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", + "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "caching-transform": "^3.0.2", + "convert-source-map": "^1.6.0", + "cp-file": "^6.2.0", + "find-cache-dir": "^2.1.0", + "find-up": "^3.0.0", + "foreground-child": "^1.5.6", + "glob": "^7.1.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.4", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "merge-source-map": "^1.1.0", + "resolve-from": "^4.0.0", + "rimraf": "^2.6.3", + "signal-exit": "^3.0.2", + "spawn-wrap": "^1.4.2", + "test-exclude": "^5.2.3", + "uuid": "^3.3.2", + "yargs": "^13.2.2", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "package-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", + "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^3.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "pegjs": { + "version": "git+ssh://git@github.com/pegjs/pegjs.git#b7b87ea8aeeaa1caf096e2da99fd95a971890ca1", + "from": "git+ssh://git@github.com/pegjs/pegjs.git", + "dev": true, + "requires": { + "@futagoza/eslint-config": "11.2.0", + "@types/node": "8", + "codecov": "3.6.1", + "eslint": "6.6.0", + "mocha": "6.2.2", + "nyc": "14.1.1", + "rimraf": "3.0.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "regexpp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", + "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==", + "dev": true + }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "requirejs": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz", + "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==", + "dev": true + }, + "resolve": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", + "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "run-async": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", + "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", + "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "spawn-wrap": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.3.tgz", + "integrity": "sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw==", + "dev": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mkdirp": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", + "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "string.prototype.trimend": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.0.tgz", + "integrity": "sha512-EEJnGqa/xNfIg05SxiPSqRS7S9qwDhYts1TSLR1BQfYUfPe1stofgGKvwERK9+9yf+PpfBMlpBaCHucXGPQfUA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimleft": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" + } + }, + "string.prototype.trimright": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" + } + }, + "string.prototype.trimstart": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.0.tgz", + "integrity": "sha512-iCP8g01NFYiiBOnwG1Xc3WZLyoo+RuBymwIlWncShXDDJYWN6DbnM3odslBJdgCdRlq94B5s63NWAZlcn2CS4w==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "teeny-request": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", + "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", + "dev": true, + "requires": { + "https-proxy-agent": "^2.2.1", + "node-fetch": "^2.2.0", + "uuid": "^3.3.2" + } + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "tslib": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "dev": true + }, + "underscore": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz", + "integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urlgrey": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", + "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "walkdir": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", + "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", + "dev": true + }, + "weak-map": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.5.tgz", + "integrity": "sha1-eWkVhNmGB/UHC9O3CkDmuyLkAes=" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", + "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + } + } + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + } + } + } +} diff --git a/core/frb/package.json b/core/frb/package.json new file mode 100644 index 0000000000..60219d6e6b --- /dev/null +++ b/core/frb/package.json @@ -0,0 +1,46 @@ +{ + "name": "frb", + "version": "4.0.3", + "description": "Functional reactive bindings", + "keywords": [ + "functional", + "reactive", + "bindings", + "observe", + "change" + ], + "author": "Kris Kowal (http://github.com/kriskowal/)", + "contributors": [ + "Kris Kowal (http://github.com/kriskowal/)", + "Benoit Marchant (http://github.com/marchant/)" + ], + "bugs": { + "mail": "benoit@montagestudio.com", + "url": "http://github.com/montagejs/frb/issues" + }, + "licenses": [{ + "type": "MIT", + "url": "http://github.com/montagejs/frb/raw/master/LICENSE.md" + }], + "license": "BSD-3-Clause", + "repository": { + "type": "git", + "url": "http://github.com/montagejs/frb.git" + }, + "main": "bindings", + "dependencies": { + "collections": "file:../collections" + }, + "mappings": { + "collections": "../collections" + }, + "devDependencies": { + "jasmine-node": "montagestudio/jasmine-node#master", + "@airbnb/node-memwatch": "^1.0.2", + "pegjs": "git://github.com/dmajda/pegjs.git" + }, + "scripts": { + "test": "jasmine-node spec", + "build-parser": "pegjs --allowed-start-rules expression,sheet grammar.pegjs" + } +} diff --git a/core/frb/parse-temp.js b/core/frb/parse-temp.js new file mode 100644 index 0000000000..a1471ad4bf --- /dev/null +++ b/core/frb/parse-temp.js @@ -0,0 +1 @@ +exports = require("frb/parse"); diff --git a/core/frb/parse.js b/core/frb/parse.js index a1471ad4bf..4b608b550a 100644 --- a/core/frb/parse.js +++ b/core/frb/parse.js @@ -1 +1,36 @@ -exports = require("frb/parse"); + +require("collections/shim"); +var grammar = require("./grammar"), + Map = require("collections/map"); + +var memo = new Map(); // could be Dict + +module.exports = parse; +function parse(text, options) { + var cache; + if (Array.isArray(text)) { + return { + type: "tuple", + args: text.map(function (text) { + return parse(text, options); + }) + }; + } else if (!options && (cache = memo.get(text))) { + return cache; + } else { + try { + var syntax = grammar.parse(text, options || Object.empty); + if (!options) { + memo.set(text,syntax); + } + return syntax; + } catch (error) { + error.message = ( + error.message.replace(/[\s\.]+$/, "") + " " + + " on line " + error.line + " column " + error.column + ); + throw error; + } + } +} + diff --git a/core/frb/samples/temperature-converter/package.json b/core/frb/samples/temperature-converter/package.json new file mode 100644 index 0000000000..6e59e3f9eb --- /dev/null +++ b/core/frb/samples/temperature-converter/package.json @@ -0,0 +1,10 @@ +{ + "name": "temperature-converter.reel", + "version": "0.0.0", + "dependencies": { + "frb": "*" + }, + "devDependencies": { + "mr": "*" + } +} diff --git a/core/frb/samples/temperature-converter/precision-converter.js b/core/frb/samples/temperature-converter/precision-converter.js new file mode 100644 index 0000000000..4765f322c9 --- /dev/null +++ b/core/frb/samples/temperature-converter/precision-converter.js @@ -0,0 +1,23 @@ + +module.exports = PrecisionConverter; +function PrecisionConverter(precision) { + this.precision = precision; + this.epsilon = Math.pow(.1, precision - 1); +} + +PrecisionConverter.prototype.convert = function (number) { + this.captured = number; + return number.toFixed(this.precision); +}; + +PrecisionConverter.prototype.revert = function (string) { + var number = +string; + if ( + this.captured == undefined || + Math.abs(this.captured - number) > this.epsilon + ) { + this.captured = number; + } + return this.captured; +}; + diff --git a/core/frb/samples/temperature-converter/temperature-converter.html b/core/frb/samples/temperature-converter/temperature-converter.html new file mode 100644 index 0000000000..d9a5c230c0 --- /dev/null +++ b/core/frb/samples/temperature-converter/temperature-converter.html @@ -0,0 +1,11 @@ + + + + + + +
+
+ + + diff --git a/core/frb/samples/temperature-converter/temperature-converter.js b/core/frb/samples/temperature-converter/temperature-converter.js new file mode 100644 index 0000000000..4cbfb3c831 --- /dev/null +++ b/core/frb/samples/temperature-converter/temperature-converter.js @@ -0,0 +1,34 @@ + +var Bindings = require("frb"); +var PrecisionConverter = require("./precision-converter"); + +// the DOM module adds support for property change listeners to the DOM element +// prototypes +require("frb/dom"); + +var bindings = Bindings.defineBindings({}, { + + // controller: + "fahrenheit": {"<->": "celsius * 1.8 + 32"}, + "celsius": {"<->": "kelvin - 272.15"}, + + // view: + // the + operator coerces the string to a number. + // the # operator reaches into the document property for the corresponding + // element. + "#fahrenheit.value": {"<->": "+fahrenheit", + converter: new PrecisionConverter(1) + }, + "#celsius.value": {"<->": "+celsius", + converter: new PrecisionConverter(1) + }, + "#kelvin.value": {"<->": "+kelvin", + converter: new PrecisionConverter(1) + } + +}, { + document: document +}); + +bindings.celsius = 0; + diff --git a/core/frb/scope.js b/core/frb/scope.js new file mode 100644 index 0000000000..f6c30bac24 --- /dev/null +++ b/core/frb/scope.js @@ -0,0 +1,14 @@ + +module.exports = Scope; +function Scope(value) { + this.parent = null; + this.value = value; +} + +Scope.prototype.nest = function (value) { + var child = Object.create(this); + child.value = value; + child.parent = this; + return child; +}; + diff --git a/core/frb/signal.js b/core/frb/signal.js new file mode 100644 index 0000000000..0542e9330f --- /dev/null +++ b/core/frb/signal.js @@ -0,0 +1,26 @@ + +var Map = require("collections/map"); + +module.exports = Signal; +function Signal(value) { + var emitters = new Map(); + emitters.getDefault = function () { + return 0; + }; + return { + observe: function (emit) { + emit(value); + emitters.set(emit, emitters.get(emit) + 1); + return function cancelObserver() { + emitters.set(emit, emitters.get(emit) - 1); + }; + }, + emit: function (_value) { + value = _value; + emitters.forEach(function (count, emit) { + emit(_value); + }); + } + }; +} + diff --git a/core/frb/spec/algebra-spec.js b/core/frb/spec/algebra-spec.js new file mode 100644 index 0000000000..7202398acc --- /dev/null +++ b/core/frb/spec/algebra-spec.js @@ -0,0 +1,56 @@ + +var solve = require("../algebra"); + +var specs = [ + + // !!x <- y + // x <- y + { + input: { + target: {type: "not", args: [ + {type: "not", args: [ + {type: "value"} + ]} + ]}, + source: {type: "value"} + }, + output: { + target: {type: "value"}, + source: {type: "value"} + } + }, + + // some{x} <- y + // every{!x} <- !y + { + input: { + target: {type: "someBlock", args: [ + {type: "value"}, + {type: "value"} + ]}, + source: {type: "value"} + }, + output: { + target: {type: "everyBlock", args: [ + {type: "value"}, + {type: "not", args: [ + {type: "value"} + ]} + ]}, + source: {type: "not", args: [ + {type: "value"} + ]} + } + } +] + +specs.forEach(function (spec) { + describe(JSON.stringify(spec.input), function () { + it("should simplify to " + JSON.stringify(spec.output), function () { + expect(solve(spec.input.target, spec.input.source)).toEqual([ + spec.output.target, + spec.output.source + ]); + }); + }); +}); diff --git a/core/frb/spec/assign-spec.js b/core/frb/spec/assign-spec.js new file mode 100644 index 0000000000..ab6a94d9fc --- /dev/null +++ b/core/frb/spec/assign-spec.js @@ -0,0 +1,174 @@ + +var assign = require("../assign"); +var Map = require("collections/map"); + +describe("assign", function () { + + it("should assign to a property", function () { + var object = {a: {b: {c: {}}}}; + assign(object, "a.b.c.d", 10); + expect(object.a.b.c.d).toBe(10); + expect(object).toEqual({a: {b: {c: {d: 10}}}}); + }); + + it("should be able to assign to a key of a map", function () { + var object = {map: new Map()}; + assign(object, "map.get($key)", 10, {key: 'key'}); + expect(object.map.get('key')).toBe(10); + }); + + it("should be able to assign to whether a collection has a value", function () { + var object = {array: []}; + assign(object, "array.has(1)", true); + expect(object.array).toEqual([1]); + assign(object, "array.has(1)", false); + expect(object.array).toEqual([]); + }); + + it("should be able to assign to equality", function () { + var object = {a: 10}; + assign(object, "a==20", true); + expect(object.a).toBe(20); + assign(object, "a==20", false); + expect(object.a).toBe(20); // still, since the value could be arbitrary + }); + + it("should be able to assign to consequent or alternate of a ternary operator", function () { + var object = {a: 10, b: 20}; + assign(object, "guard == 'a' ? a : b", 30); + expect(object).toEqual({a: 10, b: 20}); + object.guard = ''; + assign(object, "guard == 'a' ? a : b", 30); + expect(object.b).toBe(30); + object.guard = 'a'; + assign(object, "guard == 'a' ? a : b", 40); + expect(object.a).toBe(40); + }); + + it("should be able to assign to a logical expression", function () { + + var object = {}; + assign(object, "a && b", true); + expect(object.a).toBe(true); + expect(object.b).toBe(true); + assign(object, "a && b", false); + expect(object.a).toBe(false); + expect(object.b).toBe(true); + + var object = {}; + assign(object, "a && b", false); + expect(object.a).toBe(undefined); + expect(object.b).toBe(undefined); + + var object = {}; + assign(object, "a || b", false); + expect(object.a).toBe(false); + expect(object.b).toBe(false); + assign(object, "a || b", true); + expect(object.a).toBe(true); + expect(object.b).toBe(false); + + var object = {}; + assign(object, "a || b", true); + expect(object.a).toBe(true); + expect(object.b).toBe(undefined); + + var object = {}; + assign(object, "a || !b", true); + expect(object.a).toBe(false); + expect(object.b).toBe(undefined); + + var object = {b: true}; + assign(object, "a || !b", true); + expect(object.a).toBe(true); + expect(object.b).toBe(true); + }); + + it("should be able to assign into the content of a ranged collection", function () { + var object = {}; + assign(object, "array.rangeContent()", [1, 2, 3]); + expect(object).toEqual({}); + object.array = []; + assign(object, "array.rangeContent()", [1, 2, 3]); + expect(object.array).toEqual([1, 2, 3]); + }); + + /* + New Spec to experiment how to mutate an array via frb + add: spec is clear, append at the end + worth adding if we splice? + */ + it("should be able to add into the content of a ranged collection", function () { + var object = {}; + object.array = [0, 1, 2, 3]; + assign(object, "array.rangeAdd()", [4, 5, 6]); + expect(object.array).toEqual([0, 1, 2, 3, 4, 5, 6]); + }); + + /* + New Spec to experiment how to mutate an array via frb + remove: is a bit muddy, no primitive in js to do so, it would mean remove all occurences? + worth adding? + */ + it("should be able to remove into the content of a ranged collection", function () { + var object = {}; + object.array = [0, 1, 2, 3, 4, 5, 6]; + assign(object, "array.rangeRemove()", [4, 5, 6]); + expect(object.array).toEqual([0, 1, 2, 3]); + }); + + /* + New Spec to experiment how to mutate an array via frb + splice method in JS Array: + let arrDeletedItems = array.splice(start[, deleteCount[, item1[, item2[, ...]]]]) + is the most efficient primitive available as it does deletion and addition in one call. + + when start is 0, and deleteCount is 0, it's basically equivallent to the current rangeContent(). + So, it might be easier to add splice and make rangeContent() work on top of it. + rangeContent() take as argument the whole scope, so for a splice, the scope needs to contain, start, deleteCount + */ + it("should be able to splice into the content of a ranged collection", function () { + var object = {}; + object.array = [0, 1, 2, 3, 4, 5, 6]; + assign(object, "array.rangeSplice()", [2 /*start*/, 3 /*deleteCount*/, 7, 8, 9]); + expect(object.array).toEqual([0, 1, 2, 3, 6, 7, 8, 9]); + }); + + + it("should be able to assign into the content of a mapped array", function () { + var object = {}; + assign(object, "array.mapContent()", [1, 2, 3]); + expect(object).toEqual({}); + object.array = []; + assign(object, "array.mapContent()", [1, 2, 3]); + expect(object.array).toEqual([1, 2, 3]); + }); + + it("should be able to assign into the content of a map", function () { + var object = {}; + assign(object, "map.mapContent()", Map.from({a: 10})); + expect(object).toEqual({}); + object.map = new Map(); + assign(object, "map.mapContent()", Map.from({a: 10, b: 20})); + expect(object.map.toObject()).toEqual({a: 10, b: 20}); + }); + + it("should be able to assign in reverse order", function () { + var object = {array: []}; + assign(object, "array.reversed()", [1, 2, 3]); + expect(object.array).toEqual([3, 2, 1]); + }); + + it("should assign to every value in a collection", function () { + var options = [{}, {}, {}]; + assign(options, "every{checked}", true); + expect(options.every(function (option) { + return option.checked; + })).toBe(true); + assign(options, "every{checked}", false); + expect(options.every(function (option) { + return option.checked; // still + })).toBe(true); + }); + +}); diff --git a/core/frb/spec/bind-defined-spec.js b/core/frb/spec/bind-defined-spec.js new file mode 100644 index 0000000000..2ecb779ea5 --- /dev/null +++ b/core/frb/spec/bind-defined-spec.js @@ -0,0 +1,23 @@ + +var Bindings = require(".."); + +describe("defined binding", function () { + + it("should bind defined", function () { + var object = Bindings.defineBindings({ + }, { + "defined": { + "<->": "property.defined()" + } + }); + expect(object.property).toBe(undefined); + + object.property = 10; + expect(object.property).toBe(10); + expect(object.defined).toBe(true); + + object.defined = false; + expect(object.property).toBe(undefined); + }); + +}); diff --git a/core/frb/spec/bind-null-spec.js b/core/frb/spec/bind-null-spec.js new file mode 100644 index 0000000000..fc7de8ae57 --- /dev/null +++ b/core/frb/spec/bind-null-spec.js @@ -0,0 +1,59 @@ + +var Bindings = require("../bindings"); + +describe("binding to null", function () { + var object; + + it("should declare binding", function () { + object = Bindings.defineBinding({}, "left", { + "<->": "condition ? right : null" + }); + }); + + it("when false, no binding", function () { + object.left = 10; + object.right = 20; + expect(object.left).toBe(10); + expect(object.right).toBe(20); + }); + + it("when becomes true, propagates <-", function () { + object.condition = true; + expect(object.left).toBe(20); + expect(object.right).toBe(20); + }); + + + it("when true, propagates ->", function () { + object.left = 30; + expect(object.left).toBe(30); + expect(object.right).toBe(30); + }); + + it("when becomes false, <- propagates null", function () { + object.condition = false; + expect(object.left).toBe(null); + expect(object.right).toBe(30); + }); + + it("when becomes true, propagages <- again", function () { + object.right = 40; + object.condition = true; + expect(object.left).toBe(40); + expect(object.right).toBe(40); + }); + + it("when true, propagates <-", function () { + object.right = 50; + expect(object.left).toBe(50); + expect(object.right).toBe(50); + }); + + it("when true, propagates -> (again)", function () { + object.left = 60; + expect(object.left).toBe(60); + expect(object.right).toBe(60); + }); + +}); + diff --git a/core/frb/spec/bind-spec.js b/core/frb/spec/bind-spec.js new file mode 100644 index 0000000000..cfe476f804 --- /dev/null +++ b/core/frb/spec/bind-spec.js @@ -0,0 +1,617 @@ + +var bind = require("../bind"); +var SortedSet = require("collections/sorted-set"); +var Map = require("collections/map"); + +Error.stackTraceLimit = 100; + +describe("bind", function () { + + describe("<-", function () { + var source = {foo: {bar: {baz: 10}}}; + var target = {foo: {bar: {baz: undefined}}}; + + var cancel = bind(target, "foo.bar.baz", { + "<-": "foo.bar.baz", + "source": source + }); + + it("initial", function () { + expect(source.foo.bar.baz).toEqual(10); + expect(target.foo.bar.baz).toEqual(10); + }); + + }); + + describe("<->", function () { + + var object = {bar: 10}; + object.self = object; + + var cancel = bind(object, "self.foo", {"<->": "self.bar"}); + + it("initial", function () { + expect(object.foo).toBe(10); + expect(object.bar).toBe(10); + }); + + it("<-", function () { + object.bar = 20; + expect(object.foo).toBe(20); + expect(object.bar).toBe(20); + }); + + it("->", function () { + object.foo = 30; + expect(object.foo).toBe(30); + expect(object.bar).toBe(30); + }); + + it("cancel", function () { + cancel(); + expect(object.foo).toBe(30); + expect(object.bar).toBe(30); + }); + + it("unbound after cancel", function () { + object.foo = 10; + expect(object.bar).toBe(30); + }); + + }); + + describe("sum", function () { + var object = {values: [1,2,3]}; + var cancel = bind(object, "sum", {"<-": "values.sum{}"}); + expect(object.sum).toBe(6); + object.values.push(4); + expect(object.sum).toBe(10); + cancel(); + object.values.unshift(); + expect(object.sum).toBe(10); + }); + + describe("average", function () { + var object = {values: [1,2,3]}; + var cancel = bind(object, "average", {"<-": "values.average{}"}); + expect(object.average).toBe(2); + object.values.push(4); + expect(object.average).toBe(2.5); + cancel(); + object.values.unshift(); + expect(object.average).toBe(2.5); + }); + + describe("content", function () { + var foo = [1, 2, 3]; + var bar = []; + var object = {foo: foo, bar: bar}; + var cancel = bind(object, "bar.rangeContent()", {"<->": "foo.rangeContent()"}); + expect(object.bar.slice()).toEqual([1, 2, 3]); + foo.push(4); + bar.push(5); + expect(object.foo.slice()).toEqual([1, 2, 3, 4, 5]); + expect(object.bar.slice()).toEqual([1, 2, 3, 4, 5]); + expect(object.foo).toBe(foo); + expect(object.bar).toBe(bar); + }); + + describe("reversed", function () { + var object = {foo: [1,2,3]}; + var cancel = bind(object, "bar", {"<-": "foo.reversed{}"}); + expect(object.bar).toEqual([3, 2, 1]); + object.foo.push(4); + expect(object.bar).toEqual([4, 3, 2, 1]); + object.foo.swap(2, 0, ['a', 'b', 'c']); + expect(object.bar).toEqual([4, 3, 'c', 'b', 'a', 2, 1]); + cancel(); + object.foo.splice(2, 3); + expect(object.bar).toEqual([4, 3, 'c', 'b', 'a', 2, 1]); + }); + + describe("reversed left hand side", function () { + var object = {foo: [1,2,3]}; + var cancel = bind(object, "bar", {"<->": "foo.reversed()"}); + // object.bar has to be sliced since observable arrays are not + // equal to plain arrays in jasmine, because of a differing + // prototype + expect(object.bar.slice()).toEqual([3, 2, 1]); + object.foo.push(4); + expect(object.bar.slice()).toEqual([4, 3, 2, 1]); + object.foo.swap(2, 0, ['a', 'b', 'c']); + expect(object.bar.slice()).toEqual([4, 3, 'c', 'b', 'a', 2, 1]); + object.bar.pop(); + expect(object.bar.slice()).toEqual([4, 3, 'c', 'b', 'a', 2]); + expect(object.foo.slice()).toEqual([2, 'a', 'b', 'c', 3, 4]); + cancel(); + object.foo.splice(2, 3); + expect(object.bar.slice()).toEqual([4, 3, 'c', 'b', 'a', 2]); + }); + + describe("tuple", function () { + var object = {a: 10, b: 20, c: 30}; + var cancel = bind(object, "d", {"<-": "[a, b, c]"}); + expect(object.d).toEqual([10, 20, 30]); + cancel(); + object.c = 40; + expect(object.d).toEqual([10, 20, 30]); + }); + + describe("record", function () { + var object = {foo: 10, bar: 20}; + var cancel = bind(object, "record", {"<-": "{a: foo, b: bar}"}); + expect(object.record).toEqual({a: 10, b: 20}); + object.foo = 20; + expect(object.record).toEqual({a: 20, b: 20}); + cancel(); + object.foo = 10; + expect(object.record).toEqual({a: 20, b: 20}); + }); + + describe("record map", function () { + var object = {arrays: [[1, 2, 3], [4, 5, 6]]}; + var cancel = bind(object, "summaries", { + "<-": "arrays.map{{length: length, sum: sum()}}" + }); + expect(object.summaries).toEqual([ + {length: 3, sum: 6}, + {length: 3, sum: 15} + ]); + object.arrays.pop(); + expect(object.summaries).toEqual([ + {length: 3, sum: 6} + ]); + object.arrays[0].push(4); + expect(object.summaries).toEqual([ + {length: 4, sum: 10} + ]); + }); + + describe("literals", function () { + var object = {}; + var cancel = bind(object, "literals", {"<-": "[0, 'foo bar']"}); + expect(object.literals).toEqual([0, "foo bar"]); + }); + + describe("has", function () { + var object = {set: [1, 2, 3], sought: 2}; + var cancel = bind(object, "has", {"<-": "set.has(sought)"}); + + expect(object.has).toBe(true); + expect(object.set.slice()).toEqual([1, 2, 3]); + + object.sought = 4; + expect(object.has).toBe(false); + expect(object.set.slice()).toEqual([1, 2, 3]); + + object.set.push(4); + expect(object.has).toBe(true); + expect(object.set.slice()).toEqual([1, 2, 3, 4]); + + object.set.pop(); + expect(object.has).toBe(false); + expect(object.set.slice()).toEqual([1, 2, 3]); + + cancel(); + object.set.push(4); + expect(object.has).toBe(false); + expect(object.set.slice()).toEqual([1, 2, 3, 4]); + }); + + describe("has <-", function () { + var object = {set: [1, 2, 3], sought: 2}; + var cancel = bind(object, "has", {"<->": "set.has(sought)"}); + + expect(object.set.slice()).toEqual([1, 2, 3]); + object.has = false; + expect(object.set.slice()).toEqual([1, 3]); + object.set.push(2); + expect(object.has).toEqual(true); + expect(object.set.slice()).toEqual([1, 3, 2]); + + //Benoit + //Todo: Add rangeAdd() to add the value of the right expression + //Todo: Add rangeRemove() to remove the value of the right expression + var target = {toMany:[0]}; + var cancel2 = bind(target, "toMany.rangeContent()", { + "<-": "", + "source": [1, 2, 3] + }); + expect(target.toMany).toEqual([1, 2, 3]); + + + }); + + describe("map", function () { + var object = { + foo: [{bar: 10}, {bar: 20}, {bar: 30}] + }; + var cancel = bind(object, "baz", { + "<-": "foo.map{bar}" + }); + expect(object.baz).toEqual([10, 20, 30]); + object.foo.push({bar: 40}); + expect(object.baz).toEqual([10, 20, 30, 40]); + }); + + describe("filter", function () { + var object = { + foo: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + }; + var cancel = bind(object, "bar", { + "<-": "foo.filter{!(%2)}" + }); + expect(object.bar).toEqual([2, 4, 6, 8, 10]); + }); + + describe("flatten", function () { + var object = { + foo: [[1], [2, 3], [4]] + }; + var cancel = bind(object, "baz", { + "<-": "foo.flatten{}" + }); + expect(object.baz).toEqual([1, 2, 3, 4]); + + object.foo.push([]); + expect(object.baz).toEqual([1, 2, 3, 4]); + + object.foo.push([5, 6]); + expect(object.baz).toEqual([1, 2, 3, 4, 5, 6]); + + object.foo[0].unshift(0); + expect(object.baz).toEqual([0, 1, 2, 3, 4, 5, 6]); + + expect(object.foo[1].slice()).toEqual([2, 3]); + object.foo.splice(1, 1); + expect(object.baz).toEqual([0, 1, 4, 5, 6]); + + cancel(); + object.foo.pop(); + expect(object.baz).toEqual([0, 1, 4, 5, 6]); + }); + + describe("flatten map", function () { + var object = { + foo: [{bar: [1]}, {bar: [2, 3]}, {bar: [4]}] + }; + var cancel = bind(object, "baz", { + "<-": "foo.flatten{bar}" + }); + expect(object.baz).toEqual([1, 2, 3, 4]); + + object.foo.push({bar: []}); + expect(object.baz).toEqual([1, 2, 3, 4]); + + object.foo.push({bar: [5, 6]}); + expect(object.baz).toEqual([1, 2, 3, 4, 5, 6]); + + object.foo[0].bar.unshift(0); + expect(object.baz).toEqual([0, 1, 2, 3, 4, 5, 6]); + + expect(object.foo[1].bar.slice()).toEqual([2, 3]); + object.foo.splice(1, 1); + expect(object.baz).toEqual([0, 1, 4, 5, 6]); + + cancel(); + object.foo.pop(); + expect(object.baz).toEqual([0, 1, 4, 5, 6]); + }); + + describe("tree replacement", function () { + var object = {qux: 10, foo: {bar: {baz: null}}}; + var cancel = bind(object, "foo.bar.baz", {"<->": "qux"}); + expect(object.foo.bar.baz).toEqual(10); + object.foo = {bar: {baz: null}}; // gets overwritten by binder + // (source to target precedes target to source) // TODO consider alts + expect(object.foo.bar.baz).toEqual(10); + object.qux = 20; + expect(object.foo.bar.baz).toEqual(20); + object.foo.bar.baz = 30; + expect(object.qux).toEqual(30); + }); + + describe("parameters", function () { + var object = {}; + var parameters = {a: 10, b: 20, c: 30}; + var source = [1, 2, 3]; + var cancel = bind(object, "foo", { + "<-": "[$a, $b, map{$c}]", + parameters: parameters, + source: source + }); + expect(object.foo).toEqual([10, 20, [30, 30, 30]]); + parameters.a = 0; + expect(object.foo).toEqual([0, 20, [30, 30, 30]]); + source.push(4); + expect(object.foo).toEqual([0, 20, [30, 30, 30, 30]]); + }); + + describe("equality and addition", function () { + var object = {a: 2, b: 1, c: 1}; + var cancel = bind(object, "d", {"<->": "a == b + c"}); + expect(object.d).toEqual(true); + object.a = 3; + expect(object.d).toEqual(false); + object.b = 2; + expect(object.d).toEqual(true); + object.c = 2; + expect(object.d).toEqual(false); + expect(object.a).toEqual(4); + object.d = true; + expect(object.a).toEqual(4); + }); + + describe("two-way negation", function () { + var object = {}; + + bind(object, "a", {"<->": "!b"}); + expect(object.a).toBe(true); + object.b = false; + expect(object.a).toBe(true); + expect(object.b).toBe(false); + + object.b = true; + expect(object.a).toBe(false); + object.b = false; + expect(object.a).toBe(true); + + object.a = false; + expect(object.b).toBe(true); + object.a = true; + expect(object.b).toBe(false); + }); + + describe("equality and assignment", function () { + var object = {choice: 2, a: 2, b: 3}; + bind(object, "isA", {"<->": "!isB"}); + bind(object, "choice == a", {"<->": "isA"}); + bind(object, "choice == b", {"<->": "isB"}); + + expect(object.choice).toBe(2); + + object.isB = true; + expect(object.isB).toBe(true); + expect(object.isA).toBe(false); + expect(object.choice).toBe(3); + + object.b = 4; + expect(object.isB).toBe(true); + expect(object.isA).toBe(false); + expect(object.choice).toBe(4); + + object.isB = true; + expect(object.choice).toBe(4); + + object.isA = true; + expect(object.choice).toBe(2); + + object.isA = false; + expect(object.isB).toBe(true); + expect(object.choice).toBe(4); + }); + + describe("gt", function () { + var object = {a: 1, b: 2}; + bind(object, "gt", {"<-": "a > b"}); + expect(object.gt).toBe(false); + object.b = 0; + expect(object.gt).toBe(true); + }); + + describe("algebra", function () { + var object = {}; + bind(object, "result", {"<-": "2 ** 3 * 3 + 7"}); + expect(object.result).toBe(Math.pow(2, 3) * 3 + 7); + }); + + describe("logic", function () { + var object = {a: false, b: false}; + bind(object, "result", {"<-": "a || b"}); + expect(object.result).toBe(false); + object.a = true; + expect(object.result).toBe(true); + object.b = true; + expect(object.result).toBe(true); + object.a = false; + object.b = false; + expect(object.result).toBe(false); + }); + + describe("convert, revert", function () { + var object = {a: 10}; + var cancel = bind(object, "b", { + "<->": "a", + convert: function (a) { + return a + 1; + }, + revert: function (b) { + return b - 1; + } + }); + expect(object.b).toEqual(11); + object.b = 12; + expect(object.a).toEqual(11); + cancel(); + object.a = 1000; + expect(object.b).toEqual(12); + }); + + describe("add <-> sub", function () { + var object = {a: 10}; + var cancel = bind(object, "b", { + "<->": "a + 1" + }); + expect(object.b).toEqual(11); + object.b = 12; + expect(object.a).toEqual(11); + cancel(); + object.a = 1000; + expect(object.b).toEqual(12); + }); + + describe("pow <-> log", function () { + var object = {a: 2, b: 3}; + var cancel = bind(object, "c", { + "<->": "a ** b" + }); + expect(object.c).toEqual(8); + object.c = 27; + expect(object.a).toEqual(3); + object.b = 2; + expect(object.c).toEqual(9); + }); + + describe("converter", function () { + var object = {a: 10}; + var cancel = bind(object, "b", { + "<->": "a", + converter: { + convert: function (a) { + return a + 1; + }, + revert: function (b) { + return b - 1; + } + } + }); + expect(object.b).toEqual(11); // 12 + object.b = 12; + expect(object.a).toEqual(11); + cancel(); + object.a = 1000; + expect(object.b).toEqual(12); + }); + + describe("content binding from sorted set", function () { + var array = ['a', 'c', 'b']; + var set = SortedSet([4, 5, 1, 3, 45, 1, 8]); + var cancel = bind(array, "rangeContent()", {"<-": "", source: set}); + expect(array.slice()).toEqual([1, 3, 4, 5, 8, 45]); + set.add(2); + expect(array.slice()).toEqual([1, 2, 3, 4, 5, 8, 45]); + set.delete(45); + expect(array.slice()).toEqual([1, 2, 3, 4, 5, 8]); + }); + + describe("view of a array", function () { + var source = { + content: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + index: 2, + length: 3 + }; + var target = []; + var cancel = bind(target, "rangeContent()", { + "<-": "content.view(index, length)", + source: source + }); + expect(target.slice()).toEqual([2, 3, 4]); + source.content.shift(); + expect(target.slice()).toEqual([3, 4, 5]); + source.length = 2; + expect(target.slice()).toEqual([3, 4]); + source.index = 3; + expect(target.slice()).toEqual([4, 5]); + source.content.unshift(0); + expect(target.slice()).toEqual([3, 4]); + }); + + describe("view of a sorted set", function () { + var array = ['a', 'c', 'b']; + var set = SortedSet([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + var source = { + set: set, + index: 2, + length: 3 + }; + var cancel = bind(array, "rangeContent()", { + "<-": "set.view(index, length)", + source: source + }); + expect(array.slice()).toEqual([2, 3, 4]); + // remove before the view + set.shift(); + expect(array.slice()).toEqual([3, 4, 5]); + // change view length + source.length = 2; + expect(array.slice()).toEqual([3, 4]); + // change view index + source.index = 3; + expect(array.slice()).toEqual([4, 5]); + // add before the view + set.unshift(0); + expect(array.slice()).toEqual([3, 4]); + // change view length (again) + source.length = 4; + expect(array.slice()).toEqual([3, 4, 5, 6]); + // remove within + set.delete(4); + expect(array.slice()).toEqual([3, 5, 6, 7]); + // add within + set.add(4); + expect(array.slice()).toEqual([3, 4, 5, 6]); + // add after + set.add(11); + expect(array.slice()).toEqual([3, 4, 5, 6]); + // remove after + set.delete(7); + expect(array.slice()).toEqual([3, 4, 5, 6]); + }); + + it("should bind a mapped key", function () { + var array = [1, 2, 3]; + var target = {}; + var cancel = bind(target, "second", {"<-": "get(1)", source: array}); + expect(target.second).toBe(2); // [1, 2, 3] + array.shift(); + expect(target.second).toBe(3); // [2, 3] + array.splice(1, 0, 2.5); + expect(target.second).toBe(2.5); // [2, 2.5, 3] + cancel(); + array.clear(); + expect(target.second).toBe(2.5); // [2, 2.5, 3] + }); + + it("should bind map content to array content", function () { + var array = []; + var map = new Map([[0, 1]]); + var cancel = bind(array, "mapContent()", {"<-": "", source: map}); + expect(array).toEqual([1]); + map.set(1, 2); + expect(array).toEqual([1, 2]); + map.delete(0); + expect(array).toEqual([2]); + cancel(); + map.set(2, 3); + expect(array).toEqual([2]); + }); + + it("should bind map content to array content", function () { + var array = []; + var map = new Map([[0, 1]]); + var cancel = bind(array, "mapContent()", {"<-": "", source: map}); + expect(array).toEqual([1]); + map.set(1, 2); + expect(array).toEqual([1, 2]); + map.delete(0); + expect(array).toEqual([2]); + cancel(); + map.set(2, 3); + expect(array).toEqual([2]); + }); + + it("should bind array to map content", function () { + var map = new Map(); + var array = [1]; + var cancel = bind(map, "mapContent()", {"<-": "", source: array}); + expect(map.toObject()).toEqual({0: 1}); + array.push(2); + expect(map.toObject()).toEqual({0: 1, 1: 2}); + array.splice(0, 1); + expect(map.toObject()).toEqual({0: 2}); + cancel(); + array.push(3); + expect(map.toObject()).toEqual({0: 2}); + }); + +}); diff --git a/core/frb/spec/binders-spec.js b/core/frb/spec/binders-spec.js new file mode 100644 index 0000000000..90d971a267 --- /dev/null +++ b/core/frb/spec/binders-spec.js @@ -0,0 +1,65 @@ + +var Binders = require("../binders"); +var Bindings = require("../bindings"); +var Observers = require("../observers"); +var Scope = require("../scope"); + +describe("makePropertyBinder", function () { + it("should work", function () { + var source = {a: 10}; + var target = {}; + var bind = Binders.makePropertyBinder( + Observers.observeValue, + Observers.makeLiteralObserver("a") + ); + var observe = Observers.makePropertyObserver( + Observers.observeValue, + Observers.makeLiteralObserver("a") + ); + var cancel = bind( + observe, + new Scope(source), + new Scope(target), + {} + ); + expect(target.a).toEqual(source.a); + }); +}); + +describe("makeOneBinder", function () { + it("should work", function () { + var o = Bindings.defineBindings({ + items: [1, 2, 3], + }, { + item: { "<->": "items.one()" } + }); + + expect(o.items).toEqual([1, 2, 3]); + expect(o.item).toBe(1); + + o.item = 3; + expect(o.items).toEqual([3]); + + }); +}); + +describe("makeRangeContentBinder", function () { + it("should work when replacing", function () { + var o = Bindings.defineBindings({ + target: [1, 2, 3] + }, { + source: {"<->": "target.rangeContent()"} + }); + + expect(o.source).toEqual([1, 2, 3]); + + var oldSource = o.source; + o.source = [2, 3]; + expect(oldSource).toEqual([1, 2, 3]); + expect(o.target).toEqual([2, 3]); + + o.target.splice(0, 3, 1, 2); + expect(oldSource).toEqual([1, 2, 3]); + + }); +}); diff --git a/core/frb/spec/bindings-spec.js b/core/frb/spec/bindings-spec.js new file mode 100644 index 0000000000..66589a3b68 --- /dev/null +++ b/core/frb/spec/bindings-spec.js @@ -0,0 +1,519 @@ + +var Bindings = require(".."); +var Map = require("collections/map"); + +Error.stackTraceLimit = 100; + +describe("bindings", function () { + + describe("computed properties", function () { + + describe("string", function () { + it("should propagate related bindings", function () { + + var object = Bindings.defineBindings({ + foo: 10, + bar: 20 + }, { + baz: { + args: ["foo", "bar"], + compute: function (foo, bar) { + return foo + bar; + } + }, + qux: { + "<-": "baz" + } + }); + + expect(object.qux).toEqual(30); + + object.bar = 30; + expect(object.qux).toEqual(40); + + }); + }); + + describe("array", function () { + + it("should propagate related bindings", function () { + + var object = Bindings.defineBindings({ + foo: 10, + bar: 20 + }, { + baz: { + args: ["foo", "bar"], + compute: function (foo, bar) { + return foo + bar; + } + }, + qux: { + "<-": "baz" + } + }); + + expect(object.qux).toEqual(30); + + object.bar = 30; + expect(object.qux).toEqual(40); + + }); + + }); + + }); + + describe("exclusive options", function () { + + it("should work", function () { + + var bindings = Bindings.defineBindings({ + options: [], + off: true, + on: false + }, { + + "!options.has('feature')": { + "<->": "off" + }, + "options.has('feature')": { + "<->": "on" + } + }); + + expect(bindings.options.slice()).toEqual([]); + + bindings.on = true; + expect(bindings.options.slice()).toEqual(['feature']); + bindings.off = true; + expect(bindings.options.slice()).toEqual([]); + + }); + + it("should work", function () { + + var bindings = Bindings.defineBindings({ + options: [], + off: true, + on: false + }, { + "options.has('feature')": { + "<-": "!off" + }, + "options.has('feature')": { + "<-": "on" + }, + "on": {"<->": "!off"} + }); + + expect(bindings.options.slice()).toEqual([]); + + bindings.on = true; + expect(bindings.options.slice()).toEqual(['feature']); + bindings.off = true; + expect(bindings.options.slice()).toEqual([]); + + }); + + }); + + it("should not update an active property", function () { + + var bindings = Bindings.defineBindings({}, { + "output": {"<->": "input", + convert: function (value) { + return Number(value).toFixed(1); + }, + revert: function (value) { + return Number(value).toFixed(1); + } + } + }); + + bindings.input = "0"; + expect(bindings.input).toEqual("0"); + expect(bindings.output).toEqual("0.0"); + + bindings.input = "1"; + expect(bindings.input).toEqual("1"); + expect(bindings.output).toEqual("1.0"); + + }); + + it("should bind elements by id", function () { + var elements = { + foo: {checked: true} + }; + var bindings = Bindings.defineBindings({}, { + "bar": {"<->": "#foo.checked"} + }, { + document: { + getElementById: function (id) { + return elements[id]; + } + } + }); + expect(bindings.bar).toBe(true); + }); + + it("should bind components by label", function () { + var components = { + foo: {checked: true} + }; + var bindings = Bindings.defineBindings({}, { + "bar": { + "<->": "@foo.checked", + components: { + getObjectByLabel: function (label) { + return components[label]; + } + } + } + }); + expect(bindings.bar).toBe(true); + }); + + it("should sort by relation", function () { + var bindings = Bindings.defineBindings({ + objects: [{foo: 10}, {foo: 30}, {foo: 20}] + }, { + sorted: {"<-": "objects.sorted{foo}"} + }); + expect(bindings.sorted).toEqual([ + {foo: 10}, + {foo: 20}, + {foo: 30} + ]); + bindings.objects.unshift({foo: 40}); + expect(bindings.sorted).toEqual([ + {foo: 10}, + {foo: 20}, + {foo: 30}, + {foo: 40} + ]); + }); + + it("should handle an every block", function () { + var object = Bindings.defineBindings({ + array: [1, 2, 3, 4, 5] + }, { + everyGreaterThanZero: { + "<-": "array.every{>0}" + } + }); + expect(object.everyGreaterThanZero).toBe(true); + + object.array.unshift(0); + expect(object.everyGreaterThanZero).toBe(false); + + Bindings.cancelBindings(object); + object.array.shift(); + expect(object.everyGreaterThanZero).toBe(false); + }); + + it("should handle a some block", function () { + var object = Bindings.defineBindings({ + array: [1, 2, 3, 4, 5] + }, { + someEqualZero: { + "<-": "array.some{==0}" + } + }); + expect(object.someEqualZero).toBe(false); + + object.array.unshift(0); + expect(object.someEqualZero).toBe(true); + + object.array.shift(); + expect(object.someEqualZero).toBe(false); + + Bindings.cancelBindings(object); + object.array.unshift(0); + expect(object.someEqualZero).toBe(false); + }); + + it("should observe undefined when an array retreats behind an observed index", function () { + var object = Bindings.defineBindings({ + bar: ["a", "b", "c"] + }, { + foo: {"<-": "bar.2"} + }); + object.bar.pop(); + expect(object.foo).toBe(undefined); + expect(object.bar.length).toBe(2); + }); + + it("should understand undefined values in a some block", function () { + var object = Bindings.defineBindings({ + array: [] + }, { + some: {"<-": "array.some{a.b}"} + }); + expect(object.some).toBe(false); + object.array.push({a: {b: 1}}); + expect(object.some).toBe(true); + object.array.set(0, {a: null}); + expect(object.some).toBe(false); + }); + + it("should bind a property chain including a numeric property", function () { + var object = Bindings.defineBindings({ + }, { + "baz": {"<-": "foo.0.bar"} + }); + expect(object.baz).toBe(undefined); + object.foo = [{bar: 1}]; + expect(object.baz).toBe(1); + }); + + it("should handle bidirectional string to number bindings", function () { + var object = Bindings.defineBindings({ + }, { + "+n": {"<->": "'' + s"} + }); + expect(object.n).toBe(undefined); + expect(object.s).toBe(undefined); + + object.n = 10; + expect(object.s).toBe("10"); + + object.s = "20"; + expect(object.n).toBe(20); + + object.n = undefined; + expect(object.s).toBe(undefined); + }); + + it("should bind to a key in a map", function () { + var object = {one: 1, two: 2}; + var map = new Map(); + + Bindings.defineBinding(map, "get('one')", { + "<-": "one", + source: object + }); + + expect(map.get('one')).toBe(1); + object.one = 0; + expect(map.get('one')).toBe(0); + }); + + it("should bind object literals to maps", function () { + + var object = Bindings.defineBinding({}, "map", { + "<-": "object.toMap()" + }); + expect(object.map.toObject()).toEqual({}); + + object.object = {a: 10}; + expect(object.map.toObject()).toEqual({a: 10}); + + object.object.a = 20; + expect(object.map.toObject()).toEqual({a: 20}); + + object.object.b = 30; // not observable + expect(object.map.toObject()).toEqual({a: 20}); + + object.object = {a: 20, b: 30}; + expect(object.map.toObject()).toEqual({a: 20, b: 30}); + + }); + + it("should watch variable property keys", function () { + + var object = Bindings.defineBinding({}, "value", { + "<-": "property(property)" + }); + expect(object.value).toBe(undefined); + + object.property = 'a'; + expect(object.value).toBe(undefined); + + object.a = 10; + expect(object.value).toBe(10); + + object.property = 'b'; + expect(object.value).toBe(undefined); + + object.b = 20; + expect(object.value).toBe(20); + + object.property = 'a'; + expect(object.value).toBe(10); + + }); + + it("should watch arbitrary pure polymorphic functions", function () { + var object = Bindings.defineBindings({ + distance: function (x, y) { + return Math.pow(x * x + y * y, .5); + } + }, { + "z": { + "<-": "distance(x, y)" + } + }); + expect(object.z).toBe(undefined); + object.x = 3; + object.y = 4; + expect(object.z).toBe(5); + object.y = 3; + expect(object.z).not.toBe(5); + object.x = 4; + expect(object.z).toBe(5); + }); + + it("should watch arbitrary observer functions", function () { + + var tick; + + var object = Bindings.defineBindings({ + observeFoo: function (emit, source, parameters, beforeChange) { + tick = emit; + } + }, { + bar: {"<-": "foo()"} + }); + + expect(object.bar).toBe(undefined); + tick(10); + expect(object.bar).toBe(10); + + }); + + it("should watch arbitrary make observer functions", function () { + + var object = Bindings.defineBindings({ + makeSpecialAddObserver: function (observeA, observeB) { + return function observeSpecialAdd(emit, source, parameters) { + return observeA(function (a) { + if (a == null) return emit(null); + return observeB(function (b) { + if (b == null) return emit(null); + return emit(a + b); + }, source, parameters); + }, source, parameters); + }; + } + }, { + c: {"<-": "specialAdd(a, b)"} + }); + + expect(object.c).toBe(null); + object.a = 2; + expect(object.c).toBe(null); + object.b = 3; + expect(object.c).toBe(5); + + }); + + it("should recognize the parent scope operator", function () { + var object = Bindings.defineBindings({ + array: [1, 2, 3, 4], + factor: 2 + }, { + factors: { + "<-": "array.map{* ^factor}" + } + }); + expect(object.factors).toEqual([2, 4, 6, 8]); + object.factor = 1; + expect(object.factors).toEqual([1, 2, 3, 4]); + }); + + it("should do one-way all and none checked buttons with every block", function () { + var a = {}, b = {}, c = {}; + var model = Bindings.defineBindings({ + items: [a, b, c] + }, { + allChecked: {"<-": "items.every{checked}"}, + noneChecked: {"<-": "items.every{!checked}"} + }); + + expect(model.allChecked).toBe(false); + expect(model.noneChecked).toBe(true); + + a.checked = true; + expect(model.allChecked).toBe(false); + expect(model.noneChecked).toBe(false); + + b.checked = true; + c.checked = true; + expect(model.allChecked).toBe(true); + expect(model.noneChecked).toBe(false); + + }); + + it("should do one-way all and none checked buttons with some block", function () { + var a = {}, b = {}, c = {}; + var model = Bindings.defineBindings({ + items: [a, b, c] + }, { + allChecked: {"<-": "!items.some{!checked}"}, + noneChecked: {"<-": "!items.some{checked}"} + }); + + expect(model.allChecked).toBe(false); + expect(model.noneChecked).toBe(true); + + a.checked = true; + expect(model.allChecked).toBe(false); + expect(model.noneChecked).toBe(false); + + b.checked = true; + c.checked = true; + expect(model.allChecked).toBe(true); + expect(model.noneChecked).toBe(false); + + }); + + it("should do two-way all and none checked buttons with every block", function () { + var a = {}, b = {}, c = {}; + var model = Bindings.defineBindings({ + items: [a, b, c] + }, { + allChecked: {"<->": "items.every{checked}"}, + noneChecked: {"<->": "items.every{!checked}"} + }); + + model.allChecked = true; + expect(a.checked).toBe(true); + expect(b.checked).toBe(true); + expect(c.checked).toBe(true); + expect(model.noneChecked).toBe(false); + + b.checked = false; + expect(model.allChecked).toBe(false); + + model.noneChecked = true; + expect(a.checked).toBe(false); + expect(b.checked).toBe(false); + expect(c.checked).toBe(false); + }); + + it("should do two-way all and none checked buttons with some block", function () { + var a = {}, b = {}, c = {}; + var model = Bindings.defineBindings({ + items: [a, b, c] + }, { + allChecked: {"<->": "!items.some{!checked}"}, + noneChecked: {"<->": "!items.some{checked}"} + }); + + model.allChecked = true; + expect(a.checked).toBe(true); + expect(b.checked).toBe(true); + expect(c.checked).toBe(true); + expect(model.noneChecked).toBe(false); + + b.checked = false; + expect(model.allChecked).toBe(false); + + model.noneChecked = true; + expect(a.checked).toBe(false); + expect(b.checked).toBe(false); + expect(c.checked).toBe(false); + }); + +}); diff --git a/core/frb/spec/complex-spec.js b/core/frb/spec/complex-spec.js new file mode 100644 index 0000000000..003c2c78a8 --- /dev/null +++ b/core/frb/spec/complex-spec.js @@ -0,0 +1,71 @@ + +var Bindings = require(".."); + +describe("complex binding chain", function () { + + it("should work", function () { + + var trace = false; + + var ui = Bindings.defineBindings({ + apples: {checked: true}, + oranges: {checked: false}, + selected: "apples", + fruit: {classList: [], value: "apples"}, + reflectApples: {checked: true} + }, { + "apples.checked": {"<->": "selected == 'apples'", trace: trace}, + "oranges.checked": {"<->": "selected == 'oranges'", trace: trace}, + "!apples.checked": {"<->": "oranges.checked", trace: trace}, + "selected": {"<->": "fruit.value", trace: trace}, + "reflectApples.checked": {"<->": "selected == 'apples'", trace: trace}, + "!reflectApples.checked": {"<->": "selected == 'oranges'", trace: trace}, + }); + + function expectApples() { + expect(ui.apples.checked).toEqual(true); + expect(ui.oranges.checked).toEqual(false); + expect(ui.selected).toEqual("apples"); + expect(ui.fruit.value).toEqual("apples"); + expect(ui.fruit.classList.length).toEqual(0); + expect(ui.reflectApples.checked).toEqual(true); + } + + function expectOranges() { + expect(ui.apples.checked).toEqual(false); + expect(ui.oranges.checked).toEqual(true); + expect(ui.selected).toEqual("oranges"); + expect(ui.fruit.value).toEqual("oranges"); + expect(ui.fruit.classList.length).toEqual(0); + expect(ui.reflectApples.checked).toEqual(false); + } + + ui.apples.checked = true; + expectApples(); + + ui.apples.checked = false; + expectOranges(); + + ui.oranges.checked = false; + ui.oranges.checked = true; + expectOranges(); + + ui.oranges.checked = false; + expectApples(); + + ui.fruit.value = "apples"; + expectApples(); + + ui.fruit.value = "oranges"; + expectOranges(); + + ui.reflectApples.checked = true; + expectApples(); + + ui.reflectApples.checked = false; + expectOranges(); + + }); + +}); + diff --git a/core/frb/spec/compute-spec.js b/core/frb/spec/compute-spec.js new file mode 100644 index 0000000000..33179a5c19 --- /dev/null +++ b/core/frb/spec/compute-spec.js @@ -0,0 +1,80 @@ + + +var compute = require("../compute"); + +Error.stackTraceLimit = 100; + +describe("compute", function () { + + it("basic", function () { + + var object = {a: 10, b: 20}; + var cancel = compute(object, "c", { + args: ["a", "b"], + compute: function (a, b) { + return a + b; + } + }); + expect(object.c).toEqual(30); + object.a = 30; + expect(object.c).toEqual(50); + cancel(); + object.b = 0; + expect(object.c).toEqual(50); + + }); + + it("moving target", function () { + + var object = {a: 10, b: 20, c: {}}; + var cancel = compute(object, "c.d", { + args: ["a", "b"], + compute: function (a, b) { + return a + b; + } + }); + + expect(object.c.d).toEqual(30); + object.a = 30; + expect(object.c.d).toEqual(50); + + // replace target + var oldTarget = object.c; + object.c = {}; + expect(object.c.d).toEqual(50); + object.a = 0; + expect(object.c.d).toEqual(20); + expect(oldTarget.d).toEqual(50); + + // cancel + cancel(); + object.b = 0; + expect(object.c.d).toEqual(20); + + }); + + it("content changes", function () { + var object = {values: [1, 2, 3], offset: 0}; + var cancel = compute(object, "sum", { + args: ["values.rangeContent()", "offset"], + compute: function (values, offset) { + return values.map(function (value) { + return value + offset; + }).sum(); + } + }); + + expect(object.sum).toEqual(6); + object.values.push(4); + expect(object.sum).toEqual(10); + + object.offset = 1; + expect(object.sum).toEqual(14); + + cancel(); + object.offset = 0; + expect(object.sum).toEqual(14); + }); + +}); + diff --git a/core/frb/spec/enumerate-spec.js b/core/frb/spec/enumerate-spec.js new file mode 100644 index 0000000000..f198b01e29 --- /dev/null +++ b/core/frb/spec/enumerate-spec.js @@ -0,0 +1,79 @@ + +var Bindings = require("../bindings"); + +describe("", function () { + + it("entries update", function () { + var object = Bindings.defineBindings({}, { + output: {"<-": "input.enumerate()"} + }); + expect(object.output).toEqual([]); + + object.input = ['a', 'b', 'c']; + expect(object.output).toEqual([ + [0, 'a'], + [1, 'b'], + [2, 'c'] + ]); + + object.input.push('d'); + expect(object.output).toEqual([ + [0, 'a'], + [1, 'b'], + [2, 'c'], + [3, 'd'] + ]); + }); + + it("index updates", function () { + var object = Bindings.defineBindings({}, { + output: {"<-": "input.enumerate()"} + }); + expect(object.output).toEqual([]); + + object.input = ['b']; + var b = object.output[0]; + expect(b).toEqual([0, 'b']); + + object.input.unshift('a'); + expect(object.output[1]).toBe(b); + expect(b[0]).toBe(1); + }); + + it("annotation", function () { + var input = []; + var output = []; + Bindings.defineBinding(output, "rangeContent()", { + "<-": "enumerate()", + source: input + }); + var cancelers = []; + output.addRangeChangeListener(function (plus, minus, index) { + cancelers.swap(index, minus.length, plus.map(function (entry) { + Bindings.defineBinding(entry, ".1.index", { + "<-": ".0" + }); + return function cancel() { + Bindings.cancelBinding(entry, ".1.index"); + }; + })).forEach(function (cancel) { + cancel && cancel(); + }); + }); + + var b = {}; + input.push(b); + expect(b.index).toBe(0); + + var a = {}; + input.unshift(a); + expect(a.index).toBe(0); + expect(b.index).toBe(1); + + input.swap(0, 2, [b, a]); + expect(b.index).toBe(0); + expect(a.index).toBe(1); + }); + +}); + diff --git a/core/frb/spec/evaluate-spec.js b/core/frb/spec/evaluate-spec.js new file mode 100644 index 0000000000..1ea80cbc5f --- /dev/null +++ b/core/frb/spec/evaluate-spec.js @@ -0,0 +1,94 @@ + +var parse = require("../parse"); +var evaluate = require("../evaluate"); +var cases = require("./evaluate"); + +describe("evaluate", function () { + cases.forEach(function (test) { + it( + "should evaluate " + JSON.stringify(test.path) + + " of " + JSON.stringify(test.input), + function () { + var output = evaluate( + test.path, + test.input, + test.parameters, + test.document, + test.components + ); + expect(output).toEqual(test.output); + } + ); + }); + + it("should allow extensible polymorphic overrides", function () { + + var isBound = evaluate("a.isBound()", { + a: { + isBound: function () { + return true; + } + } + }); + expect(isBound).toBe(true); + }); + + it("should be resilient to beginning of expression containing 'map' being undefined", function () { + var result = evaluate("a.images.edges.map{node}", { + a: {} + }); + expect(result).toBe(undefined); + }); + + it("should be resilient to beginning of expression containing 'filter' being undefined", function () { + var result = evaluate("a.images.edges.filter{node}", { + a: {} + }); + expect(result).toBe(undefined); + }); + + it("should be resilient to beginning of expression containing 'some' being undefined", function () { + var result = evaluate("a.images.edges.some{node}", { + a: {} + }); + expect(result).toBe(undefined); + }); + + it("should be resilient to beginning of expression containing 'every' being undefined", function () { + var result = evaluate("a.images.edges.every{node}", { + a: {} + }); + expect(result).toBe(undefined); + }); + + it("should be resilient to beginning of expression containing 'sorted' being undefined", function () { + var result = evaluate("a.images.edges.sorted{node}", { + a: {} + }); + expect(result).toBe(undefined); + }); + + it("should be resilient to beginning of expression containing 'group' being undefined", function () { + var result = evaluate("a.images.edges.group{node}", { + a: {} + }); + expect(result).toBe(undefined); + }); + + it("should be resilient to beginning of expression containing 'min' being undefined", function () { + var result = evaluate("a.images.edges.min{node}", { + a: {} + }); + expect(result).toBe(undefined); + }); + + it("should be resilient to beginning of expression containing 'max' being undefined", function () { + var result = evaluate("a.images.edges.max{node}", { + a: {} + }); + expect(result).toBe(undefined); + }); + + +}); + diff --git a/core/frb/spec/evaluate-with-observe-spec.js b/core/frb/spec/evaluate-with-observe-spec.js new file mode 100644 index 0000000000..ade5ca46b7 --- /dev/null +++ b/core/frb/spec/evaluate-with-observe-spec.js @@ -0,0 +1,31 @@ + +var parse = require("../parse"); +var compile = require("../compile-observer"); +var Scope = require("../scope"); +var cases = require("./evaluate"); + +describe("observe", function () { + cases.forEach(function (test) { + it( + "should observe initial value of " + JSON.stringify(test.path) + + " with " + JSON.stringify(test.input), + function () { + var syntax = parse(test.path); + var observe = compile(syntax); + var output; + var scope = new Scope(test.input); + scope.parameters = test.parameters; + scope.document = test.document; + scope.components = test.components; + var cancel = observe(function (initial) { + output = initial; + }, scope) || Function.noop; + cancel(); + if (Array.isArray(output)) { + output = output.slice(); // to ditch observable prototype + } + expect(output).toEqual(test.output); + } + ); + }); +}); diff --git a/core/frb/spec/evaluate.js b/core/frb/spec/evaluate.js new file mode 100644 index 0000000000..96d35836cb --- /dev/null +++ b/core/frb/spec/evaluate.js @@ -0,0 +1,780 @@ + +// these cases are used ot test evaluation compiler (evaluate-spec.js) and +// observer compiler (for the initial value, evaluate-with-observe-spec.js). +module.exports = [ + + { + path: "10", + output: 10 + }, + + { + path: "10.1", + output: 10.1 + }, + + { + path: "-10", + output: -10 + }, + + { + path: "'a'", + output: "a" + }, + + { + path: "'\\''", + output: "'" + }, + + { + path: "", + input: 10, + output: 10 + }, + + { + path: "a", + input: {a: 10}, + output: 10 + }, + + { + path: "a.b", + input: {a: {b: 10}}, + output: 10 + }, + + { + path: "$", + parameters: 10, + output: 10 + }, + + { + path: "$a", + parameters: {a: 10}, + output: 10 + }, + + { + path: "$a.b", + parameters: {a: {b: 10}}, + output: 10 + }, + + { + path: "#id", + document: { + getElementById: function (id) { + return id; + } + }, + output: "id" + }, + + { + path: "#id.value", + document: { + getElementById: function (id) { + return {value: id}; + } + }, + output: "id" + }, + + { + path: "@label", + components: { + getObjectByLabel: function (label) { + return label; + } + }, + output: "label" + }, + + { + path: "@label.value", + components: { + getObjectByLabel: function (label) { + return {value: label}; + } + }, + output: "label" + }, + + { + path: '[a, b, c]', + input: {a: 1, b: 2, c: 3}, + output: [1, 2, 3] + }, + + { + path: '{a: x, b: y, c: z}', + input: {x: 1, y: 2, z: 3}, + output: {a: 1, b: 2, c: 3} + }, + + { + path: "{a: get(0), b: get(1)}", + input: [10, 20], + output: {a: 10, b: 20} + }, + + { + path: "scope.{foo: x}", + input: {scope: {x: 10}}, + output: {foo: 10} + }, + + { + path: "scope.(a + b)", + input: {scope: {a: 2, b: 3}}, + output: 5 + }, + + { + path: "scope.(a + b).(() + 2)", + input: {scope: {a: 2, b: 3}}, + output: 7 + }, + + { + path: "scope.(a + b).(+2)", + input: {scope: {a: 2, b: 3}}, + output: 2 + }, + + { + path: "scope.[a, b]", + input: {scope: {a: 2, b: 3}}, + output: [2, 3] + }, + + { + path: "map{* 2}", + input: [1, 2, 3], + output: [2, 4, 6] + }, + + { + path: "map{* $factor}", + input: [1, 2, 3], + parameters: {factor: 2}, + output: [2, 4, 6] + }, + + { + path: "array.map{* $factor}", + input: {array: [1, 2, 3]}, + parameters: {factor: 2}, + output: [2, 4, 6] + }, + + { + path: '[1, 2, 3].map{* $factor}', + parameters: {factor: 2}, + output: [2, 4, 6] + }, + + { + path: "array.filter{!(% 2)}", + input: {array: [1, 2, 3, 4]}, + output: [2, 4] + }, + + { + path: "array.some{== 'a'}", + input: {array: ['a', 'b', 'c']}, + output: true + }, + + { + path: "array.some{== 'a'}", + input: {array: ['b', 'c', 'd']}, + output: false + }, + + { + path: "array.every{> 0}", + input: {array: [1, 2, 3]}, + output: true + }, + + { + path: "array.every{> 0}", + input: {array: [0, 1, 2, 3]}, + output: false + }, + + { + path: "sorted{foo}.map{foo}.reversed()", + input: [{foo: 3}, {foo: 1}, {foo: 2}], + output: [3, 2, 1] + }, + + { + path: "sortedSet{foo}.map{foo}.reversed()", + input: [{foo: 3}, {foo: 1}, {foo: 1}, {foo: 2}], + output: [3, 2, 1] + }, + + { + path: "group{score}", + input: [{score: 1, name: "Josh"}, {score: 1, name: "Ben"}, {score: 2, name: "Alice"}], + output: [ + [1, [{score: 1, name: "Josh"}, {score: 1, name: "Ben"}]], + [2, [{score: 2, name: "Alice"}]] + ] + }, + + { + path: "groupMap{score}.items()", + input: [{score: 1, name: "Josh"}, {score: 1, name: "Ben"}, {score: 2, name: "Alice"}], + output: [ + [1, [{score: 1, name: "Josh"}, {score: 1, name: "Ben"}]], + [2, [{score: 2, name: "Alice"}]] + ] + }, + + { + path: "max{x}", + input: [{x: 0}, {x: 2}, {x: 1}], + output: {x: 2} + }, + + { + path: "min{x}", + input: [{x: 0}, {x: 2}, {x: 1}], + output: {x: 0} + }, + + //{ + // path: "min()", + // input: [1, 2, 3], + // output: 1 + //}, + + //{ + // path: "max()", + // input: [1, 2, 3], + // output: 3 + //}, + + { + path: "sum()", + input: [1, 2, 3], + output: 6 + }, + + { + path: "average()", + input: [1, 2, 3], + output: 2 + }, + + { + path: "last()", + input: [1, 2, 3], + output: 3 + }, + + { + path: "last()", + input: [], + output: null + }, + + { + path: "flatten()", + input: [[1], [2, 3], [4]], + output: [1, 2, 3, 4] + }, + + { + path: "&concat([1, 2, 3], [4, 5, 6])", + output: [1, 2, 3, 4, 5, 6] + }, + + { + path: "view($start, $length)", + input: [1, 2, 3, 4, 5], + parameters: {start: 2, length: 2}, + output: [3, 4] + }, + + { + path: "a && b", + input: {a: true, b: true}, + output: true + }, + + { + path: "a && b", + input: {a: false, b: true}, + output: false + }, + + { + path: "a && b", + input: {a: true, b: false}, + output: false + }, + + { + path: "a && b", + input: {a: false, b: false}, + output: false + }, + + { + path: "a || b", + input: {a: true, b: true}, + output: true + }, + + { + path: "a || b", + input: {a: false, b: true}, + output: true + }, + + { + path: "a || b", + input: {a: true, b: false}, + output: true + }, + + { + path: "a || b", + input: {a: false, b: false}, + output: false + }, + + { + path: "rangeContent()", + input: [1, 2, 3], + output: [1, 2, 3] + }, + + { + path: "mapContent()", + input: [1, 2, 3], + output: [1, 2, 3] + }, + + { + path: "? 1 : 2", + input: true, + output: 1 + }, + + { + path: "? 1 : 2", + input: false, + output: 2 + }, + + { + path: "x ?? 10", + input: {x: 20}, + output: 20 + }, + + { + path: "x ?? 10", + input: {x: null}, + output: 10 + }, + + { + path: "x ?? 10", + input: {x: undefined}, + output: 10 + }, + + { + path: "x ?? 10", + input: null, + output: 10 + }, + + { + path: "x ?? 10", + input: undefined, + output: 10 + }, + + { + path: "x ?? y", + input: {y: 20}, + output: 20 + }, + + { + path: "x ?? y ?? 10", + input: {y: 20}, + output: 20 + }, + + { + path: "x.defined()", + input: null, + output: false + }, + + { + path: "x.defined()", + input: {x: 10}, + output: true + }, + + { + path: "x.defined()", + input: {x: undefined}, + output: false + }, + + { + path: "x.defined() && x", + input: {x: null}, + output: false + }, + + { + path: "x.defined() && x", + input: {x: false}, + output: false + }, + + { + path: "x.defined() && x", + input: {x: true}, + output: true + }, + + { + path: "!x", + input: null, + output: true + }, + + { + path: "!!x", + input: null, + output: false + }, + + { + path: "&range(())", + input: 3, + output: [0, 1, 2] + }, + + { + path: "3.range()", + input: null, + output: [0, 1, 2] + }, + + { + path: "x.startsWith(y)", + input: { + x: "|.!", + y: "|." + }, + output: true + }, + + { + path: "x.startsWith(y)", + input: { + x: "|.!", + y: ".!" + }, + output: false + }, + + { + path: "x.startsWith(y)", + input: { + x: undefined, + y: undefined + }, + output: undefined + }, + + { + path: "x.startsWith(y)", + input: { + x: "", + y: undefined + }, + output: undefined + }, + + { + path: "x.startsWith(y)", + input: { + x: undefined, + y: "" + }, + output: undefined + }, + + { + path: "x.endsWith(y)", + input: { + x: "|.!", + y: "|.!" + }, + output: true + }, + + { + path: "x.endsWith(y)", + input: { + x: "|.!", + y: "|." + }, + output: false + }, + + { + path: "x.endsWith(y)", + input: { + x: undefined, + y: undefined + }, + output: undefined + }, + + { + path: "x.endsWith(y)", + input: { + x: "", + y: undefined + }, + output: undefined + }, + + { + path: "x.endsWith(y)", + input: { + x: undefined, + y: "" + }, + output: undefined + }, + + { + path: "x.contains(y)", + input: { + x: undefined, + y: undefined + }, + output: undefined + }, + + { + path: "x.contains(y)", + input: { + x: "", + y: undefined + }, + output: undefined + }, + + { + path: "x.contains(y)", + input: { + x: undefined, + y: "" + }, + output: undefined + }, + + { + path: "&contains(x, y)", + input: { + x: "?!^*", + y: "^" + }, + output: true + }, + + { + path: "join()", + input: ['a', 'b', 'c'], + output: 'abc' + }, + + { + path: "join()", + input: null, + output: undefined + }, + + { + path: "split()", + input: "abc", + output: ['a', 'b', 'c'] + }, + + { + path: "split(', ')", + input: "a, b, c", + output: ['a', 'b', 'c'] + }, + + { + path: "split()", + input: null, + output: undefined + }, + + { + path: "toString()", + input: "Hello, World!", + output: "Hello, World!" + }, + + { + path: "toString()", + input: 10, + output: "10" + }, + + { + path: "toString()", + input: null, + output: null + }, + + { + path: "toString()", + input: {a: 10}, + output: null + }, + + { + path: "toMap().entriesArray()", + input: {a: 10, b: 20}, + output: [['a', 10], ['b', 20]] + }, + + { + path: "toMap().entriesArray()", + input: [['a', 10], ['b', 20]], + output: [['a', 10], ['b', 20]] + }, + + { + path: "toMap().toMap().entriesArray()", + input: {a: 10, b: 20}, + output: [['a', 10], ['b', 20]] + }, + + { + path: "1 <=> 2", + input: null, + output: -1 + }, + + { + path: "3 <=> 1", + input: null, + output: 2 + }, + + { + path: "2 <=> 2", + input: null, + output: 0 + }, + + { + path: "path('')", + input: 1, + output: 1 + }, + + { + path: "path(path)", + input: {x: 2, y: 3, path: "x + y"}, + output: 5 + }, + + { + path: "foo.path($path)", + input: {foo: {a: 10}}, + parameters: {path: "a"}, + output: 10 + }, + + { + path: "path('(]')", + input: "i should not be", + output: void 0 + }, + + { + path: "path(())", + input: "(]", + output: void 0 + }, + + { + path: "path(())", + input: "(", + output: void 0 + }, + + { + path: "property(property)", + input: { + a: 10, + property: "a" + }, + output: 10 + }, + + { + path: "path(path)", + input: { + a: {b: 10}, + path: "a.b" + }, + output: 10 + }, + + { + path: "borks('borkish')", + input: { + borks: function (borky) { + return borky == "borkish"; + } + }, + output: true + }, + + { + path: "isAdmin ? 'admin' : 'user'", + input: {}, + output: undefined + }, + + { + path: "isAdmin ? 'admin' : 'user'", + input: {isAdmin: true}, + output: 'admin' + }, + + { + path: "isAdmin ? 'admin' : 'user'", + input: {isAdmin: false}, + output: 'user' + } + +]; diff --git a/core/frb/spec/expand-spec.js b/core/frb/spec/expand-spec.js new file mode 100644 index 0000000000..3ee4007f41 --- /dev/null +++ b/core/frb/spec/expand-spec.js @@ -0,0 +1,107 @@ + +var expand = require("../expand"); +var parse = require("../parse"); +var stringify = require("../stringify"); +var compileObserver = require("../compile-observer"); +var Scope = require("../scope"); + +var cases = [ + + { + input: "0", + with: "@a", + output: "0" + }, + + { + input: "[a, 0]", + with: "(2 + 2)", + output: "[(2 + 2).a, 0]" + }, + + { + input: "map{foo}", + with: "@bar", + output: "@bar.map{foo}" + }, + + { + input: "a.b", + with: "@x", + output: "@x.a.b" + }, + + { + input: "* 2", + with: "3 + 4", + output: "(3 + 4) * 2" + }, + + { + input: "*", + with: "2", + output: "2 * 2" + }, + + { + input: "y + z", + with: "@a", + output: "@a.y + @a.z" + }, + + { + input: "?:", + with: "$0", + output: "$0 ? $0 : $0" + }, + + { + input: "startsWith(())", + with: "@x", + output: "@x.startsWith(@x)" + } + +]; + +describe("expand", function () { + + // generic cases + cases.forEach(function (test) { + it("should expand " + JSON.stringify(test.input) + " with " + JSON.stringify(test.with), function () { + var output = stringify(expand(parse(test.input), new Scope(parse(test.with)))); + expect(output).toEqual(test.output); + }); + }); + + + it("should expand component labels from a serializer", function () { + + var syntax = parse("@a"); + var a = {}; + var observe = compileObserver(syntax); + var scope = new Scope(); + scope.components = { + getObjectByLabel: function (label) { + expect(label).toBe("a"); + return a; + } + }; + var cancel = observe(function (_a) { + expect(_a).toBe(a); + }, scope); + + var scope = new Scope(); + scope.components = { + getObjectLabel: function (_a) { + expect(_a).toBe(a); + return "b"; + }, + }; + syntax.component = a; + var syntax = expand(syntax, scope); + expect(stringify(syntax)).toBe("@b"); + + }); + +}); + diff --git a/core/frb/spec/filter-map-spec.js b/core/frb/spec/filter-map-spec.js new file mode 100644 index 0000000000..cb760a190b --- /dev/null +++ b/core/frb/spec/filter-map-spec.js @@ -0,0 +1,21 @@ + +var Bindings = require("../bindings"); +describe("filter", function () { + it("should propagate range changes to map, even if the filters are respecitvely the same", function () { + + var object = Bindings.defineBindings({ + input: "abcdefghijklmnopqrstuvwxyz", + pluck: [0, 1, 2, 3] + }, { + output: { + "<-": "pluck.filter{this < $input.length}.map{$input[this]}" + } + }); + + expect(object.output).toEqual(['a', 'b', 'c', 'd']); + + object.pluck.splice(0, 1, 4); + expect(object.output).toEqual(['e', 'b', 'c', 'd']); + + }); +}); diff --git a/core/frb/spec/filter-spec.js b/core/frb/spec/filter-spec.js new file mode 100644 index 0000000000..83f0a4cba4 --- /dev/null +++ b/core/frb/spec/filter-spec.js @@ -0,0 +1,44 @@ + +var Bindings = require("../bindings"); + +describe("filter", function () { + + it("should handle NaN predicates", function () { + var object = Bindings.defineBinding({}, "filtered", { + "<-": "array.filter{}" + }); + expect(object.filtered).toEqual([]); + + object.array = [NaN, true, true, true]; + expect(object.filtered.length).toEqual(3); + + object.array.set(2, false); + expect(object.filtered.length).toEqual(2); + }); + + it("should handle >1 numeric predicates", function () { + var object = Bindings.defineBinding({}, "filtered", { + "<-": "array.filter{}" + }); + expect(object.filtered).toEqual([]); + + object.array = [2, 3, 4]; + expect(object.filtered.length).toEqual(3); + object.array.set(2, 0); + expect(object.filtered.length).toEqual(2); + }); + + it("should avoid optimizing away a subtle swap with one unchanged value", function () { + var object = Bindings.defineBinding({}, "filtered", { + "<-": "array.filter{}" + }); + + object.array = [0, 1, 1, 2, 0]; + expect(object.filtered).toEqual([1, 1, 2]); + + object.array.splice(0, 4, 0, 1, 2, 1, 0); + expect(object.filtered).toEqual([1, 2, 1]); + + }); + +}); diff --git a/core/frb/spec/gate-spec.js b/core/frb/spec/gate-spec.js new file mode 100644 index 0000000000..cf1ed2fa71 --- /dev/null +++ b/core/frb/spec/gate-spec.js @@ -0,0 +1,67 @@ + +var Map = require("collections/map"); +var Bindings = require("../bindings"); + +describe("extensible logic gates", function () { + var gate; + + beforeEach(function () { + gate = Bindings.defineBindings({ + inputs: new Map() + }, { + output: {"<-": "inputs.items().every{.1}"} + }); + }); + + it("should work with one input", function () { + + expect(gate.output).toBe(true); + + gate.inputs.set('a', false); + expect(gate.output).toBe(false); + + gate.inputs.set('a', true); + expect(gate.output).toBe(true); + + gate.inputs.delete('a'); + expect(gate.output).toBe(true); + + }); + + it("should work with multiple inputs", function () { + + expect(gate.output).toBe(true); + + gate.inputs.set('a', true); + expect(gate.output).toBe(true); + + gate.inputs.set('b', false); + expect(gate.output).toBe(false); + + gate.inputs.delete('b'); + expect(gate.output).toBe(true); + + gate.inputs.delete('a'); + expect(gate.output).toBe(true); + + }); + + it("should work with multiple inputs", function () { + + expect(gate.output).toBe(true); + + gate.inputs.set('a', false); + expect(gate.output).toBe(false); + + gate.inputs.set('b', false); + expect(gate.output).toBe(false); + + gate.inputs.delete('b'); + expect(gate.output).toBe(false); + + gate.inputs.delete('a'); + expect(gate.output).toBe(true); + + }); + +}); diff --git a/core/frb/spec/group-spec.js b/core/frb/spec/group-spec.js new file mode 100644 index 0000000000..f234a2f592 --- /dev/null +++ b/core/frb/spec/group-spec.js @@ -0,0 +1,149 @@ + +var Bindings = require("../bindings"); +var SortedSet = require("collections/sorted-set"); + +describe("group block", function () { + + var sam = {name: "Sam", gender: "female"}; + var jamie = {name: "Jamie", gender: "male"}; + var leslie = {name: "Leslie", gender: "male"}; + var pat = {name: "Pat", gender: "female"}; + var bobby = {name: "Bobby", gender: "female"}; + var max = {name: "Max", gender: "male"}; + + var object = { + folks: [sam, jamie, leslie, pat, bobby, max] + }; + + it("should define and initialize a group binding", function () { + + Bindings.defineBinding(object, "folksByGender", { + "<-": "folks.group{gender}.map{[.0, .1.map{name}]}" + }); + + expect(object.folksByGender).toEqual([ + ["female", [ + "Sam", + "Pat", + "Bobby" + ]], + ["male", [ + "Jamie", + "Leslie", + "Max" + ]] + ]); + + }); + + it("should respond to a property change affecting the relation", function () { + + leslie.gender = "female"; + + expect(object.folksByGender).toEqual([ + ["female", [ + "Sam", + "Pat", + "Bobby", + "Leslie" + ]], + ["male", [ + "Jamie", + "Max" + ]] + ]); + + }); + + it("should respond to the removal of an iteration", function () { + + object.folks.pop(); + + expect(object.folksByGender).toEqual([ + ["female", [ + "Sam", + "Pat", + "Bobby", + "Leslie" + ]], + ["male", [ + "Jamie" + ]] + ]); + + }); + + it("should remove empty groups", function () { + + object.folks.delete(jamie); + + expect(object.folksByGender).toEqual([ + ["female", [ + "Sam", + "Pat", + "Bobby", + "Leslie" + ]] + ]); + + }); + + it("should create a new group", function () { + + object.folks.unshift(max); + + expect(object.folksByGender).toEqual([ + ["female", [ + "Sam", + "Pat", + "Bobby", + "Leslie" + ]], + ["male", [ + "Max" + ]] + ]); + + }); + + it("should work with group map block as well", function () { + + Bindings.cancelBinding(object, "folksByGender"); + Bindings.defineBinding(object, "folksByGender", { + "<-": "folks.groupMap{gender}.items().map{[.0, .1.map{name}]}" + }); + + expect(object.folksByGender).toEqual([ + ["male", [ + "Max" + ]], + ["female", [ + "Sam", + "Leslie", + "Pat", + "Bobby" + ]] + ]); + + }); + + it("should use the same collection type for the equivalence classes", function () { + + Bindings.cancelBinding(object, "folksByGender"); + Bindings.defineBinding(object, "folksByGender", { + "<-": "folks.groupMap{gender}" + }); + + object.folks = new SortedSet([max, sam], function (a, b) { + return a.name === b.name; + }, function (a, b) { + return Object.compare(a.name, b.name); + }); + + expect(object.folksByGender.get('male') instanceof SortedSet).toBe(true); + + }); + + +}); + diff --git a/core/frb/spec/items-spec.js b/core/frb/spec/items-spec.js new file mode 100644 index 0000000000..a253ac70af --- /dev/null +++ b/core/frb/spec/items-spec.js @@ -0,0 +1,35 @@ + +var Bindings = require("../bindings"); +var Map = require("collections/map"); + +describe("items", function () { + it("should work", function () { + + var object = Bindings.defineBinding({}, "items", { + "<-": "map.items()" + }); + + expect(object.items).toEqual([]); + + object.map = new Map(); + expect(object.items).toEqual([]); + + object.map.set(0, 'a'); + expect(object.items).toEqual([[0, 'a']]); + + object.map.set(0, 'b'); + expect(object.items).toEqual([[0, 'b']]); + + object.map.set(1, 'a'); + expect(object.items).toEqual([[0, 'b'], [1, 'a']]); + + object.map.delete(0); + expect(object.items).toEqual([[1, 'a']]); + + Bindings.cancelBindings(object); + + object.map.set(0, 'c'); + expect(object.items).toEqual([[1, 'a']]); + + }); +}); diff --git a/core/frb/spec/language.js b/core/frb/spec/language.js new file mode 100644 index 0000000000..5a0eaa1bc6 --- /dev/null +++ b/core/frb/spec/language.js @@ -0,0 +1,1193 @@ +// these cases are used to test both "parse" and "stringify" +module.exports = [ + + { + path: "", + syntax: {type: "value"} + }, + + { + path: "this", + syntax: {type: "value"}, + nonCanon: true + }, + + { + path: "1", + syntax: {type: "literal", value: 1} + }, + + { + path: "1.2", + syntax: {type: "literal", value: 1.2} + }, + + { + path: "true", + syntax: {type: "literal", value: true} + }, + + { + path: "false", + syntax: {type: "literal", value: false} + }, + + { + path: "null", + syntax: {type: "literal", value: null} + }, + + { + path: "'\"'", + syntax: {type: "literal", value: "\""} + }, + + { + path: "'\\''", + syntax: {type: "literal", value: "'"} + }, + + { + path: "\"\\\"\"", + syntax: {type: "literal", value: "\""}, + nonCanon: true + }, + + { + path: "\"'\"", + syntax: {type: "literal", value: "'"}, + nonCanon: true + }, + + { + path: "a", + syntax: {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]} + }, + + { + path: "a.b", + syntax: {type: "property", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "literal", value: "b"} + ]} + }, + + { + path: "a[b]", + syntax: {type: "property", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} + ]} + }, + + { + path: "this[b]", + syntax: {type: "property", args: [ + {type: "value"}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} + ]} + }, + + { + path: "get(key)", + syntax: {type: "get", args: [ + {type: "value"}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "key"} + ]} + ]} + }, + + { + path: ".0", + syntax: {type: "property", args: [ + {type: "value"}, + {type: "literal", value: 0} + ]} + }, + + { + path: "a.0.b", + syntax: {type: "property", args: [ + {type: "property", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "literal", value: 0} + ]}, + {type: "literal", value: "b"} + ]} + }, + + { + path: "(a + b).x", + syntax: {type: "property", args: [ + {type: "add", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} + ]}, + {type: "literal", value: "x"} + ]} + }, + + { + path: "1 + 2 + 3", + syntax: {type: "add", args: [ + {type: "add", args: [ + {type: "literal", value: 1}, + {type: "literal", value: 2} + ]}, + {type: "literal", value: 3} + ]} + }, + + { + path: "1 + (2 + 3)", + syntax: {type: "add", args: [ + {type: "literal", value: 1}, + {type: "add", args: [ + {type: "literal", value: 2}, + {type: "literal", value: 3} + ]} + ]}, + nonCanon: true + }, + + { + path: "!a", + syntax: {type: "not", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]} + ]} + }, + + { + path: "!!a", + syntax: {type: "not", args: [ + {type: "not", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]} + ]} + ]} + }, + + { + path: "!(a && b)", + syntax: {type: "not", args: [ + {type: "and", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} + ]} + ]} + }, + + { + path: "a.[b, c]", + syntax: {type: "with", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"}, + ]}, + {type: "tuple", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"}, + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "c"}, + ]} + ]} + ]} + }, + + { + path: "a.{foo: x}", + syntax: {type: "with", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"}, + ]}, + {type: "record", args: { + foo: {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "x"} + ]} + }} + ]} + }, + + { + path: "a.(b + c)", + syntax: {type: "with", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"}, + ]}, + {type: "add", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"}, + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "c"}, + ]} + ]} + ]} + }, + + { + path: "a.('a')", + syntax: {type: "with", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"}, + ]}, + {type: "literal", value: "a"} + ]} + }, + + { + path: "^a", + syntax: {type: "parent", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]} + ]} + }, + + { + path: "^a.foo(bar)", + syntax: {type: "foo", args: [ + {type: "parent", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "bar"} + ]} + ]} + }, + + { + path: "[]", + syntax: {type: "tuple", args: [ + ]} + }, + + { + path: "[,]", + syntax: {type: "tuple", args: [ + {type: "value"}, + {type: "value"} + ]}, + nonCanon: true + }, + + { + path: "[,,]", + syntax: {type: "tuple", args: [ + {type: "value"}, + {type: "value"}, + {type: "value"} + ]}, + nonCanon: true + }, + + { + path: "[this]", + syntax: {type: "tuple", args: [ + {type: "value"} + ]} + }, + + { + path: "[this, this]", + syntax: {type: "tuple", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "[a]", + syntax: {type: "tuple", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]} + ]} + }, + + { + path: "map{}", + syntax: {type: "mapBlock", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "a.map{}", + syntax: {type: "mapBlock", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "value"} + ]} + }, + + { + path: "a.map{b}", + syntax: {type: "mapBlock", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} + ]} + }, + + { + path: "map{* $factor}", + syntax: {type: "mapBlock", args: [ + {type: "value"}, + {type: "mul", args: [ + {type: "value"}, + {type: "property", args: [ + {type: "parameters"}, + {type: "literal", value: "factor"} + ]} + ]} + ]} + }, + + { + path: "filter{}", + syntax: {type: "filterBlock", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "some{}", + syntax: {type: "someBlock", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "every{}", + syntax: {type: "everyBlock", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "sorted{}", + syntax: {type: "sortedBlock", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "sortedSet{}", + syntax: {type: "sortedSetBlock", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "flatten()", + syntax: {type: "flatten", args: [ + {type: "value"} + ]} + }, + + { + path: "flatten{}", + syntax: {type: "flatten", args: [ + {type: "value"} + ]}, + nonCanon: true + }, + + { + path: "a.flatten{}", + syntax: {type: "flatten", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]} + ]}, + nonCanon: true + }, + + { + path: "a.flatten{b}", + syntax: {type: "flatten", args: [ + {type: "mapBlock", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} + ]} + ]} + }, + + { + path: "function(0, '\\'')", + syntax: {type: "function", args: [ + {type: "value"}, + {type: "literal", value: 0}, + {type: "literal", value: "'"} + ]} + }, + + { + path: "$foo", + syntax: {type: "property", args: [ + {type: "parameters"}, + {type: "literal", value: "foo"} + ]} + }, + + { + path: "{a: 10}", + syntax: {type: "record", args: { + a: {type: "literal", value: 10} + }} + }, + + { + path: "{a: 10, b: 20}", + syntax: {type: "record", args: { + a: {type: "literal", value: 10}, + b: {type: "literal", value: 20} + }} + }, + + { + path: "2 == 2", + syntax: {type: "equals", args: [ + {type: "literal", value: 2}, + {type: "literal", value: 2} + ]} + }, + + { + path: "2 != 2", + syntax: {type: "not", args: [ + {type: "equals", args: [ + {type: "literal", value: 2}, + {type: "literal", value: 2} + ]} + ]} + }, + + { + path: "2 + 2", + syntax: {type: "add", args: [ + {type: "literal", value: 2}, + {type: "literal", value: 2} + ]} + }, + + { + path: "2 + 2 == 4", + syntax: {type: "equals", args: [ + {type: "add", args: [ + {type: "literal", value: 2}, + {type: "literal", value: 2} + ]}, + {type: "literal", value: 4} + ]} + }, + + { + path: "!0", + syntax: {type: "not", args: [ + {type: "literal", value: 0} + ]} + }, + + { + path: "-1", + syntax: {type: "neg", args: [ + {type: "literal", value: 1} + ]} + }, + + { + path: "2 * 2 + 4", + syntax: {type: "add", args: [ + {type: "mul", args: [ + {type: "literal", value: 2}, + {type: "literal", value: 2} + ]}, + {type: "literal", value: 4} + ]} + }, + + { + path: "2 * (2 + 4)", + syntax: {type: "mul", args: [ + {type: "literal", value: 2}, + {type: "add", args: [ + {type: "literal", value: 2}, + {type: "literal", value: 4} + ]} + ]} + }, + + { + path: "x != a && y != b", + syntax: {type: "and", args: [ + {type: "not", args: [ + {type: "equals", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "x"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]} + ]}, + ]}, + {type: "not", args: [ + {type: "equals", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "y"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} + ]} + ]} + ]} + }, + + { + path: "!(% 2)", + syntax: {type: "not", args: [ + {type: "mod", args: [ + {type: "value"}, + {type: "literal", value: 2} + ]} + ]} + }, + + { + path: "a || b && c", + syntax: {type: "or", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "and", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "c"} + ]} + ]} + ]} + }, + + { + path: "this[a]", + syntax: {type: "property", args: [ + {type: "value"}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]} + ]} + }, + + { + path: "this['~' + a]", + syntax: {type: "property", args: [ + {type: "value"}, + {type: "add", args: [ + {type: "literal", value: "~"}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]} + ]} + ]} + }, + + { + path: "this['a']", + syntax: {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + nonCanon: true + }, + + { + path: "x['!']", + syntax: {type: "property", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "x"} + ]}, + {type: "literal", value: "!"} + ]} + }, + + { + path: "x['a']", + syntax: {type: "property", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "x"} + ]}, + {type: "literal", value: "a"} + ]}, + nonCanon: true + }, + + { + path: "array[array.length - 1]", + syntax: {type: "property", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "array"} + ]}, + {type: "sub", args: [ + {type: "property", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "array"} + ]}, + {type: "literal", value: "length"} + ]}, + {type: "literal", value: 1} + ]} + ]} + }, + + { + path: "*", + syntax: {type: "mul", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "**", + syntax: {type: "pow", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "//", + syntax: {type: "root", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "%%", + syntax: {type: "log", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "n rem 2", + syntax: {type: "rem", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "n"} + ]}, + {type: "literal", value: 2} + ]} + }, + + { + path: "min()", + syntax: {type: "min", args: [ + {type: "value"} + ]} + }, + + { + path: "min{x}", + syntax: {type: "minBlock", args: [ + {type: "value"}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "x"} + ]} + ]} + }, + + { + path: "array.max()", + syntax: {type: "max", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "array"} + ]} + ]} + }, + + { + path: "#id", + syntax: {type: "element", id: "id"} + }, + + { + path: "#body.classList.has('darkmode')", + syntax: {type: "has", args: [ + {type: "property", args: [ + {type: "element", id: "body"}, + {type: "literal", value: "classList"} + ]}, + {type: "literal", value: "darkmode"} + ]} + }, + + { + path: "@owner", + syntax: {type: "component", label: "owner"} + }, + + { + path: "x ? a : b", + syntax: {type: "if", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "x"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} + ]} + }, + + { + path: "x ? a : y ? b : c", + syntax: {type: "if", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "x"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "if", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "y"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "c"} + ]} + ]} + ]} + }, + + { + path: "x ? y ? a : b : z ? c : d", + syntax: {type: "if", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "x"} + ]}, + {type: "if", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "y"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} + ]}, + {type: "if", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "z"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "c"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "d"} + ]} + ]} + ]} + }, + + { + path: "(x ? a : b) ? c : d", + syntax: {type: "if", args: [ + {type: "if", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "x"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "c"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "d"} + ]} + ]} + }, + + { + path: "x ?? 10", + syntax: {type: "default", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "x"} + ]}, + {type: "literal", value: 10} + ]} + }, + + { + path: "!x ?? 10", + syntax: {type: "default", args: [ + {type: "not", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "x"} + ]}, + ]}, + {type: "literal", value: 10} + ]} + }, + + { + path: "x ?? 10 + 10", + syntax: {type: "add", args: [ + {type: "default", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "x"} + ]}, + {type: "literal", value: 10} + ]}, + {type: "literal", value: 10} + ]} + }, + + { + path: "x ?? y ?? 10", + syntax: {type: "default", args: [ + {type: "default", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "x"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "y"} + ]} + ]}, + {type: "literal", value: 10} + ]} + }, + + { + path: '&range(10, 20, 1)', + syntax: {type: "range", args: [ + {type: "literal", value: 10}, + {type: "literal", value: 20}, + {type: "literal", value: 1} + ], inline: true} + }, + + { + path: "1 <=> 2", + syntax: {type: "compare", args: [ + {type: "literal", value: 1}, + {type: "literal", value: 2} + ]} + }, + + { + path: "group{}", + syntax: {type: "groupBlock", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "groupMap{}", + syntax: {type: "groupMapBlock", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "()", + syntax: {type: "value"}, + nonCanon: true + }, + + { + path: "path(this)", + syntax: {type: "path", args: [ + {type: "value"}, + {type: "value"} + ]} + }, + + { + path: "path(this, this)", + syntax: {type: "path", args: [ + {type: "value"}, + {type: "value"}, + {type: "value"} + ]} + }, + + // Extended component sheet syntax + + { + path: "\n@foo < 'module' Module {\n}\n\n", + syntax: {type: "sheet", blocks: [ + {type: "block", + label: "foo", + module: "module", + exports: {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "Module"}, + ]}, + connection: "prototype", + statements: [] + } + ]}, + options: { + startRule: "sheet" + } + }, + + { + path: "\n@foo : 'module' {\n}\n\n", + syntax: {type: "sheet", blocks: [ + {type: "block", + label: "foo", + module: "module", + connection: "object", + statements: [] + } + ]}, + options: { + startRule: "sheet" + } + }, + + { + path: "\n@foo {\n a <-> b;\n}\n\n", + syntax: {type: "sheet", blocks: [ + {type: "block", label: "foo", statements: [ + {type: "bind2", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} + ]} + ]} + ]}, + options: { + startRule: "sheet" + } + }, + + { + path: "\n@foo {\n a <-> b, converter: @converter;\n}\n\n", + syntax: {type: "sheet", blocks: [ + {type: "block", label: "foo", statements: [ + {type: "bind2", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} + ], descriptor: { + converter: {type: "component", label: "converter"} + }} + ]} + ]}, + options: { + startRule: "sheet" + } + }, + + { + path: "\n@foo {\n a: 10;\n}\n\n", + syntax: {type: "sheet", blocks: [ + {type: "block", label: "foo", statements: [ + {type: "assign", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "literal", value: 10} + ]} + ]} + ]}, + options: { + startRule: "sheet" + } + }, + + { + path: "\n@foo {\n on action -> @foo;\n}\n\n", + syntax: {type: "sheet", blocks: [ + {type: "block", label: "foo", statements: [ + { + type: "event", + event: "action", + when: "on", + listener: {type: "component", label: "foo"} + } + ]} + ]}, + options: { + startRule: "sheet" + } + }, + + { + path: "@foo:thingy", + syntax: { type: "component", label: "foo:thingy" } + }, + + // Invalid syntax + + { + path: "(", + invalid: "Expected end of input but \"(\" found." + }, + + { + path: ")", + invalid: "Expected end of input but \")\" found." + }, + + { + path: "[", + invalid: "Expected end of input but \"[\" found." + }, + + { + path: "]", + invalid: "Expected end of input but \"]\" found." + }, + + { + path: "#", + invalid: "Expected end of input but \"#\" found." + } + +]; diff --git a/core/frb/spec/logic-bindings-spec.js b/core/frb/spec/logic-bindings-spec.js new file mode 100644 index 0000000000..ed92bdf54e --- /dev/null +++ b/core/frb/spec/logic-bindings-spec.js @@ -0,0 +1,219 @@ + +var Bindings = require("../bindings"); + +describe("logic bindings", function () { + + describe("and bindings", function () { + + it("one way, true to false", function () { + var object = Bindings.defineBindings({}, { + "a && b": {"<-": "c"} + }); + expect(object.a).toBe(undefined); + expect(object.b).toBe(undefined); + expect(object.c).toBe(undefined); + + object.c = true; + expect(object.a).toBe(true); + expect(object.b).toBe(true); + expect(object.c).toBe(true); + + object.c = false; + expect(object.a).toBe(false); + expect(object.b).toBe(true); + expect(object.c).toBe(false); + }); + + it("one way, false to true", function () { + var object = Bindings.defineBindings({}, { + "a && b": {"<-": "c"} + }); + expect(object.a).toBe(undefined); + expect(object.b).toBe(undefined); + expect(object.c).toBe(undefined); + + object.c = false; + expect(object.a).toBe(undefined); + expect(object.b).toBe(undefined); + expect(object.c).toBe(false); + + object.c = true; + expect(object.a).toBe(true); + expect(object.b).toBe(true); + expect(object.c).toBe(true); + }); + + it("two-way, a, b, c", function () { + var object = Bindings.defineBindings({}, { + "a && b": {"<->": "c"} + }); + expect(object.a).toBe(undefined); + expect(object.b).toBe(undefined); + expect(object.c).toBe(undefined); + + object.a = true; + expect(object.a).toBe(true); + expect(object.b).toBe(undefined); + expect(object.c).toBe(undefined); + + object.b = true; + expect(object.a).toBe(true); + expect(object.b).toBe(true); + expect(object.c).toBe(true); + + object.c = false; + expect(object.a).toBe(false); + expect(object.b).toBe(true); + expect(object.c).toBe(false); + + object.a = true; + expect(object.a).toBe(true); + expect(object.b).toBe(true); + expect(object.c).toBe(true); + }); + + it("two-way, b, a, c", function () { + var object = Bindings.defineBindings({}, { + "a && b": {"<->": "c"} + }); + expect(object.a).toBe(undefined); + expect(object.b).toBe(undefined); + expect(object.c).toBe(undefined); + + object.b = true; + expect(object.a).toBe(undefined); + expect(object.b).toBe(true); + expect(object.c).toBe(undefined); + + object.a = true; + expect(object.a).toBe(true); + expect(object.b).toBe(true); + expect(object.c).toBe(true); + + object.c = false; + expect(object.a).toBe(false); + expect(object.b).toBe(true); + expect(object.c).toBe(false); + + object.a = true; + expect(object.a).toBe(true); + expect(object.b).toBe(true); + expect(object.c).toBe(true); + }); + + }); + + describe("or bindings", function () { + + it("one way, true to false", function () { + var object = Bindings.defineBindings({}, { + "a || b": {"<-": "c"} + }); + expect(object.a).toBe(undefined); + expect(object.b).toBe(undefined); + expect(object.c).toBe(undefined); + + object.c = true; + expect(object.a).toBe(true); + expect(object.b).toBe(undefined); + expect(object.c).toBe(true); + + object.c = false; + expect(object.a).toBe(false); + expect(object.b).toBe(false); + expect(object.c).toBe(false); + }); + + it("one way, false to true", function () { + var object = Bindings.defineBindings({}, { + "a || b": {"<-": "c"} + }); + expect(object.a).toBe(undefined); + expect(object.b).toBe(undefined); + expect(object.c).toBe(undefined); + + object.c = false; + expect(object.a).toBe(false); + expect(object.b).toBe(false); + expect(object.c).toBe(false); + + object.c = true; + expect(object.a).toBe(true); + expect(object.b).toBe(false); + expect(object.c).toBe(true); + }); + + it("two-way, a, b, c", function () { + var object = Bindings.defineBindings({}, { + "a || b": {"<->": "c"} + }); + expect(object.a).toBe(undefined); + expect(object.b).toBe(undefined); + expect(object.c).toBe(undefined); + + object.a = true; + expect(object.a).toBe(true); + expect(object.b).toBe(undefined); + expect(object.c).toBe(true); + + object.b = true; + expect(object.a).toBe(true); + expect(object.b).toBe(true); + expect(object.c).toBe(true); + + object.c = false; + expect(object.a).toBe(false); + expect(object.b).toBe(false); + expect(object.c).toBe(false); + + object.a = true; + expect(object.a).toBe(true); + expect(object.b).toBe(false); + expect(object.c).toBe(true); + }); + + it("two-way, b, a, c", function () { + var object = Bindings.defineBindings({}, { + "a || b": {"<->": "c"} + }); + expect(object.a).toBe(undefined); + expect(object.b).toBe(undefined); + expect(object.c).toBe(undefined); + + object.b = true; + expect(object.a).toBe(undefined); + expect(object.b).toBe(true); + expect(object.c).toBe(true); + + object.a = true; + expect(object.a).toBe(true); + expect(object.b).toBe(true); + expect(object.c).toBe(true); + + object.c = false; + expect(object.a).toBe(false); + expect(object.b).toBe(false); + expect(object.c).toBe(false); + + object.a = true; + expect(object.a).toBe(true); + expect(object.b).toBe(false); + expect(object.c).toBe(true); + }); + + it("two-way with algebra solver", function () { + var object = Bindings.defineBindings({}, { + "a || !b": {"<->": "c"} + }); + expect(object.a).toBe(undefined); + expect(object.b).toBe(undefined); + expect(object.c).toBe(true); + + object.c = false; + expect(object.a).toBe(false); + expect(object.b).toBe(true); + expect(object.c).toBe(false); + }); + + }); +}); diff --git a/core/frb/spec/merge-spec.js b/core/frb/spec/merge-spec.js new file mode 100644 index 0000000000..19164c1b54 --- /dev/null +++ b/core/frb/spec/merge-spec.js @@ -0,0 +1,181 @@ + +require("collections/shim"); +var Merge = require("../merge"); +var ot = Merge.ot; +var diff = Merge.diff; +var apply = Merge.apply; +var merge = Merge.merge; +var graphOt = Merge.graphOt; + +var specs = [ + { + name: "constraint", + target: "abcde", + source: "ace", + cost: 2, + ot: [ + ["retain", 1], + ["delete", 1], + ["retain", 1], + ["delete", 1], + ["retain", 1] + ], + diff: [ + // abcde + [1, 1], // acde + [2, 1] // ace + ] + }, + { + name: "relaxation", + target: "ace", + source: "abcde", + cost: 2, + ot: [ + ["retain", 1], + ["insert", 1], + ["retain", 1], + ["insert", 1], + ["retain", 1] + ], + diff: [ + // ace + [1, 0, "b"], // abce + [3, 0, "d"] // abcde + ] + }, + { + name: "duplication", + target: "abc", + source: "aabbcc", + cost: 3, + ot: [ + ["insert", 1], + ["retain", 1], + ["insert", 1], + ["retain", 1], + ["insert", 1], + ["retain", 1] + ], + diff: [ + // abc + [0, 0, "a"], // aabc + [2, 0, "b"], // aabbc + [4, 0, "c"] + ] + }, + { + name: "de-duplication", + target: "aabbcc", + source: "abc", + cost: 3, + ot: [ + ["delete", 1], + ["retain", 1], + ["delete", 1], + ["retain", 1], + ["delete", 1], + ["retain", 1] + ], + diff: [ + // aabbcc + [0, 1], // abbcc + [1, 1], // abcc + [2, 1] // abc + ] + }, + { + name: "reversal", + target: "fedcba", + source: "abcdef", + cost: 10, + ot: [ + ["delete", 5], + ["retain", 1], + ["insert", 5] + ], + diff: [ + // fedcba + [0, 5], // a + [1, 0, "bcdef"] // abcdef + ] + }, + { + name: "complete replacement", + target: "aaa", + source: "bbb", + cost: 6, + ot: [ + ["delete", 3], + ["insert", 3] + ], + diff: [ + // aaa + [0, 3, "bbb"] // bbb + ] + }, + { + name: "complete retention", + target: "aaa", + source: "aaa", + cost: 0, + ot: [ + ["retain", 3], + ], + diff: [ + ] + } +]; + +describe("graphOt cost", function () { + specs.forEach(function (spec) { + if (spec.ot) { + it("should compute cost of " + spec.name, function () { + expect(graphOt(spec.target, spec.source).cost).toEqual(spec.cost); + }); + } + }); +}); + +describe("ot", function () { + specs.forEach(function (spec) { + if (spec.ot) { + it("should compute shortest OT for " + spec.name, function () { + expect(ot(spec.target, spec.source)).toEqual(spec.ot); + }); + } + }); +}); + +describe("diff", function () { + specs.forEach(function (spec) { + if (spec.diff) { + it("should diff " + spec.name, function () { + expect(diff(spec.target, spec.source)).toEqual(spec.diff); + }); + } + }); +}); + +describe("apply", function () { + specs.forEach(function (spec) { + if (spec.diff) { + it("should apply a patch for " + spec.name, function () { + var target = spec.target.split(""); + apply(target, spec.diff); + expect(target.join("")).toEqual(spec.source); + }); + } + }); +}); + +describe("merge", function () { + specs.forEach(function (spec) { + it("should merge for " + spec.name, function () { + var target = spec.target.split(""); + merge(target, spec.source); + expect(target.join("")).toEqual(spec.source); + }); + }); +}); + diff --git a/core/frb/spec/min-max-spec.js b/core/frb/spec/min-max-spec.js new file mode 100644 index 0000000000..5d287716fc --- /dev/null +++ b/core/frb/spec/min-max-spec.js @@ -0,0 +1,51 @@ + +var Bindings = require("../bindings"); + +Error.stackTraceLimit = 100; + +describe("min and max blocks", function () { + it("should work", function () { + + var a = {x: 3}; + var b = {x: 2}; + var c = {x: 1}; + var d = {x: 0}; + + var object = Bindings.defineBindings({}, { + max: {"<-": "objects.max{x}"}, + min: {"<-": "objects.min{x}"} + }); + + expect(object.max).toBe(void 0); + expect(object.min).toBe(void 0); + + object.objects = [d, a, c, b]; + expect(object.max.x).toBe(a.x); + expect(object.min.x).toBe(0); + + object.objects.shift(); // [a, c, b] + expect(object.max.x).toBe(a.x); + expect(object.min.x).toBe(1); + + object.objects.shift(); // [c, b] + expect(object.max.x).toBe(b.x); + expect(object.min.x).toBe(1); + + object.objects.splice(1, 0, a); // [c, a, b] + expect(object.max.x).toBe(a.x); + expect(object.min.x).toBe(1); + + object.objects.clear(); + expect(object.max).toBe(void 0); + expect(object.min).toBe(void 0); + + object.objects = [d]; + expect(object.max).toBe(d); + expect(object.min).toBe(d); + + object.objects = null; + expect(object.max).toBe(void 0); + expect(object.min).toBe(void 0); + + }); +}); diff --git a/core/frb/spec/observe-enumeration-spec.js b/core/frb/spec/observe-enumeration-spec.js new file mode 100644 index 0000000000..8b45b9d5c9 --- /dev/null +++ b/core/frb/spec/observe-enumeration-spec.js @@ -0,0 +1,56 @@ + +var bind = require("../bind"); + +describe("observe enumeration", function () { + + it("simple pipeline", function () { + var input = ['a', 'b', 'c']; + var output = []; + var cancel = bind(output, "rangeContent()", { + "<-": "enumerate()", + source: input + }); + var a = output[0]; + var b = output[1]; + var c = output[2]; + expect(output).toEqual([ + [0, 'a'], + [1, 'b'], + [2, 'c'] + ]); + input.unshift('z'); + expect(output).toEqual([ + [0, 'z'], + [1, 'a'], + [2, 'b'], + [3, 'c'] + ]); + expect(a[0]).toEqual(1); + }); + + it("complex pipeline", function () { + var input = ['b', 'c', 'd', 'e']; + var output = []; + var cancel = bind(output, "rangeContent()", { + "<-": "enumerate().map{!(.0 % 2)}", + source: input + }); + expect(output).toEqual([true, false, true, false]); + input.unshift('a'); + expect(output).toEqual([true, false, true, false, true]); + }); + + it("values at even indexes", function () { + var input = ['b', 'c', 'd', 'e']; + var output = []; + var cancel = bind(output, "rangeContent()", { + "<-": "enumerate().filter{!(.0 % 2)}.map{.1}", + source: input + }); + expect(output).toEqual(['b', 'd']); + input.unshift('a'); + expect(output).toEqual(['a', 'c', 'e']); + }); + +}); + diff --git a/core/frb/spec/observe-join-spec.js b/core/frb/spec/observe-join-spec.js new file mode 100644 index 0000000000..b916915cfc --- /dev/null +++ b/core/frb/spec/observe-join-spec.js @@ -0,0 +1,64 @@ + +var Bindings = require("../bindings"); + +describe("join", function () { + + it("should observe changes to the input", function () { + + var object = Bindings.defineBindings({}, { + joined: {"<->": "terms.join(delimiter)"} + }); + + object.terms = ['a', 'b', 'c']; + expect(object.joined).toBe(undefined); + + object.terms = null; + object.delimiter = ', '; + expect(object.joined).toBe(undefined); + + object.terms = ['a', 'b', 'c']; + expect(object.joined).toBe("a, b, c"); + + object.terms.push('d'); + expect(object.joined).toBe("a, b, c, d"); + + object.terms.clear(); + expect(object.joined).toBe(""); + + // -> + object.joined = 'x, y, z'; + expect(object.terms.slice()).toEqual(['x', 'y', 'z']); + }); + + it("two-way bindings should work for split as well", function () { + var object = Bindings.defineBindings({}, { + split: {"<->": "string.split(', ')"} + }); + + object.string = 'a, b, c'; + expect(object.split.slice()).toEqual(['a', 'b', 'c']); + + object.split = ['x', 'y']; + expect(object.string).toEqual('x, y'); + }); + + it("should join on null string if no argument given", function () { + + var object = Bindings.defineBindings({}, { + joined: {"<-": "terms.join()"} + }); + + expect(object.joined).toBe(undefined); + + object.terms = ['a', 'b', 'c']; + expect(object.joined).toBe('abc'); + + object.terms.push('d'); + expect(object.joined).toBe("abcd"); + + object.terms.clear(); + expect(object.joined).toBe(""); + }); + +}); + diff --git a/core/frb/spec/observe-sorted-set-spec.js b/core/frb/spec/observe-sorted-set-spec.js new file mode 100644 index 0000000000..c2e9ebce86 --- /dev/null +++ b/core/frb/spec/observe-sorted-set-spec.js @@ -0,0 +1,39 @@ + +var Bindings = require("../bindings"); + +describe("observe sorted set", function () { + it("should work", function () { + + var a = {name: 'a', index: 0}; + var b = {name: 'b', index: 0}; + var c = {name: 'c', index: 0}; + var d = {name: 'd', index: 0}; + + var array = [a, b, c, d]; + + var object = Bindings.defineBindings({ + array: array + }, { + "sortedSet": {"<-": "array.sortedSet{index}"} + }); + + expect(object.sortedSet.toArray()).toEqual([d]); + + d.index = 3; + expect(object.sortedSet.toArray()).toEqual([c, d]); + + c.index = 2; + expect(object.sortedSet.toArray()).toEqual([b, c, d]); + + b.index = 1; + expect(object.sortedSet.toArray()).toEqual([a, b, c, d]); + + a.index = 4; + expect(object.sortedSet.toArray()).toEqual([b, c, d, a]); + + b.index = 4; + expect(object.sortedSet.toArray()).toEqual([c, d, b]); + + }); +}); + diff --git a/core/frb/spec/observe-sorted-spec.js b/core/frb/spec/observe-sorted-spec.js new file mode 100644 index 0000000000..b07dda80c0 --- /dev/null +++ b/core/frb/spec/observe-sorted-spec.js @@ -0,0 +1,30 @@ + +var Bindings = require("../bindings"); + +describe("sorted block observer", function () { + + it("", function () { + + var array = [ + {key: 0, value: 'c'}, + {key: 1, value: 'b'}, + {key: 2, value: 'a'} + ]; + + var object = Bindings.defineBindings({ + array: array + }, { + sorted: {"<-": "array.sorted{value}"} + }); + + expect(object.sorted).toEqual([array[2], array[1], array[0]]); + + array[1].value = 'd'; + expect(object.sorted).toEqual([array[2], array[0], array[1]]); + + array[1].value = 'd'; + expect(object.sorted).toEqual([array[2], array[0], array[1]]); + }); + +}); + diff --git a/core/frb/spec/observe-spec.js b/core/frb/spec/observe-spec.js new file mode 100644 index 0000000000..c053adf07b --- /dev/null +++ b/core/frb/spec/observe-spec.js @@ -0,0 +1,141 @@ +var observe = require("../observe"); + +describe("observe", function () { + + it("should observe a property", function () { + var spy = jasmine.createSpy(); + var object = {}; + + var cancel = observe(object, "a", spy); + expect(spy.argsForCall).toEqual([]); + + object.a = 10; + expect(spy.argsForCall).toEqual([ + [10, 'a', object] + ]); + + cancel(); + object.a = 20; + expect(spy.argsForCall).toEqual([ + [10, 'a', object] + ]); + + }); + + it("should observe a property before it changes", function () { + var spy = jasmine.createSpy(); + var object = {}; + var cancel = observe(object, 'a', { + change: function (value) { + expect(value).toBe(10); + spy(); + }, + beforeChange: true + }); + object.a = 10; + object.a = 20; + expect(spy).toHaveBeenCalled(); + }); + + it("should observe incremental changes", function () { + var spy = jasmine.createSpy(); + var object = {}; + var cancel = observe(object, 'array', { + change: function (array) { + spy(array.slice()); + }, + contentChange: true + }); + object.array = []; + object.array.push(10); + object.array.pop(); + object.array = []; + cancel(); + object.array = [10]; + expect(spy.argsForCall).toEqual([ + [[]], + [[10]], + [[]], + [[]] + ]); + }); + + it("should observe content changes", function () { + var spy = jasmine.createSpy(); + var object = {}; + var cancel = observe(object, 'array', { + contentChange: function (plus, minus, index) { + spy(plus, minus, index); + }, + }); + object.array = []; + object.array.push(10); + object.array.pop(); + object.array = []; + cancel(); + object.array = [10]; + expect(spy.argsForCall).toEqual([ + //[[], [], 0], + [[10], [], 0], + [[], [10], 0] + //[[]] + ]); + }); + + it("should pass content-less values through content-change-observer", function () { + var spy = jasmine.createSpy(); + var object = {}; + var cancel = observe(object, 'array', { + change: function (array) { + spy(array); + }, + contentChange: true + }); + object.array = 10; + expect(spy.argsForCall).toEqual([ + [10] + ]); + }); + + it("should observe a range property before its content changes", function () { + var spy = jasmine.createSpy(); + var originalArray = [10]; + var object = { + a: originalArray + }; + + observe(object, 'a', { + change: function (value) { + expect(value).toBe(originalArray); + expect(value.length).toBe(1); + spy(); + }, + beforeChange: true, + contentChange: true + }); + + originalArray.push(20); + + expect(spy).toHaveBeenCalled(); + }); + + it("should observe a range property after its content changes", function () { + var spy = jasmine.createSpy(); + var originalArray = [10]; + var object = { + a: originalArray + }; + + observe(object, 'a', { + change: spy, + contentChange: true + }); + + originalArray.push(20); + + var updatedArray = spy.mostRecentCall.args[0]; + expect(updatedArray).toEqual(originalArray); + expect(updatedArray.length).toBe(2); + + }); +}); diff --git a/core/frb/spec/observers-spec.js b/core/frb/spec/observers-spec.js new file mode 100644 index 0000000000..2b82cc1dea --- /dev/null +++ b/core/frb/spec/observers-spec.js @@ -0,0 +1,696 @@ + +require("collections/shim"); +var Map = require("collections/map"); +var Observers = require("../observers"); +var Operators = require("../operators"); +var Scope = require("../scope"); +var Signal = require("../signal"); + +// This observer is not used by the language, but is useful for testing +function makeRelationObserver(relation, thisp) { + return function observeRelation(emit, scope) { + return emit(relation.call(thisp, scope.value)) || Function.noop; + }; +} + +describe("makeLiteralObserver", function () { + // TODO +}); + +describe("observeValue", function () { + // TODO +}); + +describe("observeParent", function () { + // TODO +}); + +describe("makeElementObserver", function () { + // TODO +}); + +describe("makeComponentObserver", function () { + // TODO +}); + +describe("makeConverterObserver", function () { + // TODO +}); + +describe("makeComputerObserver", function () { + // TODO +}); + +describe("observeProperty", function () { + it("should work", function () { + var object = {}; + var spy = jasmine.createSpy(); + var cancel = Observers.observeProperty(object, "a", spy, new Scope()); + expect(spy).toHaveBeenCalledWith(undefined, "a", object); + object.a = 10; + expect(spy).toHaveBeenCalledWith(10, "a", object); + cancel(); + object.a = 20; + }); + + it("should be cancelable", function () { + var object = {}; + var spy = jasmine.createSpy(); + var cancel = Observers.observeProperty(object, "a", spy, new Scope()); + object.a = 10; + cancel(); + object.b = 20; + expect(spy.argsForCall).toEqual([ + [undefined, "a", object], + [10, "a", object] + ]); + }); + + // Note that we cannot observe property deletion because that is a + // configuration change, not intercepted by "set" on a property descriptor. + +}); + +describe("makePropertyObserver", function () { + + it("should react to property changes", function () { + + var object = {foo: 10}; + + var cancel = Observers.makePropertyObserver( + Observers.makeLiteralObserver(object), + Observers.makeLiteralObserver("foo") + )(function (value) { + object.bar = value; + }, new Scope()); + + object.foo = 20; + expect(object.bar).toBe(20); + + cancel(); + object.foo = 30; + expect(object.bar).toBe(20); + + }); + + it("should react to property changes", function () { + + var object = {}; + var observeObject = Observers.observeValue; + var observeKey = Observers.makeLiteralObserver("foo"); + var observeValue = Observers.makePropertyObserver(observeObject, observeKey); + var spy = jasmine.createSpy(); + var cancel = observeValue(spy, new Scope(object)); + + expect(spy).toHaveBeenCalledWith(undefined, "foo", object); + object.foo = 10; + + cancel(); + object.foo = 20; + expect(spy).toHaveBeenCalledWith(10, "foo", object); + expect(spy.callCount).toBe(2); + + }); + +}); + +describe("observeGet", function () { + + it("should work", function () { + var map = new Map(); + var spy = jasmine.createSpy(); + var cancel = Observers.observeGet(map, "a", spy, new Scope()); + expect(spy).toHaveBeenCalledWith(undefined, "a", map); + map.set("a", 10); + expect(spy).toHaveBeenCalledWith(10, "a", map); + cancel(); + map.set("a", 20); + }); + + it("should be cancelable", function () { + var map = new Map(); + var spy = jasmine.createSpy(); + var cancel = Observers.observeGet(map, "a", spy, new Scope()); + map.set("a", 10); + cancel(); + map.set("b", 20); + expect(spy.argsForCall).toEqual([ + [undefined, "a", map], + [10, "a", map] + ]); + }); + + it("should observe deletion", function () { + var map = Map.from({a: 10}); + var spy = jasmine.createSpy(); + var cancel = Observers.observeGet(map, "a", spy, new Scope()); + map.delete("a"); + cancel(); + map.set("a", 20); + expect(spy.argsForCall).toEqual([ + [10, "a", map], + [undefined, "a", map] + ]); + }); + +}); + +describe("makeGetObserver", function () { + // TODO +}); + +describe("makeHasObserver", function () { + // TODO +}); + +// Compound Literals + +describe("makeArrayObserver", function () { + + it("should react to changes to each value", function () { + + var array; + var object = {a: 1, b: 2, c: 3}; + + var cancel = Observers.makeArrayObserver( + Observers.makePropertyObserver( + Observers.makeLiteralObserver(object), + Observers.makeLiteralObserver('a') + ), + Observers.makePropertyObserver( + Observers.makeLiteralObserver(object), + Observers.makeLiteralObserver('b') + ), + Observers.makePropertyObserver( + Observers.makeLiteralObserver(object), + Observers.makeLiteralObserver('c') + ) + )(function (_array) { + array = _array; + }, new Scope()); + + expect(array).toEqual([1, 2, 3]); + object.a = 0; + expect(array).toEqual([0, 2, 3]); + cancel(); + object.a = 1; + expect(array).toEqual([0, 2, 3]); + + }); + +}); + +// TODO makeObjectObserver + +// Operators + +describe("makeNotObserver", function () { + + it("should work", function () { + var valueSignal = new Signal(false); + var makeNotObserver = Observers.makeOperatorObserverMaker(Operators.not); + var observeNot = makeNotObserver(valueSignal.observe); + var spy = jasmine.createSpy(); + var cancel = observeNot(spy, new Scope()); + expect(spy).toHaveBeenCalledWith(true); + valueSignal.emit(true); + expect(spy).toHaveBeenCalledWith(false); + cancel(); + valueSignal.emit(false); + expect(spy.callCount).toBe(2); + }); + +}); + +describe("makeDefinedObserver", function () { + + it("should work", function () { + var valueSignal = new Signal(); + var observeDefined = Observers.makeDefinedObserver(valueSignal.observe); + var spy = jasmine.createSpy(); + var cancel = observeDefined(spy, new Scope()); + expect(spy).toHaveBeenCalledWith(false); + valueSignal.emit(1); + cancel(); + }); + +}); + +describe("makeDefinedObserver", function () { + // TODO +}); + +describe("makeDefaultObserver", function () { + // TODO +}); + +// Comprehensions + +describe("makeMapBlockObserver", function () { + + it("should project range content changes", function () { + + var spy = jasmine.createSpy(); + + var array = [1,2,3]; + + var cancel = Observers.makeMapBlockObserver( + Observers.makeLiteralObserver(array), + makeRelationObserver(function (n) { + return n * 2; + }) + )(function (mapped) { + function rangeChange(plus, minus, index) { + spy(index, minus.slice(), plus.slice()); + } + rangeChange(mapped, [], 0); + mapped.addRangeChangeListener(rangeChange); + }, new Scope()); + + array.push(4); + array.set(0, 0); + cancel(); + array.push(5); + + expect(spy.argsForCall).toEqual([ + [0, [], [2, 4, 6]], + [3, [], [8]], + [0, [2], [0]] + ]); + + }); + + it("should project replacement of the source", function () { + + var spy = jasmine.createSpy(); + + var object = {array: [1,2,3]}; + + var cancel = Observers.makeMapBlockObserver( + Observers.makePropertyObserver( + Observers.makeLiteralObserver(object), + Observers.makeLiteralObserver('array') + ), + makeRelationObserver(function (n) { + return n * 2; + }) + )(function (mapped) { + function rangeChange(plus, minus, index) { + spy(index, minus.slice(), plus.slice()); + } + rangeChange(mapped, [], 0); + mapped.addRangeChangeListener(rangeChange); + }, new Scope()); + + object.array.push(4); + object.array = []; + object.array.push(1, 2, 3); + var array = object.array; + + cancel(); + array.push('a'); + object.array = 10; + + expect(spy.argsForCall).toEqual([ + [0, [], [2, 4, 6]], + [3, [], [8]], + [0, [2, 4, 6, 8], []], + [0, [], [2, 4, 6]] + ]); + + }); + + it("should grant access to the parent scope", function () { + var input = {values: [1, 2, 3], factor: 2}; + // values.map{this * ^factor} + var observeValues = Observers.makeMapBlockObserver( + Observers.makePropertyObserver( + Observers.observeValue, + Observers.makeLiteralObserver("values") + ), + Observers.makeOperatorObserverMaker(Operators.mul)( + Observers.observeValue, + Observers.makeParentObserver( + Observers.makePropertyObserver( + Observers.observeValue, + Observers.makeLiteralObserver("factor") + ) + ) + ) + ); + var spy = jasmine.createSpy(); + var cancel = observeValues(spy, new Scope(input)); + expect(spy).toHaveBeenCalledWith([2, 4, 6]); + }); + +}); + +describe("makeFilterBlockObserver", function () { + + it("should work", function () { + var input = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + var output; + var cancel = Observers.makeRangeContentObserver(Observers.makeFilterBlockObserver( + Observers.makeLiteralObserver(input), + makeRelationObserver(function (value) { + return !(value & 1); + }) + ))(function (_output) { + output = _output.slice(); + }, new Scope()); + + expect(output).toEqual([2, 4, 6, 8]); + + input.push(10, 11, 12); + expect(output).toEqual([2, 4, 6, 8, 10, 12]); + + input.splice(2, 4); + expect(output).toEqual([2, 8, 10, 12]); + + input.shift(); + expect(output).toEqual([2, 8, 10, 12]); + + input.shift(); + expect(output).toEqual([8, 10, 12]); + + input.push(13, 13, 13, 13, 13, 13); + expect(output).toEqual([8, 10, 12]); + + input.push(14); + expect(output).toEqual([8, 10, 12, 14]); + + }); + +}); + +describe("makeSortedBlockObserver", function () { + + it("should work", function () { + + var input = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + var output; + var cancel = Observers.makeRangeContentObserver(Observers.makeSortedBlockObserver( + Observers.makeLiteralObserver(input), + makeRelationObserver(function (value) { + return -value; + }) + ))(function (_output) { + output = _output.slice(); + }, new Scope()); + + expect(output).toEqual([9, 8, 7, 6, 5, 4, 3, 2, 1]); + + input.shift(); + expect(output).toEqual([9, 8, 7, 6, 5, 4, 3, 2]); + + input.reverse(); + expect(output).toEqual([9, 8, 7, 6, 5, 4, 3, 2]); + + input.sort(); + expect(output).toEqual([9, 8, 7, 6, 5, 4, 3, 2]); + + input.pop(); + expect(output).toEqual([8, 7, 6, 5, 4, 3, 2]); + + input.push(4.5); + expect(output).toEqual([8, 7, 6, 5, 4.5, 4, 3, 2]); + + cancel(); + + input.clear(); + expect(output).toEqual([8, 7, 6, 5, 4.5, 4, 3, 2]); + + }); +}); + +// TODO makeSomeBlockObserver +// TODO makeEveryBlockObserver +// TODO makeMinBlockObserver +// TODO makeMaxBlockObserver +// TODO makeSortedSetBlockObserver + +describe("makeReversedObserver", function () { + + it("should reverse", function () { + + var spy = jasmine.createSpy(); + + var array = [1,2,3]; + + var cancel = Observers.makeReversedObserver( + Observers.makeLiteralObserver(array) + )(function (reversed) { + function rangeChange(plus, minus, index) { + spy(index, minus.slice(), plus.slice()); + } + rangeChange(reversed, [], 0); + reversed.addRangeChangeListener(rangeChange); + }, new Scope()); + + array.push(4); + array.set(0, 0); + cancel(); + array.push(5); + + expect(spy.argsForCall).toEqual([ + [0, [], [3, 2, 1]], + [0, [], [4]], + [3, [1], [0]] + ]); + + }); +}); + +describe("makeFlattenObserver", function () { + + it("should work", function () { + + var input = [[1, 2, 3], [7, 8, 9]]; + var output; + var calls = 0; + + var cancel = Observers.makeFlattenObserver(Observers.makeLiteralObserver(input)) + (function (_output) { + output = _output; + calls++; + }, new Scope()); + + input.swap(1, 0, [[4, 5, 6]]); + //expect(input).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9]]); + expect(output).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); + + input[1].splice(1, 1); + //expect(input).toEqual([[1, 2, 3], [4, 6], [7, 8, 9]]); + expect(output).toEqual([1, 2, 3, 4, 6, 7, 8, 9]); + + input.splice(1, 1); + //expect(input).toEqual([[1, 2, 3], [7, 8, 9]]); + expect(output).toEqual([1, 2, 3, 7, 8, 9]); + + input.push([10]); + //expect(input).toEqual([[1, 2, 3], [7, 8, 9], [10]]); + expect(output).toEqual([1, 2, 3, 7, 8, 9, 10]); + + input.shift(); + //expect(input).toEqual([[7, 8, 9], [10]]); + expect(output).toEqual([7, 8, 9, 10]); + + input.unshift([1, 2, 3]); + //expect(input).toEqual([[1, 2, 3], [7, 8, 9], [10]]); + expect(output).toEqual([1, 2, 3, 7, 8, 9, 10]); + + input[0].push(4, 5, 6); + //expect(input).toEqual([[1, 2, 3, 4, 5, 6], [7, 8, 9], [10]]); + expect(output).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + cancel(); + input.push([11]); + input[0].unshift(0); + //expect(input).toEqual([[0, 1, 2, 3, 4, 5, 6], [7, 8, 9], [10], [11]]); + expect(output).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + expect(calls).toBe(1); + + }); + + it("should work when the source is replaced", function () { + + var array = [[1, 2, 3], [7, 8, 9]]; + var object = {array: array}; + var output; + + var cancel = Observers.makeFlattenObserver( + Observers.makePropertyObserver( + Observers.makeLiteralObserver(object), + Observers.makeLiteralObserver('array') + ) + )(function (_output) { + output = _output; + }, new Scope()); + + expect(output).toEqual([1, 2, 3, 7, 8, 9]); + + array.splice(1, 0, [4, 5, 6]); + expect(output).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); + + array[1].splice(1, 1); + expect(output).toEqual([1, 2, 3, 4, 6, 7, 8, 9]); + + object.array = []; + expect(output).toEqual([]); + + object.array.push([1, 2, 3]); + expect(output).toEqual([1, 2, 3]); + + cancel(); + + array.push([10]); + object.array = [['a', 'b', 'c']]; + expect(output).toEqual([1, 2, 3]); + + }); + +}); + +describe("makeSumObserver", function () { + it("should react to array changes and replacements", function () { + + var sum; + var object = {array: [1,2,3]}; + + var cancel = Observers.makeSumObserver( + Observers.makePropertyObserver( + Observers.makeLiteralObserver(object), + Observers.makeLiteralObserver('array') + ) + )(function (_sum) { + sum = _sum; + }, new Scope()); + + expect(sum).toBe(6); + + object.array.push(4); + expect(sum).toBe(10); + + object.array = []; + expect(sum).toBe(0); + + object.array.push(1, 2, 3); + expect(sum).toBe(6); + + object.array.push(0); + expect(sum).toBe(6); + + }); + +}); + +describe("makeAverageObserver", function () { + + it("should react to array changes and replacements", function () { + + var average; + var object = {array: [1,2,3]}; + + var cancel = Observers.makeAverageObserver( + Observers.makePropertyObserver( + Observers.makeLiteralObserver(object), + Observers.makeLiteralObserver('array') + ) + )(function (_average) { + average = _average; + }, new Scope()); + + expect(average).toBe(2); + + object.array.push(4); + expect(average).toBe(2.5); + + object.array = []; + expect(isNaN(average)).toBe(true); + + object.array.push(1, 2, 3); + expect(average).toBe(2); + + object.array.push(0); + expect(average).toBe(1.5); + + }); + +}); + +describe("makeViewObserver", function () { + // TODO +}); + +// Advanced Compound Observers + +xdescribe("makeExpressionObserver", function () { + + it("should work", function () { + var inputSignal = new Signal(); + var expressionSignal = new Signal(); + var observeOutput = Observers.makeExpressionObserver( + inputSignal.observe, + expressionSignal.observe + ); + var spy = jasmine.createSpy(); + var cancel = observeOutput(spy, new Scope()); + expect(spy).not.toHaveBeenCalled(); + + inputSignal.emit(10); + expect(spy).not.toHaveBeenCalled(); + + expressionSignal.emit("this + 1"); + expect(spy).toHaveBeenCalledWith(11); + + }); + +}); + +describe("makeWithObserver", function () { + // TODO +}); + +describe("makeConditionalObserver", function () { + // TODO +}); + +describe("makeOrObserver", function () { + // TODO +}); + +describe("makeAndObserver", function () { + // TODO +}); + +describe("makeOnlyObserver", function () { + it("should work", function () { + var collectionSignal = new Signal; + var observeOutput = Observers.makeOnlyObserver( + collectionSignal.observe + ); + var spy = jasmine.createSpy(); + var cancel = observeOutput(spy, new Scope()); + expect(spy).not.toHaveBeenCalled(); + + collectionSignal.emit([1]); + expect(spy).toHaveBeenCalledWith(1); + }) +}); + + +// Utility Methods +// --------------- + +describe("makeUniq", function () { + // TODO +}); + +describe("autoCancelPrevious", function () { + // TODO +}); + +describe("once", function () { + // TODO +}); diff --git a/core/frb/spec/only-binder-spec.js b/core/frb/spec/only-binder-spec.js new file mode 100644 index 0000000000..72ba150a01 --- /dev/null +++ b/core/frb/spec/only-binder-spec.js @@ -0,0 +1,44 @@ + +var Bindings = require(".."); +var Set = require("collections/set"); + +describe("only binder", function () { + + it("should bind arrays", function () { + var object = Bindings.defineBindings({ + array: [1, 2, 3], + item: 4 + }, { + "array.only()": { + "<-": "item" + } + }); + + expect(object.array.length).toBe(1); + expect(object.array[0]).toBe(4); + + object.item = 5; + expect(object.array.length).toBe(1); + expect(object.array[0]).toBe(5); + }); + + it("should bind sets", function () { + var object = Bindings.defineBindings({ + set: new Set([1, 2, 3]), + item: 4 + }, { + "set.only()": { + "<-": "item" + } + }); + + expect(object.set.length).toBe(1); + expect(object.set.has(4)).toBe(true); + expect(object.set.has(1)).toBe(false); + + object.item = 5; + expect(object.set.has(4)).toBe(false); + expect(object.set.has(5)).toBe(true); + }); + +}); diff --git a/core/frb/spec/override-spec.js b/core/frb/spec/override-spec.js new file mode 100644 index 0000000000..4a7e5f540c --- /dev/null +++ b/core/frb/spec/override-spec.js @@ -0,0 +1,57 @@ + +var Bindings = require("../bindings"); +var observeProperty = require("../observers").observeProperty; +var observeKey = require("../observers").observeKey; + +describe("overriding observer", function () { + + it("should delegate to an alternate object property", function () { + + var object = {}; + + var proxy = { + object: object, + observeProperty: function (key, emit, source, parameters, beforeChange) { + return observeProperty( + this.object, + "~" + key, + emit, + source, + parameters, + beforeChange + ); + } + }; + + var target = {}; + + Bindings.defineBinding(target, "x", {"<-": "x", source: proxy}); + + object["~x"] = 10; + expect(target.x).toBe(10); + + }); + + it("should delegate to an alternate map key", function () { + + var array = [1, 2, 3]; + + var proxy = { + observeKey: function (key, emit, source, parameters, beforeChange) { + return observeKey(array, key, source, parameters, beforeChange); + } + }; + + var target = Bindings.defineBinding({ + array: array + }, "first", {"<-": "array[0]"}); + + expect(target.first).toBe(1); + + array.shift(); + expect(target.first).toBe(2); + + }); + +}); + diff --git a/core/frb/spec/parse-spec.js b/core/frb/spec/parse-spec.js new file mode 100644 index 0000000000..5adfa23504 --- /dev/null +++ b/core/frb/spec/parse-spec.js @@ -0,0 +1,20 @@ + +var parse = require("../parse"); +var language = require("./language"); + +describe("parse", function () { + language.forEach(function (test) { + if (test.invalid) { + it("should not parse " + JSON.stringify(test.path), function () { + expect(function () { + parse(test.path); + }).toThrow(); // TODO iron out the error messages + }); + } else { + it("should parse " + JSON.stringify(test.path), function () { + expect(parse(test.path, test.options)).toEqual(test.syntax); + }); + } + }) +}); + diff --git a/core/frb/spec/path-spec.js b/core/frb/spec/path-spec.js new file mode 100644 index 0000000000..9df9c9d700 --- /dev/null +++ b/core/frb/spec/path-spec.js @@ -0,0 +1,35 @@ +var Bindings = require("../bindings"); + +describe("path", function () { + + it("should do evil", function () { + + var object = Bindings.defineBindings({}, { + "result": {"<-": "path(path)"} + }); + + expect(object.result).toBe(); + + object.path = "x + 2"; + expect(object.result).toBe(); + + object.x = 2; + expect(object.result).toBe(4); + + }); + + it("should do evil iteratively", function () { + + var object = {}; + Bindings.defineBindings(object, { + "result": {"<-": "source.map{path($path)}"} + }, object); + + object.source = [1, 2, 3]; + object.path = "*2"; + expect(object.result).toEqual([2, 4, 6]); + + }); + +}); + diff --git a/core/frb/spec/pluck-spec.js b/core/frb/spec/pluck-spec.js new file mode 100644 index 0000000000..9eec1f8cbf --- /dev/null +++ b/core/frb/spec/pluck-spec.js @@ -0,0 +1,62 @@ + +var Bindings = require("../bindings"); + +describe("plucking indexes", function () { + + it("a pluck array should reflect values from the input", function () { + + var object = Bindings.defineBindings({ + input: ["a", "b", "c", "d"], + pluck: null + }, { + output: { + "<-": "pluck.defined() ? pluck.filter{<$input.length}.map{$input[()]} : []" + } + }); + + expect(object.output).toEqual([]) + + object.pluck = [1, 2]; + expect(object.output).toEqual(["b", "c"]) + + object.pluck = [1, 2]; + expect(object.output).toEqual(["b", "c"]) + + object.pluck.splice(0, object.pluck.length, 1, 2); + expect(object.output).toEqual(["b", "c"]) + + object.pluck.splice(1, 0, 0, 0); + expect(object.output).toEqual(["b", "a", "a", "c"]) + + // out of range + object.pluck = [10]; + expect(object.output).toEqual([]); + }); + + // There was a bug in this particular sub-case, using filter. + it("should work", function () { + + var object = Bindings.defineBindings({ + input: ["a", "b", "c", "d"], + pluck: [] + }, { + output: { + "<-": "pluck.filter{<$input.length}" + } + }); + + expect(object.output).toEqual([]); + + object.pluck = [1, 2]; + expect(object.output).toEqual([1, 2]) + + object.pluck = [1, 2]; + expect(object.output).toEqual([1, 2]) + + object.pluck.splice(0, object.pluck.length, 1, 2); + expect(object.output).toEqual([1, 2]) + + }); + +}); + diff --git a/core/frb/spec/range-content-reflexive-spec.js b/core/frb/spec/range-content-reflexive-spec.js new file mode 100644 index 0000000000..76d86205ef --- /dev/null +++ b/core/frb/spec/range-content-reflexive-spec.js @@ -0,0 +1,188 @@ + +var Bindings = require("../bindings"); + +Error.stackTraceLimit = Infinity; + +describe("as array bindings", function () { + + it("should propagate an array even with falsy source", function () { + var object = Bindings.defineBindings({ + }, { + foo: {"<-": "bar.asArray()"} + }); + expect(object.foo).toEqual([]); + }); + +}); + +describe("one way range content bindings", function () { + + it("should propagate", function () { + var object = Bindings.defineBindings({ + yang: ['a', 'b', 'c'] + }, { + yin: {"<-": "yang.rangeContent()"} + }); + expect(object.yin).toEqual(['a', 'b', 'c']); + }); + + it("should propagate array to left from falsy source", function () { + var object = Bindings.defineBindings({ + }, { + "foo.rangeContent()": {"<-": "bar.rangeContent()"} + }); + expect(object.foo).toEqual([]); + expect(object.foo).not.toBe(object.bar); + }); + +}); + +describe("two way bindings with range content on both sides", function () { + + it("should propagate array to left from falsy source", function () { + var object = Bindings.defineBindings({ + }, { + "foo.rangeContent()": {"<-": "bar.rangeContent()"} + }); + expect(object.foo).toEqual([]); + expect(object.foo).not.toBe(object.bar); + }); + + it("should propagate array to right from falsy source", function () { + var object = Bindings.defineBindings({ + }, { + "foo.rangeContent()": {"<->": "bar.rangeContent()"} + }); + expect(object.bar.slice()).toEqual([]); + expect(object.bar).not.toBe(object.foo); + }); + + it("should propagate content change from left to right", function () { + var object = Bindings.defineBindings({ + }, { + "foo.rangeContent()": {"<->": "bar.rangeContent()"} + }); + object.foo.push(1); + expect(object.bar.slice()).toEqual([1]); + }); + + it("should propagate content change from left to right", function () { + var object = Bindings.defineBindings({ + foo: [], + bar: [] + }, { + "foo.rangeContent()": {"<->": "bar.rangeContent()"} + }); + object.foo.push(1); + expect(object.bar.slice()).toEqual([1]); + }); + + it("should propagate content change from right to left", function () { + var object = Bindings.defineBindings({ + bar: [] + }, { + "foo.rangeContent()": {"<-": "bar.rangeContent()"} + }); + object.bar.push(1); + expect(object.foo.slice()).toEqual([1]); + }); + + it("right to left should propagate on assignment", function () { + + var object = Bindings.defineBindings({ + }, { + "yin.rangeContent()": {"<->": "yang.rangeContent()"} + }); + + object.yang = [1]; + expect(object.yin).toEqual([1]); + + }); + + // FIXME + xit("left to right should propagate on assignment", function () { + + var object = Bindings.defineBindings({ + }, { + "yin.rangeContent()": {"<->": "yang.rangeContent()"} + }); + + object.yin = [1]; + expect(object.yang.slice()).toEqual([1]); + + }); + + // FIXME + xit("left to right should propagate on assignment overriding initial value", function () { + + var object = Bindings.defineBindings({ + yin: [] + }, { + "yin.rangeContent()": {"<->": "yang.rangeContent()"} + }); + + object.yin = [1]; + expect(object.yang.slice()).toEqual([1]); + + }); + + // FIXME + xit("left to right should propagate on assignment overriding initial values on both sides", function () { + + var object = Bindings.defineBindings({ + yin: [], + yang: [] + }, { + "yin.rangeContent()": {"<->": "yang.rangeContent()"} + }); + + object.yin = [1]; + expect(object.yang.slice()).toEqual([1]); + + }); + + it("range content changes should propagate left to right", function () { + + var object = Bindings.defineBindings({ + yin: [], + yang: [] + }, { + "yin.rangeContent()": {"<->": "yang.rangeContent()"} + }); + + object.yin.push(1); + expect(object.yang.slice()).toEqual([1]); + + }); + + it("range content changes should propagate right to left", function () { + + var object = Bindings.defineBindings({ + yin: [], + yang: [] + }, { + "yin.rangeContent()": {"<->": "yang.rangeContent()"} + }); + + object.yang.push(1); + expect(object.yin.slice()).toEqual([1]); + }); + + it("right to left should precede left to right", function () { + + var object = Bindings.defineBindings({ + yin: [], + yang: [1, 2, 3] + }, { + "yin.rangeContent()": {"<->": "yang.rangeContent()"} + }); + + expect(object.yin.slice()).toEqual([1, 2, 3]); + + object.yang = ['a', 'b', 'c']; + expect(object.yin.slice()).toEqual(['a', 'b', 'c']); + + }); + +}); + diff --git a/core/frb/spec/range-spec.js b/core/frb/spec/range-spec.js new file mode 100644 index 0000000000..e853d9b4e5 --- /dev/null +++ b/core/frb/spec/range-spec.js @@ -0,0 +1,45 @@ + +var Bindings = require("../bindings"); + +describe("range", function () { + + it("property.range()", function () { + + var object = Bindings.defineBindings({}, { + "stack": {"<-": "height.range()"} + }); + + expect(object.stack).toEqual([]); + + object.height = 2; + expect(object.stack).toEqual([0, 1]); + + object.height = 1; + expect(object.stack).toEqual([0]); + + object.height = 3; + expect(object.stack).toEqual([0, 1, 2]); + + }); + + it("&range(height)", function () { + + var object = Bindings.defineBindings({}, { + "stack": {"<-": "&range(height)"} + }); + + expect(object.stack).toEqual([]); + + object.height = 2; + expect(object.stack).toEqual([0, 1]); + + object.height = 1; + expect(object.stack).toEqual([0]); + + object.height = 3; + expect(object.stack).toEqual([0, 1, 2]); + + }); + +}); + diff --git a/core/frb/spec/readme-spec.js b/core/frb/spec/readme-spec.js new file mode 100644 index 0000000000..a461a4dd7e --- /dev/null +++ b/core/frb/spec/readme-spec.js @@ -0,0 +1,1653 @@ + +var PropertyChanges = require("collections/listen/property-changes"); +var Map = require("collections/map"); +var Bindings = require("../bindings"); +var bind = require("../bind"); +var observe = require("../observe"); +var Frb = require(".."); + +Error.stackTraceLimit = 100; + +describe("Tutorial", function () { + + it("Introduction", function () { + // mock + var document = {body: {innerHTML: ""}}; + + // example starts here + var model = {content: "Hello, World!"}; + var cancelBinding = bind(document, "body.innerHTML", { + "<-": "content", + "source": model + }); + + // continued + model.content = "Farewell."; + expect(document.body.innerHTML).toBe("Farewell."); + + // continued + cancelBinding(); + model.content = "Hello again!"; // doesn't take + expect(document.body.innerHTML).toBe("Farewell."); + }); + + it("Two-way Bindings", function () { + // exapmle begins here + + var object = {}; + var cancel = bind(object, "foo", { + "<->": "bar" + }); + + // <- + object.bar = 10; + expect(object.foo).toBe(10); + + // -> + object.foo = 20; + expect(object.bar).toBe(20); + }); + + it("Right-to-left", function () { + var object = {foo: 10, bar: 20}; + var cancel = bind(object, "foo", { + "<->": "bar" + }); + expect(object.foo).toBe(20); + expect(object.bar).toBe(20); + }); + + it("Property chains", function () { + var foo = {a: {b: 10}}; + var bar = {a: {b: 10}}; + var cancel = bind(foo, "a.b", { + "<->": "a.b", + source: bar + }); + // <- + bar.a.b = 20; + expect(foo.a.b).toBe(20); + // -> + foo.a.b = 30; + expect(bar.a.b).toBe(30); + + // "Structure changes" + var a = foo.a; + expect(a.b).toBe(30); // from before + + //foo.a = {b: 40}; // orphan a and replace + foo.a = {}; // orphan + foo.a.b = 40; // replace + // -> + expect(bar.a.b).toBe(40); // updated + + bar.a.b = 50; + // <- + expect(foo.a.b).toBe(50); // new one updated + expect(a.b).toBe(30); // from before it was orphaned + }); + + it("Strings", function () { + var object = {name: "world"}; + bind(object, "greeting", {"<-": "'hello ' + name + '!'"}); + expect(object.greeting).toBe("hello world!"); + }); + + it("Sum", function () { + var object = {array: [1, 2, 3]}; + bind(object, "sum", {"<-": "array.sum()"}); + expect(object.sum).toEqual(6); + }); + + it("Average", function () { + var object = {array: [1, 2, 3]}; + bind(object, "average", {"<-": "array.average()"}); + expect(object.average).toEqual(2); + }); + + it("Rounding", function () { + var object = {number: -0.5}; + Bindings.defineBindings(object, { + "round": {"<-": "number.round()"}, + "floor": {"<-": "number.floor()"}, + "ceil": {"<-": "number.ceil()"} + }); + expect(object.round).toBe(0); + expect(object.floor).toBe(-1); + expect(object.ceil).toBe(0); + }); + + it("Last", function () { + var array = [1, 2, 3]; + var object = {array: array, last: null}; + Bindings.defineBinding(object, "last", {"<-": "array.last()"}); + expect(object.last).toBe(3); + + array.push(4); + expect(object.last).toBe(4); + + // Continued... + var changed = jasmine.createSpy(); + PropertyChanges.addOwnPropertyChangeListener(object, "last", changed); + array.unshift(0); + array.splice(3, 0, 3.5); + expect(object.last).toBe(4); + expect(changed).not.toHaveBeenCalled(); + + array.pop(); + expect(object.last).toBe(3); + + array.clear(); + expect(object.last).toBe(null); + }); + + it("Only", function () { + var object = {array: [], only: null}; + Bindings.defineBindings(object, { + only: {"<->": "array.only()"} + }); + + object.array = [1]; + expect(object.only).toBe(1); + + object.array.pop(); + expect(object.only).toBe(undefined); + + object.array = [1, 2, 3]; + expect(object.only).toBe(undefined); + + // (binding) + // Continued from above... + object.only = 2; + expect(object.array.slice()).toEqual([2]); + // Note that slice() is necessary only because the testing scaffold + // does not consider an observable array equivalent to a plain array + // with the same content + + object.only = null; + object.array.push(3); + expect(object.array.slice()).toEqual([2, 3]); + + }); + + it("One", function () { + var object = {array: [], one: null}; + Bindings.defineBindings(object, { + one: {"<-": "array.one()"} + }); + + expect(object.one).toBe(undefined); + + object.array.push(1); + expect(object.one).toBe(1); + + // Still there... + object.array.push(2); + expect(object.one).toBe(1); + }); + + it("Map", function () { + var object = {objects: [ + {number: 10}, + {number: 20}, + {number: 30} + ]}; + bind(object, "numbers", {"<-": "objects.map{number}"}); + expect(object.numbers).toEqual([10, 20, 30]); + object.objects.push({number: 40}); + expect(object.numbers).toEqual([10, 20, 30, 40]); + }); + + it("Filter", function () { + var object = {numbers: [1, 2, 3, 4, 5, 6]}; + bind(object, "evens", {"<-": "numbers.filter{!(%2)}"}); + expect(object.evens).toEqual([2, 4, 6]); + object.numbers.push(7, 8); + object.numbers.shift(); + object.numbers.shift(); + expect(object.evens).toEqual([4, 6, 8]); + }); + + it("Scope", function () { + var object = Bindings.defineBindings({ + numbers: [1, 2, 3, 4, 5], + maxNumber: 3 + }, { + smallNumbers: { + "<-": "numbers.filter{this <= ^maxNumber}" + } + }); + expect(object.smallNumbers).toEqual([1, 2, 3]); + }); + + it("This", function () { + var object = Bindings.defineBindings({ + "this": 10 + }, { + that: {"<-": ".this"} + }); + expect(object.that).toBe(object["this"]); + }); + + it("Some", function () { + var object = Bindings.defineBindings({ + options: [ + {checked: true}, + {checked: false}, + {checked: false} + ] + }, { + anyChecked: { + "<-": "options.some{checked}" + } + }); + expect(object.anyChecked).toBe(true); + }); + + it("Every", function () { + var object = Bindings.defineBindings({ + options: [ + {checked: true}, + {checked: false}, + {checked: false} + ] + }, { + allChecked: { + "<-": "options.every{checked}" + } + }); + expect(object.allChecked).toBe(false); + }); + + it("Some / Every (Two-way)", function () { + var object = Bindings.defineBindings({ + options: [ + {checked: true}, + {checked: false}, + {checked: false} + ] + }, { + allChecked: { + "<->": "options.every{checked}" + }, + noneChecked: { + "<->": "!options.some{checked}" + } + }); + + object.noneChecked = true; + expect(object.options.every(function (option) { + return !option.checked + })); + + object.allChecked = true; + expect(object.noneChecked).toBe(false); + + // continued... + object.allChecked = false; + expect(object.options.every(function (option) { + return option.checked; // still checked + })); + + }); + + it("Sorted", function () { + var object = {numbers: [5, 2, 7, 3, 8, 1, 6, 4]}; + bind(object, "sorted", {"<-": "numbers.sorted{}"}); + expect(object.sorted).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); + }); + + it("Sorted (by property)", function () { + var object = {arrays: [[1, 2, 3], [1, 2], [], [1, 2, 3, 4], [1]]}; + bind(object, "sorted", {"<-": "arrays.sorted{-length}"}); + expect(object.sorted.map(function (array) { + return array.slice(); // to clone + })).toEqual([ + [1, 2, 3, 4], + [1, 2, 3], + [1, 2], + [1], + [] + ]); + + // Continued... + object.arrays[0].push(4, 5); + expect(object.sorted.map(function (array) { + return array.slice(); // to clone + })).toEqual([ + [1, 2, 3, 4, 5], // new + [1, 2, 3, 4], + // old + [1, 2], + [1], + [] + ]); + }); + + it("Unique and Sorted", function () { + + var object = Bindings.defineBindings({ + folks: [ + {id: 4, name: "Bob"}, + {id: 2, name: "Alice"}, + {id: 3, name: "Bob"}, + {id: 1, name: "Alice"}, + {id: 1, name: "Alice"} // redundant + ] + }, { + inOrder: {"<-": "folks.sortedSet{id}"}, + byId: {"<-": "folks.map{[id, this]}.toMap()"}, + byName: {"<-": "inOrder.toArray().group{name}.toMap()"} + }); + + expect(object.inOrder.toArray()).toEqual([ + object.byId.get(1), + object.byId.get(2), + object.byId.get(3), + object.byId.get(4) + ]); + + expect(object.byName.get("Alice")).toEqual([ + object.byId.get(1), + object.byId.get(2) + ]); + + }); + + it("Unique and Sorted (Array)", function () { + var object = Bindings.defineBindings({ + folks: [ + {id: 4, name: "Bob"}, + {id: 2, name: "Alice"}, + {id: 3, name: "Bob"}, + {id: 1, name: "Alice"}, + {id: 1, name: "Alice"} // redundant + ] + }, { + index: {"<-": "folks.group{id}.sorted{.0}.map{.1.last()}"} + }); + + expect(object.index).toEqual([ + {id: 1, name: "Alice"}, + {id: 2, name: "Alice"}, + {id: 3, name: "Bob"}, + {id: 4, name: "Bob"} + ]); + }); + + it("Min and Max", function () { + var object = Bindings.defineBindings({}, { + min: {"<-": "values.min()"}, + max: {"<-": "values.max()"} + }); + + expect(object.min).toBe(undefined); + expect(object.max).toBe(undefined); + + object.values = [2, 3, 2, 1, 2]; + expect(object.min).toBe(1); + expect(object.max).toBe(3); + + object.values.push(4); + expect(object.max).toBe(4); + }); + + it("Min and Max (by property)", function () { + var object = Bindings.defineBindings({}, { + loser: {"<-": "rounds.min{score}.player"}, + winner: {"<-": "rounds.max{score}.player"} + }); + + object.rounds = [ + {score: 0, player: "Luke"}, + {score: 100, player: "Obi Wan"}, + {score: 250, player: "Vader"} + ]; + expect(object.loser).toEqual("Luke"); + expect(object.winner).toEqual("Vader"); + + object.rounds[1].score = 300; + expect(object.winner).toEqual("Obi Wan"); + }); + + it("Group", function () { + var store = Bindings.defineBindings({}, { + "clothingByColor": {"<-": "clothing.group{color}"} + }); + store.clothing = [ + {type: 'shirt', color: 'blue'}, + {type: 'pants', color: 'red'}, + {type: 'blazer', color: 'blue'}, + {type: 'hat', color: 'red'} + ]; + expect(store.clothingByColor).toEqual([ + ['blue', [ + {type: 'shirt', color: 'blue'}, + {type: 'blazer', color: 'blue'} + ]], + ['red', [ + {type: 'pants', color: 'red'}, + {type: 'hat', color: 'red'} + ]] + ]); + + // continued... + Bindings.cancelBinding(store, "clothingByColor"); + Bindings.defineBindings(store, { + "clothingByColor": {"<-": "clothing.groupMap{color}"} + }); + var blueClothes = store.clothingByColor.get('blue'); + expect(blueClothes).toEqual([ + {type: 'shirt', color: 'blue'}, + {type: 'blazer', color: 'blue'} + ]); + + store.clothing.push({type: 'gloves', color: 'blue'}); + expect(blueClothes).toEqual([ + {type: 'shirt', color: 'blue'}, + {type: 'blazer', color: 'blue'}, + {type: 'gloves', color: 'blue'} + ]); + }); + + it("View", function () { + var SortedSet = require("collections/sorted-set"); + var controller = { + index: SortedSet([1, 2, 3, 4, 5, 6, 7, 8]), + start: 2, + length: 4 + }; + var cancel = bind(controller, "view", { + "<-": "index.view(start, length)" + }); + + expect(controller.view).toEqual([3, 4, 5, 6]); + + // change the window length + controller.length = 3; + expect(controller.view).toEqual([3, 4, 5]); + + // change the window position + controller.start = 5; + expect(controller.view).toEqual([6, 7, 8]); + + // add content behind the window + controller.index.add(0); + expect(controller.view).toEqual([5, 6, 7]); + }); + + it("Enumerate", function () { + var object = {letters: ['a', 'b', 'c', 'd']}; + bind(object, "lettersAtEvenIndexes", { + "<-": "letters.enumerate().filter{!(.0 % 2)}.map{.1}" + }); + expect(object.lettersAtEvenIndexes).toEqual(['a', 'c']); + object.letters.shift(); + expect(object.lettersAtEvenIndexes).toEqual(['b', 'd']); + }); + + it("Range", function () { + var object = Bindings.defineBinding({}, "stack", { + "<-": "&range(length)" + }); + expect(object.stack).toEqual([]); + + object.length = 3; + expect(object.stack).toEqual([0, 1, 2]); + + object.length = 1; + expect(object.stack).toEqual([0]); + }); + + it("Flatten", function () { + var arrays = [[1, 2, 3], [4, 5, 6]]; + var object = {}; + bind(object, "flat", { + "<-": "flatten()", + source: arrays + }); + expect(object.flat).toEqual([1, 2, 3, 4, 5, 6]); + + // Continued... + arrays.push([7, 8, 9]); + arrays[0].unshift(0); + expect(object.flat).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + + // Continued... + var flat = object.flat; + arrays.splice(0, arrays.length); + expect(object.flat).toBe(flat); // === same object + }); + + it("Concat", function () { + var object = Bindings.defineBinding({ + head: 10, + tail: [20, 30] + }, "flat", { + "<-": "[head].concat(tail)" + }); + expect(object.flat).toEqual([10, 20, 30]); + }); + + it("Reversed", function () { + var object = {forward: [1, 2, 3]}; + bind(object, "backward", { + "<->": "forward.reversed()" + }); + expect(object.backward.slice()).toEqual([3, 2, 1]); + object.forward.push(4); + expect(object.forward.slice()).toEqual([1, 2, 3, 4]); + expect(object.backward.slice()).toEqual([4, 3, 2, 1]); + + // Continued... + object.backward.pop(); + expect(object.backward.slice()).toEqual([4, 3, 2]); + expect(object.forward.slice()).toEqual([2, 3, 4]); + }); + + it("Has", function () { + var object = { + haystack: [1, 2, 3], + needle: 3 + }; + bind(object, "hasNeedle", {"<-": "haystack.has(needle)"}); + expect(object.hasNeedle).toBe(true); + object.haystack.pop(); // 3 comes off + expect(object.hasNeedle).toBe(false); + + // Continued from above... + object.needle = 2; + expect(object.hasNeedle).toBe(true); + + // Continued from above... + var Set = require("collections/set"); + object.haystack = new Set([1, 2, 3]); + expect(object.hasNeedle).toBe(true); + + // Continued from above... + var Map = require("collections/map"); + object.haystack = new Map([[1, "a"], [2, "b"]]); + object.needle = 2; + expect(object.hasNeedle).toBe(true); + object.needle = 3; + expect(object.hasNeedle).toBe(false); + }); + + it("Has (DOM)", function () { + // mock + var document = {body: {classList: []}}; + + // example begins here + var model = {darkMode: false}; + bind(document.body, "classList.has('dark')", { + "<-": "darkMode", + source: model + }); + }); + + it("Get", function () { + var object = { + array: [1, 2, 3], + second: null + }; + var cancel = bind(object, "second", { + "<->": "array.get(1)" + }); + expect(object.array.slice()).toEqual([1, 2, 3]); + expect(object.second).toBe(2); + + object.array.shift(); + expect(object.array.slice()).toEqual([2, 3]); + expect(object.second).toBe(3); + + object.second = 4; + expect(object.array.slice()).toEqual([2, 4]); + + cancel(); + object.array.shift(); + expect(object.second).toBe(4); // still + }); + + it("Get (Map)", function () { + var Map = require("collections/map"); + var a = {id: 0}, b = {id: 1}; + var object = { + source: new Map([[a, 10], [b, 20]]), + key: null, + selected: null + }; + + var cancel = bind(object, "selected", { + "<-": "source.get(key)" + }); + expect(object.selected).toBe(undefined); + + object.key = a; + expect(object.selected).toBe(10); + + object.key = b; + expect(object.selected).toBe(20); + + object.source.set(b, 30); + expect(object.selected).toBe(30); + + var SortedMap = require("collections/sorted-map"); + object.source = SortedMap(); + expect(object.selected).toBe(undefined); + + object.source.set(b, 40); + expect(object.selected).toBe(40); + + cancel(); + object.key = a; // no effect + expect(object.selected).toBe(40); + }); + + it("Get (all content)", function () { + var Map = require("collections/map"); + var object = { + a: Map.from({a: 10}), + b: new Map() + }; + var cancel = bind(object, "a.mapContent()", {"<->": "b.mapContent()"}); + expect(object.a.toObject()).toEqual({}); + expect(object.b.toObject()).toEqual({}); + + object.a.set('a', 10); + expect(object.a.toObject()).toEqual({a: 10}); + expect(object.b.toObject()).toEqual({a: 10}); + + object.b.set('b', 20); + expect(object.a.toObject()).toEqual({a: 10, b: 20}); + expect(object.b.toObject()).toEqual({a: 10, b: 20}); + }); + + it("Keys, Values, Entries", function () { + var Map = require("collections/map"); + var object = Bindings.defineBindings({}, { + keys: {"<-": "map.keysArray()"}, + values: {"<-": "map.valuesArray()"}, + entries: {"<-": "map.entriesArray()"} + }); + object.map = Map.from({a: 10, b: 20, c: 30}); + expect(object.keys).toEqual(['a', 'b', 'c']); + expect(object.values).toEqual([10, 20, 30]); + expect(object.entries).toEqual([['a', 10], ['b', 20], ['c', 30]]); + + object.map.set('d', 40); + object.map.delete('a'); + expect(object.keys).toEqual(['b', 'c', 'd']); + expect(object.values).toEqual([20, 30, 40]); + expect(object.entries).toEqual([['b', 20], ['c', 30], ['d', 40]]); + }); + + it("Coerce to Map", function () { + var object = Bindings.defineBindings({}, { + map: {"<-": "entries.toMap()"} + }); + + // map property will persist across changes to entries + var map = object.map; + expect(map).not.toBe(null); + + object.entries = {a: 10}; + expect(map.keysArray()).toEqual(['a']); + expect(map.has('a')).toBe(true); + expect(map.get('a')).toBe(10); + + // Continued... + object.entries = [['b', 20], ['c', 30]]; + expect(map.keysArray()).toEqual(['b', 'c']); + + object.entries.push(object.entries.shift()); + expect(map.keysArray()).toEqual(['c', 'b']); + + // Continued... + object.entries = [['a', 10], ['a', 20]]; + expect(map.get('a')).toEqual(20); + object.entries.pop(); + expect(map.get('a')).toEqual(10); + + // Continued... + object.entries = Map.from({a: 10}); + expect(map.keysArray()).toEqual(['a']); + + }); + + it("Array Content", function () { + var object = { + array: [1, 2, 3] + }; + Bindings.defineBindings(object, { + first: {"<-": "array.0"}, + second: {"<-": "array.get(1)"} + }); + expect(object.first).toBe(1); + expect(object.second).toBe(2); + + // ...continued + var array = [1, 2, 3]; + var object = {}; + Bindings.defineBindings(object, { + first: { + "<-": ".0", + source: array + }, + second: { + "<-": "get(1)", + source: array + } + }); + expect(object.first).toBe(1); + expect(object.second).toBe(2); + + // ... continued + var object = { + array: [1, 2, 3], + index: 0 + }; + Bindings.defineBinding(object, "last", { + "<-": "array.get(array.length - 1)" + }); + expect(object.last).toBe(3); + + object.array.pop(); + expect(object.last).toBe(2); + + // ... continued + var SortedSet = require("collections/sorted-set"); + var object = { + set: SortedSet(), + array: [] + }; + Bindings.defineBindings(object, { + "array.rangeContent()": {"<-": "set"} + }); + object.set.addEach([5, 2, 6, 1, 4, 3]); + expect(object.array).toEqual([1, 2, 3, 4, 5, 6]); + + // ... continued + var Map = require("collections/map"); + var object = { + map: new Map(), + array: [] + }; + Bindings.defineBinding(object, "map.mapContent()", { + "<-": "array" + }); + object.array.push(1, 2, 3); + expect(object.map.toObject()).toEqual({ + 0: 1, + 1: 2, + 2: 3 + }); + }); + + it("Equals", function () { + var fruit = {apples: 1, oranges: 2}; + bind(fruit, "equal", {"<-": "apples == oranges"}); + expect(fruit.equal).toBe(false); + fruit.oranges = 1; + expect(fruit.equal).toBe(true); + }); + + it("Equals (Model)", function () { + var component = { + orangeElement: {checked: false}, + appleElement: {checked: true} + }; + Bindings.defineBindings(component, { + "orangeElement.checked": {"<->": "fruit == 'orange'"}, + "appleElement.checked": {"<->": "fruit == 'apple'"}, + }); + + component.orangeElement.checked = true; + expect(component.fruit).toEqual("orange"); + + component.appleElement.checked = true; + expect(component.fruit).toEqual("apple"); + }); + + // No tests for Value section + + it("With", function () { + var object = { + context: {a: 10, b: 20} + }; + Bindings.defineBinding(object, "sum", { + "<-": "context.(a + b)" + }); + expect(object.sum).toBe(30); + + Bindings.cancelBinding(object, "sum"); + object.context.a = 20; + expect(object.sum).toBe(30); // unchanged + }); + + it("With (tuple)", function () { + var object = { + context: {a: 10, b: 20} + }; + Bindings.defineBindings(object, { + "duple": {"<-": "context.[a, b]"}, + "pair": {"<-": "context.{key: a, value: b}"} + }); + expect(object.duple).toEqual([10, 20]); + expect(object.pair).toEqual({key: 10, value: 20}); + + Bindings.cancelBindings(object); + }); + + it("Operators", function () { + var object = {height: 10}; + bind(object, "heightPx", {"<-": "height + 'px'"}); + expect(object.heightPx).toEqual("10px"); + + // ... continued + var object = { + number: null, + string: null, + }; + Bindings.defineBinding(object, "+number", { + "<-": "string" + }); + object.string = '10'; + expect(object.number).toBe(10); + }); + + it("Conditional", function () { + var object = Bindings.defineBindings({ + condition: null, + consequent: 10, + alternate: 20 + }, { + choice: {"<->": "condition ? consequent : alternate"} + }); + + expect(object.choice).toBe(undefined); // no choice made + + object.condition = true; + expect(object.choice).toBe(10); + + object.condition = false; + expect(object.choice).toBe(20); + + // continued... + object.choice = 30; + expect(object.alternate).toBe(30); + + object.condition = true; + object.choice = 40; + expect(object.consequent).toBe(40); + }); + + it("And", function () { + var object = Bindings.defineBindings({ + left: undefined, + right: undefined + }, { + and: {"<-": "left && right"} + }); + + object.right = 10; + expect(object.and).toBe(undefined); + + // Continued... + object.left = 20; + expect(object.and).toBe(10); + }); + + it("And (bound)", function () { + var object = Bindings.defineBindings({}, { + "left && right": { + "<-": "leftAndRight" + } + }); + + object.leftAndRight = true; + expect(object.left).toBe(true); + expect(object.right).toBe(true); + + // Continued... + object.leftAndRight = false; + expect(object.left).toBe(false); + expect(object.right).toBe(true); + }); + + it("And (checkbox)", function () { + var controller = Bindings.defineBindings({ + checkbox: { + checked: false, + disabled: false + }, + model: { + expanded: false, + children: [1, 2, 3] + } + }, { + "checkbox.checked": {"<->": "model.expanded && expandable"}, + "checkbox.disabled": {"<-": "!expandable"}, + "expandable": {"<-": "model.children.length > 0"} + }); + + expect(controller.checkbox.checked).toBe(false); + expect(controller.checkbox.disabled).toBe(false); + + // check the checkbox + controller.checkbox.checked = true; + expect(controller.model.expanded).toBe(true); + + // alter the model such that the checkbox is unchecked and + // disabled + controller.model.children.clear(); + expect(controller.checkbox.checked).toBe(false); + expect(controller.checkbox.disabled).toBe(true); + }); + + it("Or", function () { + var object = Bindings.defineBindings({ + left: undefined, + right: undefined + }, { + or: {"<-": "left || right"} + }); + + object.right = 10; + expect(object.or).toBe(10); + + // Continued... + object.left = 20; + expect(object.or).toBe(20); + + // Continued... + object.right = undefined; + expect(object.or).toBe(20); + }); + + it("Default", function () { + var object = Bindings.defineBindings({ + left: undefined, + right: undefined + }, { + or: {"<-": "left ?? right"} + }); + + object.right = 10; + expect(object.or).toBe(10); + + object.left = false; + expect(object.or).toBe(false); + }); + + it("Defined", function () { + var object = Bindings.defineBindings({}, { + ready: { + "<-": "value.defined()" + } + }); + expect(object.ready).toBe(false); + + object.value = 10; + expect(object.ready).toBe(true); + }); + + it("Defined (bind)", function () { + var object = Bindings.defineBindings({ + value: 10, + operational: true + }, { + "value.defined()": {"<-": "operational"} + }); + expect(object.value).toBe(10); + + object.operational = false; + expect(object.value).toBe(undefined); + + // Continued... + object.operational = true; + expect(object.value).toBe(undefined); + + // Continued... + Bindings.defineBindings(object, { + "value == 10": { + "<-": "operational" + } + }); + expect(object.value).toBe(10); + }); + + it("Algebra", function () { + var caesar = {toBe: false}; + bind(caesar, "notToBe", {"<->": "!toBe"}); + expect(caesar.toBe).toEqual(false); + expect(caesar.notToBe).toEqual(true); + + caesar.notToBe = false; + expect(caesar.toBe).toEqual(true); + }); + + it("Literals", function () { + var object = {}; + bind(object, "greeting", {"<-": "'Hello, World!'"}); + expect(object.greeting).toBe("Hello, World!"); + + // Continued from above... + bind(object, 'four', {"<-": "2 + 2"}); + }); + + it("Tuples", function () { + var object = {array: [[1, 2, 3], [4, 5]]}; + bind(object, "summary", {"<-": "array.map{[length, sum()]}"}); + expect(object.summary).toEqual([ + [3, 6], + [2, 9] + ]); + }); + + it("Records", function () { + var object = {array: [[1, 2, 3], [4, 5]]}; + bind(object, "summary", { + "<-": "array.map{{length: length, sum: sum()}}" + }); + expect(object.summary).toEqual([ + {length: 3, sum: 6}, + {length: 2, sum: 9} + ]); + }); + + it("Parameters", function () { + var object = {a: 10, b: 20, c: 30}; + bind(object, "foo", { + "<-": "[$a, $b, $c]", + parameters: object + }); + expect(object.foo).toEqual([10, 20, 30]); + // continued... + object.a = 0; + object.b = 1; + object.c = 2; + expect(object.foo).toEqual([0, 1, 2]); + // continued... + var object = {}; + bind(object, "ten", {"<-": "$", parameters: 10}); + expect(object.ten).toEqual(10); + }); + + it("Observers", function () { + var results = []; + var object = {foo: {bar: 10}}; + var cancel = observe(object, "foo.bar", function (value) { + results.push(value); + }); + + object.foo.bar = 10; + expect(results).toEqual([10]); + + object.foo.bar = 20; + expect(results).toEqual([10, 20]); + }); + + it("Observers (beforeChange)", function () { + var results = []; + var object = {foo: {bar: 10}}; + var cancel = observe(object, "foo.bar", { + change: function (value) { + results.push(value); + }, + beforeChange: true + }); + + expect(results).toEqual([10]); + + object.foo.bar = 20; + expect(results).toEqual([10, 10]); + + object.foo.bar = 30; + expect(results).toEqual([10, 10, 20]); + }); + + it("Observers (contentChange true)", function () { + var lastResult; + var array = [[1, 2, 3], [4, 5, 6]]; + observe(array, "map{sum()}", { + change: function (sums) { + lastResult = sums.slice(); + // 1. [6, 15] + // 2. [6, 15, 0] + // 3. [10, 15, 0] + }, + contentChange: true + }); + + expect(lastResult).toEqual([6, 15]); + + array.push([0]); + expect(lastResult).toEqual([6, 15, 0]); + + array[0].push(4); + expect(lastResult).toEqual([10, 15, 0]); + }); + + it("Nested Observers", function () { + var i = 0; + var array = [[1, 2, 3], [4, 5, 6]]; + var cancel = observe(array, "map{sum()}", function (array) { + function rangeChange() { + if (i === 0) { + expect(array.slice()).toEqual([6, 15]); + } else if (i === 1) { + expect(array.slice()).toEqual([6, 15, 0]); + } else if (i === 2) { + expect(array.slice()).toEqual([10, 15, 0]); + } + i++; + } + rangeChange(); + array.addRangeChangeListener(rangeChange); + return function cancelRangeChange() { + array.removeRangeChangeListener(rangeChange); + }; + }); + array.push([0]); + array[0].push(4); + cancel(); + }); + + it("Nested Observers (property observers)", function () { + var object = {foo: {bar: 10}}; + var cancel = observe(object, "foo", function (foo) { + return observe(foo, "bar", function (bar) { + expect(bar).toBe(10); + }); + }); + }); + + it("Bindings", function () { + var target = Bindings.defineBindings({}, { + "fahrenheit": {"<->": "celsius * 1.8 + 32"}, + "celsius": {"<->": "kelvin - 272.15"} + }); + target.celsius = 0; + expect(target.fahrenheit).toEqual(32); + expect(target.kelvin).toEqual(272.15); + }); + + it("Binding Descriptors", function () { + // mock + var document = {body: {classList: []}}; + + // example begins here + var object = Bindings.defineBindings({ + darkMode: false, + document: document + }, { + "document.body.classList.has('dark')": { + "<-": "darkMode" + } + }); + + // Continued from above... + var bindings = Bindings.getBindings(object); + var descriptor = Bindings.getBinding(object, "document.body.classList.has('dark')"); + Bindings.cancelBinding(object, "document.body.classList.has('dark')"); + Bindings.cancelBindings(object); + expect(Object.keys(bindings)).toEqual([]); + }); + + it("Converters (convert, revert)", function () { + var object = Bindings.defineBindings({ + a: 10 + }, { + b: { + "<->": "a", + convert: function (a) { + return a * 2; + }, + revert: function (b) { + return b / 2; + } + } + }); + expect(object.b).toEqual(20); + + object.b = 10; + expect(object.a).toEqual(5); + }); + + it("Converters (converter)", function () { + function Multiplier(factor) { + this.factor = factor; + } + Multiplier.prototype.convert = function (value) { + return value * this.factor; + }; + Multiplier.prototype.revert = function (value) { + return value / this.factor; + }; + + var doubler = new Multiplier(2); + + var object = Bindings.defineBindings({ + a: 10 + }, { + b: { + "<->": "a", + converter: doubler + } + }); + expect(object.b).toEqual(20); + + object.b = 10; + expect(object.a).toEqual(5); + }); + + it("Converters (reverter)", function () { + var uriConverter = { + convert: encodeURI, + revert: decodeURI + }; + var model = Bindings.defineBindings({}, { + "title": { + "<->": "location", + reverter: uriConverter + } + }); + + model.title = "Hello, World!"; + expect(model.location).toEqual("Hello,%20World!"); + + model.location = "Hello,%20Dave."; + expect(model.title).toEqual("Hello, Dave."); + }); + + it("Computed Properties", function () { + // mock + var window = {location: {}}; + var QS = {stringify: function () {}}; + + // example begins here... + Bindings.defineBindings({ + window: window, + form: { + q: "", + charset: "utf-8" + } + }, { + "queryString": { + args: ["form.q", "form.charset"], + compute: function (q, charset) { + return "?" + QS.stringify({ + q: q, + charset: charset + }); + } + }, + "window.location.search": { + "<-": "queryString" + } + }); + }); + + describe("Polymorphic Extensibility", function () { + + it("works for observer method", function () { + + function Clock() { + } + + Clock.prototype.observeTime = function (emit) { + var cancel, timeoutHandle; + function tick() { + if (cancel) { + cancel(); + } + cancel = emit(Date.now()); + timeoutHandle = setTimeout(tick, 1000); + } + tick(); + return function cancelTimeObserver() { + clearTimeout(timeoutHandle); + if (cancel) { + cancel(); + } + }; + }; + + var object = Bindings.defineBindings({ + clock: new Clock() + }, { + "time": {"<-": "clock.time()"} + }); + + expect(object.time).not.toBe(undefined); + + Bindings.cancelBindings(object); + + }); + + it("works for observer maker method", function () { + + function Clock() { + } + + Clock.prototype.observeTime = function (emit, resolution) { + var cancel, timeoutHandle; + function tick() { + if (cancel) { + cancel(); + } + cancel = emit(Date.now()); + timeoutHandle = setTimeout(tick, resolution); + } + tick(); + return function cancelTimeObserver() { + clearTimeout(timeoutHandle); + if (cancel) { + cancel(); + } + }; + }; + + Clock.prototype.makeTimeObserver = function (observeResolution) { + var self = this; + return function observeTime(emit, scope) { + return observeResolution(function replaceResolution(resolution) { + return self.observeTime(emit, resolution); + }, scope); + }; + }; + + var object = Bindings.defineBindings({ + clock: new Clock() + }, { + "time": {"<-": "clock.time(1000)"} + }); + + expect(object.time).not.toBe(undefined); + + Bindings.cancelBindings(object); + + }); + }); + +}); + +describe("declarations", function () { + it("should work", function () { + + // create an object + var object = Bindings.defineBindings({ // prototype + // simple properties + foo: 0, + graph: [ + {numbers: [1,2,3]}, + {numbers: [4,5,6]} + ] + }, { + // extended property descriptors + bar: {"<->": "foo", enumerable: false}, + numbers: {"<-": "graph.map{numbers}.flatten()"}, + sum: {"<-": "numbers.sum()"}, + reversed: {"<-": "numbers.reversed()"} + }); + + expect(object.bar).toEqual(object.foo); + object.bar = 10; + expect(object.bar).toEqual(object.foo); + expect.foo = 20; + expect(object.bar).toEqual(object.foo); + + // note that the identity of the bound numbers array never + // changes, because all of the changes to that array are + // incrementally updated + var numbers = object.numbers; + + // first computation + expect(object.sum).toEqual(21); + + // adds an element to graph, + // which pushes [7, 8, 9] to "graph.map{numbers}", + // which splices [7, 8, 9] to the end of + // "graph.map{numbers}.flatten()", + // which increments "sum()" by [7, 8, 9].sum() + object.graph.push({numbers: [7, 8, 9]}); + expect(object.sum).toEqual(45); + + // splices [1] to the beginning of [1, 2, 3], + // which splices [1] to the beginning of "...flatten()" + // which increments "sum()" by [1].sum() + object.graph[0].numbers.unshift(1); + expect(object.sum).toEqual(46); + + // cancels the entire observer hierarchy, then attaches + // listeners to the new one. updates the sum. + object.graph = [{numbers: [1,2,3]}]; + expect(object.sum).toEqual(6); + + expect(object.reversed).toEqual([3, 2, 1]); + + expect(object.numbers).toBe(numbers) // still the same object + + Frb.cancelBindings(object); // cancels all bindings on this object and + // their transitive observers and event listeners as deep as + // they go + + }); +}); + +describe("Bindings", function () { + + it("Bindings", function () { + var target = Bindings.defineBindings({}, { + "fahrenheit": {"<->": "celsius * 1.8 + 32"}, + "celsius": {"<->": "kelvin - 272.15"} + }); + target.celsius = 0; + expect(target.fahrenheit).toEqual(32); + expect(target.kelvin).toEqual(272.15); + }); + + it("Binding Descriptors", function () { + var document = {body: {classList: []}}; + + var object = Bindings.defineBindings({ + darkMode: false, + document: document + }, { + "document.body.classList.has('dark')": { + "<-": "darkMode" + } + }); + + // continued + Bindings.cancelBindings(object); + expect(Bindings.getBindings(object)).toEqual(new Map()); + }); + + it("Converters", function () { + Bindings.defineBindings({ + a: 10 + }, { + b: { + "<-": "a", + convert: function (a) { + return a * 2; + }, + revert: function (b) { + return a / 2; + } + } + }); + + // continue + Bindings.defineBindings({ + a: 10 + }, { + b: { + "<-": "a", + converter: { + factor: 2, + convert: function (a) { + return a * this.factor; + }, + revert: function (b) { + return a / this.factor; + } + } + } + }); + }); + + it("Computed Properties", function () { + /* + // preamble + var window = {location: {search: ""}}; + + Bindings.defineBindings({ + window: window, + form: { + q: "", + charset: "utf-8" + } + }, { + queryString: { + args: ["form.q", "form.charset"], + compute: function (q, charset) { + return "?" + QS.stringify({ + q: q, + charset: charset + }); + } + }, + "window.location.search": { + "<-": "queryString" + } + }); + */ + }); + + it("Bind", function () { + + var bind = require("../bind"); + + var source = [{numbers: [1,2,3]}, {numbers: [4,5,6]}]; + var target = {}; + var cancel = bind(target, "summary", { + "<-": "map{[numbers.sum(), numbers.average()]}", + source: source + }); + + expect(target.summary).toEqual([ + [6, 2], + [15, 5] + ]); + + cancel(); + + }); + + it("Evaluate (compile)", function () { + var parse = require("../parse"); + var compile = require("../compile-evaluator"); + var Scope = require("../scope"); + + // example begins here... + var syntax = parse("a.b"); + var evaluate = compile(syntax); + var c = evaluate(new Scope({a: {b: 10}})) + expect(c).toBe(10); + }); + + it("Evaluate", function () { + var evaluate = require("../evaluate"); + + // example begins here... + var c = evaluate("a.b", {a: {b: 10}}) + expect(c).toBe(10); + }); + + it("Stringify", function () { + var stringify = require("../stringify"); + + // example begins here... + var syntax = {type: "and", args: [ + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "a"} + ]}, + {type: "property", args: [ + {type: "value"}, + {type: "literal", value: "b"} + ]} + ]}; + + var path = stringify(syntax); + expect(path).toBe("a && b"); + }); + +}); + +describe("Reference", function () { + + it("Observe", function () { + var observe = require("../observe"); + + // begin + var source = [1, 2, 3]; + var sum; + var cancel = observe(source, "sum()", function (newSum) { + sum = newSum; + }); + + expect(sum).toBe(6); + + source.push(4); + expect(sum).toBe(10); + + source.unshift(0); // no change + expect(sum).toBe(10); + + cancel(); + source.splice(0, source.length); // would change + expect(sum).toBe(10); + + }); + + it("Observe (descriptors)", function () { + var observe = require("../observe"); + + // begin + var object = {}; + var cancel = observe(object, "array", { + change: function (value) { + // may return a cancel function for a nested observer + }, + parameters: {}, + beforeChange: false, + contentChange: true + }); + + object.array = []; // emits [] + object.array.push(10); // emits [10] + }); + + it("Compute", function () { + var compute = require("../compute"); + + var source = {operands: [10, 20]}; + var target = {}; + var cancel = compute(target, "sum", { + source: source, + args: ["operands.0", "operands.1"], + compute: function (a, b) { + return a + b; + } + }); + + expect(target.sum).toEqual(30); + + source.operands.set(1, 30); + expect(target.sum).toEqual(40); + }); + +}); diff --git a/core/frb/spec/stringify-spec.js b/core/frb/spec/stringify-spec.js new file mode 100644 index 0000000000..fa56afe079 --- /dev/null +++ b/core/frb/spec/stringify-spec.js @@ -0,0 +1,21 @@ + +var stringify = require("../stringify"); +var parse = require("../parse"); +var language = require("./language"); + +describe("stringify", function () { + language.forEach(function (test) { + if (!test.nonCanon && !test.invalid) { + it("should stringify " + JSON.stringify(test.syntax), function () { + expect(stringify(test.syntax)).toEqual(test.path); + }); + it("stringify should round-trip through parse " + JSON.stringify(test.syntax), function () { + expect(stringify(parse(test.path, test.options))).toEqual(test.path); + }); + it("parse should round-trip through stringify " + JSON.stringify(test.syntax), function () { + expect(parse(stringify(test.syntax), test.options)).toEqual(test.syntax); + }); + } + }) +}); + diff --git a/core/frb/spec/view-spec.js b/core/frb/spec/view-spec.js new file mode 100644 index 0000000000..5a5b0e73b0 --- /dev/null +++ b/core/frb/spec/view-spec.js @@ -0,0 +1,59 @@ +var Bindings = require("../bindings"); + +describe("view", function () { + var object; + beforeEach(function () { + object = { + array: [2, 4, 6, 8] + }; + }); + + it("updates", function () { + Bindings.defineBindings(object, { + view: {"<-": "array.view(1, 2)"} + }); + + expect(object.view).toEqual([4, 6]); + object.array.splice(1, 1); + expect(object.view).toEqual([6, 8]); + }); + + it("accepts 0 for the start", function () { + Bindings.defineBindings(object, { + view: {"<-": "array.view(0, 2)"} + }); + + expect(object.view).toEqual([2, 4]); + }); + + it("accepts a length longer than the array", function () { + Bindings.defineBindings(object, { + view: {"<-": "array.view(2, 3)"} + }); + + expect(object.view).toEqual([6, 8]); + object.array.push(10); + expect(object.view).toEqual([6, 8, 10]); + object.array.push(12); + expect(object.view).toEqual([6, 8, 10]); + }); + + it("start defaults to zero", function () { + Bindings.defineBindings(object, { + view: {"<-": "array.view(length)"} + }); + + object.length = 3; + expect(object.view).toEqual([2, 4, 6]); + object.array.splice(2, object.array.length - 2); + expect(object.view).toEqual([2, 4]); + object.array.unshift(0); + expect(object.view).toEqual([0, 2, 4]); + object.array.splice(2, 0, 3); + object.array.splice(1, 0, 1); + object.length += 1; + expect(object.view).toEqual([0, 1, 2, 3]); + }); + +}); + diff --git a/core/frb/stringify.js b/core/frb/stringify.js new file mode 100644 index 0000000000..1bde02d8ce --- /dev/null +++ b/core/frb/stringify.js @@ -0,0 +1,305 @@ +"use strict"; + +var parse = require("./parse"); +var precedence = require("./language").precedence; +var typeToToken = require("./language").operatorTypes; +var tokenToType = require("./language").operatorTokens; + +module.exports = stringify; +function stringify(syntax, scope) { + return stringify.semantics.stringify(syntax, scope); +} + +function makeBlockStringifier(type) { + return function (syntax, scope, stringifier) { + var chain = type + '{' + stringifier.stringify(syntax.args[1], scope) + '}'; + if (syntax.args[0].type === "value") { + return chain; + } else { + return stringifier.stringify(syntax.args[0], scope) + '.' + chain; + } + }; +} + +stringify.semantics = { + + makeBlockStringifier: makeBlockStringifier, + + stringifyChild: function stringifyChild(child, scope) { + var arg = this.stringify(child, scope); + if (!arg) return "this"; + return arg; + }, + + stringify: function (syntax, scope, parent) { + var stringifiers = this.stringifiers, + string, + i, countI, args; + + if (stringifiers[syntax.type]) { + // operators + string = stringifiers[syntax.type](syntax, scope, this); + } else if (syntax.inline) { + // inline invocations + string = "&"; + string += syntax.type; + string += "("; + + args = syntax.args; + for(i=0, countI = args.length;i 0 ? ", " : ""; + string += this.stringifyChild(args[i],scope); + } + string += ")"; + + } else { + // method invocations + var chain; + if (syntax.args.length === 1 && syntax.args[0].type === "mapBlock") { + // map block function calls + chain = syntax.type + "{" + this.stringify(syntax.args[0].args[1], scope) + "}"; + syntax = syntax.args[0]; + } else { + // normal function calls + chain = syntax.type; + chain += "("; + + args = syntax.args; + for(i=1, countI = args.length;i 1 ? ", " : ""; + chain += this.stringifyChild(args[i],scope); + } + chain += ")"; + } + // left-side if it exists + if (syntax.args[0].type === "value") { + string = chain; + } else { + string = this.stringify(syntax.args[0], scope) + "." + chain; + } + } + // parenthesize if we're going backward in precedence + if ( + !parent || + (parent.type === syntax.type && parent.type !== "if") || + // TODO check on weirdness of "if" + precedence.get(parent.type).has(syntax.type) + ) { + return string; + } else { + return "(" + string + ")"; + } + }, + + stringifiers: { + + value: function (syntax, scope, stringifier) { + return ''; + }, + + literal: function (syntax, scope, stringify) { + if (typeof syntax.value === 'string') { + return "'" + syntax.value.replace("'", "\\'") + "'"; + } else { + return "" + syntax.value; + } + }, + + parameters: function (syntax, scope, stringifier) { + return '$'; + }, + + record: function (syntax, scope, stringifier) { + return "{" + Object.map(syntax.args, function (value, key) { + var string; + if (value.type === "value") { + string = "this"; + } else { + string = stringifier.stringify(value, scope); + } + return key + ": " + string; + }).join(", ") + "}"; + }, + + tuple: function (syntax, scope, stringifier) { + return "[" + Object.map(syntax.args, function (value) { + if (value.type === "value") { + return "this"; + } else { + return stringifier.stringify(value); + } + }).join(", ") + "]"; + }, + + component: function (syntax, scope) { + var label; + if (scope && scope.components && syntax.component) { + if (scope.components.getObjectLabel) { + label = scope.components.getObjectLabel(syntax.component); + } else if (scope.components.getLabelForObject) { + // I am hoping that we will change Montage to use this API + // for consistency with document.getElementById, + // components.getObjectByLabel, & al + label = scope.components.getLabelForObject(syntax.component); + } + } else { + label = syntax.label; + } + return '@' + label; + }, + + element: function (syntax) { + return '#' + syntax.id; + }, + + mapBlock: makeBlockStringifier("map"), + filterBlock: makeBlockStringifier("filter"), + someBlock: makeBlockStringifier("some"), + everyBlock: makeBlockStringifier("every"), + sortedBlock: makeBlockStringifier("sorted"), + sortedSetBlock: makeBlockStringifier("sortedSet"), + groupBlock: makeBlockStringifier("group"), + groupMapBlock: makeBlockStringifier("groupMap"), + minBlock: makeBlockStringifier("min"), + maxBlock: makeBlockStringifier("max"), + + property: function (syntax, scope, stringifier) { + if (syntax.args[0].type === "value") { + if (typeof syntax.args[1].value === "string") { + return syntax.args[1].value; + } else if (syntax.args[1].type === "literal") { + return "." + syntax.args[1].value; + } else { + return "this[" + stringifier.stringify(syntax.args[1], scope) + "]"; + } + } else if (syntax.args[0].type === "parameters") { + return "$" + syntax.args[1].value; + } else if ( + syntax.args[1].type === "literal" && + /^[\w\d_]+$/.test(syntax.args[1].value) + ) { + return stringifier.stringify(syntax.args[0], scope, { + type: "scope" + }) + '.' + syntax.args[1].value; + } else { + return stringifier.stringify(syntax.args[0], { + type: "scope" + }, scope) + '[' + stringifier.stringify(syntax.args[1], scope) + ']'; + } + }, + + "with": function (syntax, scope, stringifier) { + var right = stringifier.stringify(syntax.args[1], scope, syntax); + return stringifier.stringify(syntax.args[0], scope) + "." + right; + }, + + not: function (syntax, scope, stringifier) { + if (syntax.args[0].type === "equals") { + return ( + stringifier.stringify(syntax.args[0].args[0], scope, {type: "equals"}) + + " != " + + stringifier.stringify(syntax.args[0].args[1], scope, {type: "equals"}) + ); + } else { + return '!' + stringifier.stringify(syntax.args[0], scope, syntax) + } + }, + + neg: function (syntax, scope, stringifier) { + return '-' + stringifier.stringify(syntax.args[0], scope, syntax) + }, + + toNumber: function (syntax, scope, stringifier) { + return '+' + stringifier.stringify(syntax.args[0], scope, syntax) + }, + + parent: function (syntax, scope, stringifier) { + return '^' + stringifier.stringify(syntax.args[0], scope, syntax) + }, + + if: function (syntax, scope, stringifier) { + return ( + stringifier.stringify(syntax.args[0], scope, syntax) + " ? " + + stringifier.stringify(syntax.args[1], scope) + " : " + + stringifier.stringify(syntax.args[2], scope) + ); + }, + + event: function (syntax, scope, stringifier) { + return syntax.when + " " + syntax.event + " -> " + stringifier.stringify(syntax.listener, scope); + }, + + binding: function (arrow, syntax, scope, stringifier) { + + var header = stringifier.stringify(syntax.args[0], scope) + " " + arrow + " " + stringifier.stringify(syntax.args[1], scope); + var trailer = ""; + + var descriptor = syntax.descriptor; + if (descriptor) { + for (var name in descriptor) { + trailer += ", " + name + ": " + stringifier.stringify(descriptor[name], scope); + } + } + + return header + trailer; + }, + + bind: function (syntax, scope, stringifier) { + return this.binding("<-", syntax, scope, stringifier); + }, + + bind2: function (syntax, scope, stringifier) { + return this.binding("<->", syntax, scope, stringifier); + }, + + assign: function (syntax, scope, stringifier) { + return stringifier.stringify(syntax.args[0], scope) + ": " + stringifier.stringify(syntax.args[1], scope); + }, + + block: function (syntax, scope, stringifier) { + var header = "@" + syntax.label; + if (syntax.connection) { + if (syntax.connection === "prototype") { + header += " < "; + } else if (syntax.connection === "object") { + header += " : "; + } + header += stringifier.stringify({type: 'literal', value: syntax.module}); + if (syntax.exports && syntax.exports.type !== "value") { + header += " " + stringifier.stringify(syntax.exports, scope); + } + } + return header + " {\n" + syntax.statements.map(function (statement) { + return " " + stringifier.stringify(statement, scope) + ";\n"; + }).join("") + "}\n"; + }, + + sheet: function (syntax, scope, stringifier) { + return "\n" + syntax.blocks.map(function (block) { + return stringifier.stringify(block, scope); + }).join("\n") + "\n"; + } + + } + +}; + +// book a stringifier for all the defined symbolic operators +typeToToken.forEach(function (token, type) { + stringify.semantics.stringifiers[type] = function (syntax, scope, stringifier) { + + var args = syntax.args, + i, countI, result = ""; + for(i = 0, countI = args.length;i 0) { + result += " "; + result += token; + result += " "; + } + result += stringifier.stringify(args[i],scope, syntax); + } + + return result.trim(); + } +}); + diff --git a/core/frb/test.js b/core/frb/test.js new file mode 100644 index 0000000000..2a04a897e6 --- /dev/null +++ b/core/frb/test.js @@ -0,0 +1,84 @@ +var Bindings = require("./bindings"); + +var o = Bindings.defineBindings({ + target: [1] +}, { + source: {"<->": "target.rangeContent()"} +}); + +assertEqual(o.source[0], 1); + +var oldSource = o.source; +assertEqual(oldSource[0], 1); + +o.source = [2]; +assertEqual(oldSource[0], 1); + +o.target.splice(0, 3, 3); +assertEqual(oldSource, o.source); +assertEqual(oldSource[0], 1); +assertEqual(o.source[0], 3); + +/* +defineBinding: + descriptor = { + "<-": "a", + target: o, + parameters: undefined, + document: document, + components: undefined + cancel: bind(o, "b", descriptor) + } + + bind (object, name, descriptor): + descriptor.target = o + descriptor.targetPath = "b" + source = o + sourcePath = "a" + + sourceScope = new Scope(o) + targetScope = new Scope(o) + + sourceSyntax = parse(sourcePath) + targetSyntax = parse(targetPath) +sourceSyntax +Object +args: Array[1] +0: Object +args: Array[2] +0: Object +type: "value" +1: Object +type: "literal" +value: "target" +length: 2 +type: "property" +length: 1 +type: "rangeContent" + +targetSyntax +Object +args: Array[2] +0: Object +type: "value" +1: Object +type: "literal" +value: "source" +length: 2 +type: "property" + + bindOneWay(target, source, ...) + bindOneWay(source, target, ...) // if two-way + +bindOneWay(): + // rotate operators from target to source + [targetSyntax, sourceSyntax] = solve(targetSyntax, sourceSyntax) + + observeSource = compileObserver(sourceSyntax) + compileBinder(targetSyntax)(observeSource, sourceScope, targetScope, ...) + +*/ + +function assertEqual(x, y) { + console.log(x===y ? "* " : "x ", x, y); +} From 83bc17fefecc8d02f715e69d6df9b031c8f3ae69 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 14:47:34 -0700 Subject: [PATCH 152/407] Fix typo --- core/meta/property-descriptor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index 57ee04500e..94a29e181e 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -39,7 +39,7 @@ var Defaults = { isMandatory: false, readOnly: false, denyDelete: false, - deleteRule: DeleteRule.Nullify, + deleteRule: DeleteRule.NULLIFY, inversePropertyName: void 0, valueType: "string", collectionValueType: "list", From 034b31dbb250792f86df720a476ad9cd23ca73f4 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 14:48:22 -0700 Subject: [PATCH 153/407] bring mr in montage --- core/mr/.editorconfig | 11 + core/mr/.gitignore | 10 + core/mr/.jshintignore | 4 + core/mr/.jshintrc | 32 + core/mr/.travis.yml | 43 + core/mr/.vscode/launch.json | 34 + core/mr/CHANGES.md | 181 ++ core/mr/CONTRIBUTING.md | 47 + core/mr/LICENSE.md | 31 + core/mr/README.md | 87 + core/mr/adhoc.html | 25 + core/mr/adhoc.js | 31 + core/mr/bin/mr | 2 + core/mr/bootstrap-node.js | 86 + core/mr/bootstrap.js | 397 +++++ core/mr/browser.js | 368 ++++ core/mr/demo/data.js | 1 + core/mr/demo/index.html | 14 + core/mr/demo/index.js | 3 + core/mr/demo/package.json | 6 + core/mr/docs/Config-API.md | 80 + core/mr/docs/How-it-works.md | 103 ++ core/mr/docs/Module-API.md | 68 + core/mr/docs/Node-compatability.md | 43 + core/mr/docs/Package-API.md | 52 + core/mr/docs/Require-API.md | 71 + core/mr/docs/Script-attributes.md | 71 + core/mr/karma.conf.js | 97 + core/mr/node.js | 196 +++ core/mr/package.json | 66 + core/mr/require.js | 1553 +++++++++++++++++ core/mr/sandbox.js | 58 + core/mr/test/README.md | 173 ++ core/mr/test/all.js | 128 ++ core/mr/test/package.json | 21 + core/mr/test/run-browser.js | 112 ++ core/mr/test/run-karma.html | 14 + core/mr/test/run-karma.js | 100 ++ core/mr/test/run-node.js | 49 + core/mr/test/run.html | 14 + .../node_modules/shimmy/browser.js | 2 + .../node_modules/shimmy/index.js | 2 + .../node_modules/shimmy/package.json | 7 + .../spec/browser-alternative/package.json | 6 + .../test/spec/browser-alternative/program.js | 3 + .../node_modules/shimmy/browser.js | 2 + .../node_modules/shimmy/index.js | 2 + .../node_modules/shimmy/package.json | 9 + .../spec/browser-alternatives/package.json | 6 + .../test/spec/browser-alternatives/program.js | 3 + core/mr/test/spec/case-sensitive/a.js | 0 core/mr/test/spec/case-sensitive/package.json | 1 + core/mr/test/spec/case-sensitive/program.js | 8 + core/mr/test/spec/comments/package.json | 1 + core/mr/test/spec/comments/program.js | 36 + core/mr/test/spec/cyclic/a.js | 34 + core/mr/test/spec/cyclic/b.js | 34 + core/mr/test/spec/cyclic/package.json | 1 + core/mr/test/spec/cyclic/program.js | 40 + core/mr/test/spec/determinism/package.json | 1 + core/mr/test/spec/determinism/program.js | 34 + core/mr/test/spec/determinism/submodule/a.js | 39 + core/mr/test/spec/determinism/submodule/b.js | 30 + .../dev-dependency/dev-dependency.js | 1 + .../node_modules/dev-dependency/package.json | 4 + .../test/spec/dev-dependencies/package.json | 5 + core/mr/test/spec/dev-dependencies/program.js | 4 + core/mr/test/spec/directory-index/bar.js | 0 .../mr/test/spec/directory-index/foo/index.js | 4 + .../mr/test/spec/directory-index/package.json | 1 + core/mr/test/spec/directory-index/program.js | 43 + .../node_modules/test.js/main.load.js | 6 + .../node_modules/test.js/package.json | 4 + core/mr/test/spec/dot-js-module/package.json | 5 + core/mr/test/spec/dot-js-module/program.js | 5 + core/mr/test/spec/exactExports/a.js | 34 + core/mr/test/spec/exactExports/package.json | 1 + core/mr/test/spec/exactExports/program.js | 35 + .../spec/extension-loader/a.extension/a.js | 1 + .../test/spec/extension-loader/package.json | 1 + core/mr/test/spec/extension-loader/program.js | 28 + .../node_modules/http-server/index.js | 2 + .../http-server/node_modules/path/index.js | 1 + .../node_modules/path/package.json | 4 + .../http-server/node_modules/url/index.js | 1 + .../http-server/node_modules/url/package.json | 7 + .../node_modules/http-server/package.json | 8 + .../node_modules/path/index.js | 0 .../node_modules/path/package.json | 4 + .../node_modules/url/index.js | 1 + .../node_modules/url/package.json | 7 + .../test/spec/flat-module-tree/package.json | 7 + core/mr/test/spec/flat-module-tree/program.js | 44 + .../spec/hasOwnProperty/hasOwnProperty.js | 30 + core/mr/test/spec/hasOwnProperty/package.json | 1 + core/mr/test/spec/hasOwnProperty/program.js | 35 + core/mr/test/spec/hasOwnProperty/toString.js | 30 + .../identify/node_modules/cyclic/module.js | 0 .../identify/node_modules/cyclic/package.json | 6 + .../spec/identify/node_modules/x/package.json | 3 + .../mr/test/spec/identify/node_modules/x/x.js | 0 core/mr/test/spec/identify/package.json | 9 + core/mr/test/spec/identify/program.js | 11 + .../node_modules/dependency/module.js | 1 + .../node_modules/dependency/package.json | 1 + .../test/spec/inject-dependency/package.json | 1 + .../mr/test/spec/inject-dependency/program.js | 7 + .../spec/inject-into-mapping/package.json | 5 + .../test/spec/inject-into-mapping/program.js | 7 + .../somedir/mapping/module.js | 1 + .../somedir/mapping/package.json | 1 + .../spec/inject-mapping/mapping/module.js | 1 + .../spec/inject-mapping/mapping/package.json | 1 + core/mr/test/spec/inject-mapping/package.json | 1 + core/mr/test/spec/inject-mapping/program.js | 10 + core/mr/test/spec/inject/package.json | 1 + core/mr/test/spec/inject/program.js | 6 + .../node_modules/nested/index.js | 3 + .../nested/node_modules/child/index.js | 3 + .../nested/node_modules/child/package.json | 3 + .../node_modules/nested/package.json | 6 + .../mr/test/spec/legacy-bundling/package.json | 5 + core/mr/test/spec/legacy-bundling/program.js | 4 + core/mr/test/spec/load-package-digit/0/a.js | 1 + .../spec/load-package-digit/0/package.json | 4 + .../test/spec/load-package-digit/package.json | 1 + .../test/spec/load-package-digit/program.js | 8 + .../load-package-name/node_modules/a/a.js | 32 + .../node_modules/a/package.json | 3 + .../test/spec/load-package-name/package.json | 2 + .../mr/test/spec/load-package-name/program.js | 39 + core/mr/test/spec/load-package/a/a.js | 32 + core/mr/test/spec/load-package/a/package.json | 3 + core/mr/test/spec/load-package/package.json | 1 + core/mr/test/spec/load-package/program.js | 39 + .../node_modules/dependency/dependency.js | 1 + .../node_modules/dependency/package.json | 4 + core/mr/test/spec/main-name/package.json | 5 + core/mr/test/spec/main-name/program.js | 6 + .../node_modules/dot-js-ext/a.something.js | 1 + .../main/node_modules/dot-js-ext/package.json | 3 + .../spec/main/node_modules/dot-slash/a.js | 1 + .../main/node_modules/dot-slash/package.json | 3 + .../test/spec/main/node_modules/dot.js/a.js | 1 + .../main/node_modules/dot.js/package.json | 3 + .../test/spec/main/node_modules/js-ext/a.js | 1 + .../main/node_modules/js-ext/package.json | 3 + .../test/spec/main/node_modules/no-ext/a.js | 1 + .../main/node_modules/no-ext/package.json | 3 + core/mr/test/spec/main/package.json | 9 + core/mr/test/spec/main/program.js | 12 + core/mr/test/spec/main/programa.js | 9 + core/mr/test/spec/method/a.js | 43 + core/mr/test/spec/method/package.json | 1 + core/mr/test/spec/method/program.js | 39 + core/mr/test/spec/missing/package.json | 1 + core/mr/test/spec/missing/program.js | 39 + core/mr/test/spec/module-error/error.js | 1 + core/mr/test/spec/module-error/package.json | 1 + core/mr/test/spec/module-error/program.js | 17 + .../spec/module-exports/module-exports.js | 32 + core/mr/test/spec/module-exports/package.json | 1 + core/mr/test/spec/module-exports/program.js | 34 + core/mr/test/spec/module-html/package.json | 1 + core/mr/test/spec/module-html/program.js | 5 + .../spec/module-html/simple-template.html | 19 + .../node_modules/sub-module/index.js | 1 + .../node_modules/sub-module/package.json | 1 + .../spec/module-main-default/package.json | 5 + .../test/spec/module-main-default/program.js | 4 + .../spec/module-metadata/node_modules/a/a.js | 1 + .../node_modules/a/package.json | 3 + .../mr/test/spec/module-metadata/package.json | 5 + core/mr/test/spec/module-metadata/program.js | 8 + core/mr/test/spec/module-reel/package.json | 1 + core/mr/test/spec/module-reel/program.js | 5 + .../test/spec/module-reel/test.reel/test.js | 1 + core/mr/test/spec/moduleTypes/a/b.js | 1 + core/mr/test/spec/moduleTypes/a/c.json | 3 + core/mr/test/spec/moduleTypes/a/five.plus-one | 1 + core/mr/test/spec/moduleTypes/a/package.json | 1 + core/mr/test/spec/moduleTypes/package.json | 1 + core/mr/test/spec/moduleTypes/program.js | 35 + core/mr/test/spec/monkeys/a.js | 32 + core/mr/test/spec/monkeys/package.json | 1 + core/mr/test/spec/monkeys/program.js | 35 + .../node_modules/bar/package.json | 6 + .../named-mappings/node_modules/foo/foo.js | 32 + .../node_modules/foo/package.json | 3 + core/mr/test/spec/named-mappings/package.json | 6 + core/mr/test/spec/named-mappings/program.js | 34 + .../node_modules/bar/package.json | 5 + .../named-packages/node_modules/foo/foo.js | 32 + .../node_modules/foo/package.json | 3 + core/mr/test/spec/named-packages/package.json | 6 + core/mr/test/spec/named-packages/program.js | 34 + .../child-package/child-module.js | 32 + .../node_modules/child-package/package.json | 8 + .../spec/named-parent-package/package.json | 8 + .../named-parent-package/parent-module.js | 32 + .../test/spec/named-parent-package/program.js | 34 + core/mr/test/spec/nested/a/b/c/d.js | 34 + core/mr/test/spec/nested/package.json | 1 + core/mr/test/spec/nested/program.js | 34 + core/mr/test/spec/not-found/package.json | 1 + core/mr/test/spec/not-found/program.js | 40 + core/mr/test/spec/overlay/package.json | 7 + core/mr/test/spec/overlay/program.js | 11 + .../node_modules/http-server/index.js | 2 + .../http-server/node_modules/path/index.js | 1 + .../node_modules/path/package.json | 4 + .../http-server/node_modules/url/index.js | 1 + .../http-server/node_modules/url/package.json | 7 + .../node_modules/http-server/package.json | 8 + .../package-lock/node_modules/url/index.js | 1 + .../url/node_modules/path/index.js | 0 .../url/node_modules/path/package.json | 4 + .../node_modules/url/package.json | 7 + .../test/spec/package-lock/package-lock.json | 36 + core/mr/test/spec/package-lock/package.json | 7 + core/mr/test/spec/package-lock/program.js | 11 + .../dev-dependency/dev-dependency.js | 1 + .../node_modules/dev-dependency/package.json | 4 + core/mr/test/spec/production/package.json | 6 + core/mr/test/spec/production/program.js | 14 + core/mr/test/spec/read/package.json | 5 + core/mr/test/spec/read/program.js | 24 + .../node_modules/foo/barz.js | 3 + .../node_modules/foo/package.json | 6 + .../test/spec/redirects-package/package.json | 6 + .../mr/test/spec/redirects-package/program.js | 3 + core/mr/test/spec/redirects/barz.js | 3 + core/mr/test/spec/redirects/package.json | 6 + core/mr/test/spec/redirects/program.js | 3 + core/mr/test/spec/relative/package.json | 1 + core/mr/test/spec/relative/program.js | 36 + core/mr/test/spec/relative/submodule/a.js | 32 + core/mr/test/spec/relative/submodule/b.js | 33 + core/mr/test/spec/return/package.json | 1 + core/mr/test/spec/return/program.js | 34 + core/mr/test/spec/return/returns.js | 32 + core/mr/test/spec/sandbox/a.js | 2 + core/mr/test/spec/sandbox/b.js | 1 + .../spec/sandbox/node_modules/dependency/c.js | 1 + .../sandbox/node_modules/dependency/main.js | 1 + .../sandbox/node_modules/dependency/other.js | 1 + .../node_modules/dependency/package.json | 1 + core/mr/test/spec/sandbox/package.json | 13 + core/mr/test/spec/sandbox/program.js | 37 + .../node_modules/dependency/main.load.js | 8 + .../node_modules/dependency/package.json | 4 + .../node_modules/dependency/second.load.js | 6 + .../spec/script-injection-dep/package.json | 5 + .../test/spec/script-injection-dep/program.js | 8 + .../test/spec/script-injection/package.json | 9 + .../packages/dependency/main.load.js | 7 + .../packages/dependency/package.json.load.js | 6 + core/mr/test/spec/script-injection/program.js | 7 + .../test/spec/serialization-compiler/model.js | 4 + .../spec/serialization-compiler/object.js | 3 + .../spec/serialization-compiler/package.json | 1 + .../spec/serialization-compiler/program.js | 8 + core/mr/test/spec/top-level/b.js | 32 + core/mr/test/spec/top-level/package.json | 1 + core/mr/test/spec/top-level/program.js | 36 + core/mr/test/spec/top-level/submodule/a.js | 34 + core/mr/test/spec/transitive/a.js | 32 + core/mr/test/spec/transitive/b.js | 32 + core/mr/test/spec/transitive/c.js | 34 + core/mr/test/spec/transitive/package.json | 1 + core/mr/test/spec/transitive/program.js | 34 + 271 files changed, 6968 insertions(+) create mode 100644 core/mr/.editorconfig create mode 100644 core/mr/.gitignore create mode 100644 core/mr/.jshintignore create mode 100644 core/mr/.jshintrc create mode 100644 core/mr/.travis.yml create mode 100644 core/mr/.vscode/launch.json create mode 100644 core/mr/CHANGES.md create mode 100644 core/mr/CONTRIBUTING.md create mode 100644 core/mr/LICENSE.md create mode 100644 core/mr/README.md create mode 100644 core/mr/adhoc.html create mode 100644 core/mr/adhoc.js create mode 100755 core/mr/bin/mr create mode 100644 core/mr/bootstrap-node.js create mode 100644 core/mr/bootstrap.js create mode 100644 core/mr/browser.js create mode 100644 core/mr/demo/data.js create mode 100644 core/mr/demo/index.html create mode 100644 core/mr/demo/index.js create mode 100644 core/mr/demo/package.json create mode 100644 core/mr/docs/Config-API.md create mode 100644 core/mr/docs/How-it-works.md create mode 100644 core/mr/docs/Module-API.md create mode 100644 core/mr/docs/Node-compatability.md create mode 100644 core/mr/docs/Package-API.md create mode 100644 core/mr/docs/Require-API.md create mode 100644 core/mr/docs/Script-attributes.md create mode 100644 core/mr/karma.conf.js create mode 100644 core/mr/node.js create mode 100644 core/mr/package.json create mode 100644 core/mr/require.js create mode 100644 core/mr/sandbox.js create mode 100644 core/mr/test/README.md create mode 100644 core/mr/test/all.js create mode 100644 core/mr/test/package.json create mode 100644 core/mr/test/run-browser.js create mode 100644 core/mr/test/run-karma.html create mode 100644 core/mr/test/run-karma.js create mode 100644 core/mr/test/run-node.js create mode 100644 core/mr/test/run.html create mode 100644 core/mr/test/spec/browser-alternative/node_modules/shimmy/browser.js create mode 100644 core/mr/test/spec/browser-alternative/node_modules/shimmy/index.js create mode 100644 core/mr/test/spec/browser-alternative/node_modules/shimmy/package.json create mode 100644 core/mr/test/spec/browser-alternative/package.json create mode 100644 core/mr/test/spec/browser-alternative/program.js create mode 100644 core/mr/test/spec/browser-alternatives/node_modules/shimmy/browser.js create mode 100644 core/mr/test/spec/browser-alternatives/node_modules/shimmy/index.js create mode 100644 core/mr/test/spec/browser-alternatives/node_modules/shimmy/package.json create mode 100644 core/mr/test/spec/browser-alternatives/package.json create mode 100644 core/mr/test/spec/browser-alternatives/program.js create mode 100644 core/mr/test/spec/case-sensitive/a.js create mode 100644 core/mr/test/spec/case-sensitive/package.json create mode 100644 core/mr/test/spec/case-sensitive/program.js create mode 100644 core/mr/test/spec/comments/package.json create mode 100644 core/mr/test/spec/comments/program.js create mode 100644 core/mr/test/spec/cyclic/a.js create mode 100644 core/mr/test/spec/cyclic/b.js create mode 100644 core/mr/test/spec/cyclic/package.json create mode 100644 core/mr/test/spec/cyclic/program.js create mode 100644 core/mr/test/spec/determinism/package.json create mode 100644 core/mr/test/spec/determinism/program.js create mode 100644 core/mr/test/spec/determinism/submodule/a.js create mode 100644 core/mr/test/spec/determinism/submodule/b.js create mode 100644 core/mr/test/spec/dev-dependencies/node_modules/dev-dependency/dev-dependency.js create mode 100644 core/mr/test/spec/dev-dependencies/node_modules/dev-dependency/package.json create mode 100644 core/mr/test/spec/dev-dependencies/package.json create mode 100644 core/mr/test/spec/dev-dependencies/program.js create mode 100644 core/mr/test/spec/directory-index/bar.js create mode 100644 core/mr/test/spec/directory-index/foo/index.js create mode 100644 core/mr/test/spec/directory-index/package.json create mode 100644 core/mr/test/spec/directory-index/program.js create mode 100644 core/mr/test/spec/dot-js-module/node_modules/test.js/main.load.js create mode 100644 core/mr/test/spec/dot-js-module/node_modules/test.js/package.json create mode 100644 core/mr/test/spec/dot-js-module/package.json create mode 100644 core/mr/test/spec/dot-js-module/program.js create mode 100644 core/mr/test/spec/exactExports/a.js create mode 100644 core/mr/test/spec/exactExports/package.json create mode 100644 core/mr/test/spec/exactExports/program.js create mode 100644 core/mr/test/spec/extension-loader/a.extension/a.js create mode 100644 core/mr/test/spec/extension-loader/package.json create mode 100644 core/mr/test/spec/extension-loader/program.js create mode 100644 core/mr/test/spec/flat-module-tree/node_modules/http-server/index.js create mode 100644 core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/path/index.js create mode 100644 core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/path/package.json create mode 100644 core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/url/index.js create mode 100644 core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/url/package.json create mode 100644 core/mr/test/spec/flat-module-tree/node_modules/http-server/package.json create mode 100644 core/mr/test/spec/flat-module-tree/node_modules/path/index.js create mode 100644 core/mr/test/spec/flat-module-tree/node_modules/path/package.json create mode 100644 core/mr/test/spec/flat-module-tree/node_modules/url/index.js create mode 100644 core/mr/test/spec/flat-module-tree/node_modules/url/package.json create mode 100644 core/mr/test/spec/flat-module-tree/package.json create mode 100644 core/mr/test/spec/flat-module-tree/program.js create mode 100644 core/mr/test/spec/hasOwnProperty/hasOwnProperty.js create mode 100644 core/mr/test/spec/hasOwnProperty/package.json create mode 100644 core/mr/test/spec/hasOwnProperty/program.js create mode 100644 core/mr/test/spec/hasOwnProperty/toString.js create mode 100644 core/mr/test/spec/identify/node_modules/cyclic/module.js create mode 100644 core/mr/test/spec/identify/node_modules/cyclic/package.json create mode 100644 core/mr/test/spec/identify/node_modules/x/package.json create mode 100644 core/mr/test/spec/identify/node_modules/x/x.js create mode 100644 core/mr/test/spec/identify/package.json create mode 100644 core/mr/test/spec/identify/program.js create mode 100644 core/mr/test/spec/inject-dependency/node_modules/dependency/module.js create mode 100644 core/mr/test/spec/inject-dependency/node_modules/dependency/package.json create mode 100644 core/mr/test/spec/inject-dependency/package.json create mode 100644 core/mr/test/spec/inject-dependency/program.js create mode 100644 core/mr/test/spec/inject-into-mapping/package.json create mode 100644 core/mr/test/spec/inject-into-mapping/program.js create mode 100644 core/mr/test/spec/inject-into-mapping/somedir/mapping/module.js create mode 100644 core/mr/test/spec/inject-into-mapping/somedir/mapping/package.json create mode 100644 core/mr/test/spec/inject-mapping/mapping/module.js create mode 100644 core/mr/test/spec/inject-mapping/mapping/package.json create mode 100644 core/mr/test/spec/inject-mapping/package.json create mode 100644 core/mr/test/spec/inject-mapping/program.js create mode 100644 core/mr/test/spec/inject/package.json create mode 100644 core/mr/test/spec/inject/program.js create mode 100644 core/mr/test/spec/legacy-bundling/node_modules/nested/index.js create mode 100644 core/mr/test/spec/legacy-bundling/node_modules/nested/node_modules/child/index.js create mode 100644 core/mr/test/spec/legacy-bundling/node_modules/nested/node_modules/child/package.json create mode 100644 core/mr/test/spec/legacy-bundling/node_modules/nested/package.json create mode 100644 core/mr/test/spec/legacy-bundling/package.json create mode 100644 core/mr/test/spec/legacy-bundling/program.js create mode 100644 core/mr/test/spec/load-package-digit/0/a.js create mode 100644 core/mr/test/spec/load-package-digit/0/package.json create mode 100644 core/mr/test/spec/load-package-digit/package.json create mode 100644 core/mr/test/spec/load-package-digit/program.js create mode 100644 core/mr/test/spec/load-package-name/node_modules/a/a.js create mode 100644 core/mr/test/spec/load-package-name/node_modules/a/package.json create mode 100644 core/mr/test/spec/load-package-name/package.json create mode 100644 core/mr/test/spec/load-package-name/program.js create mode 100644 core/mr/test/spec/load-package/a/a.js create mode 100644 core/mr/test/spec/load-package/a/package.json create mode 100644 core/mr/test/spec/load-package/package.json create mode 100644 core/mr/test/spec/load-package/program.js create mode 100644 core/mr/test/spec/main-name/node_modules/dependency/dependency.js create mode 100644 core/mr/test/spec/main-name/node_modules/dependency/package.json create mode 100644 core/mr/test/spec/main-name/package.json create mode 100644 core/mr/test/spec/main-name/program.js create mode 100644 core/mr/test/spec/main/node_modules/dot-js-ext/a.something.js create mode 100644 core/mr/test/spec/main/node_modules/dot-js-ext/package.json create mode 100644 core/mr/test/spec/main/node_modules/dot-slash/a.js create mode 100644 core/mr/test/spec/main/node_modules/dot-slash/package.json create mode 100644 core/mr/test/spec/main/node_modules/dot.js/a.js create mode 100644 core/mr/test/spec/main/node_modules/dot.js/package.json create mode 100644 core/mr/test/spec/main/node_modules/js-ext/a.js create mode 100644 core/mr/test/spec/main/node_modules/js-ext/package.json create mode 100644 core/mr/test/spec/main/node_modules/no-ext/a.js create mode 100644 core/mr/test/spec/main/node_modules/no-ext/package.json create mode 100644 core/mr/test/spec/main/package.json create mode 100644 core/mr/test/spec/main/program.js create mode 100644 core/mr/test/spec/main/programa.js create mode 100644 core/mr/test/spec/method/a.js create mode 100644 core/mr/test/spec/method/package.json create mode 100644 core/mr/test/spec/method/program.js create mode 100644 core/mr/test/spec/missing/package.json create mode 100644 core/mr/test/spec/missing/program.js create mode 100644 core/mr/test/spec/module-error/error.js create mode 100644 core/mr/test/spec/module-error/package.json create mode 100644 core/mr/test/spec/module-error/program.js create mode 100644 core/mr/test/spec/module-exports/module-exports.js create mode 100644 core/mr/test/spec/module-exports/package.json create mode 100644 core/mr/test/spec/module-exports/program.js create mode 100644 core/mr/test/spec/module-html/package.json create mode 100644 core/mr/test/spec/module-html/program.js create mode 100644 core/mr/test/spec/module-html/simple-template.html create mode 100644 core/mr/test/spec/module-main-default/node_modules/sub-module/index.js create mode 100644 core/mr/test/spec/module-main-default/node_modules/sub-module/package.json create mode 100644 core/mr/test/spec/module-main-default/package.json create mode 100644 core/mr/test/spec/module-main-default/program.js create mode 100644 core/mr/test/spec/module-metadata/node_modules/a/a.js create mode 100644 core/mr/test/spec/module-metadata/node_modules/a/package.json create mode 100644 core/mr/test/spec/module-metadata/package.json create mode 100644 core/mr/test/spec/module-metadata/program.js create mode 100644 core/mr/test/spec/module-reel/package.json create mode 100644 core/mr/test/spec/module-reel/program.js create mode 100644 core/mr/test/spec/module-reel/test.reel/test.js create mode 100644 core/mr/test/spec/moduleTypes/a/b.js create mode 100644 core/mr/test/spec/moduleTypes/a/c.json create mode 100644 core/mr/test/spec/moduleTypes/a/five.plus-one create mode 100644 core/mr/test/spec/moduleTypes/a/package.json create mode 100644 core/mr/test/spec/moduleTypes/package.json create mode 100644 core/mr/test/spec/moduleTypes/program.js create mode 100644 core/mr/test/spec/monkeys/a.js create mode 100644 core/mr/test/spec/monkeys/package.json create mode 100644 core/mr/test/spec/monkeys/program.js create mode 100644 core/mr/test/spec/named-mappings/node_modules/bar/package.json create mode 100644 core/mr/test/spec/named-mappings/node_modules/foo/foo.js create mode 100644 core/mr/test/spec/named-mappings/node_modules/foo/package.json create mode 100644 core/mr/test/spec/named-mappings/package.json create mode 100644 core/mr/test/spec/named-mappings/program.js create mode 100644 core/mr/test/spec/named-packages/node_modules/bar/package.json create mode 100644 core/mr/test/spec/named-packages/node_modules/foo/foo.js create mode 100644 core/mr/test/spec/named-packages/node_modules/foo/package.json create mode 100644 core/mr/test/spec/named-packages/package.json create mode 100644 core/mr/test/spec/named-packages/program.js create mode 100644 core/mr/test/spec/named-parent-package/node_modules/child-package/child-module.js create mode 100644 core/mr/test/spec/named-parent-package/node_modules/child-package/package.json create mode 100644 core/mr/test/spec/named-parent-package/package.json create mode 100644 core/mr/test/spec/named-parent-package/parent-module.js create mode 100644 core/mr/test/spec/named-parent-package/program.js create mode 100644 core/mr/test/spec/nested/a/b/c/d.js create mode 100644 core/mr/test/spec/nested/package.json create mode 100644 core/mr/test/spec/nested/program.js create mode 100644 core/mr/test/spec/not-found/package.json create mode 100644 core/mr/test/spec/not-found/program.js create mode 100644 core/mr/test/spec/overlay/package.json create mode 100644 core/mr/test/spec/overlay/program.js create mode 100644 core/mr/test/spec/package-lock/node_modules/http-server/index.js create mode 100644 core/mr/test/spec/package-lock/node_modules/http-server/node_modules/path/index.js create mode 100644 core/mr/test/spec/package-lock/node_modules/http-server/node_modules/path/package.json create mode 100644 core/mr/test/spec/package-lock/node_modules/http-server/node_modules/url/index.js create mode 100644 core/mr/test/spec/package-lock/node_modules/http-server/node_modules/url/package.json create mode 100644 core/mr/test/spec/package-lock/node_modules/http-server/package.json create mode 100644 core/mr/test/spec/package-lock/node_modules/url/index.js create mode 100644 core/mr/test/spec/package-lock/node_modules/url/node_modules/path/index.js create mode 100644 core/mr/test/spec/package-lock/node_modules/url/node_modules/path/package.json create mode 100644 core/mr/test/spec/package-lock/node_modules/url/package.json create mode 100644 core/mr/test/spec/package-lock/package-lock.json create mode 100644 core/mr/test/spec/package-lock/package.json create mode 100644 core/mr/test/spec/package-lock/program.js create mode 100644 core/mr/test/spec/production/node_modules/dev-dependency/dev-dependency.js create mode 100644 core/mr/test/spec/production/node_modules/dev-dependency/package.json create mode 100644 core/mr/test/spec/production/package.json create mode 100644 core/mr/test/spec/production/program.js create mode 100644 core/mr/test/spec/read/package.json create mode 100644 core/mr/test/spec/read/program.js create mode 100644 core/mr/test/spec/redirects-package/node_modules/foo/barz.js create mode 100644 core/mr/test/spec/redirects-package/node_modules/foo/package.json create mode 100644 core/mr/test/spec/redirects-package/package.json create mode 100644 core/mr/test/spec/redirects-package/program.js create mode 100644 core/mr/test/spec/redirects/barz.js create mode 100644 core/mr/test/spec/redirects/package.json create mode 100644 core/mr/test/spec/redirects/program.js create mode 100644 core/mr/test/spec/relative/package.json create mode 100644 core/mr/test/spec/relative/program.js create mode 100644 core/mr/test/spec/relative/submodule/a.js create mode 100644 core/mr/test/spec/relative/submodule/b.js create mode 100644 core/mr/test/spec/return/package.json create mode 100644 core/mr/test/spec/return/program.js create mode 100644 core/mr/test/spec/return/returns.js create mode 100644 core/mr/test/spec/sandbox/a.js create mode 100644 core/mr/test/spec/sandbox/b.js create mode 100644 core/mr/test/spec/sandbox/node_modules/dependency/c.js create mode 100644 core/mr/test/spec/sandbox/node_modules/dependency/main.js create mode 100644 core/mr/test/spec/sandbox/node_modules/dependency/other.js create mode 100644 core/mr/test/spec/sandbox/node_modules/dependency/package.json create mode 100644 core/mr/test/spec/sandbox/package.json create mode 100644 core/mr/test/spec/sandbox/program.js create mode 100644 core/mr/test/spec/script-injection-dep/node_modules/dependency/main.load.js create mode 100644 core/mr/test/spec/script-injection-dep/node_modules/dependency/package.json create mode 100644 core/mr/test/spec/script-injection-dep/node_modules/dependency/second.load.js create mode 100644 core/mr/test/spec/script-injection-dep/package.json create mode 100644 core/mr/test/spec/script-injection-dep/program.js create mode 100644 core/mr/test/spec/script-injection/package.json create mode 100644 core/mr/test/spec/script-injection/packages/dependency/main.load.js create mode 100644 core/mr/test/spec/script-injection/packages/dependency/package.json.load.js create mode 100644 core/mr/test/spec/script-injection/program.js create mode 100644 core/mr/test/spec/serialization-compiler/model.js create mode 100644 core/mr/test/spec/serialization-compiler/object.js create mode 100644 core/mr/test/spec/serialization-compiler/package.json create mode 100644 core/mr/test/spec/serialization-compiler/program.js create mode 100644 core/mr/test/spec/top-level/b.js create mode 100644 core/mr/test/spec/top-level/package.json create mode 100644 core/mr/test/spec/top-level/program.js create mode 100644 core/mr/test/spec/top-level/submodule/a.js create mode 100644 core/mr/test/spec/transitive/a.js create mode 100644 core/mr/test/spec/transitive/b.js create mode 100644 core/mr/test/spec/transitive/c.js create mode 100644 core/mr/test/spec/transitive/package.json create mode 100644 core/mr/test/spec/transitive/program.js diff --git a/core/mr/.editorconfig b/core/mr/.editorconfig new file mode 100644 index 0000000000..c9721be133 --- /dev/null +++ b/core/mr/.editorconfig @@ -0,0 +1,11 @@ +# http://EditorConfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/core/mr/.gitignore b/core/mr/.gitignore new file mode 100644 index 0000000000..6ba1cdc9e7 --- /dev/null +++ b/core/mr/.gitignore @@ -0,0 +1,10 @@ +.npmignore +.tmp +.idea +.DS_Store +atlassian-ide-plugin.xml +npm-debug.log +report/ +node_modules/ +!test/spec/**/node_modules +out/ diff --git a/core/mr/.jshintignore b/core/mr/.jshintignore new file mode 100644 index 0000000000..909137170f --- /dev/null +++ b/core/mr/.jshintignore @@ -0,0 +1,4 @@ +**/node_modules/** +**/test/spec/** +**/report/** +**/packages/** \ No newline at end of file diff --git a/core/mr/.jshintrc b/core/mr/.jshintrc new file mode 100644 index 0000000000..ab9705cfc5 --- /dev/null +++ b/core/mr/.jshintrc @@ -0,0 +1,32 @@ +{ + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "forin": true, + "noarg": true, + "noempty": true, + "nonew": true, + "undef": true, + "unused": "paramsignore", + "trailing": true, + "indent": 4, + "boss": true, + "eqnull": true, + "browser": true, + "globals": { + "CustomEvent": true, + "WebSocket": false, + + "require": false, + "exports": false, + "module": false, + "global": false, + + "WeakMap": true, + "Map": true, + "Set": true, + + "console": false + } +} diff --git a/core/mr/.travis.yml b/core/mr/.travis.yml new file mode 100644 index 0000000000..a3096c1c7b --- /dev/null +++ b/core/mr/.travis.yml @@ -0,0 +1,43 @@ +language: node_js +cache: + directories: + - node_modules +node_js: + - "10" + - "8" + - "6" + - "4" +script: npm run $COMMAND +env: +- COMMAND=test +- COMMAND=test:karma +- COMMAND=integration MR_VERSION=. MOP_VERSION=latest +- COMMAND=integration MR_VERSION=. MOP_VERSION="#master" +- COMMAND=integration MR_VERSION=. MOP_VERSION="18.0.0" +jobs: + include: + - stage: lint + node_js: 9 + env: + script: npm run lint + - stage: deploy + node_js: 4 + script: skip + env: + deploy: + provider: npm + email: "${NPM_EMAIL}" + api_key: "${NPM_API_KEY}" + on: + tags: true +stages: + - lint + - test + - deploy +notifications: + irc: + channels: + - "chat.freenode.net#montage" + on_success: false + template: + - "%{author} broke the %{repository} tests on %{branch}: %{build_url}" diff --git a/core/mr/.vscode/launch.json b/core/mr/.vscode/launch.json new file mode 100644 index 0000000000..14658d4433 --- /dev/null +++ b/core/mr/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "npm test mr", + "program": "${workspaceFolder}/core/mr/test/run-node.js" + }, + { + "type": "node", + "runtimeVersion": "10.16.3", + "request": "launch", + "name": "Debug mop-integration", + "runtimeExecutable": "npm", + "env": {"MOP_VERSION":"#master", "MR_VERSION":"#master"}, + "runtimeArgs": [ + "run-script", + "integration-debug" + "--inspect-brk=9229" + ], + "port": 9229 + }, + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceFolder}/core/mr/require" + } + ] +} diff --git a/core/mr/CHANGES.md b/core/mr/CHANGES.md new file mode 100644 index 0000000000..cfe3bacdd1 --- /dev/null +++ b/core/mr/CHANGES.md @@ -0,0 +1,181 @@ +### 17.0.12 + - Fix commented require issue + - Add support for require('dir') dir/index.js + +### 17.0.0 + - Update Travis NodeJS to 4.8.0 + - Migrate Montage custom loaders to MontageRequire + - Import support for module types html, mjson and reel from Montage.js + - Import module metadata annotation from Montage.js + - Upgrade tests stack + - Migrate specs to Jasmine 2.5.2O (npm run test:jasmine) + - Revamp NodeJS tests runner (npm test) + - Migrate Phantom.js tests runner to Karma (npm run test:karma) + +### 16.0.4 + - Memory optimization by caching a regex and making sure XHRs that are re-used don’t hold on their responses by calling abort() after the request succeeded or failed + - Minimize object creation and closure scope + - speed optimization + - reducing scope lookup + - reducing scope lookup in closure + +### 16.0.3 + - updates bluebird dependency to ~3.4.6 + +### 16.0.2 + + - Fixes a bug where a JSON module would fail to load because it would try to re-parse the content while it was already done + +### 16.0.1 + + - Addresses an issue caused by IE11 non-standard Map.prototype.set that returns undefined instead of Map itself + +### 16.0.0 + + - Performance Improvements + +### 0.15.7 + + - Don't load Mr's `devDependencies` into the mappings + +### 0.15.6 + + - Update Q to v1.0.1 + +### 0.15.5 + + - Disable Firebug workaround that breaks modern Firefox + +### 0.15.4 + + - Fix display name for packages beginning with digits + +### 0.15.3 + + - Revert sourceMappingURL change, causes issues in Chrome and doesn't work great in Firefox. + +### 0.15.2 + + - Change `//@` for `//#` for SourceURL comment to match the spec + - Use `sourceMappingURL` instead of `sourceURL`. This allows the evaled code + to appear as source files in Firefox. + - Friendlier display names for modules: + `__FILE__http______localhost__8081__montagejs__mr__demo__data__` is now + `mr_demo__data` + +### 0.15.1 + + - Fix requiring dependencies with ".js" in their name in script-injection mode + (thanks @evax) + - Fix requiring twice a module that throws an error + +## 0.15.0 + + - Added `moduleTypes` config parameter so that all loadable extensions are + known. This fixes a bug where modules with a "." in them would not be loaded + as JavaScript modules. When implementing a custom extension loader you must + add the extension to the `config.moduleTypes` array when loading a package. + +### 0.14.2 + + - Use overlays in config given to `loadPackage`. + +### 0.14.1 + + - Correct extension detection + +## 0.14.0 + + - Remove support for `directories` `package.json` property. Node ignores the + property, and continuing to support it breaks compatibility + - Remove support for package reflexive module names + - Fix main linkage for relative identifiers. Previously, a package with a + `main` property that started with "./" would be linked incorrectly. + - Fix loading of modules with a `.min.js` extension + - Don't block XHR for the `file:` protocol. Firefox and Safari allow it as + long as requests remain within the HTML page's directory. + - Add support for the `browser` property in `package.json`, as pioneered by + Browserify + - Add "sandbox", to inject dependencies into a module. Require with + `require("core/mr/sandbox")` + +### 0.13.4 + + - Update Q from v0.9.6 to v0.9.7 + - Fix loading of bundles + - Wait for preload to finish before issuing requests for modules that might + be included in one of the bundles + +### 0.13.3 + + - Use `config.read` when running on Node + +### 0.13.2 + + - Use `config.read` to load `package.json` if given to `loadPackage` + +### 0.13.1 + + - Fix `require.identify` to work with cyclic package dependencies + +## 0.13.0 + + - Fix bootstrap stopping if document had finished loading. + - Update to Q v0.9.6 + - Add more complete demo and split the readme into multiple documentation + files. + +### 0.12.14 + + - Fix bug when loading dependencies that use script-injection which are not + included in a preloading bundle. Before Mr would hang when waiting for them + to load. + +### 0.12.13 + + - Fix bug in preloading, where isResolved was replaced with isPending in Q 0.9 + +### 0.12.12 + + - Fix preloading. Fixes some logic in figuring out whether to issue a script + request for a package.json in production + - Test runner updates + +### 0.12.11 + + - Add injectDependency and injectMapping + - Update case sensitivity test to capture errors on first require, for case + sensitive file systems + - Add support for running tests under PhantomJS and Travis + +### 0.12.10 + + - Update Q from v0.9.0 to v0.9.2 + +### 0.12.9 + + - Update Q from v0.8.12 to v0.9.0 + +### 0.12.8 + + - Defer throwing load errors to execution (Fixes #14) + - Update bootstrapping for latest Q + +### 0.12.7 + + - Support returned exports in bootstrapping + - Export more Node utilities (when used on Node) + - Require.urlToPath -> Require.locationToPath(location) + - Add Require.filePathToLocation(path) + - Add Require.directoryPathToLocation(path) + - Add Require.findPackagePath(directory) + - Add Require.findPackageLocationAndModuleId(path) + +### 0.12.6 + + - Add support for `production` mode. Currently causes Mr to ignore + `devDependencies` + +## 0.12.5 + + - Update Q to 0.8.12 diff --git a/core/mr/CONTRIBUTING.md b/core/mr/CONTRIBUTING.md new file mode 100644 index 0000000000..b3ffcb4e61 --- /dev/null +++ b/core/mr/CONTRIBUTING.md @@ -0,0 +1,47 @@ +Contributing +============ + +Pull requests are gladly accepted. We also really appreciate tests. + +Tests +----- + +On the command line the tests can be run by running `npm test`. This will run the tests in Node, and then in PhantomJS. + +The tests can also be run directly in the browser by opening `spec/run.html`. Note that the tests must be accessed through a web server, and not through a `file://` url. + +### Creating + +Here's how to create a new test: + + 1. Create a new directory in `spec/`. + 2. Add the name of the directory to the bottom of the array in `spec/require-spec.js`. If the test should not be run on Node.js then instead of a string add an object: `{name: "YOUR-NAME", node: false}`. + 3. Inside the new directory create a `package.json` file. The contents of this file can just be `{}` (an empty JSON object), unless you are testing or using some of the `package.json` features. + 4. Inside the new directory create a `program.js` file. This is where the test is. The contents of this file depends on whether the test is synchronous or asynchronous: + + * Synchronous test + + ```javascript + var test = require('test'); + + // your test here + test.assert(true === true, "assertion message"); + + test.print('DONE', 'info'); + ``` + + * Asynchronous test + + ```javascript + var test = require('test'); + + // your test starts here... + return /* async call */.then(function () { + // ...and continues here + test.assert(true === true, "assertion message"); + + test.print('DONE', 'info'); + }); + ``` + +Add any other modules you need inside the directory, including `node_modules` directories if you are testing package dependencies. diff --git a/core/mr/LICENSE.md b/core/mr/LICENSE.md new file mode 100644 index 0000000000..ae28ed02b2 --- /dev/null +++ b/core/mr/LICENSE.md @@ -0,0 +1,31 @@ +3-clause BSD license +==================== + +Copyright 2012-2014 Motorola Mobility LLC, Montage Studio Inc, and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC, Montage Studio, Montage nor the + names of its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/core/mr/README.md b/core/mr/README.md new file mode 100644 index 0000000000..65394ee3c4 --- /dev/null +++ b/core/mr/README.md @@ -0,0 +1,87 @@ + +# Montage Require (aka mr) + +[![npm version](https://img.shields.io/npm/v/mr.svg?style=flat)](https://www.npmjs.com/package/mr) + +[![Build Status](https://travis-ci.org/montagejs/mr.svg?branch=master)](http://travis-ci.org/montagejs/mr) + +[![Analytics](https://ga-beacon.appspot.com/UA-35717912-2/montagejs/mr)](https://github.com/montagejs/mr) + +This is a CommonJS module system, highly compatible with NodeJS, +intended for front-end development of web applications using npm style +packages. It is designed to be automatically replaced by the Montage +Optimizer with a smaller, faster and bundled production module system. + +Mr is installed as a package in your application using npm: + +```bash +$ npm init # if you don't already have a package.json +$ npm install --save mr +``` + +In an HTML file next to your `package.json` add the Mr script and provide a +module to load: + +```html + +``` + +Start writing your code in `index.js`, using the `require` function as you +would in Node. Have a look at the [demo](https://github.com/montagejs/mr/tree/master/demo) +for working example. + +You can place your `package.json` in a different location, or avoid having one +at all, with other [script tag attributes](https://github.com/montagejs/mr/tree/master/docs/Script-attributes.md). + +## Optimization + +Take a look at [Mop, the Montage Optimizer](https://github.com/montagejs/mop) +to optimize applications for production. The optimizer can bundle packages with +all of the dependent modules, can preload bundles of progressive enhancements +in phases, and can generate HTML5 application cache manifests. + +## Documentation + +Mr is [compatible with Node and npm](https://github.com/montagejs/mr/tree/master/docs/Node-compatability.md), although +there are some differences. + +There is documentation for: + + - [`package.json` properties](https://github.com/montagejs/mr/tree/master/docs/Package-API.md) + - [`require` function](https://github.com/montagejs/mr/tree/master/docs/Require-API.md) + - [`module` object](https://github.com/montagejs/mr/tree/master/docs/Module-API.md) + - [The package `config` object](https://github.com/montagejs/mr/tree/master/docs/Config-API.md) + +And you may be interested in an in-depth look at [how Mr works](https://github.com/montagejs/mr/tree/master/docs/How-it-works.md). + +## Compatibility + +At present, Mr depends on `document.querySelector` and +probably several other recent EcmaScript methods that might not be +available in legacy browsers. With your help, I intend to isolate and +fix these bugs. + +At time of writing, tests pass in Chrome 21, Safari 5.1.5, and Firefox +13 on Mac OS 10.6. + + +## Maintenance + +Tests are in the `spec` directory. Use `npm test` to run the tests in +NodeJS or open `spec/run.html` in a browser. + +To run the tests in your browser, simply use `npm run test:jasmine`. + +To run the tests using Karma use `npm run test:karma` and for continious tests run with file changes detection `npm run test:karma-dev`. + +## About + +This implementation is a part from Motorola Mobility’s [Montage][] web +application framework. The module system was written by Tom Robinson +and Kris Kowal. Motorola holds the copyright on much of the original +content, and provided it as open source under the permissive BSD +3-Clause license. This project is maintained by Kris Kowal and Stuart +Knightley, continuing with that license. + +[Montage]: http://github.com/montage.js/montage + diff --git a/core/mr/adhoc.html b/core/mr/adhoc.html new file mode 100644 index 0000000000..824687316e --- /dev/null +++ b/core/mr/adhoc.html @@ -0,0 +1,25 @@ + + + + + + +

Open your browser's console.

+
+ + + + + + + + + + + + + +
Package location:
Module Identifier:
+
+ + diff --git a/core/mr/adhoc.js b/core/mr/adhoc.js new file mode 100644 index 0000000000..432185bb91 --- /dev/null +++ b/core/mr/adhoc.js @@ -0,0 +1,31 @@ +/* global URL:true */ + +var URL = require("mini-url"); +var QS = require("qs"); + +var a = document.createElement("a"); + +var packageLocation; +var moduleId; + +if (window.location.search) { + var query = QS.parse(window.location.search.slice(1)); + var packageLocation = query['package-location']; + var moduleId = query['module-id']; + document.querySelector("[name=package-location]").value = packageLocation; + document.querySelector("[name=module-id]").value = moduleId; + run(packageLocation, moduleId); +} + +function run(packageLocation, moduleId) { + packageLocation = URL.resolve(window.location, packageLocation); + moduleId = moduleId || ""; + + console.log("Require:", "package:", JSON.stringify(packageLocation), "id:", JSON.stringify(moduleId)); + require.loadPackage(packageLocation) + .invoke("async", moduleId) + .then(function (exports) { + console.log("Exports:", exports); + console.log("Packages:", require.packages); + }); +} diff --git a/core/mr/bin/mr b/core/mr/bin/mr new file mode 100755 index 0000000000..635bd9d007 --- /dev/null +++ b/core/mr/bin/mr @@ -0,0 +1,2 @@ +#!/usr/bin/env node --harmony_weakmaps --harmony_proxies +require("../bootstrap-node"); diff --git a/core/mr/bootstrap-node.js b/core/mr/bootstrap-node.js new file mode 100644 index 0000000000..9224eba727 --- /dev/null +++ b/core/mr/bootstrap-node.js @@ -0,0 +1,86 @@ + +/* + Based in part on Motorola Mobility’s Montage + Copyright (c) 2012, Motorola Mobility LLC. All Rights Reserved. + 3-Clause BSD License + https://github.com/motorola-mobility/montage/blob/master/LICENSE.md +*/ +/*jshint node:true */ +var Require = require("./require"); +require("./node"); // patches Require +var URL = require("url"); +var Promise = require("bluebird"); +var FS = require("fs"); +var PATH = require("path"); + +Require.overlays = ["node", "server", "montage"]; + +var bootstrap = function () { + var command = process.argv.slice(0, 3); + var args = process.argv.slice(2); + var program = args.shift(); + FS.realpath(program, function (error, program) { + if (error) { + throw new Error(error); + } + findPackage(PATH.dirname(program), function (error, directory) { + if (error === "Can't find package") { + loadFreeModule(program, command, args); + } else if (error) { + throw new Error(error); + } else { + loadPackagedModule(directory, program, command, args); + } + }); + }); +}; + +function findPackage(directory, callback) { + if (directory === PATH.dirname(directory)) { + return callback("Can't find package"); + } + var packageJson = PATH.join(directory, "package.json"); + FS.stat(packageJson, function (error, stat) { + if (error || !stat.isFile()) { + findPackage(PATH.dirname(directory), callback); + } else { + callback(null, directory); + } + }); +} + +var loadPackagedModule = function (directory, program, command, args) { + loadPackage(directory) + .then(function (require) { + var id = program.slice(directory.length + 1); + return require.async(id); + }); +}; + +exports.loadPackage = loadPackage; +function loadPackage(location, config) { + if (location.slice(location.length - 1, location.length) !== "/") { + location += "/"; + } + config = config || {}; + config.location = URL.resolve(Require.getLocation(), location); + return Require.loadPackage(config.location, config); +} + +var loadFreeModule = function (program, command, args) { + program = URL.resolve("file:" + program, ""); + var directory = URL.resolve(program, "./"); + var descriptions = {}; + descriptions[directory] = Promise.resolve({}); + return Require.loadPackage(directory, { + descriptions: descriptions + }) + .then(function (require) { + var id = program.slice(directory.length); + return require.async(id); + }); +}; + +if (require.main === module) { + bootstrap(); +} diff --git a/core/mr/bootstrap.js b/core/mr/bootstrap.js new file mode 100644 index 0000000000..2ab5b61e0e --- /dev/null +++ b/core/mr/bootstrap.js @@ -0,0 +1,397 @@ +/*global module: false, define, callbackApplication */ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define('mr', [], factory); + } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require, exports, module); + } else { + // Browser globals (root is window) + root.Montage = factory({}, {}, {}); + } +}(this, function (require, exports, module) { + "use strict"; + + // reassigning causes eval to not use lexical scope. + var globalEval = eval, + global = globalEval('this'); + + // + // + // + + function upperCaseChar(_, c) { + return c.toUpperCase(); + } + + var paramsCache, + dataAttrPattern = /^data-(.*)$/, + boostrapPattern = /^(.*)bootstrap.js(?:[\?\.]|$)/i, + letterAfterDashPattern = /-([a-z])/g; + + function getParams() { + var i, j, + match, script, scripts, + mrLocation, attr, name; + + if (!paramsCache) { + paramsCache = {}; + // Find the + + +

Hello Mr

+ +

+ + diff --git a/core/mr/demo/index.js b/core/mr/demo/index.js new file mode 100644 index 0000000000..457ac4d605 --- /dev/null +++ b/core/mr/demo/index.js @@ -0,0 +1,3 @@ +var data = require("data"); + +document.getElementById("hello").textContent = data; diff --git a/core/mr/demo/package.json b/core/mr/demo/package.json new file mode 100644 index 0000000000..3937b7c7c8 --- /dev/null +++ b/core/mr/demo/package.json @@ -0,0 +1,6 @@ +{ + "name": "mr-demo", + "dependencies": { + "mr": "*" + } +} diff --git a/core/mr/docs/Config-API.md b/core/mr/docs/Config-API.md new file mode 100644 index 0000000000..dcfabe7a60 --- /dev/null +++ b/core/mr/docs/Config-API.md @@ -0,0 +1,80 @@ +Package `config` API +==================== + +`Require.loadPackage` accepts the following configuration options for +all packages in a fresh module system. + +- **makeLoader**: the module loader maker, which by default depends on + whether the loader is running on a browser or on Node. On the + browser, it is a stack of `Require.MappingsLoader`, + `Require.ExtensionsLoader`, `Require.PathsLoader`, + `Require.MemoizedLoader`, then either `Require.ScriptLoader` or + `Require.XhrLoader` depending on `config.define` for the config of + the particular package. +- **makeCompiler**: the compiler maker for each package, which by + default is a stack of the `Require.JsonCompiler`, + `Require.ShebangCompiler`, `Require.DependenciesCompiler`, and + `LintCompiler` middleware. +- **lint**: an optional event handler that accepts a `module` if its + `text` is invalid JavaScript. There is no default value. `lint` is + used by `Require.LintCompiler` middleware. +- **read**: an optional resource reader, a function that must accept a + fully qualified URL and return a promise for the content of that + resource as a string. The default reader depends on whether Montage + Require is running in a browser or on Node. + +Mr then adds shared state for all packages to the `config`. + +- **registry**: the location of each known package by name, for those + packages that have either designated their own name, or been named + by a dependent package in the `dependencies` or `mappings` + properties of their package description. +- **getPackage**: returns the `require` function for a package that + has already been loaded, or throws an error. +- **loadPackage**: returns a memoized promise for the description of a + package at a given location. +- **descriptions**: promises for each package description that is + loading or has been loaded, by location. +- **descriptionLocations**: an object mapping package locations to the + locations of their package descriptions if an alternate is injected + with `require.injectPackageDescriptionLocation`. + +Then, for each package, Mr creates a `config` that +prototypically inherits from the master `config` and expands on that +configuration with details synthesized from the content of the package +description, `package.json`. This is the config that gets passed to +`Require.makeRequire(config)`. + +- **location**: the package's location directory, including a trailing + slash. +- **name**: the name of this package, if it has one. +- **packageDescription**: the original package description, either + parsed from a `package.json` or injected by + `require.injectPackageDescription`. +- **define**: true if this package uses script injection to load + resources. +- **modules**: object mapping module descriptions by identifier +- **lib**: the root directory location where modules can be found, by + default the same as `location`. +- **paths**: a prioritized array of directories in which to search for + modules for this package, by default just the `lib` directory. It + is inadvisable to give this array multiple entries on the + client-side, and thus inadvisable for packages that might be used + both client and server side. Really, just don't use this. It is + used only by `PathsLoaders` middleware to convert module identifiers + to locations. +- **mappings**: object mapping module identifier prefixes to + dependencies. These dependencies are suitable for passing to + `require.loadPackage`. +- **packagesDirectory**: the location in which to look for unknown + packages by name, by default `node_modules` within this package. +- **exposedConfigs**: an array of `config` properties instructing + `makeRequire` to copy those properties from `config` to each + `require` function, by default `paths`, `mappings`, `location`, + `packageDescription`, `packages`, and `modules`. + +Within `Require.makeRequire(config)`, Mr uses `makeLoader` +and `makeConfig` with its own `config` to produce `config.load` and +`config.compile` properties. The `config.load` in particular is +distinct and used internally by `require.load`, which memoizes and +compiles modules. diff --git a/core/mr/docs/How-it-works.md b/core/mr/docs/How-it-works.md new file mode 100644 index 0000000000..00411d9dd4 --- /dev/null +++ b/core/mr/docs/How-it-works.md @@ -0,0 +1,103 @@ +How It Works +============ + +In broad strokes, Montage Require uses so-called "XML" HTTP requests to +fetch modules, then uses a regular expression to scan for `require` +calls within each JavaScript module, then executes the module with some +variation of `eval`. Then, with the Montage Optimizer, `mop`, Montage +Require can also serve as the runtime for loading modules with bundled +script-injection with no alteration to the source code of an +application. With script-injection, XHR and `eval` are not necessary, +so applications are suitable for production, cross-domain, and with +content security policies (CSP) that forbid `eval`. + +In slightly thinner strokes, Montage Require has an asynchronous phase +and a synchronous phase. In the asynchronous "loading" phase, Montage +Require fetches every module that it will need in the synchronous phase. +It then passes into the synchronous "execution" phase, where `require` +calls actually occur. The asynchronous portion includes +`require.async`, `require.load`, and `require.deepLoad`, which return +[Q][] promises. The synchronous phase employs `require` calls directly +to transitively instantiate modules on demand. The system must be +kicked off with `require.async` since no modules are loaded initially. + +[Q]: http://github.com/kriskowal/q + +Some alternatives to Montage Require use a full JavaScript parser to +cull the false positives you will occasionally see when using regular +expressions to scan for static `require` calls. This is a trade-off +between weight and accuracy. Montage Require does not block execution +when it is unable to load these false-positive modules, but instead +continues to the execution to "wait and see" whether the module can run +to completion without the module that failed to load. Also, Montage +Require can be configured to use an alternate dependency parser. + +Around this system, Montage Require supports packages. This entails +asynchronously loading and parsing `package.json` files, then +configuring and connecting the module systems of each package in the +"load" phase. Package dependencies are loaded on demand. + +Each package has an isolated module identifier name space. The +`package.json` dictates how that name space forwards to other packages +through the `dependencies` property, as well as internal aliases from +the package's `name`, `main`, and `redirects` properties. + +Additionally, Montage Require is very configurable and pluggable. +Montage itself vastly extends the capabilities of Montage Require so +that it can load HTML templates. Montage's internal configuration +includes middleware stacks for loading and compiling. The loader +middleware stack can be overridden with `config.makeLoader` or +`config.load`. The compiler middleware can be overridden with +`config.makeCompiler` or `config.compile`. The makers are called to +create loaders or compilers *per package*, each receiving the +configuration for their particular package. + +The signature of loader middleware is `makeLoader(config, nextLoader)` +which must return a function of the form `load(id, module)`. The +signature of compiler middleware if `makeCompiler(config, nextCompiler)` +which must return a function of the form `compile(module)`. + +As part of the bootstrapping process, configuration begins with a call +to `Require.loadPackage(dependency, config)` that returns a promise for +the `require` function of the package. + +`config` is an optional base configuration that can contain alternate +`makeLoader`, `makeCompiler`, and `parseDependencies` functions. +Montage Require then takes ownership of the `config` object and uses it +to store information shared by all packages like the registries of known +packages by name and location, and memoized promises for each package +while they load. + +`dependency` declares the location of the package, and can also inform +the module system of the consistent `hash` of the package. Dependency +can be a `location` string for short, but gets internally normalized to +an object with a `location` property. The `hash` is only necessary for +optimized packages since they use script-injection. The injected +scripts call `define` for each module, identifying the module by the +containing package `hash` and module `id`. + +The `require` function for any package has a similar `loadPackage` +function that can take a dependency argument. That dependency may have +a `name` instead of `location`. In that case, Montage Require infers +the location based on the known locations of packages with that name, or +assumes the package exists within the `node_modules` directory of the +dependent package. This is a relatively safe assumption if the +application was installed with NPM. + +Montage Require also supports a form of dependency injection. These +features were implemented because `bootstrap.js` (and in Montage proper, +`montage.js`) would need to load and instantiate certain resources +before being able to instantiate a module system. To avoid reloading +these already-instantiated resources, the bootstrapper would inject them +into the packages before handing control over to the application. + +`require.inject(id, exports)` adds the exports for a given module to a +package. + +`require.injectPackageDescription(location, description)` allows the +module system to read the content of a `package.json` for the package at +`location` without fetching the corresponding file. + +`require.injectPackageDescriptionLocation(location, +descriptionLocation)` instructs the module system to look in an +alternate location for the `package.json` for a particular package. diff --git a/core/mr/docs/Module-API.md b/core/mr/docs/Module-API.md new file mode 100644 index 0000000000..65672195cb --- /dev/null +++ b/core/mr/docs/Module-API.md @@ -0,0 +1,68 @@ +`module` API +============ + +The `module` object is available within each module and passed to loader and +compiler middleware for decoration. The `module` from other +modules can be obtained by calling `require.getModuleDescriptor(id)`. + +The module object has the following properties: + +- **id**: the identifier of the module within its containing package +- **exports**: the interface of the module, if it has been instantiated +- **location**: the URL from which the module is or will be loaded. Equivalent to `__filename` in Node. +- **directory**: the directory URL, including trailing slash, containing + the module. Equivalent to `__dirname` in Node. +- **display**: the location and id of a module separated by an + hash (#), for display purposes +- **require**: the package containing the module +- **text**: the text of the module, only available in development. After + optimization, a module is declared with its `factory` as a + JavaScript function and has no corresponding `text`. The `text` is + useful for compiler middleware. +- **factory**: a function that, when called with the arguments + `require`, `exports`, and `module`, either populates `exports`, + reassigns `module.exports`, or returns `exports` to instantiate the + module. +- **dependencies**: an array of module identifiers of modules that + must be loaded before calling the factory, produced by + `parseDependencies`. +- **extraDependencies**: an array of additional module identifiers for + modules that must be loaded before calling the factory that may be + specified through other means than `parseDependencies`. +- **dependees**: an object with a key for every module that declares + this module as a dependency, populated automatically by `deepLoad`. +- **redirect**: the identifier of a module that stands in for this + module, so `require` returns its exports instead. A redirect is an + implied dependency. Redirect cycles should be avoided. +- **mappingRedirect**: the identifier of a module in another package + that provides this module, so `require` returns its exports instead. +- **mappingRequire**: the `require` function of the package that + provides this module. +- **injected**: whether this module's exports were injected by + `require.inject(id, exports)`. + + +## returnable exports + +A module can return an exports object. + +**add.js** + +```javascript + +return function (a, b) { + return a + b; +} +``` + +**the-answer.js** + +```javascript +// the exports of add is the returned function +var add = require("add"); + +module.exports = add(29, 13); +``` + +Note: This would make the module incompatible with NodeJS, where the idiom +`module.exports =` prevails. diff --git a/core/mr/docs/Node-compatability.md b/core/mr/docs/Node-compatability.md new file mode 100644 index 0000000000..1d26f8d957 --- /dev/null +++ b/core/mr/docs/Node-compatability.md @@ -0,0 +1,43 @@ +Node and npm compatibility +========================== + +Montage fully supports CommonJS Modules and Packages. It also supports +some of the extensions from NodeJS and npm: + +- **module.exports**: Modules that do not have cyclic dependencies + (modules with dependencies that in turn ultimately depend their own + exports) can redefine their exports object by assigning to + `module.exports`. +- **dependencies**: If a package declares a package dependency using + NPM’s `dependencies` property, Montage looks for that package in + the package’s `node_modules` subdirectory. Mr also + supports the case where a package with the same name is already + loaded by a parent package. Unlike NPM, with Montage packages, you + can use mappings to find individual packages in alternate locations or + give them different local names. +- **devDependencies**: Development dependencies are treated the same as + `dependencies`, except in production mode where they are ignored. +- **JSON**: Resources with the `.json` extension can be loaded as JSON + formatted modules. + + +## Differences + +There are some differences with the Node.js module system you should be aware +of: + +- `dependencies` version predicates are ignored. + +//1/21/2020 the following is fixed, `__filename` and `__dirname` are now injected + - `__filename` and `__dirname` are not injected into module scope. Consider + using `module.location` and `module.directory` instead. + + +- Because Mr cannot know if a URL points to a file or a directory, when you + require a directory `index.js` is not sought. To make a package using an + `index.js` compatible with Montage Require, add a `redirects` block to + `package.json`. See the [package API](./Package-API.md) + +In addition to these differences Mr adds some additional properties to +[package.json](./Package-API.md), [module](./Module-API.md) and +[require](./Require-API.md). diff --git a/core/mr/docs/Package-API.md b/core/mr/docs/Package-API.md new file mode 100644 index 0000000000..baa4352c4d --- /dev/null +++ b/core/mr/docs/Package-API.md @@ -0,0 +1,52 @@ +package.json (package description) +================================== + +Mr configures each package based on the contents of `package.json`, the +package description, and the shared configuration. These properties are +meaningful to Mr: + +- **name**: the name of the package, which may be used to connect + common dependencies of the same name in subpackages. +- **dependencies**: an object mapping a string that represents both a + module identifier prefix and a package name, to an ignored version + predicate. +- **mappings**: an object that maps a module identifier prefix to a + dependency. The dependency may be a location string, or an object + with `location`, `name`, or `hash` properties. The location may be + inferred from dependencies of already discovered packages, or from + the location of the dependent package and the name. The `hash` is + generated by an optimizer and only used for loading modules with + script injection. + + ```json + "mappings": { + "q": { + "name": "q", + "location": "packages/q" + } + } + ``` + + will cause Mr to load the Q package from `./packages/q` instead of + `./node_modules/q`. + +- **overlay**: an object defining alternate configurations depending + on the platform. Keys correspond to engines and values are + alternate properties to overwrite on the package description. For + the browser, the `window`, `browser`, and `montage` engines are + applied. This property is likely to be deprecated and replaced by + an `if` block or other content-negotiation blocks in the future. +- **main**: the module identifier of the module that represents this + package when required in other packages by the mapping module + identier, or in this package by its own name. +- **production**: when set to `true` it puts the system into production mode. + Currently this only ignores any `devDependencies`. +- **redirects**: an object that maps module identifiers to an alternate module identifier. + + ```json + "redirects": { + "foo": "foo/index" + } + ``` + + will cause `require("foo")` to return the exports from the `foo/index` module. diff --git a/core/mr/docs/Require-API.md b/core/mr/docs/Require-API.md new file mode 100644 index 0000000000..972de42345 --- /dev/null +++ b/core/mr/docs/Require-API.md @@ -0,0 +1,71 @@ +`require` API +============= + +A `require` function stands for a package. Specialized `require` +functions exist within each module. Calling `require` from outside a +module will return the exports of the module with the given top-level +identifier. Calling `require` within a module will resolve the given +identifier relative to the current module and return the exports of the +corresponding module. `require` will throw an exception if a needed +module has not yet been loaded. + +- **async(id)**: returns a promise for the exports of the module with + the given identifier. +- **location**: the URL of the package, including the trailing slash + for the directory. +- **resolve(id)**: returns the top-level identifier for a module, + relative to the current module. +- **load(id)**: returns a memoized promise for the loading of the + corresponding module. +- **deepLoad(id)**: returns a memoized promise that the module and its + transitive dependencies have all loaded. +- **identify(id, require)**: a module may have a different identifier + in another package. This returns the identifier for a module in a + subpackage. +- **getModuleDescriptor(id)**: returns a memoized `module` object + describing the module in this package for the given identifier. If + one does not exist, it creates one with `id`, `display`, and + `require` properties to get things started. +- **loadPackage(dependency)**: returns a promise for a `require` + function representing the given package. The `dependency` may be by + `name`, `location`, or both. If by `name` without `location`, the + `location` is inferred from the registry of known packages, or from + the `node_modules` directory within this package. If by `name` and + `location`, the location is added to the registry of known package + names. +- **getPackage(dependency)**: returns the `require` function for an + already loaded package, or throws an error. +- **inject(id, exports)**: adds a module for a given identifier with + the given exports, and sets its `module.injected` to true. This + prevents the module system from attempting to load the module. +- **injectMapping(mapping, prefix)**: Adds a mapping-style dependency + to a package. The mapping object describes the dependent package in + the same fashion as the value from the `mappings` property of a + package.json. If the mapping does not provide a module name-space + prefix, you can provide one as the second argument. +- **injectDependency(name, version)**: Adds an NPM-style dependency to + a package. The name and version should be as in an NPM `dependency` + property in a `package.json`. The version is presently ignored but + may in the future detect incompatibilities with another package + installed with the same name. Mr will not support multiple versions + of the same package. +- **injectPackageDescription(location, description)**: informs the + module system of the parsed contents of the `package.json` for the + package at the given location. This may be a lie. This prevents + the module system from attempting to load the `package.json`. The + corresponding `package.json` need not actually exist. +- **injectPackageDescriptionLocation(location, descriptionLocation)**: + informs the module system of an alternate URL from which to download + the `package.json` for this package. +- **read(location)**: an exposed internal utility for reading the + contents of a resource at a given URL. Returns a promise for the + corresponding text. +- **config**: the configuration object for this package. The `config` + provided by the module system to each package prototypically + inherits from the `config` given to the initial + `Require.loadPackage` and contains additional properties obtained by + analyzing `package.json`. Many but not all of these properties have + the same name and shape as those in `package.json`. +- **packageDescription**: the original parsed contents of the + `package.json`, or that object delegated by + `injectPackageDescription`. diff --git a/core/mr/docs/Script-attributes.md b/core/mr/docs/Script-attributes.md new file mode 100644 index 0000000000..a629d603f0 --- /dev/null +++ b/core/mr/docs/Script-attributes.md @@ -0,0 +1,71 @@ +Script attributes +================= + +### data-module + +`data-module` instructs Mr to `require` the given module after it +has finished bootstrapping and the DOM content has loaded. + +```html + +``` + +will load `package.json` and then `index.js`. + +### data-auto-package + +`data-auto-package` indicates that there is no `package.json` for this +application, and instructs Mr to pretend that an empty one exists +in the same directory as the HTML document. + +```html + +``` + +will load just `index.js`. + +### data-package + +`data-package` indicates that there is a `package.json` and that it can be +found at the given location. The default location is the same directory as +the HTML file. + +```html + +``` + +will load `../package.json` and then `../index.js`, because the module id is +relative to the root of the package. + + +Optimizer script attributes +=========================== + +The Montage Optimizer can convert entire packages to production ready versions +without manual alteration. The optimizer rewrites HTML, particularly replacing +the bootstrapping script with a bundle. As such, the run-time supports some +additional options. + +These options are added automatically by Mop and should not be added or +modified manually. + +### data-bootstrap + +Indicates that this script element is the `bootstrap.js` script and denotes +the location of that script. + +This is normally inferred from being a script with a `bootstrap.js` file name, +but thw optimizer replaces the ` + + + + diff --git a/core/mr/test/run-karma.js b/core/mr/test/run-karma.js new file mode 100644 index 0000000000..76c94f9fb0 --- /dev/null +++ b/core/mr/test/run-karma.js @@ -0,0 +1,100 @@ +/* global global:true, __dirname, jasmineRequire */ + +/*jshint evil:true */ +// reassigning causes eval to not use lexical scope. +var globalEval = eval, + global = globalEval('this'); +/*jshint evil:false */ + +// Bootsrap Karma +if (global.__karma__) { + global.__karma__.loaded = function() { + console.log('karma loaded'); + }; + +// Bootstrap Browser fallback +} else { + + // Init + var jasmine = jasmineRequire.core(jasmineRequire); + var jasmineEnv = jasmine.getEnv(); + + // Export interface + var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); + global.jasmine = jasmine; + for (var property in jasmineInterface) { + if (jasmineInterface.hasOwnProperty(property)) { + global[property] = jasmineInterface[property]; + } + } + + // Default reporter + jasmineEnv.addReporter(jasmineInterface.jsApiReporter); + + // Html reporter + jasmineRequire.html(jasmine); + var htmlReporter = new jasmine.HtmlReporter({ + env: jasmineEnv, + getContainer: function() { return document.body; }, + createElement: function() { return document.createElement.apply(document, arguments); }, + createTextNode: function() { return document.createTextNode.apply(document, arguments); }, + timer: new jasmine.Timer() + }); + htmlReporter.initialize(); + + jasmineEnv.addReporter(htmlReporter); +} + +global.queryString = function queryString(parameter) { + var i, key, value, equalSign; + var loc = location.search.substring(1, location.search.length); + var params = loc.split('&'); + for (i = 0; i < params.length; i++) { + equalSign = params[i].indexOf('='); + if (equalSign < 0) { + key = params[i]; + if (key === parameter) { + value = true; + break; + } + } + else { + key = params[i].substring(0, equalSign); + if (key === parameter) { + value = decodeURIComponent(params[i].substring(equalSign+1)); + break; + } + } + } + return value; +}; + +function injectScript(src, module, callback) { + var script = document.createElement('script'); + script.async = true; + script.src = src; + script.setAttribute('data-module', module); + script.addEventListener('load', function () { + callback(null, module); + }); + script.addEventListener('error', function(err) { + callback(err, module); + }); + script.addEventListener('abort', function(err) { + callback(err, module); + }); + document.head.appendChild(script); +} + +function injectBase(href) { + var script = document.createElement('base'); + script.href = href; + document.head.appendChild(script); +} + +injectBase('/base/test/'); +injectScript('../bootstrap.js', 'all', function (err) { + if (err) { + throw err; + } +}); diff --git a/core/mr/test/run-node.js b/core/mr/test/run-node.js new file mode 100644 index 0000000000..ab1ccab53b --- /dev/null +++ b/core/mr/test/run-node.js @@ -0,0 +1,49 @@ +/*jshint node:true, browser:false */ +var jasmineRequire = require('jasmine-core/lib/jasmine-core/jasmine.js'); +var JasmineConsoleReporter = require('jasmine-console-reporter'); + +// Init +var jasmine = jasmineRequire.core(jasmineRequire); +var jasmineEnv = jasmine.getEnv(); + +// Export interface +var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); +global.jasmine = jasmine; +global.jasmineRequire = jasmineRequire; +for (var property in jasmineInterface) { + if (jasmineInterface.hasOwnProperty(property)) { + global[property] = jasmineInterface[property]; + } +} + +// Default reporter +jasmineEnv.addReporter(jasmineInterface.jsApiReporter); + +// Html reporter +var consoleReporter = new JasmineConsoleReporter({ + colors: 1, + cleanStack: 1, + verbosity: 4, + listStyle: 'indent', + activity: false +}); +jasmineEnv.addReporter(consoleReporter); + +// Exit code +var exitCode = 0; +jasmineEnv.addReporter({ + specDone: function(result) { + exitCode = exitCode || result.status === 'failed'; + } +}); + +// Execute +var mrRequire = require('../bootstrap-node'); +var PATH = require("path"); + +mrRequire.loadPackage(PATH.join(__dirname, ".")).then(function (mr) { + return mr.async("all"); +}).then(function () { + console.log('Done'); + process.exit(exitCode); +}).thenReturn(); \ No newline at end of file diff --git a/core/mr/test/run.html b/core/mr/test/run.html new file mode 100644 index 0000000000..9223e6be6c --- /dev/null +++ b/core/mr/test/run.html @@ -0,0 +1,14 @@ + + + + Montage-Require - Browser + + + + + + + + + + diff --git a/core/mr/test/spec/browser-alternative/node_modules/shimmy/browser.js b/core/mr/test/spec/browser-alternative/node_modules/shimmy/browser.js new file mode 100644 index 0000000000..a1e665cadf --- /dev/null +++ b/core/mr/test/spec/browser-alternative/node_modules/shimmy/browser.js @@ -0,0 +1,2 @@ +var test = require("browser-alternative-spec/test"); +test.assert(typeof window !== "undefined"); diff --git a/core/mr/test/spec/browser-alternative/node_modules/shimmy/index.js b/core/mr/test/spec/browser-alternative/node_modules/shimmy/index.js new file mode 100644 index 0000000000..19f905ae11 --- /dev/null +++ b/core/mr/test/spec/browser-alternative/node_modules/shimmy/index.js @@ -0,0 +1,2 @@ +var test = require("browser-alternative-spec/test"); +test.assert(typeof window === "undefined"); diff --git a/core/mr/test/spec/browser-alternative/node_modules/shimmy/package.json b/core/mr/test/spec/browser-alternative/node_modules/shimmy/package.json new file mode 100644 index 0000000000..da05d65b63 --- /dev/null +++ b/core/mr/test/spec/browser-alternative/node_modules/shimmy/package.json @@ -0,0 +1,7 @@ +{ + "main": "./index.js", + "browser": "./browser.js", + "dependencies": { + "browser-alternative-spec": "*" + } +} diff --git a/core/mr/test/spec/browser-alternative/package.json b/core/mr/test/spec/browser-alternative/package.json new file mode 100644 index 0000000000..20a46fd3c2 --- /dev/null +++ b/core/mr/test/spec/browser-alternative/package.json @@ -0,0 +1,6 @@ +{ + "name": "browser-alternative-spec", + "dependencies": { + "shimmy": "*" + } +} diff --git a/core/mr/test/spec/browser-alternative/program.js b/core/mr/test/spec/browser-alternative/program.js new file mode 100644 index 0000000000..b95b45c3d1 --- /dev/null +++ b/core/mr/test/spec/browser-alternative/program.js @@ -0,0 +1,3 @@ +var test = require("test"); +require("shimmy"); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/browser-alternatives/node_modules/shimmy/browser.js b/core/mr/test/spec/browser-alternatives/node_modules/shimmy/browser.js new file mode 100644 index 0000000000..d9571cf1c3 --- /dev/null +++ b/core/mr/test/spec/browser-alternatives/node_modules/shimmy/browser.js @@ -0,0 +1,2 @@ +var test = require("browser-alternatives-spec/test"); +test.assert(typeof window !== "undefined"); diff --git a/core/mr/test/spec/browser-alternatives/node_modules/shimmy/index.js b/core/mr/test/spec/browser-alternatives/node_modules/shimmy/index.js new file mode 100644 index 0000000000..35a9359071 --- /dev/null +++ b/core/mr/test/spec/browser-alternatives/node_modules/shimmy/index.js @@ -0,0 +1,2 @@ +var test = require("browser-alternatives-spec/test"); +test.assert(typeof window === "undefined"); diff --git a/core/mr/test/spec/browser-alternatives/node_modules/shimmy/package.json b/core/mr/test/spec/browser-alternatives/node_modules/shimmy/package.json new file mode 100644 index 0000000000..1f0bc16785 --- /dev/null +++ b/core/mr/test/spec/browser-alternatives/node_modules/shimmy/package.json @@ -0,0 +1,9 @@ +{ + "main": "./index.js", + "browser": { + "index": "./browser.js" + }, + "dependencies": { + "browser-alternatives-spec": "*" + } +} diff --git a/core/mr/test/spec/browser-alternatives/package.json b/core/mr/test/spec/browser-alternatives/package.json new file mode 100644 index 0000000000..d6df5b2676 --- /dev/null +++ b/core/mr/test/spec/browser-alternatives/package.json @@ -0,0 +1,6 @@ +{ + "name": "browser-alternatives-spec", + "dependencies": { + "shimmy": "*" + } +} diff --git a/core/mr/test/spec/browser-alternatives/program.js b/core/mr/test/spec/browser-alternatives/program.js new file mode 100644 index 0000000000..b95b45c3d1 --- /dev/null +++ b/core/mr/test/spec/browser-alternatives/program.js @@ -0,0 +1,3 @@ +var test = require("test"); +require("shimmy"); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/case-sensitive/a.js b/core/mr/test/spec/case-sensitive/a.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/mr/test/spec/case-sensitive/package.json b/core/mr/test/spec/case-sensitive/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/case-sensitive/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/case-sensitive/program.js b/core/mr/test/spec/case-sensitive/program.js new file mode 100644 index 0000000000..85e96dcf40 --- /dev/null +++ b/core/mr/test/spec/case-sensitive/program.js @@ -0,0 +1,8 @@ +var test = require("test"); +try { + require("a"); + require("A"); + test.assert(false, "should fail to require alternate spelling"); +} catch (error) { +} +test.print("DONE", "info"); diff --git a/core/mr/test/spec/comments/package.json b/core/mr/test/spec/comments/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/comments/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/comments/program.js b/core/mr/test/spec/comments/program.js new file mode 100644 index 0000000000..a65b29f24b --- /dev/null +++ b/core/mr/test/spec/comments/program.js @@ -0,0 +1,36 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ +// require("a"); + // require("a"); +/* require("b"); */ + /* require("b"); */ +var test = require('test'); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/cyclic/a.js b/core/mr/test/spec/cyclic/a.js new file mode 100644 index 0000000000..900aaa7d10 --- /dev/null +++ b/core/mr/test/spec/cyclic/a.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ +exports.a = function () { + return b; +}; +var b = require('b'); diff --git a/core/mr/test/spec/cyclic/b.js b/core/mr/test/spec/cyclic/b.js new file mode 100644 index 0000000000..18f025c631 --- /dev/null +++ b/core/mr/test/spec/cyclic/b.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ +var a = require('a'); +exports.b = function () { + return a; +}; diff --git a/core/mr/test/spec/cyclic/package.json b/core/mr/test/spec/cyclic/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/cyclic/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/cyclic/program.js b/core/mr/test/spec/cyclic/program.js new file mode 100644 index 0000000000..3abc36ccdc --- /dev/null +++ b/core/mr/test/spec/cyclic/program.js @@ -0,0 +1,40 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ +var test = require('test'); +var a = require('a'); +var b = require('b'); + +test.assert(a.a, 'a exists'); +test.assert(b.b, 'b exists') +test.assert(a.a().b === b.b, 'a gets b'); +test.assert(b.b().a === a.a, 'b gets a'); + +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/determinism/package.json b/core/mr/test/spec/determinism/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/determinism/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/determinism/program.js b/core/mr/test/spec/determinism/program.js new file mode 100644 index 0000000000..1059f1fdd6 --- /dev/null +++ b/core/mr/test/spec/determinism/program.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ +var test = require('test'); +var a = require('submodule/a'); +test.assert(a, "a is defined"); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/determinism/submodule/a.js b/core/mr/test/spec/determinism/submodule/a.js new file mode 100644 index 0000000000..1dc51d8a36 --- /dev/null +++ b/core/mr/test/spec/determinism/submodule/a.js @@ -0,0 +1,39 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ +var test = require('test'); +var pass = false; +var test = require('test'); +try { + require('a'); +} catch (exception) { + pass = true; +} +test.assert(pass, 'require does not fall back to relative modules when absolutes are not available.') diff --git a/core/mr/test/spec/determinism/submodule/b.js b/core/mr/test/spec/determinism/submodule/b.js new file mode 100644 index 0000000000..9d6c37a7f5 --- /dev/null +++ b/core/mr/test/spec/determinism/submodule/b.js @@ -0,0 +1,30 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ diff --git a/core/mr/test/spec/dev-dependencies/node_modules/dev-dependency/dev-dependency.js b/core/mr/test/spec/dev-dependencies/node_modules/dev-dependency/dev-dependency.js new file mode 100644 index 0000000000..4387befddd --- /dev/null +++ b/core/mr/test/spec/dev-dependencies/node_modules/dev-dependency/dev-dependency.js @@ -0,0 +1 @@ +module.exports = 10; diff --git a/core/mr/test/spec/dev-dependencies/node_modules/dev-dependency/package.json b/core/mr/test/spec/dev-dependencies/node_modules/dev-dependency/package.json new file mode 100644 index 0000000000..8ed565fbba --- /dev/null +++ b/core/mr/test/spec/dev-dependencies/node_modules/dev-dependency/package.json @@ -0,0 +1,4 @@ +{ + "name": "dev-dependency", + "main": "dev-dependency" +} diff --git a/core/mr/test/spec/dev-dependencies/package.json b/core/mr/test/spec/dev-dependencies/package.json new file mode 100644 index 0000000000..9751dbbadb --- /dev/null +++ b/core/mr/test/spec/dev-dependencies/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "dev-dependency": "*" + } +} diff --git a/core/mr/test/spec/dev-dependencies/program.js b/core/mr/test/spec/dev-dependencies/program.js new file mode 100644 index 0000000000..b192f34af8 --- /dev/null +++ b/core/mr/test/spec/dev-dependencies/program.js @@ -0,0 +1,4 @@ +var test = require("test"); +var ten = require("dev-dependency"); +test.assert(10 === ten, "can require module from devDependency"); +test.print("DONE", "info"); diff --git a/core/mr/test/spec/directory-index/bar.js b/core/mr/test/spec/directory-index/bar.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/mr/test/spec/directory-index/foo/index.js b/core/mr/test/spec/directory-index/foo/index.js new file mode 100644 index 0000000000..5aec4df51f --- /dev/null +++ b/core/mr/test/spec/directory-index/foo/index.js @@ -0,0 +1,4 @@ + +exports.program = function () { + return require('program'); +}; diff --git a/core/mr/test/spec/directory-index/package.json b/core/mr/test/spec/directory-index/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/directory-index/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/directory-index/program.js b/core/mr/test/spec/directory-index/program.js new file mode 100644 index 0000000000..2228389a07 --- /dev/null +++ b/core/mr/test/spec/directory-index/program.js @@ -0,0 +1,43 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require('test'); +var bar = require('bar'); + +// Should all resolve same module +var foo1 = require('foo'); +var foo2 = require('foo/index'); +var foo3 = require('foo/index.js'); + +test.assert(foo1.program() === exports, 'exact exports'); +test.assert(foo2.program() === exports, 'exact exports'); +test.assert(foo3.program() === exports, 'exact exports'); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/dot-js-module/node_modules/test.js/main.load.js b/core/mr/test/spec/dot-js-module/node_modules/test.js/main.load.js new file mode 100644 index 0000000000..94dafa9715 --- /dev/null +++ b/core/mr/test/spec/dot-js-module/node_modules/test.js/main.load.js @@ -0,0 +1,6 @@ +montageDefine("xxx", "main", { + dependencies: [], + factory: function(require, exports, module) { + module.exports = 10; +}}); + diff --git a/core/mr/test/spec/dot-js-module/node_modules/test.js/package.json b/core/mr/test/spec/dot-js-module/node_modules/test.js/package.json new file mode 100644 index 0000000000..0ff1aa69dd --- /dev/null +++ b/core/mr/test/spec/dot-js-module/node_modules/test.js/package.json @@ -0,0 +1,4 @@ +{ + "hash": "xxx", + "useScriptInjection": true +} diff --git a/core/mr/test/spec/dot-js-module/package.json b/core/mr/test/spec/dot-js-module/package.json new file mode 100644 index 0000000000..74477bd337 --- /dev/null +++ b/core/mr/test/spec/dot-js-module/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "test.js": "*" + } +} diff --git a/core/mr/test/spec/dot-js-module/program.js b/core/mr/test/spec/dot-js-module/program.js new file mode 100644 index 0000000000..05f16bdc16 --- /dev/null +++ b/core/mr/test/spec/dot-js-module/program.js @@ -0,0 +1,5 @@ +var test = require('test'); + +test.assert(require("test.js/main") === 10, "can require dependency with .js in, in script-injection mode"); + +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/exactExports/a.js b/core/mr/test/spec/exactExports/a.js new file mode 100644 index 0000000000..92befcceb7 --- /dev/null +++ b/core/mr/test/spec/exactExports/a.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +exports.program = function () { + return require('program'); +}; diff --git a/core/mr/test/spec/exactExports/package.json b/core/mr/test/spec/exactExports/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/exactExports/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/exactExports/program.js b/core/mr/test/spec/exactExports/program.js new file mode 100644 index 0000000000..1036daff52 --- /dev/null +++ b/core/mr/test/spec/exactExports/program.js @@ -0,0 +1,35 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require('test'); +var a = require('a'); +test.assert(a.program() === exports, 'exact exports'); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/extension-loader/a.extension/a.js b/core/mr/test/spec/extension-loader/a.extension/a.js new file mode 100644 index 0000000000..4387befddd --- /dev/null +++ b/core/mr/test/spec/extension-loader/a.extension/a.js @@ -0,0 +1 @@ +module.exports = 10; diff --git a/core/mr/test/spec/extension-loader/package.json b/core/mr/test/spec/extension-loader/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/extension-loader/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/extension-loader/program.js b/core/mr/test/spec/extension-loader/program.js new file mode 100644 index 0000000000..17bb04e8c8 --- /dev/null +++ b/core/mr/test/spec/extension-loader/program.js @@ -0,0 +1,28 @@ +var test = require('test'); + +var extensionRe = /([^\/]+)\.extension$/; +var ExtensionLoader = function (config, load) { + return function (id, module) { + var match = extensionRe.exec(id); + if (match) { + module.redirect = id + "/" + match[1]; + return module; + } else { + return load(id, module); + } + }; +}; + +var config = {}; +config.makeLoader = function (config) { + return ExtensionLoader(config, require.config.makeLoader(config)); +}; + +return require.loadPackage(module.directory, config) +.then(function (packageRequire) { + return packageRequire.async("a.extension"); +}).then(function (aExports) { + test.assert(aExports === 10, 'require with extension loader'); + test.print('DONE', 'info'); +}); + diff --git a/core/mr/test/spec/flat-module-tree/node_modules/http-server/index.js b/core/mr/test/spec/flat-module-tree/node_modules/http-server/index.js new file mode 100644 index 0000000000..a8c3762520 --- /dev/null +++ b/core/mr/test/spec/flat-module-tree/node_modules/http-server/index.js @@ -0,0 +1,2 @@ +require("path"); +require("url"); diff --git a/core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/path/index.js b/core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/path/index.js new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/path/index.js @@ -0,0 +1 @@ + diff --git a/core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/path/package.json b/core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/path/package.json new file mode 100644 index 0000000000..f58dd10d3d --- /dev/null +++ b/core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/path/package.json @@ -0,0 +1,4 @@ +{ + "name": "path", + "version": "1" +} diff --git a/core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/url/index.js b/core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/url/index.js new file mode 100644 index 0000000000..114cf36fc8 --- /dev/null +++ b/core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/url/index.js @@ -0,0 +1 @@ +require("path"); diff --git a/core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/url/package.json b/core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/url/package.json new file mode 100644 index 0000000000..7b746de31c --- /dev/null +++ b/core/mr/test/spec/flat-module-tree/node_modules/http-server/node_modules/url/package.json @@ -0,0 +1,7 @@ +{ + "name": "url", + "version": "1", + "dependencies": { + "path": "1" + } +} diff --git a/core/mr/test/spec/flat-module-tree/node_modules/http-server/package.json b/core/mr/test/spec/flat-module-tree/node_modules/http-server/package.json new file mode 100644 index 0000000000..dff60abbda --- /dev/null +++ b/core/mr/test/spec/flat-module-tree/node_modules/http-server/package.json @@ -0,0 +1,8 @@ +{ + "name": "http-server", + "version": "1", + "dependencies": { + "path": "1", + "url": "1" + } +} diff --git a/core/mr/test/spec/flat-module-tree/node_modules/path/index.js b/core/mr/test/spec/flat-module-tree/node_modules/path/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/mr/test/spec/flat-module-tree/node_modules/path/package.json b/core/mr/test/spec/flat-module-tree/node_modules/path/package.json new file mode 100644 index 0000000000..2c7e6cae71 --- /dev/null +++ b/core/mr/test/spec/flat-module-tree/node_modules/path/package.json @@ -0,0 +1,4 @@ +{ + "name": "path", + "version": "2" +} diff --git a/core/mr/test/spec/flat-module-tree/node_modules/url/index.js b/core/mr/test/spec/flat-module-tree/node_modules/url/index.js new file mode 100644 index 0000000000..114cf36fc8 --- /dev/null +++ b/core/mr/test/spec/flat-module-tree/node_modules/url/index.js @@ -0,0 +1 @@ +require("path"); diff --git a/core/mr/test/spec/flat-module-tree/node_modules/url/package.json b/core/mr/test/spec/flat-module-tree/node_modules/url/package.json new file mode 100644 index 0000000000..5f0280c58e --- /dev/null +++ b/core/mr/test/spec/flat-module-tree/node_modules/url/package.json @@ -0,0 +1,7 @@ +{ + "name": "url", + "version": "2", + "dependencies": { + "path": "2" + } +} diff --git a/core/mr/test/spec/flat-module-tree/package.json b/core/mr/test/spec/flat-module-tree/package.json new file mode 100644 index 0000000000..a20e4759b7 --- /dev/null +++ b/core/mr/test/spec/flat-module-tree/package.json @@ -0,0 +1,7 @@ +{ + "_args": [], + "dependencies": { + "http-server": "1", + "url": "2" + } +} diff --git a/core/mr/test/spec/flat-module-tree/program.js b/core/mr/test/spec/flat-module-tree/program.js new file mode 100644 index 0000000000..79e08830e7 --- /dev/null +++ b/core/mr/test/spec/flat-module-tree/program.js @@ -0,0 +1,44 @@ +/* + * Up to npm 3, npm always installed packages in the package's node_modules + * directory. Starting in npm 3, npm now tries to avoid duplication by floating + * dependencies-of-dependencies as far up the directory structure as possible + * without causing version conflicts. + * + * This means when a module requires a node_module, that dependency may have + * been installed to ./node_modules, ../node_modules, ../../node_modules, etc. + * There is no way to determine where a dependency has been installed (until + * npm 5's package-lock.json), as npm 3+ is non-deterministic and the location + * a dependency is installed to can change depending on install order. + * + * Imagine a simple web application project that runs an http server. The + * packages are: http-server, url, and path. The dependencies between packages + * are: + * + * flat-module-tree -> [http-server@1, url@2] flat-module-tree + * http-server@1 -> [path@1, url@1] / \ + * url@1 -> [path@1] http-server@1 url@2 + * url@2 -> [path@2] / \ | + * path@1 -> [] path@1 <- url@1 path@2 + * path@2 -> [] + * + * This test's directory structure is a possible result of running npm install: + * + * flat-module-tree + * | + * node_modules + * / | \ + * http-server@1 path@2 url@2 + * | + * node_modules + * / \ + * url@1 path@1 + * + * To understand how npm 3+ works and why it is non-deterministic, see + * https://npm.github.io/how-npm-works-docs/npm3/how-npm3-works.html + */ + +var test = require('test'); + +require("http-server"); +require("url"); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/hasOwnProperty/hasOwnProperty.js b/core/mr/test/spec/hasOwnProperty/hasOwnProperty.js new file mode 100644 index 0000000000..9d6c37a7f5 --- /dev/null +++ b/core/mr/test/spec/hasOwnProperty/hasOwnProperty.js @@ -0,0 +1,30 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ diff --git a/core/mr/test/spec/hasOwnProperty/package.json b/core/mr/test/spec/hasOwnProperty/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/hasOwnProperty/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/hasOwnProperty/program.js b/core/mr/test/spec/hasOwnProperty/program.js new file mode 100644 index 0000000000..92301b24f8 --- /dev/null +++ b/core/mr/test/spec/hasOwnProperty/program.js @@ -0,0 +1,35 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var hasOwnProperty = require('hasOwnProperty'); +var toString = require('toString'); +var test = require('test'); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/hasOwnProperty/toString.js b/core/mr/test/spec/hasOwnProperty/toString.js new file mode 100644 index 0000000000..9d6c37a7f5 --- /dev/null +++ b/core/mr/test/spec/hasOwnProperty/toString.js @@ -0,0 +1,30 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ diff --git a/core/mr/test/spec/identify/node_modules/cyclic/module.js b/core/mr/test/spec/identify/node_modules/cyclic/module.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/mr/test/spec/identify/node_modules/cyclic/package.json b/core/mr/test/spec/identify/node_modules/cyclic/package.json new file mode 100644 index 0000000000..67a029630f --- /dev/null +++ b/core/mr/test/spec/identify/node_modules/cyclic/package.json @@ -0,0 +1,6 @@ +{ + "name": "cyclic", + "dependencies": { + "identify": "*" + } +} diff --git a/core/mr/test/spec/identify/node_modules/x/package.json b/core/mr/test/spec/identify/node_modules/x/package.json new file mode 100644 index 0000000000..6507d73981 --- /dev/null +++ b/core/mr/test/spec/identify/node_modules/x/package.json @@ -0,0 +1,3 @@ +{ + "name": "x" +} diff --git a/core/mr/test/spec/identify/node_modules/x/x.js b/core/mr/test/spec/identify/node_modules/x/x.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/mr/test/spec/identify/package.json b/core/mr/test/spec/identify/package.json new file mode 100644 index 0000000000..4f497ea605 --- /dev/null +++ b/core/mr/test/spec/identify/package.json @@ -0,0 +1,9 @@ +{ + "name": "identify", + "dependencies": { + "cyclic": "*", + "a": "*", + "x": "*", + "y": "*" + } +} diff --git a/core/mr/test/spec/identify/program.js b/core/mr/test/spec/identify/program.js new file mode 100644 index 0000000000..0a2ab8aa87 --- /dev/null +++ b/core/mr/test/spec/identify/program.js @@ -0,0 +1,11 @@ +var test = require("test"); + +// ensure relevant packages are loaded +require("x/x"); +require("cyclic/module"); + +var xRequire = require.getPackage({name: "x"}); + +test.assert(require.identify('z', xRequire) === 'x/z'); + +test.print("DONE", "info"); diff --git a/core/mr/test/spec/inject-dependency/node_modules/dependency/module.js b/core/mr/test/spec/inject-dependency/node_modules/dependency/module.js new file mode 100644 index 0000000000..4387befddd --- /dev/null +++ b/core/mr/test/spec/inject-dependency/node_modules/dependency/module.js @@ -0,0 +1 @@ +module.exports = 10; diff --git a/core/mr/test/spec/inject-dependency/node_modules/dependency/package.json b/core/mr/test/spec/inject-dependency/node_modules/dependency/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/inject-dependency/node_modules/dependency/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/inject-dependency/package.json b/core/mr/test/spec/inject-dependency/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/inject-dependency/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/inject-dependency/program.js b/core/mr/test/spec/inject-dependency/program.js new file mode 100644 index 0000000000..25f613e7ae --- /dev/null +++ b/core/mr/test/spec/inject-dependency/program.js @@ -0,0 +1,7 @@ +var test = require("test"); +require.injectDependency("dependency"); +module.exports = require.async("dependency/module") +.then(function (value) { + test.assert(value === 10, "the injected dependency should export 10"); + test.print("DONE", "info"); +}); diff --git a/core/mr/test/spec/inject-into-mapping/package.json b/core/mr/test/spec/inject-into-mapping/package.json new file mode 100644 index 0000000000..ebaf00c83d --- /dev/null +++ b/core/mr/test/spec/inject-into-mapping/package.json @@ -0,0 +1,5 @@ +{ + "mappings": { + "mapping": "somedir/mapping" + } +} diff --git a/core/mr/test/spec/inject-into-mapping/program.js b/core/mr/test/spec/inject-into-mapping/program.js new file mode 100644 index 0000000000..577641a937 --- /dev/null +++ b/core/mr/test/spec/inject-into-mapping/program.js @@ -0,0 +1,7 @@ +var test = require("test"); +require.inject("mapping/dependency", { + foo: true +}) +var Dependency = require("mapping/module"); +test.assert(Dependency.foo === true, "the injected depency should export foo"); + diff --git a/core/mr/test/spec/inject-into-mapping/somedir/mapping/module.js b/core/mr/test/spec/inject-into-mapping/somedir/mapping/module.js new file mode 100644 index 0000000000..ac7fbb5cf4 --- /dev/null +++ b/core/mr/test/spec/inject-into-mapping/somedir/mapping/module.js @@ -0,0 +1 @@ +module.exports = require("dependency"); // Injected by program diff --git a/core/mr/test/spec/inject-into-mapping/somedir/mapping/package.json b/core/mr/test/spec/inject-into-mapping/somedir/mapping/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/inject-into-mapping/somedir/mapping/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/inject-mapping/mapping/module.js b/core/mr/test/spec/inject-mapping/mapping/module.js new file mode 100644 index 0000000000..4387befddd --- /dev/null +++ b/core/mr/test/spec/inject-mapping/mapping/module.js @@ -0,0 +1 @@ +module.exports = 10; diff --git a/core/mr/test/spec/inject-mapping/mapping/package.json b/core/mr/test/spec/inject-mapping/mapping/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/inject-mapping/mapping/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/inject-mapping/package.json b/core/mr/test/spec/inject-mapping/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/inject-mapping/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/inject-mapping/program.js b/core/mr/test/spec/inject-mapping/program.js new file mode 100644 index 0000000000..fd79b747fa --- /dev/null +++ b/core/mr/test/spec/inject-mapping/program.js @@ -0,0 +1,10 @@ +var test = require("test"); +require.injectMapping({ + "location": "mapping", + "name": "dependency" +}); +module.exports = require.async("dependency/module") +.then(function (value) { + test.assert(value === 10, "the injected dependency should export 10"); + test.print("DONE", "info"); +}); diff --git a/core/mr/test/spec/inject/package.json b/core/mr/test/spec/inject/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/inject/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/inject/program.js b/core/mr/test/spec/inject/program.js new file mode 100644 index 0000000000..79461dbe40 --- /dev/null +++ b/core/mr/test/spec/inject/program.js @@ -0,0 +1,6 @@ +var test = require("test"); +require.inject("dependency", { + foo: true +}); +var dependency = require("dependency"); +test.assert(dependency.foo === true, "the injected dependency should export true"); diff --git a/core/mr/test/spec/legacy-bundling/node_modules/nested/index.js b/core/mr/test/spec/legacy-bundling/node_modules/nested/index.js new file mode 100644 index 0000000000..ad0eeea26a --- /dev/null +++ b/core/mr/test/spec/legacy-bundling/node_modules/nested/index.js @@ -0,0 +1,3 @@ +var child = require('child'); + +exports.foo = child.foo; \ No newline at end of file diff --git a/core/mr/test/spec/legacy-bundling/node_modules/nested/node_modules/child/index.js b/core/mr/test/spec/legacy-bundling/node_modules/nested/node_modules/child/index.js new file mode 100644 index 0000000000..49ad1a9e12 --- /dev/null +++ b/core/mr/test/spec/legacy-bundling/node_modules/nested/node_modules/child/index.js @@ -0,0 +1,3 @@ +exports.foo = function () { + return 1; +}; \ No newline at end of file diff --git a/core/mr/test/spec/legacy-bundling/node_modules/nested/node_modules/child/package.json b/core/mr/test/spec/legacy-bundling/node_modules/nested/node_modules/child/package.json new file mode 100644 index 0000000000..c5a537829f --- /dev/null +++ b/core/mr/test/spec/legacy-bundling/node_modules/nested/node_modules/child/package.json @@ -0,0 +1,3 @@ +{ + "main": "index" +} \ No newline at end of file diff --git a/core/mr/test/spec/legacy-bundling/node_modules/nested/package.json b/core/mr/test/spec/legacy-bundling/node_modules/nested/package.json new file mode 100644 index 0000000000..e87fce4f76 --- /dev/null +++ b/core/mr/test/spec/legacy-bundling/node_modules/nested/package.json @@ -0,0 +1,6 @@ +{ + "main": "index", + "dependencies": { + "child":"*" + } +} \ No newline at end of file diff --git a/core/mr/test/spec/legacy-bundling/package.json b/core/mr/test/spec/legacy-bundling/package.json new file mode 100644 index 0000000000..76eeea62fe --- /dev/null +++ b/core/mr/test/spec/legacy-bundling/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "nested":"*" + } +} \ No newline at end of file diff --git a/core/mr/test/spec/legacy-bundling/program.js b/core/mr/test/spec/legacy-bundling/program.js new file mode 100644 index 0000000000..a91ec7210e --- /dev/null +++ b/core/mr/test/spec/legacy-bundling/program.js @@ -0,0 +1,4 @@ +var test = require('test'); +var nested = require('nested'); +test.assert(nested.foo() === 1, 'child module identifier'); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/load-package-digit/0/a.js b/core/mr/test/spec/load-package-digit/0/a.js new file mode 100644 index 0000000000..4387befddd --- /dev/null +++ b/core/mr/test/spec/load-package-digit/0/a.js @@ -0,0 +1 @@ +module.exports = 10; diff --git a/core/mr/test/spec/load-package-digit/0/package.json b/core/mr/test/spec/load-package-digit/0/package.json new file mode 100644 index 0000000000..eedad7e8b0 --- /dev/null +++ b/core/mr/test/spec/load-package-digit/0/package.json @@ -0,0 +1,4 @@ +{ + "name": "0", + "main": "a.js" +} diff --git a/core/mr/test/spec/load-package-digit/package.json b/core/mr/test/spec/load-package-digit/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/load-package-digit/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/load-package-digit/program.js b/core/mr/test/spec/load-package-digit/program.js new file mode 100644 index 0000000000..efb68e86dd --- /dev/null +++ b/core/mr/test/spec/load-package-digit/program.js @@ -0,0 +1,8 @@ +var test = require("test"); +module.exports = require.loadPackage("0") +.then(function (zero) { + return zero.async(""); +}) +.then(function () { + test.print("DONE", "info"); +}) diff --git a/core/mr/test/spec/load-package-name/node_modules/a/a.js b/core/mr/test/spec/load-package-name/node_modules/a/a.js new file mode 100644 index 0000000000..7ed4ee22a1 --- /dev/null +++ b/core/mr/test/spec/load-package-name/node_modules/a/a.js @@ -0,0 +1,32 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +module.exports = 10; diff --git a/core/mr/test/spec/load-package-name/node_modules/a/package.json b/core/mr/test/spec/load-package-name/node_modules/a/package.json new file mode 100644 index 0000000000..d888d3e1c4 --- /dev/null +++ b/core/mr/test/spec/load-package-name/node_modules/a/package.json @@ -0,0 +1,3 @@ +{ + "main": "a.js" +} diff --git a/core/mr/test/spec/load-package-name/package.json b/core/mr/test/spec/load-package-name/package.json new file mode 100644 index 0000000000..2c63c08510 --- /dev/null +++ b/core/mr/test/spec/load-package-name/package.json @@ -0,0 +1,2 @@ +{ +} diff --git a/core/mr/test/spec/load-package-name/program.js b/core/mr/test/spec/load-package-name/program.js new file mode 100644 index 0000000000..4d9b971e29 --- /dev/null +++ b/core/mr/test/spec/load-package-name/program.js @@ -0,0 +1,39 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require("test"); +module.exports = require.loadPackage({name: "a"}) +.then(function (a) { + return a.async(""); +}) +.then(function () { + test.print("DONE", "info"); +}) diff --git a/core/mr/test/spec/load-package/a/a.js b/core/mr/test/spec/load-package/a/a.js new file mode 100644 index 0000000000..7ed4ee22a1 --- /dev/null +++ b/core/mr/test/spec/load-package/a/a.js @@ -0,0 +1,32 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +module.exports = 10; diff --git a/core/mr/test/spec/load-package/a/package.json b/core/mr/test/spec/load-package/a/package.json new file mode 100644 index 0000000000..d888d3e1c4 --- /dev/null +++ b/core/mr/test/spec/load-package/a/package.json @@ -0,0 +1,3 @@ +{ + "main": "a.js" +} diff --git a/core/mr/test/spec/load-package/package.json b/core/mr/test/spec/load-package/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/load-package/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/load-package/program.js b/core/mr/test/spec/load-package/program.js new file mode 100644 index 0000000000..6ddadf56fa --- /dev/null +++ b/core/mr/test/spec/load-package/program.js @@ -0,0 +1,39 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require("test"); +module.exports = require.loadPackage("a") +.then(function (a) { + return a.async(""); +}) +.then(function () { + test.print("DONE", "info"); +}) diff --git a/core/mr/test/spec/main-name/node_modules/dependency/dependency.js b/core/mr/test/spec/main-name/node_modules/dependency/dependency.js new file mode 100644 index 0000000000..ec01c2c141 --- /dev/null +++ b/core/mr/test/spec/main-name/node_modules/dependency/dependency.js @@ -0,0 +1 @@ +module.exports = true; diff --git a/core/mr/test/spec/main-name/node_modules/dependency/package.json b/core/mr/test/spec/main-name/node_modules/dependency/package.json new file mode 100644 index 0000000000..58561ba1a9 --- /dev/null +++ b/core/mr/test/spec/main-name/node_modules/dependency/package.json @@ -0,0 +1,4 @@ +{ + "name": "dependency", + "main": "missing.js" +} diff --git a/core/mr/test/spec/main-name/package.json b/core/mr/test/spec/main-name/package.json new file mode 100644 index 0000000000..8306757f1f --- /dev/null +++ b/core/mr/test/spec/main-name/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "dependency": "*" + } +} diff --git a/core/mr/test/spec/main-name/program.js b/core/mr/test/spec/main-name/program.js new file mode 100644 index 0000000000..33feb18407 --- /dev/null +++ b/core/mr/test/spec/main-name/program.js @@ -0,0 +1,6 @@ +var test = require("test"); +var dep = require("dependency/dependency"); + +test.assert(dep === true, "can require module with name of package"); +test.print("DONE", "info"); + diff --git a/core/mr/test/spec/main/node_modules/dot-js-ext/a.something.js b/core/mr/test/spec/main/node_modules/dot-js-ext/a.something.js new file mode 100644 index 0000000000..f9678122e8 --- /dev/null +++ b/core/mr/test/spec/main/node_modules/dot-js-ext/a.something.js @@ -0,0 +1 @@ +module.exports = 40; diff --git a/core/mr/test/spec/main/node_modules/dot-js-ext/package.json b/core/mr/test/spec/main/node_modules/dot-js-ext/package.json new file mode 100644 index 0000000000..3f5138320e --- /dev/null +++ b/core/mr/test/spec/main/node_modules/dot-js-ext/package.json @@ -0,0 +1,3 @@ +{ + "main": "a.something.js" +} diff --git a/core/mr/test/spec/main/node_modules/dot-slash/a.js b/core/mr/test/spec/main/node_modules/dot-slash/a.js new file mode 100644 index 0000000000..4387befddd --- /dev/null +++ b/core/mr/test/spec/main/node_modules/dot-slash/a.js @@ -0,0 +1 @@ +module.exports = 10; diff --git a/core/mr/test/spec/main/node_modules/dot-slash/package.json b/core/mr/test/spec/main/node_modules/dot-slash/package.json new file mode 100644 index 0000000000..35e7b12f0c --- /dev/null +++ b/core/mr/test/spec/main/node_modules/dot-slash/package.json @@ -0,0 +1,3 @@ +{ + "main": "./a.js" +} diff --git a/core/mr/test/spec/main/node_modules/dot.js/a.js b/core/mr/test/spec/main/node_modules/dot.js/a.js new file mode 100644 index 0000000000..9320b557ea --- /dev/null +++ b/core/mr/test/spec/main/node_modules/dot.js/a.js @@ -0,0 +1 @@ +module.exports = 20; diff --git a/core/mr/test/spec/main/node_modules/dot.js/package.json b/core/mr/test/spec/main/node_modules/dot.js/package.json new file mode 100644 index 0000000000..d888d3e1c4 --- /dev/null +++ b/core/mr/test/spec/main/node_modules/dot.js/package.json @@ -0,0 +1,3 @@ +{ + "main": "a.js" +} diff --git a/core/mr/test/spec/main/node_modules/js-ext/a.js b/core/mr/test/spec/main/node_modules/js-ext/a.js new file mode 100644 index 0000000000..dc0f411ccd --- /dev/null +++ b/core/mr/test/spec/main/node_modules/js-ext/a.js @@ -0,0 +1 @@ +module.exports = 50; diff --git a/core/mr/test/spec/main/node_modules/js-ext/package.json b/core/mr/test/spec/main/node_modules/js-ext/package.json new file mode 100644 index 0000000000..d888d3e1c4 --- /dev/null +++ b/core/mr/test/spec/main/node_modules/js-ext/package.json @@ -0,0 +1,3 @@ +{ + "main": "a.js" +} diff --git a/core/mr/test/spec/main/node_modules/no-ext/a.js b/core/mr/test/spec/main/node_modules/no-ext/a.js new file mode 100644 index 0000000000..dc1b1836de --- /dev/null +++ b/core/mr/test/spec/main/node_modules/no-ext/a.js @@ -0,0 +1 @@ +module.exports = 30; diff --git a/core/mr/test/spec/main/node_modules/no-ext/package.json b/core/mr/test/spec/main/node_modules/no-ext/package.json new file mode 100644 index 0000000000..7b4272a64f --- /dev/null +++ b/core/mr/test/spec/main/node_modules/no-ext/package.json @@ -0,0 +1,3 @@ +{ + "main": "a" +} diff --git a/core/mr/test/spec/main/package.json b/core/mr/test/spec/main/package.json new file mode 100644 index 0000000000..da7b3aa9ce --- /dev/null +++ b/core/mr/test/spec/main/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "dot-slash": "*", + "js-ext": "*", + "no-ext": "*", + "dot-js-ext": "*", + "dot.js": "*" + } +} diff --git a/core/mr/test/spec/main/program.js b/core/mr/test/spec/main/program.js new file mode 100644 index 0000000000..f96b10004e --- /dev/null +++ b/core/mr/test/spec/main/program.js @@ -0,0 +1,12 @@ +var test = require('test'); + +test.assert(require('dot-slash') === 10, 'main with "./"'); +test.assert(require('js-ext') === 20, 'main with ".js" extension'); +test.assert(require('no-ext') === 30, 'main with no extension'); +test.assert(require('dot-js-ext') === 40, 'main with "." in module name and ".js" extension'); + +test.assert(require('js-ext') === require("js-ext/a"), 'can require "main" without extension'); + +test.assert(require('dot.js') === 50, 'package with .js in name and main with ".js" extension'); + +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/main/programa.js b/core/mr/test/spec/main/programa.js new file mode 100644 index 0000000000..50276753e3 --- /dev/null +++ b/core/mr/test/spec/main/programa.js @@ -0,0 +1,9 @@ + +require('dot-slash') === 10; +var a = require('js-ext'); +valid = a === 20; +console.log("a"); + +require('no-ext') === 30; +require('dot-js-ext') === 40 +require('js-ext') === require("js-ext/a"); diff --git a/core/mr/test/spec/method/a.js b/core/mr/test/spec/method/a.js new file mode 100644 index 0000000000..26b4eabb13 --- /dev/null +++ b/core/mr/test/spec/method/a.js @@ -0,0 +1,43 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +exports.foo = function () { + return this; +}; +exports.set = function (x) { + this.x = x; +}; +exports.get = function () { + return this.x; +}; +exports.getClosed = function () { + return exports.x; +}; diff --git a/core/mr/test/spec/method/package.json b/core/mr/test/spec/method/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/method/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/method/program.js b/core/mr/test/spec/method/program.js new file mode 100644 index 0000000000..f4dab2d556 --- /dev/null +++ b/core/mr/test/spec/method/program.js @@ -0,0 +1,39 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require('test'); +var a = require('a'); +var foo = a.foo; +test.assert(a.foo() == a, 'calling a module member'); +test.assert(foo() == (function (){return this})(), 'members not implicitly bound'); +a.set(10); +test.assert(a.get() == 10, 'get and set') +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/missing/package.json b/core/mr/test/spec/missing/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/missing/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/missing/program.js b/core/mr/test/spec/missing/program.js new file mode 100644 index 0000000000..aa55fa0592 --- /dev/null +++ b/core/mr/test/spec/missing/program.js @@ -0,0 +1,39 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require('test'); +try { + require('bogus'); + test.print('FAIL require throws error when module missing', 'fail'); +} catch (exception) { + test.print('PASS require throws error when module missing', 'pass'); +} +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/module-error/error.js b/core/mr/test/spec/module-error/error.js new file mode 100644 index 0000000000..fec10516e4 --- /dev/null +++ b/core/mr/test/spec/module-error/error.js @@ -0,0 +1 @@ +throw new Error("whoops"); diff --git a/core/mr/test/spec/module-error/package.json b/core/mr/test/spec/module-error/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/module-error/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/module-error/program.js b/core/mr/test/spec/module-error/program.js new file mode 100644 index 0000000000..df112c47bc --- /dev/null +++ b/core/mr/test/spec/module-error/program.js @@ -0,0 +1,17 @@ +var test = require('test'); + +function run() { + try { + require("./error"); + } catch (_error) { + if (_error.message === "whoops") { + return true; + } + } + return false; +} + +test.assert(run() === true, "First require fails"); +test.assert(run() === true, "Second require still fails"); + +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/module-exports/module-exports.js b/core/mr/test/spec/module-exports/module-exports.js new file mode 100644 index 0000000000..7ed4ee22a1 --- /dev/null +++ b/core/mr/test/spec/module-exports/module-exports.js @@ -0,0 +1,32 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +module.exports = 10; diff --git a/core/mr/test/spec/module-exports/package.json b/core/mr/test/spec/module-exports/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/module-exports/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/module-exports/program.js b/core/mr/test/spec/module-exports/program.js new file mode 100644 index 0000000000..ea079c8f39 --- /dev/null +++ b/core/mr/test/spec/module-exports/program.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require('test'); +test.assert(require('module-exports') === 10, 'replacing module exports should replace the module exports'); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/module-html/package.json b/core/mr/test/spec/module-html/package.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/core/mr/test/spec/module-html/package.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/core/mr/test/spec/module-html/program.js b/core/mr/test/spec/module-html/program.js new file mode 100644 index 0000000000..a391e9881e --- /dev/null +++ b/core/mr/test/spec/module-html/program.js @@ -0,0 +1,5 @@ +var test = require('test'); +var html = require("simple-template.html"); + +test.assert(html.content.indexOf('') !== -1, 'can require html'); +test.print("DONE", "info"); diff --git a/core/mr/test/spec/module-html/simple-template.html b/core/mr/test/spec/module-html/simple-template.html new file mode 100644 index 0000000000..fade0df5ea --- /dev/null +++ b/core/mr/test/spec/module-html/simple-template.html @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/core/mr/test/spec/module-main-default/node_modules/sub-module/index.js b/core/mr/test/spec/module-main-default/node_modules/sub-module/index.js new file mode 100644 index 0000000000..c0564fefa2 --- /dev/null +++ b/core/mr/test/spec/module-main-default/node_modules/sub-module/index.js @@ -0,0 +1 @@ +exports.Exported = 1; \ No newline at end of file diff --git a/core/mr/test/spec/module-main-default/node_modules/sub-module/package.json b/core/mr/test/spec/module-main-default/node_modules/sub-module/package.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/core/mr/test/spec/module-main-default/node_modules/sub-module/package.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/core/mr/test/spec/module-main-default/package.json b/core/mr/test/spec/module-main-default/package.json new file mode 100644 index 0000000000..70a0aaea4e --- /dev/null +++ b/core/mr/test/spec/module-main-default/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "sub-module": "*" + } +} \ No newline at end of file diff --git a/core/mr/test/spec/module-main-default/program.js b/core/mr/test/spec/module-main-default/program.js new file mode 100644 index 0000000000..13d2d27c23 --- /dev/null +++ b/core/mr/test/spec/module-main-default/program.js @@ -0,0 +1,4 @@ +var test = require('test'); +var Exported = require('sub-module').Exported; + +test.assert(typeof Exported, 1); diff --git a/core/mr/test/spec/module-metadata/node_modules/a/a.js b/core/mr/test/spec/module-metadata/node_modules/a/a.js new file mode 100644 index 0000000000..02fd16ce50 --- /dev/null +++ b/core/mr/test/spec/module-metadata/node_modules/a/a.js @@ -0,0 +1 @@ +exports.Exported = {}; \ No newline at end of file diff --git a/core/mr/test/spec/module-metadata/node_modules/a/package.json b/core/mr/test/spec/module-metadata/node_modules/a/package.json new file mode 100644 index 0000000000..2b3cbf01bc --- /dev/null +++ b/core/mr/test/spec/module-metadata/node_modules/a/package.json @@ -0,0 +1,3 @@ +{ + "main": "a" +} diff --git a/core/mr/test/spec/module-metadata/package.json b/core/mr/test/spec/module-metadata/package.json new file mode 100644 index 0000000000..8fde6cdc89 --- /dev/null +++ b/core/mr/test/spec/module-metadata/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "a": "*" + } +} \ No newline at end of file diff --git a/core/mr/test/spec/module-metadata/program.js b/core/mr/test/spec/module-metadata/program.js new file mode 100644 index 0000000000..a97e20a7c4 --- /dev/null +++ b/core/mr/test/spec/module-metadata/program.js @@ -0,0 +1,8 @@ +var test = require('test'); +var Exported = require('a').Exported; + +test.assert(typeof Exported._montage_metadata === 'object', 'import metadata'); +test.assert(typeof Exported._montage_metadata.require === 'function', 'import metadata'); +test.assert(Exported._montage_metadata.module === 'a', 'import metadata'); +test.assert(Exported._montage_metadata.property === 'Exported', 'import metadata'); +test.print('DONE', 'info'); \ No newline at end of file diff --git a/core/mr/test/spec/module-reel/package.json b/core/mr/test/spec/module-reel/package.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/core/mr/test/spec/module-reel/package.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/core/mr/test/spec/module-reel/program.js b/core/mr/test/spec/module-reel/program.js new file mode 100644 index 0000000000..26d92ab498 --- /dev/null +++ b/core/mr/test/spec/module-reel/program.js @@ -0,0 +1,5 @@ +var test = require('test'); +var reel = require("test.reel"); + +test.assert(reel.Hello === "World", 'import string'); +test.print('DONE', 'info'); \ No newline at end of file diff --git a/core/mr/test/spec/module-reel/test.reel/test.js b/core/mr/test/spec/module-reel/test.reel/test.js new file mode 100644 index 0000000000..2d97c12298 --- /dev/null +++ b/core/mr/test/spec/module-reel/test.reel/test.js @@ -0,0 +1 @@ +exports.Hello = "World"; \ No newline at end of file diff --git a/core/mr/test/spec/moduleTypes/a/b.js b/core/mr/test/spec/moduleTypes/a/b.js new file mode 100644 index 0000000000..d9fee6c35e --- /dev/null +++ b/core/mr/test/spec/moduleTypes/a/b.js @@ -0,0 +1 @@ +module.exports = "pass" diff --git a/core/mr/test/spec/moduleTypes/a/c.json b/core/mr/test/spec/moduleTypes/a/c.json new file mode 100644 index 0000000000..3f10abcb4b --- /dev/null +++ b/core/mr/test/spec/moduleTypes/a/c.json @@ -0,0 +1,3 @@ +{ + "pass": true +} diff --git a/core/mr/test/spec/moduleTypes/a/five.plus-one b/core/mr/test/spec/moduleTypes/a/five.plus-one new file mode 100644 index 0000000000..7ed6ff82de --- /dev/null +++ b/core/mr/test/spec/moduleTypes/a/five.plus-one @@ -0,0 +1 @@ +5 diff --git a/core/mr/test/spec/moduleTypes/a/package.json b/core/mr/test/spec/moduleTypes/a/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/moduleTypes/a/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/moduleTypes/package.json b/core/mr/test/spec/moduleTypes/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/moduleTypes/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/moduleTypes/program.js b/core/mr/test/spec/moduleTypes/program.js new file mode 100644 index 0000000000..3c2915618a --- /dev/null +++ b/core/mr/test/spec/moduleTypes/program.js @@ -0,0 +1,35 @@ +var test = require('test'); + +var config = { + moduleTypes: ["plus-one"], + makeCompiler: function (config) { + var compile = require.config.makeCompiler(config); + return function (module) { + var isPlusOne = (module.location || "").match(/\.plus-one$/); + if (isPlusOne) { + module.exports = parseInt(module.text, 10) + 1; + return Promise.resolve(module); + } else { + return compile(module); + } + }; + } +}; + +return require.loadPackage(module.directory + "a", config) + .then(function (packageRequire) { + return packageRequire.async("five.plus-one") + .then(function (six) { + test.assert(six === 6, 'can require .plus-one modules'); + return packageRequire.async("b"); + }) + .then(function (b) { + test.assert(b === "pass", 'can require javascript module'); + return packageRequire.async("c.json"); + }) + .then(function (json) { + test.assert(json.pass === true, 'can require json module'); + test.print('DONE', 'info'); + + }); +}); diff --git a/core/mr/test/spec/monkeys/a.js b/core/mr/test/spec/monkeys/a.js new file mode 100644 index 0000000000..07580ded4c --- /dev/null +++ b/core/mr/test/spec/monkeys/a.js @@ -0,0 +1,32 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +require('program').monkey = 10; diff --git a/core/mr/test/spec/monkeys/package.json b/core/mr/test/spec/monkeys/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/monkeys/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/monkeys/program.js b/core/mr/test/spec/monkeys/program.js new file mode 100644 index 0000000000..9902103e3c --- /dev/null +++ b/core/mr/test/spec/monkeys/program.js @@ -0,0 +1,35 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var a = require('a'); +var test = require('test'); +test.assert(exports.monkey == 10, 'monkeys permitted'); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/named-mappings/node_modules/bar/package.json b/core/mr/test/spec/named-mappings/node_modules/bar/package.json new file mode 100644 index 0000000000..1787775b47 --- /dev/null +++ b/core/mr/test/spec/named-mappings/node_modules/bar/package.json @@ -0,0 +1,6 @@ +{ + "version": "0.0.0", + "dependencies": { + "foo": "*" + } +} diff --git a/core/mr/test/spec/named-mappings/node_modules/foo/foo.js b/core/mr/test/spec/named-mappings/node_modules/foo/foo.js new file mode 100644 index 0000000000..f1886d8fc3 --- /dev/null +++ b/core/mr/test/spec/named-mappings/node_modules/foo/foo.js @@ -0,0 +1,32 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +exports.foo = 10; diff --git a/core/mr/test/spec/named-mappings/node_modules/foo/package.json b/core/mr/test/spec/named-mappings/node_modules/foo/package.json new file mode 100644 index 0000000000..d33e38387d --- /dev/null +++ b/core/mr/test/spec/named-mappings/node_modules/foo/package.json @@ -0,0 +1,3 @@ +{ + "main": "foo.js" +} diff --git a/core/mr/test/spec/named-mappings/package.json b/core/mr/test/spec/named-mappings/package.json new file mode 100644 index 0000000000..a3e464917c --- /dev/null +++ b/core/mr/test/spec/named-mappings/package.json @@ -0,0 +1,6 @@ +{ + "mappings": { + "bar": {"name": "bar"}, + "foo": {"name": "foo"} + } +} diff --git a/core/mr/test/spec/named-mappings/program.js b/core/mr/test/spec/named-mappings/program.js new file mode 100644 index 0000000000..aba51fccaf --- /dev/null +++ b/core/mr/test/spec/named-mappings/program.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require("test"); +test.assert(require("bar/foo").foo === 10, 'can require through shared dependency'); +test.print("DONE", "info"); diff --git a/core/mr/test/spec/named-packages/node_modules/bar/package.json b/core/mr/test/spec/named-packages/node_modules/bar/package.json new file mode 100644 index 0000000000..9c4e95ac44 --- /dev/null +++ b/core/mr/test/spec/named-packages/node_modules/bar/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "foo": "*" + } +} diff --git a/core/mr/test/spec/named-packages/node_modules/foo/foo.js b/core/mr/test/spec/named-packages/node_modules/foo/foo.js new file mode 100644 index 0000000000..f1886d8fc3 --- /dev/null +++ b/core/mr/test/spec/named-packages/node_modules/foo/foo.js @@ -0,0 +1,32 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +exports.foo = 10; diff --git a/core/mr/test/spec/named-packages/node_modules/foo/package.json b/core/mr/test/spec/named-packages/node_modules/foo/package.json new file mode 100644 index 0000000000..d33e38387d --- /dev/null +++ b/core/mr/test/spec/named-packages/node_modules/foo/package.json @@ -0,0 +1,3 @@ +{ + "main": "foo.js" +} diff --git a/core/mr/test/spec/named-packages/package.json b/core/mr/test/spec/named-packages/package.json new file mode 100644 index 0000000000..3c66534074 --- /dev/null +++ b/core/mr/test/spec/named-packages/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "foo": "*", + "bar": "*" + } +} diff --git a/core/mr/test/spec/named-packages/program.js b/core/mr/test/spec/named-packages/program.js new file mode 100644 index 0000000000..aba51fccaf --- /dev/null +++ b/core/mr/test/spec/named-packages/program.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require("test"); +test.assert(require("bar/foo").foo === 10, 'can require through shared dependency'); +test.print("DONE", "info"); diff --git a/core/mr/test/spec/named-parent-package/node_modules/child-package/child-module.js b/core/mr/test/spec/named-parent-package/node_modules/child-package/child-module.js new file mode 100644 index 0000000000..1a48af9eb4 --- /dev/null +++ b/core/mr/test/spec/named-parent-package/node_modules/child-package/child-module.js @@ -0,0 +1,32 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +module.exports = require("named-parent-package/parent-module"); diff --git a/core/mr/test/spec/named-parent-package/node_modules/child-package/package.json b/core/mr/test/spec/named-parent-package/node_modules/child-package/package.json new file mode 100644 index 0000000000..d1501de4a2 --- /dev/null +++ b/core/mr/test/spec/named-parent-package/node_modules/child-package/package.json @@ -0,0 +1,8 @@ +{ + "name": "child-package", + "version": "0.0.0", + "main": "child-module", + "dependencies": { + "named-parent-package": "0.0.0" + } +} diff --git a/core/mr/test/spec/named-parent-package/package.json b/core/mr/test/spec/named-parent-package/package.json new file mode 100644 index 0000000000..0dc1664af7 --- /dev/null +++ b/core/mr/test/spec/named-parent-package/package.json @@ -0,0 +1,8 @@ +{ + "name": "named-parent-package", + "version": "0.0.0", + "main": "program", + "dependencies": { + "child-package": "0.0.0" + } +} diff --git a/core/mr/test/spec/named-parent-package/parent-module.js b/core/mr/test/spec/named-parent-package/parent-module.js new file mode 100644 index 0000000000..7ed4ee22a1 --- /dev/null +++ b/core/mr/test/spec/named-parent-package/parent-module.js @@ -0,0 +1,32 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +module.exports = 10; diff --git a/core/mr/test/spec/named-parent-package/program.js b/core/mr/test/spec/named-parent-package/program.js new file mode 100644 index 0000000000..923cee2815 --- /dev/null +++ b/core/mr/test/spec/named-parent-package/program.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require("test"); +test.assert(require("child-package") === require("parent-module"), 'child package requires module from named parent package'); +test.print("DONE", "info"); diff --git a/core/mr/test/spec/nested/a/b/c/d.js b/core/mr/test/spec/nested/a/b/c/d.js new file mode 100644 index 0000000000..7cdd28c025 --- /dev/null +++ b/core/mr/test/spec/nested/a/b/c/d.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +exports.foo = function () { + return 1; +}; diff --git a/core/mr/test/spec/nested/package.json b/core/mr/test/spec/nested/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/nested/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/nested/program.js b/core/mr/test/spec/nested/program.js new file mode 100644 index 0000000000..db99f7b4ff --- /dev/null +++ b/core/mr/test/spec/nested/program.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require('test'); +test.assert(require('a/b/c/d').foo() == 1, 'nested module identifier'); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/not-found/package.json b/core/mr/test/spec/not-found/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/not-found/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/not-found/program.js b/core/mr/test/spec/not-found/program.js new file mode 100644 index 0000000000..b7f0940f73 --- /dev/null +++ b/core/mr/test/spec/not-found/program.js @@ -0,0 +1,40 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require('test'); +test.print("Can't XHR a.js expected", "info"); +try { + require("a"); +} catch (exception) { + test.print(exception.message); + test.assert(/Can't require module "a" via "program" because Can't XHR /.test(exception.message)); +} +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/overlay/package.json b/core/mr/test/spec/overlay/package.json new file mode 100644 index 0000000000..f09d46656a --- /dev/null +++ b/core/mr/test/spec/overlay/package.json @@ -0,0 +1,7 @@ +{ + "overlay": { + "abc": { + "pass": 10 + } + } +} diff --git a/core/mr/test/spec/overlay/program.js b/core/mr/test/spec/overlay/program.js new file mode 100644 index 0000000000..4b989d8d36 --- /dev/null +++ b/core/mr/test/spec/overlay/program.js @@ -0,0 +1,11 @@ +var test = require("test"); + +var config = { + overlays: ["abc"] +}; + +return require.loadPackage(module.directory, config) +.then(function (packageRequire) { + test.assert(packageRequire.packageDescription.pass === 10, "overlay applied") + test.print("DONE", "info"); +}); diff --git a/core/mr/test/spec/package-lock/node_modules/http-server/index.js b/core/mr/test/spec/package-lock/node_modules/http-server/index.js new file mode 100644 index 0000000000..a8c3762520 --- /dev/null +++ b/core/mr/test/spec/package-lock/node_modules/http-server/index.js @@ -0,0 +1,2 @@ +require("path"); +require("url"); diff --git a/core/mr/test/spec/package-lock/node_modules/http-server/node_modules/path/index.js b/core/mr/test/spec/package-lock/node_modules/http-server/node_modules/path/index.js new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/core/mr/test/spec/package-lock/node_modules/http-server/node_modules/path/index.js @@ -0,0 +1 @@ + diff --git a/core/mr/test/spec/package-lock/node_modules/http-server/node_modules/path/package.json b/core/mr/test/spec/package-lock/node_modules/http-server/node_modules/path/package.json new file mode 100644 index 0000000000..f58dd10d3d --- /dev/null +++ b/core/mr/test/spec/package-lock/node_modules/http-server/node_modules/path/package.json @@ -0,0 +1,4 @@ +{ + "name": "path", + "version": "1" +} diff --git a/core/mr/test/spec/package-lock/node_modules/http-server/node_modules/url/index.js b/core/mr/test/spec/package-lock/node_modules/http-server/node_modules/url/index.js new file mode 100644 index 0000000000..114cf36fc8 --- /dev/null +++ b/core/mr/test/spec/package-lock/node_modules/http-server/node_modules/url/index.js @@ -0,0 +1 @@ +require("path"); diff --git a/core/mr/test/spec/package-lock/node_modules/http-server/node_modules/url/package.json b/core/mr/test/spec/package-lock/node_modules/http-server/node_modules/url/package.json new file mode 100644 index 0000000000..7b746de31c --- /dev/null +++ b/core/mr/test/spec/package-lock/node_modules/http-server/node_modules/url/package.json @@ -0,0 +1,7 @@ +{ + "name": "url", + "version": "1", + "dependencies": { + "path": "1" + } +} diff --git a/core/mr/test/spec/package-lock/node_modules/http-server/package.json b/core/mr/test/spec/package-lock/node_modules/http-server/package.json new file mode 100644 index 0000000000..dff60abbda --- /dev/null +++ b/core/mr/test/spec/package-lock/node_modules/http-server/package.json @@ -0,0 +1,8 @@ +{ + "name": "http-server", + "version": "1", + "dependencies": { + "path": "1", + "url": "1" + } +} diff --git a/core/mr/test/spec/package-lock/node_modules/url/index.js b/core/mr/test/spec/package-lock/node_modules/url/index.js new file mode 100644 index 0000000000..114cf36fc8 --- /dev/null +++ b/core/mr/test/spec/package-lock/node_modules/url/index.js @@ -0,0 +1 @@ +require("path"); diff --git a/core/mr/test/spec/package-lock/node_modules/url/node_modules/path/index.js b/core/mr/test/spec/package-lock/node_modules/url/node_modules/path/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/mr/test/spec/package-lock/node_modules/url/node_modules/path/package.json b/core/mr/test/spec/package-lock/node_modules/url/node_modules/path/package.json new file mode 100644 index 0000000000..2c7e6cae71 --- /dev/null +++ b/core/mr/test/spec/package-lock/node_modules/url/node_modules/path/package.json @@ -0,0 +1,4 @@ +{ + "name": "path", + "version": "2" +} diff --git a/core/mr/test/spec/package-lock/node_modules/url/package.json b/core/mr/test/spec/package-lock/node_modules/url/package.json new file mode 100644 index 0000000000..5f0280c58e --- /dev/null +++ b/core/mr/test/spec/package-lock/node_modules/url/package.json @@ -0,0 +1,7 @@ +{ + "name": "url", + "version": "2", + "dependencies": { + "path": "2" + } +} diff --git a/core/mr/test/spec/package-lock/package-lock.json b/core/mr/test/spec/package-lock/package-lock.json new file mode 100644 index 0000000000..9d7c1c42b0 --- /dev/null +++ b/core/mr/test/spec/package-lock/package-lock.json @@ -0,0 +1,36 @@ +{ + "name": "package-lock", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "http-server": { + "version": "1", + "requires": { + "path": "1", + "url": "1" + }, + "dependencies": { + "path": { + "version": "1" + }, + "url": { + "version": "1", + "requires": { + "path": "1" + } + } + } + }, + "url": { + "version": "2", + "requires": { + "path": "2" + }, + "dependencies": { + "path": { + "version": "2" + } + } + } + } +} diff --git a/core/mr/test/spec/package-lock/package.json b/core/mr/test/spec/package-lock/package.json new file mode 100644 index 0000000000..a20e4759b7 --- /dev/null +++ b/core/mr/test/spec/package-lock/package.json @@ -0,0 +1,7 @@ +{ + "_args": [], + "dependencies": { + "http-server": "1", + "url": "2" + } +} diff --git a/core/mr/test/spec/package-lock/program.js b/core/mr/test/spec/package-lock/program.js new file mode 100644 index 0000000000..bc0a7f91e0 --- /dev/null +++ b/core/mr/test/spec/package-lock/program.js @@ -0,0 +1,11 @@ +/* + * Note that the node_modules structure is not an expected result from + * running npm install, but the package should still load using the + * package-lock.json. + */ + +var test = require('test'); + +require("http-server"); +require("url"); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/production/node_modules/dev-dependency/dev-dependency.js b/core/mr/test/spec/production/node_modules/dev-dependency/dev-dependency.js new file mode 100644 index 0000000000..4387befddd --- /dev/null +++ b/core/mr/test/spec/production/node_modules/dev-dependency/dev-dependency.js @@ -0,0 +1 @@ +module.exports = 10; diff --git a/core/mr/test/spec/production/node_modules/dev-dependency/package.json b/core/mr/test/spec/production/node_modules/dev-dependency/package.json new file mode 100644 index 0000000000..8ed565fbba --- /dev/null +++ b/core/mr/test/spec/production/node_modules/dev-dependency/package.json @@ -0,0 +1,4 @@ +{ + "name": "dev-dependency", + "main": "dev-dependency" +} diff --git a/core/mr/test/spec/production/package.json b/core/mr/test/spec/production/package.json new file mode 100644 index 0000000000..d70ffb9c7a --- /dev/null +++ b/core/mr/test/spec/production/package.json @@ -0,0 +1,6 @@ +{ + "production": true, + "devDependencies": { + "dev-dependency": "*" + } +} diff --git a/core/mr/test/spec/production/program.js b/core/mr/test/spec/production/program.js new file mode 100644 index 0000000000..023b71ac9f --- /dev/null +++ b/core/mr/test/spec/production/program.js @@ -0,0 +1,14 @@ +var test = require("test"); +module.exports = require.async("dev-dependency") +.then(function (a) { + throw "should not be able to require dev-dependency in production mode"; +}, function (error) { + console.log(error.message); + test.assert( + /Can\'t require module "dev-dependency" via "program"/.test(error.message), + "cannot require dev-dependency in production mode" + ); +}) +.then(function () { + test.print("DONE", "info"); +}); diff --git a/core/mr/test/spec/read/package.json b/core/mr/test/spec/read/package.json new file mode 100644 index 0000000000..040e411387 --- /dev/null +++ b/core/mr/test/spec/read/package.json @@ -0,0 +1,5 @@ +{ + "mappings": { + "bluebird": "../../../node_modules/bluebird" + } +} diff --git a/core/mr/test/spec/read/program.js b/core/mr/test/spec/read/program.js new file mode 100644 index 0000000000..74d13b7f94 --- /dev/null +++ b/core/mr/test/spec/read/program.js @@ -0,0 +1,24 @@ +var test = require("test"); +var Promise = require("bluebird"); + +function read(location) { + if (location === "http://test/package.json") { + return Promise.resolve(JSON.stringify({name: "pass"})); + } else if (location === "http://test/module.js") { + return Promise.resolve("module.exports = 5"); + } else { + return Promise.reject(new Error(location + " not here")); + } +} + +module.exports = require.loadPackage({location: "http://test/"}, { read: read }) +.then(function (pkg) { + test.assert(pkg.config.name === "pass"); + + return pkg.async("module"); +}) +.then(function (exports) { + test.assert(exports === 5); + + test.print("DONE", "info"); +}); diff --git a/core/mr/test/spec/redirects-package/node_modules/foo/barz.js b/core/mr/test/spec/redirects-package/node_modules/foo/barz.js new file mode 100644 index 0000000000..7aabcc17e4 --- /dev/null +++ b/core/mr/test/spec/redirects-package/node_modules/foo/barz.js @@ -0,0 +1,3 @@ +exports.foo = function() { + return 1; +} \ No newline at end of file diff --git a/core/mr/test/spec/redirects-package/node_modules/foo/package.json b/core/mr/test/spec/redirects-package/node_modules/foo/package.json new file mode 100644 index 0000000000..f8dbab7bc9 --- /dev/null +++ b/core/mr/test/spec/redirects-package/node_modules/foo/package.json @@ -0,0 +1,6 @@ +{ + "name": "foo", + "redirects": { + "bar": "barz" + } +} diff --git a/core/mr/test/spec/redirects-package/package.json b/core/mr/test/spec/redirects-package/package.json new file mode 100644 index 0000000000..aa31120c98 --- /dev/null +++ b/core/mr/test/spec/redirects-package/package.json @@ -0,0 +1,6 @@ +{ + "name": "redirects-package", + "dependencies": { + "foo": "*" + } +} diff --git a/core/mr/test/spec/redirects-package/program.js b/core/mr/test/spec/redirects-package/program.js new file mode 100644 index 0000000000..244394c513 --- /dev/null +++ b/core/mr/test/spec/redirects-package/program.js @@ -0,0 +1,3 @@ +var test = require('test'); +test.assert(require('foo/bar').foo() == 1, 'nested module identifier'); +test.print('DONE', 'info'); \ No newline at end of file diff --git a/core/mr/test/spec/redirects/barz.js b/core/mr/test/spec/redirects/barz.js new file mode 100644 index 0000000000..7aabcc17e4 --- /dev/null +++ b/core/mr/test/spec/redirects/barz.js @@ -0,0 +1,3 @@ +exports.foo = function() { + return 1; +} \ No newline at end of file diff --git a/core/mr/test/spec/redirects/package.json b/core/mr/test/spec/redirects/package.json new file mode 100644 index 0000000000..98ffd46b56 --- /dev/null +++ b/core/mr/test/spec/redirects/package.json @@ -0,0 +1,6 @@ +{ + "name": "redirects", + "redirects": { + "bar": "barz" + } +} diff --git a/core/mr/test/spec/redirects/program.js b/core/mr/test/spec/redirects/program.js new file mode 100644 index 0000000000..c9e00d478b --- /dev/null +++ b/core/mr/test/spec/redirects/program.js @@ -0,0 +1,3 @@ +var test = require('test'); +test.assert(require('bar').foo() == 1, 'nested module identifier'); +test.print('DONE', 'info'); \ No newline at end of file diff --git a/core/mr/test/spec/relative/package.json b/core/mr/test/spec/relative/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/relative/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/relative/program.js b/core/mr/test/spec/relative/program.js new file mode 100644 index 0000000000..e4eaafc7c0 --- /dev/null +++ b/core/mr/test/spec/relative/program.js @@ -0,0 +1,36 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require('test'); +var a = require('submodule/a'); +var b = require('submodule/b'); +test.assert(a.foo == b.foo, 'a and b share foo through a relative require'); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/relative/submodule/a.js b/core/mr/test/spec/relative/submodule/a.js new file mode 100644 index 0000000000..e4434717f8 --- /dev/null +++ b/core/mr/test/spec/relative/submodule/a.js @@ -0,0 +1,32 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +exports.foo = require('./b').foo; diff --git a/core/mr/test/spec/relative/submodule/b.js b/core/mr/test/spec/relative/submodule/b.js new file mode 100644 index 0000000000..59c768e326 --- /dev/null +++ b/core/mr/test/spec/relative/submodule/b.js @@ -0,0 +1,33 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +exports.foo = function () { +}; diff --git a/core/mr/test/spec/return/package.json b/core/mr/test/spec/return/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/return/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/return/program.js b/core/mr/test/spec/return/program.js new file mode 100644 index 0000000000..22262d0998 --- /dev/null +++ b/core/mr/test/spec/return/program.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require("test"); +test.assert(require("returns") === 10, 'module return value should replace exports'); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/return/returns.js b/core/mr/test/spec/return/returns.js new file mode 100644 index 0000000000..b15de354cf --- /dev/null +++ b/core/mr/test/spec/return/returns.js @@ -0,0 +1,32 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +return 10; diff --git a/core/mr/test/spec/sandbox/a.js b/core/mr/test/spec/sandbox/a.js new file mode 100644 index 0000000000..fdcef99713 --- /dev/null +++ b/core/mr/test/spec/sandbox/a.js @@ -0,0 +1,2 @@ +exports.value = require("./b"); +exports.c = require("dependency/c"); diff --git a/core/mr/test/spec/sandbox/b.js b/core/mr/test/spec/sandbox/b.js new file mode 100644 index 0000000000..beb840b5e7 --- /dev/null +++ b/core/mr/test/spec/sandbox/b.js @@ -0,0 +1 @@ +module.exports = "original"; diff --git a/core/mr/test/spec/sandbox/node_modules/dependency/c.js b/core/mr/test/spec/sandbox/node_modules/dependency/c.js new file mode 100644 index 0000000000..f053ebf797 --- /dev/null +++ b/core/mr/test/spec/sandbox/node_modules/dependency/c.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/core/mr/test/spec/sandbox/node_modules/dependency/main.js b/core/mr/test/spec/sandbox/node_modules/dependency/main.js new file mode 100644 index 0000000000..4d706eb4f1 --- /dev/null +++ b/core/mr/test/spec/sandbox/node_modules/dependency/main.js @@ -0,0 +1 @@ +module.exports = require("other"); diff --git a/core/mr/test/spec/sandbox/node_modules/dependency/other.js b/core/mr/test/spec/sandbox/node_modules/dependency/other.js new file mode 100644 index 0000000000..7f4090ad98 --- /dev/null +++ b/core/mr/test/spec/sandbox/node_modules/dependency/other.js @@ -0,0 +1 @@ +module.exports = "other"; diff --git a/core/mr/test/spec/sandbox/node_modules/dependency/package.json b/core/mr/test/spec/sandbox/node_modules/dependency/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/sandbox/node_modules/dependency/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/sandbox/package.json b/core/mr/test/spec/sandbox/package.json new file mode 100644 index 0000000000..65a62fb231 --- /dev/null +++ b/core/mr/test/spec/sandbox/package.json @@ -0,0 +1,13 @@ +{ + "mappings": { + "mr": "../../..", + "bluebird": "../../../node_modules/bluebird" + }, + "redirects": { + "d": "a" + }, + + "dependencies": { + "dependency": "*" + } +} diff --git a/core/mr/test/spec/sandbox/program.js b/core/mr/test/spec/sandbox/program.js new file mode 100644 index 0000000000..a64a2f17c5 --- /dev/null +++ b/core/mr/test/spec/sandbox/program.js @@ -0,0 +1,37 @@ +var test = require("test"); +var Promise = require("bluebird"); + +var sandbox = require("core/mr/sandbox"); + +var a = require("./a"); +var dep = require("dependency/main"); + +return Promise.all([ + sandbox(require, "./a", { + "./b": "mocked" + }), + sandbox(require, "dependency/main", { + "other": "mocked" + }), + sandbox(require, "d", { + "./b": "redirected" + }) +]) +.spread(function (sandboxedA, sandboxedDep, sandboxedD) { + var a2 = require("./a"); + var dep2 = require("dependency/main"); + + test.assert(a.value === "original", "a.b is the original"); + test.assert(sandboxedA.value === "mocked", "sandboxedA.b is the mock"); + test.assert(a.c === sandboxedA.c, "a.c and sandboxedA.c are the same"); + test.assert(a.d === sandboxedA.d, "a.d and sandboxedA.d are the same"); + test.assert(a2.value === "original", "a2.b is the original"); + + test.assert(dep === "other", "dep is the original"); + test.assert(sandboxedDep === "mocked", "sandboxedDep is the mock"); + test.assert(dep2 === "other", "dep2 is the original"); + + test.assert(sandboxedD.value === "redirected", "sandboxedD.b is redirected"); +}).then(function () { + test.print('DONE', 'info'); +}); diff --git a/core/mr/test/spec/script-injection-dep/node_modules/dependency/main.load.js b/core/mr/test/spec/script-injection-dep/node_modules/dependency/main.load.js new file mode 100644 index 0000000000..1bf8312dff --- /dev/null +++ b/core/mr/test/spec/script-injection-dep/node_modules/dependency/main.load.js @@ -0,0 +1,8 @@ +montageDefine("xxx", "main", { + dependencies: ["second"], + factory: function(require, exports, module) { + var second = require("second"); + exports.main = true; + exports.second = second; +}}); + diff --git a/core/mr/test/spec/script-injection-dep/node_modules/dependency/package.json b/core/mr/test/spec/script-injection-dep/node_modules/dependency/package.json new file mode 100644 index 0000000000..0ff1aa69dd --- /dev/null +++ b/core/mr/test/spec/script-injection-dep/node_modules/dependency/package.json @@ -0,0 +1,4 @@ +{ + "hash": "xxx", + "useScriptInjection": true +} diff --git a/core/mr/test/spec/script-injection-dep/node_modules/dependency/second.load.js b/core/mr/test/spec/script-injection-dep/node_modules/dependency/second.load.js new file mode 100644 index 0000000000..24f9049125 --- /dev/null +++ b/core/mr/test/spec/script-injection-dep/node_modules/dependency/second.load.js @@ -0,0 +1,6 @@ +montageDefine("xxx", "second", { + dependencies: [], + factory: function(require, exports, module) { + module.exports = true; +}}); + diff --git a/core/mr/test/spec/script-injection-dep/package.json b/core/mr/test/spec/script-injection-dep/package.json new file mode 100644 index 0000000000..8306757f1f --- /dev/null +++ b/core/mr/test/spec/script-injection-dep/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "dependency": "*" + } +} diff --git a/core/mr/test/spec/script-injection-dep/program.js b/core/mr/test/spec/script-injection-dep/program.js new file mode 100644 index 0000000000..2dae8df439 --- /dev/null +++ b/core/mr/test/spec/script-injection-dep/program.js @@ -0,0 +1,8 @@ +var test = require("test"); + +var dep = require("dependency/main"); + +test.assert(dep.main, "dependency loaded"); +test.assert(dep.second, "dependency's dependency loaded"); + +test.print("DONE", "info"); diff --git a/core/mr/test/spec/script-injection/package.json b/core/mr/test/spec/script-injection/package.json new file mode 100644 index 0000000000..a66a89f54c --- /dev/null +++ b/core/mr/test/spec/script-injection/package.json @@ -0,0 +1,9 @@ +{ + "mappings": { + "dependency": { + "name": "dependency", + "hash": "xxx", + "location": "packages/dependency/" + } + } +} diff --git a/core/mr/test/spec/script-injection/packages/dependency/main.load.js b/core/mr/test/spec/script-injection/packages/dependency/main.load.js new file mode 100644 index 0000000000..be505c2050 --- /dev/null +++ b/core/mr/test/spec/script-injection/packages/dependency/main.load.js @@ -0,0 +1,7 @@ +montageDefine("xxx", "main", { + dependencies: [], + factory: function(require, exports, module) { + exports.main = true; + } +}); + diff --git a/core/mr/test/spec/script-injection/packages/dependency/package.json.load.js b/core/mr/test/spec/script-injection/packages/dependency/package.json.load.js new file mode 100644 index 0000000000..632a75f0aa --- /dev/null +++ b/core/mr/test/spec/script-injection/packages/dependency/package.json.load.js @@ -0,0 +1,6 @@ +montageDefine("xxx", "package.json", { + exports: { + "hash": "xxx", + "useScriptInjection": true + } +}); diff --git a/core/mr/test/spec/script-injection/program.js b/core/mr/test/spec/script-injection/program.js new file mode 100644 index 0000000000..1e9fa8f68f --- /dev/null +++ b/core/mr/test/spec/script-injection/program.js @@ -0,0 +1,7 @@ +var test = require("test"); + +var main = require("dependency/main"); + +test.assert(main.main === true, "can load module in script injection package"); + +test.print("DONE", "info"); diff --git a/core/mr/test/spec/serialization-compiler/model.js b/core/mr/test/spec/serialization-compiler/model.js new file mode 100644 index 0000000000..2ebf986b40 --- /dev/null +++ b/core/mr/test/spec/serialization-compiler/model.js @@ -0,0 +1,4 @@ +exports.Model = function Model(value) { + this.value = value; +}; + diff --git a/core/mr/test/spec/serialization-compiler/object.js b/core/mr/test/spec/serialization-compiler/object.js new file mode 100644 index 0000000000..783ea14dc6 --- /dev/null +++ b/core/mr/test/spec/serialization-compiler/object.js @@ -0,0 +1,3 @@ +var Model = require('./model').Model; + +exports.model = new Model(10) diff --git a/core/mr/test/spec/serialization-compiler/package.json b/core/mr/test/spec/serialization-compiler/package.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/core/mr/test/spec/serialization-compiler/package.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/core/mr/test/spec/serialization-compiler/program.js b/core/mr/test/spec/serialization-compiler/program.js new file mode 100644 index 0000000000..9498448611 --- /dev/null +++ b/core/mr/test/spec/serialization-compiler/program.js @@ -0,0 +1,8 @@ +var test = require("test"); +var object = require("./object"); + +test.assert(object.model._montage_metadata, "should expose _montage_metadata on exports properties"); +test.assert(object.model._montage_metadata.module === 'object', "should expose module on _montage_metadata"); +test.assert(object.model._montage_metadata.property === 'model', "should expose module on _montage_metadata"); +test.assert(object.model._montage_metadata.require, "should expose require on _montage_metadata"); +test.print("DONE", "info"); diff --git a/core/mr/test/spec/top-level/b.js b/core/mr/test/spec/top-level/b.js new file mode 100644 index 0000000000..84393a4dba --- /dev/null +++ b/core/mr/test/spec/top-level/b.js @@ -0,0 +1,32 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +exports.foo = function() {}; diff --git a/core/mr/test/spec/top-level/package.json b/core/mr/test/spec/top-level/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/top-level/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/top-level/program.js b/core/mr/test/spec/top-level/program.js new file mode 100644 index 0000000000..23fe6c2ce0 --- /dev/null +++ b/core/mr/test/spec/top-level/program.js @@ -0,0 +1,36 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require('test'); +var a = require('submodule/a'); +var b = require('b'); +test.assert(a.foo().foo === b.foo, 'require works with top-level identifiers'); +test.print('DONE', 'info'); diff --git a/core/mr/test/spec/top-level/submodule/a.js b/core/mr/test/spec/top-level/submodule/a.js new file mode 100644 index 0000000000..90248af1ef --- /dev/null +++ b/core/mr/test/spec/top-level/submodule/a.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +exports.foo = function () { + return require('b'); +}; diff --git a/core/mr/test/spec/transitive/a.js b/core/mr/test/spec/transitive/a.js new file mode 100644 index 0000000000..e58ee63018 --- /dev/null +++ b/core/mr/test/spec/transitive/a.js @@ -0,0 +1,32 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +exports.foo = require('b').foo; diff --git a/core/mr/test/spec/transitive/b.js b/core/mr/test/spec/transitive/b.js new file mode 100644 index 0000000000..26db137ee1 --- /dev/null +++ b/core/mr/test/spec/transitive/b.js @@ -0,0 +1,32 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +exports.foo = require('c').foo; diff --git a/core/mr/test/spec/transitive/c.js b/core/mr/test/spec/transitive/c.js new file mode 100644 index 0000000000..7cdd28c025 --- /dev/null +++ b/core/mr/test/spec/transitive/c.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +exports.foo = function () { + return 1; +}; diff --git a/core/mr/test/spec/transitive/package.json b/core/mr/test/spec/transitive/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/core/mr/test/spec/transitive/package.json @@ -0,0 +1 @@ +{} diff --git a/core/mr/test/spec/transitive/program.js b/core/mr/test/spec/transitive/program.js new file mode 100644 index 0000000000..821c179855 --- /dev/null +++ b/core/mr/test/spec/transitive/program.js @@ -0,0 +1,34 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var test = require('test'); +test.assert(require('a').foo() == 1, 'transitive'); +test.print('DONE', 'info'); From 7fcca1987795627e12d35f2f8fc6b4167d568110 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 14:49:06 -0700 Subject: [PATCH 154/407] brong q-io as promise-io using bluebird in montage --- core/promise-io/.gitignore | 4 + core/promise-io/.travis.yml | 3 + core/promise-io/CHANGES.md | 171 ++++ core/promise-io/LICENSE | 19 + core/promise-io/README.md | 929 ++++++++++++++++++ core/promise-io/buffer-stream.js | 59 ++ core/promise-io/coverage-report.js | 44 + core/promise-io/deprecate.js | 51 + core/promise-io/fs-boot.js | 307 ++++++ core/promise-io/fs-common.js | 492 ++++++++++ core/promise-io/fs-mock.js | 555 +++++++++++ core/promise-io/fs-root.js | 145 +++ core/promise-io/fs.js | 355 +++++++ core/promise-io/fs2http.js | 65 ++ core/promise-io/http-apps.js | 152 +++ core/promise-io/http-apps/chain.js | 24 + core/promise-io/http-apps/content.js | 87 ++ core/promise-io/http-apps/cookie.js | 177 ++++ core/promise-io/http-apps/decorators.js | 178 ++++ core/promise-io/http-apps/fs.js | 417 ++++++++ core/promise-io/http-apps/html.js | 58 ++ core/promise-io/http-apps/json.js | 78 ++ core/promise-io/http-apps/negotiate.js | 120 +++ core/promise-io/http-apps/proxy.js | 27 + core/promise-io/http-apps/redirect.js | 209 ++++ core/promise-io/http-apps/route.js | 125 +++ core/promise-io/http-apps/status.js | 175 ++++ core/promise-io/http-cookie.js | 75 ++ core/promise-io/http.js | 411 ++++++++ core/promise-io/package.json | 50 + core/promise-io/reader.js | 133 +++ .../promise-io/spec/fs/boot-directory-spec.js | 47 + core/promise-io/spec/fs/contains-spec.js | 11 + core/promise-io/spec/fs/fixtures/hello.txt | 1 + core/promise-io/spec/fs/issues/1-spec.js | 33 + core/promise-io/spec/fs/make-tree-spec.js | 92 ++ core/promise-io/spec/fs/mock/append-spec.js | 41 + .../promise-io/spec/fs/mock/copy-tree-spec.js | 91 ++ .../promise-io/spec/fs/mock/fixture/hello.txt | 1 + core/promise-io/spec/fs/mock/link-spec.js | 70 ++ .../promise-io/spec/fs/mock/make-tree-spec.js | 109 ++ core/promise-io/spec/fs/mock/merge-spec.js | 67 ++ core/promise-io/spec/fs/mock/move-spec.js | 219 +++++ core/promise-io/spec/fs/mock/object-spec.js | 20 + core/promise-io/spec/fs/mock/range-spec.js | 26 + core/promise-io/spec/fs/mock/read-spec.js | 40 + .../spec/fs/mock/remove-directory-spec.js | 37 + .../spec/fs/mock/remove-tree-spec.js | 39 + core/promise-io/spec/fs/mock/root-spec.js | 32 + core/promise-io/spec/fs/mock/stat-spec.js | 26 + .../spec/fs/mock/symbolic-link-spec.js | 86 ++ .../spec/fs/mock/working-directory-spec.js | 31 + core/promise-io/spec/fs/mock/write-spec.js | 73 ++ core/promise-io/spec/fs/range-spec.js | 23 + core/promise-io/spec/fs/range-spec.txt | 1 + core/promise-io/spec/fs/read-spec.js | 22 + core/promise-io/spec/fs/relative-spec.js | 25 + core/promise-io/spec/fs/reroot-spec.js | 45 + core/promise-io/spec/fs/write-spec.js | 38 + core/promise-io/spec/http-apps/cookie-spec.js | 52 + .../spec/http-apps/directory-list-spec.js | 86 ++ .../spec/http-apps/fixtures/01234.txt | 1 + .../spec/http-apps/fixtures/1234.txt | 1 + .../spec/http-apps/fixtures/5678.txt | 0 .../spec/http-apps/fixtures/9012/3456.txt | 0 core/promise-io/spec/http-apps/hosts-spec.js | 49 + .../spec/http-apps/interpret-range-spec.js | 47 + .../spec/http-apps/partial-range-spec.js | 186 ++++ core/promise-io/spec/http-apps/proxy-spec.js | 82 ++ .../spec/http-apps/symbolic-link-spec.js | 110 +++ core/promise-io/spec/http/agent-spec.js | 97 ++ core/promise-io/spec/http/basic-spec.js | 107 ++ .../spec/http/normalize-request-spec.js | 204 ++++ core/promise-io/spec/lib/jasmine-promise.js | 42 + core/promise-io/writer.js | 113 +++ 75 files changed, 8218 insertions(+) create mode 100644 core/promise-io/.gitignore create mode 100644 core/promise-io/.travis.yml create mode 100644 core/promise-io/CHANGES.md create mode 100644 core/promise-io/LICENSE create mode 100644 core/promise-io/README.md create mode 100644 core/promise-io/buffer-stream.js create mode 100644 core/promise-io/coverage-report.js create mode 100644 core/promise-io/deprecate.js create mode 100644 core/promise-io/fs-boot.js create mode 100644 core/promise-io/fs-common.js create mode 100644 core/promise-io/fs-mock.js create mode 100644 core/promise-io/fs-root.js create mode 100644 core/promise-io/fs.js create mode 100644 core/promise-io/fs2http.js create mode 100644 core/promise-io/http-apps.js create mode 100644 core/promise-io/http-apps/chain.js create mode 100644 core/promise-io/http-apps/content.js create mode 100644 core/promise-io/http-apps/cookie.js create mode 100644 core/promise-io/http-apps/decorators.js create mode 100644 core/promise-io/http-apps/fs.js create mode 100644 core/promise-io/http-apps/html.js create mode 100644 core/promise-io/http-apps/json.js create mode 100644 core/promise-io/http-apps/negotiate.js create mode 100644 core/promise-io/http-apps/proxy.js create mode 100644 core/promise-io/http-apps/redirect.js create mode 100644 core/promise-io/http-apps/route.js create mode 100644 core/promise-io/http-apps/status.js create mode 100644 core/promise-io/http-cookie.js create mode 100644 core/promise-io/http.js create mode 100644 core/promise-io/package.json create mode 100644 core/promise-io/reader.js create mode 100644 core/promise-io/spec/fs/boot-directory-spec.js create mode 100644 core/promise-io/spec/fs/contains-spec.js create mode 100644 core/promise-io/spec/fs/fixtures/hello.txt create mode 100644 core/promise-io/spec/fs/issues/1-spec.js create mode 100644 core/promise-io/spec/fs/make-tree-spec.js create mode 100644 core/promise-io/spec/fs/mock/append-spec.js create mode 100644 core/promise-io/spec/fs/mock/copy-tree-spec.js create mode 100644 core/promise-io/spec/fs/mock/fixture/hello.txt create mode 100644 core/promise-io/spec/fs/mock/link-spec.js create mode 100644 core/promise-io/spec/fs/mock/make-tree-spec.js create mode 100644 core/promise-io/spec/fs/mock/merge-spec.js create mode 100644 core/promise-io/spec/fs/mock/move-spec.js create mode 100644 core/promise-io/spec/fs/mock/object-spec.js create mode 100644 core/promise-io/spec/fs/mock/range-spec.js create mode 100644 core/promise-io/spec/fs/mock/read-spec.js create mode 100644 core/promise-io/spec/fs/mock/remove-directory-spec.js create mode 100644 core/promise-io/spec/fs/mock/remove-tree-spec.js create mode 100644 core/promise-io/spec/fs/mock/root-spec.js create mode 100644 core/promise-io/spec/fs/mock/stat-spec.js create mode 100644 core/promise-io/spec/fs/mock/symbolic-link-spec.js create mode 100644 core/promise-io/spec/fs/mock/working-directory-spec.js create mode 100644 core/promise-io/spec/fs/mock/write-spec.js create mode 100644 core/promise-io/spec/fs/range-spec.js create mode 100644 core/promise-io/spec/fs/range-spec.txt create mode 100644 core/promise-io/spec/fs/read-spec.js create mode 100644 core/promise-io/spec/fs/relative-spec.js create mode 100644 core/promise-io/spec/fs/reroot-spec.js create mode 100644 core/promise-io/spec/fs/write-spec.js create mode 100644 core/promise-io/spec/http-apps/cookie-spec.js create mode 100644 core/promise-io/spec/http-apps/directory-list-spec.js create mode 100644 core/promise-io/spec/http-apps/fixtures/01234.txt create mode 100644 core/promise-io/spec/http-apps/fixtures/1234.txt create mode 100644 core/promise-io/spec/http-apps/fixtures/5678.txt create mode 100644 core/promise-io/spec/http-apps/fixtures/9012/3456.txt create mode 100644 core/promise-io/spec/http-apps/hosts-spec.js create mode 100644 core/promise-io/spec/http-apps/interpret-range-spec.js create mode 100644 core/promise-io/spec/http-apps/partial-range-spec.js create mode 100644 core/promise-io/spec/http-apps/proxy-spec.js create mode 100644 core/promise-io/spec/http-apps/symbolic-link-spec.js create mode 100644 core/promise-io/spec/http/agent-spec.js create mode 100644 core/promise-io/spec/http/basic-spec.js create mode 100644 core/promise-io/spec/http/normalize-request-spec.js create mode 100644 core/promise-io/spec/lib/jasmine-promise.js create mode 100644 core/promise-io/writer.js diff --git a/core/promise-io/.gitignore b/core/promise-io/.gitignore new file mode 100644 index 0000000000..8462c7799f --- /dev/null +++ b/core/promise-io/.gitignore @@ -0,0 +1,4 @@ +node_modules +.idea +.coverage_data +cover_html diff --git a/core/promise-io/.travis.yml b/core/promise-io/.travis.yml new file mode 100644 index 0000000000..20fd86b6a5 --- /dev/null +++ b/core/promise-io/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - 0.10 diff --git a/core/promise-io/CHANGES.md b/core/promise-io/CHANGES.md new file mode 100644 index 0000000000..118be5502a --- /dev/null +++ b/core/promise-io/CHANGES.md @@ -0,0 +1,171 @@ + + +## 1.11.5 + + - Allow HTTP requests to specified a false agent, per the underlying Node.js + behavior. (@grahamjenson) + - Use npm carat operator for dependencies. (@anton-rudeshko) + - The Node.js response argument is no longer being forwarded to HTTP request + handlers. + +## 1.11.4 + + - Fixes mock modification and access times. + +## 1.11.3 + + - Update dependency to Q 1.0.1. + - Fix an invalid reference in HTTP apps. + +## 1.11.2 + + - Fix race condition in `copyTree` + - Decode `pathInfo` in HTTP requests + - Pass all options through HTTP requests to Node.js’s HTTP client. + - Allow `listDirectories` override in HTTP file services. (@stuk) + +## 1.11.1 + + - Fix to `fs.reroot`, binding this properly (@Stuk) + - `fs.reroot` no longer traverses into directory prefixes implicitly. + We do not expect anyone was depending on this strange behavior, + but do expect folks were not using `reroot` because of it. + - Many missing methods added to RootFs (@3on) + - HTTP request normalization has been greatly improved. + Particularly, the ambiguity of `host` vs `hostname` is resolved. + `host` stands for both the hostname and the port, but only includes the port + if it differs from the default port for the protocol. + HTTP requests will normalize any combination of `host`, `port`, and + `hostname`. + The cookie jar HTTP application adapter has been updated. + - Support for URL authorization has been improved. If you use the `auth` + portion of a URL, it will be implicitly normalized to the corresponding + `Authorization` header, for basic HTTP auth. + Note that the convention in Q-IO is to use lower-case for all input and + output headers, since they are case-insensitive. + - MockFS now handles file modes properly (@Stuk) + - `fs.copyTree` fixed for duplicate file modes (@Stuk) + - Fix for HTTP clients on Node.js 0.11, which changed how it interprets the + `agent` option. + - Fixed a bug, NodeWriter was missing a `node` property, referring back to the + Node.js writable stream. + +## 1.11.0 + + - Adds `removeDirectory` and `statLink` to the Mock file system interface. + +## 1.10.7-8 + + - Fixes support for range content requests, such that Q-IO based web serves can + host static audio and video content to the web. Further work needed for the + more escoteric non-contiguous multi-range requests. + - Allow `copyTree` to write over existing trees. (@nerfin) + +## 1.10.6 + + - Restores the "request.terms.host" property to report which host pattern was + selected by a host negotiator. + +## 1.10.5 + + - Fixes support for host negotiation. + +## 1.10.4 + + - Fixes the `followInsecureSymbolicLinks` flag on the file tree HTTP + app. (@francoisfrisch) + - Fixes an error that gets missed when an HTTP request body is not + a proper forEachable. (@OliverJAsh) + +## 1.10.3 + + - Fix support of Node 0.6 path separators (@Sevinf) + +## 1.10.2 + + - Fix remoteTree for directories containing symbolic links. + - Tolerate "." in makeTree + - Stream writers now return reliable promises for finishing and flushing. + +## 1.10.0 + + - Add support for HTTP agents (@yuxhuang) + +## 1.9.4 + + - Updated dependencies + +## 1.9.3 + + - Fixes a regression in supporting `FS.read(path, "b")`. + +## 1.9.2 + + - Fixes `append` and aligns open flags with the underlying Node, except for + the default of UTF-8 if bytewise is not specified in the mode flag. + - Add `node` property to `Reader`, as advertised. + - Bug fixes + +## 1.9.1 + + - Brings the mock file system API up to date with file system as of 1.9.0. + +## 1.9.0 + + - Distinguishes `move` and `rename`. `move` will work across file system + boundaries. + +## 1.8.0 + + - Allows `move` to overwrite the target, or overwrite a target directory if + empty. + +## 1.7.2 + + - Fixes JSON content HTTP responses. + +## 1.7.1 + + - Fixes a bug in the HTTP redirect trap. + +## 1.7.0 + + - Added FileTree option followInsecureSymbolicLinks (@francoisfrisch) + +## 0.0.12 + + - Addressed Node 0.7.* compatibility. (@strager) + - Synchronized Q to 0.8.2. + +## 0.0.11 + + - Synchronized Q dependency. + +## 0.0.10 + + - Removed spurious log message. + +## 0.0.9 + + - Fixed a bug in observing the closing of files. (#1, @hornairs) + +## 0.0.8 + + - Reorganized, moved q-io to the top level. + - Reved dependencies. + - Moved q-io-buffer to its own project. + +## 0.0.3 + + - reved up Q to version 0.2 so duck-promises will work + +## 0.0.2 + + - added buffer-io + - added buffer mode to readers and writers + +## 0.0.1 + + - fixed dependency on broken q package + - restructured for overlay style packaging compatibility + diff --git a/core/promise-io/LICENSE b/core/promise-io/LICENSE new file mode 100644 index 0000000000..4c95761c66 --- /dev/null +++ b/core/promise-io/LICENSE @@ -0,0 +1,19 @@ + +Copyright 2009-2013 Kristopher Michael Kowal. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/core/promise-io/README.md b/core/promise-io/README.md new file mode 100644 index 0000000000..3619329d07 --- /dev/null +++ b/core/promise-io/README.md @@ -0,0 +1,929 @@ + +[![Build Status](https://secure.travis-ci.org/kriskowal/q-io.png)](http://travis-ci.org/kriskowal/q-io) + +# Q-IO + +Interfaces for IO that make use of promises. + +Q-IO now subsumes all of [Q-HTTP][] and [Q-FS][]. + +[Q-HTTP]: https://github.com/kriskowal/q-http +[Q-FS]: https://github.com/kriskowal/q-fs + +The Q-IO package does not export a main module. You must reach in +directly for `q-io/fs`, `q-io/http`, and `q-io/http-apps`. + +## Filesystem + +```javascript +var FS = require("q-io/fs"); +``` + +File system API for Q promises with method signatures patterned after +[CommonJS/Fileystem/A](http://wiki.commonjs.org/wiki/Filesystem/A) but +returning promises and promise streams. + +### open(path, options) + +Open returns a promise for either a buffer or string Reader or a Writer +depending on the flags. + +The options can be omitted, abbreviated to a `flags` string, or expanded +to an `options` object. + +- ``flags``: ``r``, ``w``, ``a``, ``b``, default of `r`, not bytewise +- ``charset``: default of ``utf-8`` +- ``bufferSize``: in bytes +- ``mode``: UNIX permissions +- ``begin`` first byte to read (defaults to zero) +- ``end`` one past the last byte to read. ``end - begin == length`` + +### read(path, options) + +`read` is a shortcut for opening a file and reading the entire contents +into memory. It returns a promise for the whole file contents. By +default, `read` provides a string decoded from UTF-8. With the bytewise +mode flag, provides a `Buffer`. + +The options argument is identical to that of `open`. + +```javascript +return FS.read(__filename, "b") +.then(function (content) { + // ... +}) +``` + +```javascript +return FS.read(__filename, { + flags: "b" +}) +``` + +### write(path, content, options) + +`write` is a shortcut for opening a file and writing its entire content +from a single string or buffer. + +The options are identical to that of `open`, but the "w" flag is +implied, and the "b" flag is implied if the content is a buffer. + +```javascript +return FS.write("hello.txt", "Hello, World!\n") +.then(function () { + return FS.read("hello.txt") +}) +.then(function (hello) { + expect(hello).toBe("Hello, World!\n") +}) +``` + +### append(path, content, options) + +`append` is a shorthand for opening a file for writing from the end of +the existing content from a single string or buffer. + +The options are identical to that of `open`, but the "w+" flags are +implied, and the "b" flag is implied if the content is a buffer. + +### copy(source, target) + +Copies a single file from one path to another. The target must be the +full path, including the file name. Unlike at the shell, the file name +is not inferred from the source path if the target turns out to be a +directory. + +Returns a promise for the completion of the operation. + +### copyTree(source, target) + +Copies a file or tree of files from one path to another. Symbolic links +are copied but not followed. + +Returns a promise for the completion of the operation. + +### list(path) + +Returns a promise for a list of file names in a directory. The file +names are relative to the given path. + +### listTree(path, guard(path, stat)) + +Returns a promise for a list of files in a directory and all the +directories it contains. Does not follow symbolic links. + +The second argument is an optional guard function that determines what +files to include and whether to traverse into another directory. It +receives the path of the file, relative to the starting path, and also +the stats object for that file. The guard must return a value like: + +- `true` indicates that the entry should be included +- `false` indicates that the file should be excluded, but should still + be traversed if it is a directory. +- `null` indiciates that a directory should not be traversed. + +### listDirectoryTree(path) + +Returns a promise for a deep list of directories. + +### makeDirectory(path, mode) + +Makes a directory at a given path. Fails if the parent directory does +not exist. Returns a promise for the completion of the operation. + +The mode is an optional Unix mode as an integer or string of octal +digits. + +### makeTree(path, mode) + +Finishes a path of directories. For any branch of the path that does +not exist, creates a directory. Fails if any branch of the path already +exists but is not a directory. + +Makes any directories with the given Unix mode. + +### remove(path) + +Removes a file at the given path. Fails if a directory exists at the +given path or if no file exists at the path. + +### removeTree(path) + +Removes a file or directory at a given path, recursively removing any +contained files and directories, without following symbolic links. + +### rename(source, target) + +Moves a file or directory from one path to another using the underlying +`rename(2)` implementation, thus it cannot move a file across devices. + +### move(source, target) + +Moves a file or directory from one path to another. If the source and +target are on different devices, falls back to copying and removing, +using `copyTree(source, target)` and, if completely successful, +`removeTree(source)`. + +### link(source, taget) + +Creates a hard link from the source + +### symbolicCopy(source, target, type) + +Creates a relative symbolic link from the target to the source with an +effect that resembles copying a file. + +The type is important for Windows. It is "file" by default, but may be +"directory" or "junction". + +### symbolicLink(target, link, type) + +Creates a symbolic link at the target path. The link may be absolute or +relative. The type *must* be "file", "directory", or "junction" and is +mandatory to encourage Windows portability. + +### chown(path, uid, gid) + +Changes the owner for a path using Unix user-id and group-id numbers. + +### chmod(path, mode) + +Changes the Unix mode for a path. Returns a promise. + +### stat(path) + +Follows all symbolic links along a path and returns a promise for the +metadata about a path as a `Stats` object. The Stats object implements: + +- `size` the size of the file in bytes +- `isDirectory()`: returns whether the path refers to a directory with + entries for other paths. +- `isFile()`: returns whether the path refers to a file physically + stored by the file system. +- `isBlockDevice()`: returns whether the path refers to a Unix device + driver, in which case there is no actual data in storage but the + operating system may allow you to communicate with the driver as a + blocks of memory. +- `isCharacterDevice()`: returns whether the path refers to a Unix + device driver, in which case there is no actual data in storage but + the operating system may allow you to communicate with the driver as + a stream. +- `isSymbolicLink()`: returns whether the path refers to a symbolic + link or junction. Stats for symbolic links are only discoverable + through `statLink` since `stat` follows symbolic links. +- `isFIFO()`: returns whether the path refers to a Unix named pipe. +- `isSocket()`: returns whether the path refers to a Unix domain + socket. +- `lastModified()`: returns the last time the path was opened for + writing as a `Date` +- `lastAccessed()`: returns the last time the path was opened for + reading or writing as a `Date` + +### statLink(path) + +Returns a promise for the `Stats` for a path without following symbolic +links. + +### statFd(fd) + +Returns a promise for the `Stats` for a Unix file descriptor number. + +### exists(path) + +Follows symbolic links and returns a promise for whether an entry exists +at a given path. + +### isFile(path) + +Follows symbolic links and returns a promise for whether a file exists +at a given path and does not cause an exception if nothing exists at +that path. + +### isDirectory(path) + +Follows symbolic links and returns a promise for whether a directory +exists at a given path and does not cause an exception if nothing exists +at that path. + +### isSymbolicLink(path) + +Returns a promise for whether a symbolic link exists at a given path and +does not cause an exception if nothing exists at that path. + +### lastModified(path) + +Follows symbolic links and returns a promise for the `Date` when the +entry at the given path was last opened for writing, but causes an +exception if no file exists at that path. + +### lastAccessed(path) + +Follows symbolic links and returns a promise for the `Date` when the +entry at the given path was last opened for reading or writing, but +causes an exception if no file exists at that path. + +### split(path) + +Splits a path into the names of entries along the path. If the path is +absolute, the first component is either a drive (with a colon) on +Windows or an empty string for the root of a Unix file system. + +### join(paths) or join(...paths) + +Joins a sequence of paths into a single normalized path. All but the +last path are assumed to refer to directories. + +### resolve(...paths) + +Like join but treats each path like a relative URL, so a terminating +slash indicates that a path is to a directory and the next path begins +at that directory. + +### normal(...paths) + +Takes a single path or sequence of paths and joins them into a single +path, eliminating self `.` and parent `..` entries when possible. + +### absolute(path) + +Joins and normalizes a path from the current working directory, +returning a string. + +### canonical(path) + +Returns a promise for the absolute, canonical location of a given path, +following symbolic links and normalizing path components. An entry does +not need to exist at the end of the path. + +### readLink(path) + +Returns a promise for the path string of a symbolic link at a given +path. + +### contains(parent, child) + +For any two absolute or relative paths, computes whether the parent path +is an ancestor of the child path. + +### relative(source, target) + +Returns a promise for the relative path from one path to another using +`..` parent links where necessary. This operation is asynchronous +because it is necessary to determine whether the source path refers to a +directory or a file. + +### relativeFromFile(source, target) + +Assuming that the source path refers to a file, returns a string for the +relative path from the source to the target path. + +### relativeFromDirectory(source, target) + +Assuming that the source path refers to a directory, returns a string +for the relative path from the source to the target path. + +### isAbsolute(path) + +Returns whether a path begins at the root of a Unix file system or a +Windows drive. + +### isRelative(path) + +Returns whether a path does not begin at the root of a Unix file system +or Windows drive. + +### isRoot(path) + +Returns whether a path is to the root of a Unix file system or a Windows +drive. + +### root(path) + +Returns the Windows drive that contains a given path, or the root of a +Unix file system. + +### directory(path) + +Returns the path to the directory containing the given path. + +### base(path, extension) + +Returns the last entry of a path. If an extension is provided and +matches the extension of the path, removes that extension. + +### extension(path) + +Returns the extension for a path (everything following the last dot `.` +in a path, unless that dot is at the beginning of the entry). + +### reroot(path) + +Returns an attenuated file system that uses the given path as its root. +The resulting file system object is identical to the parent except that +the child cannot open any file that is not within the root. Hard links +are effectively inside the root regardless, but symbolic links cannot be +followed outside of the jail. + +### toObject(path) + +Reads every file in the file system under a given path and returns a +promise for an object that contains the absolute path and a Buffer for +each of those files. + +### glob(pattern) + +Not yet implemented + +### match(pattern, path) + +Not yet implemented + +## Mock Filesystem + +Q-IO provides a mock filesystem interface. The mock filesystem has the +same interface as the real one and has most of the same features, but +operates on a purely in-memory, in-process, in-javascript filesystem. + +A mock filesystem can be created from a data structure. Objects are +directories. Keys are paths. A buffer is a file’s contents. Anything +else is coerced to a string, then to a buffer in the UTF-8 encoding. + +```javascript +var MockFs = require("q-io/fs-mock"); +var mockFs = MockFs({ + "a": { + "b": { + "c.txt": "Content of a/b/c.txt" + } + }, + "a/b/d.txt": new Buffer("Content of a/b/d.txt", "utf-8") +}) +``` + +You can also instantiate a mock file system with the content of a +subtree of a real file system. You receive a promise for the mock +filesystem. + +```javascript +var FS = require("q-io/fs"); +FS.mock(__dirname) +.then(function (fs) { + // +}) +.done(); +``` + +## HTTP + +The HTTP module resembles [CommonJS/JSGI][]. + +```javascript +var HTTP = require("q-io/http"); +``` + +[CommonJS/JSGI]: http://wiki.commonjs.org/wiki/JSGI + +### Server(app) + +The `http` module exports a `Server` constructor. + +- accepts an application, returns a server. +- calls the application function when requests are received. + - if the application returns a response object, sends that + response. +- ``listen(port)`` + - accepts a port number. + - returns a promise for undefined when the server has begun + listening. +- ``stop()`` + - returns a promise for undefined when the server has stopped. + +### request(request object or url) + +The `http` module exports a `request` function that returns a promise +for a response. + +- accepts a [request object](\#request) or a URL string. +- returns a promise for a [response object](\#response). + +### read(request object or url) + +The `http` module exports a `read` function, analogous to +`Fs.read(path)`, but returning a promise for the content of an OK HTTP +response. + +- accepts a [request object](\#request) or a URL string. +- returns a promise for the response body as a string provided + that the request is successful with a 200 status. + - rejects the promise with the response as the reason for + failure if the request fails. + +### normalizeRequest(request object or url) + +- coerces URLs into request objects. +- completes an incomplete request object based on its `url`. + +### normalizeResponse(response) + +- coerces strings, arrays, and other objects supporting + ``forEach`` into proper response objects. +- if it receives `undefined`, it returns `undefined`. This is used as + a singal to the requester that the responder has taken control of + the response stream. + +### request + +A complete request object has the following properties. + +- ``url`` the full URL of the request as a string +- ``path`` the full path as a string +- ``scriptName`` the routed portion of the path, like ``""`` for + ``http://example.com/`` if no routing has occurred. +- ``pathInfo`` the part of the path that remains to be routed, + like ``/`` for ``http://example.com`` or ``http://example.com/`` + if no routing has occurred. +- ``version`` the requested HTTP version as an array of strings. +- ``method`` like ``"GET"`` +- ``scheme`` like ``"http"`` +- ``host`` like ``"example.com"`` in case of default ports (80 or 443), otherwise like ``example.com:8080`` +- ``hostname`` like ``"example.com"`` +- ``port`` the port number, like ``80`` +- ``remoteHost`` +- ``remotePort`` +- ``headers`` + corresponding values, possibly an array for multiple headers + of the same name. +- ``agent`` + a custom node [HTTP](http://nodejs.org/api/http.html#http_class_http_agent) + or [HTTPS](http://nodejs.org/api/https.html#https_class_https_agent) + agent. HTTP and HTTPS agents can implement custom socket pools, + allow use of SSL client certificates and self-signed certificates. +- ``body`` an array of string or node buffers +- ``node`` the wrapped Node request object + +### response + +A complete response object has the following properties. + +- ``status`` the HTTP status code as a number, like ``200``. +- [``headers``](\#headers) +- [``body``](\#body) any `forEach`able, such as an array of strings +- ``onclose`` is an optional function that this library will call + when a response concludes. +- ``node`` the wrapped Node response object. + +### headers + +Headers are an object mapping lower-case header-names to corresponding +values, possibly an array for multiple headers of the same name, for +both requests and responses. + +### body + +body is a representation of a readable stream, either for the content of +a request or a response. It is implemented as a Q-IO reader. + +- ``forEach(callback)`` + - accepts a ``callback(chunk)`` function + - accepts a chunk as either a string or a ``Buffer`` + - returns undefined or a promise for undefined when the + chunk has been flushed. + - returns undefined or a promise for undefined when the stream + is finished writing. + - the ``forEach`` function for arrays of strings or buffers is + sufficient for user-provided bodies +- the ``forEach`` function is the only necessary function for + bodies provided to this library. +- in addition to ``forEach``, bodies provided by this library + support the entire readable stream interface provided by + ``q-io``. +- ``read()`` + - returns a promise for the entire body as a string or a + buffer. + +### application + +An HTTP application is a function that accepts a request and returns a +response. The `request` function itself is an application. +Applications can be chained and combined to make advanced servers and +clients. + +- accepts a request +- returns a response, a promise for a response, or nothing if no + response should be sent. + + +## Streams + +### Reader + +Reader instances have the following methods: + +- `read()` +- `forEach(callback)` +- `close()` +- `node` the underlying node reader + +Additionally, the `Reader` constructor has the following methods: + +- `read(stream, charset)` accepts any foreachable and returns either a + buffer or a string if given a charset. +- `join(buffers)` consolidates an array of buffers into a single + buffer. The buffers array is collapsed in place and the new first + and only buffer is returned. + +The `reader` module exports a function that accepts a Node reader and +returns a Q reader. + +### Writer + +Writer instances have the following methods: + +- `write(content)` writes a chunk of content, either from a string or + a buffer. +- `flush()` returns a promise to drain the outbound content all the + way to its destination. +- `close()` +- `destroy()` +- `node` the underlying node writer + +The `writer` module exports a function that accepts a Node writer and +returns a Q writer. + +### Buffer + +```javascript +var BufferStream = require("q-io/buffer-stream"); +var stream = BufferStream(new Buffer("Hello, World!\n", "utf-8"), "utf-8") +``` + +## HTTP Applications + +The HTTP applications module provides a comprehensive set of JSGI-alike +applications and application factories, suitable for use with the `http` +server and client. + +```javascript +var Apps = require("q-io/http-apps"); +``` + +### ok(content, contentType, status) : Response + +Creates an `HTTP 200 Ok` response with the given content, content type, +and status. + +The content may be a string, buffer, array of strings, array of buffers, +a readable stream of strings or buffers, or (generally) anything that +implements `forEach`. + +The default content type is `text/plain`. + +The default status is `200`. + +### badRequest(request) : Response + +An application that returns an `HTTP 400 Bad request` response for any +request. + +### notFound(request) : Response + +An application that returns an `HTTP 404 Not found` response for any +request. + +### methodNotAllowed(request) : Response + +An application that returns an `HTTP 405 Method not allowed` response +for any request. This is suitable for any endpoint where there is no +viable handler for the request method. + +### notAcceptable(request) : Response + +An application that returns an `HTTP 406 Not acceptable` response for +any request. This is suitable for any situation where content +negotiation has failed, for example, if you cannot response with any of +the accepted encoding, charset, or language. + +### redirect(request, location, status, tree) : Response + +Not to be confused with an HTTP application, this is a utility that +generates redirect responses. + +The returns response issues a redirect to the given location. The +utility fully qualifies the location. + +This particular method should be used directly to generate an `HTTP 301 +Temporary redirect` response, but passing `307` in the status argument +turns it into an `HTTP 307 Permanent redirect` response. + +This particular method should be used to send all requests to a specific +location, but setting the `tree` argument to `true` causes the redirect +to follow the remaining unrouted path from the redirect location, so if +you move an entire directory tree from one location to another, this +redirect can forward to all of them. + +### redirectTree(request, location) : Response + +Produces an `HTTP 301 Temporary redirect` from one directory tree to +another, using `redirect`. + +### permanentRedirect(request, location) : Response + +Produces an `HTTP 307 Permanent redirect` to a given location, using +`redirect`. + +### permanentRedirectTree(request, location) : Response + +Produces an `HTTP 307 Permanent redirect` from one directory tree to +another, using `redirect`. + +### file(request, path, contentType) : Response + +Produces an HTTP response with the file at a given path. By default, it +infers the content type from the extension of the path. + +The file utility produces an `e-tag` header suitable for cache control, +and may produce an `HTTP 304 Not modified` if the requested resource has +the same entity tag. + +The file utility may produce an `HTTP 206 Partial content` response with +a `content-range` header if the request has a `range` header. If the +partial range request cannot be satisified, it may respond `HTTP 416 Not +satisfiable`. + +In all cases, the response body is streamed from the file system. + +### etag(stat) + +Computes an entity tag for a file system `Stats` object, using the +`node.ino`, `size`, and last modification time. + +### directory(request, path) + +This is not yet implemented. + +### json(object, visitor, tabs) : Response + +Returns an `HTTP 200 Ok` response from some JSON, using the same +argumensts as `JSON.stringify`. + +### Content(body, contentType, status) : Application + +A factory that produces an HTTP application that will always respond +with the given content, content type, and status. The default content +type is `text/plain` and the default status is `200`. + +The body may be a string, array of strings or buffers, or a readable +stream of strings or buffers. + +### File(path, contentType) : Application + +A factory that produces an HTTP application that will always respond +with the file at the given path. The content type is inferred from the +path extension by default, but can be overridden with `contentType`. + +### FileTree(path, options) : Application + +A factory that produces an HTTP application that responds to all +requests with files within a branch of the file system starting at the +given path and using any unprocessed portion of the request location. + +Options include: + +- `notFound(request, response)`: alternate 404 responder, defaults to + `HttpApps.notFound` +- `file(request, path, contentType, fs)`: alternate file responder, + defaults to `HttpApps.file` +- `contentType`: forces the content type of file requests, forwarded + to the `file` handler. +- `directory(request, path, contentType, fs)`: alternate directory + responder, defaults to `HttpApps.directory`. +- `redirectSymbolicLinks`: directs the client to use a redirect + response for symbolic links instead of following links internally. +- `permanent`: symbolic links that are turned into HTTP redirects will + be permanent. +- `followInsecureSymbolicLinks`: directs `FileTree` to serve files + that are outside the root path of the file tree if a symbolic link + traverses there. +- `fs`: alternate file system, defaults to the `fs` module. + +### Redirect(path) : Application + +A factory that produces an HTTP application that temporarily redirects +to the given path. + +### RedirectTree(path) : Application + +A factory that produces an HTTP application that redirects all requests +under the requested path to parallel locations at the given path. + +### PermanentRedirect(path) : Application + +A factory that produces an HTTP application that redirects all requests +to an exact location and instructs the requester's cache never to ask +again. + +### PermanentRedirectTree(path) : Application + +A factory that produces an HTTP application that redirects all requests +under the request path to a parallel location under the given path and +instructs the requester's cache never to ask again. + +### Cap(app, notFound) : Application + +A factory that produces an HTTP application that will cause an `HTTP 404 +Not found` response if the request has not reached the end of its route +(meaning `pathInfo` is not `""` or `"/"`), or will forward to the given +application. + +### Routing + +Several routing application factories have the same form. They all take +an object as their first argument and an optional fallback application +as their second. The object maps each of the supported options for keys +to an HTTP application for handling that option. + +- Branch(paths, notFound) routes the next unprocessed path component +- Method(methods, notFound) routes the HTTP method. Methods are + upper-case. +- ContentType(contentTypes, notAcceptable) routes based on the + "accept" request header and produces a "content-type" response + header. +- Langauge(languages, notAcceptable) routes based on the + "accept-language" header and produces a "language" response header. +- Charaset(charsets, notAcceptable) routes based on the + "accept-charset" header and produces a "charset" response header. +- Encoding(encodings, notAcceptable) routes based on the + "accept-encoding" request header and produces an "encoding" response + header. +- Host(hosts, notFound) routes based on the host name of the request + "host" header, which defaults to "*". This is equivalent to virtual + host mapping. + +### Select(selector) : Application + +Produces an HTTP application that uses a function to determine the next +application to route. The `selector` is a function that accepts the +request and returns an HTTP application. + +### FirstFound(apps) + +Returns an HTTP application that attempts to respond with each of a +series of applications and returns the first response that does not have +a `404 Not found` status, or whatever response comes last. + +### Error(application, debug) : Application + +Wraps an application such that any exceptions get converted into `HTTP +500 Server error` responses. If `debug` is enabled, produces the +exception and stack traces in the body of the response. + +### Log(application, log, stamp) : Application + +Wraps an application such that request and response times are logged. +The `log` function reports to `console.log` by default. The +`stamp(message)` function prefixes an ISO time stamp by default. + +### Time(application) : Application + +Adds an `x-response-time` header to the response, with the time from receiving +starting the request to starting the response in miliseconds. + +### Date(application) : Application + +Adds a `date` header to the response with the current date for cache +control purposes. + +### Tap(app, tap) : Application + +Wraps an application such that the `tap` function receives the request +first. If the tap returns nothing, the request goes to the `app`. If +the `tap` returns a response, the `app` never gets called. + +### Trap(app, trap) : Application + +Wraps an application such that the `trap` function receives the +response. If it returns nothing, the response if forwarded. If the +`trap` returns a response, the original response is discarded. + +### ParseQuery(application) + +Wraps an application such that the query string is parsed and placed in +`request.parse`. + + +## Coverage + +Use `npm run cover` to generate and view a coverage report of Q-IO. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FilePercentageMissing
fs-boot.js87%41
fs.js72%100
reader.js94%8
writer.js91%8
fs-common.js87%52
fs-root.js88%11
fs-mock.js91%46
buffer-stream.js89%6
http.js93%25
http-apps.js80%286
http-cookie.js79%15
+ +--- + +Copyright 2009–2013 Kristopher Michael Kowal +MIT License (enclosed) + diff --git a/core/promise-io/buffer-stream.js b/core/promise-io/buffer-stream.js new file mode 100644 index 0000000000..bad038f667 --- /dev/null +++ b/core/promise-io/buffer-stream.js @@ -0,0 +1,59 @@ + +var Q = require("q-bluebird"); +var Reader = require("./reader"); + +module.exports = BufferStream; +function BufferStream(chunks, charset) { + if (!(this instanceof BufferStream)) { + return new BufferStream(chunks, charset); + } + if (!chunks) { + chunks = []; + } else if (!Array.isArray(chunks)) { + chunks = [chunks]; + } + this._charset = charset; + this._chunks = chunks; + this._close = Q.defer(); + this.closed = this._close.promise; +} + +BufferStream.prototype.forEach = function (write, thisp) { + var self = this; + var chunks = self._chunks; + return Q.fcall(function () { + chunks.splice(0, chunks.length).forEach(write, thisp); + }); +}; + +BufferStream.prototype.read = function () { + var result; + result = Reader.join(this._chunks); + if (this._charset) { + result = result.toString(this._charset); + } + return Q.resolve(result); +}; + +BufferStream.prototype.write = function (chunk) { + if (this._charset) { + chunk = new Buffer(String(chunk), this._charset); + } else { + if (!(chunk instanceof Buffer)) { + throw new Error("Can't write strings to buffer stream without a charset: " + chunk); + } + } + this._chunks.push(chunk); + return Q.resolve(); +}; + +BufferStream.prototype.close = function () { + this._close.resolve(); + return Q.resolve(); +}; + +BufferStream.prototype.destroy = function () { + this._close.resolve(); + return Q.resolve(); +}; + diff --git a/core/promise-io/coverage-report.js b/core/promise-io/coverage-report.js new file mode 100644 index 0000000000..50b29ff1b9 --- /dev/null +++ b/core/promise-io/coverage-report.js @@ -0,0 +1,44 @@ + +require("collections/shim"); +var Q = require("q-bluebird"); +var FS = require("./fs"); + +FS.listTree(".coverage_data", function (name, stat) { + return (/^\.coverage_data\/coveragefile/).test(name); +}) +.then(function (list) { + return Q.all(list.map(function (file) { + return FS.read(file) + .then(function (content) { + return JSON.parse(content); + }) + .then(function (coverage) { + console.log(""); + console.log(" "); + console.log(" "); + console.log(" "); + console.log(" "); + console.log(" "); + console.log(" "); + console.log(" "); + console.log(" "); + Object.forEach(coverage.files, function (file, path) { + path = FS.relativeFromDirectory(__dirname, path); + if (/^spec/.test(path)) + return; + console.log(" "); + console.log(" "); + console.log(" "); + console.log(" "); + console.log(" "); + }); + console.log(" "); + console.log("
FilePercentageMissing
" + path + "" + (file.stats.percentage * 100).toFixed(0) + "%" + file.stats.missing + "
"); + }, function (error) { + error.message = "Can't parse " + file + " because " + error.message; + throw error; + }) + })) +}) +.done() + diff --git a/core/promise-io/deprecate.js b/core/promise-io/deprecate.js new file mode 100644 index 0000000000..5a6b1120b3 --- /dev/null +++ b/core/promise-io/deprecate.js @@ -0,0 +1,51 @@ +/** + * @module deprecate + */ + +/** + * Prints out a deprecation warning to the console.warn with the format: + * `name` is deprecated, use `alternative` instead. + * It can also print out a stack trace with the line numbers. + * @param {String} name - Name of the thing that is deprecated. + * @param {String} alternative - Name of alternative that should be used instead. + * @param {Number} [stackTraceLimit] - depth of the stack trace to print out. Set to falsy value to disable stack. + */ +exports.deprecationWarning = function deprecationWarning(name, alternative, stackTraceLimit) { + if (stackTraceLimit) { + var depth = Error.stackTraceLimit; + Error.stackTraceLimit = stackTraceLimit; + } + if (typeof console !== "undefined" && typeof console.warn === "function") { + var stack = (stackTraceLimit ? new Error("").stack : "") ; + if(alternative) { + console.warn(name + " is deprecated, use " + alternative + " instead.", stack); + } else { + //name is a complete message + console.warn(name, stack); + } + } + if (stackTraceLimit) { + Error.stackTraceLimit = depth; + } +} + +/** + * Provides a function that can replace a method that has been deprecated. + * Prints out a deprecation warning to the console.warn with the format: + * `name` is deprecated, use `alternative` instead. + * It will also print out a stack trace with the line numbers. + * @param {Object} scope - The object that will be used as the `this` when the `deprecatedFunction` is applied. + * @param {Function} deprecatedFunction - The function object that is deprecated. + * @param {String} name - Name of the method that is deprecated. + * @param {String} alternative - Name of alternative method that should be used instead. + * @returns {Function} deprecationWrapper + */ +exports.deprecateMethod = function deprecate(scope, deprecatedFunction, name, alternative) { + var deprecationWrapper = function () { + // stackTraceLimit = 3 // deprecationWarning + deprecate + caller of the deprecated method + deprecationWarning(name, alternative, 3); + return deprecatedFunction.apply(scope ? scope : this, arguments); + }; + deprecationWrapper.deprecatedFunction = deprecatedFunction; + return deprecationWrapper; +} diff --git a/core/promise-io/fs-boot.js b/core/promise-io/fs-boot.js new file mode 100644 index 0000000000..ea77fd1bd8 --- /dev/null +++ b/core/promise-io/fs-boot.js @@ -0,0 +1,307 @@ +(function (exports) { + +// -- kriskowal Kris Kowal Copyright (C) 2009-2010 MIT License +// -- tlrobinson Tom Robinson TODO + +/** + * Pure JavaScript implementations of file system path + * manipulation. + */ + +// NOTE: this file may be used is the engine bootstrapping +// process, so any "requires" must be accounted for in +// narwhal.js + +/*whatsupdoc*/ +/*markup markdown*/ + +var regExpEscape = function (str) { + return str.replace(/[-[\]{}()*+?.\\^$|,#\s]/g, "\\$&"); +}; + +var path = require("path"); + +/** + * @name ROOT + * * `/` on Unix + * * `\` on Windows + */ + +/** + * @name SEPARATOR + * * `/` on Unix + * * `\` on Windows + */ + +/** + * @name ALT_SEPARATOR + * * undefined on Unix + * * `/` on Windows + */ + +exports.ROOT = exports.SEPARATOR = path.sep || (process.platform === "win32" ? "\\": "/"); +if (path.sep === "\\") { + exports.ALT_SEPARATOR = "/"; +} else { + exports.ALT_SEPARATOR = undefined; +} + +// we need to make sure the separator regex is always in sync with the separators. +// this caches the generated regex and rebuild if either separator changes. +var separatorCached, altSeparatorCached, separatorReCached; +/** + * @function + */ +exports.SEPARATORS_RE = function () { + if ( + separatorCached !== exports.SEPARATOR || + altSeparatorCached !== exports.ALT_SEPARATOR + ) { + separatorCached = exports.SEPARATOR; + altSeparatorCached = exports.ALT_SEPARATOR; + separatorReCached = new RegExp("[" + + (separatorCached || "").replace(/[-[\]{}()*+?.\\^$|,#\s]/g, "\\$&") + + (altSeparatorCached || "").replace(/[-[\]{}()*+?.\\^$|,#\s]/g, "\\$&") + + "]", "g"); + } + return separatorReCached; +} + +/** + * separates a path into components. If the path is + * absolute, the first path component is the root of the + * file system, indicated by an empty string on Unix, and a + * drive letter followed by a colon on Windows. + * @returns {Array * String} + */ +exports.split = function (path) { + var parts; + try { + parts = String(path).split(exports.SEPARATORS_RE()); + } catch (exception) { + throw new Error("Cannot split " + (typeof path) + ", " + JSON.stringify(path)); + } + // this special case helps isAbsolute + // distinguish an empty path from an absolute path + // "" -> [] NOT [""] + if (parts.length === 1 && parts[0] === "") + return []; + // "a" -> ["a"] + // "/a" -> ["", "a"] + return parts; +}; + +/** + * Takes file system paths as variadic arguments and treats + * each as a file or directory path and returns the path + * arrived by traversing into the those paths. All + * arguments except for the last must be paths to + * directories for the result to be meaningful. + * @returns {String} path + */ +exports.join = function () { + if (arguments.length === 1 && Array.isArray(arguments[0])) + return exports.normal.apply(exports, arguments[0]); + return exports.normal.apply(exports, arguments); +}; + +/** + * Takes file system paths as variadic arguments and treats + * each path as a location, in the URL sense, resolving each + * new location based on the previous. For example, if the + * first argument is the absolute path of a JSON file, and + * the second argument is a path mentioned in that JSON + * file, `resolve` returns the absolute path of the + * mentioned file. + * @returns {String} path + */ +exports.resolve = function () { + var root = ""; + var parents = []; + var children = []; + var leaf = ""; + for (var i = 0; i < arguments.length; i++) { + var path = String(arguments[i]); + if (path == "") + continue; + var parts = path.split(exports.SEPARATORS_RE()); + if (exports.isAbsolute(path)) { + root = parts.shift() + exports.SEPARATOR; + parents = []; + children = []; + } + leaf = parts.pop(); + if (leaf == "." || leaf == "..") { + parts.push(leaf); + leaf = ""; + } + for (var j = 0; j < parts.length; j++) { + var part = parts[j]; + if (part == "." || part == "") { + } else if (part == "..") { + if (children.length) { + children.pop(); + } else { + if (root) { + } else { + parents.push(".."); + } + } + } else { + children.push(part); + } + }; + } + path = parents.concat(children).join(exports.SEPARATOR); + if (path) leaf = exports.SEPARATOR + leaf; + return root + path + leaf; +}; + +/** + * Takes paths as any number of arguments and reduces them + * into a single path in normal form, removing all "." path + * components, and reducing ".." path components by removing + * the previous path component if possible. + * @returns {String} path + */ +exports.normal = function () { + var root = ""; + var parents = []; + var children = []; + for (var i = 0, ii = arguments.length; i < ii; i++) { + var path = String(arguments[i]); + // empty paths have no affect + if (path === "") + continue; + var parts = path.split(exports.SEPARATORS_RE()); + if (exports.isAbsolute(path)) { + root = parts.shift() + exports.SEPARATOR; + parents = []; + children = []; + } + for (var j = 0, jj = parts.length; j < jj; j++) { + var part = parts[j]; + if (part === "." || part === "") { + } else if (part == "..") { + if (children.length) { + children.pop(); + } else { + if (root) { + } else { + parents.push(".."); + } + } + } else { + children.push(part); + } + } + } + path = parents.concat(children).join(exports.SEPARATOR); + return root + path; +}; + +/*** + * @returns {Boolean} whether the given path begins at the + * root of the file system or a drive letter. + */ +exports.isAbsolute = function (path) { + // for absolute paths on any operating system, + // the first path component always determines + // whether it is relative or absolute. On Unix, + // it is empty, so ["", "foo"].join("/") == "/foo", + // "/foo".split("/") == ["", "foo"]. + var parts = exports.split(path); + // split("") == []. "" is not absolute. + // split("/") == ["", ""] is absolute. + // split(?) == [""] does not occur. + if (parts.length == 0) + return false; + return exports.isRoot(parts[0]); +}; + +/** + * @returns {Boolean} whether the given path does not begin + * at the root of the file system or a drive letter. + */ +exports.isRelative = function (path) { + return !exports.isAbsolute(path); +}; + +/** + * @returns {Boolean} whether the given path component + * corresponds to the root of the file system or a drive + * letter, as applicable. + */ +exports.isRoot = function (first) { + if (exports.SEPARATOR === "\\") { + return /[a-zA-Z]:$/.test(first); + } else { + return first == ""; + } +}; + +/** + * @returns {String} the Unix root path or corresponding + * Windows drive for a given path. + */ +exports.root = function (path) { + if (!exports.isAbsolute(path)) + path = require("./fs").absolute(path); + var parts = exports.split(path); + return exports.join(parts[0], ""); +}; + +/** + * @returns {String} the parent directory of the given path. + */ +exports.directory = function (path) { + path = exports.normal(path); + var absolute = exports.isAbsolute(path); + var parts = exports.split(path); + // XXX needs to be sensitive to the root for + // Windows compatibility + if (parts.length) { + if (parts[parts.length - 1] == "..") { + parts.push(".."); + } else { + parts.pop(); + } + } else { + parts.unshift(".."); + } + return parts.join(exports.SEPARATOR) || ( + exports.isRelative(path) ? + "" : exports.ROOT + ); +}; + +/** + * @returns {String} the last component of a path, without + * the given extension if the extension is provided and + * matches the given file. + * @param {String} path + * @param {String} extention an optional extention to detect + * and remove if it exists. + */ +exports.base = function (path, extension) { + var base = path.split(exports.SEPARATORS_RE()).pop(); + if (extension) + base = base.replace( + new RegExp(regExpEscape(extension) + "$"), + "" + ); + return base; +}; + +/** + * @returns {String} the extension (e.g., `txt`) of the file + * at the given path. + */ +exports.extension = function (path) { + path = exports.base(path); + path = path.replace(/^\.*/, ""); + var index = path.lastIndexOf("."); + return index <= 0 ? "" : path.substring(index); +}; + +})(typeof exports !== "undefined" ? exports : FS_BOOT = {}); diff --git a/core/promise-io/fs-common.js b/core/promise-io/fs-common.js new file mode 100644 index 0000000000..d38f2f1586 --- /dev/null +++ b/core/promise-io/fs-common.js @@ -0,0 +1,492 @@ +var Q = require("q-bluebird"); +var Boot = require("./fs-boot"); +var RootFs = require("./fs-root"); +var MockFs = require("./fs-mock"); + +// TODO patternToRegExp +// TODO glob +// TODO match + +var concat = function (arrays) { + return Array.prototype.concat.apply([], arrays); +}; + +exports.update = function (exports, workingDirectory) { + + for (var name in Boot) { + exports[name] = Boot[name]; + } + + /** + * Read a complete file. + * @param {String} path Path to the file. + * @param {String} [options.flags] The mode to open the file with. + * @param {String} [options.charset] The charset to open the file with. + * @param {Object} [options] An object with options. + * second argument. + * @returns {Promise * (String || Buffer)} + */ + exports.read = function (path, flags, charset, options) { + if (typeof flags === "object") { + options = flags; + } else if (typeof charset === "object") { + options = charset; + options.flags = flags; + } else { + options = options || {}; + options.flags = flags; + options.charset = charset; + } + options.flags = options.flags || "r"; + return Q.when(this.open(path, options), function (stream) { + return stream.read(); + }, function (error) { + error.message = "Can't read " + path + " because " + error.message; + error.path = path; + error.flags = flags; + error.charset = charset; + throw error; + }); + }; + + /** + * Write content to a file, overwriting the existing content. + * @param {String} path Path to the file. + * @param {String || Buffer} content + * @param {String} [options.flags] The mode to open the file with. + * @param {String} [options.charset] The charset to open the file with. + * @param {Object} [options] An object with options. + * @returns {Promise * Undefined} a promise that resolves + * when the writing is complete. + */ + exports.write = function (path, content, flags, charset, options) { + var self = this; + if (typeof flags === "object") { + options = flags; + } else if (typeof charset === "object") { + options = charset; + options.flags = flags; + } else { + options = options || {}; + options.flags = flags; + options.charset = charset; + } + flags = options.flags || "w"; + if (flags.indexOf("b") !== -1) { + if (!(content instanceof Buffer)) { + content = new Buffer(content); + } + } else if (content instanceof Buffer) { + flags += "b"; + } + options.flags = flags; + return Q.when(self.open(path, options), function (stream) { + return Q.when(stream.write(content), function () { + return stream.close(); + }); + }); + }; + + /** + * Append content to the end of a file. + * @param {String} path Path to the file. + * @param {String || Buffer} content + * @param {String} [options.flags] The mode to open the file with. + * @param {String} [options.charset] The charset to open the file with. + * @param {Object} [options] An object with options. + * @returns {Promise * Undefined} a promise that resolves + * when the writing is complete. + */ + exports.append = function (path, content, flags, charset, options) { + var self = this; + if (typeof flags === "object") { + options = flags; + } else if (typeof charset === "object") { + options = charset; + options.flags = flags; + } else { + options = options || {}; + options.flags = flags; + options.charset = charset; + } + flags = options.flags || "a"; + if (content instanceof Buffer) { + flags += "b"; + } + options.flags = flags; + return Q.when(self.open(path, options), function (stream) { + return Q.when(stream.write(content), function () { + return stream.close(); + }); + }); + }; + + exports.move = function (source, target) { + var self = this; + return this.rename(source, target) + .catch(function (error) { + if (error.crossDevice) { + return self.copyTree(source, target) + .then(function () { + return self.removeTree(source); + }); + } else { + throw error; + } + }); + }; + + exports.copy = function (source, target) { + var self = this; + return Q.when(self.stat(source), function (stat) { + var mode = stat.node.mode; + return Q.all([ + self.open(source, {flags: "rb"}), + self.open(target, {flags: "wb", mode: mode}) + ]); + }) + .spread(function (reader, writer) { + return Q.when(reader.forEach(function (block) { + return writer.write(block); + }), function () { + return Q.all([ + reader.close(), + writer.close() + ]); + }); + }); + }; + + exports.copyTree = function (source, target) { + var self = this; + return Q.when(self.stat(source), function (stat) { + if (stat.isFile()) { + return self.copy(source, target); + } else if (stat.isDirectory()) { + return self.exists(target).then(function (targetExists) { + function copySubTree() { + return Q.when(self.list(source), function (list) { + return Q.all(list.map(function (child) { + return self.copyTree( + self.join(source, child), + self.join(target, child) + ); + })); + }); + } + if (targetExists) { + return copySubTree(); + } else { + return Q.when(self.makeDirectory(target, stat.node.mode), copySubTree); + } + }); + } else if (stat.isSymbolicLink()) { + // TODO copy the link and type with readPath (but what about + // Windows junction type?) + return self.symbolicCopy(source, target); + } + }); + }; + + exports.listTree = function (basePath, guard) { + var self = this; + basePath = String(basePath || ''); + if (!basePath) + basePath = "."; + guard = guard || function () { + return true; + }; + var stat = self.stat(basePath); + return Q.when(stat, function (stat) { + var paths = []; + var mode; // true:include, false:exclude, null:no-recur + try { + var include = guard(basePath, stat); + } catch (exception) { + return Q.reject(exception); + } + return Q.when(include, function (include) { + if (include) { + paths.push([basePath]); + } + if (include !== null && stat.isDirectory()) { + return Q.when(self.list(basePath), function (children) { + paths.push.apply(paths, children.map(function (child) { + var path = self.join(basePath, child); + return self.listTree(path, guard); + })); + return paths; + }); + } else { + return paths; + } + }); + }, function noSuchFile(reason) { + return []; + }).then(Q.all).then(concat); + }; + + exports.listDirectoryTree = function (path) { + return this.listTree(path, function (path, stat) { + return stat.isDirectory(); + }); + }; + + exports.makeTree = function (path, mode) { + path = String(path); + var self = this; + var parts = self.split(path); + var at = []; + if (self.isAbsolute(path)) { + // On Windows use the root drive (e.g. "C:"), on *nix the first + // part is the falsey "", and so use the ROOT ("/") + at.push(parts.shift() || self.ROOT); + } + return parts.reduce(function (parent, part) { + return Q.when(parent, function () { + at.push(part); + var parts = self.join(at) || "."; + var made = self.makeDirectory(parts, mode); + return Q.when(made, null, function rejected(error) { + // throw away errors for already made directories + if (error.exists) { + return; + } else { + throw error; + } + }); + }); + }, undefined); + }; + + exports.removeTree = function (path) { + var self = this; + return Q.when(self.statLink(path), function (stat) { + if (stat.isSymbolicLink()) { + return self.remove(path); + } else if (stat.isDirectory()) { + return self.list(path) + .then(function (list) { + // asynchronously remove every subtree + return Q.all(list.map(function (name) { + return self.removeTree(self.join(path, name)); + })) + .then(function () { + return self.removeDirectory(path); + }); + }); + } else { + return self.remove(path); + } + }); + }; + + exports.symbolicCopy = function (source, target, type) { + var self = this; + return Q.when(self.relative(target, source), function (relative) { + return self.symbolicLink(target, relative, type || "file"); + }); + }; + + exports.exists = function (path) { + return Q.when(this.stat(path), function () { + return true; + }, function () { + return false; + }); + }; + + exports.isFile = function (path) { + return Q.when(this.stat(path), function (stat) { + return stat.isFile(); + }, function (reason) { + return false; + }); + }; + + exports.isDirectory = function (path) { + return Q.when(this.stat(path), function (stat) { + return stat.isDirectory(); + }, function (reason) { + return false; + }); + }; + + exports.isSymbolicLink = function (path) { + return Q.when(this.statLink(path), function (stat) { + return stat.isSymbolicLink(); + }, function (reason) { + return false; + }); + }; + + exports.lastModified = function (path) { + var self = this; + return self.stat(path).invoke('lastModified'); + }; + + exports.lastAccessed = function (path) { + var self = this; + return self.stat(path).invoke('lastAccessed'); + }; + + exports.absolute = function (path) { + if (this.isAbsolute(path)) + return this.normal(path); + return this.join(workingDirectory(), path); + }; + + exports.relative = function (source, target) { + var self = this; + return Q.when(this.isDirectory(source), function (isDirectory) { + if (isDirectory) { + return self.relativeFromDirectory(source, target); + } else { + return self.relativeFromFile(source, target); + } + }); + }; + + exports.relativeFromFile = function (source, target) { + var self = this; + source = self.absolute(source); + target = self.absolute(target); + source = source.split(self.SEPARATORS_RE()); + target = target.split(self.SEPARATORS_RE()); + source.pop(); + while ( + source.length && + target.length && + target[0] == source[0] + ) { + source.shift(); + target.shift(); + } + while (source.length) { + source.shift(); + target.unshift(".."); + } + return target.join(self.SEPARATOR); + }; + + exports.relativeFromDirectory = function (source, target) { + if (!target) { + target = source; + source = workingDirectory(); + } + source = this.absolute(source); + target = this.absolute(target); + source = source.split(this.SEPARATORS_RE()); + target = target.split(this.SEPARATORS_RE()); + if (source.length === 2 && source[1] === "") + source.pop(); + while ( + source.length && + target.length && + target[0] == source[0] + ) { + source.shift(); + target.shift(); + } + while (source.length) { + source.shift(); + target.unshift(".."); + } + return target.join(this.SEPARATOR); + }; + + exports.contains = function (parent, child) { + var i, ii; + parent = this.absolute(parent); + child = this.absolute(child); + parent = parent.split(this.SEPARATORS_RE()); + child = child.split(this.SEPARATORS_RE()); + if (parent.length === 2 && parent[1] === "") + parent.pop(); + if (parent.length > child.length) + return false; + for (i = 0, ii = parent.length; i < ii; i++) { + if (parent[i] !== child[i]) + break; + } + return i == ii; + }; + + exports.reroot = reroot; + function reroot(path) { + var self = this; + path = path || this.ROOT; + return RootFs(self, path); + } + + exports.toObject = function (path) { + var self = this; + var list = self.listTree(path || "", function (path, stat) { + return stat.isFile(); + }); + return Q.when(list, function (list) { + var tree = {}; + return Q.all(list.map(function (path) { + return Q.when(self.read(path, "rb"), function (content) { + tree[path] = content; + }); + })).then(function () { + return tree; + }); + }); + }; + + exports.merge = function (fss) { + var tree = {}; + var done; + fss.forEach(function (fs) { + done = Q.when(done, function () { + return fs.listTree("", function (path, stat) { + return stat.isFile(); + }) + .then(function (list) { + return Q.all(list.map(function (path) { + return Q.when(fs.read(path, "rb"), function (content) { + tree[path] = content; + }); + })); + }); + }); + }) + return Q.when(done, function () { + return MockFs(tree); + }); + }; + + exports.Stats = Stats; + function Stats(nodeStat) { + this.node = nodeStat; + this.size = nodeStat.size; + } + + var stats = [ + "isDirectory", + "isFile", + "isBlockDevice", + "isCharacterDevice", + "isSymbolicLink", + "isFIFO", + "isSocket" + ]; + + stats.forEach(function (name) { + Stats.prototype[name] = function () { + return this.node[name](); + }; + }); + + Stats.prototype.lastModified = function () { + return new Date(this.node.mtime); + }; + + Stats.prototype.lastAccessed = function () { + return new Date(this.node.atime); + }; + +} + diff --git a/core/promise-io/fs-mock.js b/core/promise-io/fs-mock.js new file mode 100644 index 0000000000..25edd790c4 --- /dev/null +++ b/core/promise-io/fs-mock.js @@ -0,0 +1,555 @@ + +var Q = require("q-bluebird"); +var Boot = require("./fs-boot"); +var Common = require("./fs-common"); +var BufferStream = require("./buffer-stream"); +var Reader = require("./reader"); +//Benoit: commmented out while bringing projects like mr, collections, frb ... in montage as it caused shim-array to be executed twice, +//once by node native require and a second one by mr. Set are standard now and this just uses .has() and .add() +//so will do for now, but something changed and it's time to bring mr in montage.js and avoid current duplication and entanglements... +// var Set = require("collections/set"); + +module.exports = MockFs; + +function MockFs(files, workingDirectory) { + if (!(this instanceof MockFs)) { + return new MockFs(files, workingDirectory); + } + this._root = new DirectoryNode(this, "/"); + + function init() { + // construct a file tree + } + + Common.update(this, function () { + return workingDirectory; + }); + + workingDirectory = workingDirectory || this.ROOT; + if (files) { + this._init(files); + } +} + +MockFs.prototype = Object.create(Boot); + +MockFs.prototype._init = function (files, tree) { + tree = tree || this.ROOT; + Object.keys(files).forEach(function (path) { + var content = files[path]; + path = this.join(tree, path); + var directory = this.directory(path); + var base = this.base(path); + var directoryNode = this._root._walk(directory, true); + var fileNode = new FileNode(this); + if (!(content instanceof Buffer)) { + if (typeof content === "object") { + // make directory + this._root._walk(path, true); + // make content + this._init(content, path); + return; + } else { + content = new Buffer(String(content), "utf-8"); + } + } + directoryNode._entries[base] = fileNode; + fileNode._chunks = [content]; + }, this); +}; + +MockFs.prototype.list = function (path) { + var self = this; + return Q.fcall(function () { + path = self.absolute(path); + var node = self._root._walk(path)._follow(path); + if (!node.isDirectory()) { + new Error("Can't list non-directory: " + JSON.stringify(path)); + } + return Object.keys(node._entries).sort(); + }); +}; + +MockFs.prototype.open = function (path, flags, charset, options) { + var self = this; + return Q.fcall(function () { + path = self.absolute(path); + var directory = self.directory(path); + var base = self.base(path); + var node = self._root._walk(directory); + if (!node.isDirectory()) { + throw new Error("Can't find " + path + " because " + directory + " is not a directory"); + } + if (typeof flags == "object") { + options = flags; + flags = options.flags; + charset = options.charset; + } else { + options = options || {}; + } + flags = flags || "r"; + var binary = flags.indexOf("b") >= 0; + var write = flags.indexOf("w") >= 0; + var append = flags.indexOf("a") >= 0; + if (!binary) { + charset = charset || "utf-8"; + } + if (write || append) { + if (!node._entries[base]) { + node._entries[base] = new FileNode(this); + if ("mode" in options) { + node._entries[base].mode = options.mode; + } + } + var fileNode = node._entries[base]._follow(path); + if (!fileNode.isFile()) { + throw new Error("Can't write non-file " + path); + } + fileNode.mtime = Date.now(); + fileNode.atime = Date.now(); + if (!append) { + fileNode._chunks.length = 0; + } + return new BufferStream(fileNode._chunks, charset); + } else { // read + if (!node._entries[base]) { + throw new Error("Can't read non-existant " + path); + } + var fileNode = node._entries[base]._follow(path); + if (!fileNode.isFile()) { + throw new Error("Can't read non-file " + path); + } + fileNode.atime = Date.now(); + if ("begin" in options && "end" in options) { + return new BufferStream( + [ + Reader.join(fileNode._chunks) + .slice(options.begin, options.end) + ], + charset + ); + } else { + return new BufferStream(fileNode._chunks, charset); + } + } + }); +}; + +MockFs.prototype.remove = function (path) { + var self = this; + return Q.fcall(function () { + path = self.absolute(path); + var directory = self.directory(path); + var name = self.base(path); + var node = self._root._walk(directory); + if (!node.isDirectory()) { + throw new Error("Can't remove file from non-directory: " + path); + } + if (!node._entries[name]) { + throw new Error("Can't remove non-existant file: " + path); + } + if (node._entries[name].isDirectory()) { + throw new Error("Can't remove directory. Use removeDirectory: " + path); + } + delete node._entries[name]; + }); +}; + +MockFs.prototype.makeDirectory = function (path, mode) { + var self = this; + return Q.fcall(function () { + path = self.absolute(path); + var directory = self.directory(path); + var name = self.base(path); + var node = self._root._walk(directory); + if (!node.isDirectory()) { + var error = new Error("Can't make directory in non-directory: " + path); + error.code = "EEXISTS"; + error.exists = true; + throw error; + } + if (node._entries[name]) { + var error = new Error("Can't make directory. Entry exists: " + path); + error.code = "EISDIR"; + error.exists = true; + error.isDirectory = true; + throw error; + } + node._entries[name] = new DirectoryNode(self); + if (mode) { + node._entries[name].mode = mode; + } + }); +}; + +MockFs.prototype.removeDirectory = function (path) { + var self = this; + return Q.fcall(function () { + path = self.absolute(path); + var directory = self.directory(path); + var name = self.base(path); + var node = self._root._walk(directory); + if (!node.isDirectory()) { + throw new Error("Can't remove directory from non-directory: " + path); + } + if (!node._entries[name]) { + throw new Error("Can't remove non-existant directory: " + path); + } + if (!node._entries[name].isDirectory()) { + throw new Error("Can't remove non-directory: " + path); + } + delete node._entries[name]; + }); +}; + +MockFs.prototype.stat = function (path) { + var self = this; + return Q.fcall(function () { + path = self.absolute(path); + return new self.Stats(self._root._walk(path)._follow(path)); + }); +}; + +MockFs.prototype.statLink = function (path) { + var self = this; + return Q.fcall(function () { + path = self.absolute(path); + return new self.Stats(self._root._walk(path)); + }); +}; + +MockFs.prototype.link = function (source, target) { + var self = this; + return Q.fcall(function () { + source = self.absolute(source); + target = self.absolute(target); + var sourceNode = self._root._walk(source)._follow(source); + if (!sourceNode.isFile()) { + throw new Error("Can't link non-file: " + source); + } + var directory = self.directory(target); + var base = self.base(target); + var targetNode = self._root._walk(directory)._follow(directory); + if (!targetNode.isDirectory()) { + throw new Error("Can't create link in non-directory: " + target); + } + if (targetNode._entries[base] && targetNode._entries[base].isDirectory()) { + throw new Error("Can't overwrite existing directory with hard link: " + target); + } + targetNode._entries[base] = sourceNode; + }); +}; + +MockFs.prototype.symbolicLink = function (target, relative, type) { + var self = this; + return Q.fcall(function () { + target = self.absolute(target); + var directory = self.directory(target); + var base = self.base(target); + var node = self._root._walk(directory); + if (node._entries[base] && node._entries[base].isDirectory()) { + throw new Error("Can't overwrite existing directory with symbolic link: " + target); + } + node._entries[base] = new LinkNode(self, relative); + }); +}; + +MockFs.prototype.chown = function (path, owner) { + var self = this; + return Q.fcall(function () { + path = self.absolute(path); + self._root._walk(path)._follow(path)._owner = owner; + }); +}; + +MockFs.prototype.chmod = function (path, mode) { + var self = this; + return Q.fcall(function () { + path = self.absolute(path); + self._root._walk(path)._follow(path).mode = mode; + }); +}; + +MockFs.prototype.rename = function (source, target) { + var self = this; + return Q.fcall(function () { + source = self.absolute(source); + target = self.absolute(target); + + var sourceDirectory = self.directory(source); + var sourceDirectoryNode = self._root._walk(sourceDirectory)._follow(sourceDirectory); + var sourceName = self.base(source); + var sourceNode = sourceDirectoryNode._entries[sourceName]; + + if (!sourceNode) { + var error = new Error("Can't copy non-existent file: " + source); + error.code = "ENOENT"; + throw error; + } + + sourceNode = sourceNode._follow(source); + + // check again after following symbolic links + if (!sourceNode) { + var error = new Error("Can't copy non-existent file: " + source); + error.code = "ENOENT"; + throw error; + } + + var targetDirectory = self.directory(target); + var targetDirectoryNode = self._root._walk(targetDirectory)._follow(targetDirectory); + var targetName = self.base(target); + var targetNode = targetDirectoryNode._entries[targetName]; // might not exist, not followed + + if (targetNode) { + targetNode = targetNode._follow(target); + } + + if (targetNode && targetNode.isDirectory()) { + var error = new Error("Can't copy over existing directory: " + target); + error.code = "EISDIR"; + throw error; + } + + // do not copy over self, even with symbolic links to confuse the issue + if (targetNode === sourceNode) { + return; + } + + targetDirectoryNode._entries[targetName] = sourceNode; + delete sourceDirectoryNode._entries[sourceName]; + }); +}; + +MockFs.prototype.readLink = function (path) { + var self = this; + return Q.fcall(function () { + path = self.absolute(path); + var node = self._root._walk(path); + if (!self.isSymbolicLink()) { + throw new Error("Can't read non-symbolic link: " + path); + } + return node._link; + }); +}; + +MockFs.prototype.canonical = function (path) { + var self = this; + return Q.fcall(function () { + path = self.absolute(path); + return self._root._canonical(path); + }); +}; + +MockFs.mock = mock; +function mock(fs, root) { + return Q.when(fs.listTree(root), function (list) { + var tree = {}; + return Q.all(list.map(function (path) { + var actual = fs.join(root, path); + var relative = fs.relativeFromDirectory(root, actual); + return Q.when(fs.stat(actual), function (stat) { + if (stat.isFile()) { + return Q.when(fs.read(path, "rb"), function (content) { + tree[relative] = content; + }); + } + }); + })).then(function () { + return MockFs(tree); + }); + }); +} + +function Node(fs) { + if (!fs) + throw new Error("FS required argument"); + this._fs = fs; + this.atime = this.mtime = Date.now(); + this.mode = parseInt("0644", 8); + this._owner = null; +} + +Node.prototype._walk = function (path, make, via) { + var parts = this._fs.split(path); + if (this._fs.isAbsolute(path)) { + parts.shift(); + return this._fs._root._walkParts(parts, make, this._fs.ROOT); + } else { + return this._walkParts(parts, make, via || this._fs.ROOT); + } +}; + +Node.prototype._walkParts = function (parts, make, via) { + if (parts.length === 0) { + return this; + } else { + var part = parts.shift(); + if (part === "") { + return this._walkParts(parts, make, this._fs.join(via, part)); + } else { + var error = new Error("Can't find " + JSON.stringify(this._fs.resolve(part, this._fs.join(parts))) + " via " + JSON.stringify(via)); + error.code = "ENOENT"; + throw error; + } + } +}; + +Node.prototype._canonical = function (path) { + if (!this._fs.isAbsolute(path)) { + throw new Error("Path must be absolute for _canonical: " + path); + } + var parts = this._fs.split(path); + parts.shift(); + var via = this._fs.ROOT; + return via + this._fs._root._canonicalParts(parts, via); +}; + +Node.prototype._canonicalParts = function (parts, via) { + if (parts.length === 0) { + return via; + } + return this._fs.join(via, this._fs.join(parts)); +}; + +Node.prototype._follow = function () { + return this; +}; + +Node.prototype._touch = function () { + this.mtime = Date.now(); +}; + +var stats = [ + "isDirectory", + "isFile", + "isBlockDevice", + "isCharacterDevice", + "isSymbolicLink", + "isFIFO", + "isSocket" +]; + +stats.forEach(function (name) { + Node.prototype[name] = function () { + return false; + }; +}); + +Node.prototype.lastAccessed = function () { + return this.atime; +}; + +Node.prototype.lastModified = function () { + return this.mtime; +}; + +function FileNode(fs) { + Node.call(this, fs); + this._chunks = []; +} + +FileNode.prototype = Object.create(Node.prototype); + +FileNode.prototype.isFile = function () { + return true; +}; + +Object.defineProperty(FileNode.prototype, "size", { + configurable: true, + enumerable: true, + get: function () { + return this._chunks.reduce(function (size, chunk) { + return size += chunk.length; + }, 0); + } +}); + +function DirectoryNode(fs) { + Node.call(this, fs); + this._entries = Object.create(null); + this.mode = parseInt("0755", 8); +} + +DirectoryNode.prototype = Object.create(Node.prototype); + +DirectoryNode.prototype.isDirectory = function () { + return true; +}; + +DirectoryNode.prototype._walkParts = function (parts, make, via) { + via = via || this._fs.ROOT; + if (parts.length === 0) { + return this; + } + var part = parts.shift(); + if (part === "") { + return this._walkParts(parts, make, this._fs.join(via, part)); + } + if (!this._entries[part]) { + if (make) { + this._entries[part] = new DirectoryNode(this._fs); + } else { + var error = new Error("Can't find " + JSON.stringify(this._fs.join(parts)) + " via " + JSON.stringify(via)); + error.code = "ENOENT"; + throw error; + + } + } + return this._entries[part]._walkParts(parts, make, this._fs.join(via, part)); +}; + +DirectoryNode.prototype._canonicalParts = function (parts, via) { + if (parts.length === 0) { + return via; + } + var part = parts.shift(); + if (part === "") { + return via; + } + if (via === this._fs.ROOT) { + via = ""; + } + if (!this._entries[part]) { + return this._fs.join(via, part, this._fs.join(parts)); + } + return this._entries[part]._canonicalParts( + parts, + this._fs.join(via, part) + ); +}; + +function LinkNode(fs, link) { + Node.call(this, fs); + this._link = link; +} + +LinkNode.prototype = Object.create(Node.prototype); + +LinkNode.prototype.isSymbolicLink = function () { + return true; +}; + +LinkNode.prototype._follow = function (via, memo) { + memo = memo || Set(); + if (memo.has(this)) { + var error = new Error("Can't follow symbolic link cycle at " + JSON.stringify(via)); + error.code = "ELOOP"; + throw error; + } + memo.add(this); + var link = this._fs.join(via, "..", this._link); + return this._walk(link, null, "")._follow(link, memo); +}; + +LinkNode.prototype._canonicalParts = function (parts, via) { + return this._fs.relativeFromDirectory(this._fs.ROOT, + this._fs._root._canonical( + this._fs.absolute(this._fs.join(via, "..", this._link)) + ) + ); +}; + +// cycle breaking +var FS = require("./fs"); + diff --git a/core/promise-io/fs-root.js b/core/promise-io/fs-root.js new file mode 100644 index 0000000000..3ccce3adc5 --- /dev/null +++ b/core/promise-io/fs-root.js @@ -0,0 +1,145 @@ + +var Q = require("q-bluebird"); +var BOOT = require("./fs-boot"); +var COMMON = require("./fs-common"); + +module.exports = RootFs; + +function RootFs(outer, root) { + var inner = Object.create(BOOT); + + function attenuate(path) { + + // the machinations of projecting a path inside a + // subroot + var actual; + // if it's absolute, we want the path relative to + // the root of the inner file system + if (outer.isAbsolute(path)) { + actual = outer.relativeFromDirectory(outer.ROOT, path); + } else { + actual = path; + } + // we join the path onto the root of the inner file + // system so that parent references from the root + // return to the root, emulating standard unix + // behavior + actual = outer.join(outer.ROOT, actual); + // then we reconstruct the path relative to the + // inner root + actual = outer.relativeFromDirectory(outer.ROOT, actual); + // and rejoin it on the outer root + actual = outer.join(root, actual); + // and find the corresponding real path + return outer.canonical(actual) + .then(function (actual) { + return actual; + }, function () { + return actual; + }).then(function (actual) { + // and verify that the outer canonical path is + // actually inside the inner canonical path, to + // prevent break-outs + if (outer.contains(root, actual)) { + return { + "inner": outer.join(outer.ROOT, outer.relativeFromDirectory(root, actual)), + "outer": actual + }; + } else { + return Q.reject("Can't find: " + JSON.stringify(path)); + } + }); + } + + function workingDirectory() { + return outer.ROOT; + } + + COMMON.update(inner, workingDirectory); + + inner.list = function (path) { + return attenuate(path).then(function (path) { + return outer.list(path.outer); + }).then(null, function (reason) { + return Q.reject("Can't list " + JSON.stringify(path)); + }); + }; + + inner.open = function (path, flags, charset) { + return attenuate(path).then(function (path) { + return outer.open(path.outer, flags, charset); + }).then(null, function (reason) { + return Q.reject("Can't open " + JSON.stringify(path)); + }); + }; + + inner.stat = function (path) { + return attenuate(path).then(function (path) { + return outer.stat(path.outer); + }).then(null, function (reason) { + return Q.reject("Can't stat " + JSON.stringify(path)); + }); + }; + + inner.statLink = function (path) { + return attenuate(path).then(function (path) { + return outer.statLink(path.outer); + }).then(null, function (reason) { + return Q.reject("Can't statLink " + JSON.stringify(path)); + }); + }; + + inner.canonical = function (path) { + return attenuate(path).then(function (path) { + return path.inner; + }).then(null, function (reason) { + return Q.reject("Can't find canonical of " + JSON.stringify(path)); + }); + }; + + inner.makeDirectory = function (path) { + return attenuate(path).then(function (path) { + return outer.makeDirectory(path.outer); + }).catch(function (error) { + throw new Error("Can't make directory " + JSON.stringify(path)); + }); + }; + + inner.removeDirectory = function (path) { + return attenuate(path).then(function (path) { + return outer.removeDirectory(path.outer); + }).catch(function (error) { + throw new Error("Can't remove directory " + JSON.stringify(path)); + }); + }; + + inner.remove = function (path) { + return attenuate(path).then(function (path) { + return outer.remove(path.outer); + }).catch(function (error) { + throw new Error("Can't remove " + JSON.stringify(path)); + }); + }; + + inner.makeTree = function (path) { + return attenuate(path).then(function (path) { + return outer.makeTree(path.outer); + }).catch(function (error) { + throw new Error("Can't make tree " + JSON.stringify(path)); + }); + }; + + inner.removeTree = function (path) { + return attenuate(path).then(function (path) { + return outer.removeTree(path.outer); + }).catch(function (error) { + throw new Error("Can't remove tree " + JSON.stringify(path)); + }); + }; + + return Q.when(outer.canonical(root), function (_root) { + root = _root; + return inner; + }); +} + diff --git a/core/promise-io/fs.js b/core/promise-io/fs.js new file mode 100644 index 0000000000..cad22b3657 --- /dev/null +++ b/core/promise-io/fs.js @@ -0,0 +1,355 @@ +/** + * An asynchronous local file system API, based on a subset + * of the `narwhal/fs` API and the `narwhal/promise` API, + * such that the method names are the same but some return + * values are promises instead of fully resolved values. + * @module + */ + +/*whatsupdoc*/ + +var FS = require("fs"); // node +var Q = require("q-bluebird"); +var Reader = require("./reader"); +var Writer = require("./writer"); +var Common = require("./fs-common"); +var Mock = require("./fs-mock"); +var Root = require("./fs-root"); + +Common.update(exports, process.cwd); +exports.Mock = Mock; +exports.Root = Root; + +// facilitates AIMD (additive increase, multiplicative decrease) for backing off +var backOffDelay = 0; +var backOffFactor = 1.0001; +function dampen(wrapped, thisp) { + var retry = function () { + var args = arguments; + var ready = backOffDelay ? Q.delay(backOffDelay) : Q.resolve(); + return ready.then(function () { + return Q.when(wrapped.apply(thisp, args), function (stream) { + backOffDelay = Math.max(0, backOffDelay - 1); + return stream; + }, function (error) { + if (error.code === "EMFILE") { + backOffDelay = (backOffDelay + 1) * backOffFactor; + return retry.apply(null, args); + } else { + throw error; + } + }); + }); + }; + return retry; +} + +/** + * @param {String} path + * @param {Object} options (flags, mode, bufferSize, charset, begin, end) + * @returns {Promise * Stream} a stream from the `q-io` module. + */ +exports.open = dampen(function (path, flags, charset, options) { + var self = this; + if (typeof flags == "object") { + options = flags; + flags = options.flags; + charset = options.charset; + } + options = options || {}; + flags = flags || "r"; + var nodeFlags = flags.replace(/b/g, "") || "r"; + var nodeOptions = { + "flags": nodeFlags + }; + if ("bufferSize" in options) { + nodeOptions.bufferSize = options.bufferSize; + } + if ("mode" in options) { + nodeOptions.mode = options.mode; + } + if ("begin" in options) { + nodeOptions.start = options.begin; + nodeOptions.end = options.end - 1; + } + if (flags.indexOf("b") >= 0) { + if (charset) { + throw new Error("Can't open a binary file with a charset: " + charset); + } + } else { + charset = charset || 'utf-8'; + } + if (flags.indexOf("w") >= 0 || flags.indexOf("a") >= 0) { + var stream = FS.createWriteStream(String(path), nodeOptions); + return Writer(stream, charset); + } else { + var stream = FS.createReadStream(String(path), nodeOptions); + return Reader(stream, charset); + } +}); + +exports.remove = function (path) { + path = String(path); + var done = Q.defer(); + FS.unlink(path, function (error) { + if (error) { + error.message = "Can't remove " + JSON.stringify(path) + ": " + error.message; + done.reject(error); + } else { + done.resolve(); + } + }); + return done.promise; +}; + +exports.rename = function (source, target) { + source = String(source); + target = String(target); + return Q.ninvoke(FS, "rename", source, target) + .fail(function (error) { + if (error.code === "EXDEV") { + error.message = "source and target are on different devices: " + error.message; + error.crossDevice = true; + } + error.message = ( + "Can't move " + JSON.stringify(source) + " to " + + JSON.stringify(target) + " because " + error.message + ); + throw error; + }); +}; + +exports.makeDirectory = function (path, mode) { + path = String(path); + var done = Q.defer(); + if (typeof mode === "string") { + mode = parseInt(mode, 8); + } else if (mode === void 0) { + mode = parseInt('755', 8); + } + FS.mkdir(path, mode, function (error) { + if (error) { + if (error.code === "EISDIR") { + error.exists = true; + error.isDirectory = true; + error.message = "directory already exists: " + error.message; + } + if (error.code === "EEXIST") { + error.exists = true; + error.message = "file exists at that path: " + error.message; + } + error.message = "Can't makeDirectory " + JSON.stringify(path) + " with mode " + mode + ": " + error.message; + done.reject(error); + } else { + done.resolve(); + } + }); + return done.promise; +}; + +exports.removeDirectory = function (path) { + path = String(path); + var done = Q.defer(); + FS.rmdir(path, function (error) { + if (error) { + error.message = "Can't removeDirectory " + JSON.stringify(path) + ": " + error.message; + done.reject(error); + } else { + done.resolve(); + } + }); + return done.promise; +}; + +/** + */ +exports.list = dampen(function (path) { + path = String(path); + var result = Q.defer(); + FS.readdir(path, function (error, list) { + if (error) { + error.message = "Can't list " + JSON.stringify(path) + ": " + error.message; + return result.reject(error); + } else { + result.resolve(list); + } + }); + return result.promise; +}); + +/** + * @param {String} path + * @returns {Promise * Stat} + */ +exports.stat = function (path) { + var self = this; + path = String(path); + var done = Q.defer(); + try { + FS.stat(path, function (error, stat) { + if (error) { + error.message = "Can't stat " + JSON.stringify(path) + ": " + error; + done.reject(error); + } else { + done.resolve(new self.Stats(stat)); + } + }); + } catch (error) { + done.reject(error); + } + return done.promise; +}; + +exports.statLink = function (path) { + path = String(path); + var done = Q.defer(); + try { + FS.lstat(path, function (error, stat) { + if (error) { + error.message = "Can't statLink " + JSON.stringify(path) + ": " + error.message; + done.reject(error); + } else { + done.resolve(stat); + } + }); + } catch (error) { + done.reject(error); + } + return done.promise; +}; + +exports.statFd = function (fd) { + fd = Number(fd); + var done = Q.defer(); + try { + FS.fstat(fd, function (error, stat) { + if (error) { + error.message = "Can't statFd file descriptor " + JSON.stringify(fd) + ": " + error.message; + done.reject(error); + } else { + done.resolve(stat); + } + }); + } catch (error) { + done.reject(error); + } + return done.promise; +}; + +exports.link = function (source, target) { + source = String(source); + target = String(target); + var done = Q.defer(); + try { + FS.link(source, target, function (error) { + if (error) { + error.message = "Can't link " + JSON.stringify(source) + " to " + JSON.stringify(target) + ": " + error.message; + done.reject(error); + } else { + done.resolve(); + } + }); + } catch (error) { + done.reject(error); + } + return done.promise; +}; + +// this lookup table translates the link types that Q-IO accepts (which have +// been normalized to full words to be consistent with the naming convention) +var linkTypes = { + "file": "file", + "directory": "dir", + "junction": "junction" +}; + +exports.symbolicLink = function (target, relative, type) { + if (!linkTypes.hasOwnProperty(type)) { + console.warn(new Error("For Windows compatibility, symbolicLink must be called with a type argument \"file\", \"directory\", or \"junction\"")); + } + type = linkTypes[type]; + target = String(target); + relative = String(relative); + var done = Q.defer(); + try { + FS.symlink(relative, target, type || 'file', function (error) { + if (error) { + error.message = "Can't create symbolicLink " + JSON.stringify(target) + " to relative location " + JSON.stringify(relative) + ": " + error.message; + done.reject(error); + } else { + done.resolve(); + } + }); + } catch (error) { + done.reject(error); + } + return done.promise; +}; + +exports.chown = function (path, uid, gid) { + path = String(path); + var done = Q.defer(); + try { + FS.chown(path, uid, gid, function (error) { + if (error) { + error.message = "Can't chown (change owner) of " + JSON.stringify(path) + " to user " + JSON.stringify(uid) + " and group " + JSON.stringify(gid) + ": " + error.message; + done.reject(error); + } else { + done.resolve(); + } + }); + } catch (error) { + done.reject(error); + } + return done.promise; +}; + +exports.chmod = function (path, mode) { + path = String(path); + mode = String(mode); + var done = Q.defer(); + try { + FS.chmod(path, mode, function (error) { + if (error) { + error.message = "Can't chmod (change permissions mode) of " + JSON.stringify(path) + " to (octal number) " + mode.toString(8) + ": " + error.message; + done.reject(error); + } else { + done.resolve(); + } + }); + } catch (error) { + done.reject(error); + } + return done.promise; +}; + +exports.canonical = function (path) { + var result = Q.defer(); + FS.realpath(path, function (error, canonicalPath) { + if (error) { + error.message = "Can't get canonical path of " + JSON.stringify(path) + " by way of C realpath: " + error.message; + result.reject(error); + } else { + result.resolve(canonicalPath); + } + }); + return result.promise; +}; + +exports.readLink = function (path) { + var result = Q.defer(); + FS.readlink(path, function (error, path) { + if (error) { + error.message = "Can't get link from " + JSON.stringify(path) + " by way of C readlink: " + error.message; + result.reject(error); + } else { + result.resolve(path); + } + }); + return result.promise; +}; + +exports.mock = function (path) { + return Mock.mock(this, path); +}; + diff --git a/core/promise-io/fs2http.js b/core/promise-io/fs2http.js new file mode 100644 index 0000000000..d54b31cf75 --- /dev/null +++ b/core/promise-io/fs2http.js @@ -0,0 +1,65 @@ + +var Q = require("q-bluebird"); +var HTTP = require("./http"); +var URL = require("url"); + +exports.Client = Client; +function Client(fs) { + var self = Object.create(Client.prototype); + + self.request = function (request) { + return Q.when(request, function (request) { + request = HTTP.normalizeRequest(request); + var url = URL.parse(request.url); + if (url.protocol !== "file:") { + return { + status: 404, + headers: {}, + body: ["Can't access protocol " + url.protocol] + }; + } else { + var path = url.pathname; + return fs.open(path, { + charset: request.charset + }).then(function (body) { + return { + status: 200, + headers: {}, + body: body + }; + }); + } + }); + }; + + self.read = function (request, qualifier) { + qualifier = qualifier || function (response) { + return response.status === 200; + }; + return Q.when(exports.request(request), function (response) { + if (!qualifier(response)){ + var error = new Error("HTTP request failed with code " + response.status); + error.response = response; + throw error; + } + return Q.invoke(response.body, "read"); + }); + }; + + return self; +} + +exports.request = function (request) { + return Q.fcall(require.async || require, "./fs") + .then(function (fs) { + return Client(fs).request(request); + }); +}; + +exports.read = function (request, qualifier) { + return Q.fcall(require.async || require, "./fs") + .then(function (fs) { + return Client(fs).read(request); + }); +}; + diff --git a/core/promise-io/http-apps.js b/core/promise-io/http-apps.js new file mode 100644 index 0000000000..6ad14e03c6 --- /dev/null +++ b/core/promise-io/http-apps.js @@ -0,0 +1,152 @@ + +/** + * Provides tools for making, routing, adapting, and decorating + * Q-JSGI web applications. + * + * Duck Types + * ---------- + * + * A Q-JSGI _app_ is a function that accepts a request and returns a + * response. The response may be promised. + * + * A Q-JSGI _request_ is an object or a promise for an object that has + * the following properties: + * + * * `method` is the HTTP request method as a string. + * * `path` is a string, guaranteed to start with `"/"`. + * * `headers` is an object mapping lower-case HTTP headers to + * their corresponding values as strings. + * * `body` is a Q-JSGI content body. + * + * A Q-JSGI _response_ is an object or a promise for an object that + * has the following properties: + * + * * `status` is the HTTP response status code as a number. + * * `headers` is an object mapping lower-case HTTP headers to their + * corresponding values as strings. + * * `body` is a Q-JSGI content body. + * + * A Q-JSGI response and request content _body_ can be as simple as an + * array of strings. It can be a promise. In general, it must be an + * object that has a `forEach` method. The `forEach` method accepts a + * `write` function. It goes without saying that `forEach` returns + * `undefined`, but it can return a promise for `undefined` that will + * resolve when it has made all of its `write` calls and the request + * or response can be closed, re-used, or kept alive.. The `forEach` + * function may call `write` with a `String` any number of times. The + * `String` may be promised. + * + * @module + */ + +require("collections/shim"); +var Q = require("q-bluebird"); +var HTTP = require("./http"); +var FS = require("./fs"); +var URL = require("url2"); +var inspect = require("util").inspect; + +exports.Chain = require("./http-apps/chain"); + +var RouteApps = require("./http-apps/route"); +exports.Cap = RouteApps.Cap; +exports.Tap = RouteApps.Tap; +exports.Trap = RouteApps.Trap; +exports.Branch = RouteApps.Branch; + +var ContentApps = require("./http-apps/content"); +exports.Content = ContentApps.Content; +exports.content = ContentApps.content; +exports.ok = ContentApps.ok; +exports.ContentRequest = ContentApps.ContentRequest; +exports.Inspect = ContentApps.Inspect; +exports.ParseQuery = ContentApps.ParseQuery; + +var FsApps = require("./http-apps/fs"); +exports.File = FsApps.File; +exports.FileTree = FsApps.FileTree; +exports.file = FsApps.file; +exports.directory = FsApps.directory; +exports.etag = FsApps.etag; + +exports.ListDirectories = FsApps.ListDirectories; +exports.listDirectory = FsApps.listDirectory; +exports.listDirectoryHtmlFragment = FsApps.listDirectoryHtmlFragment; +exports.listDirectoryText = FsApps.listDirectoryText; +exports.listDirectoryMarkdown = FsApps.listDirectoryMarkdown; +exports.listDirectoryJson = FsApps.listDirectoryJson; +exports.listDirectoryData = FsApps.listDirectoryData; +exports.DirectoryIndex = FsApps.DirectoryIndex; + +var HtmlApps = require("./http-apps/html"); +exports.HandleHtmlFragmentResponses = HtmlApps.HandleHtmlFragmentResponses; +exports.handleHtmlFragmentResponse = HtmlApps.handleHtmlFragmentResponse; +exports.escapeHtml = HtmlApps.escapeHtml; + +var JsonApps = require("./http-apps/json"); +exports.HandleJsonResponses = JsonApps.HandleJsonResponses; +exports.handleJsonResponse = JsonApps.handleJsonResponse; +exports.Json = JsonApps.Json; +exports.json = JsonApps.json; +exports.JsonRequest = JsonApps.JsonRequest; + +var RedirectApps = require("./http-apps/redirect"); +exports.PermanentRedirect = RedirectApps.PermanentRedirect; +exports.PermanentRedirectTree = RedirectApps.PermanentRedirectTree; +exports.TemporaryRedirect = RedirectApps.TemporaryRedirect; +exports.TemporaryRedirectTree = RedirectApps.TemporaryRedirectTree; +exports.Redirect = RedirectApps.Redirect; +exports.RedirectTree = RedirectApps.RedirectTree; +exports.permanentRedirect = RedirectApps.permanentRedirect; +exports.permanentRedirectTree = RedirectApps.permanentRedirectTree; +exports.temporaryRedirect = RedirectApps.temporaryRedirect; +exports.temporaryRedirectTree = RedirectApps.temporaryRedirectTree; +exports.redirectTree = RedirectApps.redirectTree; +exports.redirect = RedirectApps.redirect; +exports.redirectText = RedirectApps.redirectText; +exports.redirectHtml = RedirectApps.redirectHtml; +exports.RedirectTrap = RedirectApps.RedirectTrap; +exports.isRedirect = RedirectApps.isRedirect; + +var ProxyApps = require("./http-apps/proxy"); +exports.Proxy = ProxyApps.Proxy; +exports.ProxyTree = ProxyApps.ProxyTree; + +var NegotiationApps = require("./http-apps/negotiate"); +exports.negotiate = NegotiationApps.negotiate; +exports.Method = NegotiationApps.Method; +exports.ContentType = NegotiationApps.ContentType; +exports.Language = NegotiationApps.Language; +exports.Charset = NegotiationApps.Charset; +exports.Encoding = NegotiationApps.Encoding; +exports.Host = NegotiationApps.Host; +exports.Select = NegotiationApps.Select; + +var StatusApps = require("./http-apps/status"); +exports.statusCodes = StatusApps.statusCodes; +exports.statusMessages = StatusApps.statusMessages; +exports.statusWithNoEntityBody = StatusApps.statusWithNoEntityBody; +exports.appForStatus = StatusApps.appForStatus; +exports.responseForStatus = StatusApps.responseForStatus; +exports.textResponseForStatus = StatusApps.textResponseForStatus; +exports.htmlResponseForStatus = StatusApps.htmlResponseForStatus; +exports.badRequest = StatusApps.badRequest; +exports.notFound = StatusApps.notFound; +exports.methodNotAllowed = StatusApps.methodNotAllowed; +exports.noLanguage = StatusApps.noLanguage; +exports.notAcceptable = StatusApps.notAcceptable; + +var DecoratorApps = require("./http-apps/decorators"); +exports.Normalize = DecoratorApps.Normalize; +exports.Date = DecoratorApps.Date; +exports.Error = DecoratorApps.Error; +exports.Debug = DecoratorApps.Debug; +exports.Log = DecoratorApps.Log; +exports.Time = DecoratorApps.Time; +exports.Headers = DecoratorApps.Headers; +exports.Permanent = DecoratorApps.Permanent; +exports.Decorators = DecoratorApps.Decorators; + +var CookieApps = require("./http-apps/cookie"); +exports.CookieJar = CookieApps.CookieJar; + diff --git a/core/promise-io/http-apps/chain.js b/core/promise-io/http-apps/chain.js new file mode 100644 index 0000000000..7fbe43ffd6 --- /dev/null +++ b/core/promise-io/http-apps/chain.js @@ -0,0 +1,24 @@ + +module.exports = Chain; +function Chain(end) { + var self = Object.create(Chain.prototype); + self.end = end || function (next) { + return next; + }; + return self; +}; + +Chain.prototype.use = function (App /*, ...args*/) { + if (!App) throw new Error("App is not defined after " + this.app); + var args = Array.prototype.slice.call(arguments, 1); + var self = this; + this.end = (function (End) { + return function Self(next) { + if (self.end !== Self && !next) throw new Error("App chain is broken after " + App); + return End(App.apply(null, [next].concat(args))); + }; + })(this.end); + this.app = App; + return this; +}; + diff --git a/core/promise-io/http-apps/content.js b/core/promise-io/http-apps/content.js new file mode 100644 index 0000000000..2b0be6a9a0 --- /dev/null +++ b/core/promise-io/http-apps/content.js @@ -0,0 +1,87 @@ +var Q = require("q-bluebird"); +var Negotiate = require("./negotiate"); +var QS = require("qs"); +var URL = require("url2"); + +/** + * Makes an app that returns a response with static content + * from memory. + * @param {Body} body a Q-JSGI + * response body + * @param {String} contentType + * @param {Number} status + * @returns {App} a Q-JSGI app + */ +exports.Content = function (body, contentType, status) { + return function () { + return exports.content(body, contentType, status); + }; +}; + +/** + * Returns a Q-JSGI response with the given content. + * @param {Body} content (optional) defaults to `[""]` + * @param {String} contentType (optional) defaults to `"text/plain"` + * @param {Number} status (optional) defaults to `200` + * @returns {Response} + */ +exports.content = +exports.ok = function (content, contentType, status) { + status = status || 200; + content = content || ""; + if (typeof content === "string") { + content = [content]; + } + contentType = contentType || "text/plain"; + return { + "status": status, + "headers": { + "content-type": contentType + }, + "body": content + }; +}; + +/** + * Wraps an app such that it expects to receive content + * in the request body and passes that content as a string + * to as the second argument to the wrapped JSGI app. + * + * @param {Function(Request, Object):Response} app + * @returns {App} + */ +exports.ContentRequest = function (app) { + return function (request, response) { + return Q.when(request.body.read(), function (body) { + return app(body, request, response); + }); + }; +}; + +/** + * @param {Function(Request):Object} + * @returns {App} + */ +exports.Inspect = function (app) { + return Negotiate.Method({"GET": function (request, response) { + return Q.when(app(request, response), function (object) { + return { + status: 200, + headers: { + "content-type": "text/plain" + }, + body: [inspect(object)] + } + }); + }}); +}; + +/** + */ +exports.ParseQuery = function (app) { + return function (request, response) { + request.query = QS.parse(URL.parse(request.url).query || ""); + return app(request, response); + }; +}; + diff --git a/core/promise-io/http-apps/cookie.js b/core/promise-io/http-apps/cookie.js new file mode 100644 index 0000000000..4658f0c734 --- /dev/null +++ b/core/promise-io/http-apps/cookie.js @@ -0,0 +1,177 @@ + +var Q = require("q-bluebird"); +var Cookie = require("../http-cookie"); +Q.longStackSupport = true; + +exports.CookieJar = function (app) { + var hostCookies = {}; // to {} of pathCookies to [] of cookies + return function (request) { + + if (!request.headers.host) { + throw new Error("Requests must have a host header"); + } + var hosts = allHostsContaining(request.headers.host); + + var now = new Date(); + + // delete expired cookies + for (var host in hostCookies) { + var pathCookies = hostCookies[host]; + for (var path in pathCookies) { + var cookies = pathCookies[path]; + for (var name in cookies) { + var cookie = cookies[name]; + if (cookie.expires && cookie.expires > now) { + delete cookie[name]; + } + } + } + } + + // collect applicable cookies + var requestCookies = concat( + Object.keys(hostCookies) + .map(function (host) { + if (!hostContains(host, request.headers.host)) { + return []; + } + var pathCookies = hostCookies[host]; + return concat( + Object.keys(pathCookies) + .map(function (path) { + if (!pathContains(path, request.path)) + return []; + var cookies = pathCookies[path]; + return ( + Object.keys(cookies) + .map(function (name) { + return cookies[name]; + }) + .filter(function (cookie) { + return cookie.secure ? + request.ssl : + true; + }) + ); + }) + ) + }) + ); + + if (requestCookies.length) { + request.headers["cookie"] = ( + requestCookies + .map(function (cookie) { + return Cookie.stringify( + cookie.key, + cookie.value + ); + }) + .join("; ") + ); + } + + return Q.when(app.apply(this, arguments), function (response) { + response.headers = response.headers || {}; + if (response.headers["set-cookie"]) { + var host = request.headers.host; + var hostParts = splitHost(host); + var hostname = hostParts[0]; + var requestHost = ipRe.test(hostname) ? host : "." + host; + // normalize to array + if (!Array.isArray(response.headers["set-cookie"])) { + response.headers["set-cookie"] = [response.headers["set-cookie"]]; + } + response.headers["set-cookie"].forEach(function (cookie) { + var date = response.headers["date"] ? + new Date(response.headers["date"]) : + new Date(); + cookie = Cookie.parse(cookie, date); + // ignore illegal host + if (cookie.host && !hostContains(requestHost, cookie.host)) + delete cookie.host; + var host = requestHost || cookie.host; + var path = cookie.path || "/"; + var pathCookies = hostCookies[host] = hostCookies[host] || {}; + var cookies = pathCookies[path] = pathCookies[path] || {}; + cookies[cookie.key] = cookie; + }) + delete response.headers["set-cookie"]; + } + + return response; + }); + + }; +}; + +var ipRe = /^\d+\.\d+\.\d+\.\d+$/; +var portRe = /^(.*)(:\d+)$/; + +function splitHost(host) { + var match = portRe.exec(host); + if (match) { + return [match[1], match[2]]; + } else { + return [host, ""]; + } +} + +function allHostsContaining(host) { + var parts = splitHost(host); + var hostname = parts[0]; + var port = parts[1]; + if (ipRe.test(hostname)) { + return [hostname + port]; + } if (hostname === "localhost") { + return [hostname + port]; + } else { + var parts = hostname.split("."); + var hosts = []; + while (parts.length > 1) { + hosts.push("." + parts.join(".") + port); + parts.shift(); + } + return hosts; + } +} + +function hostContains(containerHost, contentHost) { + var containerParts = splitHost(containerHost); + var containerHostname = containerParts[0]; + var containerPort = containerParts[1]; + var contentParts = splitHost(contentHost); + var contentHostname = contentParts[0]; + var contentPort = contentParts[1]; + if (containerPort !== contentPort) { + return false; + } + if (ipRe.test(containerHostname) || ipRe.test(contentHostname)) { + return containerHostname === contentHostname; + } else if (/^\./.test(containerHostname)) { + return ( + contentHostname.lastIndexOf(containerHostname) === + contentHostname.length - containerHostname.length + ) || ( + containerHostname.slice(1) === contentHostname + ); + } else { + return containerHostname === contentHostname; + } +}; + +function pathContains(container, content) { + if (/^\/$/.test(container)) { + return content.indexOf(container) === 0; + } else { + return ( + content === container || + content.indexOf(container + "/") === 0 + ); + } +} + +function concat(arrays) { + return [].concat.apply([], arrays); +} + diff --git a/core/promise-io/http-apps/decorators.js b/core/promise-io/http-apps/decorators.js new file mode 100644 index 0000000000..ac063f4402 --- /dev/null +++ b/core/promise-io/http-apps/decorators.js @@ -0,0 +1,178 @@ + +var Q = require("q-bluebird"); +var HTTP = require("../http"); +var RouteApps = require("./route"); +var StatusApps = require("./status"); + +exports.Normalize = function (app) { + return function (request, response) { + var request = HTTP.normalizeRequest(request); + return Q.when(app(request, response), function (response) { + return HTTP.normalizeResponse(response); + }); + }; +}; + +exports.Date = function (app, present) { + present = present || function () { + return new Date(); + }; + return RouteApps.Trap(app, function (response, request) { + response.headers["date"] = "" + present(); + }); +}; + +/** + * Decorates a JSGI application such that rejected response promises + * get translated into `500` server error responses with no content. + * + * @param {App} app + * @returns {App} + */ +exports.Error = function (app, debug) { + return function (request, response) { + return Q.when(app(request, response), null, function (error) { + if (!debug) + error = undefined; + return StatusApps.responseForStatus(request, 500, error && error.stack || error); + }); + }; +}; + +exports.Debug = function (app) { + return exports.Error(app, true); +}; + +/** + * Decorates a Q-JSGI application such that all requests and responses + * are logged. + * + * @param {App} app + * @returns {App} + */ +exports.Log = function (app, log, stamp) { + log = log || console.log; + stamp = stamp || function (message) { + return new Date().toISOString() + " " + message; + }; + return function (request, response) { + var remoteHost = + request.remoteHost + ":" + + request.remotePort; + var requestLine = + request.method + " " + + request.path + " " + + "HTTP/" + request.version.join("."); + log(stamp( + remoteHost + " " + + "--> " + + requestLine + )); + return Q.when(app(request, response), function (response) { + if (response) { + log(stamp( + remoteHost + " " + + "<== " + + response.status + " " + + requestLine + " " + + (response.headers["content-length"] || "-") + )); + } else { + log(stamp( + remoteHost + " " + + "... " + + "... " + + requestLine + " (response undefined / presumed streaming)" + )); + } + return response; + }, function (reason) { + log(stamp( + remoteHost + " " + + "!!! " + + requestLine + " " + + (reason && reason.message || reason) + )); + return Q.reject(reason); + }); + }; +}; + +/** + * Decorates a Q-JSGI application such that all responses have an + * X-Response-Time header with the time between the request and the + * response in milliseconds, not including any time needed to stream + * the body to the client. + * + * @param {App} app + * @returns {App} + */ +exports.Time = function (app) { + return function (request, response) { + var start = new Date(); + return Q.when(app(request, response), function (response) { + var stop = new Date(); + if (response && response.headers) { + response.headers["x-response-time"] = "" + (stop - start); + } + return response; + }); + }; +}; + +/** + * Decorates a Q-JSGI application such that all responses have the + * given additional headers. These headers do not override the + * application's given response headers. + * + * @param {Object} headers + * @param {App} app decorated application. + */ +exports.Headers = function (app, headers) { + return function (request, response) { + return Q.when(app(request, response), function (response) { + if (response && response.headers) { + Object.keys(headers).forEach(function (key) { + if (!(key in response.headers)) { + response.headers[key] = headers[key]; + } + }); + } + return response; + }); + }; +}; + +var farFuture = + 1000 * // ms + 60 * // s + 60 * // m + 24 * // h + 365 * // d + 10; // years +exports.Permanent = function (app, future) { + future = future || function () { + return new Date(new Date().getTime() + farFuture); + }; + app = RouteApps.Tap(app, function (request, response) { + request.permanent = future; + }); + app = RouteApps.Trap(app, function (response, request) { + response.headers["expires"] = "" + future(); + }); + return app; +}; + +/** + * Wraps a Q-JSGI application in a sequence of decorators. + * @param {Array * Decorator} decorators + * @param {App} app + * @returns {App} + */ +exports.Decorators = function (decorators, app) { + decorators.reversed().forEach(function (Middleware) { + app = Middleware(app); + }); + return app; +}; + diff --git a/core/promise-io/http-apps/fs.js b/core/promise-io/http-apps/fs.js new file mode 100644 index 0000000000..d857b9bb6b --- /dev/null +++ b/core/promise-io/http-apps/fs.js @@ -0,0 +1,417 @@ + +var Q = require("q-bluebird"); +var URL = require("url2"); +var MimeTypes = require("mime"); +var FS = require("../fs"); +var StatusApps = require("./status"); +var RedirectApps = require("./redirect"); +var Negotiation = require("./negotiate"); +var HtmlApps = require("./html"); +var Deprecate = require("../deprecate"); + +/** + * @param {String} path + * @param {String} contentType + * @returns {App} + */ +exports.File = function (path, contentType) { + return function (request, response) { + return exports.file(request, String(path), contentType); + }; +}; + +/** + * @param {String} path + * @param {{ + notFound, + file, + directory, + contentType, + redirectSymbolicLinks:Boolean, + redirect:Function(location), + permanent:Boolean + * }} options + * @returns {App} + */ +exports.FileTree = function (root, options) { + if (!options) + options = {}; + options.notFound = options.notFound || StatusApps.notFound; + options.file = options.file || exports.file; + options.directory = options.directory || exports.directory; + options.fs = options.fs || FS; + var fs = options.fs; + root = fs.canonical(root); + return function (request, response) { + var location = URL.parse(request.url); + request.fs = fs; + var redirect = options.redirect || ( + request.permanent || options.permanent ? + RedirectApps.permanentRedirect : + RedirectApps.temporaryRedirect + ); + return Q.when(root, function (root) { + var path = fs.join(root, request.pathInfo.slice(1)); + return Q.when(fs.canonical(path), function (canonical) { + //TODO remove for 2.0.0 + if (options.followInsecureSymlinks) { + Deprecate.deprecationWarning("followInsecureSymlinks", "followInsecureSymbolicLinks"); + options.followInsecureSymbolicLinks = true; + } + if (!fs.contains(root, canonical) && !options.followInsecureSymbolicLinks) + return options.notFound(request, response); + if (path !== canonical && options.redirectSymbolicLinks) + return redirect(request, fs.relativeFromFile(path, canonical)); + // TODO: relativeFromFile should be designed for URL’s, not generalized paths. + // HTTP.relative(pathToDirectoryLocation(path), pathToFile/DirectoryLocation(canonical)) + return Q.when(fs.stat(canonical), function (stat) { + if (stat.isFile()) { + return options.file(request, canonical, options.contentType, fs); + } else if (stat.isDirectory()) { + return options.directory(request, canonical, options.contentType, fs); + } else { + return options.notFound(request, response); + } + }); + }, function (reason) { + return options.notFound(request, response); + }); + }); + }; +}; + +exports.file = function (request, path, contentType, fs) { + fs = fs || FS; + // TODO last-modified header + contentType = contentType || MimeTypes.lookup(path); + return Q.when(fs.stat(path), function (stat) { + var etag = exports.etag(stat); + var options = { + flags: "rb" + }; + var range; + var status = 200; + var headers = { + "content-type": contentType, + etag: etag + }; + + // Partial range requests + if ("range" in request.headers) { + // Invalid cache + if ( + "if-range" in request.headers && + etag != request.headers["if-range"] + ) { + // Normal 200 for entire, altered content + } else { + // Truncate to the first requested continuous range + range = interpretFirstRange(request.headers["range"], stat.size); + // Like Apache, ignore the range header if it is invalid + if (range) { + if (range.end > stat.size) { + range.end = stat.size; + } + if (range.end <= range.begin) { + return StatusApps.responseForStatus(request, 416); // not satisfiable + } + status = 206; // partial content + headers["content-range"] = ( + "bytes " + + range.begin + "-" + (range.end - 1) + + "/" + stat.size + ); + headers["content-length"] = "" + (range.end - range.begin); + options.begin = range.begin; + options.end = range.end; + } else { + return StatusApps.responseForStatus(request, 416); // not satisfiable + } + } + // Full requests + } else { + // Cached + // We do not use date-based caching + // TODO consider if-match? + if (etag == request.headers["if-none-match"]) + return StatusApps.responseForStatus(request, 304); + headers["content-length"] = "" + stat.size; + } + + // TODO sendfile + return { + status: status, + headers: headers, + body: fs.open(path, options), + file: path, + range: range + }; + }); +}; + +var rangesExpression = /^\s*bytes\s*=\s*(\d*\s*-\s*\d*\s*(?:,\s*\d*\s*-\s*\d*\s*)*)$/; +var rangeExpression = /^\s*(\d*)\s*-\s*(\d*)\s*$/; + +var interpretRange = function (text, size) { + var match = rangeExpression.exec(text); + if (!match) + return; + if (match[1] == "" && match[2] == "") + return; + var begin, end; + if (match[1] == "") { + begin = 0; + end = +match[2] + 1; + } else if (match[2] == "") { + begin = +match[1]; + end = size; + } else { + begin = +match[1]; + end = +match[2] + 1; + } + return { + begin: begin, + end: end + }; +}; + +var interpretFirstRange = exports.interpretFirstRange = function (text, size) { + var match = rangesExpression.exec(text); + if (!match) + return; + var texts = match[1].split(/\s*,\s*/); + var range = interpretRange(texts[0], size); + for (var i = 0, ii = texts.length; i < ii; i++) { + var next = interpretRange(texts[i], size); + if (next.begin <= range.end) { + range.end = next.end; + } else { + return; // Can't satisfy non-contiguous ranges TODO + } + } + return range; +}; + +/** + * @param {Stat} + * @returns {String} + */ +exports.etag = function (stat) { + return [ + stat.node.ino, + stat.size, + stat.lastModified().getTime() + ].join("-"); +}; + +/** + * @param {Request} request + * @param {String} path + * @param {Response} + */ +exports.directory = function (request, path) { + var response = StatusApps.notFound(request); + response.directory = path; + return response; +}; + +exports.ListDirectories = function (app, listDirectory) { + listDirectory = listDirectory || exports.listDirectory; + return function (request) { + if (request.directoryIndex) { + throw new Error("DirectoryIndex must be used after ListDirectories"); + } + request.listDirectories = true; + return Q.fcall(app, request) + .then(function (response) { + if (response.directory !== void 0) { + return listDirectory(request, response); + } else { + return response; + } + }); + }; +}; + +exports.listDirectory = function (request, response) { + // TODO advisory to have JSON or HTML fragment handler. + request.location = URL.parse(request.path); + if (request.location.file) { + return RedirectApps.redirect(request, request.location.file + "/"); + } + var handlers = {}; + handlers["text/plain"] = exports.listDirectoryText; + handlers["text/markdown"] = exports.listDirectoryMarkdown; + if (request.handleHtmlFragmentResponse) { + handlers["text/html"] = exports.listDirectoryHtmlFragment; + } + if (request.handleJsonResponse) { + handlers["application/json"] = exports.listDirectoryJson; + } + var handleResponse = Negotiation.negotiate(request, handlers) || function () { + return response; + }; + return handleResponse(request, response); +}; + +exports.listDirectoryHtmlFragment = function (request, response) { + return exports.listDirectoryData(request, response) + .then(function (data) { + return { + status: 200, + headers: { + "content-type": "text/html" + }, + htmlTitle: "Directory Index", + htmlFragment: { + forEach: function (write) { + write("
    \n"); + Object.keys(data).sort().forEach(function (name) { + var stat = data[name]; + var suffix = ""; + if (stat.type === "directory") { + suffix = "/"; + } + write("
  • " + HtmlApps.escapeHtml(name + suffix) + "
  • \n"); + }); + write("
\n"); + } + } + }; + }); +}; + +exports.listDirectoryText = function (request, response) { + return exports.listDirectoryData(request, response) + .then(function (data) { + return { + status: 200, + headers: { + "content-type": "text/plain" + }, + body: { + forEach: function (write) { + Object.keys(data).sort().forEach(function (name) { + var stat = data[name]; + var suffix = ""; + if (stat.type === "directory") { + suffix = "/"; + } + write(name + suffix + "\n"); + }); + } + } + }; + }); +}; + +exports.listDirectoryMarkdown = function (request, response) { + return exports.listDirectoryData(request, response) + .then(function (data) { + return { + status: 200, + headers: { + "content-type": "text/plain" + }, + body: { + forEach: function (write) { + write("\n# Directory Index\n\n"); + Object.keys(data).forEach(function (name) { + var stat = data[name]; + var suffix = ""; + if (stat.type === "directory") { + suffix = "/"; + } + write("- " + name + suffix + "\n"); + }); + write("\n"); + } + } + }; + }); +}; + +exports.listDirectoryJson = function (request, response) { + return exports.listDirectoryData(request, response) + .then(function (data) { + return { + status: 200, + headers: {}, + data: data + }; + }); +}; + +exports.listDirectoryData = function (request, response) { + if (!request.fs) { + throw new Error("Can't list a directory without a designated file system"); + } + var fs = request.fs; + return Q.invoke(fs, "list", response.directory) + .then(function (list) { + list.sort(); + return list.map(function (name) { + return Q.invoke(fs, "stat", fs.join(response.directory, name)) + .then(function (stat) { + if (stat.isDirectory()) { + return {name: name, stat: { + type: "directory" + }}; + } else if (stat.isFile()) { + return {name: name, stat: { + type: "file" + }}; + } + }, function () { + // ignore unstatable entries + }); + }) + }) + .all() + .then(function (stats) { + var data = {}; + stats.forEach(function (entry) { + if (entry) { + data[entry.name] = entry.stat; + } + }); + return data; + }); +}; + +exports.DirectoryIndex = function (app, indexFile) { + indexFile = indexFile || "index.html"; + return function (request) { + request.directoryIndex = true; + request.location = URL.parse(request.path); + // redirect index.html to containing directory + // TODO worry about whether this file actually exists + if (request.location.file === indexFile) { + return RedirectApps.redirect(request, "."); + } else { + return Q.fcall(app, request) + .then(function (response) { + if (response.directory !== void 0) { + if (request.location.file) { + return RedirectApps.redirect(request, request.location.file + "/"); + } else { + var index = request.fs.join(response.directory, indexFile); + return Q.invoke(request.fs, "isFile", index) + .then(function (isFile) { + if (isFile) { + request.url = URL.resolve(request.url, indexFile); + request.pathInfo += indexFile; + return app(request); + } else { + return response; + } + }); + } + } else { + return response; + } + }); + } + }; +}; + diff --git a/core/promise-io/http-apps/html.js b/core/promise-io/http-apps/html.js new file mode 100644 index 0000000000..a77d58dc1a --- /dev/null +++ b/core/promise-io/http-apps/html.js @@ -0,0 +1,58 @@ + +var Q = require("q-bluebird"); +// TODO negotiate text/html vs text/html+fragment (or other mime type) + +/** + * @param {Request} request + * @param {String} path + * @param {String} contentType + * @returns {Response} + */ +exports.HandleHtmlFragmentResponses = function (app, handleHtmlFragmentResponse) { + handleHtmlFragmentResponse = handleHtmlFragmentResponse || exports.handleHtmlFragmentResponse; + return function (request) { + request.handleHtmlFragmentResponse = handleHtmlFragmentResponse; + return Q.fcall(app, request) + .then(function (response) { + if (response.htmlFragment) { + return Q.fcall(handleHtmlFragmentResponse, response); + } else { + return response; + } + }); + }; +}; + +exports.handleHtmlFragmentResponse = function (response) { + var htmlFragment = response.htmlFragment; + delete response.htmlFragment; + response.headers["content-type"] = "text/html; charset=utf-8"; + response.body = { + forEach: function (write) { + write("\n"); + write("\n"); + write(" \n"); + if (response.htmlTitle !== void 0) { + write(" " + escapeHtml(response.htmlTitle) + "\n"); + } + write(" \n"); + write(" \n"); + htmlFragment.forEach(function (line) { + write(" " + line); + }); + write(" \n"); + write("\n"); + } + }; + return response; +}; + +exports.escapeHtml = escapeHtml; +function escapeHtml(text) { + return String(text) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) +} + diff --git a/core/promise-io/http-apps/json.js b/core/promise-io/http-apps/json.js new file mode 100644 index 0000000000..b1b7d937d1 --- /dev/null +++ b/core/promise-io/http-apps/json.js @@ -0,0 +1,78 @@ +var Q = require("q-bluebird"); +var Content = require("./content"); +var Status = require("./status"); + +exports.HandleJsonResponses = function (app, reviver, tab) { + return function (request) { + request.handleJsonResponse = exports.handleJsonResponse; + return Q.fcall(app, request) + .then(function (response) { + if (response.data !== void 0) { + return Q.fcall(exports.handleJsonResponse, response, reviver, tab); + } else { + return response; + } + }); + }; +}; + +exports.handleJsonResponse = function (response, revivier, tab) { + response.headers["content-type"] = "application/json"; + response.body = { + forEach: function (write) { + write(JSON.stringify(response.data, revivier, tab)); + } + }; + return response; +}; + +/** + * Wraps a Q-JSGI application such that the child application may + * simply return an object, which will in turn be serialized into a + * Q-JSGI response. + * + * @param {Function(Request):Object} app an application that accepts a + * request and returns a JSON serializable object. + * @returns {App} + */ +exports.Json = function (app, reviver, tabs) { + return function (request, response) { + return Q.when(app(request, response), function (object) { + return exports.json(object, reviver, tabs); + }); + }; +}; + +/** + * @param {Object} content data to serialize as JSON + * @param {Function} reviver + * @param {Number|String} tabs + * @returns {Response} + */ +exports.json = function (content, reviver, tabs) { + try { + var json = JSON.stringify(content, reviver, tabs); + } catch (exception) { + return Q.reject(exception); + } + return Content.ok([json], "application/json"); +}; + +/** + * @param {Function(Request, Object):Response} app + * @param {App} badRequest + * @returns {App} + */ +exports.JsonRequest = function (app, badRequest) { + if (!badRequest) + badRequest = Status.badRequest; + return Content.ContentRequest(function (content, request, response) { + try { + var object = JSON.parse(content); + } catch (error) { + return badRequest(request, error); + } + return app(object, request, response); + }); +}; + diff --git a/core/promise-io/http-apps/negotiate.js b/core/promise-io/http-apps/negotiate.js new file mode 100644 index 0000000000..7bb8306f33 --- /dev/null +++ b/core/promise-io/http-apps/negotiate.js @@ -0,0 +1,120 @@ + +var Q = require("q-bluebird"); +var MimeParse = require("mimeparse"); +var Status = require("./status"); + +exports.negotiate = negotiate; +function negotiate(request, types, header) { + var keys = Object.keys(types); + var accept = request.headers[header || "accept"] || "*"; + var best = MimeParse.bestMatch(keys, accept); + return types[best]; +} + +/// branch on HTTP method +/** + * @param {Object * App} methods + * @param {App} notAllowed (optional) + * @returns {App} + */ +exports.Method = function (methods, methodNotAllowed) { + var keys = Object.keys(methods); + if (!methodNotAllowed) + methodNotAllowed = Status.methodNotAllowed; + return function (request) { + var method = request.method; + if (Object.has(keys, method)) { + return Object.get(methods, method)(request); + } else { + return methodNotAllowed(request); + } + }; +}; + +var Negotiator = function (requestHeader, responseHeader, respond) { + return function (types, notAcceptable) { + var keys = Object.keys(types); + if (!notAcceptable) + notAcceptable = Status.notAcceptable; + return function (request) { + var accept = request.headers[requestHeader] || "*"; + var type = MimeParse.bestMatch(keys, accept); + request.terms = request.terms || {}; + request.terms[responseHeader] = type; + if (Object.has(keys, type)) { + return Q.when(types[type](request), function (response) { + if ( + respond !== null && + response && + response.status === 200 && + response.headers + ) { + response.headers[responseHeader] = type; + } + return response; + }); + } else { + return notAcceptable(request); + } + }; + }; +}; + +/// branch on HTTP content negotiation +/** + * Routes based on content negotiation, between the request's `accept` + * header and the application's list of possible content types. + * + * @param {Object * App} types mapping content types to apps that can + * handle them. + * @param {App} notAcceptable + * @returns {App} + */ +exports.ContentType = Negotiator("accept", "content-type"); +exports.Language = Negotiator("accept-language", "language"); +exports.Charset = Negotiator("accept-charset", "charset"); +exports.Encoding = Negotiator("accept-encoding", "encoding"); + +exports.Host = function (appForHost, notAcceptable) { + var table = Object.keys(appForHost).map(function (pattern) { + var parts = pattern.split(":"); + return [ + pattern, + parts[0] || "*", + parts[1] || "*", + appForHost[pattern] + ]; + }); + if (!notAcceptable) { + notAcceptable = Status.notAcceptable; + } + return function (request) { + // find first matching host for app + for (var index = 0; index < table.length; index++) { + var row = table[index]; // [hostname, port, app] + var pattern = row[0]; + var hostname = row[1]; + var port = row[2]; + var app = row[3]; + if ( + (hostname === "*" || hostname === request.hostname) && + (port === "*" || port === "" + request.port) + ) { + request.terms = request.terms || {}; + request.terms.host = pattern; + return app(request); + } + } + return notAcceptable(request); + }; +}; + +// Branch on a selector function based on the request +exports.Select = function (select) { + return function (request) { + return Q.when(select(request), function (app) { + return app(request); + }); + }; +}; + diff --git a/core/promise-io/http-apps/proxy.js b/core/promise-io/http-apps/proxy.js new file mode 100644 index 0000000000..046fecc314 --- /dev/null +++ b/core/promise-io/http-apps/proxy.js @@ -0,0 +1,27 @@ + +var HTTP = require("../http"); +var URL = require("url2"); +var Q = require("q-bluebird"); + +exports.Proxy = function (app) { + if (typeof app === "string") { + var location = app; + app = function (request) { + request.url = location; + return request; + }; + } + return function (request, response) { + return Q.when(app.apply(this, arguments), function (request) { + return HTTP.request(request); + }); + }; +}; + +exports.ProxyTree = function (url) { + return exports.Proxy(function (request) { + request.url = URL.resolve(url, request.pathInfo.replace(/^\//, "")); + return request; + }); +}; + diff --git a/core/promise-io/http-apps/redirect.js b/core/promise-io/http-apps/redirect.js new file mode 100644 index 0000000000..261d83d9ce --- /dev/null +++ b/core/promise-io/http-apps/redirect.js @@ -0,0 +1,209 @@ + +var Q = require("q-bluebird"); +var URL = require("url2"); +var Http = require("../http"); +var Negotiation = require("./negotiate"); +var HtmlApps = require("./html"); + +/** + * @param {String} path + * @param {Number} status (optional) default is `301` + * @returns {App} + */ +exports.PermanentRedirect = function (location, status, tree) { + return function (request, response) { + return exports.permanentRedirect(request, location, status, tree); + }; +}; + +/** + * @param {String} path + * @param {Number} status (optional) default is `301` + * @returns {App} + */ +exports.PermanentRedirectTree = function (location, status) { + return function (request, response) { + return exports.permanentRedirect(request, location, status, true); + }; +}; + +/** + * @param {String} path + * @param {Number} status (optional) default is `307` + * @returns {App} + */ +exports.TemporaryRedirect = function (location, status, tree) { + return function (request, response) { + return exports.temporaryRedirect(request, location, status, tree); + }; +}; + +/** + * @param {String} path + * @param {Number} status (optional) default is `307` + * @returns {App} + */ +exports.TemporaryRedirectTree = function (location, status) { + return function (request, response) { + return exports.temporaryRedirect(request, location, status, true); + }; +}; + +/** + * @param {String} path + * @param {Number} status (optional) default is `307` + * @returns {App} + */ +exports.Redirect = function (location, status, tree) { + return function (request, response) { + return exports.redirect(request, location, status, tree); + }; +}; + +/** + * @param {String} path + * @param {Number} status (optional) default is `307` + * @returns {App} + */ +exports.RedirectTree = function (location, status) { + return function (request, response) { + return exports.redirect(request, location, status, true); + }; +}; + +exports.permanentRedirect = function (request, location, status) { + return exports.redirect(request, location, status || 301); +}; + +exports.permanentRedirectTree = function (request, location, status) { + return exports.redirect(request, location, status || 301, true); +}; + +exports.temporaryRedirect = function (request, location, status) { + return exports.redirect(request, location, status || 307); +}; + +exports.temporaryRedirectTree = function (request, location, status) { + return exports.redirect(request, location, status || 307, true); +}; + +exports.redirectTree = function (request, location, status) { + return exports.redirect(request, location, status, true); +}; + +/** + * @param {String} location + * @param {Number} status (optional) default is `301` + * @returns {Response} + */ +exports.redirect = function (request, location, status, tree) { + + // request.permanent gets set by Permanent middleware + status = status || (request.permanent ? 301 : 307); + + // ascertain that the location is absolute, per spec + location = URL.resolve(request.url, location); + + // redirect into a subtree with the remaining unrouted + // portion of the path, if so configured + if (tree) { + location = URL.resolve( + location, + request.pathInfo.replace(/^\//, "") + ); + } + + var handlers = {}; + handlers["text/plain"] = exports.redirectText; + if (request.handleHtmlFragmentResponse) { + handlers["text/html"] = exports.redirectHtml; + } + var handler = Negotiation.negotiate(request, handlers) || exports.redirectText; + return handler(request, location, status); + +}; + +exports.redirectText = function (request, location, status) { + var content = ( + (request.permanent ? "Permanent redirect\n" : "Temporary redirect\n") + + "See: " + location + "\n" + ); + var contentLength = content.length; + return { + status: status, + headers: { + location: location, + "content-type": "text/plain" + }, + body: [content] + }; +}; + +exports.redirectHtml = function (request, location, status) { + var title = request.permanent ? "Permanent redirect" : "Temporary redirect"; + return { + status: status, + headers: { + location: location, + "content-type": "text/html" + }, + htmlTitle: title, + htmlFragment: { + forEach: function (write) { + write("

" + HtmlApps.escapeHtml(title) + "

\n"); + write( + "

See: " + + HtmlApps.escapeHtml(location) + + "

\n" + ); + } + } + }; +}; + +exports.RedirectTrap = function (app, maxRedirects) { + maxRedirects = maxRedirects || 20; + return function (request, response) { + var remaining = maxRedirects; + var deferred = Q.defer(); + var self = this; + var args = arguments; + + request = Http.normalizeRequest(request); + + // try redirect loop + function next() { + Q.fcall(function () { + return app(request, response); + }) + .then(function (response) { + if (exports.isRedirect(response)) { + if (remaining--) { + request.url = response.headers.location; + next(); + } else { + throw new Error("Maximum redirects."); + } + } else { + deferred.resolve(response); + } + }) + .fail(deferred.reject) + } + next(); + + return deferred.promise; + }; +}; + +exports.isRedirect = function (response) { + return isRedirect[response.status] || false; +}; + +var isRedirect = { + 301: true, + 302: true, + 303: true, + 307: true +}; + diff --git a/core/promise-io/http-apps/route.js b/core/promise-io/http-apps/route.js new file mode 100644 index 0000000000..c96f817581 --- /dev/null +++ b/core/promise-io/http-apps/route.js @@ -0,0 +1,125 @@ + +var Q = require("q-bluebird"); +var StatusApps = require("./status"); + +/** + * Makes a Q-JSGI app that only responds when there is nothing left + * on the path to route. If the there is unprocessed data on the + * path, the returned app either forwards to the `notFound` app or + * returns a `404 Not Found` response. + * + * @param {App} app a Q-JSGI application to + * respond to this end of the routing chain. + * @param {App} notFound (optional) defaults + * to the `notFound` app. + * @returns {App} + */ +exports.Cap = function (app, notFound) { + notFound = notFound || StatusApps.notFound; + return function (request, response) { + // TODO Distinguish these cases + if (request.pathInfo === "" || request.pathInfo === "/") { + return app(request, response); + } else { + return notFound(request, response); + } + }; +}; + +/** + * Wraps an app with a function that will observe incoming requests + * before giving the app an opportunity to respond. If the "tap" + * function returns a response, it will be used in lieu of forwarding + * the request to the wrapped app. + */ +exports.Tap = function (app, tap) { + return function (request, response) { + var self = this, args = arguments; + return Q.when(tap.apply(this, arguments), function (response) { + if (response) { + return response; + } else { + return app.apply(self, args); + } + }); + }; +}; + +/** + * Wraps an app with a "trap" function that intercepts and may + * alter or replace the response of the wrapped application. + */ +exports.Trap = function (app, trap) { + return function (request, response) { + return Q.when(app.apply(this, arguments), function (response) { + if (response) { + response.headers = response.headers || {}; + return trap(response, request) || response; + } + }); + }; +}; + +/** + * Makes a Q-JSGI app that branches requests based on the next + * unprocessed path component. + * @param {Object * App} paths a mapping from path components (single + * file or directory names) to Q-JSGI applications for subsequent + * routing. The mapping may be a plain JavaScript `Object` record, + * which must own the mapping properties, or an object that has + * `has(key)` and `get(key)` methods in its prototype chain. + * @param {App} notFound a Q-JSGI application + * that handles requests for which the next file name does not exist + * in paths. + * @returns {App} + */ +exports.Branch = function (paths, notFound) { + if (!paths) + paths = {}; + if (!notFound) + notFound = StatusApps.notFound; + return function (request, response) { + if (!/^\//.test(request.pathInfo)) { + return notFound(request, response); + } + var path = request.pathInfo.slice(1); + var parts = path.split("/"); + var part = decodeURIComponent(parts.shift()); + if (Object.has(paths, part)) { + request.scriptName = request.scriptName + part + "/"; + request.pathInfo = path.slice(part.length); + return Object.get(paths, part)(request, response); + } + return notFound(request, response); + }; +}; + +/** + * Returns the response of the first application that returns a + * non-404 response status. + * + * @param {Array * App} apps a cascade of applications to try + * successively until one of them returns a non-404 status. + * @returns {App} + */ +exports.FirstFound = function (cascade) { + return function (request, response) { + var i = 0, ii = cascade.length; + function next() { + var response = cascade[i++](request, response); + if (i < ii) { + return Q.when(response, function (response) { + if (response.status === 404) { + return next(); + } else { + return response; + } + }); + } else { + return response; + } + } + return next(); + }; +}; + diff --git a/core/promise-io/http-apps/status.js b/core/promise-io/http-apps/status.js new file mode 100644 index 0000000000..6bff97150e --- /dev/null +++ b/core/promise-io/http-apps/status.js @@ -0,0 +1,175 @@ + +var Negotiation = require("./negotiate"); +var HtmlApps = require("./html"); + +/** + * {Object * String} a mapping of HTTP status codes to + * their standard descriptions. + */ +// Every standard HTTP code mapped to the appropriate message. +// Stolen from Rack which stole from Mongrel +exports.statusCodes = { + 100: 'Continue', + 101: 'Switching Protocols', + 102: 'Processing', + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 207: 'Multi-Status', + 300: 'Multiple Choices', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 307: 'Temporary Redirect', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Request Entity Too Large', + 414: 'Request-URI Too Large', + 415: 'Unsupported Media Type', + 416: 'Request Range Not Satisfiable', + 417: 'Expectation Failed', + 422: 'Unprocessable Entity', + 423: 'Locked', + 424: 'Failed Dependency', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 507: 'Insufficient Storage' +}; + +/** + * {Object * Number} a mapping from HTTP status descriptions + * to HTTP status codes. + */ +exports.statusMessages = {}; +for (var code in exports.statusCodes) + exports.statusMessages[exports.statusCodes[code]] = +code; + +/** + * Determines whether an HTTP response should have a + * response body, based on its status code. + * @param {Number} status + * @returns whether the HTTP response for the given status + * code has content. + */ +exports.statusWithNoEntityBody = function (status) { + return (status >= 100 && status <= 199) || + status == 204 || status == 304; +}; + +/** + * @param {Number} status + * @returns {Function(Request) :Response} a JSGI app that returns + * a plain text response with the given status code. + */ +exports.appForStatus = function (status) { + return function (request) { + return exports.responseForStatus(request, status, request.method + " " + request.path); + }; +}; + +/** + * @param {Number} status an HTTP status code + * @param {String} message (optional) a message to include + * in the response body. + * @returns a JSGI HTTP response object with the given status + * code and message as its body, if the status supports + * a body. + */ +exports.responseForStatus = function(request, status, addendum) { + if (exports.statusCodes[status] === undefined) + throw "Unknown status code"; + + var message = exports.statusCodes[status]; + + // RFC 2616, 10.2.5: + // The 204 response MUST NOT include a message-body, and thus is always + // terminated by the first empty line after the header fields. + // RFC 2616, 10.3.5: + // The 304 response MUST NOT contain a message-body, and thus is always + // terminated by the first empty line after the header fields. + if (exports.statusWithNoEntityBody(status)) { + return {status: status, headers: {}}; + } else { + var handlers = {}; + handlers["text/plain"] = exports.textResponseForStatus; + if (request.handleHtmlFragmentResponse) { + handlers["text/html"] = exports.htmlResponseForStatus; + } + var responseForStatus = Negotiation.negotiate(request, handlers) || exports.textResponseForStatus; + return responseForStatus(request, status, message, addendum); + } +}; + +exports.textResponseForStatus = function (request, status, message, addendum) { + var content = message + "\n"; + if (addendum) { + content += addendum + "\n"; + } + var contentLength = content.length; + return { + status: status, + statusMessage: message, + headers: { + "content-length": contentLength + }, + body: [content] + }; +}; + +exports.htmlResponseForStatus = function (request, status, message, addendum) { + return { + status: status, + statusMessage: message, + headers: {}, + htmlTitle: message, + htmlFragment: { + forEach: function (write) { + write("

" + HtmlApps.escapeHtml(message) + "

\n"); + write("

Status: " + status + "

\n"); + if (addendum) { + write("
" + HtmlApps.escapeHtml(addendum) + "
\n"); + } + } + } + } +}; + +/** + * {App} an application that returns a 400 response. + */ +exports.badRequest = exports.appForStatus(400); +/** + * {App} an application that returns a 404 response. + */ +exports.notFound = exports.appForStatus(404); +/** + * {App} an application that returns a 405 response. + */ +exports.methodNotAllowed = exports.appForStatus(405); +/** + * {App} an application that returns a 405 response. + */ +exports.noLanguage = +exports.notAcceptable = exports.appForStatus(406); + diff --git a/core/promise-io/http-cookie.js b/core/promise-io/http-cookie.js new file mode 100644 index 0000000000..723ea76111 --- /dev/null +++ b/core/promise-io/http-cookie.js @@ -0,0 +1,75 @@ + +/** + * Provides utilities for reading and writing HTTP cookies. + * @module + */ + +/*whatsupdoc*/ + +var QS = require("qs"); + +/** + * @param {String} cookie + * @returns {Object} + */ +exports.parse = function (cookie, date) { + date = date || new Date(); + var parsed = {}; + var terms = cookie.split(/[;,]/g); + var keyValue = terms.shift().split("="); + parsed.key = decodeURIComponent(keyValue[0]); + parsed.value = decodeURIComponent(keyValue[1]); + terms.forEach(function (term) { + var parts = term.split("=").map(function (part) { + return part.trim(); + }); + var key = parts[0], value = parts[1]; + if (/^domain$/i.test(key)) { + parsed.domain = value; + } else if (/^path$/i.test(key)) { + parsed.path = value; + } else if (/^expires$/i.test(key)) { + parsed.expires = new Date( + +new Date() + // actual now + (new Date(value) - date) // server offset + ); + } else if (/^max-age$/i.test(key)) { + parsed.expires = new Date( + new Date().getTime() + + (value * 1000) + ); + } else if (/^secure$/i.test(key)) { + parsed.secure = true; + } else if (/^httponly$/i.test(key)) { + parsed.httpOnly = true; + } + }); + return parsed; +}; + +/** + * @param {String} key + * @param {String} value + * @param {Object} options (optional) + * @returns {String} a cookie string + */ +exports.stringify = function (key, value, options) { + var cookie = ( + encodeURIComponent(key) + "=" + + encodeURIComponent(value) + ); + if (options) { + if (options.domain) + cookie += "; Domain=" + encodeURIComponent(options.domain); + if (options.path) + cookie += "; Path=" + encodeURIComponent(options.path); + if (options.expires) + cookie += "; Expires=" + options.expires.toGMTString(); + if (options.secure) + cookie += "; Secure"; + if (options.httpOnly) + cookie += "; HttpOnly"; + } + return cookie; +}; + diff --git a/core/promise-io/http.js b/core/promise-io/http.js new file mode 100644 index 0000000000..40632937dd --- /dev/null +++ b/core/promise-io/http.js @@ -0,0 +1,411 @@ +/** + * A promise-based Q-JSGI server and client API. + * @module + */ + +/*whatsupdoc*/ + +var HTTP = require("http"); // node +var HTTPS = require("https"); // node +var URL = require("url2"); // node +var Q = require("q-bluebird"); +var Reader = require("./reader"); + +/** + * @param {respond(request Request)} respond a JSGI responder function that + * receives a Request object as its argument. The JSGI responder promises to + * return an object of the form `{status, headers, body}`. The status and + * headers must be fully resolved, but the body may be a promise for an object + * with a `forEach(write(chunk String))` method, albeit an array of strings. + * The `forEach` method may promise to resolve when all chunks have been + * written. + * @returns a Node Server object. + */ +exports.Server = function (respond) { + var self = Object.create(exports.Server.prototype); + + var server = HTTP.createServer(function (_request, _response) { + var request = exports.ServerRequest(_request); + var response = exports.ServerResponse(_response); + + var closed = Q.defer(); + _request.on("end", function (error, value) { + if (error) { + closed.reject(error); + } else { + closed.resolve(value); + } + }); + + Q.when(request, function (request) { + return Q.when(respond(request, response), function (response) { + if (!response) + return; + + _response.writeHead(response.status, response.headers); + + if (response.onclose || response.onClose) + Q.when(closed, response.onclose || response.onClose); + + return Q.when(response.body, function (body) { + var length; + if ( + Array.isArray(body) && + (length = body.length) && + body.every(function (chunk) { + return typeof chunk === "string" + }) + ) { + body.forEach(function (chunk, i) { + if (i < length - 1) { + _response.write(chunk, response.charset); + } else { + _response.end(chunk, response.charset); + } + }); + } else if (body) { + var end; + var done = body.forEach(function (chunk) { + end = Q.when(end, function () { + return Q.when(chunk, function (chunk) { + _response.write(chunk, response.charset); + }); + }); + }); + return Q.when(done, function () { + return Q.when(end, function () { + _response.end(); + }); + }); + } else { + _response.end(); + } + }); + + }) + }) + .done(); // should be .fail(self.emitter("error")) + + }); + + var stopped = Q.defer(); + server.on("close", function (err) { + if (err) { + stopped.reject(err); + } else { + stopped.resolve(); + } + }); + + /*** + * Stops the server. + * @returns {Promise * Undefined} a promise that will + * resolve when the server is stopped. + */ + self.stop = function () { + server.close(); + listening = undefined; + return stopped.promise; + }; + + var listening = Q.defer(); + server.on("listening", function (err) { + if (err) { + listening.reject(err); + } else { + listening.resolve(self); + } + }); + + /*** + * Starts the server, listening on the given port + * @param {Number} port + * @returns {Promise * Undefined} a promise that will + * resolve when the server is ready to receive + * connections + */ + self.listen = function (/*...args*/) { + if (typeof server.port !== "undefined") + return Q.reject(new Error("A server cannot be restarted or " + + "started on a new port")); + server.listen.apply(server, arguments); + return listening.promise; + }; + + self.stopped = stopped.promise; + + self.node = server; + self.nodeServer = server; // Deprecated + self.address = server.address.bind(server); + + return self; +}; + +Object.defineProperties(exports.Server, { + + port: { + get: function () { + return this.node.port; + } + }, + + host: { + get: function () { + return this.node.host; + } + } + +}); + +/** + * A wrapper for a Node HTTP Request, as received by + * the Q HTTP Server, suitable for use by the Q HTTP Client. + */ + +exports.ServerRequest = function (_request, ssl) { + var request = Object.create(_request, requestDescriptor); + /*** {Array} HTTP version. (JSGI) */ + request.version = _request.httpVersion.split(".").map(Math.floor); + /*** {String} HTTP method, e.g., `"GET"` (JSGI) */ + request.method = _request.method; + /*** {String} path, starting with `"/"` */ + request.path = _request.url; + /*** {String} pathInfo, starting with `"/"`, the + * portion of the path that has not yet + * been routed (JSGI) */ + request._pathInfo = null; + /*** {String} scriptName, the portion of the path that + * has already been routed (JSGI) */ + request.scriptName = ""; + /*** {String} (JSGI) */ + request.scheme = "http"; + + var address = _request.connection.address(); + /*** {String} hostname */ + if (_request.headers.host) { + request.hostname = _request.headers.host.split(":")[0]; + } else { + request.hostname = address.address; + } + /*** {String} host */ + request.port = address.port; + var defaultPort = request.port === (ssl ? 443 : 80); + request.host = request.hostname + (defaultPort ? "" : ":" + request.port); + + var socket = _request.socket; + /*** {String} */ + request.remoteHost = socket.remoteAddress; + /*** {Number} */ + request.remotePort = socket.remotePort; + + /*** {String} url */ + request.url = URL.format({ + protocol: request.scheme, + host: _request.headers.host, + port: request.port === (ssl ? 443 : 80) ? null : request.port, + path: request.path + }); + /*** A Q IO asynchronous text reader */ + request.body = Reader(_request); + /*** {Object} HTTP headers (JSGI)*/ + request.headers = _request.headers; + /*** The underlying Node request */ + request.node = _request; + request.nodeRequest = _request; // Deprecated + /*** The underlying Node TCP connection */ + request.nodeConnection = _request.connection; + + return Q.when(request.body, function (body) { + request.body = body; + return request; + }); +}; + +var requestDescriptor = { + pathInfo: { + get: function () { + // Postpone this until the server requests it because + // decodeURIComponent may throw an error if the path is not valid. + // If we throw while making a server request, it will crash the + // server and be uncatchable. + if (this._pathInfo === null) { + this._pathInfo = decodeURIComponent(URL.parse(this.url).pathname); + } + return this._pathInfo; + }, + set: function (pathInfo) { + this._pathInfo = pathInfo; + } + } +}; + +exports.ServerResponse = function (_response, ssl) { + var response = Object.create(_response); + response.ssl = ssl; + response.node = _response; + response.nodeResponse = _response; // Deprecated + return response; +}; + +exports.normalizeRequest = function (request) { + if (typeof request === "string") { + request = {url: request}; + } + request.method = request.method || "GET"; + request.headers = request.headers || {}; + if (request.url) { + var url = URL.parse(request.url); + request.ssl = url.protocol === "https:"; + request.hostname = url.hostname; + request.host = url.host; + request.port = +url.port; + request.path = (url.pathname || "") + (url.search || ""); + request.auth = url.auth || void 0; + } + request.host = request.host || request.headers.host; + request.port = request.port || (request.ssl ? 443 : 80); + if (request.host && !request.hostname) { + request.hostname = request.host.split(":")[0]; + } + if (request.hostname && request.port && !request.host) { + var defaultPort = request.ssl ? 443 : 80; + request.host = request.hostname + (defaultPort ? "" : ":" + request.port); + } + request.headers.host = request.headers.host || request.host; + request.path = request.path || "/"; + return request; +}; + +exports.normalizeResponse = function (response) { + if (response === void 0) { + return; + } + if (typeof response == "string") { + response = [response]; + } + if (response.forEach) { + response = { + status: 200, + headers: {}, + body: response + } + } + return response; +}; + +/** + * Issues an HTTP request. + * + * @param {Request {host, port, method, path, headers, + * body}} request (may be a promise) + * @returns {Promise * Response} promise for a response + */ +exports.request = function (request) { + return Q.when(request, function (request) { + + request = exports.normalizeRequest(request); + + var deferred = Q.defer(); + var http = request.ssl ? HTTPS : HTTP; + + var requestOptions = { + hostname: request.hostname, + port: request.port || (request.ssl ? 443 : 80), + localAddress: request.localAddress, + socketPath: request.socketPath, + method: request.method, + path: request.path, + headers: request.headers, + auth: request.auth // Generates the appropriate header + }; + + if (request.agent !== undefined) { + requestOptions.agent = request.agent; + } + + var _request = http.request(requestOptions, function (_response) { + deferred.resolve(exports.ClientResponse(_response, request.charset)); + _response.on("error", function (error) { + // TODO find a better way to channel + // this into the response + console.warn(error && error.stack || error); + deferred.reject(error); + }); + }); + + _request.on("error", function (error) { + deferred.reject(error); + }); + + Q.when(request.body, function (body) { + var end, done; + if (body) { + done = body.forEach(function (chunk) { + end = Q.when(end, function () { + return Q.when(chunk, function (chunk) { + _request.write(chunk, request.charset); + }); + }); + }); + } + return Q.when(end, function () { + return Q.when(done, function () { + _request.end(); + }); + }); + }).done(); + + return deferred.promise; + }); +}; + +/** + * Issues a GET request to the given URL and returns + * a promise for a `String` containing the entirety + * of the response. + * + * @param {String} url + * @returns {Promise * String} or a rejection if the + * status code is not exactly 200. The reason for the + * rejection is the full response object. + */ +exports.read = function (request, qualifier) { + qualifier = qualifier || function (response) { + return response.status === 200; + }; + return Q.when(exports.request(request), function (response) { + if (!qualifier(response)){ + var error = new Error("HTTP request failed with code " + response.status); + error.response = response; + throw error; + } + return Q.post(response.body, 'read', []); + }); +}; + + +/** + * A wrapper for the Node HTTP Response as provided + * by the Q HTTP Client API, suitable for use by the + * Q HTTP Server API. + */ +exports.ClientResponse = function (_response, charset) { + var response = Object.create(exports.ClientResponse.prototype); + /*** {Number} HTTP status code */ + response.status = _response.statusCode; + /*** HTTP version */ + response.version = _response.httpVersion; + /*** {Object} HTTP headers */ + response.headers = _response.headers; + /*** + * A Q IO asynchronous text reader. + */ + response.node = _response; + response.nodeResponse = _response; // Deprecated + response.nodeConnection = _response.connection; // Deprecated + return Q.when(Reader(_response, charset), function (body) { + response.body = body; + return response; + }); +}; + diff --git a/core/promise-io/package.json b/core/promise-io/package.json new file mode 100644 index 0000000000..b4dc283d68 --- /dev/null +++ b/core/promise-io/package.json @@ -0,0 +1,50 @@ +{ + "name": "q-io", + "version": "1.11.6", + "description": "IO using Q promises", + "homepage": "http://github.com/kriskowal/q-io/", + "author": "Kris Kowal (http://github.com/kriskowal/)", + "contributors": [ + "Montage Studio", + "Stuart Knightley", + "Jean-romain Prevost", + "Andreas Pizsa (http://github.com/AndreasPizsa/)" + ], + "bugs": { + "mail": "kris@cixar.com", + "url": "http://github.com/kriskowal/q-io/issues" + }, + "licenses": [ + { + "type": "MIT", + "url": "http://github.com/kriskowal/q-io/raw/master/LICENSE" + } + ], + "repository": { + "type": "git", + "url": "http://github.com/kriskowal/q-io.git" + }, + "dependencies": { + "q-bluebird": "0.0.1", + "qs": "^1.2.1", + "url2": "^0.0.0", + "mime": "^1.2.11", + "mimeparse": "^0.1.4", + "collections": "../collections" + }, + "devDependencies": { + "jshint": "^0.9.1", + "cover": "^0.2.8", + "jasmine-node": "^1.7", + "opener": "^1.3" + }, + "scripts": { + "test": "jasmine-node spec", + "test-browser": "opener spec/q-spec.html", + "lint": "jshint q.js", + "cover": "cover run jasmine-node spec && cover report html && opener cover_html/index.html" + }, + "engines": { + "node": ">=0.6.0" + } +} diff --git a/core/promise-io/reader.js b/core/promise-io/reader.js new file mode 100644 index 0000000000..9ab12f10be --- /dev/null +++ b/core/promise-io/reader.js @@ -0,0 +1,133 @@ + +var Q = require("q-bluebird"); + +/** + * Wraps a Node readable stream, providing an API similar + * to a Narwhal synchronous `io` stream except returning + * Q promises for long latency operations. + * @param stream any Node readable stream + * @returns {Promise * Reader} a promise for + * the text stream reader. + * @constructor + */ +module.exports = Reader; +function Reader(_stream, charset) { + var self = Object.create(Reader.prototype); + + if (charset && _stream.setEncoding) // TODO complain about inconsistency + _stream.setEncoding(charset); + + var begin = Q.defer(); + var end = Q.defer(); + + _stream.on("error", function (reason) { + begin.reject(reason); + }); + + var chunks = []; + var receiver; + + _stream.on("end", function () { + begin.resolve(self); + end.resolve() + }); + + _stream.on("data", function (chunk) { + begin.resolve(self); + if (receiver) { + receiver(chunk); + } else { + chunks.push(chunk); + } + }); + + function slurp() { + var result; + if (charset) { + result = chunks.join(""); + } else { + result = self.constructor.join(chunks); + } + chunks.splice(0, chunks.length); + return result; + } + + /*** + * Reads all of the remaining data from the stream. + * @returns {Promise * String} a promise for a String + * containing the entirety the remaining stream. + */ + self.read = function () { + receiver = undefined; + var deferred = Q.defer(); + Q.done(end.promise, function () { + deferred.resolve(slurp()); + }); + return deferred.promise; + }; + + /*** + * Reads and writes all of the remaining data from the + * stream in chunks. + * @param {Function(Promise * String)} write a function + * to be called on each chunk of input from this stream. + * @returns {Promise * Undefined} a promise that will + * be resolved when the input is depleted. + */ + self.forEach = function (write) { + if (chunks && chunks.length) + write(slurp()); + receiver = write; + return Q.when(end.promise, function () { + receiver = undefined; + }); + }; + + self.close = function () { + _stream.destroy(); + }; + + self.node = _stream; + + return begin.promise; +} + +/* + Reads an entire forEachable stream of buffers and returns a single buffer. +*/ +Reader.read = read; +function read(stream, charset) { + var chunks = []; + stream.forEach(function (chunk) { + chunks.push(chunk); + }); + if (charset) { + return chunks.join(""); + } else { + return join(chunks); + } +} + +Reader.join = join; +function join(buffers) { + var length = 0; + var at; + var i; + var ii = buffers.length; + var buffer; + var result; + for (i = 0; i < ii; i++) { + buffer = buffers[i]; + length += buffer.length; + } + result = new Buffer(length); + at = 0; + for (i = 0; i < ii; i++) { + buffer = buffers[i]; + buffer.copy(result, at, 0); + at += buffer.length; + } + buffers.splice(0, ii, result); + return result; +} + diff --git a/core/promise-io/spec/fs/boot-directory-spec.js b/core/promise-io/spec/fs/boot-directory-spec.js new file mode 100644 index 0000000000..0955dd1286 --- /dev/null +++ b/core/promise-io/spec/fs/boot-directory-spec.js @@ -0,0 +1,47 @@ + +var FS = require("../../fs-boot"); +var normalize = FS.normal; + +var specs = [ + { + "from": "foo", + "to": "" + }, + { + "from": "", + "to": ".." + }, + { + "from": ".", + "to": ".." + }, + { + "from": "..", + "to": normalize("../..") + }, + { + "from": "../foo", + "to": ".." + }, + { + "from": "/foo/bar", + "to": normalize("/foo") + }, + { + "from": "/foo", + "to": normalize("/") + }, + { + "from": "/", + "to": "/" + } +]; + +describe("fs-boot directory", function () { + specs.forEach(function (spec) { + it("should parse " + JSON.stringify(spec.from), function () { + expect(FS.directory(spec.from)).toBe(spec.to); + }); + }); +}); + diff --git a/core/promise-io/spec/fs/contains-spec.js b/core/promise-io/spec/fs/contains-spec.js new file mode 100644 index 0000000000..3f9c1afee7 --- /dev/null +++ b/core/promise-io/spec/fs/contains-spec.js @@ -0,0 +1,11 @@ + +var FS = require("../../fs"); + +require("../lib/jasmine-promise"); + +describe("contains", function () { + it("reflexive case", function () { + expect(FS.contains("/a/b", "/a/b")).toBe(true); + }); +}); + diff --git a/core/promise-io/spec/fs/fixtures/hello.txt b/core/promise-io/spec/fs/fixtures/hello.txt new file mode 100644 index 0000000000..8ab686eafe --- /dev/null +++ b/core/promise-io/spec/fs/fixtures/hello.txt @@ -0,0 +1 @@ +Hello, World! diff --git a/core/promise-io/spec/fs/issues/1-spec.js b/core/promise-io/spec/fs/issues/1-spec.js new file mode 100644 index 0000000000..f1ab40d93b --- /dev/null +++ b/core/promise-io/spec/fs/issues/1-spec.js @@ -0,0 +1,33 @@ + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); + +describe("write and remove", function () { + + it("should write and remove a file", function () { + + var fixture = FS.join(__dirname, "fixture.txt"); + + return FS.write(fixture, "1234") + .then(function (result) { + expect(result).toBe(undefined); + }) + + .then(function () { + return FS.remove(fixture); + }) + .then(function (result) { + expect(result).toBe(undefined); + }) + + .then(function () { + return FS.exists(fixture) + }) + .then(function (exists) { + expect(exists).toBe(false); + }) + + }); +}); + diff --git a/core/promise-io/spec/fs/make-tree-spec.js b/core/promise-io/spec/fs/make-tree-spec.js new file mode 100644 index 0000000000..9000cfd30a --- /dev/null +++ b/core/promise-io/spec/fs/make-tree-spec.js @@ -0,0 +1,92 @@ +"use strict"; + +require("../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../fs"); +var _n = FS.normal; + +describe("makeTree", function () { + it("should make a branch of a tree", function () { + + return Q.fcall(function () { + return FS.makeTree("a/b/c"); + }) + + .then(function () { + return FS.listTree("a"); + }) + .then(function (list) { + expect(list).toEqual([ + "a", + _n("a/b"), + _n("a/b/c") + ]); + }) + + .then(function () { + return FS.exists("a/b/c"); + }) + .then(function (exists) { + expect(exists).toBe(true); + }) + + .then(function () { + return FS.isDirectory("a/b/c"); + }) + .then(function (isDirectory) { + expect(isDirectory).toBe(true); + }) + + }); + + it("should make a branch of a tree even if some of it already exists", function () { + + return Q.fcall(function () { + return FS.makeTree("a/b/c/d"); + }) + + .then(function () { + return FS.listTree("a"); + }) + .then(function (list) { + expect(list).toEqual([ + "a", + _n("a/b"), + _n("a/b/c"), + _n("a/b/c/d") + ]); + }) + .then(function () { + return FS.removeTree("a"); + }) + }); + + it("should make branch from an absolute path", function () { + + return Q.fcall(function () { + return FS.makeTree(FS.absolute("a/b/c/d")); + }) + + .then(function () { + return FS.listTree("a"); + }) + .then(function (list) { + expect(list).toEqual([ + "a", + _n("a/b"), + _n("a/b/c"), + _n("a/b/c/d") + ]); + }) + .then(function () { + return FS.removeTree("a"); + }) + }); + + it("should tolerate .", function () { + return Q.fcall(function () { + return FS.makeTree("."); + }) + }); +}); + diff --git a/core/promise-io/spec/fs/mock/append-spec.js b/core/promise-io/spec/fs/mock/append-spec.js new file mode 100644 index 0000000000..887836b097 --- /dev/null +++ b/core/promise-io/spec/fs/mock/append-spec.js @@ -0,0 +1,41 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); +/*global describe,it,expect */ + +describe("append", function () { + + it("appends to a file on a mock filesystem", function () { + return FS.mock(FS.join(__dirname, "fixture")) + .then(function (mock) { + return Q.fcall(function () { + return mock.append("hello.txt", "Goodbye!\n"); + }) + .then(function () { + return mock.read("hello.txt"); + }) + .then(function (contents) { + expect(contents).toBe("Hello, World!\nGoodbye!\n"); + }); + }); + }); + + it("calls open correctly", function () { + return FS.mock(FS.join(__dirname, "fixture")) + .then(function (mock) { + mock.open = function (path, options) { + expect(path).toBe("hello.txt"); + expect(options.flags).toBe("a"); + expect(options.charset).toBe("utf8"); + + return Q.resolve({write: function () {}, close: function () {}}); + }; + + return mock.append("hello.txt", "Goodbye!\n", "a", "utf8"); + }); + }); + +}); + diff --git a/core/promise-io/spec/fs/mock/copy-tree-spec.js b/core/promise-io/spec/fs/mock/copy-tree-spec.js new file mode 100644 index 0000000000..2093b6290e --- /dev/null +++ b/core/promise-io/spec/fs/mock/copy-tree-spec.js @@ -0,0 +1,91 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); +var Mock = require("../../../fs-mock"); +var normalize = FS.normal; + +describe("copyTree", function () { + it("should copy a tree", function () { + + var mock = Mock({ + "a/b": { + "c": { + "d": 66, + "e": 99 + } + } + }); + + return Q.fcall(function () { + return mock.copyTree("a/b", "a/f"); + }) + .then(function () { + return Q.all([ + mock.isDirectory("a/f"), + mock.exists("a/f/c"), + mock.isFile("a/f/c/d"), + mock.read("a/f/c/e") + ]) + }) + .then(function (existence) { + expect(existence.every(Boolean)).toBe(true); + }) + + .then(function () { + return mock.listTree(); + }) + .then(function (list) { + expect(list).toEqual([ + ".", + "a", + normalize("a/b"), + normalize("a/b/c"), + normalize("a/b/c/d"), + normalize("a/b/c/e"), + normalize("a/f"), + normalize("a/f/c"), + normalize("a/f/c/d"), + normalize("a/f/c/e") + ]); + }) + + }); + + it("should preserve permissions", function () { + var mock = Mock({ + "a/b": { + "c": { + "d": 66, + "e": 99 + } + } + }); + + var mode0777 = parseInt("0777", 8); + var mode0700 = parseInt("0700", 8); + + return Q.all([ + mock.chmod("a/b/c/d", mode0777), + mock.chmod("a/b", mode0700), + mock.chmod("a/b/c", mode0700) + ]) + .then(function () { + return mock.copyTree("a/b", "a/f"); + }) + .then(function () { + return Q.all([ + mock.stat("a/f/c/d"), + mock.stat("a/f"), + mock.stat("a/f/c") + ]); + }) + .spread(function (dStat, fStat, cStat) { + expect(dStat.node.mode & mode0777).toEqual(mode0777); + expect(fStat.node.mode & mode0777).toEqual(mode0700); + expect(cStat.node.mode & mode0777).toEqual(mode0700); + }); + }); +}); + diff --git a/core/promise-io/spec/fs/mock/fixture/hello.txt b/core/promise-io/spec/fs/mock/fixture/hello.txt new file mode 100644 index 0000000000..8ab686eafe --- /dev/null +++ b/core/promise-io/spec/fs/mock/fixture/hello.txt @@ -0,0 +1 @@ +Hello, World! diff --git a/core/promise-io/spec/fs/mock/link-spec.js b/core/promise-io/spec/fs/mock/link-spec.js new file mode 100644 index 0000000000..30cb46da01 --- /dev/null +++ b/core/promise-io/spec/fs/mock/link-spec.js @@ -0,0 +1,70 @@ + +require("../../lib/jasmine-promise"); +var MockFs = require("../../../fs-mock"); +var normalize = require('../../../fs').normal; + +describe("link", function () { + it("should", function () { + var mock = MockFs(); + + // make some content + return mock.makeTree("a/b") + .then(function () { + return mock.write("a/b/c.txt", "Hello, World!") + }) + + // verify content + .then(function () { + return mock.read("a/b/c.txt") + }) + .then(function (content) { + expect(content).toBe("Hello, World!"); + }) + + // link it + .then(function () { + return mock.link("a/b/c.txt", "a/b/d.txt") + }) + + // should be non-destructive + .then(function () { + return mock.read("a/b/c.txt") + }) + .then(function (content) { + expect(content).toBe("Hello, World!"); + }) + + // should be listed + .then(function () { + return mock.listTree() + }) + .then(function (content) { + expect(content).toEqual([ + ".", + "a", + normalize("a/b"), + normalize("a/b/c.txt"), + normalize("a/b/d.txt") + ]) + }) + + // should be identified as a file + .then(function () { + return mock.isFile("a/b/d.txt"); + }) + .then(function (isFile) { + expect(isFile).toBe(true); + }) + + // should have the same content + .then(function () { + return mock.read("a/b/d.txt"); + }) + .then(function (content) { + expect(content).toBe("Hello, World!"); + }); + + }); +}); + + diff --git a/core/promise-io/spec/fs/mock/make-tree-spec.js b/core/promise-io/spec/fs/mock/make-tree-spec.js new file mode 100644 index 0000000000..ab12b00a4a --- /dev/null +++ b/core/promise-io/spec/fs/mock/make-tree-spec.js @@ -0,0 +1,109 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); +var Mock = require("../../../fs-mock"); +var normalize = FS.normal; + +describe("makeTree", function () { + it("should make a branch of a tree", function () { + + var mock = Mock({ + "a": {} + }); + + return Q.fcall(function () { + return mock.makeTree("a/b/c"); + }) + + .then(function () { + return mock.listTree(); + }) + .then(function (list) { + expect(list).toEqual([ + ".", + "a", + normalize("a/b"), + normalize("a/b/c") + ]); + }) + + .then(function () { + return mock.exists("a/b/c") + }) + .then(function (exists) { + expect(exists).toBe(true); + }) + + .then(function () { + return mock.isDirectory("a/b/c") + }) + .then(function (isDirectory) { + expect(isDirectory).toBe(true); + }) + + }); + + it("should make a branch of a tree even if some of it already exists", function () { + + var mock = Mock({ + "a/b": {} + }); + + return Q.fcall(function () { + return mock.makeTree("a/b/c/d"); + }) + + .then(function () { + return mock.listTree(); + }) + .then(function (list) { + expect(list).toEqual([ + ".", + "a", + normalize("a/b"), + normalize("a/b/c"), + normalize("a/b/c/d") + ]); + }) + }); + + it("should make an absolute tree from a subdirectory", function () { + var mock = Mock({ + "a/b": { + "c": { + "d.ext": 66 + } + } + }, "/a/b"); + + return Q.fcall(function () { + return mock.makeTree("/a/b/c/x/y/z"); + }) + .then(function () { + return Q.all([ + mock.isDirectory("a/b/c/x"), + mock.isDirectory("a/b/c/x/y"), + mock.isDirectory("a/b/c/x/y/z") + ]); + }) + .then(function () { + return mock.listTree("/"); + }) + .then(function (list) { + expect(list).toEqual([ + "/", + "/a", + "/a/b", + "/a/b/c", + "/a/b/c/d.ext", + "/a/b/c/x", + "/a/b/c/x/y", + "/a/b/c/x/y/z" + ]); + }); + }); + +}); + diff --git a/core/promise-io/spec/fs/mock/merge-spec.js b/core/promise-io/spec/fs/mock/merge-spec.js new file mode 100644 index 0000000000..469b73ee69 --- /dev/null +++ b/core/promise-io/spec/fs/mock/merge-spec.js @@ -0,0 +1,67 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); +var Mock = require("../../../fs-mock"); +var normalize = FS.normal; + +describe("makeTree", function () { + it("should make a branch of a tree", function () { + + var merged = FS.merge([ + Mock({ + "a": 10, + "b": 20, + "1/2/3": "123" + }), + Mock({ + "a": 20, + "c": 30 + }), + Mock({ + "a": 30, + "d": 40 + }), + ]) + + return merged.then(function (merged) { + + return Q.fcall(function () { + return merged.listTree(); + }) + .then(function (list) { + expect(list.sort()).toEqual([ + ".", + "1", + normalize("1/2"), + normalize("1/2/3"), + "a", + "b", + "c", + "d", + ]); + }) + + // overridden by a previous tree + .then(function () { + return merged.read("a"); + }) + .then(function (a) { + expect(a).toBe("30"); + }) + + // not overridden + .then(function () { + return merged.read("b"); + }) + .then(function (a) { + expect(a).toBe("20"); + }) + + }) + + }); + +}); + diff --git a/core/promise-io/spec/fs/mock/move-spec.js b/core/promise-io/spec/fs/mock/move-spec.js new file mode 100644 index 0000000000..07221e56f1 --- /dev/null +++ b/core/promise-io/spec/fs/mock/move-spec.js @@ -0,0 +1,219 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); +var Mock = require("../../../fs-mock"); + +describe("move", function () { + it("should move", function () { + + return FS.mock(FS.join(__dirname, "fixture")) + .then(function (mock) { + + // initial list + return Q.fcall(function () { + return mock.listTree() + }) + .then(function (list) { + expect(list).toEqual([ + ".", + "hello.txt" + ]); + }) + + // initial content + .then(function () { + return mock.read("hello.txt"); + }) + .then(function (content) { + expect(content).toBe("Hello, World!\n"); + }) + + // move! + .then(function () { + return mock.move("hello.txt", "new-hello.txt"); + }) + + // list after + .then(function () { + return mock.listTree(); + }) + .then(function (list) { + expect(list).toEqual([ + ".", + "new-hello.txt" + ]); + }) + + // content after + .then(function () { + return mock.read("new-hello.txt"); + }) + .then(function (content) { + expect(content).toBe("Hello, World!\n"); + }) + + }); + }); + + it("should not delete a node if the source and target are the same", function () { + + return FS.mock(FS.join(__dirname, "fixture")) + .then(function (mock) { + + // initial list + return Q.fcall(function () { + return mock.listTree() + }) + .then(function (list) { + expect(list).toEqual([ + ".", + "hello.txt" + ]); + }) + + // initial content + .then(function () { + return mock.read("hello.txt"); + }) + .then(function (content) { + expect(content).toBe("Hello, World!\n"); + }) + + // move! + .then(function () { + return mock.move("hello.txt", "hello.txt"); + }) + + // list after + .then(function () { + return mock.listTree(); + }) + .then(function (list) { + expect(list).toEqual([ + ".", + "hello.txt" + ]); + }) + + // content after + .then(function () { + return mock.read("hello.txt"); + }) + .then(function (content) { + expect(content).toBe("Hello, World!\n"); + }) + }); + + }); + + it("should not delete a node if the source and target refer to the same node", function () { + + return FS.mock(FS.join(__dirname, "fixture")) + .then(function (mock) { + + return Q.fcall(function () { + return Q.all([ + mock.symbolicCopy("hello.txt", "a.txt"), + mock.symbolicCopy("hello.txt", "b.txt"), + ]); + }) + + // initial list + .then(function () { + return mock.listTree() + }) + .then(function (list) { + expect(list).toEqual([ + ".", + "a.txt", + "b.txt", + "hello.txt" + ]); + }) + + // initial content + .then(function () { + return mock.read("hello.txt"); + }) + .then(function (content) { + expect(content).toBe("Hello, World!\n"); + }) + + // move! + .then(function () { + return mock.move("a.txt", "b.txt"); + }) + + // list after + .then(function () { + return mock.listTree(); + }) + .then(function (list) { + expect(list).toEqual([ + ".", + "a.txt", + "b.txt", + "hello.txt" + ]); + }) + + // content after + .then(function () { + return mock.read("hello.txt"); + }) + .then(function (content) { + expect(content).toBe("Hello, World!\n"); + }) + }); + + }); + + it("should fail to move over an existing directory", function () { + var mock = Mock({ + "hi.txt": "Hello, World!", + "hello": {} + }); + + return Q.fcall(function () { + return mock.isDirectory("/hello"); + }) + .then(function (isDirectory) { + expect(isDirectory).toBe(true); + }) + + .then(function () { + return mock.move("/hi.txt", "/hello"); + }) + .then(function () { + throw new Error("Move should not succeed."); + }, function (error) { + }) + + }); + + it("should fail to move over an existing directory of the same name", function () { + var mock = Mock({ + "hello": {} + }); + + return Q.fcall(function () { + return mock.isDirectory("/hello"); + }) + .then(function (isDirectory) { + expect(isDirectory).toBe(true); + }) + + .then(function () { + return mock.move("/hello", "/hello"); + }) + .then(function () { + throw new Error("Move should not succeed."); + }, function (error) { + }) + + }); + +}); + diff --git a/core/promise-io/spec/fs/mock/object-spec.js b/core/promise-io/spec/fs/mock/object-spec.js new file mode 100644 index 0000000000..27062c3861 --- /dev/null +++ b/core/promise-io/spec/fs/mock/object-spec.js @@ -0,0 +1,20 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); + +describe("toObject", function () { + it("should take a snapshot of a tree", function () { + + return FS.mock(FS.join(__dirname, "fixture")) + .invoke("toObject") + .then(function (tree) { + + expect(tree["hello.txt"].toString("utf-8")).toEqual("Hello, World!\n"); + expect(Object.keys(tree)).toEqual(["hello.txt"]); + + }); + }); +}); + diff --git a/core/promise-io/spec/fs/mock/range-spec.js b/core/promise-io/spec/fs/mock/range-spec.js new file mode 100644 index 0000000000..8103b9e508 --- /dev/null +++ b/core/promise-io/spec/fs/mock/range-spec.js @@ -0,0 +1,26 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); +var Mock = require("../../../fs-mock"); + +describe("open range", function () { + it("read a partial range of a file", function () { + + return FS.mock(FS.join(__dirname, "fixture")) + .then(function (mock) { + + return mock.read("hello.txt", { + begin: 1, + end: 3 + }) + .then(function (content) { + expect(content).toBe("el"); + }) + + }); + + }); +}); + diff --git a/core/promise-io/spec/fs/mock/read-spec.js b/core/promise-io/spec/fs/mock/read-spec.js new file mode 100644 index 0000000000..8fd721a5ad --- /dev/null +++ b/core/promise-io/spec/fs/mock/read-spec.js @@ -0,0 +1,40 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); +/*global describe,it,expect */ + +describe("read", function () { + it("should read a file from a mock filesystem", function () { + + return FS.mock(FS.join(__dirname, "fixture")) + .then(function (mock) { + + return Q.fcall(function () { + return mock.read("hello.txt"); + }) + .then(function (content) { + expect(content).toBe("Hello, World!\n"); + }) + + }); + }); + + it("calls open correctly", function () { + return FS.mock(FS.join(__dirname, "fixture")) + .then(function (mock) { + mock.open = function (path, options) { + expect(path).toBe("hello.txt"); + expect(options.flags).toBe("a"); + expect(options.charset).toBe("utf8"); + + return Q.resolve({read: function () {}, close: function () {}}); + }; + + return mock.read("hello.txt", "a", "utf8"); + }); + }); + +}); + diff --git a/core/promise-io/spec/fs/mock/remove-directory-spec.js b/core/promise-io/spec/fs/mock/remove-directory-spec.js new file mode 100644 index 0000000000..624f3fa232 --- /dev/null +++ b/core/promise-io/spec/fs/mock/remove-directory-spec.js @@ -0,0 +1,37 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); + +describe("removeDirectory", function () { + it("should remove a directory", function () { + + return FS.mock(FS.join(__dirname)) + .then(function (mock) { + + // now you see it + return Q.fcall(function () { + return mock.isDirectory("fixture"); + }) + .then(function (isDirectory) { + expect(isDirectory).toBe(true); + }) + + .then(function () { + return mock.removeDirectory("fixture"); + }) + + // now you don't + .then(function () { + return mock.isDirectory("fixture"); + }) + .then(function (isDirectory) { + expect(isDirectory).toBe(false); + }) + + }); + + }); +}); + diff --git a/core/promise-io/spec/fs/mock/remove-tree-spec.js b/core/promise-io/spec/fs/mock/remove-tree-spec.js new file mode 100644 index 0000000000..b81ccf0b65 --- /dev/null +++ b/core/promise-io/spec/fs/mock/remove-tree-spec.js @@ -0,0 +1,39 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); +var Mock = require("../../../fs-mock"); +var normalize = FS.normal; + +describe("removeTree", function () { + it("should remove a tree", function () { + + var mock = Mock({ + "a/b": { + "c": { + "d": 66, + "e": 99 + } + } + }); + + return Q.fcall(function () { + return mock.removeTree("a/b/c"); + }) + + .then(function () { + return mock.listTree(); + }) + .then(function (list) { + expect(list).toEqual([ + ".", + "a", + normalize("a/b") + ]); + }) + + }); + +}); + diff --git a/core/promise-io/spec/fs/mock/root-spec.js b/core/promise-io/spec/fs/mock/root-spec.js new file mode 100644 index 0000000000..a3612d94bc --- /dev/null +++ b/core/promise-io/spec/fs/mock/root-spec.js @@ -0,0 +1,32 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); +var Mock = require("../../../fs-mock"); +var Root = require("../../../fs-root"); + +describe("root", function () { + it("should make a filesystem from a subtree of a mock filesystem", function () { + + var mock = Mock({ + "a/b/1": 10, + "a/b/2": 20, + "a/b/3": 30 + }); + + var chroot = Root(mock, "a/b"); + + return chroot.invoke("listTree") + .then(function (list) { + expect(list).toEqual([ + ".", + "1", + "2", + "3" + ]); + }); + + }); +}); + diff --git a/core/promise-io/spec/fs/mock/stat-spec.js b/core/promise-io/spec/fs/mock/stat-spec.js new file mode 100644 index 0000000000..9ff01122f7 --- /dev/null +++ b/core/promise-io/spec/fs/mock/stat-spec.js @@ -0,0 +1,26 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); +/*global describe,it,expect */ + +describe("stat", function () { + it("should return a Stats object", function () { + return FS.mock(FS.join(__dirname, "fixture")) + .then(function (mock) { + return mock.stat("hello.txt"); + }) + .then(function (stat) { + expect(stat.node).toBeDefined(); + expect(stat.size).toBeDefined(); + expect(stat.size).toBeGreaterThan(0); + expect(stat.isDirectory()).toBe(false); + expect(stat.isFile()).toBe(true); + }); + }); + + + +}); + diff --git a/core/promise-io/spec/fs/mock/symbolic-link-spec.js b/core/promise-io/spec/fs/mock/symbolic-link-spec.js new file mode 100644 index 0000000000..ffdd03acdc --- /dev/null +++ b/core/promise-io/spec/fs/mock/symbolic-link-spec.js @@ -0,0 +1,86 @@ + +require("../../lib/jasmine-promise"); +var MockFs = require("../../../fs-mock"); +var normalize = require('../../../fs').normal; + +describe("symbolic link", function () { + it("should", function () { + var mock = MockFs(); + + // make some content + return mock.makeTree("a/b") + .then(function () { + return mock.write("a/b/c.txt", "Hello, World!") + }) + + // verify content + .then(function () { + return mock.read("a/b/c.txt") + }) + .then(function (content) { + expect(content).toBe("Hello, World!"); + }) + + // link it + .then(function () { + return mock.symbolicCopy("/a/b/c.txt", "a/b/d.txt", "file") + }) + + // should have a link + .then(function () { + return mock.readLink("a/b/d.txt") + }) + .then(function (link) { + expect(link).toBe("c.txt"); + }) + + // should have a canonical path + .then(function () { + return mock.canonical("a/b/d.txt") + }) + .then(function (canonical) { + expect(canonical).toBe("/a/b/c.txt"); + }) + + // should be listed + .then(function () { + return mock.listTree() + }) + .then(function (content) { + expect(content).toEqual([ + ".", + "a", + normalize("a/b"), + normalize("a/b/c.txt"), + normalize("a/b/d.txt") + ]) + }) + + // should be non-destructive + .then(function () { + return mock.read("a/b/c.txt") + }) + .then(function (content) { + expect(content).toBe("Hello, World!"); + }) + + // should be identified as a file + .then(function () { + return mock.isSymbolicLink("a/b/d.txt"); + }) + .then(function (isSymbolicLink) { + expect(isSymbolicLink).toBe(true); + }) + + // should have the same content + .then(function () { + return mock.read("a/b/d.txt"); + }) + .then(function (content) { + expect(content).toBe("Hello, World!"); + }); + + }); +}); + + diff --git a/core/promise-io/spec/fs/mock/working-directory-spec.js b/core/promise-io/spec/fs/mock/working-directory-spec.js new file mode 100644 index 0000000000..768a689340 --- /dev/null +++ b/core/promise-io/spec/fs/mock/working-directory-spec.js @@ -0,0 +1,31 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var Mock = require("../../../fs-mock"); + +describe("mock working directory", function () { + it("should perform actions relative to the given working directory", function () { + + var mock = Mock({ + "a/b": { + "c": { + "d": 66, + "e": 99 + } + } + }, "a/b/c"); + + return mock.listTree() + .then(function (list) { + expect(list).toEqual([ + ".", + "d", + "e" + ]); + }) + + }); + +}); + diff --git a/core/promise-io/spec/fs/mock/write-spec.js b/core/promise-io/spec/fs/mock/write-spec.js new file mode 100644 index 0000000000..b03a3a7a30 --- /dev/null +++ b/core/promise-io/spec/fs/mock/write-spec.js @@ -0,0 +1,73 @@ +"use strict"; + +require("../../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../../fs"); +/*global describe,it,expect */ + +describe("write", function () { + it("should write a file to a mock filesystem", function () { + return FS.mock(FS.join(__dirname, "fixture")) + .then(function (mock) { + + return Q.fcall(function () { + return mock.write("hello.txt", "Goodbye!\n"); + }) + .then(function () { + return mock.isFile("hello.txt"); + }) + .then(function (isFile) { + expect(isFile).toBe(true); + return mock.read("hello.txt"); + }) + .then(function (content) { + expect(content.toString("utf-8")).toBe("Goodbye!\n"); + }) + }); + }); + + it("takes a flags argument", function () { + return FS.mock(FS.join(__dirname, "fixture")) + .then(function (mock) { + return Q.fcall(function () { + return mock.write("hello.txt", "Goodbye!\n", "a"); + }) + .then(function () { + return mock.read("hello.txt"); + }) + .then(function (contents) { + expect(contents).toBe("Hello, World!\nGoodbye!\n"); + }); + }); + }); + + it("takes an options object with flags", function () { + return FS.mock(FS.join(__dirname, "fixture")) + .then(function (mock) { + return Q.fcall(function () { + return mock.write("hello.txt", "Goodbye!\n", { flags: "a" }); + }) + .then(function () { + return mock.read("hello.txt"); + }) + .then(function (contents) { + expect(contents).toBe("Hello, World!\nGoodbye!\n"); + }); + }); + }); + + it("calls open correctly", function () { + return FS.mock(FS.join(__dirname, "fixture")) + .then(function (mock) { + mock.open = function (path, options) { + expect(path).toBe("hello.txt"); + expect(options.flags).toBe("a"); + expect(options.charset).toBe("utf8"); + return Q.resolve({write: function () {}, close: function () {}}); + }; + + return mock.write("hello.txt", "Goodbye!\n", "a", "utf8"); + }); + }); +}); + diff --git a/core/promise-io/spec/fs/range-spec.js b/core/promise-io/spec/fs/range-spec.js new file mode 100644 index 0000000000..c230bc6501 --- /dev/null +++ b/core/promise-io/spec/fs/range-spec.js @@ -0,0 +1,23 @@ +"use strict"; + +require("../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var FS = require("../../fs"); + +describe("open range", function () { + it("read a partial range of a file", function () { + + var name = FS.join(module.directory || __dirname, "range-spec.txt"); + + return FS.open(name, { + begin: 1, + end: 3 + }) + .invoke("read") + .then(function (content) { + expect(content).toBe("23"); + }) + + }); +}); + diff --git a/core/promise-io/spec/fs/range-spec.txt b/core/promise-io/spec/fs/range-spec.txt new file mode 100644 index 0000000000..81c545efeb --- /dev/null +++ b/core/promise-io/spec/fs/range-spec.txt @@ -0,0 +1 @@ +1234 diff --git a/core/promise-io/spec/fs/read-spec.js b/core/promise-io/spec/fs/read-spec.js new file mode 100644 index 0000000000..592a658533 --- /dev/null +++ b/core/promise-io/spec/fs/read-spec.js @@ -0,0 +1,22 @@ + +require("../lib/jasmine-promise"); +var FS = require("../../fs"); + +describe("read", function () { + + it("should read in utf-8", function () { + return FS.read(FS.join(__dirname, "fixtures/hello.txt")) + .then(function (text) { + expect(text).toBe("Hello, World!\n"); + }); + }); + + it("should read in bytewise", function () { + return FS.read(FS.join(__dirname, "fixtures/hello.txt"), "b") + .then(function (bytes) { + expect(bytes instanceof Buffer).toBe(true); + }); + }); + +}); + diff --git a/core/promise-io/spec/fs/relative-spec.js b/core/promise-io/spec/fs/relative-spec.js new file mode 100644 index 0000000000..49035ceeca --- /dev/null +++ b/core/promise-io/spec/fs/relative-spec.js @@ -0,0 +1,25 @@ + +var FS = require("../../fs"); + +describe("relativeFromDirectory", function () { + + it("should find the relative path from a directory", function () { + + expect(FS.relativeFromDirectory("/a/b", "/a/b")).toBe(""); + expect(FS.relativeFromDirectory("/a/b/", "/a/b")).toBe(""); + expect(FS.relativeFromDirectory("/a/b", "/a/b/")).toBe(""); + expect(FS.relativeFromDirectory("/a/b/", "/a/b/")).toBe(""); + + expect(FS.relativeFromDirectory("/a/b", "/a/b/c")).toBe("c"); + expect(FS.relativeFromDirectory("/a/b/", "/a/b/c")).toBe("c"); + expect(FS.relativeFromDirectory("/a/b", "/a/b/c/")).toBe("c"); + expect(FS.relativeFromDirectory("/a/b/", "/a/b/c/")).toBe("c"); + + expect(FS.relativeFromDirectory("/a/b", "/a")).toBe(".."); + expect(FS.relativeFromDirectory("/a/b/", "/a")).toBe(".."); + expect(FS.relativeFromDirectory("/a/b", "/a/")).toBe(".."); + expect(FS.relativeFromDirectory("/a/b/", "/a/")).toBe(".."); + + }); + +}); diff --git a/core/promise-io/spec/fs/reroot-spec.js b/core/promise-io/spec/fs/reroot-spec.js new file mode 100644 index 0000000000..40672a0471 --- /dev/null +++ b/core/promise-io/spec/fs/reroot-spec.js @@ -0,0 +1,45 @@ +var FS = require("../../fs"); + +require("../lib/jasmine-promise"); + +describe("reroot", function () { + + it("should still have makeDirectory()", function() { + return FS.reroot("/") + .then(function(fs) { + expect(fs.makeTree instanceof Function).toBe(true); + expect(fs.makeDirectory instanceof Function).toBe(true); + }); + }); + + it("should have a makeDirectory() that creates within the attenuated root", function() { + var tmpdir = FS.join(__dirname, 'tmp'); + return FS.removeTree(tmpdir) + .then(null, function () { + // ignore fail + }).then(function() { + return FS.makeTree(tmpdir) + }).then(function () { + return FS.isDirectory(tmpdir); + }).then(function (isDirectory) { + if (!isDirectory) + throw new Error("Failed to create tmpdir"); + }).then(function() { + return FS.reroot(tmpdir); + }).then(function(fs) { + return fs.makeDirectory('/foo'); + }).then(function() { + var outerFooDir = FS.join(tmpdir, 'foo'); + return FS.isDirectory(outerFooDir); + }).then(function(isDirectory) { + if (!isDirectory) + throw new Error("Directory not created"); + }) + .finally(function () { + return FS.removeTree(tmpdir) + .then(null, function () { + // ignore fail + }) + }) + }); +}); diff --git a/core/promise-io/spec/fs/write-spec.js b/core/promise-io/spec/fs/write-spec.js new file mode 100644 index 0000000000..d7642be645 --- /dev/null +++ b/core/promise-io/spec/fs/write-spec.js @@ -0,0 +1,38 @@ + +require("../lib/jasmine-promise"); +var FS = require("../../fs"); + +describe("write", function () { + it("should write in utf-8", function () { + var path = FS.join(__dirname, "fixtures/so-it-is-written.txt"); + var content = "Hello, World!\n"; + return FS.write(path, content) + .then(function (result) { + expect(result).toBe(undefined); + return FS.read(path) + }) + .then(function (readContent) { + expect(readContent).toEqual(content); + }) + .finally(function () { + return FS.remove(path); + }); + }); + + it("should write bytewise", function () { + var path = FS.join(__dirname, "fixtures/so-it-is-written.txt"); + var content = "Good bye, cruel World!\n"; + return FS.write(path, new Buffer(content, "utf-8")) + .then(function (result) { + expect(result).toBe(undefined); + return FS.read(path) + }) + .then(function (readContent) { + expect(readContent).toEqual(content); + }) + .finally(function () { + return FS.remove(path); + }); + }); +}); + diff --git a/core/promise-io/spec/http-apps/cookie-spec.js b/core/promise-io/spec/http-apps/cookie-spec.js new file mode 100644 index 0000000000..7cd57b19f1 --- /dev/null +++ b/core/promise-io/spec/http-apps/cookie-spec.js @@ -0,0 +1,52 @@ + +require("../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var Http = require("../../http"); +var Apps = require("../../http-apps"); + +describe("http cookies", function () { + + var hosts = ["localhost", "127.0.0.1"]; + + hosts.forEach(function (host) { + it("should work on host " + host, function () { + + var server = Http.Server(function (request) { + return { + status: 200, + headers: { + "set-cookie": "a=10; MaxAge=1" + }, + body: [request.headers.cookie || ""] + }; + }); + + var request = Apps.Normalize(Apps.CookieJar(Http.request)); + + return server.listen(0) + .then(function (server) { + var address = server.node.address(); + return request("http://" + host + ":" + address.port) + .get("body") + .invoke("read") + .invoke("toString", "utf-8") + .then(function (content) { + expect(content).toEqual(""); // no cookie first time + }) + .then(function () { + return request("http://" + host + ":" + address.port) + .get("body") + .invoke("read") + .invoke("toString", "utf-8") + }) + .then(function (content) { + expect(content).toEqual("a=10"); // cookie set second time + }) + }) + .timeout(1000) + .finally(server.stop) + }); + }); + +}); + diff --git a/core/promise-io/spec/http-apps/directory-list-spec.js b/core/promise-io/spec/http-apps/directory-list-spec.js new file mode 100644 index 0000000000..a85f51965b --- /dev/null +++ b/core/promise-io/spec/http-apps/directory-list-spec.js @@ -0,0 +1,86 @@ + +require("../lib/jasmine-promise"); +var Http = require("../../http"); +var Apps = require("../../http-apps"); +var FS = require("../../fs"); + +describe("directory lists", function () { + + it("should be fine in plain text", function () { + + var fixture = FS.join(module.directory || __dirname, "fixtures"); + + var app = new Apps.Chain() + .use(Apps.Cap) + .use(Apps.ListDirectories) + .use(function () { + return Apps.FileTree(fixture); + }) + .end() + + return Http.Server(app) + .listen(0) + .then(function (server) { + var port = server.address().port; + return Http.read({ + url: "http://127.0.0.1:" + port + "/", + headers: { + accept: "text/plain" + }, + charset: 'utf-8' + }) + .then(function (content) { + expect(content).toEqual("01234.txt\n1234.txt\n5678.txt\n9012/\n"); + }) + .finally(server.stop); + }); + + }); + + it("should be fine in html", function () { + + var fixture = FS.join(module.directory || __dirname, "fixtures"); + + var app = new Apps.Chain() + .use(Apps.Cap) + .use(Apps.HandleHtmlFragmentResponses) + .use(Apps.ListDirectories) + .use(function () { + return Apps.FileTree(fixture); + }) + .end() + + return Http.Server(app) + .listen(0) + .then(function (server) { + var port = server.address().port; + return Http.read({ + url: "http://127.0.0.1:" + port + "/", + headers: { + accept: "text/html" + }, + charset: 'utf-8' + }) + .then(function (content) { + expect(content).toEqual( + "\n" + + "\n" + + " \n" + + " Directory Index\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + ); + }) + .finally(server.stop); + }); + + }); +}); diff --git a/core/promise-io/spec/http-apps/fixtures/01234.txt b/core/promise-io/spec/http-apps/fixtures/01234.txt new file mode 100644 index 0000000000..635a18bd80 --- /dev/null +++ b/core/promise-io/spec/http-apps/fixtures/01234.txt @@ -0,0 +1 @@ +01234.txt diff --git a/core/promise-io/spec/http-apps/fixtures/1234.txt b/core/promise-io/spec/http-apps/fixtures/1234.txt new file mode 100644 index 0000000000..81c545efeb --- /dev/null +++ b/core/promise-io/spec/http-apps/fixtures/1234.txt @@ -0,0 +1 @@ +1234 diff --git a/core/promise-io/spec/http-apps/fixtures/5678.txt b/core/promise-io/spec/http-apps/fixtures/5678.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/promise-io/spec/http-apps/fixtures/9012/3456.txt b/core/promise-io/spec/http-apps/fixtures/9012/3456.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/promise-io/spec/http-apps/hosts-spec.js b/core/promise-io/spec/http-apps/hosts-spec.js new file mode 100644 index 0000000000..fc1d88f98d --- /dev/null +++ b/core/promise-io/spec/http-apps/hosts-spec.js @@ -0,0 +1,49 @@ + +require("../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var Http = require("../../http"); +var Negotiate = require("../../http-apps/negotiate"); +var Content = require("../../http-apps/content"); + +describe("http host negotiation", function () { + it("should work", function () { + return Http.Server(Negotiate.Host({ + "localhost:999": function (request) { + throw new Error("Should not get here"); + }, + "127.0.0.1:999": function (request) { + throw new Error("Should not get here"); + }, + "localhost": function (request) { + return Content.ok("Hello, Localhost"); + }, + "127.0.0.1": function (request) { + return Content.ok("Hello, 127.0.0.1"); + } + })) + .listen(0) + .then(function (server) { + var port = server.address().port; + return Q.fcall(function () { + return Http.read({ + "url": "http://localhost:" + port, + "charset": "utf-8" + }) + .then(function (result) { + expect(result).toEqual("Hello, Localhost"); + }); + }) + .then(function () { + return Http.read({ + "url": "http://127.0.0.1:" + port, + "charset": "utf-8" + }) + .then(function (result) { + expect(result).toEqual("Hello, 127.0.0.1"); + }); + }) + .finally(server.stop); + }); + }); +}); + diff --git a/core/promise-io/spec/http-apps/interpret-range-spec.js b/core/promise-io/spec/http-apps/interpret-range-spec.js new file mode 100644 index 0000000000..ca6cb8cd1b --- /dev/null +++ b/core/promise-io/spec/http-apps/interpret-range-spec.js @@ -0,0 +1,47 @@ + +// http://labs.apache.org/webarch/http/draft-fielding-http/p5-range.html#range.units + +var Apps = require("../../http-apps/fs"); + +var size = 10000; +var tests = [ + { + description: "The first 500 bytes (byte offsets 0-499, inclusive)", + input: "bytes=0-499", + oracle: {begin: 0, end: 500} + }, + { + description: "The second 500 bytes (byte offsets 500-999, inclusive)", + input: "bytes=500-999", + oracle: {begin: 500, end: 1000} + }, + { + description: "The initial 500 bytes with 0 elided", + input: "bytes=-499", + oracle: {begin: 0, end: 500} + }, + { + description: "The final 500 bytes with final elided (byte offsets 9500-9999, inclusive)", + input: "bytes=9500-", + oracle: {begin: 9500, end: 10000} + }, + { + description: "Legal but not canonical specification of the second 500 bytes (byte offsets 500-999, inclusive)", + input: "bytes=500-600,601-999", + oracle: {begin: 500, end: 1000} + }, + { + description: "Legal but not canonical specification of the second 500 bytes (byte offsets 500-999, inclusive)", + input: "bytes=500-700,601-999", + oracle: {begin: 500, end: 1000} + } +]; + +describe("range interpretation", function () { + tests.forEach(function (test) { + it("should interpret " + test.input, function () { + expect(Apps.interpretFirstRange(test.input, size)).toEqual(test.oracle); + }); + }); +}); + diff --git a/core/promise-io/spec/http-apps/partial-range-spec.js b/core/promise-io/spec/http-apps/partial-range-spec.js new file mode 100644 index 0000000000..56c5ff0282 --- /dev/null +++ b/core/promise-io/spec/http-apps/partial-range-spec.js @@ -0,0 +1,186 @@ + +require("../lib/jasmine-promise"); +var Http = require("../../http"); +var Apps = require("../../http-apps"); +var FS = require("../../fs"); + +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1 +describe("HTTP Range", function () { + var fixture, app, serveAndTest; + describe("Byte Ranges" , function () { + beforeEach(function () { + fixture = FS.join(module.directory || __dirname, "fixtures", "01234.txt"); + app = new Apps.Chain() + .use(Apps.Cap) + .use(function () { + return Apps.File(fixture); + }) + .end(); + serveAndTest = function serveAndTest(rangeExp, expectedStatus) { + return Http.Server(app) + .listen(0) + .then(function (server) { + var port = server.node.address().port; + return Http.read({ + "url": "http://127.0.0.1:" + port + "/", + "headers": { + "range": rangeExp + } + }, function (response) { + return response.status === expectedStatus; + }).finally(server.stop); + }) + } + }); + + // A byte range operation MAY specify a single range of bytes, or a set of ranges within a single entity. + // ranges-specifier = byte-ranges-specifier + // byte-ranges-specifier = bytes-unit "=" byte-range-set + // byte-range-set = 1#( byte-range-spec | suffix-byte-range-spec ) + // byte-range-spec = first-byte-pos "-" [last-byte-pos] + // first-byte-pos = 1*DIGIT + // last-byte-pos = 1*DIGIT + describe("byte range spec", function () { + // The first-byte-pos value in a byte-range-spec gives the byte-offset of the first byte in a range. The + // last-byte-pos value gives the byte-offset of the last byte in the range; that is, the byte positions + // specified are inclusive. Byte offsets start at zero. + it("positions are inclusive", function () { + return serveAndTest("bytes=0-2", 206) + .then(function (content) { + expect(content.toString('utf-8')).toEqual('012'); + }); + }); + it("last position is optional", function () { + return serveAndTest("bytes=0-", 206) + .then(function (content) { + expect(content.toString('utf-8')).toEqual('01234.txt\n'); + }); + }); + + // TODO + xit("non contiguous ranges", function () { + return serveAndTest("bytes=0-2,4-5", 206) + .then(function (content) { + expect(content.toString('utf-8')).toEqual('0124.'); + }); + }); + + // If the last-byte-pos value is present, it MUST be greater than + // or equal to the first-byte-pos in that byte-range-spec, or the + // byte- range-spec is syntactically invalid. The recipient of a + // byte-range- set that includes one or more syntactically invalid + // byte-range-spec values MUST ignore the header field that + // includes that byte-range- set. + describe("invalid syntax should be ignored", function () { + it("last positions must be greater than first", function () { + return serveAndTest("bytes=4-3", 216) + .then(function (content) { + expect(false).toBe(true); + }, function (error) { + expect(error.response.status).toBe(416); + }) + }); + it("if any part is invalid, all of it is invalid", function () { + return serveAndTest("bytes=0-2,4-3", 216) + .then(function (content) { + expect(false).toBe(true); + }, function (error) { + expect(error.response.status).toBe(416); + }); + }); + }); + + // If the last-byte-pos value is absent, or if the value is greater than or equal to the current length of + // the entity-body, last-byte-pos is taken to be equal to one less than the current length of the entity- + // body in bytes. + describe("last position should be truncated to the length of the content", function () { + it("single range", function () { + return serveAndTest("bytes=0-10", 206) + .then(function (content) { + expect(content.toString('utf-8')).toEqual('01234.txt\n'); + }); + }); + // TODO + xit("multiple ranges", function () { + return serveAndTest("bytes=0-2,1-10", 206) + .then(function (content) { + expect(content.toString('utf-8')).toEqual('0121234.txt\n'); + }); + }); + }); + }); + + //By its choice of last-byte-pos, a client can limit the number of + //bytes retrieved without knowing the size of + // the entity. + // suffix-byte-range-spec = "-" suffix-length + // suffix-length = 1*DIGIT + describe("suffix byte range spec", function () { + it("can limit the number of bytes retrieved", function () { + return serveAndTest("bytes=-3", 206) + .then(function (content) { + expect(content.toString('utf-8')).toEqual('0123'); + }); + }); + + // A suffix-byte-range-spec is used to specify the suffix of the + // entity-body, of a length given by the suffix-length value. (That + // is, this form specifies the last N bytes of an entity-body.) If + // the entity is shorter than the specified suffix-length, the + // entire entity-body is used. + it("last position should be truncated to the length", function () { + return serveAndTest("bytes=-20", 206) + .then(function (content) { + expect(content.toString('utf-8')).toEqual('01234.txt\n'); + }); + }); + }); + + // If a syntactically valid byte-range-set includes at least one byte- + // range-spec whose first-byte-pos is less than the current length of + // the entity-body, or at least one suffix-byte-range-spec with a non- + // zero suffix-length, then the byte-range-set is satisfiable. + // Otherwise, the byte-range-set is unsatisfiable. If the + // byte-range-set is unsatisfiable, the server SHOULD return a response + // with a status of 416 (Requested range not satisfiable). Otherwise, + // the server SHOULD return a response with a status of 206 (Partial + // Content) containing the satisfiable ranges of the entity-body. + describe("satisfiability", function () { + it("should return 416 if a byte range spec is unsatisfiable", function () { + return serveAndTest("bytes=10-11", 416) + .then(function (content) { + expect(true).toBeTruthy(); + }).fail(function () { + expect(false).toBeTruthy(); + }); + }); + + it("should return 416 if a suffix byte range spec is unsatisfiable", function () { + return serveAndTest("bytes=1-0", 206) + .then(function (content) { + expect(true).toBeFalsy(); + }).fail(function (error) { + expect(error.response.status).toBe(416); + }); + }); + + it("should return 416 if all byte range spec are unsatisfiable", function () { + return serveAndTest("bytes=10-11,-0", 416) + .then(function (content) { + expect(true).toBeTruthy(); + }).fail(function () { + expect(false).toBeTruthy(); + }); + }); + + // TODO + xit("should return 200 if at least one byte range spec is satisfiable", function () { + return serveAndTest("bytes=10-11,0-2", 200) + .then(function (content) { + expect(content.toString('utf-8')).toEqual('012'); + }); + }); + + }); + }); +}); diff --git a/core/promise-io/spec/http-apps/proxy-spec.js b/core/promise-io/spec/http-apps/proxy-spec.js new file mode 100644 index 0000000000..451ea59fed --- /dev/null +++ b/core/promise-io/spec/http-apps/proxy-spec.js @@ -0,0 +1,82 @@ + +require("../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var Http = require("../../http"); +var Apps = require("../../http-apps"); +var FS = require("../../fs"); + +describe("http proxy", function () { + + it("should work", function () { + + var requestProxy; + var responseProxy; + var requestActual; + var responseActual; + + var app = Apps.Chain() + .use(Apps.Trap, function (response) { + responseActual = response; + return response; + }) + .use(Apps.Tap, function (request) { + requestActual = request; + }) + .use(function (next) { + return Apps.Branch({ + "foo": Apps.Branch({ + "bar": new Apps.Chain() + .use(Apps.Cap) + .use(function () { + return Apps.Content(["Hello, World!"]) + }) + .end() + }) + }) + }) + .end(); + + var server1 = Http.Server(app); + + return Q.when(server1.listen(0)) + .then(function (server1) { + var port = server1.node.address().port; + + var server2 = Http.Server( + Apps.Trap( + Apps.Tap( + Apps.ProxyTree("http://127.0.0.1:" + port + "/foo/"), + function (request) { + requestProxy = request; + } + ), + function (response) { + responseProxy = response; + return response; + } + ) + ); + + return [server1, server2.listen(0)]; + }) + .spread(function (server1, server2) { + var port = server2.node.address().port; + return Http.read({ + url: "http://127.0.0.1:" + port + "/bar", + charset: "utf-8" + }) + .then(function (content) { + expect(content).toBe("Hello, World!"); + expect(requestActual).toBeTruthy(); + expect(responseActual).toBeTruthy(); + expect(requestProxy).toBeTruthy(); + expect(responseProxy).toBeTruthy(); + }) + .finally(server1.stop) + .finally(server2.stop) + }) + + }); + +}); + diff --git a/core/promise-io/spec/http-apps/symbolic-link-spec.js b/core/promise-io/spec/http-apps/symbolic-link-spec.js new file mode 100644 index 0000000000..1bd3bd7c84 --- /dev/null +++ b/core/promise-io/spec/http-apps/symbolic-link-spec.js @@ -0,0 +1,110 @@ +/*global __dirname*/ +require("../lib/jasmine-promise"); +var Http = require("../../http"); +var Apps = require("../../http-apps"); +var FS = require("../../fs"); +var Mock = require("../../fs-mock"); + +describe("FileTree followInsecureSymbolicLinks", function () { + var testApp; + function makeApp(followInsecureSymbolicLinks) { + var fixture = FS.join(module.directory || __dirname, "fixtures"); + return FS.mock(fixture) + .then(function (mockFS) { + return FS.merge([ mockFS, Mock({ "6789/0123.txt": "0123\n" })]); + }) + .then(function (mockFS) { + return mockFS.symbolicCopy("6789", FS.join("9012","linkedDir"), "directory").thenResolve(mockFS); + }) + .then(function (mockFS) { + return mockFS.symbolicCopy( FS.join("6789","0123.txt"), FS.join("9012","linkedFile.txt"), "file").thenResolve(mockFS); + }) + .then(function (mockFS) { + return new Apps.Chain() + .use(Apps.ListDirectories) + .use(function () { + return Apps.FileTree(FS.join("/","9012"), {fs: mockFS, followInsecureSymbolicLinks: followInsecureSymbolicLinks}); + }) + .end() + }).then(function (app) { + return Http.Server(app).listen(0) + }); + }; + + describe("if false", function () { + beforeEach(function () { + testApp = makeApp(false) + }); + it("should not follow symbolic links to directories", function () { + return testApp.then(function (server) { + var port = server.address().port; + return Http.read({ + url: "http://127.0.0.1:" + port + "/linkedDir/", + headers: { + accept: "text/plain" + }, + charset: 'utf-8' + }) + .then(null, function (error) { + expect(error.response.status).toEqual(404); + }) + .finally(server.stop); + }); + }); + it("should not follow symbolic links to files", function () { + return testApp.then(function (server) { + var port = server.address().port; + return Http.read({ + url: "http://127.0.0.1:" + port + "/linkedFile.txt", + headers: { + accept: "text/plain" + }, + charset: 'utf-8' + }) + .then(null, function (error) { + expect(error.response.status).toEqual(404); + }) + .finally(server.stop); + }); + }); + }); + + describe("if true", function () { + beforeEach(function () { + testApp = makeApp(true) + }); + it("should follow symbolic links to directories", function () { + return testApp.then(function (server) { + var port = server.address().port; + return Http.read({ + url: "http://127.0.0.1:" + port + "/linkedDir/", + headers: { + accept: "text/plain" + }, + charset: 'utf-8' + }) + .then(function (content) { + expect(content).toEqual("0123.txt\n"); + }) + .finally(server.stop); + }); + }); + it("should follow symbolic links to files", function () { + return testApp.then(function (server) { + var port = server.address().port; + return Http.read({ + url: "http://127.0.0.1:" + port + "/linkedFile.txt", + headers: { + accept: "text/plain" + }, + charset: 'utf-8' + }) + .then(function (content) { + expect(content).toEqual("0123\n"); + }) + .finally(server.stop); + }); + }); + }); + +}); diff --git a/core/promise-io/spec/http/agent-spec.js b/core/promise-io/spec/http/agent-spec.js new file mode 100644 index 0000000000..9ae1925b54 --- /dev/null +++ b/core/promise-io/spec/http/agent-spec.js @@ -0,0 +1,97 @@ +require("../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var HTTPS = require("https"); +var http = require('../../http'); + +describe("https agent", function () { + + it("should allow use self-signed certificate", function () { + var options = { + passphrase: 'q-io-https', + key: '-----BEGIN RSA PRIVATE KEY-----\n' + +'Proc-Type: 4,ENCRYPTED\n' + +'DEK-Info: DES-EDE3-CBC,6ACDE0E22CA24B54\n' + +'\n' + +'MHs2noP3Vm8SyHkZ/KaPv2tkwEQx3CZVFmaoTUakJsj/QT2itDWoEzRZaffmqJ5z\n' + +'tCYyXFhwfJinJVwkkOGU5Hv2n8hYNcsRucFC5Y+BUQKEuAtalq3YGoIWWggDPzqb\n' + +'xGEI46PZS+kt8wGcMau1ArSwv2GuY3P8yjV97uus8Mc+U7XazkAgtlLX95wu9cGL\n' + +'HrMUCll0SdCwwiQ+yOh/KPvLTOImq8l4h1EUi1EbLREvobpVvwWgwKPixKFSLg/3\n' + +'KM9WK8P7nuLoJlTTgwXjwOZRWGCaq/m8UkmeMGN3+OBjRnVOdwksvFJSRuHyy24F\n' + +'9DQHo3Lh+MRrlTq5byYFAYuom4yszxeTm6LS9iXlJNB2mrf1LHPvPlR86oXz8EoI\n' + +'/sgfYLOUUMtF5779dmeyUFeekW8w09uFt3BfcxWbh+AVraIHkH3iXHwvD5lrx2re\n' + +'MXP1PHZnzbhEkV01bRL0+keQuS3gd+q723iQnY6NqXPtnaQxBLt6Z0NLBAh6NbR5\n' + +'CsPRyDnNCYz2/i/3lxe2vGeVAGLgu5U2r8MvKXGMn6off+94jmLGWgWcnqlt/bfg\n' + +'Ob9hiZEjSAX+NOvnvhcT/J4NYnVoZPqodM+zvgSDMP1CGQ84k4Ubsnvv70wYvFbY\n' + +'CNqS/TpnK5uDE9vbwFyJhgOTXF+8PZJE3cRfOo9INIo39b9TTE7jPbgSF+k5V3Dd\n' + +'4t55Xt9GaJAHr3xVViNtOKssbXtnQ73P3dg4pTZkA4E0i0kYZhmR45qXL9989Pbm\n' + +'lNlirMIj8P4ysX2/MxT2KYcG6IBSHnAzx5raY0xSFUOeAB5q9GheqA==\n' + +'-----END RSA PRIVATE KEY-----\n', + cert: '-----BEGIN CERTIFICATE-----\n' + +'MIICsDCCAhmgAwIBAgIJANjH3x23QvNtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV\n' + +'BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX\n' + +'aWRnaXRzIFB0eSBMdGQwHhcNMTMwOTEyMDEyMjUzWhcNMjMwOTEwMDEyMjUzWjBF\n' + +'MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50\n' + +'ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB\n' + +'gQDDPt+Ouup0iECslVxoA9mspIDVtgjph9bAuYmB84K3nz/CTA2E8mJhfDCFUk+v\n' + +'ipKvD+bnVgR8asql8AutHzaiCr6yOxUfs9gRgJ37TQGmTFpKx5/OegV2t7khq8Uc\n' + +'Wp7SChZuQ2CZrDjNeZl6K7CFtcbU6yV4tYKsNyNQslSyvwIDAQABo4GnMIGkMB0G\n' + +'A1UdDgQWBBTn1FqVoBznKu+Vtl5Bspq4PGwxEjB1BgNVHSMEbjBsgBTn1FqVoBzn\n' + +'Ku+Vtl5Bspq4PGwxEqFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt\n' + +'U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJANjH3x23\n' + +'QvNtMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAMw8bTEPx+rlIxTMR\n' + +'rvZriPeMzlh45vhRyy7JO1C+RnzSVBWUyL/Ca7QbrFcq4nGai2xFnqEEiPKWrP8b\n' + +'ve1oBz9V18gv6T4oiqOpFmoh2c/IVNr2qE01GdGIos/wb1cYEFRsMLPplbv4PROR\n' + +'89Yb8/2e/5CzxW4LAMNRqD5GwKE=\n' + +'-----END CERTIFICATE-----\n' + }; + + var server = HTTPS.createServer(options, function (req, res) { + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Content-Length': 2 + }); + res.end('ok'); + }); + + server.listen(0); + + var port = server.address().port; + + var allow = http.request({ + "host": "localhost", + "port": port, + "ssl": true, + "agent": new HTTPS.Agent({ + rejectUnauthorized: false + }) + }) + .then(function (response) { + expect(Q.isPromise(response.body)).toBe(false); + return response.body.read() + .then(function (body) { + expect(body.toString("utf-8")).toBe("ok"); + }); + }); + + var reject = http.request({ + host: "localhost", + port: port, + ssl: true, + agent: new HTTPS.Agent({ + rejectUnauthorized: true + }) + }).then(function (response) { + // should not be here + expect(response).toBeUndefined(); + }).fail(function(err) { + expect(Q.isPromise(err)).toBe(false); + expect(err).toEqual(jasmine.any(Error)); + // expect(err.message).toBe('DEPTH_ZERO_SELF_SIGNED_CERT'); + // Node.js 0.11 expect(err.message).toBe('self signed certificate'); + }); + + return Q.all([allow, reject]).finally(function () { + server.close(); + }); + }); +}); diff --git a/core/promise-io/spec/http/basic-spec.js b/core/promise-io/spec/http/basic-spec.js new file mode 100644 index 0000000000..85a285089f --- /dev/null +++ b/core/promise-io/spec/http/basic-spec.js @@ -0,0 +1,107 @@ + +require("../lib/jasmine-promise"); +var Q = require("q-bluebird"); +var HTTP = require("../../http"); + +describe("http server and client", function () { + + it("should work as both server and client", function () { + var response = { + "status": 200, + "headers": { + "content-type": "text/plain" + }, + "body": [ + "Hello, World!" + ] + }; + + var server = HTTP.Server(function () { + return response; + }); + + return server.listen(0) + .then(function (server) { + var port = server.address().port; + + var request = { + "host": "localhost", + "port": port, + "headers": { + "host": "localhost" + } + }; + + return HTTP.request(request) + .then(function (response) { + expect(Q.isPromise(response.body)).toBe(false); + var acc = []; + return response.body.read() + .then(function (body) { + expect(body.toString("utf-8")).toBe("Hello, World!"); + }); + }) + }) + .finally(server.stop) + }); + + it("should defer a response", function () { + var response = { + "status": 200, + "headers": { + "content-type": "text/plain; charset=utf-8" + }, + "body": { + "forEach": function (write) { + var deferred = Q.defer(); + write("Hello, World!"); + setTimeout(function () { + deferred.resolve(); + }, 100); + return deferred.promise; + } + } + }; + + var server = HTTP.Server(function () { + return response; + }); + + return server.listen(0).then(function (server) { + var port = server.node.address().port; + + var request = { + "host": "localhost", + "port": port, + "headers": { + "host": "localhost" + }, + "charset": "utf-8" + }; + + return HTTP.request(request) + .then(function (response) { + var acc = []; + return response.body.read() + .then(function (body) { + expect(body).toBe("Hello, World!"); + }); + }) + }) + .finally(server.stop) + }); + + it('should successfully access resources that require HTTP Basic authentication when using the username:password@host.com URL syntax', function(){ + // This tries to access a public resource, see http://test.webdav.org/ + // + // The resource is password protected, but there's no content behind it + // so we will actually receive a 404; that's ok though as at least it's + // a well-defined and expected status. + return HTTP.request('http://user1:user1@test.webdav.org/auth-basic/') + .then(function(response){ + expect(response.status).not.toBe(401); + expect(response.status).toBe(404); + }); + }); +}); + diff --git a/core/promise-io/spec/http/normalize-request-spec.js b/core/promise-io/spec/http/normalize-request-spec.js new file mode 100644 index 0000000000..6591f0177d --- /dev/null +++ b/core/promise-io/spec/http/normalize-request-spec.js @@ -0,0 +1,204 @@ +var normalizeRequest = require("../../http").normalizeRequest; + +describe("request normalization", function () { + + it("must parse string HTTP URL into components", function () { + var request = normalizeRequest("http://google.com"); + expect(request).toEqual({ + url: "http://google.com", + method: "GET", + ssl: false, + host: "google.com", + hostname: "google.com", + port: 80, + path: "/", + headers: { host: "google.com" } + }); + }); + + it("must parse string HTTP URL with path into components with path specified", function () { + var request = normalizeRequest("http://google.com/search?q=q-io"); + expect(request).toEqual({ + url: "http://google.com/search?q=q-io", + method: "GET", + ssl: false, + host: "google.com", + hostname: "google.com", + port: 80, + path: "/search?q=q-io", + headers: { host: "google.com" } + }); + }); + + it("must parse string HTTP URL with port into components with port specified", function () { + var request = normalizeRequest("http://google.com:8080/search?q=q-io"); + expect(request).toEqual({ + url: "http://google.com:8080/search?q=q-io", + method: "GET", + ssl: false, + host: "google.com:8080", + hostname: "google.com", + port: 8080, + path: "/search?q=q-io", + headers: { host: "google.com:8080" } + }); + }); + + it("must parse string HTTPS URL into components", function () { + var request = normalizeRequest("https://google.com"); + expect(request).toEqual({ + url: "https://google.com", + method: "GET", + ssl: true, + host: "google.com", + hostname: "google.com", + port: 443, + path: "/", + headers: { host: "google.com" } + }); + }); + + it("must parse string HTTPS URL with port into components with port overriden", function () { + var request = normalizeRequest("https://google.com:8080"); + expect(request).toEqual({ + url: "https://google.com:8080", + method: "GET", + ssl: true, + host: "google.com:8080", + hostname: "google.com", + port: 8080, + path: "/", + headers: { host: "google.com:8080" } + }); + }); + + it("must parse string HTTP URL of request object into components", function () { + var request = normalizeRequest({ url: "http://google.com/search?q=q-io" }); + expect(request).toEqual({ + url: "http://google.com/search?q=q-io", + method: "GET", + ssl: false, + host: "google.com", + hostname: "google.com", + port: 80, + path: "/search?q=q-io", + headers: { host: "google.com" } + }); + }); + + it("must preserve request method of request object with string HTTP URL", function () { + var request = normalizeRequest({ + method: "POST", + url: "http://google.com/search?q=q-io" + }); + expect(request).toEqual({ + url: "http://google.com/search?q=q-io", + method: "POST", + ssl: false, + host: "google.com", + hostname: "google.com", + port: 80, + path: "/search?q=q-io", + headers: { host: "google.com" } + }); + }); + + it("must preserve host header of request object with string HTTP URL", function () { + var request = normalizeRequest({ + url: "http://google.com/search?q=q-io", + headers: { host: "yahoo.com" } + }); + expect(request).toEqual({ + url: "http://google.com/search?q=q-io", + method: "GET", + ssl: false, + host: "google.com", + hostname: "google.com", + port: 80, + path: "/search?q=q-io", + headers: { host: "yahoo.com" } + }); + }); + + it("must ignore host of request object with string HTTP URL", function () { + var request = normalizeRequest({ + url: "http://google.com/search?q=q-io", + host: "yahoo.com", + hostname: "yahoo.com" + }); + expect(request).toEqual({ + url: "http://google.com/search?q=q-io", + method: "GET", + ssl: false, + host: "google.com", + hostname: "google.com", + port: 80, + path: "/search?q=q-io", + headers: { host: "google.com" } + }); + }); + + it("must ignore port number of request object with string HTTP URL", function () { + var request = normalizeRequest({ + url: "http://google.com/search?q=q-io", + port: 8080 + }); + expect(request).toEqual({ + url: "http://google.com/search?q=q-io", + method: "GET", + ssl: false, + host: "google.com", + hostname: "google.com", + port: 80, + path: "/search?q=q-io", + headers: { host: "google.com" } + }); + }); + + it("must ignore path string of request object with string HTTP URL", function () { + var request = normalizeRequest({ + url: "http://google.com/search?q=q-io", + path: "/" + }); + expect(request).toEqual({ + url: "http://google.com/search?q=q-io", + method: "GET", + ssl: false, + host: "google.com", + hostname: "google.com", + port: 80, + path: "/search?q=q-io", + headers: { host: "google.com" } + }); + }); + + it("must fill all missing fields of request object", function () { + var request = normalizeRequest({ + host: "google.com" + }); + expect(request).toEqual({ + method: "GET", + host: "google.com", + hostname: "google.com", + port: 80, + path: "/", + headers: { host: "google.com" } + }); + }); + + it("must preserve host header of request object", function () { + var request = normalizeRequest({ + host: "google.com", + headers: { host: "yahoo.com" } + }); + expect(request).toEqual({ + method: "GET", + host: "google.com", + hostname: "google.com", + port: 80, + path: "/", + headers: { host: "yahoo.com" } + }); + }); + +}); diff --git a/core/promise-io/spec/lib/jasmine-promise.js b/core/promise-io/spec/lib/jasmine-promise.js new file mode 100644 index 0000000000..811e9c1c9d --- /dev/null +++ b/core/promise-io/spec/lib/jasmine-promise.js @@ -0,0 +1,42 @@ +"use strict"; + +var Q = require("q-bluebird"); + +/** + * Modifies the way that individual specs are run to easily test async + * code with promises. + * + * A spec may return a promise. If it does, then the spec passes if and + * only if that promise is fulfilled within a very short period of time. + * If it is rejected, or if it isn't fulfilled quickly, the spec fails. + * + * In this way, we can use promise chaining to structure our asynchronous + * tests. Expectations all down the chain of promises are all checked and + * guaranteed to be run and resolved or the test fails. + * + * This is a big win over the runs() and watches() code that jasmine + * supports out of the box. + */ +jasmine.Block.prototype.execute = function (onComplete) { + var spec = this.spec; + try { + var result = this.func.call(spec, onComplete); + + // It seems Jasmine likes to return the suite if you pass it anything. + // So make sure it's a promise first. + if (result && typeof result.then === "function") { + Q.timeout(result, 500).then(function () { + onComplete(); + }, function (error) { + spec.fail(error); + onComplete(); + }); + } else if (this.func.length === 0) { + onComplete(); + } + } catch (error) { + spec.fail(error); + onComplete(); + } +}; + diff --git a/core/promise-io/writer.js b/core/promise-io/writer.js new file mode 100644 index 0000000000..bc94e0049a --- /dev/null +++ b/core/promise-io/writer.js @@ -0,0 +1,113 @@ + +var Q = require("q-bluebird"); + +/** + * Wraps a Node writable stream, providing an API similar to + * Narwhal's synchronous `io` streams, except returning and + * accepting promises for long-latency operations. + * + * @param stream any Node writable stream + * @returns {Promise * Writer} a promise for the + * text writer. + */ +module.exports = Writer; + +var version = process.versions.node.split('.'); +var supportsFinish = version[0] >= 0 && version[1] >= 10; + +function Writer(_stream, charset) { + var self = Object.create(Writer.prototype); + + if (charset && _stream.setEncoding) // TODO complain about inconsistency + _stream.setEncoding(charset); + + var drained = Q.defer(); + + _stream.on("error", function (reason) { + drained.reject(reason); + drained = Q.defer(); + }); + + _stream.on("drain", function () { + drained.resolve(); + drained = Q.defer(); + }); + + /*** + * Writes content to the stream. + * @param {String} content + * @returns {Promise * Undefined} a promise that will + * be resolved when the buffer is empty, meaning + * that all of the content has been sent. + */ + self.write = function (content) { + if (!_stream.writeable && !_stream.writable) + return Q.reject(new Error("Can't write to non-writable (possibly closed) stream")); + if (typeof content !== "string") { + content = new Buffer(content); + } + if (!_stream.write(content)) { + return drained.promise; + } else { + return Q.resolve(); + } + }; + + /*** + * Waits for all data to flush on the stream. + * + * @returns {Promise * Undefined} a promise that will + * be resolved when the buffer is empty + */ + self.flush = function () { + return drained.promise; + }; + + /*** + * Closes the stream, waiting for the internal buffer + * to flush. + * + * @returns {Promise * Undefined} a promise that will + * be resolved when the stream has finished writing, + * flushing, and closed. + */ + self.close = function () { + var finished; + + if (supportsFinish) { // new Streams, listen for `finish` event + finished = Q.defer(); + _stream.on("finish", function () { + finished.resolve(); + }); + _stream.on("error", function (reason) { + finished.reject(reason); + }); + } + + _stream.end(); + drained.resolve(); // we will get no further drain events + if (finished) { // closing not explicitly observable + return finished.promise; + } else { + return Q(); // just resolve for old Streams + } + }; + + /*** + * Terminates writing on a stream, closing before + * the internal buffer drains. + * + * @returns {Promise * Undefined} a promise that will + * be resolved when the stream has finished closing. + */ + self.destroy = function () { + _stream.destroy(); + drained.resolve(); // we will get no further drain events + return Q.resolve(); // destruction not explicitly observable + }; + + self.node = _stream; + + return Q(self); // todo returns the begin.promise +} + From 31ce56b200db758f4a8c834e25009e1c39d6ad1a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 14:51:12 -0700 Subject: [PATCH 155/407] performance optimization --- .../serializer/montage-malker.js | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/core/serialization/serializer/montage-malker.js b/core/serialization/serializer/montage-malker.js index be7a8a2f05..527916fb4d 100644 --- a/core/serialization/serializer/montage-malker.js +++ b/core/serialization/serializer/montage-malker.js @@ -80,7 +80,10 @@ var MontageWalker = exports.MontageWalker = Montage.specialize({ value: function(value, name) { var type = this._getTypeOf(value); - if (type === "object") { + //Happens often so let's do that first + if (type === "MontageObject") { + this._visitCustomType(type, value, name); + } else if (type === "object") { this._visitObject(value, name); } else if (type === "array") { this._visitArray(value, name); @@ -225,13 +228,28 @@ var MontageWalker = exports.MontageWalker = Montage.specialize({ args; if (typeof visitor[methodName] === "function") { - args = Array.prototype.slice.call(arguments, 1); - // the first parameter of the handler function is always the malker - args.unshift(this); - return visitor[methodName].apply( - visitor, - args); + if(arguments.length === 3) { + visitor[methodName].call( + visitor, this, arguments[1], arguments[2]); + } + else if(arguments.length === 2) { + visitor[methodName].call( + visitor, this, arguments[1]); + } + else if(arguments.length === 1) { + visitor[methodName].call( + visitor, this); + } + else { + args = Array.prototype.slice.call(arguments, 1); + // the first parameter of the handler function is always the malker + args.unshift(this); + + return visitor[methodName].apply( + visitor, + args); + } } } } From 3a4c1cf59436d511616415d3f6a4155d7c3bd90d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 14:54:49 -0700 Subject: [PATCH 156/407] implementation --- .../raw-embedded-value-to-object-converter.js | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/data/converter/raw-embedded-value-to-object-converter.js b/data/converter/raw-embedded-value-to-object-converter.js index 47756b5ccc..7fb452faa0 100644 --- a/data/converter/raw-embedded-value-to-object-converter.js +++ b/data/converter/raw-embedded-value-to-object-converter.js @@ -88,19 +88,69 @@ exports.RawEmbeddedValueToObjectConverter = RawValueToObjectConverter.specialize */ revert: { value: function (v) { + var self = this, + revertedValue, + result; + if (v) { if (!this.compiledRevertSyntax) { - return Promise.resolve(v); + return Promise.all([this._descriptorToFetch, this.service]).then(function (values) { + var objectDescriptor = values[0], + service = values[1]; + + if(Array.isArray(v)) { + if(v.length) { + revertedValue = []; + for(var i=0, countI=v.length, promises;(i Date: Fri, 17 Apr 2020 14:57:13 -0700 Subject: [PATCH 157/407] misc fixes --- data/service/data-operation.js | 48 +++++++++++----------------------- data/service/data-service.js | 30 +++++++++++++++------ 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index b894e0e05c..055623801f 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -74,6 +74,7 @@ var Montage = require("core/core").Montage, to a BeginTransaction operation, then the batch will be executed within that transaction */ "batch", + "batchupdate", "batchcompleted", "batchfailed", @@ -97,6 +98,8 @@ var Montage = require("core/core").Montage, /* Attempting to create a transaction within an existing one will fail */ "createtransactionfailed", + "transactioncancelled", + "createsavepoint", "performtransaction", @@ -162,15 +165,18 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy constructor: { value: function DataOperation() { - this.time = Date.now(); + this.timeStamp = performance.now(); this.id = uuid.generate(); - //Not sure we need this, it's not used anywhere - //this.creationIndex = exports.DataOperation.prototype._currentIndex + 1 || 0; - //exports.DataOperation.prototype._currentIndex = this.creationIndex; + this.constructionIndex = exports.DataOperation.prototype.constructionSequence++; + exports.DataOperation.prototype.constructionSequence = this.constructionIndex; } }, + constructionSequence: { + value: 0 + }, + bubbles: { value: true }, @@ -183,7 +189,7 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy value:function (serializer) { serializer.setProperty("id", this.id); serializer.setProperty("type", DataOperationType.intValueForMember(this.type)); - serializer.setProperty("time", this.time); + serializer.setProperty("timeStamp", this.timeStamp); serializer.setProperty("dataDescriptor", this.dataDescriptor); if(this.referrerId) { serializer.setProperty("referrerId", this.referrerId); @@ -213,9 +219,9 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy this.type = DataOperationType.memberWithIntValue(value); } - value = deserializer.getProperty("time"); + value = deserializer.getProperty("timeStamp"); if (value !== void 0) { - this.time = value; + this.timeStamp = value; } value = deserializer.getProperty("dataDescriptor"); @@ -251,10 +257,6 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy } }, - creationIndex: { - value: undefined - }, - /*************************************************************************** * Basic Properties */ @@ -370,27 +372,6 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy value: undefined }, - - /** - * creationTime - * A number used to order operations according to when they were created. - * // Add deprecation of "time" bellow - * This is initialized when an operation is created to the value of - * [Date.now()]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now}. - * The value can then be changed, but it should only be changed to values - * returned by `Date.now()`. - * - * Two operations can have the same `time` value if they were created within - * a millisecond of each other, and if so the operations' - * [index]{@link DataOperation#index} can be used to determine which one was - * created first. - * - * @type {number} - */ - creationTime: { - value: undefined - }, - /** * An operation that preceded and this one is related to. For a ReadUpdated, it would be the Read operation. * @@ -477,7 +458,7 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy * * @type {number} */ - index: { + constructionIndex: { value: undefined }, @@ -710,6 +691,7 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy validateCancelled: DataOperationType.validatecancelled, Batch: DataOperationType.batch, + BatchUpdate: DataOperationType.batchupdate, BatchCompleted: DataOperationType.batchcompleted, BatchFailed: DataOperationType.batchfailed, diff --git a/data/service/data-service.js b/data/service/data-service.js index 2a2d01c0a9..ca532fdc2b 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -1411,7 +1411,21 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { */ getObjectProperties: { value: function (object, propertyNames) { - if (this.isRootService) { + + + /* + Benoit: + + - If we create an object, properties that are not relations can't + be fetched. We need to make sure we don't actually try. + + - If a property is a relationship and it wasn't set on the object, + as an object, we can't get it either. + */ + if(this.isObjectCreated(object)) { + //Not much we can do there anyway, punt + return Promise.resolve(true); + } else if (this.isRootService) { // Get the data, accepting property names as an array or as a list // of string arguments while avoiding the creation of any new array. var names = Array.isArray(propertyNames) ? propertyNames : arguments, @@ -1928,12 +1942,6 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } }, - createDataObject: { - value: function (type) { - - } - }, - dispatchDataEventTypeForObject: { value: function (eventType, object, detail) { /* @@ -2075,6 +2083,12 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } }, + isObjectCreated: { + value: function(object) { + return this.createdDataObjects.has(object); + } + }, + /** * A set of the data objects moified by the user after they were fetched. * * @@ -2579,7 +2593,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { _serviceIdentifierForQuery: { value: function (query) { - var parameters = query.criteria.parameters, + var parameters = (query && query.criteria) ? query.criteria.parameters : null, serviceModuleID = parameters && parameters.serviceIdentifier, mapping, propertyName; From c4ef9b73322ef015e9f55afb414d1db9c379137e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 15:02:15 -0700 Subject: [PATCH 158/407] test fix/additions and bring montage-testing in montage as testing --- test/package.json | 5 +- .../application-test/package-subtype.json | 2 +- test/spec/application-test/package.json | 2 +- test/spec/core/date-spec.js | 2 +- test/spec/core/date/all-day.ics | 32 + test/spec/core/date/between_dates.ics | 116 ++ test/spec/core/date/ical-expander.js | 113 ++ test/spec/core/date/icaljs-issue-257.ics | 48 + test/spec/core/date/icaljs-issue-285.ics | 46 + test/spec/core/date/invalid_dates.ics | 52 + test/spec/core/date/recur.ics | 58 + test/spec/trigger/package.json | 2 +- testing/.editorconfig | 11 + testing/.gitignore | 9 + testing/.jshintignore | 4 + testing/.jshintrc | 40 + testing/.travis.yml | 14 + testing/CHANGES.md | 10 + testing/LICENSE.md | 27 + testing/README.md | 35 + testing/jasmine-additions.js | 112 ++ testing/karma.conf.js | 120 ++ testing/package.json | 42 + testing/run.js | 108 ++ testing/test-controller.js | 19 + testing/test/all.js | 11 + testing/test/package.json | 9 + testing/test/run-browser.js | 112 ++ testing/test/run-karma.html | 14 + testing/test/run-karma.js | 103 ++ testing/test/run-mr.html | 14 + testing/test/run-node.js | 52 + testing/test/run.html | 14 + .../test/spec/application/as-application.html | 62 + testing/test/spec/application/as-owner.html | 60 + testing/test/spec/application/test.js | 40 + testing/test/spec/require-spec.js | 33 + testing/test/spec/test-controller-spec.js | 16 + testing/test/spec/testpageloader-spec.js | 209 ++++ testing/testpageloader.js | 1059 +++++++++++++++++ 40 files changed, 2831 insertions(+), 6 deletions(-) create mode 100644 test/spec/core/date/all-day.ics create mode 100644 test/spec/core/date/between_dates.ics create mode 100644 test/spec/core/date/ical-expander.js create mode 100644 test/spec/core/date/icaljs-issue-257.ics create mode 100644 test/spec/core/date/icaljs-issue-285.ics create mode 100644 test/spec/core/date/invalid_dates.ics create mode 100644 test/spec/core/date/recur.ics create mode 100644 testing/.editorconfig create mode 100644 testing/.gitignore create mode 100644 testing/.jshintignore create mode 100644 testing/.jshintrc create mode 100644 testing/.travis.yml create mode 100644 testing/CHANGES.md create mode 100644 testing/LICENSE.md create mode 100644 testing/README.md create mode 100644 testing/jasmine-additions.js create mode 100644 testing/karma.conf.js create mode 100644 testing/package.json create mode 100644 testing/run.js create mode 100644 testing/test-controller.js create mode 100644 testing/test/all.js create mode 100644 testing/test/package.json create mode 100644 testing/test/run-browser.js create mode 100644 testing/test/run-karma.html create mode 100644 testing/test/run-karma.js create mode 100644 testing/test/run-mr.html create mode 100644 testing/test/run-node.js create mode 100644 testing/test/run.html create mode 100644 testing/test/spec/application/as-application.html create mode 100644 testing/test/spec/application/as-owner.html create mode 100644 testing/test/spec/application/test.js create mode 100644 testing/test/spec/require-spec.js create mode 100644 testing/test/spec/test-controller-spec.js create mode 100644 testing/test/spec/testpageloader-spec.js create mode 100644 testing/testpageloader.js diff --git a/test/package.json b/test/package.json index 9247899cef..869889afce 100644 --- a/test/package.json +++ b/test/package.json @@ -2,9 +2,10 @@ "name": "montage-tests", "version": "0.0.0", "mappings": { + "mr": "../core/mr", "montage": "../", - "montage-testing": "../node_modules/montage-testing", - "collections": "../node_modules/collections", + "montage-testing": "../testing", + "collections": "../core/collections", "package-a": { "name": "package-a", "location": "package-a" diff --git a/test/spec/application-test/package-subtype.json b/test/spec/application-test/package-subtype.json index 9b4afd1f86..625de4b69a 100644 --- a/test/spec/application-test/package-subtype.json +++ b/test/spec/application-test/package-subtype.json @@ -3,7 +3,7 @@ "mappings": { "specs": "../", "montage": "../../../", - "montage-testing": "../../../node_modules/montage-testing" + "montage-testing": "../../../testing" }, "applicationPrototype":"application-test[MyApp]" } diff --git a/test/spec/application-test/package.json b/test/spec/application-test/package.json index 5eecb9b27e..bc18b73baa 100644 --- a/test/spec/application-test/package.json +++ b/test/spec/application-test/package.json @@ -2,7 +2,7 @@ "name": "test", "mappings": { "montage": "../../../", - "montage-testing": "../../../node_modules/montage-testing" + "montage-testing": "../../../testing" }, "applicationPrototype":"application-test[MyApp]" } diff --git a/test/spec/core/date-spec.js b/test/spec/core/date-spec.js index fb110bf936..4bfaec1934 100644 --- a/test/spec/core/date-spec.js +++ b/test/spec/core/date-spec.js @@ -98,7 +98,7 @@ describe("core/date-spec", function () { expected.setUTCSeconds(0); expected.setUTCMilliseconds(0); //Error message: "incorrect conversion of: '" + sourceString + "'" ; - expect(cresultDate.toUTCString()).toBe(expected.toUTCString()); + expect(resultDate.toUTCString()).toBe(expected.toUTCString()); }); it("should parse not case sensitive", function () { diff --git a/test/spec/core/date/all-day.ics b/test/spec/core/date/all-day.ics new file mode 100644 index 0000000000..3a9921816a --- /dev/null +++ b/test/spec/core/date/all-day.ics @@ -0,0 +1,32 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +VERSION:2.0 +X-WR-CALNAME:test +PRODID:-//Apple Inc.//Mac OS X 10.13.3//EN +X-APPLE-CALENDAR-COLOR:#63DA38 +X-WR-TIMEZONE:Europe/Oslo +CALSCALE:GREGORIAN +BEGIN:VEVENT +CREATED:20180519T175529Z +UID:63AC62EB-6B7E-4D07-B081-10D527B44454 +RRULE:FREQ=WEEKLY +DTEND;VALUE=DATE:20180520 +TRANSP:TRANSPARENT +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +SUMMARY:Repeating all day event +DTSTART;VALUE=DATE:20180519 +DTSTAMP:20180519T184221Z +SEQUENCE:0 +END:VEVENT +BEGIN:VEVENT +CREATED:20180519T184153Z +UID:30505118-E8B4-41F8-A53C-0C7AAB70E6BB +DTEND;VALUE=DATE:20180528 +TRANSP:TRANSPARENT +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +SUMMARY:Non-repeating all day event +DTSTART;VALUE=DATE:20180527 +DTSTAMP:20180519T184213Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR diff --git a/test/spec/core/date/between_dates.ics b/test/spec/core/date/between_dates.ics new file mode 100644 index 0000000000..7fc1fe723b --- /dev/null +++ b/test/spec/core/date/between_dates.ics @@ -0,0 +1,116 @@ +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN + +BEGIN:VEVENT +SUMMARY:One day before +DESCRIPTION:Event that occurs one day before the tested date +CREATED:20180518T165432Z +UID:1 +DTEND;VALUE=DATE:20180502 +DTSTART;VALUE=DATE:20180501 +DTSTAMP:20180518T165619Z +SEQUENCE:0 +END:VEVENT + +BEGIN:VEVENT +SUMMARY:One day after +CREATED:20180518T165622Z +DESCRIPTION:Event that occurs one day after the tested date +UID:2 +DTEND:20180503T100000 +DTSTART:20180503T090000 +DTSTAMP:20180518T165633Z +SEQUENCE:0 +END:VEVENT + +BEGIN:VEVENT +SUMMARY:Event 9am +DESCRIPTION:Event on test date at 9am +CREATED:20180518T165017Z +UID:3 +DTEND:20180502T100000 +DTSTART:20180502T090000 +DTSTAMP:20180518T165027Z +SEQUENCE:0 +END:VEVENT + +BEGIN:VEVENT +SUMMARY:starting a day before +DESCRIPTION:Starting a day before and ending on test date +CREATED:20180518T165334Z +UID:4 +DTEND:20180502T143000 +DTSTART:20180501T200000 +DTSTAMP:20180518T165420Z +SEQUENCE:0 +END:VEVENT + +BEGIN:VEVENT +SUMMARY:Until next day +DESCRIPTION: Starting on test date and ending on the next day +CREATED:20180518T165312Z +UID:5 +DTEND:20180503T020000 +DTSTART:20180502T220000 +DTSTAMP:20180518T165428Z +SEQUENCE:0 +END:VEVENT + +BEGIN:VEVENT +SUMMARY:All week +DESCRIPTION:All week event +CREATED:20180518T165435Z +UID:6 +DTEND;VALUE=DATE:20180505 +DTSTART;VALUE=DATE:20180430 +DTSTAMP:20180518T165512Z +SEQUENCE:0 +END:VEVENT + +BEGIN:VEVENT +SUMMARY:Event afternoon +DESCRIPTION:Event on test date, afternoon +CREATED:20180518T165029Z +UID:7 +DTEND:20180502T170000 +DTSTART:20180502T160000 +DTSTAMP:20180518T165311Z +SEQUENCE:0 +END:VEVENT + +BEGIN:VEVENT +SUMMARY:Recurrent event +DESCRIPTION:Recurrent event with one occurrence on test date +CREATED:20180518T175735Z +UID:8 +RRULE:FREQ=DAILY;COUNT=5 +DTEND:20180430T120000 +DTSTART:20180430T110000 +DTSTAMP:20180518T175810Z +SEQUENCE:0 +END:VEVENT + +BEGIN:VEVENT +SUMMARY:recurrent event - not included +DESCRIPTION: Reccurent event that doesnt repeat on test date +CREATED:20180518T180833Z +UID:9 +RRULE:FREQ=WEEKLY;COUNT=4;BYDAY=MO,TU,TH,FR +DTEND:20180430T100000 +DTSTART:20180430T090000 +DTSTAMP:20180518T180938Z +SEQUENCE:0 +END:VEVENT + +BEGIN:VEVENT +CREATED:20180518T190318Z +UID:10 +DTEND;VALUE=DATE:20180504 +SUMMARY:Next day +DTSTART;VALUE=DATE:20180503 +DTSTAMP:20180518T190326Z +SEQUENCE:0 +END:VEVENT + +END:VCALENDAR \ No newline at end of file diff --git a/test/spec/core/date/ical-expander.js b/test/spec/core/date/ical-expander.js new file mode 100644 index 0000000000..9e32af188c --- /dev/null +++ b/test/spec/core/date/ical-expander.js @@ -0,0 +1,113 @@ +'use strict'; + +/* eslint-env mocha */ +/* eslint-disable func-names, prefer-arrow-callback */ + +const fs = require('fs'); +const IcalExpander = require('../'); +const assert = require('assert'); +const path = require('path'); + +// NOTE: Run with TZ=Etc/UTC mocha ical-parser.js +// https://github.com/mozilla-comm/ical.js/issues/257 + +const icaljsIssue257 = fs.readFileSync(path.join(__dirname, 'icaljs-issue-257.ics'), 'utf-8'); +const icaljsIssue285 = fs.readFileSync(path.join(__dirname, 'icaljs-issue-285.ics'), 'utf-8'); +const recurIcs = fs.readFileSync(path.join(__dirname, 'recur.ics'), 'utf-8'); +const invalidDates = fs.readFileSync(path.join(__dirname, 'invalid_dates.ics'), 'utf-8'); +const betweenTestCalendar = fs.readFileSync(path.join(__dirname, 'between_dates.ics'), 'utf-8'); +const allDayEventCalendar = fs.readFileSync(path.join(__dirname, 'all-day.ics'), 'utf-8'); + +it('should show first date', function () { + const events = new IcalExpander({ ics: icaljsIssue257 }) + .between(new Date('2016-07-24T00:00:00.000Z'), new Date('2016-07-26T00:00:00.000Z')); + + assert.equal(events.events[0].summary, 'Test-Event'); + assert.equal(events.occurrences.length, 0); +}); + +it('should show recurring modified date', function () { + const events = new IcalExpander({ ics: icaljsIssue257 }) + .between(new Date('2016-07-31T00:00:00.000Z'), new Date('2016-08-02T00:00:00.000Z')); + + assert.equal(events.events[0].summary, 'Test-Event - Reccurence #2'); + assert.equal(events.occurrences.length, 0); +}); + +it('should show nothing at recurring exdate', function () { + const events = new IcalExpander({ ics: icaljsIssue257 }) + .between(new Date('2016-08-07T00:00:00.000Z'), new Date('2016-08-10T00:00:00.000Z')); + + assert.equal(events.events.length, 0); + assert.equal(events.occurrences.length, 0); +}); + +it('should parse issue 285 case correctly', function () { + const events = new IcalExpander({ ics: icaljsIssue285 }) + .between(new Date('2017-01-03T00:00:00.000Z'), new Date('2017-01-25T00:00:00.000Z')); + + assert.deepEqual(events.events.map(e => e.startDate.toJSDate().toISOString()), ['2017-01-18T08:00:00.000Z']); + assert.deepEqual(events.occurrences.map(e => e.startDate.toJSDate().toISOString()), [ + '2017-01-03T08:00:00.000Z', + '2017-01-10T08:00:00.000Z', + '2017-01-24T08:00:00.000Z', + ]); +}); + +it('should parse all recurring events without going on forever', function () { + const events = new IcalExpander({ ics: recurIcs }) + .all(); + + const outEvents = events.events.map(e => ({ startDate: e.startDate, summary: e.summary })); + const outOccurrences = events.occurrences.map(o => ({ startDate: o.startDate, summary: o.item.summary })); + const allEvents = [].concat(outEvents, outOccurrences); + + assert.equal(allEvents.length, 1001); +}); + + +it('should correctly parse an ical file with invalid start/end dates', function () { + const events = new IcalExpander({ ics: invalidDates, skipInvalidDates: true }) + .all(); + + const outEvents = events.events.map(e => ({ startDate: e.startDate, summary: e.summary })); + const outOccurrences = events.occurrences.map(o => ({ startDate: o.startDate, summary: o.item.summary })); + const allEvents = [].concat(outEvents, outOccurrences); + + assert.equal(allEvents.length, 2); +}); + +it('should include events that are partially between two dates', function () { + const eventIdsBetween = ['3', '4', '5', '6', '7']; + const occurrenceIdsBetween = ['8']; + + const events = new IcalExpander({ ics: betweenTestCalendar }) + .between(new Date('2018-05-02T00:00:00.000Z'), new Date('2018-05-02T23:59:59.999Z')); + assert.equal(events.events.length, 5); + assert.equal(events.occurrences.length, 1); + + events.events.forEach((event) => { + assert.ok( + eventIdsBetween.findIndex(id => id === event.uid) >= 0, + `${event.uid} is not a valid event between provided dates`); + }); + + events.occurrences.forEach((occurrence) => { + assert.ok( + occurrenceIdsBetween.findIndex(id => id === occurrence.item.uid) >= 0, + `${occurrence.item.uid} is not a valid occurrence between provided dates`); + }); +}); + +it('should support open ended limits', function () { + const events = new IcalExpander({ ics: allDayEventCalendar, maxIterations: 10 }) + .between(new Date('2018-05-19T23:59:00.000Z')); + + assert.equal(events.events.length, 1); + assert(events.events[0].summary === 'Non-repeating all day event'); + assert.equal(events.occurrences.length, 10); + assert(events.occurrences.every(o => o.item.summary === 'Repeating all day event')); + + // events.events.forEach(e => console.log(e.summary, e.uid)); + // events.occurrences.forEach(o => console.log(o.item.summary, o.item.uid)); +}); diff --git a/test/spec/core/date/icaljs-issue-257.ics b/test/spec/core/date/icaljs-issue-257.ics new file mode 100644 index 0000000000..7c68d4011b --- /dev/null +++ b/test/spec/core/date/icaljs-issue-257.ics @@ -0,0 +1,48 @@ +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN + +BEGIN:VEVENT +CREATED:20160728T114557Z +LAST-MODIFIED:20160728T122120Z +DTSTAMP:20160728T122120Z +UID:9fda684c-373b-4f58-9fc7-6db9f06218b5 +SUMMARY:Test-Event +RRULE:FREQ=WEEKLY;UNTIL=20160912T080000Z +EXDATE:20160808T080000Z +DTSTART;TZID=Europe/Berlin:20160725T100000 +DTEND;TZID=Europe/Berlin:20160725T110000 +TRANSP:OPAQUE +SEQUENCE:3 +X-MOZ-GENERATION:4 +END:VEVENT + +BEGIN:VEVENT +CREATED:20160728T115039Z +LAST-MODIFIED:20160728T115048Z +DTSTAMP:20160728T115048Z +UID:9fda684c-373b-4f58-9fc7-6db9f06218b5 +SUMMARY:Test-Event - Reccurence #2 +RECURRENCE-ID;TZID=Europe/Berlin:20160801T100000 +DTSTART;TZID=Europe/Berlin:20160801T100000 +DTEND;TZID=Europe/Berlin:20160801T110000 +TRANSP:OPAQUE +SEQUENCE:2 +X-MOZ-GENERATION:3 +END:VEVENT + +BEGIN:VEVENT +CREATED:20160728T114921Z +LAST-MODIFIED:20160728T122231Z +DTSTAMP:20160728T122231Z +UID:9fda684c-373b-4f58-9fc7-6db9f06218b5 +SUMMARY:Test-Event +TRANSP:OPAQUE +CLASS:PUBLIC +RECURRENCE-ID;TZID=Europe/Berlin:20160725T100000 +DTSTART;TZID=Europe/Berlin:20160725T100000 +DTEND;TZID=Europe/Berlin:20160725T113000 +END:VEVENT + +PRODID:-//Inf-IT//InfCloud 0.12.1//EN +END:VCALENDAR diff --git a/test/spec/core/date/icaljs-issue-285.ics b/test/spec/core/date/icaljs-issue-285.ics new file mode 100644 index 0000000000..f5b390c646 --- /dev/null +++ b/test/spec/core/date/icaljs-issue-285.ics @@ -0,0 +1,46 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.12.2//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20170119T180642Z +UID:99C096E7-0A03-48C2-B606-0BC558147842 +RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=4 +DTEND;TZID=Europe/Berlin:20170103T100000 +TRANSP:OPAQUE +SUMMARY:test event +DTSTART;TZID=Europe/Berlin:20170103T090000 +DTSTAMP:20170119T180700Z +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +SEQUENCE:0 +END:VEVENT +BEGIN:VEVENT +CREATED:20170119T180642Z +UID:99C096E7-0A03-48C2-B606-0BC558147842 +DTEND;TZID=Europe/Berlin:20170118T100000 +TRANSP:OPAQUE +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +SUMMARY:test event +DTSTART;TZID=Europe/Berlin:20170118T090000 +DTSTAMP:20170119T180706Z +SEQUENCE:0 +RECURRENCE-ID;TZID=Europe/Berlin:20170117T090000 +END:VEVENT +END:VCALENDAR diff --git a/test/spec/core/date/invalid_dates.ics b/test/spec/core/date/invalid_dates.ics new file mode 100644 index 0000000000..cf5c9ed079 --- /dev/null +++ b/test/spec/core/date/invalid_dates.ics @@ -0,0 +1,52 @@ +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN + +BEGIN:VEVENT +SUMMARY:Test-Event - Wrong dates #1 +DESCRIPTION:Test-Event description +CREATED:20170127T204945Z +LAST-MODIFIED:20170202T143211Z +DTSTAMP:20170202T143211Z +SEQUENCE:1493956582 +UID:1d36e202-3e9b-11e7-a919-92ebcb67fe33 +DTSTART;TZID=America/Chicago:20170325 +DTEND;TZID=America/Chicago:20170326 +END:VEVENT + +BEGIN:VEVENT +SUMMARY:Test-Event - Valid dates #1 +DESCRIPTION:Test-Event description +CREATED:20170111T161600Z +LAST-MODIFIED:20170223T201105Z +DTSTAMP:20170223T201105Z +SEQUENCE:1493956582 +UID:1d36e202-3e9b-11e7-a919-92ebcb67fe33 +DTSTART;TZID=America/Chicago:20170624T073000 +DTEND;TZID=America/Chicago:20170624T130000 +END:VEVENT + +BEGIN:VEVENT +SUMMARY:Kids: Test-Event - Wrong dates #3 +CREATED:20170130T202802Z +LAST-MODIFIED:20170215T162028Z +DTSTAMP:20170215T162028Z +SEQUENCE:1493956582 +UID:1d36e202-3e9b-11e7-a919-92ebcb67fe33 +DTSTART;TZID=America/Chicago:20170702 +DTEND;TZID=America/Chicago:20170703 +END:VEVENT + +BEGIN:VEVENT +SUMMARY:Test-Event - Valid dates #2 +CREATED:20160616T212841Z +LAST-MODIFIED:20160721T135829Z +DTSTAMP:20160721T135829Z +SEQUENCE:1493956582 +UID:1d36e202-3e9b-11e7-a919-92ebcb67fe33 +DTSTART;TZID=America/Chicago:20160826T190000 +DTEND;TZID=America/Chicago:20160826T213000 +END:VEVENT + +PRODID:-//Inf-IT//InfCloud 0.12.1//EN +END:VCALENDAR diff --git a/test/spec/core/date/recur.ics b/test/spec/core/date/recur.ics new file mode 100644 index 0000000000..a87a9f1bc1 --- /dev/null +++ b/test/spec/core/date/recur.ics @@ -0,0 +1,58 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:mikael test +X-WR-TIMEZONE:Europe/Oslo +X-WR-CALDESC:mikaels testkalender 👌 +BEGIN:VTIMEZONE +TZID:Europe/Oslo +X-LIC-LOCATION:Europe/Oslo +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE + +BEGIN:VEVENT +DTSTART;VALUE=DATE:20170323 +DTEND;VALUE=DATE:20170324 +RRULE:FREQ=WEEKLY;BYDAY=TH +DTSTAMP:20170323T230627Z +UID:qdm32vss2jugkokaf9ja69pjs0@google.com +CREATED:20170323T230533Z +DESCRIPTION: +LAST-MODIFIED:20170323T230533Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY: +TRANSP:TRANSPARENT +END:VEVENT + +BEGIN:VEVENT +DTSTART:20170322T180000Z +DTEND:20170322T200000Z +DTSTAMP:20170323T230627Z +UID:0ir2mhbf4i7m882oo4r3a23i80@google.com +CREATED:20170323T230505Z +DESCRIPTION: +LAST-MODIFIED:20170323T230505Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:test recurring +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/test/spec/trigger/package.json b/test/spec/trigger/package.json index 5d07ba23d3..3dd6fb9a63 100644 --- a/test/spec/trigger/package.json +++ b/test/spec/trigger/package.json @@ -3,7 +3,7 @@ "mappings": { "test": "../../", "montage": "../../../", - "montage-testing": "../../../node_modules/montage-testing" + "montage-testing": "../../../testing" }, "dependencies": { "inject-description-location": "*", diff --git a/testing/.editorconfig b/testing/.editorconfig new file mode 100644 index 0000000000..10211c7a0f --- /dev/null +++ b/testing/.editorconfig @@ -0,0 +1,11 @@ +# http://EditorConfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/testing/.gitignore b/testing/.gitignore new file mode 100644 index 0000000000..d321659598 --- /dev/null +++ b/testing/.gitignore @@ -0,0 +1,9 @@ +.npmignore +.tmp +.idea +.DS_Store +atlassian-ide-plugin.xml +npm-debug.log +report/ +node_modules/ +out/ \ No newline at end of file diff --git a/testing/.jshintignore b/testing/.jshintignore new file mode 100644 index 0000000000..023114c312 --- /dev/null +++ b/testing/.jshintignore @@ -0,0 +1,4 @@ +**/node_modules/** +**/report/** +**/reporter/** +**/support/** diff --git a/testing/.jshintrc b/testing/.jshintrc new file mode 100644 index 0000000000..048d06d43b --- /dev/null +++ b/testing/.jshintrc @@ -0,0 +1,40 @@ +{ + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "forin": true, + "noarg": true, + "noempty": true, + "nonew": true, + "undef": true, + "unused": "vars", + "trailing": true, + "indent": 4, + "boss": true, + "eqnull": true, + "browser": true, + "globals": { + "CustomEvent": true, + "WebSocket": false, + + "require": false, + "exports": false, + "module": false, + "global": false, + + "describe": false, + "it": false, + "expect": false, + "beforeEach": false, + "beforeAll": false, + "afterEach": false, + "afterAll": false, + + "WeakMap": true, + "Map": true, + "Set": true, + + "console": false + } +} diff --git a/testing/.travis.yml b/testing/.travis.yml new file mode 100644 index 0000000000..232d04c9e7 --- /dev/null +++ b/testing/.travis.yml @@ -0,0 +1,14 @@ +language: node_js +node_js: + - "4.8.0" +script: npm run $COMMAND +env: + - COMMAND=test + - COMMAND=test:karma +notifications: + irc: + channels: + - "chat.freenode.net#montage" + on_success: false + template: + - "%{author} broke the %{repository} tests on %{branch}: %{build_url}" diff --git a/testing/CHANGES.md b/testing/CHANGES.md new file mode 100644 index 0000000000..ebac908849 --- /dev/null +++ b/testing/CHANGES.md @@ -0,0 +1,10 @@ +### 5.0.0 + + - Upgrade tests stack + - Add travis support + - Remove hardcoded dependency in favor of npm packages for Jasmine and js-beautify + - Migrate specs to Jasmine 2.5.2O (npm run test:jasmine) + - Revamp NodeJS tests runner (npm test) + - Migrate Phantom.js tests runner to Karma (npm run test:karma) + - jshint and other minors fix. + - Add missing LICENSE.md BSD 3-Clause License Montage Studio, inc based on package.json current author and license \ No newline at end of file diff --git a/testing/LICENSE.md b/testing/LICENSE.md new file mode 100644 index 0000000000..461620273e --- /dev/null +++ b/testing/LICENSE.md @@ -0,0 +1,27 @@ +Copyright (c) 2013-2015, Montage Studio, inc. +All rights reserved, licensed under the BSD 3-Clause License: + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 0000000000..ccfc19d6ac --- /dev/null +++ b/testing/README.md @@ -0,0 +1,35 @@ +[![Build Status](https://travis-ci.org/montagejs/montage-testing.svg?branch=master)](http://travis-ci.org/montagejs/montage-testing) + +Montage Testing +=============================== + +MontageJS uses some pure unit tests that are straightforward [Jasmine specs][1]. + + To install the test code, run `npm install` in your project folder. This installs the [montage-testing][2] package, which adds some useful utilities for writing jasmine tests. You will need the file run-tests.html. + + For an example of how we implement unit testing, see the [digit][3] repository: + + * [run-tests][4] loads our test environment. + * `data-module="all"` inside the final script tag tells the system to load [test/all.js][5]. + * all.js specifies a list of module ids for the runner to execute. + + >Note that in this example, all the tests load a page in an iframe using + `TestPageLoader.queueTest()`. These are akin to integration tests since they test the component in a real environment. + + We also test some components by [mocking their dependencies][6]. + + [1]: https://github.com/montagejs/montage/blob/master/test/all.js "Jasmine specs" + [2]: https://github.com/montagejs/montage-testing "montage-testing" + [3]: https://github.com/montagejs/digit "digit" + [4]: https://github.com/montagejs/digit/blob/master/test/run.html "run-tests" + [5]: https://github.com/montagejs/digit/tree/master/test "test/all.js" + [6]: https://github.com/montagejs/montage/blob/master/test/spec/base/abstract-button-spec.js "mocking their dependencies" + +## Maintenance + +Tests are in the `test` directory. Use `npm test` to run the tests in +NodeJS or open `test/run.html` in a browser. + +To run the tests in your browser, simply use `npm run test:jasmine`. + +To run the tests using Karma use `npm run test:karma` and for continious tests run with file changes detection `npm run test:karma-dev`. \ No newline at end of file diff --git a/testing/jasmine-additions.js b/testing/jasmine-additions.js new file mode 100644 index 0000000000..9fea88e4db --- /dev/null +++ b/testing/jasmine-additions.js @@ -0,0 +1,112 @@ +/* global global:true, __dirname, jasmineRequire */ + +/*jshint evil:true */ +// reassigning causes eval to not use lexical scope. +var globalEval = eval, + global = globalEval('this'); +/*jshint evil:false */ + +// Init +var jasmine = jasmineRequire.core(jasmineRequire); +var jasmineEnv = jasmine.getEnv(); + +// Export interface +var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); +global.jasmine = jasmine; +for (var property in jasmineInterface) { + if (jasmineInterface.hasOwnProperty(property)) { + global[property] = jasmineInterface[property]; + } +} + +// +// Init Reporter +// + +function queryString(parameter) { + var i, key, value, equalSign; + var loc = location.search.substring(1, location.search.length); + var params = loc.split('&'); + for (i=0; i + + + Montage-Testing - Karma + + + + + + + + + + diff --git a/testing/test/run-karma.js b/testing/test/run-karma.js new file mode 100644 index 0000000000..f61dac00c0 --- /dev/null +++ b/testing/test/run-karma.js @@ -0,0 +1,103 @@ +/* global global:true, __dirname, jasmineRequire */ + +/*jshint evil:true */ +// reassigning causes eval to not use lexical scope. +var globalEval = eval, + global = globalEval('this'); +/*jshint evil:false */ + +// Bootsrap Karma +if (global.__karma__) { + + //jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000; + + global.__karma__.loaded = function() { + console.log('karma loaded'); + }; + +// Bootstrap Browser fallback +} else { + + // Init + var jasmine = jasmineRequire.core(jasmineRequire); + var jasmineEnv = jasmine.getEnv(); + + // Export interface + var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); + global.jasmine = jasmine; + for (var property in jasmineInterface) { + if (jasmineInterface.hasOwnProperty(property)) { + global[property] = jasmineInterface[property]; + } + } + + // Default reporter + jasmineEnv.addReporter(jasmineInterface.jsApiReporter); + + // Html reporter + jasmineRequire.html(jasmine); + var htmlReporter = new jasmine.HtmlReporter({ + env: jasmineEnv, + getContainer: function() { return document.body; }, + createElement: function() { return document.createElement.apply(document, arguments); }, + createTextNode: function() { return document.createTextNode.apply(document, arguments); }, + timer: new jasmine.Timer() + }); + htmlReporter.initialize(); + + jasmineEnv.addReporter(htmlReporter); +} + +global.queryString = function queryString(parameter) { + var i, key, value, equalSign; + var loc = location.search.substring(1, location.search.length); + var params = loc.split('&'); + for (i=0; i + + + Montage-Testing - Browser + + + + + + + + + + diff --git a/testing/test/run-node.js b/testing/test/run-node.js new file mode 100644 index 0000000000..bddeac0179 --- /dev/null +++ b/testing/test/run-node.js @@ -0,0 +1,52 @@ +/*jshint node:true, browser:false */ +var jasmineRequire = require('jasmine-core/lib/jasmine-core/jasmine.js'); +var JasmineConsoleReporter = require('jasmine-console-reporter'); +var Montage = require('montage'); +var PATH = require("path"); + +// Init +var jasmine = jasmineRequire.core(jasmineRequire); +var jasmineEnv = jasmine.getEnv(); + +// Export interface +var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); +global.jasmine = jasmine; +global.jasmineRequire = jasmineRequire; +for (var property in jasmineInterface) { + if (jasmineInterface.hasOwnProperty(property)) { + global[property] = jasmineInterface[property]; + } +} + +// Default reporter +jasmineEnv.addReporter(jasmineInterface.jsApiReporter); + +// Html reporter +var consoleReporter = new JasmineConsoleReporter({ + colors: 1, + cleanStack: 1, + verbosity: 4, + listStyle: 'indent', + activity: false +}); +jasmineEnv.addReporter(consoleReporter); + +// Exit code +var exitCode = 0; +jasmineEnv.addReporter({ + specDone: function(result) { + exitCode = exitCode || result.status === 'failed'; + } +}); + +// Execute +Montage.loadPackage(PATH.join(__dirname, "."), { + mainPackageLocation: PATH.join(__dirname, "../") +}) +.then(function (mr) { + return mr.async("all"); +}).then(function () { + console.log('Done'); + process.exit(exitCode); +}).thenReturn(); + diff --git a/testing/test/run.html b/testing/test/run.html new file mode 100644 index 0000000000..203d8b413f --- /dev/null +++ b/testing/test/run.html @@ -0,0 +1,14 @@ + + + + Montage-Testing - Browser + + + + + + + + + + diff --git a/testing/test/spec/application/as-application.html b/testing/test/spec/application/as-application.html new file mode 100644 index 0000000000..18b62606f7 --- /dev/null +++ b/testing/test/spec/application/as-application.html @@ -0,0 +1,62 @@ + + + + + + Slot Test + + + + + + + diff --git a/testing/test/spec/application/as-owner.html b/testing/test/spec/application/as-owner.html new file mode 100644 index 0000000000..359b4396bc --- /dev/null +++ b/testing/test/spec/application/as-owner.html @@ -0,0 +1,60 @@ + + + + + + Slot Test + + + + + + + + diff --git a/testing/test/spec/application/test.js b/testing/test/spec/application/test.js new file mode 100644 index 0000000000..c1cf4b6259 --- /dev/null +++ b/testing/test/spec/application/test.js @@ -0,0 +1,40 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ +var Montage = require("montage").Montage; + +exports.Test = Montage.specialize( { + svg:{ + value: null + }, + html:{ + value: null + } +}); diff --git a/testing/test/spec/require-spec.js b/testing/test/spec/require-spec.js new file mode 100644 index 0000000000..c5339d9bc9 --- /dev/null +++ b/testing/test/spec/require-spec.js @@ -0,0 +1,33 @@ + /*global require,exports,describe,it,expect */ +describe("require-spec", function () { + + beforeEach(function () { + /* ... Set up your object ... */ + }); + + afterEach(function () { + /* ... Tear it down ... */ + }); + + // + it("load core module", function () { + var montageRequire = require("montage/core/core"); + expect(typeof montageRequire.Montage).toEqual("function"); + }); + + it("load alias module", function () { + var montageRequire = require("montage"); + expect(typeof montageRequire.Montage).toEqual("function"); + }); + + it("load inject module", function () { + var URL = require("montage/core/mini-url"); + expect(typeof URL.resolve).toEqual("function"); + }); + + it("load test-controller module", function () { + var TestController = require('montage-testing/test-controller').TestController; + expect(typeof TestController).toEqual("function"); + }); + +}); diff --git a/testing/test/spec/test-controller-spec.js b/testing/test/spec/test-controller-spec.js new file mode 100644 index 0000000000..1cfec5d302 --- /dev/null +++ b/testing/test/spec/test-controller-spec.js @@ -0,0 +1,16 @@ +/*global require,exports,describe,it,expect */ +describe("test-controller", function () { + + beforeEach(function () { + /* ... Set up your object ... */ + }); + + afterEach(function () { + /* ... Tear it down ... */ + }); + + it("load test-controller module", function () { + var TestController = require('montage-testing/test-controller').TestController; + expect(typeof TestController).toEqual("function"); + }); +}); \ No newline at end of file diff --git a/testing/test/spec/testpageloader-spec.js b/testing/test/spec/testpageloader-spec.js new file mode 100644 index 0000000000..788359a07a --- /dev/null +++ b/testing/test/spec/testpageloader-spec.js @@ -0,0 +1,209 @@ +var Montage = require("montage").Montage, + TestPageLoader = require("montage-testing/testpageloader").TestPageLoader; + +TestPageLoader.queueTest("application/as-application", {src: "spec/application/as-application.html"}, function (testPage) { + describe("application-spec", function () { + describe("Application used in application label", function () { + it("should draw correctly", function () { + expect(testPage.test).toBeDefined(); + }); + + it("should be THE application", function () { + expect(testPage.test.theOne).toEqual("true"); + }); + }); + }); +}); + +TestPageLoader.queueTest("application/as-owner", {src: "spec/application/as-owner.html"}, function (testPage) { + describe("application-spec", function () { + describe("Application used in owner label", function () { + it("should draw correctly", function () { + expect(testPage.test).toBeDefined(); + }); + }); + }); + + describe("testpageloader-spec", function () { + var text, pureElement; + + beforeAll(function () { + text = testPage.document.getElementById("text").component; + pureElement = testPage.document.getElementById("pureElement"); + }); + + describe("keyEvent", function () { + it("calls its callback", function (done) { + testPage.keyEvent({ target: text, keyCode: "J".charCodeAt(0) }, "keypress", function () { + done(); + }); + }); + + it("fires an event on a component", function (done) { + text.addEventListener("keypress", function (event) { + expect(event.keyCode).toEqual("J".charCodeAt(0)); + done(); + }); + testPage.keyEvent({ target: text, keyCode: "J".charCodeAt(0) }, "keypress"); + }); + + it("fires an event on an element", function (done) { + pureElement.addEventListener("keypress", function (event) { + expect(event.keyCode).toEqual("J".charCodeAt(0)); + done(); + }); + testPage.keyEvent({ target: pureElement, keyCode: "J".charCodeAt(0) }, "keypress"); + }); + }); + + describe("wheelEvent", function () { + it("calls its callback", function (done) { + testPage.wheelEvent({ target: text }, "wheel", function () { + done(); + }); + }); + + it("fires an event on a component", function (done) { + text.addEventListener("wheel", function () { + done(); + }); + testPage.wheelEvent({ target: text }, "wheel"); + }); + + it("fires an event on an element", function (done) { + pureElement.addEventListener("wheel", function () { + done(); + }); + testPage.wheelEvent({ target: pureElement }, "wheel"); + }); + }); + + describe("mouseEvent", function () { + it("calls its callback", function (done) { + testPage.mouseEvent({ target: text }, "click", function () { + done(); + }); + }); + + it("fires an event on a component", function (done) { + text.addEventListener("click", function () { + done(); + }); + testPage.mouseEvent({ target: text }, "click"); + }); + + it("fires an event on an element", function (done) { + pureElement.addEventListener("click", function () { + done(); + }); + testPage.mouseEvent({ target: pureElement }, "click"); + }); + }); + + describe("touchEvent", function () { + it("calls its callback", function (done) { + testPage.touchEvent({ target: text }, "touchStart", function () { + done(); + }); + }); + + it("fires an event on a component", function (done) { + text.addEventListener("touchStart", function (event) { + expect(event.touches.length).toBe(1); + done(); + }); + testPage.touchEvent({ target: text }, "touchStart"); + }); + + it("fires an event on an element", function (done) { + pureElement.addEventListener("touchStart", function (event) { + expect(event.touches.length).toBe(1); + done(); + }); + testPage.touchEvent({ target: pureElement }, "touchStart"); + }); + }); + + describe("clickOrTouch", function () { + var oldTouch = global.Touch; + + it("dispatches mouse events", function (done) { + var isMousedownFired = false, + isMouseupFired = false, + isClickFired = false; + global.Touch = void 0; + text.addEventListener("mousedown", function () { + isMousedownFired = true; + }); + text.addEventListener("mouseup", function () { + expect(isMousedownFired).toBe(true); + isMouseupFired = true; + }); + text.addEventListener("click", function () { + expect(isMouseupFired).toBe(true); + isClickFired = true; + }); + testPage.clickOrTouch({ target: text }, function () { + expect(isClickFired).toBe(true); + done(); + }); + }); + + it("dispatches touch events", function (done) { + var isTouchstartFired = false, + isTouchendFired = false, + isClickFired = false; + global.Touch = oldTouch || Function.noop; + text.addEventListener("touchstart", function () { + isTouchstartFired = true; + }); + text.addEventListener("touchend", function () { + expect(isTouchstartFired).toBe(true); + isTouchendFired = true; + }); + text.addEventListener("click", function () { + expect(isTouchendFired).toBe(true); + isClickFired = true; + }); + testPage.clickOrTouch({ target: text }, function () { + expect(isClickFired).toBe(true); + done(); + }); + }); + + afterAll(function () { + global.Touch = oldTouch; + }); + }); + + describe("dragElementOffsetTo", function () { + it("drags an element", function (done) { + var isMousedownFired = false, + isMousemoveFired = false, + moveX, moveY, + isMouseupFired = false; + pureElement.addEventListener("mousedown", function () { + isMousedownFired = true; + }); + pureElement.addEventListener("mousemove", function (event) { + isMousemoveFired = true; + moveX = event.clientX; + moveY = event.clientY; + }); + pureElement.addEventListener("mouseup", function () { + isMouseupFired = true; + }); + testPage.dragElementOffsetTo(pureElement, 200, 150, function () { + expect(isMousedownFired).toBe(true); + }, function () { + expect(isMousemoveFired).toBe(true); + expect(moveX).toBe(pureElement.offsetLeft + 200); + expect(moveY).toBe(pureElement.offsetLeft + 150); + }, function () { + expect(isMouseupFired).toBe(true); + done(); + }) + }); + }); + }); +}); diff --git a/testing/testpageloader.js b/testing/testpageloader.js new file mode 100644 index 0000000000..3a95bc4501 --- /dev/null +++ b/testing/testpageloader.js @@ -0,0 +1,1059 @@ +/* global describe, spyOn, expect, beforeAll, afterAll, testName, queryString */ + +/** + * @see https://developer.mozilla.org/en/DOM/HTMLIFrameElement + */ +var Montage = require("montage").Montage; +var Point = require("montage/core/geometry/point").Point; +var ActionEventListener = require("montage/core/event/action-event-listener").ActionEventListener; +var MutableEvent = require("montage/core/event/mutable-event").MutableEvent; +var Promise = require("montage/core/promise").Promise; +var defaultEventManager; + +if (!console.group) { + console.group = console.groupEnd = console.log; +} + +var TestPageLoader = exports.TestPageLoader = Montage.specialize( { + + constructor: { + value: function TestPageLoader() { + + if (!global.document) { + throw new Error('TestPageLoader require browser enviroment'); + } else if (typeof global.testpage === "undefined") { + if (!this.iframe) { + this.iframe = global.document.createElement("iframe"); + this.iframe.id = "testpage"; + + this.iframe.style.width = '100%'; + this.iframe.style.height = '100%'; + this.iframe.style.left = '75%'; + this.iframe.style.top = '0'; + this.iframe.style.position = 'absolute'; + this.iframe.style.border = 'none'; + this.iframe.style.zIndex = '100'; + this.iframe.style.pointerEvents = 'none'; + + global.document.body.appendChild(this.iframe); + } + global.testpage = this; + this.loaded = false; + return this; + } else { + return global.testpage; + } + } + }, + + iframeSrc: { + value: null + }, + + drawHappened: { + value: false + }, + + willNeedToDraw: { + value: false + }, + + testQueue: { + value: [] + }, + + loading: { + value: false + }, + + endTest: { + value: function() { + this.loading = false; + this.callNext(); + } + }, + + callNext: { + value: function() { + if (!this.loading && this.testQueue.length !== 0) { + var self = this; + this.unloadTest(); + setTimeout(function() { + self.loadTest(self.testQueue.shift()); + self.loading = true; + }, 0); + } + } + }, + + loadTest: { + value: function(promiseForFrameLoad, test) { + + var pageFirstDraw = {}; + var pageFirstDrawPromise = new Promise(function(resolve, reject) { + pageFirstDraw.resolve = resolve; + pageFirstDraw.reject = reject; + }); + + var testName = test.testName, + testCallback = test.callback, + timeoutLength = test.timeoutLength, + self = this, + src; + + pageFirstDraw.promise = pageFirstDrawPromise; + + if (!timeoutLength) { + timeoutLength = 10000; + } + + this.loaded = false; + // + promiseForFrameLoad.then( function(frame) { + // implement global function that montage is looking for at load + // this is little bit ugly and I'd like to find a better solution + + self.global.montageWillLoad = function() { + var firstDraw = true; + self.require.async("montage/ui/component").then(function (COMPONENT) { + var root = COMPONENT.__root__; + self.rootComponent = root; + // override the default drawIfNeeded behaviour + var originalDrawIfNeeded = root.drawIfNeeded; + root.drawIfNeeded = function() { + var continueDraw = function() { + originalDrawIfNeeded.call(root); + self.drawHappened++; + if(firstDraw) { + self.loaded = true; + // assign the application delegate to test so that the convenience methods work + if (! self.global.test && self.require("montage/core/application").application) { + self.global.test = self.require("montage/core/application").application.delegate; + } + if (typeof testCallback === "function") { + if (test.firstDraw) { + pageFirstDraw.resolve(self); + } else { + // francois HACK + // not sure how to deal with this + // if at first draw the page isn't complete the tests will fail + // so we wait an arbitrary 100ms for subsequent draws to happen... + setTimeout(function() { + pageFirstDraw.resolve(self); + }, 100); + } + } + firstDraw = false; + } + if (self._drawHappened) { + self._drawHappened(); + } + }; + + var pause = queryString("pause"); + if (firstDraw && pause === "true") { + var handleKeyUp = function(event) { + if (event.which === 82) { + self.document.removeEventListener("keyup", handleKeyUp,false); + document.removeEventListener("keyup", handleKeyUp,false); + continueDraw(); + } + }; + self.document.addEventListener("keyup", handleKeyUp,false); + document.addEventListener("keyup", handleKeyUp,false); + } else { + continueDraw(); + } + + self.willNeedToDraw = false; + }; + var originalAddToDrawList = root._addToDrawList; + root._addToDrawList = function(childComponent) { + originalAddToDrawList.call(root, childComponent); + self.willNeedToDraw = true; + }; + + defaultEventManager = null; + + return self.require.async("montage/core/event/event-manager").then(function (exports) { + defaultEventManager = exports.defaultEventManager; + }); + + }); + }; + }); + + + var promiseForTestPage = pageFirstDraw.promise.timeout(timeoutLength); + return promiseForTestPage.then(function(self) { + return self; + }) + .catch(function(reason) { + console.error(testName + " - " + reason.message); + return self; + }); + } + }, + + loadFrame: { + value: function(options) { + + var self = this, src; + var frameLoad = {}; + var frameLoadPromise = new Promise(function(resolve, reject) { + frameLoad.resolve = resolve; + frameLoad.reject = reject; + }); + frameLoad.promise = frameLoadPromise; + + var callback = function() { + frameLoad.resolve(self.global); + if (self.testWindow) { + self.testWindow.removeEventListener("load", callback, true); + } else { + self.iframe.removeEventListener("load", callback, true); + } + }; + + if (options.src) { + src = options.src; + } else { + src = options.directory + options.testName + ".html"; + } + + if (options.newWindow) { + self.testWindow = global.open(src, "test-global"); + global.addEventListener("unload", function() { + self.unloadTest(testName); + }, false); + self.testWindow.addEventListener("load", callback, true); + } else { + self.iframe.src = src; + self.iframe.addEventListener("load", callback, true); + } + + return frameLoad.promise; + } + }, + + unloadTest: { + enumerable: false, + value: function(testName) { + this.loaded = false; + if (this.testWindow) { + this.testWindow.close(); + this.testWindow = null; + } else { + this.iframe.src = ""; + } + return this; + } + }, + + nextDraw: { + value: function(numDraws, forceDraw) { + var theTestPage = this, + deferred = {}, + deferredPromise = new Promise(function(resolve, reject) { + deferred.resolve = resolve; + deferred.reject = reject; + }); + + deferred.promise = deferredPromise; + + this.drawHappened = false; + + if (!numDraws) { + numDraws = 1; + } + + theTestPage._drawHappened = function() { + if(theTestPage.drawHappened === numDraws) { + deferred.resolve(numDraws); + theTestPage._drawHappened = null; + } + }; + + if(forceDraw) { + this.rootComponent.drawTree(); + } + + return deferred.promise.timeout(1000); + } + }, + + waitForDraw: { + value: function(numDraws, forceDraw) { + var theTestPage = this; + theTestPage.drawHappened = false; + numDraws = numDraws || 1; + + return new Promise(function (resolve, reject) { + + (function waitForDraw(done) { + var hasDraw = theTestPage.drawHappened >= numDraws; + if (hasDraw) { + resolve(theTestPage.drawHappened); + } else { + setTimeout(function () { + waitForDraw(done); + }); + } + }()); + + if (forceDraw) { + theTestPage.rootComponent.drawTree(); + } + }); + } + }, + + waitForComponentDraw: { + value: function(component, numDraws, forceDraw) { + var theTestPage = this; + var currentDraw = component.draw; + numDraws = numDraws || 1; + + if (!currentDraw.oldDraw) { + component.draw = function draw() { + var result = draw.oldDraw.apply(this, arguments); + draw.drawHappened++; + return result; + }; + + component.draw.oldDraw = currentDraw; + } + component.draw.drawHappened = 0; + + return new Promise(function (resolve, reject) { + (function waitForDraw(done) { + + var hasDraw = component.draw.drawHappened === numDraws; + if (hasDraw) { + resolve(theTestPage.drawHappened); + } else { + // wait a little bit before resolving the promise + // Make sure the DOM changes have been applied. + setTimeout(function () { + resolve(theTestPage.drawHappened); + }, 50); + } + }()); + + if (forceDraw) { + theTestPage.rootComponent.drawTree(); + } + }); + } + }, + + getElementById: { + enumerable: false, + value: function(elementId) { + return this.document.getElementById(elementId); + } + }, + + querySelector: { + enumerable: false, + value: function(selector) { + return this.document.querySelector(selector); + } + }, + + querySelectorAll: { + enumerable: false, + value: function(selector) { + return this.document.querySelectorAll(selector); + } + }, + + test: { + enumerable: false, + get: function() { + return this.global.test; + } + }, + + document: { + get: function() { + if (this.testWindow) { + return this.testWindow.document; + } else { + return this.iframe.contentDocument; + } + } + }, + + global: { + get: function() { + if (this.testWindow) { + return this.testWindow; + } else { + return this.iframe.contentWindow; + } + } + }, + + require: { + get: function() { + // Handle transition period from `require` to `mr`. + return this.global.mr || this.global.require; + } + }, + + addListener: { + value: function(component, fn, type) { + type = type || "action"; + var buttonSpy = { + doSomething: fn || function(event) { + return 1+1; + } + }; + spyOn(buttonSpy, 'doSomething'); + + var actionListener = new ActionEventListener().initWithHandler_action_(buttonSpy, "doSomething"); + component.addEventListener(type, actionListener); + + return buttonSpy.doSomething; + } + }, + + keyEvent: { + enumerable: false, + value: function(eventInfo, eventName, callback) { + if (!eventName) { + eventName = "keypress"; + } + eventInfo.modifiers = eventInfo.modifiers || ""; + eventInfo.keyCode = eventInfo.keyCode || 0; + eventInfo.charCode = eventInfo.charCode || 0; + + var doc = this.iframe.contentDocument, + mofifiers = eventInfo.modifiers.split(" "), + event = { + altGraphKey: false, + altKey: mofifiers.indexOf("alt") !== -1, + bubbles: true, + cancelBubble: false, + cancelable: true, + charCode: eventInfo.charCode, + clipboardData: undefined, + ctrlKey: mofifiers.indexOf("control") !== -1, + currentTarget: null, + defaultPrevented: false, + detail: 0, + eventPhase: 0, + keyCode: eventInfo.keyCode, + layerX: 0, + layerY: 0, + metaKey: mofifiers.indexOf("meta") !== -1, + pageX: 0, + pageY: 0, + preventDefault: Function.noop, + returnValue: true, + shiftKey: mofifiers.indexOf("shift") !== -1, + srcElement: eventInfo.target, + target: eventInfo.target, + timeStamp: new Date().getTime(), + type: eventName, + view: doc.defaultView, + which: eventInfo.charCode || eventInfo.keyCode + }, + targettedEvent = MutableEvent.fromEvent(event); + + defaultEventManager.handleEvent(targettedEvent); + + if (typeof callback === "function") { + if(this.willNeedToDraw) { + this.waitForDraw(); + setTimeout(callback); + } else { + callback(); + } + } + return eventInfo; + } + }, + + wheelEvent: { + enumerable: false, + value: function(eventInfo, eventName, callback) { + var doc = this.iframe.contentDocument, + event = doc.createEvent("CustomEvent"); + + event.initEvent(eventName, true, true); + event.wheelDeltaY = eventInfo.wheelDeltaY; + event.deltaY = eventInfo.deltaY; + eventInfo.target.dispatchEvent(event); + + if (typeof callback === "function") { + if(this.willNeedToDraw) { + this.waitForDraw(); + setTimeout(callback); + } else { + callback(); + } + } + return eventInfo; + } + }, + + mouseEvent: { + enumerable: false, + value: function(eventInfo, eventName, callback) { + if (!eventName) { + eventName = "click"; + } + eventInfo.clientX = eventInfo.clientX || eventInfo.target.offsetLeft; + eventInfo.clientY = eventInfo.clientY || eventInfo.target.offsetTop; + + var doc = this.iframe.contentDocument, + event = doc.createEvent('MouseEvents'); + + event.initMouseEvent(eventName, true, true, doc.defaultView, + null, null, null, eventInfo.clientX, eventInfo.clientY, + false, false, false, false, + 0, null); + eventInfo.target.dispatchEvent(event); + if (typeof callback === "function") { + if(this.willNeedToDraw) { + this.waitForDraw(); + setTimeout(callback); + } else { + callback(); + } + } + return eventInfo; + } + }, + + touchEvent: { + enumerable: false, + value: function(eventInfo, eventName, callback) { + if (!eventName) { + eventName = "touchstart"; + } + var doc = this.document, + simulatedEvent = doc.createEvent("CustomEvent"), + fakeEvent, + touch, + eventManager, + // We need to dispatch a fake event through the event manager + // to fake the timestamp because it's not possible to modify + // the timestamp of an event. + dispatchThroughEventManager = eventInfo.timeStamp != null; + + if (typeof eventInfo.touches !== "undefined") { + // if you have a touches array we assume you know what you are doing + simulatedEvent.initEvent(eventName, true, true, doc.defaultView, 1, null, null, null, null, false, false, false, false, 0, null); + simulatedEvent.touches = eventInfo.touches; + simulatedEvent.targetTouches = eventInfo.targetTouches; + simulatedEvent.changedTouches = eventInfo.changedTouches; + } else { + touch = {}; + touch.clientX = eventInfo.clientX || eventInfo.target.offsetLeft; + touch.clientY = eventInfo.clientY || eventInfo.target.offsetTop; + touch.target = eventInfo.target; + touch.identifier = eventInfo.identifier || 500; + simulatedEvent.initEvent(eventName, true, true, doc.defaultView, 1, null, null, null, null, false, false, false, false, 0, null); + simulatedEvent.touches = [touch]; + simulatedEvent.targetTouches = [touch]; + simulatedEvent.changedTouches = [touch]; + } + + if (dispatchThroughEventManager) { + fakeEvent = this._createFakeEvent(simulatedEvent, eventInfo); + eventManager = this.require("montage/ui/component").__root__.eventManager; + eventManager.handleEvent(fakeEvent); + } else { + eventInfo.target.dispatchEvent(simulatedEvent); + } + + if (typeof callback === "function") { + if(this.willNeedToDraw) { + this.waitForDraw(); + setTimeout(callback); + } else { + callback(); + } + } + return eventInfo; + } + }, + + _createFakeEvent: { + value: function(event, fakeProperties) { + var fakeEvent; + + fakeEvent = Object.create(event); + Object.defineProperty(fakeEvent, "timeStamp", { + value: fakeProperties.timeStamp + }); + Object.defineProperty(fakeEvent, "target", { + value: fakeProperties.target + }); + Object.defineProperty(fakeEvent, "preventDefault", { + value: function(){ + return event.preventDefault(); + } + }); + Object.defineProperty(fakeEvent, "stopPropagation", { + value: function(){ + return event.stopPropagation(); + } + }); + Object.defineProperty(fakeEvent, "stopImmediatePropagation", { + value: function(){ + return event.stopImmediatePropagation(); + } + }); + + Object.defineProperty(fakeEvent, "bubbles", { + value: event.bubbles + }); + + Object.defineProperty(fakeEvent, "type", { + value: event.type + }); + + return fakeEvent; + } + }, + + clickOrTouch: { + enumerable: false, + value: function(eventInfo, callback) { + if (global.Touch) { + this.touchEvent(eventInfo, "touchstart"); + this.touchEvent(eventInfo, "touchend"); + this.mouseEvent(eventInfo, "click"); + } else { + this.mouseEvent(eventInfo, "mousedown"); + this.mouseEvent(eventInfo, "mouseup"); + this.mouseEvent(eventInfo, "click"); + } + if (typeof callback === "function") { + if(this.willNeedToDraw) { + this.waitForDraw(); + setTimeout(callback); + } else { + callback(); + } + } + return eventInfo; + } + }, + + dragElementOffsetTo: { + enumerable: false, + value: function(element, offsetX, offsetY, startCallback, moveCallback, endCallback, options) { + var self = this; + var startEventName = "mousedown"; + var moveEventName = "mousemove"; + var endEventName = "mouseup"; + var eventFactoryName = "mouseEvent"; + + if (options) { + if(options.pointerType === "touch" || global.Touch) { + startEventName = "touchstart"; + moveEventName = "touchmove"; + endEventName = "touchend"; + eventFactoryName = "touchEvent"; + } + } + + // mousedown + self.mouseEvent({target: element}, startEventName); + + if (startCallback) { + startCallback(); + } + + // Mouse move doesn't happen instantly + setTimeout(function() { + var ax = element.offsetLeft + offsetX/2, + ay = element.offsetTop + offsetY/2, + bx = element.offsetLeft + offsetX, + by = element.offsetTop + offsetY; + + // Do two moves to be slightly realistic + self[eventFactoryName]({ + target: element, + clientX: ax, + clientY: ay + }, moveEventName); + + var eventInfo = self[eventFactoryName]({ + target: element, + clientX: bx, + clientY: by + }, moveEventName); + + if (moveCallback) { + moveCallback(); + } + + // mouse up + self[eventFactoryName](eventInfo, endEventName); + + if (endCallback) { + endCallback(); + } + }); + } + }, + + fireEventsOnTimeline: { + value: function(timeline, callback) { + var i, j, stepKey; + for (i = 0; i < timeline.length; i++) { + var line = timeline[i]; + // keep initial values that we increment later + var clientX = line.target.offsetLeft; + var clientY = line.target.offsetTop; + for (j = 0; j < line.steps.length; j++) { + var step = line.steps[j]; + var time = step.time; + delete step.time; + var eventInfo = { + type: line.type, + target: line.target, + identifier: line.identifier + }; + if (line.fakeTimeStamp) { + eventInfo.timeStamp = time; + } + for (stepKey in step) { + if(stepKey.indexOf(line.type) !== -1) { + eventInfo.eventType = stepKey; + var typeInfo = step[stepKey]; + if (typeInfo) { + eventInfo.clientX = clientX = clientX + typeInfo.dx; + eventInfo.clientY = clientY = clientY + typeInfo.dy; + } else { + eventInfo.clientX = clientX; + eventInfo.clientY = clientY; + } + } else { + eventInfo[stepKey] = step[stepKey]; + } + } + console.log("_scheduleEventForTime", eventInfo); + this._scheduleEventForTime(eventInfo, time, callback); + } + } + } + }, + + _nextStepTime: { + value: 0 + }, + + _eventsInOrder: { + value: null + }, + + _scheduleEventForTime: { + value: function(eventInfo, t, callback) { + var self = this; + if(!self._eventsInOrder) { + self._eventsInOrder = []; + self._touchesInProgress = []; + var foo = function() { + setTimeout(function() { + var events = self._eventsInOrder[self._nextStepTime]; + if (events) { + console.log("********** nextStepTime:" + self._nextStepTime + " **********"); + } + while(!events || self._eventsInOrder.length === self._nextStepTime) { + self._nextStepTime++; + events = self._eventsInOrder[self._nextStepTime]; + if (events) { + console.log("********** nextStepTime:" + self._nextStepTime + " **********"); + } + } + self._dispatchScheduledEvents(events); + callback(self._nextStepTime); + self._nextStepTime++; + if(self._eventsInOrder.length > self._nextStepTime) { + // while we have more events in the time line keep going. + foo(); + } else { + self._eventsInOrder = null; + self._nextStepTime = 0; + } + }, 10); + }; + foo(); + } + if(self._eventsInOrder[t]) { + self._eventsInOrder[t].push(eventInfo); + } else { + self._eventsInOrder[t] = [eventInfo]; + } + } + }, + + _touchesInProgress: { + value: null + }, + + _dispatchScheduledEvents: { + value: function(eventFragments) { + var i, eventInfos = {}, eventInfo; + for (i = 0; i < eventFragments.length; i++) { + var pointer = eventFragments[i]; + if(pointer.type === "touch") { + if(pointer.eventType === "touchstart") { + this._touchesInProgress.push(pointer); + } else if(pointer.typeName === "touchend") { + this._touchesInProgress.splice(this._touchesInProgress.indexOf(pointer),1); + } + if(eventInfo = eventInfos[pointer.eventType]) { + // if the event is already initialized all we need to do is add to the changedTouches. + eventInfo.changedTouches.push(pointer); + } else { + eventInfo = {}; + eventInfo.target = pointer.target; + eventInfo.timeStamp = pointer.timeStamp; + eventInfo.changedTouches = [pointer]; + eventInfos[pointer.eventType] = eventInfo; + } + } else { + // mouse event + this.mouseEvent(pointer, pointer.eventType); + } + } + // at the end we know all the touches + for(var eventType in eventInfos) { + if (eventInfos.hasOwnProperty(eventType)) { + eventInfo = eventInfos[eventType]; + eventInfo.touches = this._touchesInProgress; + // this is not strictly correct + eventInfo.targetTouches = eventInfo.changedTouches; + this.touchEvent(eventInfo, eventType); + } + } + } + }, + + evaluateNode: { + enumerable: false, + value: function(xpathExpression, contextNode, namespaceResolver, resultType, result) { + if (!contextNode) { + contextNode = this.document; + } + if (!resultType) { + resultType = XPathResult.FIRST_ORDERED_NODE_TYPE; + } + + var pathResult = this.iframe.contentDocument.evaluate(xpathExpression, contextNode, namespaceResolver, resultType, result); + if (pathResult) { + switch (pathResult.resultType) { + case XPathResult.NUMBER_TYPE: + return pathResult.numberValue; + case XPathResult.BOOLEAN_TYPE: + return pathResult.booleanValue; + case XPathResult.STRING_TYPE: + return pathResult.stringValue; + default: + return pathResult.singleNodeValue; + } + } + } + }, + + evaluateBoolean: { + enumerable: false, + value: function(xpathExpression) { + return this.evaluateNode(xpathExpression, null, null, XPathResult.BOOLEAN_TYPE, null); + } + }, + + evaluateNumber: { + enumerable: false, + value: function(xpathExpression) { + return this.evaluateNode(xpathExpression, null, null, XPathResult.NUMBER_TYPE, null); + } + }, + + evaluateString: { + enumerable: false, + value: function(xpathExpression) { + return this.evaluateNode(xpathExpression, null, null, XPathResult.STRING_TYPE, null); + } + }, + + handleEvent: { + enumerable: false, + value: function(event) { + if (this[event.type]) { + this[event.type](event); + } + } + }, + + loaded: { + value: false + }, + + iframe: { + value: null + }, + + testWindow: { + value: null + } +}, { + + queueTest: { + value: function(testName, options, callback) { + console.log("TestPageLoader.queueTest() - " + testName); + var testPage = global.testPage = TestPageLoader.testPage; + options = TestPageLoader.options(testName, options, callback); + describe(testName, function() { + + beforeAll(function (done) { + console.group(testName); + testPage.loadTest(testPage.loadFrame(options), options).then(function(theTestPage) { + expect(theTestPage.loaded).toBe(true); + done(); + }); + }); + + // add the rest of the assertions + options.callback(testPage); + + afterAll(function(done) { + testPage.unloadTest(); + console.groupEnd(); + done(); + }); + }); + } + }, + + options: { + value: function(testName, options) { + var callback = arguments[2]; + if (typeof options === "function") { + options = { callback: options}; + } else { + options = options || {}; + options.callback = callback; + } + options.testName = testName; + // FIXME Hack to get current directory + var dir; + if ( + this.options.caller.arguments && + this.options.caller.arguments[2] && + this.options.caller.arguments[2].directory + ) { + dir = this.options.caller.arguments[2].directory; + } else { + dir = this.options.caller.caller.arguments[2].directory; + } + + options.directory = dir; + + return options; + } + }, + + testPage: { + get: function() { + var testPage = global.testpage; + if (!testPage) { + testPage = new TestPageLoader(); + } + return testPage; + } + } +}); + +var EventInfo = exports.EventInfo = Montage.specialize( { + + target: { + value: null + }, + + clientX: { + value: null + }, + + clientY: { + value: null + }, + + pageX: { + value: null + }, + + pageY: { + value: null + }, + + initWithElement: { + value: function(element) { + if (element != null) { + this.target = element; + + var elementDelta = this.positionOfElement(element); + this.clientX = elementDelta.x + element.offsetWidth / 2; + this.clientY = elementDelta.y + element.offsetHeight / 2; + this.pageX = elementDelta.x + element.offsetWidth / 2; + this.pageY = elementDelta.y + element.offsetHeight / 2; + + } else { + this.target = global.testpage.global.document; + } + return this; + } + }, + + initWithSelector: { + value: function(selector) { + var element = this.querySelector(selector); + return this.initWithElement(element); + } + }, + + initWithElementAndPosition: { + value: function(element, x, y) { + this.initWithElement(element); + this.clientX = x; + this.clientY = y; + return this; + } + }, + + positionOfElement: { + value: function(element) { + return Point.convertPointFromNodeToPage(element); + } + }, + + move: { + value: function(x, y) { + if (x) { + this.clientX += x; + this.pageX += x; + } + if (y) { + this.clientY += y; + this.pageY += y; + } + } + }, + + testPageLoader: { + value: null + } + +}); + +global.loaded = function() { + global.testpage.loaded = true; +}; From 3268e38f428bc731200c0b95de06eab2911867d3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 15:03:07 -0700 Subject: [PATCH 159/407] Make inDocument a public API --- ui/component.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/ui/component.js b/ui/component.js index c6fdc423de..bc2adbf7e3 100644 --- a/ui/component.js +++ b/ui/component.js @@ -1204,6 +1204,11 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto }, _inDocument: { + get: function() { + return this.inDocument; + } + }, + inDocument: { value: false }, @@ -1217,9 +1222,9 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto this.unregisterDroppable(); } - if (this._inDocument && typeof this.exitDocument === "function") { + if (this.inDocument && typeof this.exitDocument === "function") { this.exitDocument(); - this._inDocument = false; + this.inDocument = false; } } }, @@ -1241,7 +1246,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto } } - if (component._inDocument) { + if (component.inDocument) { component.__exitDocument(); } }; @@ -3079,7 +3084,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto } } this._shouldBuildOut = false; - if (this._inDocument) { + if (this.inDocument) { this._buildIn(); } } @@ -3099,7 +3104,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto this.__shouldBuildOut = value; if (value) { this._shouldBuildIn = false; - if (this._inDocument) { + if (this.inDocument) { this._buildOut(); } } @@ -3295,7 +3300,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto bubbles: false }); this.dispatchEvent(event); - this._inDocument = true; + this.inDocument = true; if (this.parentComponent) { this.parentComponent._childWillEnterDocument(); } From a7a6ee7ee43039bee532aa58cf2ba43c72ab9ad3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 15:13:26 -0700 Subject: [PATCH 160/407] add files entry to make inlined projects accessible to other projects --- package.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/package.json b/package.json index ca492f9385..229397bf57 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,13 @@ "strange": "^1.7.2", "ical.js": "~1.4.0" }, + "files": [ + "./core/mr", + "./core/collections", + "./core/promise-io", + "./core/frb", + "./testing" + ], "devDependencies": { "montage-testing": "./testing", "concurrently": "^3.4.0", From 34370f9870f6199dc04c4a9778674b91e88c5d8f Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 15:32:26 -0700 Subject: [PATCH 161/407] trying to fix package.json for sub folders-projects --- package.json | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 229397bf57..d8186b8970 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,10 @@ }, "production": true, "dependencies": { - "mr": "./core/mr", - "collections": "./core/collections", - "q-io": "./core/promise-io", - "frb": "./core/frb", + "mr": "file:./core/mr", + "collections": "file:./core/collections", + "q-io": "file:./core/promise-io", + "frb": "file:./core/frb", "bluebird": "~3.5.5", "htmlparser2": "~3.0.5", "weak-map": "^1.0.5", @@ -64,14 +64,14 @@ "ical.js": "~1.4.0" }, "files": [ - "./core/mr", - "./core/collections", - "./core/promise-io", - "./core/frb", - "./testing" + "core/mr", + "core/collections", + "core/promise-io", + "core/frb", + "testing" ], "devDependencies": { - "montage-testing": "./testing", + "montage-testing": "file:./testing", "concurrently": "^3.4.0", "http-server": "^0.10.0", "xhr2": "^0.1.4", @@ -113,7 +113,8 @@ "test:jasmine": "concurrently \"http-server -p 8085\" \"open http://localhost:8085/test/run.html\"", "test-collections": "node core/collections/test/run-node.js", "test-frb": "jasmine-node core/frb/spec", - "build-frb-parser": "pegjs --allowed-start-rules expression,sheet core/frb/grammar.pegjs" + "build-frb-parser": "pegjs --allowed-start-rules expression,sheet core/frb/grammar.pegjs", + "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";" }, "bin": { "mr": "core/mr/bin/mr" From cd98774996c7765a4d23bb1dc4758f414a290a3c Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 16:00:12 -0700 Subject: [PATCH 162/407] trying pre-install on mr --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index d8186b8970..44616b0da4 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "pegjs": "git://github.com/dmajda/pegjs.git" }, "scripts": { + "preinstall": "npm install core/mr", "test-mr": "node core/mr/test/run-node.js", "lint-mr": "jshint core/mr", "integration": "MONTAGE_VERSION=${MONTAGE_VERSION:=./} MOP_VERSION=${MOP_VERSION:=#master} node node_modules/mop-integration/integration", From 116b94a049411ff0fde93f5efa58e019a9341642 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Apr 2020 16:53:19 -0700 Subject: [PATCH 163/407] tweak to fix install issues --- package.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 44616b0da4..4b60933261 100644 --- a/package.json +++ b/package.json @@ -64,11 +64,11 @@ "ical.js": "~1.4.0" }, "files": [ - "core/mr", - "core/collections", - "core/promise-io", - "core/frb", - "testing" + "core/mr/*", + "core/collections/*", + "core/promise-io/*", + "core/frb/*", + "testing/*" ], "devDependencies": { "montage-testing": "file:./testing", @@ -92,7 +92,6 @@ "pegjs": "git://github.com/dmajda/pegjs.git" }, "scripts": { - "preinstall": "npm install core/mr", "test-mr": "node core/mr/test/run-node.js", "lint-mr": "jshint core/mr", "integration": "MONTAGE_VERSION=${MONTAGE_VERSION:=./} MOP_VERSION=${MOP_VERSION:=#master} node node_modules/mop-integration/integration", From 38feeb3e890a18271a8b12fd0e059ebba9145b81 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 18 Apr 2020 16:28:51 -0700 Subject: [PATCH 164/407] remove require of collections, mr, frb, promise-io, as separate packages --- core/bindings.js | 2 +- core/collections/README.md | 2 +- core/collections/package.json | 1 - core/collections/test/spec/array-spec.js | 6 +-- core/collections/test/spec/clone-spec.js | 4 +- core/collections/test/spec/deque-fuzz.js | 4 +- core/collections/test/spec/deque-spec.js | 2 +- core/collections/test/spec/dict-spec.js | 2 +- core/collections/test/spec/fast-map-spec.js | 2 +- core/collections/test/spec/fast-set-spec.js | 6 +-- core/collections/test/spec/heap-spec.js | 2 +- core/collections/test/spec/iterator-spec.js | 2 +- core/collections/test/spec/lfu-map-spec.js | 2 +- core/collections/test/spec/lfu-set-spec.js | 2 +- core/collections/test/spec/list-spec.js | 2 +- .../test/spec/listen/array-changes-spec.js | 2 +- .../test/spec/listen/property-changes-spec.js | 4 +- core/collections/test/spec/lru-map-spec.js | 2 +- core/collections/test/spec/lru-set-spec.js | 2 +- core/collections/test/spec/map-spec.js | 2 +- core/collections/test/spec/order.js | 2 +- core/collections/test/spec/regexp-spec.js | 2 +- core/collections/test/spec/set-spec.js | 2 +- core/collections/test/spec/set.js | 2 +- core/collections/test/spec/shim-array-spec.js | 2 +- .../test/spec/shim-functions-spec.js | 4 +- .../collections/test/spec/shim-object-spec.js | 6 +-- .../test/spec/sorted-array-map-spec.js | 2 +- .../test/spec/sorted-array-set-spec.js | 2 +- .../test/spec/sorted-array-spec.js | 2 +- core/collections/test/spec/sorted-map-spec.js | 2 +- core/collections/test/spec/sorted-set-spec.js | 6 +-- core/converter/expression-converter.js | 6 +-- core/converter/pipeline-converter.js | 6 +-- core/core.js | 26 +++++----- core/counted-set.js | 2 +- core/criteria.js | 12 ++--- core/deprecate.js | 4 +- core/event/event-manager.js | 4 +- core/extras/object.js | 6 +-- core/extras/string.js | 2 +- core/frb/README.md | 52 +++++++++---------- core/frb/bindings.js | 2 +- core/frb/compile-evaluator.js | 6 +-- core/frb/dom.js | 2 +- core/frb/expand.js | 4 +- core/frb/language-temp.js | 8 +-- core/frb/language.js | 4 +- core/frb/merge.js | 2 +- core/frb/observers.js | 16 +++--- core/frb/operators.js | 8 +-- core/frb/parse-temp.js | 2 +- core/frb/parse.js | 4 +- .../temperature-converter.js | 4 +- core/frb/signal.js | 2 +- core/frb/spec/assign-spec.js | 2 +- core/frb/spec/bind-spec.js | 4 +- core/frb/spec/bindings-spec.js | 2 +- core/frb/spec/gate-spec.js | 2 +- core/frb/spec/group-spec.js | 2 +- core/frb/spec/items-spec.js | 2 +- core/frb/spec/merge-spec.js | 2 +- core/frb/spec/observers-spec.js | 4 +- core/frb/spec/only-binder-spec.js | 2 +- core/frb/spec/readme-spec.js | 22 ++++---- core/gate.js | 4 +- core/localizer.js | 14 ++--- core/meta/object-descriptor.js | 4 +- core/promise-io/coverage-report.js | 2 +- core/promise-io/fs-mock.js | 2 +- core/promise-io/http-apps.js | 2 +- core/range-controller.js | 28 +++++++++- core/range.js | 2 +- core/serialization/bindings.js | 14 ++--- .../deserializer/montage-deserializer.js | 2 +- .../deserializer/montage-interpreter.js | 2 +- .../deserializer/serialization-extractor.js | 4 +- core/serialization/serialization.js | 18 +++---- .../serializer/montage-malker.js | 2 +- core/tree-controller.js | 6 +-- core/undo-manager.js | 4 +- .../raw-value-to-object-converter.js | 6 +-- data/model/data-ordering.js | 8 +-- data/service/authorization-manager.js | 24 ++++----- data/service/data-service.js | 6 +-- data/service/data-stream.js | 6 +-- data/service/data-trigger.js | 4 +- data/service/expression-data-mapping.js | 12 ++--- data/service/graphql-service.js | 10 ++-- data/service/http-service.js | 10 ++-- data/service/indexed-d-b-data-service.js | 4 +- data/service/mapping-rule.js | 4 +- data/service/persistent-data-service.js | 4 +- data/service/raw-data-service.js | 16 +++--- data/service/snapshot-service.js | 2 +- node.js | 4 +- package.json | 13 +---- test/mocks/component.js | 2 +- test/mocks/dom.js | 6 +-- test/mocks/event.js | 2 +- test/package.json | 3 +- test/spec/core/core-spec.js | 4 +- test/spec/core/localizer-spec.js | 4 +- .../spec/core/localizer/serialization-spec.js | 2 +- test/spec/core/range-controller-spec.js | 2 +- test/spec/core/set-spec.js | 2 +- test/spec/core/undo-manager-spec.js | 6 +-- test/spec/data/authorization-manager.js | 46 ++++++++-------- test/spec/paths-spec.js | 6 +-- .../montage-deserializer-element-spec.js | 2 +- .../montage-deserializer-spec.js | 2 +- .../authentication-manager-panel.js | 2 +- .../authorization-manager-panel.js | 4 +- ui/base/abstract-control.js | 2 +- ui/base/abstract-image.js | 2 +- ui/component.js | 6 +-- ui/control.js | 4 +- ui/flow.reel/flow.js | 8 +-- ui/list-item.reel/list-item.js | 16 +++--- ui/repetition.reel/repetition.js | 6 +-- ui/slider.reel/slider.js | 14 ++--- ui/tree-list.reel/tree-list.js | 10 ++-- 122 files changed, 366 insertions(+), 355 deletions(-) diff --git a/core/bindings.js b/core/bindings.js index 369b519e6c..14e1e6aa69 100644 --- a/core/bindings.js +++ b/core/bindings.js @@ -8,4 +8,4 @@ require("./deprecate").deprecationWarning("montage/core/bindings", "montage/core /** * @deprecated console.log montage/core/bindings is deprecated, use montage/core/core instead */ -exports.Bindings = require("frb"); +exports.Bindings = require("core/frb/bindings"); diff --git a/core/collections/README.md b/core/collections/README.md index 2a76101677..eb7c7085ec 100644 --- a/core/collections/README.md +++ b/core/collections/README.md @@ -17,7 +17,7 @@ or bundler when using Collections in web browsers has the advantage of only incorporating the modules you need. However, you can just embed ` - + + diff --git a/core/mr/test/spec/main/node_modules/dot.js/a.js b/core/mr/test/spec/main/node_modules/dot.js/a.js index 9320b557ea..dc0f411ccd 100644 --- a/core/mr/test/spec/main/node_modules/dot.js/a.js +++ b/core/mr/test/spec/main/node_modules/dot.js/a.js @@ -1 +1 @@ -module.exports = 20; +module.exports = 50; diff --git a/core/mr/test/spec/main/node_modules/js-ext/a.js b/core/mr/test/spec/main/node_modules/js-ext/a.js index dc0f411ccd..9320b557ea 100644 --- a/core/mr/test/spec/main/node_modules/js-ext/a.js +++ b/core/mr/test/spec/main/node_modules/js-ext/a.js @@ -1 +1 @@ -module.exports = 50; +module.exports = 20; diff --git a/core/mr/test/spec/read/package.json b/core/mr/test/spec/read/package.json index 040e411387..9dbb4c8b09 100644 --- a/core/mr/test/spec/read/package.json +++ b/core/mr/test/spec/read/package.json @@ -1,5 +1,5 @@ { "mappings": { - "bluebird": "../../../node_modules/bluebird" + "bluebird": "../../../../../node_modules/bluebird" } } diff --git a/core/mr/test/spec/sandbox/package.json b/core/mr/test/spec/sandbox/package.json index 65a62fb231..cb7803908b 100644 --- a/core/mr/test/spec/sandbox/package.json +++ b/core/mr/test/spec/sandbox/package.json @@ -1,7 +1,7 @@ { "mappings": { "mr": "../../..", - "bluebird": "../../../node_modules/bluebird" + "bluebird": "../../../../../node_modules/bluebird" }, "redirects": { "d": "a" diff --git a/core/mr/test/spec/sandbox/program.js b/core/mr/test/spec/sandbox/program.js index a64a2f17c5..6a825f4750 100644 --- a/core/mr/test/spec/sandbox/program.js +++ b/core/mr/test/spec/sandbox/program.js @@ -1,7 +1,7 @@ var test = require("test"); var Promise = require("bluebird"); -var sandbox = require("core/mr/sandbox"); +var sandbox = require("mr/sandbox"); var a = require("./a"); var dep = require("dependency/main"); From 0e195ab6d5f735db2b864e21a28adc934e4af2c1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:26:00 -0700 Subject: [PATCH 190/407] bluebird-based q implementation for promise-q --- core/q.js | 452 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 452 insertions(+) create mode 100644 core/q.js diff --git a/core/q.js b/core/q.js new file mode 100644 index 0000000000..0d0a8b2765 --- /dev/null +++ b/core/q.js @@ -0,0 +1,452 @@ +//var B = require("bluebird/js/main/promise")(); +var B = require("bluebird"); +var Bproto = B.prototype; + +//deferredPrototype is Object.prototype, and that creates a mess +// var deferredPrototype = B.pending().constructor.prototype; + +// deferredPrototype.makeNodeResolver = function() { +// return this.asCallback; +// }; + +function bind(fn, ctx) { + return function() { + return fn.apply(ctx, arguments); + }; +} + +module.exports = Q; +function Q(value) { + return B.cast(value); +} + +Object.defineProperty(Q, "longStackSupport", { + set: function(val) { + if (val) { + B.longStackTraces(); + } + }, + get: function() { + return B.haveLongStackTraces(); + } +}); + +Q.reject = B.rejected; +Q.defer = function() { + var b = B.pending(); + b.resolve = bind(b.resolve, b); + b.reject = bind(b.reject, b); + //b.progress doesn't exists anymore + //b.notify = bind(b.progress, b); + b.makeNodeResolver = function() { + return this.asCallback; + }; + return b; +}; +Q.all = B.all; +Q.allSettled = B.allSettled; +Bproto.allSettled = Bproto.all; + +Q.delay = function (object, timeout) { + if (timeout === void 0) { + timeout = object; + object = void 0; + } + return Q(object).delay(timeout); +}; + +Q.timeout = function (object, ms, message) { + return Q(object).timeout(ms, message); +}; + +Q.spread = function(a, b, c) { + return Q(a).spread(b, c); +}; + +Q.join = function (x, y) { + return Q.spread([x, y], function (x, y) { + if (x === y) { + // TODO: "===" should be Object.is or equiv + return x; + } else { + throw new Error("Can't join: not the same: " + x + " " + y); + } + }); +}; + +Q.race = B.race; + +Q["try"] = function(callback) { + return B.try(callback); +}; + + +Q.fapply = function (callback, args) { + return B.try(callback, args); +}; + +Q.fcall = function (callback) { + return B.try(callback, [].slice.call(arguments, 1)); +}; + +Q.fbind = function(callback) { + var args = [].slice.call(arguments, 1); + return function() { + return B.try(callback, [].slice.call(arguments).concat(args), this); + }; +}; + +Q.async = B.coroutine; +Q.spawn = B.spawn; +Q.cast = B.cast; +Q.isPromise = B.is; + +Q.promised = promised; +function promised(callback) { + return function () { + return Q.spread([this, all(arguments)], function (self, args) { + return callback.apply(self, args); + }); + }; +} + +Q.isThenable = function(o) { + return o && typeof o.then === "function"; +}; + +Q.Promise = function(handler) { + return new B(handler); +}; + +Bproto.inspect = Bproto.toJSON; +Bproto.thenResolve = Bproto.thenReturn; +Bproto.thenReject = Bproto.thenThrow; +Bproto.progress = Bproto.progressed; + +Bproto.dispatch = function(op, args) { + if (op === "then" || op === "get" || op === "post" || op === "keys") { + return this[op].apply(this, args); + } + return B.rejected(new Error( + "Fulfilled promises do not support the " + op + " operator" + )); +}; + +Bproto.post = function(name, args, thisp) { + return this.then(function(val) { + if (name == null) { + return val.apply(thisp, args); + } + else { + return val[name].apply(val, args); + } + }); +}; + +Bproto.invoke = function(name) { + var args = [].slice.call(arguments, 1); + return this.post(name, args); +}; + +Bproto.fapply = function(args) { + return this.post(void 0, args); +}; + +Bproto.fcall = function() { + var args = [].slice.call(arguments); + return this.post(void 0, args); +}; + +Bproto.keys = function() { + return this.then(function(val) { + return Object.keys(val); + }); +}; + +Bproto.timeout = function(ms, message) { + var self = this; + setTimeout(function() { + var e = new Error(message || "Timed out after " + ms + " ms"); + self.reject(e); + }, ms); + return this.then(); +}; + +function delay(ms, val) { + var d = B.pending(); + setTimeout(function(){ + d.fulfill(val); + }, ms); + return d.promise; +} + +Bproto.delay = function(ms) { + return this.then(function(value){ + return delay(ms, value); + }); +}; + +var b = B.fulfilled(); +function thrower(e){ + process.nextTick(function(){ + throw e; + }); +} +Q.nextTick = function(fn) { + b.then(fn).caught(thrower); +}; + +Q.resolve = B.fulfilled; +Q.fulfill = B.fulfilled; +Q.isPromiseAlike = Q.isThenable; +Q.when = function(a, b, c, d) { + return B.cast(a).then(b, c, d); +}; +Q.fail = function(a, b) { + return B.cast(a).caught(b); +}; +Q.fin = function(a, b) { + return B.cast(a).lastly(b); +}; +Q.progress = function(a, b) { + return B.cast(a).progress(b); +}; +Q.thenResolve = function(a, b) { + return B.cast(a).thenReturn(b); +}; +Q.thenReject = function(a, b) { + return B.cast(a).thenThrow(b); +}; +Q.isPending = function(a, b) { + return B.cast(a).isPending() +}; +Q.isFulfilled = function(a, b) { + return B.cast(a).isFulfilled(); +}; +Q.isRejected = function(a, b) { + return B.cast(a).isRejected(); +}; +Q.master = function(a, b) { + return a; +}; +Q.makePromise = function (b) { + return Q.Promise(b); +}; +Q.dispatch = function(a, b, c) { + return B.cast(a).dispatch(b, c); +}; +Q.get = function(a, b) { + return B.cast(a).get(b); +}; +Q.keys = function(a, b) { + return B.cast(a).keys(); +}; +Q.post = function(a, b, c) { + return B.cast(a).post(b, c); +}; +Q.mapply = function(a, b, c) { + return B.cast(a).post(b, c); +}; +Q.send = function(a, b) { + return B.cast(a).post(b, [].slice.call(arguments, 2)); +}; +Q.set = function(a, b, c) { + return B.cast(a).then(function(val){ + return val[b] = c; + }); +}; +Q.delete = function(a, b) { + return B.cast(a).then(function(val){ + return delete val[b]; + }); +}; +Q.nearer = function(v) { + if( B.is(v) && v.isFulfilled()) { + return v.inspect().value; + } + return v; +}; + +Bproto.fail = Bproto.caught; +Bproto.fin = Bproto.lastly; + +Bproto.mapply = Bproto.post; +Bproto.fbind = function() { + return Q.fbind.apply(Q, [this].concat(Array.prototype.slice.call(arguments))); +}; + +Bproto.send = function(){ + return this.post(name, [].slice.call(arguments)); +}; + +Bproto.mcall = Bproto.send; + +//wtf? +Bproto.passByCopy = function(v) { + return v; +}; + + +/** + * Terminates a chain of promises, forcing rejections to be + * thrown as exceptions. + * @param {Any*} promise at the end of a chain of promises + * @returns nothing + */ +Q.done = function (object, fulfilled, rejected, progress) { + return Q(object).done(fulfilled, rejected, progress); +}; + +Bproto.done = function (fulfilled, rejected, progress) { + var onUnhandledError = function (error) { + // forward to a future turn so that ``when`` + // does not catch it and turn it into a rejection. + var onerror = Q.onerror; + var sheduled = scheduler(function () { + promise._attachExtraTrace(error); + if (onerror) { + onerror(error); + } else { + throw error; + } + }); + if (scheduler.isStatic === true && typeof sheduled === "function") { + sheduled(); + } + }; + + // Avoid unnecessary `nextTick`ing via an unnecessary `when`. + var promise = fulfilled || rejected || progress ? + this.then(fulfilled, rejected, progress) : + this; + + if (typeof process === "object" && process && process.domain) { + onUnhandledError = process.domain.bind(onUnhandledError); + } + + promise.then(void 0, onUnhandledError); +}; + + + +/* + Benoit 4/21/2020 + + NQ doesn't seems to be used anyway, commenting +*/ +// var NQ = {}; + +// NQ.makeNodeResolver = function (resolve) { +// return function (error, value) { +// if (error) { +// resolve(Q.reject(error)); +// } else if (arguments.length > 2) { +// resolve(Array.prototype.slice.call(arguments, 1)); +// } else { +// resolve(value); +// } +// }; +// }; + +Q.nfapply = function(callback, args) { + +}; + +/*Directly copypasted from Q */ +//// BEGIN UNHANDLED REJECTION TRACKING + +// This promise library consumes exceptions thrown in handlers so they can be +// handled by a subsequent promise. The exceptions get added to this array when +// they are created, and removed when they are handled. Note that in ES6 or +// shimmed environments, this would naturally be a `Set`. + +var unhandledReasons = []; +var unhandledRejections = []; +var unhandledReasonsDisplayed = false; +var trackUnhandledRejections = true; +function displayUnhandledReasons() { + if ( + !unhandledReasonsDisplayed && + typeof window !== "undefined" && + window.console + ) { + console.warn("[Q] Unhandled rejection reasons (should be empty):", + unhandledReasons); + } + + unhandledReasonsDisplayed = true; +} + +function logUnhandledReasons() { + for (var i = 0; i < unhandledReasons.length; i++) { + var reason = unhandledReasons[i]; + console.warn("Unhandled rejection reason:", reason); + } +} + +function resetUnhandledRejections() { + unhandledReasons.length = 0; + unhandledRejections.length = 0; + unhandledReasonsDisplayed = false; + + if (!trackUnhandledRejections) { + trackUnhandledRejections = true; + + // Show unhandled rejection reasons if Node exits without handling an + // outstanding rejection. (Note that Browserify presently produces a + // `process` global without the `EventEmitter` `on` method.) + if (typeof process !== "undefined" && process.on) { + process.on("exit", logUnhandledReasons); + } + } +} + +function trackRejection(promise, reason) { + if (!trackUnhandledRejections) { + return; + } + + unhandledRejections.push(promise); + if (reason && typeof reason.stack !== "undefined") { + unhandledReasons.push(reason.stack); + } else { + unhandledReasons.push("(no stack) " + reason); + } + displayUnhandledReasons(); +} + +function untrackRejection(promise) { + if (!trackUnhandledRejections) { + return; + } + + var at = unhandledRejections.indexOf(promise); + if (at !== -1) { + unhandledRejections.splice(at, 1); + unhandledReasons.splice(at, 1); + } +} + +Q.resetUnhandledRejections = resetUnhandledRejections; + +Q.getUnhandledReasons = function () { + // Make a copy so that consumers can't interfere with our internal state. + return unhandledReasons.slice(); +}; + +Q.stopUnhandledRejectionTracking = function () { + resetUnhandledRejections(); + if (typeof process !== "undefined" && process.on) { + process.removeListener("exit", logUnhandledReasons); + } + trackUnhandledRejections = false; +}; + +resetUnhandledRejections(); +/*Directly copypasted from Q end */ + + + +B.onPossiblyUnhandledRejection(function(reason, promise){ + trackRejection(promise, reason); +}); From 21ef4439322922b822fb8b376f3d2c41504a5c41 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:28:15 -0700 Subject: [PATCH 191/407] - performance optimization - require changes to follow dependency changes --- core/core.js | 106 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 82 insertions(+), 24 deletions(-) diff --git a/core/core.js b/core/core.js index 16a9445ad6..56831e8cdf 100644 --- a/core/core.js +++ b/core/core.js @@ -2,7 +2,7 @@ * @module montage/core/core */ -require("core/collections/shim"); +require("./collections/shim"); require("./shim/object"); require("./shim/array"); require("./extras/object"); @@ -17,15 +17,17 @@ require("./extras/weak-map"); require("proxy-polyfill/proxy.min"); -var Map = require("core/collections/map"); -var WeakMap = require("core/collections/weak-map"); -var Set = require("core/collections/set"); +var Map = require("./collections/map"); +var WeakMap = require("./collections/weak-map"); +var Set = require("./collections/set"); var ATTRIBUTE_PROPERTIES = "AttributeProperties", UNDERSCORE = "_", PROTO = "__proto__", VALUE = "value", + WRITABLE = "writable", ENUMERABLE = "enumerable", + CONFIGURABLE = "configurable", SERIALIZABLE = "serializable", FUNCTION = "function", UNDERSCORE_UNICODE = 95, @@ -425,9 +427,60 @@ valuePropertyDescriptor.value = function Montage_defineProperty(obj, prop, descr var isValueDescriptor = (VALUE in descriptor); + // reset defaults appropriately for framework. if (PROTO in descriptor) { - descriptor.__proto__ = (isValueDescriptor ? (typeof descriptor.value === FUNCTION ? _defaultFunctionValueProperty : _defaultObjectValueProperty) : _defaultAccessorProperty); + //Replaces the tweak of __proto__ + if(isValueDescriptor) { + if(typeof descriptor.value === FUNCTION) { + // _defaultFunctionValueProperty = { + // writable: true, + // enumerable: false, + // configurable: true + // /*, + // serializable: false + // */ + // }; + + //Montage objects defineProperty function value : should be non-enumerable by default + if(!hasProperty.call(descriptor, ENUMERABLE)) descriptor.enumerable = false; + + if(!hasProperty.call(descriptor, WRITABLE)) descriptor.writable = true; + if(!hasProperty.call(descriptor, CONFIGURABLE)) descriptor.configurable = true; + + } else { + // var _defaultObjectValueProperty = { + // writable: true, + // enumerable: true, + // configurable: true, + // serializable: "reference" + // }; + + if(!hasProperty.call(descriptor, ENUMERABLE)) { + descriptor.enumerable = prop.charCodeAt(0) === UNDERSCORE_UNICODE ? false : true; + } + + if(!hasProperty.call(descriptor, WRITABLE)) descriptor.writable = true; + if(!hasProperty.call(descriptor, CONFIGURABLE)) descriptor.configurable = true; + if(!hasProperty.call(descriptor, SERIALIZABLE) && descriptor.enumerable && descriptor.writable) descriptor.serializable = "reference"; + + } + } else { + // var _defaultAccessorProperty = {s + // enumerable: true, + // configurable: true, + // serializable: true + // }; + + if(!hasProperty.call(descriptor, ENUMERABLE)) { + descriptor.enumerable = prop.charCodeAt(0) === UNDERSCORE_UNICODE ? false : true; + } + + if(!hasProperty.call(descriptor, CONFIGURABLE)) descriptor.configurable = true; + if(!hasProperty.call(descriptor, SERIALIZABLE) && descriptor.enumerable && descriptor.writable) descriptor.serializable = true; + } + + //descriptor.__proto__ = (isValueDescriptor ? (typeof descriptor.value === FUNCTION ? _defaultFunctionValueProperty : _defaultObjectValueProperty) : _defaultAccessorProperty); } else { var defaults; if (isValueDescriptor) { @@ -448,20 +501,25 @@ valuePropertyDescriptor.value = function Montage_defineProperty(obj, prop, descr } } - if (!hasProperty.call(descriptor, ENUMERABLE) && prop.charCodeAt(0) === UNDERSCORE_UNICODE) { - descriptor.enumerable = false; - } + // if (!hasProperty.call(descriptor, ENUMERABLE) && prop.charCodeAt(0) === UNDERSCORE_UNICODE) { + // descriptor.enumerable = false; + // } - if (!hasProperty.call(descriptor, SERIALIZABLE)) { - if (! descriptor.enumerable) { - descriptor.serializable = false; - } else if (descriptor.get && !descriptor.set) { - descriptor.serializable = false; - } else if (descriptor.writable === false) { - descriptor.serializable = false; + if(prop !== "_localization") { + + if (!hasProperty.call(descriptor, SERIALIZABLE)) { + if (! descriptor.enumerable) { + descriptor.serializable = false; + } else if (descriptor.get && !descriptor.set) { + descriptor.serializable = false; + } else if (descriptor.writable === false) { + descriptor.serializable = false; + } } + } + if (SERIALIZABLE in descriptor) { // get the _serializableAttributeProperties property or creates it through the entire chain if missing. getAttributeProperties(obj, SERIALIZABLE)[prop] = descriptor.serializable; @@ -1021,7 +1079,7 @@ Montage.defineProperty(Montage.prototype, "callDelegateMethod", { // Property Changes -var PropertyChanges = require("core/collections/listen/property-changes"); +var PropertyChanges = require("./collections/listen/property-changes"); Object.addEach(Montage, PropertyChanges.prototype); Object.addEach(Montage.prototype, PropertyChanges.prototype); @@ -1136,7 +1194,7 @@ Object.addEach(Montage.prototype, PropertyChanges.prototype); * @extends frb * @typedef {string} FRBExpression */ -var Bindings = exports.Bindings = require("core/frb/bindings"); +var Bindings = exports.Bindings = require("./frb/bindings"); var bindingPropertyDescriptors = { @@ -1249,13 +1307,13 @@ Montage.defineProperties(Montage.prototype, bindingPropertyDescriptors); // Paths -var parse = require("core/frb/parse"), - evaluate = require("core/frb/evaluate"), - assign = require("core/frb/assign"), - bind = require("core/frb/bind"), - compileObserver = require("core/frb/compile-observer"), - Scope = require("core/frb/scope"), - Observers = require("core/frb/observers"), +var parse = require("./frb/parse"), + evaluate = require("./frb/evaluate"), + assign = require("./frb/assign"), + bind = require("./frb/bind"), + compileObserver = require("./frb/compile-observer"), + Scope = require("./frb/scope"), + Observers = require("./frb/observers"), autoCancelPrevious = Observers.autoCancelPrevious; From f225a146b376b7485ef932b173f7a631a02a0834 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:28:44 -0700 Subject: [PATCH 192/407] fix require --- core/deprecate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/deprecate.js b/core/deprecate.js index 6cbad851b6..32ef46168f 100644 --- a/core/deprecate.js +++ b/core/deprecate.js @@ -1,6 +1,6 @@ /* global console */ var Montage = require("./core").Montage; - Map = require("core/collections/map"); + Map = require("./collections/map"); var deprecatedFeaturesOnceMap = new Map(); From d25894302df06968d34ec481a641643f6cddff85 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:29:40 -0700 Subject: [PATCH 193/407] - brings frb in - performance improvements removing some array.map/filter --- core/frb/README.md | 2 +- core/frb/algebra.js | 24 +++++++++++++---- core/frb/bind.js | 15 ++++------- core/frb/compile-assigner.js | 8 +++++- core/frb/compile-evaluator.js | 31 +++++++++++++++------- core/frb/expand.js | 8 +++--- core/frb/observers.js | 44 ++++++++++++++++++++++++------- core/frb/spec/bindings-spec.js | 2 +- core/frb/spec/gate-spec.js | 2 +- core/frb/spec/group-spec.js | 2 +- core/frb/spec/items-spec.js | 2 +- core/frb/spec/merge-spec.js | 2 +- core/frb/spec/observers-spec.js | 4 +-- core/frb/spec/only-binder-spec.js | 2 +- core/frb/spec/readme-spec.js | 22 ++++++++-------- 15 files changed, 112 insertions(+), 58 deletions(-) diff --git a/core/frb/README.md b/core/frb/README.md index 47682e7c5c..156462adf8 100644 --- a/core/frb/README.md +++ b/core/frb/README.md @@ -711,7 +711,7 @@ from that collection as an array. The `view` binding reacts to changes to the collection and the position and length of the window. ```javascript -var SortedSet = require("core/collections/sorted-set"); +var SortedSet = require("montage/core/collections/sorted-set"); var controller = { index: SortedSet([1, 2, 3, 4, 5, 6, 7, 8]), start: 2, diff --git a/core/frb/algebra.js b/core/frb/algebra.js index bf5a60c3be..e80b7cc14a 100644 --- a/core/frb/algebra.js +++ b/core/frb/algebra.js @@ -8,19 +8,26 @@ function solve(target, source) { solve.semantics = { - solve: function (target, source) { + memo: new Map(), + + _cacheSolve: function (target, source, targetMemo) { + var targetSourceMemo, + simplification, + canRotateTargetToSource, + canRotateSourceToTarget; + while (true) { // simplify the target while (this.simplifiers.hasOwnProperty(target.type)) { - var simplification = this.simplifiers[target.type](target); + simplification = this.simplifiers[target.type](target); if (simplification) { target = simplification; } else { break; } } - var canRotateTargetToSource = this.rotateTargetToSource.hasOwnProperty(target.type); - var canRotateSourceToTarget = this.rotateSourceToTarget.hasOwnProperty(source.type); + canRotateTargetToSource = this.rotateTargetToSource.hasOwnProperty(target.type); + canRotateSourceToTarget = this.rotateSourceToTarget.hasOwnProperty(source.type); // solve for bindable target (rotate terms to source) if (!canRotateTargetToSource && !canRotateSourceToTarget) { break; @@ -32,8 +39,15 @@ solve.semantics = { source = source.args[0]; } } + //Trick to take care of all cases in one line: + return (targetMemo || ( this.memo.set(target, (targetMemo = new Map())) && targetMemo)).set(source,(targetSourceMemo = [target, source])) && targetSourceMemo; + + }, + + solve: function (target, source) { + var targetMemo = this.memo.get(target); - return [target, source]; + return (targetMemo && targetMemo.get(source)) || this._cacheSolve(target, source, targetMemo); }, simplifiers: { diff --git a/core/frb/bind.js b/core/frb/bind.js index 18f4bf7770..a4b4780c2c 100644 --- a/core/frb/bind.js +++ b/core/frb/bind.js @@ -23,22 +23,17 @@ function bind(target, targetPath, descriptor) { twoWay = descriptor.twoWay = TWO_WAY in descriptor, sourcePath = descriptor.sourcePath = !twoWay ? descriptor[ONE_WAY] : descriptor[TWO_WAY] || "", parameters = descriptor.parameters = descriptor.parameters || source, - document = descriptor.document, - components = descriptor.components, trace = descriptor.trace, // TODO: consider the possibility that source and target have intrinsic // scope properties - sourceScope = /*descriptor.sourceScope =*/ new Scope(source), - targetScope = /*descriptor.targetScope =*/ new Scope(target); + sourceScope = /*descriptor.sourceScope =*/ new Scope(source), + targetScope = /*descriptor.targetScope =*/ new Scope(target); - sourceScope.parameters = parameters; - sourceScope.document = document; - sourceScope.components = components; - targetScope.parameters = parameters; - targetScope.document = document; - targetScope.components = components; + sourceScope.parameters = targetScope.parameters = parameters; + sourceScope.document = targetScope.document = descriptor.document; + sourceScope.components = targetScope.components = descriptor.components; // promote convert and revert from a converter object up to the descriptor if (descriptor.converter) { diff --git a/core/frb/compile-assigner.js b/core/frb/compile-assigner.js index ab69afac9b..58a725efb0 100644 --- a/core/frb/compile-assigner.js +++ b/core/frb/compile-assigner.js @@ -53,7 +53,13 @@ compile.semantics = { return assignParent(value, scope.parent); }; } else if (compilers.hasOwnProperty(syntax.type)) { - var argEvaluators = syntax.args.map(this.compileEvaluator, this.compileEvaluator.semantics); + var argEvaluators = [], + semantics = this.compileEvaluator.semantics; + for(var i=0, args = syntax.args, countI = args.length;i Date: Thu, 9 Jul 2020 17:30:05 -0700 Subject: [PATCH 194/407] add serializationl/deserialization --- core/enum.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/core/enum.js b/core/enum.js index 32d64fe86b..5576a57095 100644 --- a/core/enum.js +++ b/core/enum.js @@ -117,6 +117,38 @@ exports.Enum = Montage.specialize( /** @lends Enum# */ { } }, + serializeSelf: { + value: function (serializer) { + var memberIterator = this._membersIntValue.keys(), + members = [] + aMember, aValue + values; + while ((aMember = memberIterator.next().value)) { + members.push(aMember); + aValue = this[aMember]; + if(typeof aValue !== "number") { + (values || (values = [])).push(aValue); + } + } + + serializer.setProperty("members", members); + if(values) { + serializer.setProperty("values", values); + } + } + }, + + deserializeSelf: { + value: function (deserializer) { + var members, values; + members = deserializer.getProperty("members"); + if (members !== void 0) { + values = deserializer.getProperty("values"); + this._addMembers(members, values, this._membersByValue, this._membersIntValue); + } + } + }, + /** * @function From be3635065a40f65939cb12fa513c96f797626c1c Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:31:30 -0700 Subject: [PATCH 195/407] - cleanup object->map internal implementation - performance improvement removing for in --- core/gate.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/core/gate.js b/core/gate.js index 9c32fbca53..5f8f3980a8 100644 --- a/core/gate.js +++ b/core/gate.js @@ -48,12 +48,10 @@ var Gate = exports.Gate = Montage.specialize(/** @lends Gate.prototype # */ { initWithDescriptor: { enumerable: false, value: function (propertyDescriptor) { - var fieldName; + var i, keys, fieldName; this.reset(); - for (fieldName in propertyDescriptor) { - if (propertyDescriptor.hasOwnProperty(fieldName)) { - this.setField(fieldName, propertyDescriptor[fieldName].value); - } + for (i=0, keys = Object.keys(propertyDescriptor); (fieldName = keys[i]); i++) { + this.setField(fieldName, propertyDescriptor[fieldName].value); } return this; } @@ -100,7 +98,7 @@ var Gate = exports.Gate = Montage.specialize(/** @lends Gate.prototype # */ { fieldValue, oldCount = this.count; - fieldValue = table[aFieldName]; + fieldValue = table.get(aFieldName); if (typeof fieldValue === "undefined" && !value) { // new field @@ -114,7 +112,7 @@ var Gate = exports.Gate = Montage.specialize(/** @lends Gate.prototype # */ { } else if (value && logger.isDebug) { logger.debug(this, aFieldName + " was not set before."); } - table[aFieldName] = !!value; + table.set(aFieldName,!!value); if (this.count === 0 && oldCount > 0) { this.callDelegateMethod(true); } else if (oldCount === 0 && this.count > 0) { @@ -130,12 +128,12 @@ var Gate = exports.Gate = Montage.specialize(/** @lends Gate.prototype # */ { removeField: { enumerable: false, value: function (aFieldName) { - var table = this.table, fieldValue = table[aFieldName]; + var table = this.table, fieldValue = table.get(aFieldName); if (typeof fieldValue !== "undefined" && !fieldValue) { // if the value was false decrement the count this.count--; } - delete table[aFieldName]; + table.delete(aFieldName); } }, @@ -179,7 +177,7 @@ var Gate = exports.Gate = Montage.specialize(/** @lends Gate.prototype # */ { reset: { enumerable: false, value: function () { - this.table = {}; + this.table = null; this.count = 0; } }, From 7414d7827808eada6e2e15b102a8f19965e7bd01 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:31:57 -0700 Subject: [PATCH 196/407] add locale object descriptor --- core/locale.mjson | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 core/locale.mjson diff --git a/core/locale.mjson b/core/locale.mjson new file mode 100644 index 0000000000..3034063266 --- /dev/null +++ b/core/locale.mjson @@ -0,0 +1,55 @@ +{ + "identifier_property_descriptor": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "identifier", + "objectDescriptor": { "@": "root" }, + "valueType": "string", + "helpKey": "" + } + }, + "language_property_descriptor": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "language", + "objectDescriptor": { "@": "root" }, + "valueType": "string", + "helpKey": "" + } + }, + "region_property_descriptor": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "region", + "objectDescriptor": { "@": "root" }, + "valueType": "string", + "helpKey": "" + } + }, + "locale": { + "object": "core/locale" + }, + "root": { + "prototype": "core/meta/module-object-descriptor", + "values": { + "name": "Locale", + "propertyDescriptors": [ + { "@": "identifier_property_descriptor" }, + { "@": "language_property_descriptor" }, + { "@": "region_property_descriptor" } + ], + "propertyDescriptorGroups": { + "all": [ + { "@": "identifier_property_descriptor" }, + { "@": "language_property_descriptor" }, + { "@": "region_property_descriptor" } + ] + }, + "propertyValidationRules": {}, + "objectDescriptorModule": { "%": "core/locale.mjson" }, + "exportName": "Locale", + "module": { "%": "core/locale" }, + "object":{ "@": "locale" } + } + } +} From c8b298b3713153a66c112a202f30db1de1371cdd Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:36:00 -0700 Subject: [PATCH 197/407] add ModuleReference object descriptor --- core/module-reference.mjson | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 core/module-reference.mjson diff --git a/core/module-reference.mjson b/core/module-reference.mjson new file mode 100644 index 0000000000..3bda5f332a --- /dev/null +++ b/core/module-reference.mjson @@ -0,0 +1,34 @@ +{ + + "id_descriptor": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "id", + "objectDescriptor": { "@": "root" }, + "valueType": "string", + "helpKey": "" + } + }, + "module-reference": { + "object": "core/module-reference" + }, + "root": { + "prototype": "core/meta/module-object-descriptor", + "values": { + "name": "ModuleReference", + "propertyDescriptors": [ + { "@": "id_descriptor" } + ], + "propertyDescriptorGroups": { + "all": [ + { "@": "id_descriptor" } + ] + }, + "propertyValidationRules": {}, + "objectDescriptorModule": { "%": "core/module-reference.mjson" }, + "exportName": "ModuleReference", + "module": { "%": "core/module-reference" }, + "object":{ "@": "module-reference" } + } + } +} From 12d9a659ae09b14d6a3554974e77f01e75ef5493 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:37:08 -0700 Subject: [PATCH 198/407] add length property --- core/range.js | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/core/range.js b/core/range.js index ca21b4b00b..fa6ae86c15 100644 --- a/core/range.js +++ b/core/range.js @@ -13,6 +13,53 @@ Montage = require("./core").Montage; exports.Range = Range; + + +// For doc/reference: + +// function Range(begin, end, bounds) { +// if (!(this instanceof Range)) return new Range(begin, end, bounds) + +// /** +// * Range's beginning, or left endpoint. +// * +// * @property {Object} begin +// */ +// this.begin = begin + +// /** +// * Range's end, or right endpoint. +// * +// * @property {Object} end +// */ +// this.end = end + +// /** +// * Range's bounds. +// * +// * Bounds signify whether the range includes or excludes that particular +// * endpoint. +// * +// * Pair | Meaning +// * -----|-------- +// * `()` | open +// * `[]` | closed +// * `[)` | left-closed, right-open +// * `(]` | left-open, right-closed +// * +// * @example +// * new Range(1, 5).bounds // => "[]" +// * new Range(1, 5, "[)").bounds // => "[)" +// * +// * @property {String} bounds +// */ +// this.bounds = bounds = bounds === undefined ? "[]" : bounds +// if (!isValidBounds(bounds)) throw new RangeError(INVALID_BOUNDS_ERR + bounds) +// } + + + + Range.prototype.equals = function(value, equals, memo) { if(value && this.compareBegin(value.begin) === 0 && this.compareEnd(value.end) === 0) { return true; @@ -94,3 +141,13 @@ Range.prototype.deserializeSelf = function (deserializer) { this.bounds = value; } }; + +Object.defineProperty(Range.prototype,"length", { + get: function (serializer) { + if(this.isFinite) { + return this.end - this.begin; + } else { + return Infinity; + } + } +}); From d90accfe76dff1d1c687139d5e19643fc340fc2c Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:39:50 -0700 Subject: [PATCH 199/407] refactor --- .../raw-foreign-value-to-object-converter.js | 170 +++++++++++++++++- 1 file changed, 161 insertions(+), 9 deletions(-) diff --git a/data/converter/raw-foreign-value-to-object-converter.js b/data/converter/raw-foreign-value-to-object-converter.js index 3bb699e050..55fffa8087 100644 --- a/data/converter/raw-foreign-value-to-object-converter.js +++ b/data/converter/raw-foreign-value-to-object-converter.js @@ -10,6 +10,56 @@ var RawValueToObjectConverter = require("./raw-value-to-object-converter").RawVa exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( /** @lends RawForeignValueToObjectConverter# */ { + /********************************************************************* + * Serialization + */ + + serializeSelf: { + value: function (serializer) { + + this.super(serializer); + + serializer.setProperty("foreignDescriptorMappings", this.foreignDescriptorMappings); + + } + }, + + deserializeSelf: { + value: function (deserializer) { + + this.super(deserializer); + + var value = deserializer.getProperty("foreignDescriptorMappings"); + if (value) { + this.foreignDescriptorMappings = value; + } + + } + }, + + /** + * foreignDescriptorMappings enables the converter to handle polymorphic relationships + * where the object that need to be found from a foreign key can be of different type, + * and potentially be stored in different raw-level storage, diffferent table in a database + * or different API endpoint. The arrau contains RawDataTypeMappings which have an expression, + * which if it evaluates as true on a value means this RawDataTypeMapping's object descriptor + * should be used. When present, the converter will evaluate the value passed, + * in polyporphic case a record like: + * { + * foreignKeyOfTypeA: null, + * foreignKeyOfTypeB: "foreign-key-value", + * foreignKeyOfTypeC: null + * } + * + * Only one of those properties can be not null at a time + * + * @type {?Array} + * */ + + foreignDescriptorMappings: { + value: undefined + }, + /********************************************************************* * Public API */ @@ -20,27 +70,129 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( * @param {Property} v The value to format. * @returns {Promise} A promise for the referenced object. The promise is * fulfilled after the object is successfully fetched. + * + */ + + _fetchConvertedDataForObjectDescriptorCriteria: { + value: function(typeToFetch, criteria) { + if (this.serviceIdentifier) { + criteria.parameters.serviceIdentifier = this.serviceIdentifier; + } + + var query = DataQuery.withTypeAndCriteria(typeToFetch, criteria); + + return this.service ? this.service.then(function (service) { + return service.rootService.fetchData(query); + }) : null; + + } + }, + + /** + * Uses foreignDescriptorMappings to find an ObjectDescriptor that matches + * the passed raw data, delegating to iindividual RawDataTypeMappings + * the job of assessing if their condition match the raw data or not. + * + * Such expression might consider a combination of raw data key/value, + * an type property, a mutually exclusive list of potential foreignKeys, + * and eventually one foreign primary key if it were to contain the type of + * the data it represents. + * + * @method + * @argument {Object} value The raw data to evaluate. + * @returns {ObjectDescriptor} An ObjectDescriptor if one is found or null. + * */ + + foreignDescriptorForValue: { + value: function(value) { + + for(var i=0, mappings = this.foreignDescriptorMappings, countI = mappings.length, iMapping;(i 0)) { var self = this, - criteria = new Criteria().initWithSyntax(self.convertSyntax, v), + criteria = this.convertCriteriaForValue(v), query; - return this._descriptorToFetch.then(function (typeToFetch) { + if(this.foreignDescriptorMappings) { + /* + Needs to loop on the mapping and evaluate value. If v is an array, it's possible + there could be foreignKeys in that array going to different ObjectDescriptor + and therefore requiring different queries, but there could be multiple of the same kind. So we need to loop on values and group by it before building a criteria for each. + */ + + if((v instanceof Array )) { + var i, countI, iValue, iValueDescriptor, groupMap = new Map(), iGroupValue; + for(i=0, countI = v.length;(i Date: Thu, 9 Jul 2020 17:40:48 -0700 Subject: [PATCH 200/407] - rename prefetchExpression readExpression - add fetchLimit --- data/model/data-query.js | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/data/model/data-query.js b/data/model/data-query.js index 11f73437a2..bf155bd701 100644 --- a/data/model/data-query.js +++ b/data/model/data-query.js @@ -24,9 +24,9 @@ exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { this.orderings = value; } - value = deserializer.getProperty("prefetchExpressions"); + value = deserializer.getProperty("readExpressions"); if (value !== void 0) { - this.prefetchExpressions = value; + this.readExpressions = value; } value = deserializer.getProperty("selectBindings"); @@ -55,6 +55,12 @@ exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { } } + value = deserializer.getProperty("fetchLimit"); + if (value !== void 0) { + this.fetchLimit = value; + } + + return result || Promise.resolve(this); } }, @@ -63,9 +69,10 @@ exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { value: function (serializer) { serializer.setProperty("criteria", this.criteria); serializer.setProperty("orderings", this.orderings); - serializer.setProperty("prefetchExpressions", this.prefetchExpressions); + serializer.setProperty("readExpressions", this.readExpressions); serializer.setProperty("selectBindings", this.selectBindings); serializer.setProperty("selectExpression", this.selectExpression); + serializer.setProperty("fetchLimit", this.fetchLimit); if (this.type.objectDescriptorInstanceModule) { serializer.setProperty("typeModule", this.type.objectDescriptorInstanceModule); @@ -234,7 +241,9 @@ exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { * @type {Array} */ - prefetchExpressions: { + //fetchExpressions + //readExpressions + readExpressions: { value: null }, @@ -255,10 +264,25 @@ exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { get: function() { return this._doesBatchResults || (typeof this.batchSize === "number"); } + }, + /** + * A property defining the maximum number of objets to retrieve. + * @type {Number} + * + * fetchLimit ? matches fetchData API, limit is SQL + * + * readLimit ? matches the operation? + * + * maximum ... + */ + + fetchLimit: { + value: null } + }, /** @lends DataQuery */ { /** From efa24b5402d8caddd2f2fea0ff18c115431fbb56 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:46:12 -0700 Subject: [PATCH 201/407] - remove de/serialization of "objectExpressions" property - add temporary de/serialization of "readLimit" --- data/service/data-operation.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 055623801f..cbe736132d 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -195,12 +195,18 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy serializer.setProperty("referrerId", this.referrerId); } serializer.setProperty("criteria", this._criteria); + /* + Hack: this is neededed for now to represent a query's fetchLimit + But it's really relevant only for a read operation.... + TODO: Needs to sort this out better... + + */ + if(this.readLimit) { + serializer.setProperty("readLimit", this.readLimit); + } if(this.data) { serializer.setProperty("data", this.data); } - if(this.objectExpressions) { - serializer.setProperty("objectExpressions", this.objectExpressions); - } if(this.snapshot) { serializer.setProperty("snapshot", this.snapshot); } @@ -244,11 +250,6 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy this.data = value; } - value = deserializer.getProperty("objectExpressions"); - if (value !== void 0) { - this.objectExpressions = value; - } - value = deserializer.getProperty("snapshot"); if (value !== void 0) { this.snapshot = value; From 2a942dd1d754aa7c96d01e8dd1660201cb8e184e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:47:26 -0700 Subject: [PATCH 202/407] rename mappingWithType to mappingForType --- data/service/data-service.js | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index a908172841..f99a9fa8c5 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -18,7 +18,8 @@ var Montage = require("core/core").Montage, defaultEventManager = require("core/event/event-manager").defaultEventManager, DataEvent = require("data/model/data-event").DataEvent, PropertyDescriptor = require("core/meta/property-descriptor").PropertyDescriptor, - DeleteRule = require("core/meta/property-descriptor").DeleteRule; + DeleteRule = require("core/meta/property-descriptor").DeleteRule, + deprecate = require("../../core/deprecate"); var AuthorizationPolicyType = new Montage(); @@ -305,6 +306,11 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { }, + /** + * Adds child Services to the receiving service. + * + * @param {Array.} childServices. childServices to add. + */ addChildServices: { value: function (childServices) { @@ -604,7 +610,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { __makePrototypeForType: { value: function (childService, objectDescriptor, constructor) { var prototype = Object.create(constructor.prototype), - mapping = childService.mappingWithType(objectDescriptor), + mapping = childService.mappingForType(objectDescriptor), requisitePropertyNames = mapping && mapping.requisitePropertyNames || new Set(), dataTriggers = DataTrigger.addTriggers(this, objectDescriptor, prototype, requisitePropertyNames), mainService = this.rootService; @@ -868,16 +874,25 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { * @param {ObjectDescriptor} type. * @returns {DataMapping|null} returns the specified mapping or null * if a mapping is not defined for the specified type. + * + * If an immediate mapping isn't found, we look up the parent chain */ - mappingWithType: { + mappingForType: { value: function (type) { - var mapping; - type = this.objectDescriptorForType(type); - mapping = this._mappingByType.has(type) && this._mappingByType.get(type); + var mapping, localType = this.objectDescriptorForType(type); + + while(localType && !(mapping = this._mappingByType.has(localType) && this._mappingByType.get(localType))) { + localType = localType.parent; + } return mapping || null; } }, + mappingWithType: { + value: deprecate.deprecateMethod(void 0, function (type) { + return this.mappingForType(type); + }, "mappingWithType", "mappingForType") + }, _mappingByType: { get: function () { @@ -1627,7 +1642,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { value: function (object, propertyName, propertyDescriptor) { var self = this, objectDescriptor = propertyDescriptor.owner, - mapping = objectDescriptor && this.mappingWithType(objectDescriptor), + mapping = objectDescriptor && this.mappingForType(objectDescriptor), data = {}, result; @@ -2598,7 +2613,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { mapping, propertyName; if (!serviceModuleID) { - mapping = this.mappingWithType(query.type); + mapping = this.mappingForType(query.type); propertyName = mapping && parameters && parameters.propertyName; serviceModuleID = propertyName && mapping.serviceIdentifierForProperty(propertyName); } From 5cafe9aa3bf377198a82a800eeb29d99554870e5 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:48:47 -0700 Subject: [PATCH 203/407] add on DataStream's data array an objectDescriptor property with a value of the stream's query objectDescriptor. --- data/service/data-stream.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/data/service/data-stream.js b/data/service/data-stream.js index af5a0a1715..1706fa5e1a 100644 --- a/data/service/data-stream.js +++ b/data/service/data-stream.js @@ -67,7 +67,12 @@ DataStream = exports.DataStream = DataProvider.specialize(/** @lends DataStream. //This will enable a better undertanding of what type of data is coming //for objects using UserInterfaceDescriptors like the CascadingList if(value && value.type) { - this.data.objectDescriptor = value.type; + Object.defineProperty(this.data,"objectDescriptor", { + value: value.type, + enumerable: false, + configurable: true + }) + // this.data.objectDescriptor = value.type; } } }, From a473e0e9029c00f064643fa1c90464ef20ab3b22 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:49:51 -0700 Subject: [PATCH 204/407] avoid creating a trigger's setter for a read only property --- data/service/data-trigger.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js index f46a4f8b89..27990a4b70 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -636,19 +636,22 @@ Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ { trigger.propertyDescriptor = descriptor; trigger._isGlobal = descriptor.isGlobal; if (descriptor.definition) { - Montage.defineProperty(prototype, descriptor.name, { + var propertyDescriptor = { get: function () { if (!this.getBinding(descriptor.name)) { this.defineBinding(descriptor.name, {"<-": descriptor.definition}); } return trigger._getValue(this); // return (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._getValue(this); - }, - set: function (value) { + } + }; + if(!descriptor.readonly) { + propertyDescriptor.set = function (value) { trigger._setValue(this, value); // (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._setValue(this, value); } - }); + } + Montage.defineProperty(prototype, descriptor.name, propertyDescriptor); } else { Montage.defineProperty(prototype, descriptor.name, { get: function () { From dd871a63b273351255ca388693839df209a5d692 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:56:11 -0700 Subject: [PATCH 205/407] add abilty to specify a rawDataTypeName and delegate to the service via a rawDataTypeNameForMapping() method for creating automated raw type names. --- data/service/expression-data-mapping.js | 181 +++++++++++++++++++++--- 1 file changed, 165 insertions(+), 16 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index f2968f1626..14c876a1cf 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -90,6 +90,11 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData this.addRequisitePropertyName.apply(this, value); } + value = deserializer.getProperty("rawDataTypeName"); + if (value) { + this.rawDataTypeName = value; + } + value = deserializer.getProperty("rawDataPrimaryKeys"); if (value) { this.rawDataPrimaryKeys = value; @@ -228,12 +233,90 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData parent: { get: function () { if (!this._parent && this.objectDescriptor && this.objectDescriptor.parent && this.service) { - this._parent = this.service.mappingWithType(this.objectDescriptor.parent); + this._parent = this.service.mappingForType(this.objectDescriptor.parent); } return this._parent; } }, + + /** + * The name of the raw type mapped to the mapping's objectDescriptor. + * Could be a different name for a REST API or + * the name of a table/storage in a database. + * If not set, returns the name of the ObjectDescriptor. + * + * In a database, this is needed for example to use + * a vertical inheritance strategy where multiple subclasses + * are stored in the same table, which also means in this case + * that a DataService will have to use RawDataTypeMapping to + * know how to instantiate RawData coming from a shared storage if + * a query's type is a superclass and results will contain instances + * of multiple types. + * + * If a mapping doesn't have a value set, it looks to it's service for an answer. + * + * @property {string} + * @default undefined + */ + _rawDataTypeName: { + value: undefined + }, + rawDataTypeName: { + get: function () { + return this._rawDataTypeName || (this._rawDataTypeName = this.service.rawDataTypeNameForMapping(this) || this.objectDescriptor.name); + }, + set: function (value) { + this._rawDataTypeName = value; + } + }, + + /** + * An expression that should be added to any criteria when reading/writing data + * for the ObjectDescriptor in the rawDataTypeName. This is necessary + * to add this expression to any fecth's criteria to get instances + * of a type that is stored in the same table as others, or fetching a super class + * and getting instances of any possible subclass. + * + * When creating data, every rawData needs to have this expression validate to + * true, so we should use it like a binding that can change the rawData + * to make the expression value. + * + * This might be worth turning into a RawDataTypeMapping for effieciency, + * but it's an implementation detail specic to a type's raw data mapping and + * therefore belongs there rather than in another construction + * at the DataService level. + * + * @property {string} + * @default undefined + */ + rawDataTypeExpression: { + value: undefined + }, + /** + * The id of the raw type mapped to the mapping's objectDescriptor. + * Could be statically generated by a tool, or dynamicallay fetched in the case + * of a database that internally maintain unique ids for every object + * created in a schema. + * + * If a mapping doesn't have a value set, it looks to it's service for an answer + * and caches it. + * + * @property {string} + * @default undefined + */ + _rawDataTypeId: { + value: undefined + }, + rawDataTypeId: { + get: function () { + return this._rawDataTypeId || (this._rawDataTypeId = this.service.rawDataTypeIdForMapping(this)); + }, + set: function (value) { + this._rawDataTypeId = value; + } + }, + /** * Array of expressions that combine to make the primary key for objects * of the type defined by this.objectDescriptor. Will use this.parent.rawDataPrimaryKeys @@ -353,14 +436,31 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData while ((propertyName = iterator.next().value)) { objectRule = objectMappingRules.get(propertyName); if(objectRule) { - rule = rawDataMappingRules.get(objectRule.sourcePath); + /* + Test for polymorphic Associations with the Exclusive Belongs To (AKA Exclusive Arc) strategy where each potential destination table + gets it's matching foreignKeyId + */ + + if(objectRule.sourcePathSyntax && objectRule.sourcePathSyntax.type === "record") { + var rawForeignKeys = Object.keys(objectRule.sourcePathSyntax.args), + j, countJ; + + for(j=0, countJ = rawForeignKeys.length;(j Date: Thu, 9 Jul 2020 17:57:14 -0700 Subject: [PATCH 206/407] uopdate to follow some API changes --- data/service/raw-data-service.js | 45 +++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index be5b82c724..69f37c6303 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -428,7 +428,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot addOneRawData: { value: function (stream, rawData, context) { var type = this._descriptorForParentAndRawData(stream.query.type, rawData), - prefetchExpressions = stream.query.prefetchExpressions, + readExpressions = stream.query.readExpressions, dataIdentifier = this.dataIdentifierForTypeRawData(type,rawData), object, //object = this.rootService.objectForDataIdentifier(dataIdentifier), @@ -453,7 +453,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot this.recordSnapshot(dataIdentifier, rawData); - result = this._mapRawDataToObject(rawData, object, context, prefetchExpressions); + result = this._mapRawDataToObject(rawData, object, context, readExpressions); if (this._isAsync(result)) { result = result.then(function () { @@ -614,7 +614,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot primaryKeyForTypeRawData: { value: function (type, rawData) { - var mapping = this.mappingWithType(type), + var mapping = this.mappingForType(type), rawDataPrimaryKeys = mapping ? mapping.rawDataPrimaryKeyExpressions : null, scope = new Scope(rawData), rawDataPrimaryKeysValues, @@ -963,7 +963,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot */ mappingForObjectDescriptor: { value: function (objectDescriptor) { - var mapping = objectDescriptor && this.mappingWithType(objectDescriptor); + var mapping = objectDescriptor && this.mappingForType(objectDescriptor); if (!mapping) { @@ -1098,7 +1098,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot * call that invoked this method. */ _mapRawDataToObject: { - value: function (record, object, context, prefetchExpressions) { + value: function (record, object, context, readExpressions) { var self = this, mapping = this.mappingForObject(object), snapshot, @@ -1115,10 +1115,10 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot this._objectsBeingMapped.add(object); - result = mapping.mapRawDataToObject(record, object, context, prefetchExpressions); + result = mapping.mapRawDataToObject(record, object, context, readExpressions); if (result) { result = result.then(function () { - result = self.mapRawDataToObject(record, object, context, prefetchExpressions); + result = self.mapRawDataToObject(record, object, context, readExpressions); if (!self._isAsync(result)) { self._objectsBeingMapped.delete(object); @@ -1143,7 +1143,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot throw error; }); } else { - result = this.mapRawDataToObject(record, object, context, prefetchExpressions); + result = this.mapRawDataToObject(record, object, context, readExpressions); if (!this._isAsync(result)) { self._objectsBeingMapped.delete(object); @@ -1167,7 +1167,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot this._objectsBeingMapped.add(object); - result = this.mapRawDataToObject(record, object, context, prefetchExpressions); + result = this.mapRawDataToObject(record, object, context, readExpressions); if (!this._isAsync(result)) { @@ -1504,6 +1504,33 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot */ offlineService: { value: undefined + }, + + /** + * Allows DataService to provide a rawDataTypeId for a Mapping's + * ObjectDescriptor + * + * @method + * @param {DataMapping} aMapping + */ + + rawDataTypeIdForMapping: { + value: function (aMapping) { + console.warn("rawDataTypeIdForMapping() needs to be overriden with a concrete implementation by subclasses of RawDataService") + } + }, + /** + * Allows DataService to provide a rawDataTypeId for a Mapping's + * ObjectDescriptor + * + * @method + * @param {DataMapping} aMapping + */ + + rawDataTypeNameForMapping: { + value: function (aMapping) { + console.warn("rawDataTypeNameForMapping() needs to be overriden with a concrete implementation by subclasses of RawDataService") + } } }); From 4059c1a2893739955006c49f984159531ee2795c Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:58:00 -0700 Subject: [PATCH 207/407] adapt to mappingWithType -> mappingForType --- test/spec/data/expression-data-mapping.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/data/expression-data-mapping.js b/test/spec/data/expression-data-mapping.js index ac220b685d..b3e28d9a68 100644 --- a/test/spec/data/expression-data-mapping.js +++ b/test/spec/data/expression-data-mapping.js @@ -235,7 +235,7 @@ describe("An Expression Data Mapping", function() { it("properly registers the object descriptor type to the mapping object in a service", function (done) { return registrationPromise.then(function () { expect(movieService.parentService).toBe(mainService); - expect(movieService.mappingWithType(movieObjectDescriptor)).toBe(movieMapping); + expect(movieService.mappingForType(movieObjectDescriptor)).toBe(movieMapping); done(); }); }); From 779db766defea1e5091ef1ec894eeffe34f642ae Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:58:33 -0700 Subject: [PATCH 208/407] text formatting cleanup --- test/run-node.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/run-node.js b/test/run-node.js index 91bec49208..f2fa95d06a 100644 --- a/test/run-node.js +++ b/test/run-node.js @@ -7,7 +7,7 @@ global.XMLHttpRequest = require('xhr2'); // Init var jasmine = jasmineRequire.core(jasmineRequire); var jasmineEnv = jasmine.getEnv(); - + // Export interface var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv); global.jasmine = jasmine; @@ -16,17 +16,17 @@ for (var property in jasmineInterface) { if (jasmineInterface.hasOwnProperty(property)) { global[property] = jasmineInterface[property]; } -} +} // Default reporter jasmineEnv.addReporter(jasmineInterface.jsApiReporter); // Html reporter var consoleReporter = new JasmineConsoleReporter({ - colors: 1, - cleanStack: 1, - verbosity: 4, - listStyle: 'indent', + colors: 1, + cleanStack: 1, + verbosity: 4, + listStyle: 'indent', activity: false }); jasmineEnv.addReporter(consoleReporter); From f5c1d4fc5cd90d580802f1abdb7cca9aabe20610 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 9 Jul 2020 17:58:50 -0700 Subject: [PATCH 209/407] Fix frb require --- test/spec/core/localizer/serialization-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/core/localizer/serialization-spec.js b/test/spec/core/localizer/serialization-spec.js index 7e387ac474..ab18f3190c 100644 --- a/test/spec/core/localizer/serialization-spec.js +++ b/test/spec/core/localizer/serialization-spec.js @@ -37,7 +37,7 @@ var Montage = require("montage").Montage, TestPageLoader = require("montage-testing/testpageloader").TestPageLoader, Map = require("montage/core/collections/map"), Bindings = require("montage/core/core").Bindings, - FrbBindings = require("montage/frb/bindings"); + FrbBindings = require("montage/core/frb/bindings"); var stripPP = function stripPrettyPrintting(str) { return str.replace(/\n\s*/g, ""); From e0ad035ccbecfc04d7dd9113a52e6dd30ecd04ab Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 16 Jul 2020 00:22:22 -0700 Subject: [PATCH 210/407] remove q-bluebird dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index e217853bc1..514cbe9a38 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "production": true, "dependencies": { "bluebird": "~3.5.5", - "q-bluebird": "0.0.1", "htmlparser2": "~3.0.5", "weak-map": "^1.0.5", "lodash.kebabcase": "^4.1.1", From f8480ed7eab522a3427654821f9c41cec58567df Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 16 Jul 2020 00:47:06 -0700 Subject: [PATCH 211/407] cache regex --- node.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/node.js b/node.js index ee40e871ba..a7bf0c3ad0 100644 --- a/node.js +++ b/node.js @@ -198,6 +198,10 @@ function parseHtmlDependencies(text/*, location*/) { return dependencies; } +var html_regex = /(.*\/)?(?=[^\/]+\.html$)/, + json_regex = /(?=[^\/]+\.json$)/, + mjson_regex = /(?=[^\/]+\.(?:mjson|meta)$)/, + reel_regex = /(.*\/)?([^\/]+)\.reel\/\2$/; MontageBoot.TemplateLoader = function (config, load) { return function (moduleId, module) { @@ -214,10 +218,10 @@ MontageBoot.TemplateLoader = function (config, load) { id = moduleId; } - var html = id.match(/(.*\/)?(?=[^\/]+\.html$)/); - var serialization = id.match(/(?=[^\/]+\.json$)/); // XXX this is not necessarily a strong indicator of a serialization alone - var meta = id.match(/(?=[^\/]+\.(?:mjson|meta)$)/); - var reelModule = id.match(/(.*\/)?([^\/]+)\.reel\/\2$/); + var html = id.match(html_regex); + var serialization = id.match(json_regex); // XXX this is not necessarily a strong indicator of a serialization alone + var meta = id.match(mjson_regex); + var reelModule = id.match(reel_regex); if (html) { return load(id, module) .then(function () { From 472a0739c97f35ee717829e851266a2b9ba80efb Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 16 Jul 2020 00:50:06 -0700 Subject: [PATCH 212/407] - fix a bug - disable regex to narrow ES6 export detection as it doesn't work in Safari - uses String.prototype.endsWith vs endsWith() and improve polyfill --- core/mr/browser.js | 54 ++++++++++++++++++++++++++++++++------ core/mr/node.js | 2 ++ core/mr/require.js | 64 ++++++++++++++++++++++++++++++---------------- 3 files changed, 90 insertions(+), 30 deletions(-) diff --git a/core/mr/browser.js b/core/mr/browser.js index 58f78d8f16..d6a28d5284 100644 --- a/core/mr/browser.js +++ b/core/mr/browser.js @@ -11,12 +11,41 @@ // This method has been added to the ECMAScript 6 specification and may not be available in all JavaScript implementations yet. However, you can polyfill String.prototype.endsWith() with the following snippet: if (!String.prototype.endsWith) { - String.prototype.endsWith = function(search, this_len) { - if (this_len === undefined || this_len > this.length) { - this_len = this.length; - } - return this.substring(this_len - search.length, this_len) === search; - }; + // String.prototype.endsWith = function endsWith(search, this_len) { + // if (this_len === undefined || this_len > this.length) { + // this_len = this.length; + // } + // return this.substring(this_len - search.length, this_len) === search; + // }; + + String.prototype.endsWith = function endsWith(search, position) { + var stringLength = this.length; + var searchString = String(search); + var searchLength = searchString.length; + var pos = stringLength; + + if (position !== undefined) { + // `ToInteger` + pos = position ? Number(position) : 0; + if (pos !== pos) { // better `isNaN` + pos = 0; + } + } + + var end = Math.min(Math.max(pos, 0), stringLength); + var start = end - searchLength; + if (start < 0) { + return false; + } + var index = -1; + while (++index < searchLength) { + if (this.charCodeAt(start + index) !== searchString.charCodeAt(index)) { + return false; + } + } + return true; + } + } bootstrap("require/browser", function (require) { @@ -78,8 +107,17 @@ bootstrap("require/browser", function (require) { module.type = JAVASCRIPT; module.location = xhr.url; - var capturedImports = ES6_IMPORT_REGEX.exec(xhr.responseText); - if((xhr.responseText.indexOf("export ") !== -1) && (xhr.responseText.match(Require.detect_ES6_export_regex))) { + //var capturedImports = ES6_IMPORT_REGEX.exec(xhr.responseText); + if((xhr.responseText.indexOf("export ") !== -1) + + //Require.detect_ES6_export_regex doesn't worm in WebKit... + //Need to find a new one + /* + && (xhr.responseText.match(Require.detect_ES6_export_regex)) + */ + + + ) { // var displayName = (`${DoubleUnderscore}${module.require.config.name}${Underscore}${module.id}`.replace(nameRegex, Underscore)), // src = `export default ${globalEvalConstantA}${displayName}${globalEvalConstantB}${xhr.responseText}${globalEvalConstantC}${module.location}`; diff --git a/core/mr/node.js b/core/mr/node.js index 8a554b5aa7..f9da0047fb 100644 --- a/core/mr/node.js +++ b/core/mr/node.js @@ -119,6 +119,8 @@ Require.Compiler = function Compiler(config) { }; }; +//Temporary: only doing this in node as this regex doesn't work in WebKit +Require.detect_ES6_export_regex = /(?<=^([^"]|"[^"]*")*)export /; Require.Loader = function Loader(config, load) { return function (location, module) { return config.read(location, module) diff --git a/core/mr/require.js b/core/mr/require.js index 98f872937b..8e4221cc01 100644 --- a/core/mr/require.js +++ b/core/mr/require.js @@ -172,13 +172,13 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { var searchLength = searchString.length; var pos = stringLength; - if (position !== undefined) { - // `ToInteger` - pos = position ? Number(position) : 0; - if (pos !== pos) { // better `isNaN` - pos = 0; - } + if (position !== undefined) { + // `ToInteger` + pos = position ? Number(position) : 0; + if (pos !== pos) { // better `isNaN` + pos = 0; } + } var end = Math.min(Math.max(pos, 0), stringLength); var start = end - searchLength; @@ -295,7 +295,13 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { if (config.registry && config.registry.has(dependency.name)) { dependency.location = config.registry.get(dependency.name); } else if (config.packageLock) { - configPath = config.location.slice(config.mainPackageLocation.length - 1); + //There's a bug in node where config.location starts with file:// + //and config.mainPackageLocation doesn't. + //So looking for lastIndexOf() then adding length to workaround, + //But needs to figure out + //why config.mainPackageLocation in node doesn't start by file:// ?? + //BUG: configPath = config.location.slice(config.mainPackageLocation.length - 1); + configPath = config.location.slice(config.location.lastIndexOf(config.mainPackageLocation)+config.mainPackageLocation.length - 1); dependencyPath = findLongestDependencyPath(dependency.name, configPath, config.packageLock); if (dependencyPath) { dependency.location = URL.resolve(config.mainPackageLocation, dependencyPath); @@ -515,7 +521,7 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { var isLowercasePattern = /^[a-z]+$/; - Require.detect_ES6_export_regex = /(?<=^([^"]|"[^"]*")*)export /; + //Require.detect_ES6_export_regex = /(?<=^([^"]|"[^"]*")*)export /; Require.makeRequire = function (config) { var require, requireForId; @@ -1418,7 +1424,7 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { return; } - if (endsWith(location, dotHTML) || endsWith(location, dotHTMLLoadJs)) { + if (location.endsWith(dotHTML) || location.endsWith(dotHTMLLoadJs)) { var match = location.match(directoryExpression); if (match) { @@ -1552,16 +1558,7 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { } var mappings = config.mappings; - var prefixes = Object.keys(mappings); - var length = prefixes.length; - function loadMapping(mappingRequire) { - var rest = id.slice(prefix.length + 1); - config.mappings[prefix].mappingRequire = mappingRequire; - module.mappingRedirect = rest; - module.mappingRequire = mappingRequire; - return mappingRequire.deepLoad(rest, config.location); - } // TODO: remove this when all code has been migrated off of the autonomous name-space problem if ( @@ -1571,7 +1568,11 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { ) { console.warn("Package reflexive module ignored:", id); } - var i, prefix; + + /* + var prefixes = Object.keys(mappings), + length = prefixes.length, + i, prefix; for (i = 0; i < length; i++) { prefix = prefixes[i]; if ( @@ -1583,6 +1584,22 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { return config.loadPackage(mappings[prefix], config).then(loadMapping); } } + */ + + var aPackage, prefix; + //It's more likely to require a package+path than the package name itself. + if((aPackage = mappings[(prefix = id.substring(0,id.indexOf("/")))]) || (aPackage = mappings[(prefix = id)])) { + return config.loadPackage(aPackage, config) + .then(function loadMapping(mappingRequire) { + var rest = id.slice(prefix.length + 1); + config.mappings[prefix].mappingRequire = mappingRequire; + module.mappingRedirect = rest; + module.mappingRequire = mappingRequire; + return mappingRequire.deepLoad(rest, config.location); + } + ) + } + return load(id, module); }; }; @@ -1624,11 +1641,14 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { * @param config * @param loader the next loader in the chain */ - var reelExpression = /([^\/]+)\.reel$/, - dotREEL = ".reel"; + var _reelExpression = /([^\/]+)\.reel$/, + _dotREEL = ".reel"; Require.ReelLoader = function(config, load) { + var reelExpression = _reelExpression, + dotREEL = _dotREEL; + return function reelLoader(id, module) { - if (endsWith(id, dotREEL)) { + if (id.endsWith(dotREEL)) { module.redirect = id; module.redirect += SLASH; module.redirect += reelExpression.exec(id)[1]; From 4be0a2998631b55f7699d9c13d03254093da49a9 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 16 Jul 2020 00:52:15 -0700 Subject: [PATCH 213/407] remove "export" from comments --- core/extras/date.js | 46 ++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/core/extras/date.js b/core/extras/date.js index 8e66d15c55..5e3ba4aba7 100644 --- a/core/extras/date.js +++ b/core/extras/date.js @@ -308,36 +308,36 @@ Object.defineProperty(Date.prototype, "isToday", { */ /* -export- const SECONDS_A_MINUTE = 60 -export- const SECONDS_A_HOUR = SECONDS_A_MINUTE * 60 -export- const SECONDS_A_DAY = SECONDS_A_HOUR * 24 -export- const SECONDS_A_WEEK = SECONDS_A_DAY * 7 +exports- const SECONDS_A_MINUTE = 60 +exports- const SECONDS_A_HOUR = SECONDS_A_MINUTE * 60 +exports- const SECONDS_A_DAY = SECONDS_A_HOUR * 24 +exports- const SECONDS_A_WEEK = SECONDS_A_DAY * 7 -export- const MILLISECONDS_A_SECOND = 1e3 -export- const MILLISECONDS_A_MINUTE = SECONDS_A_MINUTE * MILLISECONDS_A_SECOND -export- const MILLISECONDS_A_HOUR = SECONDS_A_HOUR * MILLISECONDS_A_SECOND -export- const MILLISECONDS_A_DAY = SECONDS_A_DAY * MILLISECONDS_A_SECOND -export- const MILLISECONDS_A_WEEK = SECONDS_A_WEEK * MILLISECONDS_A_SECOND +exports- const MILLISECONDS_A_SECOND = 1e3 +exports- const MILLISECONDS_A_MINUTE = SECONDS_A_MINUTE * MILLISECONDS_A_SECOND +exports- const MILLISECONDS_A_HOUR = SECONDS_A_HOUR * MILLISECONDS_A_SECOND +exports- const MILLISECONDS_A_DAY = SECONDS_A_DAY * MILLISECONDS_A_SECOND +exports- const MILLISECONDS_A_WEEK = SECONDS_A_WEEK * MILLISECONDS_A_SECOND // English locales -export- const MS = 'millisecond' -export- const S = 'second' -export- const MIN = 'minute' -export- const H = 'hour' -export- const D = 'day' -export- const W = 'week' -export- const M = 'month' -export- const Q = 'quarter' -export- const Y = 'year' -export- const DATE = 'date' +exports- const MS = 'millisecond' +exports- const S = 'second' +exports- const MIN = 'minute' +exports- const H = 'hour' +exports- const D = 'day' +exports- const W = 'week' +exports- const M = 'month' +exports- const Q = 'quarter' +exports- const Y = 'year' +exports- const DATE = 'date' -export- const FORMAT_DEFAULT = 'YYYY-MM-DDTHH:mm:ssZ' +exports- const FORMAT_DEFAULT = 'YYYY-MM-DDTHH:mm:ssZ' -export- const INVALID_DATE_STRING = 'Invalid Date' +exports- const INVALID_DATE_STRING = 'Invalid Date' // regex -export- const REGEX_PARSE = /^(\d{4})-?(\d{1,2})-?(\d{0,2})[^0-9]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?.?(\d{1,3})?$/ -export- const REGEX_FORMAT = /\[([^\]]+)]|Y{2,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g +exports- const REGEX_PARSE = /^(\d{4})-?(\d{1,2})-?(\d{0,2})[^0-9]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?.?(\d{1,3})?$/ +exports- const REGEX_FORMAT = /\[([^\]]+)]|Y{2,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g */ /* From 9e385c482a26052f14c679777de29503c9ce5a52 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 16 Jul 2020 00:54:10 -0700 Subject: [PATCH 214/407] - fix dependencies - add ability for object being deserialized to know bindings are being defined - replace a for-in by Object.keys() loop --- core/serialization/bindings.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/core/serialization/bindings.js b/core/serialization/bindings.js index 67236ed8ba..ba02d7e665 100644 --- a/core/serialization/bindings.js +++ b/core/serialization/bindings.js @@ -1,9 +1,9 @@ -var Bindings = require("core/frb/bindings"), - stringify = require("core/frb/stringify"), - assign = require("core/frb/assign"), - evaluate = require("core/frb/evaluate"), - expand = require("core/frb/expand"), - Scope = require("core/frb/scope"), +var Bindings = require("../frb/bindings"), + stringify = require("../frb/stringify"), + assign = require("../frb/assign"), + evaluate = require("../frb/evaluate"), + expand = require("../frb/expand"), + Scope = require("../frb/scope"), Serializer = require("../serialization/serializer/montage-serializer").MontageSerializer, ONE_ASSIGNMENT = "=", ONE_WAY = "<-", @@ -77,10 +77,12 @@ var deserializeObjectBindings = exports.deserializeObjectBindings = function (de components: deserializer }, targetPath, - descriptor; + descriptor, + i, keys; /* jshint forin: true */ - for (targetPath in bindings) { + //for (targetPath in bindings) { + for (i=0, keys = Object.keys(bindings); (targetPath = keys[i]); i++) { /* jshint forin: false */ descriptor = bindings[targetPath]; @@ -108,7 +110,10 @@ var deserializeObjectBindings = exports.deserializeObjectBindings = function (de deserializer ); } else { - Bindings.defineBinding(object, targetPath, descriptor, commonDescriptor); + //TODO: use the API on object firt so it has an opportunity to know what's being bound to him. + object.defineBinding + ? object.defineBinding(targetPath, descriptor, commonDescriptor) + : Bindings.defineBinding(object, targetPath, descriptor, commonDescriptor); } } }; From 2b3e3e417f86814df685e38a7a0e7198e7e4bc68 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 16 Jul 2020 00:55:34 -0700 Subject: [PATCH 215/407] - fix bug in Locale - adjust deendencies between the 2 --- core/date/calendar.js | 4 ++-- core/locale.js | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/core/date/calendar.js b/core/date/calendar.js index 572f69fbac..b8419faeb3 100644 --- a/core/date/calendar.js +++ b/core/date/calendar.js @@ -1,6 +1,6 @@ var Montage = require("../core").Montage, Enum = require("../enum").Enum, - Locale = require("../locale"), + //Locale = require("../locale"), TimeZone = require("./time-zone"), CalendarDate = require("./calendar-date"), Range = require("../range").Range, @@ -222,4 +222,4 @@ var Calendar = exports.Calendar = Montage.specialize({ }); //To avoid a cycle. -Locale.Calendar = Calendar; +// Locale.Calendar = Calendar; diff --git a/core/locale.js b/core/locale.js index 3c0a09261d..989b65ba3f 100644 --- a/core/locale.js +++ b/core/locale.js @@ -1,4 +1,5 @@ var Montage = require("./core").Montage, +Calendar = require("./date/calendar").Calendar, currentEnvironment = require("./environment").currentEnvironment; /* @@ -280,7 +281,7 @@ var Locale = exports.Locale = Montage.specialize({ if(!this._systemLocale) { var systemLocaleIdentifier = currentEnvironment.systemLocaleIdentifier, //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/resolvedOptions - resolvedOptions = Intl.DateTimeFormat(navigatorLocaleIdentifier).resolvedOptions(), + resolvedOptions = Intl.DateTimeFormat(systemLocaleIdentifier).resolvedOptions(), calendar = resolvedOptions.calendar, /*"gregory"*/ day = resolvedOptions.day, /*"numeric"*/ month = resolvedOptions.month, /*"numeric"*/ @@ -326,3 +327,5 @@ var Locale = exports.Locale = Montage.specialize({ } } }); + +Locale.Calendar = Calendar; From 0ebf0896cd03cab3a668ac36cb580a0efa03a380 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 16 Jul 2020 00:56:20 -0700 Subject: [PATCH 216/407] - add updated cached localizablePropertyDescriptors --- core/meta/object-descriptor.js | 50 ++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/core/meta/object-descriptor.js b/core/meta/object-descriptor.js index 725336f884..806d58b4b2 100644 --- a/core/meta/object-descriptor.js +++ b/core/meta/object-descriptor.js @@ -488,6 +488,13 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends this._propertyDescriptors.splice(index, 1); this._propertyDescriptorsTable.delete(descriptor.name); descriptor._owner = null; + + if(descriptor.isLocalizable) { + descriptor.removeOwnPropertyChangeListener("isLocalizable", this); + + index = this.localizablePropertyDescriptors.indexOf(descriptor); + this.localizablePropertyDescriptors.splice(index, 1); + } } } @@ -501,11 +508,30 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends this._propertyDescriptorsTable.set(descriptor.name, descriptor); } descriptor._owner = descriptor._owner || this; + + if(descriptor.isLocalizable) { + this.localizablePropertyDescriptors.push(descriptor); + } } } } }, + /** + * Property Range change listener on this._ownPropertyDescriptors and + * this.parent.propertyDescriptors + */ + + handleIsLocalizablePropertyChange: { + value: function(changeValue, key, object) { + if(object.isLocalizable) { + this.localizablePropertyDescriptors.push(object); + } else { + this.localizablePropertyDescriptors.splice(this.localizablePropertyDescriptors.indexOf(object), 1); + } + } + }, + _preparePropertyDescriptorsCache: { value: function () { var ownDescriptors = this._ownPropertyDescriptors, @@ -527,6 +553,14 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends descriptor._owner = this; this._propertyDescriptors.push(descriptor); this._propertyDescriptorsTable.set(descriptor.name, descriptor); + + if(descriptor.isLocalizable) { + + this.localizablePropertyDescriptors.push(descriptor); + descriptor.addOwnPropertyChangeListener("isLocalizable", this); + + } + } this.addRangeAtPathChangeListener("_ownPropertyDescriptors", this, "_handlePropertyDescriptorsRangeChange"); this.addRangeAtPathChangeListener("parent.propertyDescriptors", this, "_handlePropertyDescriptorsRangeChange"); @@ -535,6 +569,7 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends } }, + /** * PropertyDescriptors for this object descriptor, not including those * provided by this.parent @@ -553,6 +588,21 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends value: false }, + /** + * A non-observed cache of an object descriptor's Property Descriptors + * + * @private + * @property {Array} + */ + _localizablePropertyDescriptors: { + value: undefined + }, + localizablePropertyDescriptors: { + get: function() { + return this._localizablePropertyDescriptors || (this._localizablePropertyDescriptors = []); + } + }, + /** * PropertyDescriptors associated to this object descriptor, including those * provided by this.parent. From 1152ed321574678d1469e934a2fea12e3ce4a36d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 16 Jul 2020 00:57:10 -0700 Subject: [PATCH 217/407] add userLocale & userLocaleCriteria properties --- data/service/data-service.js | 60 ++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index f99a9fa8c5..73d98442bc 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -5,6 +5,7 @@ var Montage = require("core/core").Montage, UserAuthenticationPolicy = require("data/service/user-authentication-policy").UserAuthenticationPolicy, UserIdentityManager = require("data/service/user-identity-manager").UserIdentityManager, DataObjectDescriptor = require("data/model/data-object-descriptor").DataObjectDescriptor, + Criteria = require("core/criteria").Criteria, DataQuery = require("data/model/data-query").DataQuery, DataStream = require("data/service/data-stream").DataStream, DataTrigger = require("data/service/data-trigger").DataTrigger, @@ -19,8 +20,8 @@ var Montage = require("core/core").Montage, DataEvent = require("data/model/data-event").DataEvent, PropertyDescriptor = require("core/meta/property-descriptor").PropertyDescriptor, DeleteRule = require("core/meta/property-descriptor").DeleteRule, - deprecate = require("../../core/deprecate"); - + deprecate = require("../../core/deprecate"), + Locale = require("core/locale").Locale; var AuthorizationPolicyType = new Montage(); AuthorizationPolicyType.NoAuthorizationPolicy = AuthorizationPolicy.NONE; @@ -81,6 +82,9 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { UserIdentityManager.registerUserIdentityService(this); } + this.addOwnPropertyChangeListener("userLocale", this); + + this._initializeOffline(); } }, @@ -3431,6 +3435,58 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { return insert ? array.splice.apply(array, [index, length].concat(insert)) : array.splice(index, length); } + }, + + /** + * Returns by default an array the Locale.systemLocale + * Subclasses have the opporyunity to oveorrides to get useLocale + * from more specific data objects (DO) + * + * @type {Locale} + */ + + _userLocale: { + value: undefined + }, + + userLocale: { + get: function() { + return this._userLocale || ((this._userLocale = Locale.systemLocale) && this._userLocale); + }, + set: function(value) { + if(value !== this._userLocale) { + this._userLocale = value; + } + } + }, + + _userLocaleCriteria: { + value: undefined + }, + + userLocaleCriteria: { + get: function() { + return this._userLocaleCriteria || (this._userLocaleCriteria = this._createUserLocaleCriteria()); + }, + set: function(value) { + if(value !== this._userLocaleCriteria) { + this._userLocaleCriteria = value; + } + } + }, + + _createUserLocaleCriteria: { + value: function() { + return new Criteria().initWithExpression("locale == $locale", { + locale: this.userLocale + }); + } + }, + + handleUserLocaleChange: { + value: function (value, key, object) { + this.userLocaleCriteria = this._createUserLocaleCriteria(); + } } From 2718d36751ba2bbf89de2e45e332d5ef55b3728b Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 16 Jul 2020 00:58:55 -0700 Subject: [PATCH 218/407] starting point of data-aware component super classes --- ui/data-collection-editor.js | 32 ++++ ui/data-editor.js | 288 +++++++++++++++++++++++++++++++++++ ui/data-object-editor.js | 32 ++++ 3 files changed, 352 insertions(+) create mode 100644 ui/data-collection-editor.js create mode 100644 ui/data-editor.js create mode 100644 ui/data-object-editor.js diff --git a/ui/data-collection-editor.js b/ui/data-collection-editor.js new file mode 100644 index 0000000000..cb2400ada1 --- /dev/null +++ b/ui/data-collection-editor.js @@ -0,0 +1,32 @@ +var DataEditor = require("./data-editor").DataEditor; + +/** + * Instantiated with an ObjectDescriptor, this reusable component simplifies the display, + * creation, edition and deletion of objects described by the object descriptor + * and further described by a criteria. It's meant to be used as a source of data coming + * from a data service, using query and a dataStream, as well as in a master-details wy + * where a DataEditor could be assign the job of editing the relationship of an object + * that's already in memory. However, even though that relationship is typically an array + * if it has to many values, it shouldn't be fetched all in memory and we'll need a way to + * move a cursor through that relationship as it is scrolled for example. + * + * It uses and cordinates the different roles of existing montage objects + * like the rangeController, query, data stream etc... + * + * - track object changes to convey it to user, preferably using new event system: + * - if target is an object, it bubbles to its objectDescriptor, the mainService and then + * the application. + * - commnunicate data operations coming from the bottom of the stack such as: + * - validation errors when saving + * - updates coming from server if someone else made an update + * - re-authorozing if editing a sensitive aspect of t he object + * + * - + * + * @class DataCollectionEditor + * @extends DataEditor + */ +exports.DataCollectionEditor = DataEditor.specialize(/** @lends DataEditor# */ { + + +}); diff --git a/ui/data-editor.js b/ui/data-editor.js new file mode 100644 index 0000000000..8a94a514b9 --- /dev/null +++ b/ui/data-editor.js @@ -0,0 +1,288 @@ +var Component = require("./component").Component, +Criteria = require("core/criteria").Criteria, +DataQuery = require("data/model/data-query").DataQuery, +DataStream = require("data/service/data-stream").DataStream, +ObjectDescriptor = require("core/meta/object-descriptor").ObjectDescriptor, +DataOrdering = require("data/model/data-ordering").DataOrdering; + + +/** + * Instantiated with an ObjectDescriptor, this reusable component simplifies the display, + * creation, edition and deletion of objects described by the object descriptor + * and further described by a criteria. It's meant to be used as a source of data coming + * from a data service, using query and a dataStream, as well as in a master-details wy + * where a DataEditor could be assign the job of editing the relationship of an object + * that's already in memory. However, even though that relationship is typically an array + * if it has to many values, it shouldn't be fetched all in memory and we'll need a way to + * move a cursor through that relationship as it is scrolled for example. + * + * It uses and cordinates the different roles of existing montage objects + * like the rangeController, query, data stream etc... + * + * - track object changes to convey it to user, preferably using new event system: + * - if target is an object, it bubbles to its objectDescriptor, the mainService and then + * the application. + * - commnunicate data operations coming from the bottom of the stack such as: + * - validation errors when saving + * - updates coming from server if someone else made an update + * - re-authorozing if editing a sensitive aspect of t he object + * + * - + * + * @class DataEditor + * @extends Component + */ + +/* + + Modeling question: do we need a DataSreamController to do for a DataStream + what the RangeController does for an array? + Should there be a super class named CollectionController? Or DataController + specialized in CollectionController and ObjectController? + +*/ + + + +exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { + /** + * A DataService used to fetch data. By default uses application.mainService + * But when we support nested editing context / data services, could be a + * different one. + * + * @type {CollectionController} + */ + _dataService: { + value: undefined + }, + + dataService: { + get: function() { + return this._dataService || this.application.mainService; + }, + set: function(value) { + if(value !== t_dataService) { + this._dataService = value; + } + } + }, + + __dataQuery: { + value: undefined + }, + + _dataQuery: { + get: function() { + if(!this.__dataQuery) { + if(this.type) { + this.__dataQuery = DataQuery.withTypeAndCriteria(this.type,this.criteria); + this.__dataQuery.orderings = this.orderings; + } + } + return this.__dataQuery; + } + }, + + + /** + * A timeout used to make sure that for any succesive + * + */ + + /* + _fetchDataTimeout: { + value: false + }, + + __needsFetchData: { + value: false + }, + + _needsFetchData: { + get: function() { + return this.__needsFetchData; + }, + set: function(value) { + if(value !== this.__needsFetchData) { + + if(value && !this.__needsFetchData) { + //Schedule fetchData + if(!this._fetchData) { + this._fetchData = this.fetchData.bind(this); + } + this._fetchDataTimeout = setTimeout(this._fetchData,0); + } + else { + //cancel Scheduled fetchData + + } + this.__needsFetchData = value; + } + } + }, + */ + + /** + * A DataEditor calls fetchData when what makes it's query changes: + * - type, criteria or data ordering. + * + */ + + fetchData: { + value: function() { + var dataService = this.dataService, + currentDataStream = this.dataStream, + dataStream, + self = this; + + + dataStream = dataService.fetchData(this._dataQuery); + dataStream.then(function(data) { + //console.log("Data fetched:",data); + self.dataStream = dataStream; + + //We need to + dataService.cancelDataStream(currentDataStream); + + }, + function(error) { + console.log("fetchData failed:",error); + + }); + } + }, + + fetchDataIfNeeded: { + value: function() { + + //Blow the cache: + this.__dataQuery = null; + + //If we're active for trhe user, we re-fetch + if(this.inDocument) { + this.fetchData(); + } + } + }, + + // deserializedFromSerialization: { + // value: function (label) { + // this.super(label); + // console.log("deserializedFromSerialization("+label+")"); + // //this.fetchData(); + // } + // }, + + // deserializedFromTemplate: { + // value: function () { + // // this.fetchData(); + // console.log("deserializedFromTemplate"); + // } + // }, + + templateDidLoad: { + value: function () { + this.fetchData(); + } + }, + + /** + * The type of data object edited. + * + * @type {ObjectDescriptor} + */ + _type: { + value: undefined + }, + type: { + get: function() { + return this._type; + }, + set: function(value) { + //console.log("set type ",value); + if(!this._type || (this._type && this._type !== value)) { + this._type = value; + this.fetchDataIfNeeded(); + } + } + }, + /** + * A Criteria narowing the instances of type that are displayed/edited. + * + * @type {CollectionController} + */ + _criteria: { + value: undefined + }, + criteria: { + get: function() { + return this._criteria; + }, + set: function(value) { + if(value !== this._criteria) { + this._criteria = value; + this.fetchDataIfNeeded(); + } + } + }, + _orderings: { + value: undefined + }, + orderings: { + get: function () { + return this._orderings; + }, + set: function (value) { + if(value !== this._orderings) { + this._orderings = value; + } + } + }, + + /** + * A RangeController, TreeController, or equivalent object that provides sorting, filtering, + * selection handling of a collection of object. + * + * @type {CollectionController} + */ + dataController: { + value: undefined + }, + + /** + * the DataStream carrying objects described by the current criteria. + * + * @type {DataStream} + */ + _dataStream: { + value: undefined + }, + + dataStream: { + get: function() { + return this._dataStream; + }, + set: function(value) { + if(value !== this._dataStream) { + this._dataStream = value; + this.data = this._dataStream.data; + } + } + }, + + _data: { + value: undefined + }, + data: { + get: function () { + return this._data; + }, + set: function (value) { + if(value !== this._data) { + this._data = value; + } + } + } + + +}); diff --git a/ui/data-object-editor.js b/ui/data-object-editor.js new file mode 100644 index 0000000000..a24e856d0c --- /dev/null +++ b/ui/data-object-editor.js @@ -0,0 +1,32 @@ +var DataEditor = require("./data-editor").DataEditor; + +/** + * Instantiated with an ObjectDescriptor, this reusable component simplifies the display, + * creation, edition and deletion of objects described by the object descriptor + * and further described by a criteria. It's meant to be used as a source of data coming + * from a data service, using query and a dataStream, as well as in a master-details wy + * where a DataEditor could be assign the job of editing the relationship of an object + * that's already in memory. However, even though that relationship is typically an array + * if it has to many values, it shouldn't be fetched all in memory and we'll need a way to + * move a cursor through that relationship as it is scrolled for example. + * + * It uses and cordinates the different roles of existing montage objects + * like the rangeController, query, data stream etc... + * + * - track object changes to convey it to user, preferably using new event system: + * - if target is an object, it bubbles to its objectDescriptor, the mainService and then + * the application. + * - commnunicate data operations coming from the bottom of the stack such as: + * - validation errors when saving + * - updates coming from server if someone else made an update + * - re-authorozing if editing a sensitive aspect of t he object + * + * - + * + * @class DataObjectEditor + * @extends DataEditor + */ +exports.DataObjectEditor = DataEditor.specialize(/** @lends DataEditor# */ { + + +}); From 6c066725c8f8df7ee91808de54979413936f201c Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 16 Jul 2020 00:59:40 -0700 Subject: [PATCH 219/407] Misc optimizations --- .../deserializer/montage-reviver.js | 67 +++++++++++-------- core/serialization/serialization.js | 16 +++-- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/core/serialization/deserializer/montage-reviver.js b/core/serialization/deserializer/montage-reviver.js index e9f3e3bc25..c4c65c7f17 100644 --- a/core/serialization/deserializer/montage-reviver.js +++ b/core/serialization/deserializer/montage-reviver.js @@ -158,6 +158,7 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont //and another time later when we need the value else if(Date.parseRFC3339(value)) { return "date"; + //} else if (typeOf === "object" && Object.keys(value.__proto__).length === 1) { } else if (typeOf === "object" && Object.keys(value).length === 1) { if ("@" in value) { return "reference"; @@ -479,7 +480,12 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont module = context._require.async(locationDesc.moduleId); } - if (Promise.is(module)) { + if(!module && this._isSync) { + throw new Error( + "Tried to revive montage object with label " + label + + " synchronously but the module was not loaded: " + JSON.stringify(value) + ); + } else if (Promise.is(module)) { if (this._isSync) { throw new Error( "Tried to revive montage object with label " + label + @@ -860,29 +866,9 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont }, reviveValue: { - value: function(value, context, label) { + value: function reviveValue(value, context, label) { var type = this.getTypeOf(value), - revived; - - if (type === "string" || type === "number" || type === "boolean" || type === "null" || type === "undefined") { - revived = this.reviveNativeValue(value, context, label); - } else if (type === "date") { - revived = this.reviveDate(value, context, label); - } else if (type === "regexp") { - revived = this.reviveRegExp(value, context, label); - } else if (type === "reference") { - revived = this.reviveObjectReference(value, context, label); - } else if (type === "array") { - revived = this.reviveArray(value, context, label); - } else if (type === "object") { - revived = this.reviveObjectLiteral(value, context, label); - } else if (type === "Element") { - revived = this.reviveElement(value, context, label); - } else if (type === "binding") { - revived = value; - } else { - revived = this._callReviveMethod("revive" + type, value, context, label); - } + revived = this[(reviveValue._methodByType[type] || ("revive" + type))](value, context, label); if (this._isSync && Promise.is(revived)) { throw new Error("Unable to revive value with label " + label + " synchronously: " + value); @@ -892,6 +878,16 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont } }, + _reviveMethodByType: { + value: {} + }, + + reviveBinding: { + value: function(value, context, label) { + return value; + } + }, + reviveNativeValue: { value: function(value, context, label) { if (label) { @@ -987,7 +983,7 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont reviveArray: { value: function(value, context, label) { var item, - promises = []; + promises; if (label) { context.setObjectLabel(value, label); @@ -997,7 +993,7 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont item = this.reviveValue(value[i], context); if (Promise.is(item)) { - promises.push( + (promises || (promises = [])).push( item.then(this._createAssignValueFunction(value, i)) ); } else { @@ -1005,7 +1001,7 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont } } - if (promises.length === 0) { + if (!promises || promises.length === 0) { return value; } else { return Promise.all(promises).then(function() { @@ -1153,6 +1149,23 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont }); +var MontageReviverProto = MontageReviver.prototype, + _reviveMethodByType = MontageReviverProto._reviveMethodByType; + +_reviveMethodByType["string"] = _reviveMethodByType["number"] = _reviveMethodByType["boolean"] = _reviveMethodByType["null"] = _reviveMethodByType["undefined"] = "reviveNativeValue"; +_reviveMethodByType["date"] = "reviveDate"; +_reviveMethodByType["regexp"] = "reviveRegExp"; +_reviveMethodByType["reference"] = "reviveObjectReference"; +_reviveMethodByType["array"] = "reviveArray"; +_reviveMethodByType["object"] = "reviveObjectLiteral"; +_reviveMethodByType["Element"] = "reviveElement"; +_reviveMethodByType["binding"] = "reviveBinding"; + +MontageReviverProto.reviveValue._methodByType = _reviveMethodByType; + +MontageReviverProto = _reviveMethodByType = null; + + MontageReviver.findProxyForElement = function (element) { return PROXY_ELEMENT_MAP.get(element); }; @@ -1194,7 +1207,7 @@ MontageReviver.defineUnitReviver("values", function (unitDeserializer, object, v else if(values) { context._reviver.deserializeMontageObjectValues( object, - montageObjectDesc.values || montageObjectDesc.properties, //deprecated + values || montageObjectDesc.properties, //deprecated context ); } diff --git a/core/serialization/serialization.js b/core/serialization/serialization.js index b68c0305bc..5269d6354d 100644 --- a/core/serialization/serialization.js +++ b/core/serialization/serialization.js @@ -87,7 +87,8 @@ var Serialization = Montage.specialize( /** @lends Serialization.prototype # */ var serializationObject = this.getSerializationObject(), labels = []; - for (var label in serializationObject) { + //for (var label in serializationObject) { + for (var i=0, label, keys = Object.keys(serializationObject); (label = keys[i]); i++ ) { if (Object.keys(serializationObject[label]).length === 0) { labels.push(label); } @@ -577,8 +578,9 @@ var SerializationInspector = Montage.specialize(/** @lends SerializationInspecto _walkRootObjects: { value: function (visitor, objects) { /* jshint forin: true */ - for (var label in objects) { - /* jshint forin: false */ + //for (var label in objects) { + for (var i=0, label, keys = Object.keys(objects); (label = keys[i]); i++) { + /* jshint forin: false */ this._walkRootObject(visitor, objects, label); } } @@ -662,8 +664,9 @@ var SerializationInspector = Montage.specialize(/** @lends SerializationInspecto parentObject[key] = object = value.data; /* jshint forin: true */ - for (var prop in object) { - /* jshint forin: false */ + //for (var prop in object) { + for (var i=0, prop, keys = Object.keys(object);(prop = keys[i]); i++) { + /* jshint forin: false */ this._walkObject(visitor, object, prop, null, value); } } @@ -723,7 +726,8 @@ var SerializationInspector = Montage.specialize(/** @lends SerializationInspecto parentObject.bindings = object = value.data; /* jshint forin: true */ - for (var key in object) { + //for (var key in object) { + for (var i=0, key, keys = Object.keys(object);(key = keys[i]); i++) { /* jshint forin: false */ this._walkBinding(visitor, object, key, value); } From 4bdde0977b28888bf85dd85604664f8e3cb7fa7e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Jul 2020 11:02:41 -0700 Subject: [PATCH 220/407] update frb dependency --- test/spec/ui/draw/list.reel/list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/ui/draw/list.reel/list.js b/test/spec/ui/draw/list.reel/list.js index ec22673dd8..be118129fe 100644 --- a/test/spec/ui/draw/list.reel/list.js +++ b/test/spec/ui/draw/list.reel/list.js @@ -5,7 +5,7 @@ */ var Montage = require("montage").Montage, Component = require("montage/ui/component").Component, - observeProperty = require("montage/frb/observers").observeProperty; + observeProperty = require("montage/core/frb/observers").observeProperty; /** @class module:"matte/ui/list.reel".List From 8a78ccc6b59e137e46fa347480e784eb146ba6bd Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Jul 2020 11:05:31 -0700 Subject: [PATCH 221/407] add comments about a potential optimization avoiding a dual draw but causes one regression, to be investigated --- ui/component.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/ui/component.js b/ui/component.js index 57ee959bc8..97f25d2449 100644 --- a/ui/component.js +++ b/ui/component.js @@ -1704,6 +1704,21 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto this._expandComponentPromise = this._instantiateTemplate().then(function() { self._isComponentExpanded = true; self._addTemplateStylesIfNeeded(); + + /* + Javier found out that there was a double draw in repetition. + Commenting out self.needsDraw = true here fixes it, + which seems to particularly improve performance for repetition + where items’s templates contain repetitions that themselves + have items with templates that contains repetitions, etc … + the impact is on component that have templates. To be benchmarked. + + But: comenting it out causes 1 repetition tests to fail: + + repetition/repetition ui/repetition-spec Repetition in a external component should draw the repetition of the 'component repetition' + + So more to look at before we can enjoy that win. + */ self.needsDraw = true; }).catch(function (error) { console.error(error); @@ -3352,6 +3367,10 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto // have been set on it. originalElement = this.originalElement; + /* + Original + */ + var attributes, i, length, name, value, attributeName, descriptor; attributes = originalElement.attributes; if (attributes) { @@ -3374,6 +3393,31 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto } } + +/* + var attributes, i, length, name, value, attributeName, descriptor; + attributes = originalElement.attributes; + if (attributes) { + length = attributes.length; + // Iterate over element's attributes + for (name of originalElement.getAttributeNames()) { + + descriptor = this._getElementAttributeDescriptor(name, this); + // check if this attribute from the markup is a well-defined attribute of the component + if (descriptor || (typeof this[name] !== 'undefined')) { + // only set the value if a value has not already been set by binding + if (typeof this._elementAttributeValues[name] === 'undefined') { + value = originalElement.getAttribute(name); + this._elementAttributeValues[name] = value; + if(this[name] === null || this[name] === undefined) { + this[name] = value; + } + } + } + } + } +*/ + // textContent is a special case since it isn't an attribute descriptor = this._getElementAttributeDescriptor('textContent', this); if(descriptor) { From c0b91bc55865aa0d139a6efea902caaefe4268fd Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 17 Jul 2020 12:09:13 -0700 Subject: [PATCH 222/407] Fix a spec regression --- test/spec/meta/event-descriptor-spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/spec/meta/event-descriptor-spec.js b/test/spec/meta/event-descriptor-spec.js index e1fb4c4c7e..0b62e5064c 100644 --- a/test/spec/meta/event-descriptor-spec.js +++ b/test/spec/meta/event-descriptor-spec.js @@ -54,10 +54,10 @@ describe("meta/event-descriptor-spec", function () { "prototype": "montage/core/meta/event-descriptor", "values": { "name": "event", - "objectDescriptor": {"@": "objectDescriptor_testobjectdescriptor"} + "objectDescriptor": {"@": "testObjectDescriptor"} } }, - "objectDescriptor_testobjectdescriptor": {} + "testObjectDescriptor": {} }; serializer = new Serializer().initWithRequire(require); serializer.setSerializationIndentation(4); From afeab5a761190626fb63c5da4cd5c4c401de2337 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 18 Jul 2020 16:53:16 -0700 Subject: [PATCH 223/407] Fix regression when evolving object to map --- core/gate.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/core/gate.js b/core/gate.js index 5f8f3980a8..a170c90590 100644 --- a/core/gate.js +++ b/core/gate.js @@ -188,13 +188,15 @@ var Gate = exports.Gate = Montage.specialize(/** @lends Gate.prototype # */ { */ toString: { value: function () { - var fieldNames = this._fields, - i, + var table = this.table, + fieldNameEnumetator = table.keys(), iField, result = ""; - for (i = 0; (iField = fieldNames[i]); i++) { - result += iField + "[" + (this._value & fieldNames[iField]) + "], "; - } + + while ((iField = fieldNameEnumetator.next().value)) { + result += iField + "[" + (this._value & table.get(iField)) + "], "; + } + return result; } } From c0a34cb5a713ca00596144e1f83f1c20b9540b44 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 18 Jul 2020 18:08:02 -0700 Subject: [PATCH 224/407] remove a filter() to improve performance --- core/range-controller.js | 59 ++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/core/range-controller.js b/core/range-controller.js index b0cb560e10..6428ec9f5c 100644 --- a/core/range-controller.js +++ b/core/range-controller.js @@ -123,24 +123,55 @@ Object.defineProperty(_RangeSelection.prototype, "swap_or_push", { itemsToAdd.contentEquals = this.contentEquals; - plus = itemsToAdd.filter(function(item, index){ - // do not add items to the selection if they aren't in content - if (content && !content.has(item)) { - return false; - } + // plus = itemsToAdd.filter(function(item, index){ + // // do not add items to the selection if they aren't in content + // if (content && !content.has(item)) { + // return false; + // } + + // // if the same item appears twice in the add list, only add it once + // if (itemsToAdd.findLast(item) > index) { + // return false; + // } + + // // if the item is already in the selection, don't add it + // // unless it's in the part that we're about to delete. + // var indexInSelection = this.find(item); + // return indexInSelection < 0 || + // (indexInSelection >= start && indexInSelection < start + minusLength); + + // }, this); + + + plus = []; + for(var indexInSelection, i=0, countI = itemsToAdd.length;(i index) { - return false; + // do not add items to the selection if they aren't in content + if (content && !content.has(itemsToAdd[i])) { + continue; + } + + // if the same item appears twice in the add list, only add it once + if (itemsToAdd.findLast(itemsToAdd[i]) > i) { + continue; + } + + // if the item is already in the selection, don't add it + // unless it's in the part that we're about to delete. + indexInSelection = this.find(itemsToAdd[i]); + if(indexInSelection < 0 || + (indexInSelection >= start && indexInSelection < start + minusLength)) { + plus.push(itemsToAdd[i]); + } } - // if the item is already in the selection, don't add it - // unless it's in the part that we're about to delete. - var indexInSelection = this.find(item); - return indexInSelection < 0 || - (indexInSelection >= start && indexInSelection < start + minusLength); + } + + // if(JSON.stringify(plus) !== JSON.stringify(plus2)) { + // debug; + // } - }, this); } else { plus = EMPTY_ARRAY; From 638b12422bdd4997358fd170a70be31987536fcf Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 18 Jul 2020 18:10:00 -0700 Subject: [PATCH 225/407] replace a for in with .hasOwnProperty() by a for() on Object.keys() --- core/serialization/serializer/montage-ast.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/serialization/serializer/montage-ast.js b/core/serialization/serializer/montage-ast.js index 8d1c91c086..c11b031629 100644 --- a/core/serialization/serializer/montage-ast.js +++ b/core/serialization/serializer/montage-ast.js @@ -45,12 +45,14 @@ var Root = exports.Root = Montage.specialize({ toJSON: { value: function() { var result = Object.create(null), - object; + myObject = this.object, + labels = Object.keys(myObject), + i, + label, + object; - for (var label in this.object) { - if (Object.hasOwnProperty.call(this.object, label)) { - - object = this.object[label]; + for(i=0;(label = labels[i]); i++) { + object = myObject[label]; if (object.toJSON) { result[label] = object.toJSON(label, 1); @@ -58,7 +60,6 @@ var Root = exports.Root = Montage.specialize({ result[label] = object; } } - } return result; } From eb69ffc9d4cd37912ef3b051744a8e2a686eea1e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 18 Jul 2020 18:12:51 -0700 Subject: [PATCH 226/407] - replace a for in by for on Object.keys() - remove unused method - misc formatting cleanup --- core/template.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/core/template.js b/core/template.js index 09bd07a833..21ae211b97 100644 --- a/core/template.js +++ b/core/template.js @@ -68,11 +68,12 @@ var Template = Montage.specialize( /** @lends Template# */ { return deserializer; } }, - getDeserializer: { - value: function () { - return this._deserializer; - } - }, + //Unused anywhere + // getDeserializer: { + // value: function () { + // return this._deserializer; + // } + // }, _serialization: { value: null @@ -316,7 +317,7 @@ var Template = Montage.specialize( /** @lends Template# */ { part.objects = objects; self._invokeDelegates(part, instances); part.stopActingAsTopComponent(); - + return part; }); } @@ -488,11 +489,10 @@ var Template = Montage.specialize( /** @lends Template# */ { object, owner = objects.owner || instances && instances.owner, objectOwner, - objectLabel; + objectLabel, + i, keys, label; - /* jshint forin: true */ - for (var label in objects) { - /* jshint forin: false */ + for (i=0, keys = Object.keys(objects);(label = keys[i]); i++) { // Don't call delegate methods on objects that were passed to // the instantiation. @@ -1008,7 +1008,7 @@ var Template = Montage.specialize( /** @lends Template# */ { break; } } - + // Store all element ids of the argument, we need to create // a serialization with the components that point to them. argumentsElementIds.push.apply(argumentsElementIds, @@ -1025,7 +1025,7 @@ var Template = Montage.specialize( /** @lends Template# */ { /* jshint forin: false */ argumentElementsCollisionTable[key] = collisionTable[key]; } - } + } } } @@ -1551,7 +1551,7 @@ var TemplateResources = Montage.specialize( /** @lends TemplateResources# */ { documentResources = DocumentResources.getInstanceForDocument(targetDocument); return documentResources.preloadResource(url); } - + return this.resolvedPromise; } }, From 3fe38f4cfaa26337f0cdcd404cd791ce68405425 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 18 Jul 2020 18:14:17 -0700 Subject: [PATCH 227/407] replace a for in with hasOwnProperty by a for() on Object.keys() --- core/serialization/serializer/montage-labeler.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/serialization/serializer/montage-labeler.js b/core/serialization/serializer/montage-labeler.js index 3e1c464e0b..4b7685e06c 100644 --- a/core/serialization/serializer/montage-labeler.js +++ b/core/serialization/serializer/montage-labeler.js @@ -95,11 +95,11 @@ exports.MontageLabeler = Montage.specialize({ */ initWithObjects: { value: function(labels) { - for (var label in labels) { - if (labels.hasOwnProperty(label)) { - this.setObjectLabel(labels[label], label); - this._userDefinedLabels[label] = true; - } + var keys = Object.keys(labels), + i, label; + for(i=0;(label = keys[i]); i++) { + this.setObjectLabel(labels[label], label); + this._userDefinedLabels[label] = true; } } }, From b6fceb8ec58a1dd14c1661cefa862f718a8e532d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 18 Jul 2020 18:14:43 -0700 Subject: [PATCH 228/407] add validity object to all-purpose Mock DOM element --- test/mocks/dom.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/mocks/dom.js b/test/mocks/dom.js index cc04ba6f03..0264b356be 100644 --- a/test/mocks/dom.js +++ b/test/mocks/dom.js @@ -149,7 +149,8 @@ exports.element = function (_document) { focus: function () {}, blur: function () {}, ownerDocument: _document || exports.document(), - tagName: "MOCK" + tagName: "MOCK", + validity: {} }; Object.addEach(result, EventTarget); From 9f2542b9ac228a8c125fdc50b3a84019b38eb013 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 18 Jul 2020 18:15:39 -0700 Subject: [PATCH 229/407] replace ._inDocument by .inDocument --- test/spec/ui/component-spec.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/spec/ui/component-spec.js b/test/spec/ui/component-spec.js index c8d6d4e001..f7c26c23fd 100644 --- a/test/spec/ui/component-spec.js +++ b/test/spec/ui/component-spec.js @@ -453,7 +453,7 @@ TestPageLoader.queueTest("draw/draw", function (testPage) { xit("should receive a first draw event after its first draw", function (done) { // TODO: we can't make this working at the moment because - // enter/exitDocument is implemented by forcing a draw + // enter/exitDocument is implemented by forcing a draw testPage.test.componentB1.addEventListener("firstDraw", testPage.test.componentB1, false); testPage.test.componentB.addEventListener("firstDraw", testPage.test.componentB, false); @@ -526,7 +526,7 @@ TestPageLoader.queueTest("draw/draw", function (testPage) { expect(testPage.test.componentE.childComponents.length).toBe(1); expect(testPage.test.componentE.childComponents[0]).toBe(componentE1); expect(componentE1.childComponents.length).toBe(1); - expect(componentE1.childComponents[0]).toBe(testPage.test.componentE11); + expect(componentE1.childComponents[0]).toBe(testPage.test.componentE11); }); it("should remove a component from its previous parent component childComponent's when it is reattached in another part of the component tree", function () { @@ -1266,9 +1266,9 @@ TestPageLoader.queueTest("draw/draw", function (testPage) { componentB._needsEnterDocument = false; componentC._needsEnterDocument = false; - componentA._inDocument = true; - componentB._inDocument = false; - componentC._inDocument = false; + componentA.inDocument = true; + componentB.inDocument = false; + componentC.inDocument = false; componentA._firstDraw = false; componentB._firstDraw = false; @@ -1308,9 +1308,9 @@ TestPageLoader.queueTest("draw/draw", function (testPage) { componentB._needsEnterDocument = false; componentC._needsEnterDocument = false; - componentA._inDocument = true; - componentB._inDocument = true; - componentC._inDocument = true; + componentA.inDocument = true; + componentB.inDocument = true; + componentC.inDocument = true; }); it("should exit the document in bottom-up order", function () { From c17b748a38b9d963659407e0dd8fb75dcdf43f73 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 18 Jul 2020 18:23:23 -0700 Subject: [PATCH 230/407] fix missing backward compatibility setter for _inDocument --- ui/component.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/component.js b/ui/component.js index 97f25d2449..79a7bf4d1e 100644 --- a/ui/component.js +++ b/ui/component.js @@ -1206,6 +1206,9 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto _inDocument: { get: function() { return this.inDocument; + }, + set: function(value) { + this.inDocument = value; } }, inDocument: { From 0ccef43c16d13441409c305519f8321b680671e4 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 19 Jul 2020 17:22:42 -0700 Subject: [PATCH 231/407] add string-to-URL-converter --- core/converter/string-to-URL-converter.js | 41 +++++++++++++++++ core/converter/string-to-URL-converter.mjson | 46 ++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 core/converter/string-to-URL-converter.js create mode 100644 core/converter/string-to-URL-converter.mjson diff --git a/core/converter/string-to-URL-converter.js b/core/converter/string-to-URL-converter.js new file mode 100644 index 0000000000..dde9139495 --- /dev/null +++ b/core/converter/string-to-URL-converter.js @@ -0,0 +1,41 @@ +/** + * @module montage/core/converter/bytes-converter + * @requires montage/core/converter/converter + * @requires montage/core/converter/number-converter + */ +var Converter = require("./converter").Converter; + +/** + * @class StringToURLConverter + * @classdesc Converts a string value to a URL instance. + * @extends Converter + */ +exports.StringToURLConverter = Converter.specialize( /** @lends StringToURLConverter# */ { + + /** + * Converts the specified value to a URL. + * @function + * @param {Property} v The value to format. + * @returns {URL} The value converted to a URL. + */ + convert: { + value: function (v) { + return new URL(v); + } + }, + + /** + * Reverts a URL to a string. + * @function + * @param {URL} v The value to revert. + * @returns {string} v + * @see StringToURLConverter#convert + */ + revert: { + value: function (v) { + return v.toString(); + } + } + +}); + diff --git a/core/converter/string-to-URL-converter.mjson b/core/converter/string-to-URL-converter.mjson new file mode 100644 index 0000000000..a7d344f285 --- /dev/null +++ b/core/converter/string-to-URL-converter.mjson @@ -0,0 +1,46 @@ +{ + "bytesConverter_decimals": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "decimals", + "objectDescriptor": { + "@": "root" + }, + "valueType": "number", + "helpKey": "" + } + }, + "converter_descriptor": { + "object": "core/converter/converter.mjson" + }, + "root": { + "prototype": "core/meta/module-object-descriptor", + "values": { + "name": "BytesConverter", + "customPrototype": false, + "parent": { + "@": "converter_descriptor" + }, + "propertyDescriptors": [ + { + "@": "bytesConverter_decimals" + } + ], + "propertyDescriptorGroups": { + "bytesConverter": [ + { + "@": "bytesConverter_decimals" + } + ] + }, + "propertyValidationRules": {}, + "objectDescriptorModule": { + "%": "core/converter/bytes-converter.mjson" + }, + "exportName": "BytesConverter", + "module": { + "%": "core/converter/bytes-converter" + } + } + } +} From bfc420722c0e4382dd9b290a6c6098602dc86775 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 19 Jul 2020 17:27:10 -0700 Subject: [PATCH 232/407] changes bootstrapping bluebird location --- montage.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/montage.js b/montage.js index 04ad4f47e5..6e1908689f 100644 --- a/montage.js +++ b/montage.js @@ -226,7 +226,8 @@ var pending = { "require": montageLocation+"core/mr/require.js", "require/browser": montageLocation+"core/mr/browser.js", - "promise": montageLocation+"node_modules/bluebird/js/browser/bluebird.min.js" + //"promise": montageLocation+"node_modules/bluebird/js/browser/bluebird.min.js" + "promise": montageLocation+"../bluebird/js/browser/bluebird.min.js" // "shim-string": "core/shim/string.js" // needed for the `endsWith` function. }; From 85aa7cc1ede3f9fa8b0fa095ccf59ed1aa1f57c1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 19 Jul 2020 17:38:51 -0700 Subject: [PATCH 233/407] - tenporarily bypassing authentication --- core/application.js | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/core/application.js b/core/application.js index e448e8bfa4..e8fcd6b5ef 100644 --- a/core/application.js +++ b/core/application.js @@ -481,25 +481,26 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio selfUserCriteria, userIdentityQuery; - //Bypassing to work offline: - if(userIdentityServices && userIdentityServices.length > 0) { - //Shortcut, there could be multiple one we need to flatten. - userIdentityObjectDescriptors = userIdentityServices[0].types; - - if(userIdentityObjectDescriptors.length > 0) { - //selfUserCriteria = new Criteria().initWithExpression("identity == $", "self"); - userIdentityQuery = DataQuery.withTypeAndCriteria(userIdentityObjectDescriptors[0]); - - authenticationPromise = self.mainService.fetchData(userIdentityQuery) - .then(function(userIdenties) { - self.user = userIdenties[0]; - }); - - } - } - else { + //Temporarily Bypassing authentication: + // if(userIdentityServices && userIdentityServices.length > 0) { + // //Shortcut, there could be multiple one we need to flatten. + // userIdentityObjectDescriptors = userIdentityServices[0].types; + + // if(userIdentityObjectDescriptors.length > 0) { + // //selfUserCriteria = new Criteria().initWithExpression("identity == $", "self"); + // userIdentityQuery = DataQuery.withTypeAndCriteria(userIdentityObjectDescriptors[0]); + + // authenticationPromise = self.mainService.fetchData(userIdentityQuery) + // .then(function(userIdenties) { + // self.userIdentity = userIdenties[0]; + // }); + + // } + // } + // else { + //Needs to beef-up the case we have a first anonymous user who could come back later. authenticationPromise = Promise.resolve(true); - } + // } return authenticationPromise.finally(function() { // if (typeof document !== "undefined") { From de59a67e99be43cad0179dc44d2a4825581c6e6a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 19 Jul 2020 18:44:26 -0700 Subject: [PATCH 234/407] activate back auth --- core/application.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/core/application.js b/core/application.js index e8fcd6b5ef..210c1e0793 100644 --- a/core/application.js +++ b/core/application.js @@ -482,25 +482,25 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio userIdentityQuery; //Temporarily Bypassing authentication: - // if(userIdentityServices && userIdentityServices.length > 0) { - // //Shortcut, there could be multiple one we need to flatten. - // userIdentityObjectDescriptors = userIdentityServices[0].types; - - // if(userIdentityObjectDescriptors.length > 0) { - // //selfUserCriteria = new Criteria().initWithExpression("identity == $", "self"); - // userIdentityQuery = DataQuery.withTypeAndCriteria(userIdentityObjectDescriptors[0]); - - // authenticationPromise = self.mainService.fetchData(userIdentityQuery) - // .then(function(userIdenties) { - // self.userIdentity = userIdenties[0]; - // }); - - // } - // } - // else { + if(userIdentityServices && userIdentityServices.length > 0) { + //Shortcut, there could be multiple one we need to flatten. + userIdentityObjectDescriptors = userIdentityServices[0].types; + + if(userIdentityObjectDescriptors.length > 0) { + //selfUserCriteria = new Criteria().initWithExpression("identity == $", "self"); + userIdentityQuery = DataQuery.withTypeAndCriteria(userIdentityObjectDescriptors[0]); + + authenticationPromise = self.mainService.fetchData(userIdentityQuery) + .then(function(userIdenties) { + self.userIdentity = userIdenties[0]; + }); + + } + } + else { //Needs to beef-up the case we have a first anonymous user who could come back later. authenticationPromise = Promise.resolve(true); - // } + } return authenticationPromise.finally(function() { // if (typeof document !== "undefined") { From be48609b879ed228759fbcdf31786569d5782721 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 19 Jul 2020 19:16:36 -0700 Subject: [PATCH 235/407] - fix content loss when exiting document. Will need to review later --- ui/cascading-list.reel/cascading-list.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ui/cascading-list.reel/cascading-list.js b/ui/cascading-list.reel/cascading-list.js index c1d06d280b..929770ee9d 100644 --- a/ui/cascading-list.reel/cascading-list.js +++ b/ui/cascading-list.reel/cascading-list.js @@ -71,10 +71,14 @@ exports.CascadingList = Component.specialize({ exitDocument: { value: function () { - this.popAll(); + /* + This empties the whole component, loosing any selection etc... + and there's no obvious way to bring it back yet. Commenting out for now: + */ + //this.popAll(); } }, - + _delegate: { value: null }, @@ -148,7 +152,7 @@ exports.CascadingList = Component.specialize({ if (index <= this._currentColumnIndex && this._currentColumnIndex !== -1) { this._pop(); - // the value of the property _currentColumnIndex + // the value of the property _currentColumnIndex // changed when _pop() has been called. if (index <= this._currentColumnIndex) { this.popAtIndex(index); @@ -220,7 +224,7 @@ exports.CascadingList = Component.specialize({ value: function () { var cascadingListItem, context = this.history.pop(); - + this._currentColumnIndex--; context.isEditing = false; this.needsDraw = true; @@ -237,7 +241,7 @@ exports.CascadingList = Component.specialize({ value: function (object, columnIndex, isEditing) { if (!this._populatePromise && object) { var self = this; - + this._populatePromise = this.loadUserInterfaceDescriptor(object).then(function (UIDescriptor) { var context = self._createCascadingListContextWithObjectAndColumnIndex( object, From 393877ee1476ab42357a401853fade38d4403988 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 19 Jul 2020 19:21:44 -0700 Subject: [PATCH 236/407] pass context to custom list item --- .../cascading-list-item.reel/cascading-list-item.html | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/cascading-list.reel/cascading-list-item.reel/cascading-list-item.html b/ui/cascading-list.reel/cascading-list-item.reel/cascading-list-item.html index b89ddadb34..82976411bc 100755 --- a/ui/cascading-list.reel/cascading-list-item.reel/cascading-list-item.html +++ b/ui/cascading-list.reel/cascading-list-item.reel/cascading-list-item.html @@ -55,6 +55,7 @@ "values": { "element": {"#": "content"}, "componentModule": {"<-": "@owner.componentModule"}, + "context": {"<-": "@owner.context"}, "data": {"<-": "@owner.context.object"}, "component.userInterfaceDescriptor": {"<-": "@owner.context.userInterfaceDescriptor"}, "component.delegate": {"<-": "@owner.delegate"} From 90ccae45b0bcde81e5757ca374ad2dac86ba384c Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 23 Jul 2020 15:39:38 -0700 Subject: [PATCH 237/407] update dependency --- test/all.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/all.js b/test/all.js index d2b2ae8404..7aa3f6a6eb 100644 --- a/test/all.js +++ b/test/all.js @@ -1,6 +1,6 @@ console.log('montage-testing', 'Start'); -module.exports = require("montage-testing").run(require, [ +module.exports = require("montage/testing/run").run(require, [ // App {name: "spec/application-spec", node: false}, // Internal From ba2e7c36ecda0dc348db31cc0999012288f0c415 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 23 Jul 2020 15:40:12 -0700 Subject: [PATCH 238/407] Fix bluebird location for apps and montage testing --- montage.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/montage.js b/montage.js index 6e1908689f..0db61f6db8 100644 --- a/montage.js +++ b/montage.js @@ -227,7 +227,9 @@ "require": montageLocation+"core/mr/require.js", "require/browser": montageLocation+"core/mr/browser.js", //"promise": montageLocation+"node_modules/bluebird/js/browser/bluebird.min.js" - "promise": montageLocation+"../bluebird/js/browser/bluebird.min.js" + "promise": (params.montageLocation === (global.location.origin+"/")) + ? montageLocation+"node_modules/bluebird/js/browser/bluebird.min.js" //montage in test + : montageLocation+"../bluebird/js/browser/bluebird.min.js" //anything else // "shim-string": "core/shim/string.js" // needed for the `endsWith` function. }; From 5e8aea0c03e5d51ddc891a4e3eb33389d7f79b43 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 23 Jul 2020 15:41:17 -0700 Subject: [PATCH 239/407] avoid CustomEvent instanciation when using MutableEvent.fromType() --- core/event/mutable-event.js | 57 +++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/core/event/mutable-event.js b/core/event/mutable-event.js index 3385daf4b3..78146051c6 100644 --- a/core/event/mutable-event.js +++ b/core/event/mutable-event.js @@ -90,10 +90,14 @@ var wrapPropertyGetter = function (key, storageKey) { */ getPreventDefault: { value: function () { - if (this._event.getPreventDefault) { - return this._event.getPreventDefault(); + if (this._event) { + if (this._event.getPreventDefault) { + return this._event.getPreventDefault(); + } + return this._event.defaultPrevented; + } else { + return this.defaultPrevented; } - return this._event.defaultPrevented; } }, @@ -243,7 +247,7 @@ var wrapPropertyGetter = function (key, storageKey) { */ bubbles: { get: function () { - return (this._bubbles !== void 0) ? this._bubbles : this._event.bubbles; + return (this._bubbles !== void 0) ? this._bubbles : (this._event && this._event.bubbles); }, set: function (value) { this._bubbles = value; @@ -282,6 +286,23 @@ var wrapPropertyGetter = function (key, storageKey) { return this._event ? this._event.targetTouches : null; } }, + + _cancelable: { + value: void 0 + }, + /** + * @type {Property} + * @default {boolean} should be false by default + */ + cancelable: { + get: function () { + return (this._cancelable !== void 0) ? this._cancelable : this._event && this._event.cancelable; + }, + set: function (value) { + this._cancelable = value; + } + }, + _defaultPrevented: { value: void 0 }, @@ -315,6 +336,21 @@ var wrapPropertyGetter = function (key, storageKey) { set: function (value) { this._timeStamp = value; } + }, + _detail: { + value: void 0 + }, + /** + * @type {Property} + * @default {Object} null + */ + detail: { + get: function () { + return (this._detail !== void 0) ? this._detail : (this._event && this._event.detail); + }, + set: function (value) { + this._detail = value; + } } }, { @@ -352,8 +388,17 @@ var wrapPropertyGetter = function (key, storageKey) { * @returns this.fromEvent(anEvent) */ fromType: { - value: function (type, canBubbleArg, cancelableArg, detail) { - return this.fromEvent(new CustomEvent(type, {bubbles: canBubbleArg, cancelable:cancelableArg, detail:detail})); + value: function MutableEvent_fromType(type, canBubbleArg, cancelableArg, detail) { + var newEvent = new this(); + + newEvent.type = type; + newEvent.bubbles = typeof canBubbleArg === "boolean" ? canBubbleArg : false; + newEvent.cancelable = typeof cancelableArg === "boolean" ? cancelableArg : false; + if(detail) newEvent.detail = detail; + + return newEvent; + + //return this.fromEvent(new CustomEvent(type, {bubbles: canBubbleArg, cancelable:cancelableArg, detail:detail})); } } From 6f0b8d3850f1423f503cc149cf4900f0548b1798 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 24 Jul 2020 17:05:45 -0700 Subject: [PATCH 240/407] Remove log --- data/service/raw-data-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 69f37c6303..dc0358f8b5 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -1529,7 +1529,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot rawDataTypeNameForMapping: { value: function (aMapping) { - console.warn("rawDataTypeNameForMapping() needs to be overriden with a concrete implementation by subclasses of RawDataService") + // console.warn("rawDataTypeNameForMapping() needs to be overriden with a concrete implementation by subclasses of RawDataService") } } From 0538c385098fbb7ea18591715fd7e8bf6d77a476 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 29 Jul 2020 18:35:27 -0700 Subject: [PATCH 241/407] Adds a test with parameter $ by itself --- core/frb/spec/language.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/frb/spec/language.js b/core/frb/spec/language.js index 5a0eaa1bc6..d12df59103 100644 --- a/core/frb/spec/language.js +++ b/core/frb/spec/language.js @@ -554,6 +554,16 @@ module.exports = [ {type: "literal", value: 4} ]} }, + { + path: "2 + 2 == $", + syntax: {type: "equals", args: [ + {type: "add", args: [ + {type: "literal", value: 2}, + {type: "literal", value: 2} + ]}, + {type: "parameters"} + ]} + }, { path: "!0", From 3ca2db72eb293db8cc7261e4b26dcb2b77bd9342 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 29 Jul 2020 18:36:17 -0700 Subject: [PATCH 242/407] avoid creating an extra criteria if this.foreignDescriptorMappings is true --- data/converter/raw-foreign-value-to-object-converter.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/converter/raw-foreign-value-to-object-converter.js b/data/converter/raw-foreign-value-to-object-converter.js index 55fffa8087..7439a1ab5c 100644 --- a/data/converter/raw-foreign-value-to-object-converter.js +++ b/data/converter/raw-foreign-value-to-object-converter.js @@ -127,7 +127,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( if((v && !(v instanceof Array )) || (v instanceof Array && v.length > 0)) { var self = this, - criteria = this.convertCriteriaForValue(v), + criteria, query; if(this.foreignDescriptorMappings) { @@ -178,6 +178,8 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( } } else { + criteria = this.convertCriteriaForValue(v); + return this._descriptorToFetch.then(function (typeToFetch) { return self._fetchConvertedDataForObjectDescriptorCriteria(typeToFetch, criteria); From 676f9a74d16a75ba9d0161e103042ec09d826987 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 1 Aug 2020 23:41:51 -0700 Subject: [PATCH 243/407] fix exception in node --- core/environment.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/environment.js b/core/environment.js index 4f7d66fb92..b064595d42 100644 --- a/core/environment.js +++ b/core/environment.js @@ -18,7 +18,9 @@ var Environment = exports.Environment = Montage.specialize({ systemLocaleIdentifier: { get: function () { - return (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.userLanguage || navigator.language || navigator.browserLanguage || 'en' + return typeof navigator === "object" + ? (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.userLanguage || navigator.language || navigator.browserLanguage || 'en' + : "en" } }, From 76c9234497ce58251aa142ce9efa1473444bb2dd Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 1 Aug 2020 23:54:21 -0700 Subject: [PATCH 244/407] fix formatting --- data/service/expression-data-mapping.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 14c876a1cf..ff1edf4aae 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -293,7 +293,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData rawDataTypeExpression: { value: undefined }, - /** + /** * The id of the raw type mapped to the mapping's objectDescriptor. * Could be statically generated by a tool, or dynamicallay fetched in the case * of a database that internally maintain unique ids for every object From c3b7490b5ece1070f2fed3805dc4700188426c7a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sat, 1 Aug 2020 23:55:21 -0700 Subject: [PATCH 245/407] change localizablePropertyDescriptors to be a Set and not an Array --- core/meta/object-descriptor.js | 37 +++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/core/meta/object-descriptor.js b/core/meta/object-descriptor.js index 806d58b4b2..57abc0c4f6 100644 --- a/core/meta/object-descriptor.js +++ b/core/meta/object-descriptor.js @@ -34,6 +34,7 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends this._propertyDescriptorGroups = {}; this._eventPropertyDescriptorsTable = new Map(); this.defineBinding("eventDescriptors", {"<-": "_eventDescriptors.concat(parent.eventDescriptors)"}); + this.defineBinding("localizablePropertyNames", {"<-": "localizablePropertyDescriptors.name"}); } }, @@ -492,8 +493,9 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends if(descriptor.isLocalizable) { descriptor.removeOwnPropertyChangeListener("isLocalizable", this); - index = this.localizablePropertyDescriptors.indexOf(descriptor); - this.localizablePropertyDescriptors.splice(index, 1); + // index = this.localizablePropertyDescriptors.indexOf(descriptor); + // this.localizablePropertyDescriptors.splice(index, 1); + this.localizablePropertyDescriptors.delete(descriptor); } } } @@ -510,7 +512,9 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends descriptor._owner = descriptor._owner || this; if(descriptor.isLocalizable) { - this.localizablePropertyDescriptors.push(descriptor); + //this.localizablePropertyDescriptors.push(descriptor); + this.localizablePropertyDescriptors.add(descriptor); + } } } @@ -525,9 +529,11 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends handleIsLocalizablePropertyChange: { value: function(changeValue, key, object) { if(object.isLocalizable) { - this.localizablePropertyDescriptors.push(object); + //this.localizablePropertyDescriptors.push(object); + this.localizablePropertyDescriptors.add(object); } else { - this.localizablePropertyDescriptors.splice(this.localizablePropertyDescriptors.indexOf(object), 1); + //this.localizablePropertyDescriptors.splice(this.localizablePropertyDescriptors.indexOf(object), 1); + this.localizablePropertyDescriptors.delete(object); } } }, @@ -556,7 +562,8 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends if(descriptor.isLocalizable) { - this.localizablePropertyDescriptors.push(descriptor); + //this.localizablePropertyDescriptors.push(descriptor); + this.localizablePropertyDescriptors.add(descriptor); descriptor.addOwnPropertyChangeListener("isLocalizable", this); } @@ -588,21 +595,27 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends value: false }, + _localizablePropertyDescriptors: { + value: undefined + }, + /** - * A non-observed cache of an object descriptor's Property Descriptors + * A Set of an ObjectDescriptor's Property Descriptors that are localizable * * @private - * @property {Array} + * @property {Set} */ - _localizablePropertyDescriptors: { - value: undefined - }, localizablePropertyDescriptors: { get: function() { - return this._localizablePropertyDescriptors || (this._localizablePropertyDescriptors = []); + //return this._localizablePropertyDescriptors || (this._localizablePropertyDescriptors = []); + return this._localizablePropertyDescriptors || (this._localizablePropertyDescriptors = new Set()); } }, + localizablePropertyNames: { + value: undefined + }, + /** * PropertyDescriptors associated to this object descriptor, including those * provided by this.parent. From c15c801093582673effd8bbe0796bca8da8d1ca4 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 2 Aug 2020 00:01:20 -0700 Subject: [PATCH 246/407] - change userLocale to userLocales - change userLocaleCriteria to userLocalesCriteria - improve listening to userLocales changes which will be needed to refresh data in the new locale(s) --- data/service/data-service.js | 46 ++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 73d98442bc..95fb453143 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -69,6 +69,8 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { exports.DataService.mainService = ObjectDescriptor.mainService = exports.DataService.mainService || this; if(this === DataService.mainService) { UserIdentityManager.mainService = DataService.mainService; + //this.addOwnPropertyChangeListener("userLocales", this); + this.addRangeAtPathChangeListener("userLocales", this, "handleUserLocalesRangeChange"); } //Deprecated now @@ -82,9 +84,6 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { UserIdentityManager.registerUserIdentityService(this); } - this.addOwnPropertyChangeListener("userLocale", this); - - this._initializeOffline(); } }, @@ -3445,47 +3444,58 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { * @type {Locale} */ - _userLocale: { + _userLocales: { value: undefined }, - userLocale: { + userLocales: { get: function() { - return this._userLocale || ((this._userLocale = Locale.systemLocale) && this._userLocale); + return this.isRootService + ? this._userLocales || ((this._userLocales = [Locale.systemLocale]) && this._userLocales) + : this.rootService.userLocales; }, set: function(value) { - if(value !== this._userLocale) { - this._userLocale = value; + if(value !== this._userLocales) { + this._userLocales = value; } } }, - _userLocaleCriteria: { + _userLocalesCriteria: { value: undefined }, - userLocaleCriteria: { + userLocalesCriteria: { get: function() { - return this._userLocaleCriteria || (this._userLocaleCriteria = this._createUserLocaleCriteria()); + return this.isRootService + ? this._userLocalesCriteria || (this._userLocalesCriteria = this._createUserLocalesCriteria()) + : this.rootService.userLocalesCriteria; + }, set: function(value) { - if(value !== this._userLocaleCriteria) { - this._userLocaleCriteria = value; + if(value !== this._userLocalesCriteria) { + this._userLocalesCriteria = value; } } }, - _createUserLocaleCriteria: { + _createUserLocalesCriteria: { value: function() { - return new Criteria().initWithExpression("locale == $locale", { - locale: this.userLocale + return new Criteria().initWithExpression("locales == $DataServiceUserLocales", { + DataServiceUserLocales: this.userLocales }); } }, - handleUserLocaleChange: { + handleUserLocalesChange: { value: function (value, key, object) { - this.userLocaleCriteria = this._createUserLocaleCriteria(); + this.userLocalesCriteria = this._createUserLocalesCriteria(); + } + }, + + handleUserLocalesRangeChange: { + value: function (plus, minus){ + this.userLocalesCriteria = this._createUserLocalesCriteria(); } } From 974ebd713e5a005f0c6746bed775e1643336d2a3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 2 Aug 2020 00:04:33 -0700 Subject: [PATCH 247/407] - improve combining criteria by handling reconciliation of parameters - add ability to change a criteria's syntax and expression after creation --- core/criteria.js | 350 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 320 insertions(+), 30 deletions(-) diff --git a/core/criteria.js b/core/criteria.js index 5f3ac3c5c2..aedb3d4616 100644 --- a/core/criteria.js +++ b/core/criteria.js @@ -21,7 +21,11 @@ var Criteria = exports.Criteria = Montage.specialize({ */ expression: { get: function() { - return this._expression; + return this._expression || ( + this._syntax + ? (this._expression = stringify(this._syntax)) + : this._expression + ); } }, /** @@ -39,12 +43,26 @@ var Criteria = exports.Criteria = Montage.specialize({ }, /** * The parsed expression, a syntactic tree. + * Now mutable to avoid creating new objects when appropriate * * @type {object} */ syntax: { get: function() { return this._syntax || (this._syntax = parse(this._expression)); + }, + set: function(value) { + if(value !== this._syntax) { + //We need to reset: + //expression if we have one: + this._expression = null; + + //_compiledSyntax + this._compiledSyntax = null; + + this._syntax = value; + } + } }, _compiledSyntax: { @@ -168,6 +186,196 @@ var Criteria = exports.Criteria = Montage.specialize({ this._scope.value = value; return this.compiledSyntax(this._scope); } + }, + + /** + * Walks a criteria's syntactic tree to assess if one of more an expression + * involving propertyName. + * + * @method + * @argument {string} propertyName - a propertyName. + * + * @returns {boolean} - boolean wether the criteri qualifies a value on propertyName. + */ + + syntaxesQualifyingPropertyName: { + value: function(propertyName) { + console.warn("syntaxesQualifyingPropertyName() implementation missing"); + } + }, + + /** + * Walks a criteria's syntax to assess if one of it contains an expression + * involving propertyName. + * + * @method + * @argument {string} propertyName - a propertyName. + * + * @returns {boolean} - boolean wether the criteri qualifies a value on propertyName. + */ + + qualifiesPropertyName: { + value: function(propertyName) { + console.warn("qualifiesPropertyName() implementation missing"); + } + }, + + + + /** + * Walks a criteria's syntax: + * - replace $ parameter by {key:value} + * - alias own's parameter keys if conclicting with + * + * for example expression: "(firstName= $firstName) && (lastName = $lastName)" + * parameters: { + * "firstName": "Han", + * "lastName": "Solo" + * } + * + * @method + * @argument {object} aliasedParameters - an object containg parameters that the receiver needs to become compatible with. + * @argument {object} parameterCounters - an object containing a paramater root name and an incremented integer + * used to build a unique key as close to author's intent + * + * @returns {Criteria} - The Criteria initialized. + */ + + _syntaxByAliasingSyntaxWithParameters: { + value: function (syntax, aliasedParameters, parameterCounter, _thisParameters) { + var aliasedSyntax = {}, + syntaxKeys = Object.keys(syntax), + i, iKey, + syntaxArg0 = syntax.args && syntax.args[0], + aliasedSyntaxArg0, + syntaxArg1 = syntax.args && syntax.args[1], + aliasedSyntaxArg1, + parameter, + parameterValue, + aliasedParameter; + + for(i=0;(iKey = syntaxKeys[i]);i++) { + + + if(iKey === "args") { + aliasedSyntax.args = []; + + if(syntaxArg0.type === "parameters") { + if (syntaxArg1.type !== "literal") { + + //We replace $ syntax by the $key/$.key syntax: + aliasedSyntaxArg0 = {}; + aliasedSyntaxArg0.type = "property"; + aliasedParameter = "parameter"+(++parameterCounter); + aliasedSyntaxArg0.args = [ + { + "type":"parameters" + }, + { + "type":"literal", + "value": aliasedParameter + } + ]; + aliasedSyntax.args[0] = aliasedSyntaxArg0; + aliasedSyntax.args[1] = this._syntaxByAliasingSyntaxWithParameters(syntaxArg1, aliasedParameters, parameterCounter, _thisParameters); + + //and we register the criteria's parameter _thisParameters under the new key; + aliasedParameters[aliasedParameter] = _thisParameters; + } else { + //We need to make sure there's no conflict with aliasedParameters + parameter = syntaxArg1.value; + parameterValue = _thisParameters[parameter]; + if(aliasedParameters.hasOwnProperty(parameter) && aliasedParameters[parameter] !== parameterValue) { + aliasedParameter = parameter+(++parameterCounter); + aliasedParameters[aliasedParameter] = parameterValue; + } else { + aliasedParameter = parameter; + } + aliasedSyntax.args[0] = { + "type":"parameters" + }; + + aliasedSyntax.args[1] = { + "type":"literal", + "value":aliasedParameter + }; + aliasedParameters[aliasedParameter] = parameterValue; + } + + } else { + aliasedSyntax.args[0] = this._syntaxByAliasingSyntaxWithParameters(syntaxArg0, aliasedParameters, parameterCounter, _thisParameters); + aliasedSyntax.args[1] = this._syntaxByAliasingSyntaxWithParameters(syntaxArg1, aliasedParameters, parameterCounter, _thisParameters); + } + } else { + aliasedSyntax[iKey] = syntax[iKey]; + } + + } + + /* + JSON.stringify(d.syntax) + //$key.has(id)", {"key":"123"} + { + "type":"has", + "args":[ + { + "type":"property", + "args":[ + { + "type":"parameters" + }, + { + "type":"literal"," + value":"key" + } + ] + }, + { + "type":"property", + "args":[ + { + "type":"value" + },{ + "type":"literal" + ,"value":"id" + } + ] + } + ] + } + + JSON.stringify(f.syntax) + "$.has(id)", "123" + { + "type":"has", + "args":[ + { + "type":"parameters" + }, + { + "type":"property", + "args":[ + { + "type":"value" + }, + { + "type":"literal", + "value":"id" + } + ] + } + ] + } + + */ + return aliasedSyntax; + } + }, + + syntaxByAliasingSyntaxWithParameters: { + value: function (aliasedParameters, parameterCounters) { + return this._syntaxByAliasingSyntaxWithParameters(this.syntax, aliasedParameters||0, 0, this.parameters); + } } },{ @@ -232,44 +440,126 @@ var Criteria = exports.Criteria = Montage.specialize({ }); +function _combinedCriteriaFromArguments(type, receiver, _arguments) { + // var args = Array.prototype.map.call(_arguments, function (argument) { + // if (typeof argument === "string") { + // return parse(argument); + // } else if (argument.syntax) { + // return argument.syntax; + // } else if (typeof argument === "object") { + // return argument; + // } + // }); + var args = [], + isInstanceReceiver = (typeof receiver === "object"), + parameters = isInstanceReceiver ? receiver.parameters : null, + i = 0, argument, countI, + j, countJ, argumentParameters, argumentParametersKeys, argumentParameter, + aliasedParameters = {}; + + for(countI = _arguments.length; (i Date: Tue, 15 Sep 2020 12:24:19 -0700 Subject: [PATCH 248/407] Fix collections jasmine tests --- .vscode/launch.json | 78 +++++++++++++++++++ core/collections/test/all.js | 2 +- core/collections/test/package.json | 2 +- core/collections/test/run-node.js | 2 +- core/collections/test/spec/array-spec.js | 6 +- core/collections/test/spec/clone-spec.js | 4 +- core/collections/test/spec/deque-fuzz.js | 4 +- core/collections/test/spec/deque-spec.js | 2 +- core/collections/test/spec/dict-spec.js | 2 +- core/collections/test/spec/fast-map-spec.js | 2 +- core/collections/test/spec/fast-set-spec.js | 6 +- core/collections/test/spec/heap-spec.js | 2 +- core/collections/test/spec/iterator-spec.js | 2 +- core/collections/test/spec/lfu-map-spec.js | 2 +- core/collections/test/spec/lfu-set-spec.js | 2 +- core/collections/test/spec/list-spec.js | 2 +- .../test/spec/listen/array-changes-spec.js | 2 +- .../test/spec/listen/property-changes-spec.js | 4 +- core/collections/test/spec/lru-map-spec.js | 2 +- core/collections/test/spec/lru-set-spec.js | 2 +- core/collections/test/spec/map-spec.js | 2 +- core/collections/test/spec/order.js | 2 +- core/collections/test/spec/regexp-spec.js | 2 +- core/collections/test/spec/set-spec.js | 2 +- core/collections/test/spec/set.js | 2 +- core/collections/test/spec/shim-array-spec.js | 2 +- .../test/spec/shim-functions-spec.js | 4 +- .../collections/test/spec/shim-object-spec.js | 21 ++++- .../test/spec/sorted-array-map-spec.js | 2 +- .../test/spec/sorted-array-set-spec.js | 2 +- .../test/spec/sorted-array-spec.js | 2 +- core/collections/test/spec/sorted-map-spec.js | 2 +- core/collections/test/spec/sorted-set-spec.js | 2 +- 33 files changed, 135 insertions(+), 42 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..7f0d9bacc2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,78 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceFolder}/services/shopify-admin-scripts/get-customers.js" + }, + { + "type": "node", + "request": "launch", + "name": "npm test", + "runtimeExecutable": "~/.nvm/versions/node/v12.16.1/bin/node", + "program": "${workspaceFolder}/test/run-node.js" + }, + { + "type": "node", + "request": "launch", + "name": "Run collections Jasmine Tests", + "runtimeExecutable": "node", + "program": "${workspaceFolder}/core/collections/test/run-node.js", + "args": [ "core/collections/test/spec", "--verbose"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "autoAttachChildProcesses": true, + "trace": false, + "enableContentValidation": false + }, + { + "type": "node", + "request": "launch", + "name": "Run frb Jasmine Tests", + "program": "${workspaceFolder}/node_modules/jasmine-node/bin/jasmine-node", + "args": [ "core/frb/spec", "--verbose"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "autoAttachChildProcesses": true, + "trace": false, + "enableContentValidation": false + }, + { + "type": "node", + "request": "launch", + "name": "npm test mr", + "runtimeExecutable": "~/.nvm/versions/node/v12.16.1/bin/node", + "program": "${workspaceFolder}/core/mr/test/run-node.js" + }, + { + "type": "node", + "request": "launch", + "name": "compile time zones", + "runtimeExecutable": "~/.nvm/versions/node/v12.16.1/bin/node", + "cwd": "${workspaceFolder}/core/date/tools/", + "program": "${workspaceFolder}/core/date/tools/compile-zones.js" + }, + { + "type": "node", + "request": "launch", + "name": "Run promise-io Jasmine Tests", + "runtimeExecutable": "~/.nvm/versions/node/v12.16.1/bin/node", + "autoAttachChildProcesses": true, + "program": "${workspaceFolder}/node_modules/jasmine-node/bin/jasmine-node", + "args": [ "${workspaceFolder}/core/promise-io/spec", "--verbose"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + + } + + + ] +} diff --git a/core/collections/test/all.js b/core/collections/test/all.js index cfd72a1eb1..dc59a8e711 100644 --- a/core/collections/test/all.js +++ b/core/collections/test/all.js @@ -32,4 +32,4 @@ module.exports = require("montage-testing").run(require, [ }, function (err) { console.log('montage-testing', 'Fail', err, err.stack); throw err; -}); \ No newline at end of file +}); diff --git a/core/collections/test/package.json b/core/collections/test/package.json index 8ea633af43..1d9a62819f 100644 --- a/core/collections/test/package.json +++ b/core/collections/test/package.json @@ -23,7 +23,7 @@ "collections": "../" }, "dependencies": { - "montage": "~16.0.x" + "montage": "file:../../.." }, "private": true } diff --git a/core/collections/test/run-node.js b/core/collections/test/run-node.js index 9007108832..d0732bdbde 100644 --- a/core/collections/test/run-node.js +++ b/core/collections/test/run-node.js @@ -38,7 +38,7 @@ jasmineEnv.addReporter({ }); // Execute -var mrRequire = require('mr/bootstrap-node'); +var mrRequire = require('../../mr/bootstrap-node'); var PATH = require("path"); mrRequire.loadPackage(PATH.join(__dirname, ".")).then(function (mr) { return mr.async("all"); diff --git a/core/collections/test/spec/array-spec.js b/core/collections/test/spec/array-spec.js index 7e91417b5a..305a686a64 100644 --- a/core/collections/test/spec/array-spec.js +++ b/core/collections/test/spec/array-spec.js @@ -1,6 +1,6 @@ -require("core/collections/shim"); -require("core/collections/listen/array-changes"); -var GenericCollection = require("core/collections/generic-collection"); +require("montage/core/collections/shim"); +require("montage/core/collections/listen/array-changes"); +var GenericCollection = require("montage/core/collections/generic-collection"); var describeDeque = require("./deque"); var describeCollection = require("./collection"); var describeOrder = require("./order"); diff --git a/core/collections/test/spec/clone-spec.js b/core/collections/test/spec/clone-spec.js index 539f7d6628..5f3718986a 100644 --- a/core/collections/test/spec/clone-spec.js +++ b/core/collections/test/spec/clone-spec.js @@ -1,6 +1,6 @@ -var Set = require("core/collections/set"); -var Map = require("core/collections/map"); +var Set = require("montage/core/collections/set"); +var Map = require("montage/core/collections/map"); describe("Clone-spec", function () { diff --git a/core/collections/test/spec/deque-fuzz.js b/core/collections/test/spec/deque-fuzz.js index 70c72d2b08..070d062ce2 100644 --- a/core/collections/test/spec/deque-fuzz.js +++ b/core/collections/test/spec/deque-fuzz.js @@ -1,6 +1,6 @@ -var Deque = require("core/collections/deque"); -require("core/collections/shim-array"); +var Deque = require("montage/core/collections/deque"); +require("montage/core/collections/shim-array"); var prng = require("./prng"); exports.fuzzDeque = fuzzDeque; diff --git a/core/collections/test/spec/deque-spec.js b/core/collections/test/spec/deque-spec.js index c9e182b363..17b41b9fcc 100644 --- a/core/collections/test/spec/deque-spec.js +++ b/core/collections/test/spec/deque-spec.js @@ -1,5 +1,5 @@ -var Deque = require("core/collections/deque"); +var Deque = require("montage/core/collections/deque"); var describeDeque = require("./deque"); var describeOrder = require("./order"); var describeToJson = require("./to-json"); diff --git a/core/collections/test/spec/dict-spec.js b/core/collections/test/spec/dict-spec.js index 613533da32..7fac8cb72b 100644 --- a/core/collections/test/spec/dict-spec.js +++ b/core/collections/test/spec/dict-spec.js @@ -1,5 +1,5 @@ -var Dict = require("core/collections/dict"); +var Dict = require("montage/core/collections/dict"); var describeDict = require("./dict"); var describeToJson = require("./to-json"); diff --git a/core/collections/test/spec/fast-map-spec.js b/core/collections/test/spec/fast-map-spec.js index 900c089b0a..f0358840c3 100644 --- a/core/collections/test/spec/fast-map-spec.js +++ b/core/collections/test/spec/fast-map-spec.js @@ -1,5 +1,5 @@ -var FastMap = require("core/collections/fast-map"); +var FastMap = require("montage/core/collections/fast-map"); var describeDict = require("./dict"); var describeMap = require("./map"); var describeToJson = require("./to-json"); diff --git a/core/collections/test/spec/fast-set-spec.js b/core/collections/test/spec/fast-set-spec.js index ca65f4cc30..1411dd1351 100644 --- a/core/collections/test/spec/fast-set-spec.js +++ b/core/collections/test/spec/fast-set-spec.js @@ -1,8 +1,8 @@ "use strict"; -var Set = require("core/collections/fast-set"); -var Iterator = require("core/collections/iterator"); -var TreeLog = require("core/collections/tree-log"); +var Set = require("montage/core/collections/fast-set"); +var Iterator = require("montage/core/collections/iterator"); +var TreeLog = require("montage/core/collections/tree-log"); var describeCollection = require("./collection"); var describeSet = require("./set"); diff --git a/core/collections/test/spec/heap-spec.js b/core/collections/test/spec/heap-spec.js index f8b3737590..c94ea764c0 100644 --- a/core/collections/test/spec/heap-spec.js +++ b/core/collections/test/spec/heap-spec.js @@ -1,5 +1,5 @@ -var Heap = require("core/collections/heap"); +var Heap = require("montage/core/collections/heap"); var permute = require("./permute"); var describeToJson = require("./to-json"); diff --git a/core/collections/test/spec/iterator-spec.js b/core/collections/test/spec/iterator-spec.js index 9e2d2d4e5b..7c0fcf0af1 100644 --- a/core/collections/test/spec/iterator-spec.js +++ b/core/collections/test/spec/iterator-spec.js @@ -1,5 +1,5 @@ -var Iterator = require("core/collections/iterator"); +var Iterator = require("montage/core/collections/iterator"); describe("Iterator-spec", function () { diff --git a/core/collections/test/spec/lfu-map-spec.js b/core/collections/test/spec/lfu-map-spec.js index a20a8ef855..614d3a41c5 100644 --- a/core/collections/test/spec/lfu-map-spec.js +++ b/core/collections/test/spec/lfu-map-spec.js @@ -1,5 +1,5 @@ -var LfuMap = require("core/collections/lfu-map"); +var LfuMap = require("montage/core/collections/lfu-map"); var describeDict = require("./dict"); var describeMap = require("./map"); var describeToJson = require("./to-json"); diff --git a/core/collections/test/spec/lfu-set-spec.js b/core/collections/test/spec/lfu-set-spec.js index b6eadb36a7..4843d29b85 100644 --- a/core/collections/test/spec/lfu-set-spec.js +++ b/core/collections/test/spec/lfu-set-spec.js @@ -1,4 +1,4 @@ -var LfuSet = require("core/collections/lfu-set"); +var LfuSet = require("montage/core/collections/lfu-set"); var describeCollection = require("./collection"); var describeSet = require("./set"); var describeToJson = require("./to-json"); diff --git a/core/collections/test/spec/list-spec.js b/core/collections/test/spec/list-spec.js index e4298f0957..97d4efdd3c 100644 --- a/core/collections/test/spec/list-spec.js +++ b/core/collections/test/spec/list-spec.js @@ -1,5 +1,5 @@ -var List = require("core/collections/list"); +var List = require("montage/core/collections/list"); var describeDeque = require("./deque"); var describeCollection = require("./collection"); var describeRangeChanges = require("./listen/range-changes"); diff --git a/core/collections/test/spec/listen/array-changes-spec.js b/core/collections/test/spec/listen/array-changes-spec.js index ca436845b1..47e655cec9 100644 --- a/core/collections/test/spec/listen/array-changes-spec.js +++ b/core/collections/test/spec/listen/array-changes-spec.js @@ -1,5 +1,5 @@ -require("core/collections/listen/array-changes"); +require("montage/core/collections/listen/array-changes"); var describeRangeChanges = require("./range-changes"); describe("Array change dispatch", function () { diff --git a/core/collections/test/spec/listen/property-changes-spec.js b/core/collections/test/spec/listen/property-changes-spec.js index b7250f4a8e..dc90991a94 100644 --- a/core/collections/test/spec/listen/property-changes-spec.js +++ b/core/collections/test/spec/listen/property-changes-spec.js @@ -5,8 +5,8 @@ https://github.com/motorola-mobility/montage/blob/master/LICENSE.md */ -require("core/collections/shim"); -var PropertyChanges = require("core/collections/listen/property-changes"); +require("montage/core/collections/shim"); +var PropertyChanges = require("montage/core/collections/listen/property-changes"); describe("PropertyChanges", function () { diff --git a/core/collections/test/spec/lru-map-spec.js b/core/collections/test/spec/lru-map-spec.js index 6c44238513..d6fe700a7b 100644 --- a/core/collections/test/spec/lru-map-spec.js +++ b/core/collections/test/spec/lru-map-spec.js @@ -1,5 +1,5 @@ -var LruMap = require("core/collections/lru-map"); +var LruMap = require("montage/core/collections/lru-map"); var describeDict = require("./dict"); var describeMap = require("./map"); var describeToJson = require("./to-json"); diff --git a/core/collections/test/spec/lru-set-spec.js b/core/collections/test/spec/lru-set-spec.js index 6560f74ed1..5dd864a39b 100644 --- a/core/collections/test/spec/lru-set-spec.js +++ b/core/collections/test/spec/lru-set-spec.js @@ -1,5 +1,5 @@ -var LruSet = require("core/collections/lru-set"); +var LruSet = require("montage/core/collections/lru-set"); var describeCollection = require("./collection"); var describeSet = require("./set"); var describeToJson = require("./to-json"); diff --git a/core/collections/test/spec/map-spec.js b/core/collections/test/spec/map-spec.js index 19c1ca9d19..fd19e5d480 100644 --- a/core/collections/test/spec/map-spec.js +++ b/core/collections/test/spec/map-spec.js @@ -1,6 +1,6 @@ // TODO test insertion order -var Map = require("core/collections/map"); +var Map = require("montage/core/collections/map"); var describeDict = require("./dict"); var describeMap = require("./map"); var describeMapChanges = require("./listen/map-changes"); diff --git a/core/collections/test/spec/order.js b/core/collections/test/spec/order.js index 3af918593b..4a324444f9 100644 --- a/core/collections/test/spec/order.js +++ b/core/collections/test/spec/order.js @@ -1,5 +1,5 @@ -var GenericCollection = require("core/collections/generic-collection"); +var GenericCollection = require("montage/core/collections/generic-collection"); module.exports = describeOrder; function describeOrder(Collection) { diff --git a/core/collections/test/spec/regexp-spec.js b/core/collections/test/spec/regexp-spec.js index aa9230e894..5737ced76b 100644 --- a/core/collections/test/spec/regexp-spec.js +++ b/core/collections/test/spec/regexp-spec.js @@ -1,5 +1,5 @@ -require("core/collections/shim-regexp"); +require("montage/core/collections/shim-regexp"); describe("RegExp-spec", function () { describe("escape", function () { diff --git a/core/collections/test/spec/set-spec.js b/core/collections/test/spec/set-spec.js index c4b4c91fa9..5e0f852cb7 100644 --- a/core/collections/test/spec/set-spec.js +++ b/core/collections/test/spec/set-spec.js @@ -1,5 +1,5 @@ -var Set = require("core/collections/set"); +var Set = require("montage/core/collections/set"); var describeCollection = require("./collection"); var describeSet = require("./set"); diff --git a/core/collections/test/spec/set.js b/core/collections/test/spec/set.js index 72e7db625d..23d9790fd3 100644 --- a/core/collections/test/spec/set.js +++ b/core/collections/test/spec/set.js @@ -1,5 +1,5 @@ -var Iterator = require("core/collections/iterator"); +var Iterator = require("montage/core/collections/iterator"); module.exports = describeSet; function describeSet(Set, sorted) { diff --git a/core/collections/test/spec/shim-array-spec.js b/core/collections/test/spec/shim-array-spec.js index 2dd87cd838..a4a6971e05 100644 --- a/core/collections/test/spec/shim-array-spec.js +++ b/core/collections/test/spec/shim-array-spec.js @@ -1,5 +1,5 @@ -require("core/collections/shim-array"); +require("montage/core/collections/shim-array"); describe("ArrayShim-spec", function () { diff --git a/core/collections/test/spec/shim-functions-spec.js b/core/collections/test/spec/shim-functions-spec.js index 00c8b56384..d154170793 100644 --- a/core/collections/test/spec/shim-functions-spec.js +++ b/core/collections/test/spec/shim-functions-spec.js @@ -1,6 +1,6 @@ -require("core/collections/shim-object"); -require("core/collections/shim-function"); +require("montage/core/collections/shim-object"); +require("montage/core/collections/shim-function"); describe("FunctionShim-spec", function () { diff --git a/core/collections/test/spec/shim-object-spec.js b/core/collections/test/spec/shim-object-spec.js index 3b78a0a746..e9bc1d3f64 100644 --- a/core/collections/test/spec/shim-object-spec.js +++ b/core/collections/test/spec/shim-object-spec.js @@ -7,9 +7,9 @@ https://github.com/motorola-mobility/montage/blob/master/LICENSE.md */ -require("core/collections/shim"); -var Dict = require("core/collections/dict"); -var Set = require("core/collections/set"); +require("montage/core/collections/shim"); +var Dict = require("montage/core/collections/dict"); +var Set = require("montage/core/collections/set"); describe("ObjectShim-spec", function () { @@ -395,6 +395,21 @@ describe("ObjectShim-spec", function () { }); }); + var a = { + "a": "a", + "b": "b", + "c": "c", + "d": "d" + }, + b = { + "d": "d", + "c": "c", + "b": "b", + "a": "a" + }; + expect(Object.equals(a, b)).toBe(true); + + }); describe("compare", function () { diff --git a/core/collections/test/spec/sorted-array-map-spec.js b/core/collections/test/spec/sorted-array-map-spec.js index 09d3db9a6c..31169c85a6 100644 --- a/core/collections/test/spec/sorted-array-map-spec.js +++ b/core/collections/test/spec/sorted-array-map-spec.js @@ -1,5 +1,5 @@ -var SortedArrayMap = require("core/collections/sorted-array-map"); +var SortedArrayMap = require("montage/core/collections/sorted-array-map"); var describeDict = require("./dict"); var describeMap = require("./map"); var describeMapChanges = require("./listen/map-changes"); diff --git a/core/collections/test/spec/sorted-array-set-spec.js b/core/collections/test/spec/sorted-array-set-spec.js index f98362fdbc..e49844fc1d 100644 --- a/core/collections/test/spec/sorted-array-set-spec.js +++ b/core/collections/test/spec/sorted-array-set-spec.js @@ -1,5 +1,5 @@ -var SortedArraySet = require("core/collections/sorted-array-set"); +var SortedArraySet = require("montage/core/collections/sorted-array-set"); var describeDeque = require("./deque"); var describeCollection = require("./collection"); var describeSet = require("./set"); diff --git a/core/collections/test/spec/sorted-array-spec.js b/core/collections/test/spec/sorted-array-spec.js index b79bddc265..18221d608c 100644 --- a/core/collections/test/spec/sorted-array-spec.js +++ b/core/collections/test/spec/sorted-array-spec.js @@ -1,5 +1,5 @@ -var SortedArray = require("core/collections/sorted-array"); +var SortedArray = require("montage/core/collections/sorted-array"); var describeCollection = require("./collection"); var describeDeque = require("./deque"); var describeOrder = require("./order"); diff --git a/core/collections/test/spec/sorted-map-spec.js b/core/collections/test/spec/sorted-map-spec.js index 2c6b8626db..a0037bbcf9 100644 --- a/core/collections/test/spec/sorted-map-spec.js +++ b/core/collections/test/spec/sorted-map-spec.js @@ -1,5 +1,5 @@ -var SortedMap = require("core/collections/sorted-map"); +var SortedMap = require("montage/core/collections/sorted-map"); var describeDict = require("./dict"); var describeToJson = require("./to-json"); diff --git a/core/collections/test/spec/sorted-set-spec.js b/core/collections/test/spec/sorted-set-spec.js index a6b7c8146c..ebd7efc548 100644 --- a/core/collections/test/spec/sorted-set-spec.js +++ b/core/collections/test/spec/sorted-set-spec.js @@ -1,5 +1,5 @@ -require("core/collections/shim-array"); +require("montage/core/collections/shim-array"); var SortedSet = require("../../collections/sorted-set"); var TreeLog = require("../../collections/tree-log"); var describeDeque = require("./deque"); From d843310b25133f97c0b7ea93e94b10829f5bc211 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 21 Sep 2020 17:48:10 -0700 Subject: [PATCH 249/407] - add first equals() implementation - not super tested - fix bug in syntaxByAliasingSyntaxWithParameters - adds ability to change parameters after creation --- core/criteria.js | 182 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 140 insertions(+), 42 deletions(-) diff --git a/core/criteria.js b/core/criteria.js index aedb3d4616..e45fc18649 100644 --- a/core/criteria.js +++ b/core/criteria.js @@ -31,9 +31,21 @@ var Criteria = exports.Criteria = Montage.specialize({ /** * @type {object} */ - parameters: { + _parameters: { value: null }, + parameters: { + get: function() { + return this._parameters; + }, + set: function(value) { + debugger; + if(value !== this._parameters) { + this._parameters = value; + } + } + }, + /** * @private * @type {object} @@ -172,6 +184,38 @@ var Criteria = exports.Criteria = Montage.specialize({ } } }, + + equals: { + value: function (otherCriteria) { + if(this._expression && otherCriteria._expression) { + if( + (this._expression === otherCriteria._expression) && + Object.equals(this.parameters, otherCriteria.parameters) + ) { + return true; + } else { + return false; + } + } else if(this._syntax && otherCriteria._syntax) { + if( + Object.equals(this._syntax, otherCriteria._syntax) && + Object.equals(this.parameters, otherCriteria.parameters) + ) { + return true; + } else { + return false; + } + } else if( + //This will force an eventual stringification of a syntax + (this.expression === otherCriteria.expression) && + Object.equals(this.parameters, otherCriteria.parameters) + ) { + return true; + } else { + return false; + } + } + }, __scope: { value: null }, @@ -241,6 +285,54 @@ var Criteria = exports.Criteria = Montage.specialize({ * @returns {Criteria} - The Criteria initialized. */ + __syntaxByAliasingSyntaxWithParameters: { + value: function (aliasedSyntax, parameterArg, parameterArgIndex, otherArg, otherArgIndex, aliasedParameters, parameterCounter, _thisParameters) { + var aliasedParameter; + + if (otherArg.type !== "literal") { + + //We replace $ syntax by the $key/$.key syntax: + aliasedSyntaxparameterArg = {}; + aliasedSyntaxparameterArg.type = "property"; + aliasedParameter = "parameter"+(++parameterCounter); + aliasedSyntaxparameterArg.args = [ + { + "type":"parameters" + }, + { + "type":"literal", + "value": aliasedParameter + } + ]; + aliasedSyntax.args[parameterArgIndex] = aliasedSyntaxparameterArg; + aliasedSyntax.args[otherArgIndex] = this._syntaxByAliasingSyntaxWithParameters(otherArg, aliasedParameters, parameterCounter, _thisParameters); + + //and we register the criteria's parameter _thisParameters under the new key; + aliasedParameters[aliasedParameter] = _thisParameters; + } else { + //We need to make sure there's no conflict with aliasedParameters + parameter = otherArg.value; + parameterValue = _thisParameters[parameter]; + if(aliasedParameters.hasOwnProperty(parameter) && aliasedParameters[parameter] !== parameterValue) { + aliasedParameter = parameter+(++parameterCounter); + aliasedParameters[aliasedParameter] = parameterValue; + } else { + aliasedParameter = parameter; + } + aliasedSyntax.args[parameterArgIndex] = { + "type":"parameters" + }; + + aliasedSyntax.args[otherArgIndex] = { + "type":"literal", + "value":aliasedParameter + }; + aliasedParameters[aliasedParameter] = parameterValue; + } + + } + }, + _syntaxByAliasingSyntaxWithParameters: { value: function (syntax, aliasedParameters, parameterCounter, _thisParameters) { var aliasedSyntax = {}, @@ -261,46 +353,52 @@ var Criteria = exports.Criteria = Montage.specialize({ aliasedSyntax.args = []; if(syntaxArg0.type === "parameters") { - if (syntaxArg1.type !== "literal") { - - //We replace $ syntax by the $key/$.key syntax: - aliasedSyntaxArg0 = {}; - aliasedSyntaxArg0.type = "property"; - aliasedParameter = "parameter"+(++parameterCounter); - aliasedSyntaxArg0.args = [ - { - "type":"parameters" - }, - { - "type":"literal", - "value": aliasedParameter - } - ]; - aliasedSyntax.args[0] = aliasedSyntaxArg0; - aliasedSyntax.args[1] = this._syntaxByAliasingSyntaxWithParameters(syntaxArg1, aliasedParameters, parameterCounter, _thisParameters); - - //and we register the criteria's parameter _thisParameters under the new key; - aliasedParameters[aliasedParameter] = _thisParameters; - } else { - //We need to make sure there's no conflict with aliasedParameters - parameter = syntaxArg1.value; - parameterValue = _thisParameters[parameter]; - if(aliasedParameters.hasOwnProperty(parameter) && aliasedParameters[parameter] !== parameterValue) { - aliasedParameter = parameter+(++parameterCounter); - aliasedParameters[aliasedParameter] = parameterValue; - } else { - aliasedParameter = parameter; - } - aliasedSyntax.args[0] = { - "type":"parameters" - }; - - aliasedSyntax.args[1] = { - "type":"literal", - "value":aliasedParameter - }; - aliasedParameters[aliasedParameter] = parameterValue; - } + this.__syntaxByAliasingSyntaxWithParameters(aliasedSyntax, syntaxArg0, 0, syntaxArg1, 1, aliasedParameters, parameterCounter, _thisParameters); + + // if (syntaxArg1.type !== "literal") { + + // //We replace $ syntax by the $key/$.key syntax: + // aliasedSyntaxArg0 = {}; + // aliasedSyntaxArg0.type = "property"; + // aliasedParameter = "parameter"+(++parameterCounter); + // aliasedSyntaxArg0.args = [ + // { + // "type":"parameters" + // }, + // { + // "type":"literal", + // "value": aliasedParameter + // } + // ]; + // aliasedSyntax.args[0] = aliasedSyntaxArg0; + // aliasedSyntax.args[1] = this._syntaxByAliasingSyntaxWithParameters(syntaxArg1, aliasedParameters, parameterCounter, _thisParameters); + + // //and we register the criteria's parameter _thisParameters under the new key; + // aliasedParameters[aliasedParameter] = _thisParameters; + // } else { + // //We need to make sure there's no conflict with aliasedParameters + // parameter = syntaxArg1.value; + // parameterValue = _thisParameters[parameter]; + // if(aliasedParameters.hasOwnProperty(parameter) && aliasedParameters[parameter] !== parameterValue) { + // aliasedParameter = parameter+(++parameterCounter); + // aliasedParameters[aliasedParameter] = parameterValue; + // } else { + // aliasedParameter = parameter; + // } + // aliasedSyntax.args[0] = { + // "type":"parameters" + // }; + + // aliasedSyntax.args[1] = { + // "type":"literal", + // "value":aliasedParameter + // }; + // aliasedParameters[aliasedParameter] = parameterValue; + // } + + } + else if(syntaxArg1.type === "parameters") { + this.__syntaxByAliasingSyntaxWithParameters(aliasedSyntax, syntaxArg1, 1, syntaxArg0, 0, aliasedParameters, parameterCounter, _thisParameters); } else { aliasedSyntax.args[0] = this._syntaxByAliasingSyntaxWithParameters(syntaxArg0, aliasedParameters, parameterCounter, _thisParameters); @@ -374,7 +472,7 @@ var Criteria = exports.Criteria = Montage.specialize({ syntaxByAliasingSyntaxWithParameters: { value: function (aliasedParameters, parameterCounters) { - return this._syntaxByAliasingSyntaxWithParameters(this.syntax, aliasedParameters||0, 0, this.parameters); + return this._syntaxByAliasingSyntaxWithParameters(this.syntax, aliasedParameters, parameterCounters||0, this.parameters); } } From 25c3ef6d11c5afebc3cb18566d70ad93242d9e96 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 21 Sep 2020 17:52:14 -0700 Subject: [PATCH 250/407] - improve support for template to load script tags in their template --- core/environment.js | 35 ++++++++++ core/template.js | 165 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 194 insertions(+), 6 deletions(-) diff --git a/core/environment.js b/core/environment.js index b064595d42..085eac42ba 100644 --- a/core/environment.js +++ b/core/environment.js @@ -67,6 +67,41 @@ var Environment = exports.Environment = Montage.specialize({ } }, + _supportsLinkRel: { + value: function _supportsLinkRel(feature){ + var tokenList; + var fakeLink = document.createElement('link'); + try { + if(fakeLink.relList && _.isFunction(fakeLink.relList.supports)){ + return fakeLink.relList.supports(feature); + } + } catch(err){ + return false; + } + } + }, + + _supportsLinkPrefetch: { + value: undefined + }, + supportsLinkPrefetch: { + value: function() { + return typeof this._supportsLinkPrefetch === "boolean" + ? this._supportsLinkPrefetch + : (this._supportsLinkPrefetch = (this.isBrowser && this._supportsLinkRel('prefetch'))); + } + }, + _supportsLinkPreload: { + value: undefined + }, + supportsLinkPreload: { + value: function() { + return typeof this._supportsLinkPreload === "boolean" + ? this._supportsLinkPreload + : (this._supportsLinkPreload = (this.isBrowser && this._supportsLinkRel('preload'))); + } + }, + _supportsPointerEvents: { value: null }, diff --git a/core/template.js b/core/template.js index 21ae211b97..8842d570be 100644 --- a/core/template.js +++ b/core/template.js @@ -8,6 +8,7 @@ var Montage = require("./core").Montage, URL = require("./mini-url"), logger = require("./logger").logger("template"), defaultEventManager = require("./event/event-manager").defaultEventManager, + currentEnvironment = require("./environment").currentEnvironment, defaultApplication; /** @@ -1451,21 +1452,173 @@ var TemplateResources = Montage.specialize( /** @lends TemplateResources# */ { } }, + /* + uses + + Similar to what's done in document-resources, and using it, but doing it here with links so it works cross origin + */ + + preloadScript: { + value: function (script, targetDocument) { + var documentResources = DocumentResources.getInstanceForDocument(targetDocument); + + if(documentResources.isResourcePreloaded(script.src)) { + return Promise.resolve(script.src) + } else if (documentResources.isResourcePreloading(script.src)) { + return documentResources.getResourcePreloadedPromise(script.src); + } else { + + var promise = new Promise(function(resolve, reject){ + + var preloadLink = document.createElement("link"); + preloadLink.href = script.src; + preloadLink.rel = "preload"; + preloadLink.as = "script"; + preloadLink.type = "text/javascript"; + preloadLink.crossOrigin = true; + targetDocument.head.appendChild(preloadLink); + + var loadingTimeout; + // We wait until all scripts are loaded, this is important + // because templateDidLoad might need to access objects that + // are defined in these scripts, the downsize is that it takes + // more time for the template to be considered loaded. + var scriptLoadedFunction = function scriptLoaded(event) { + documentResources.setResourcePreloaded(script.src); + preloadLink.removeEventListener("load", scriptLoaded, false); + preloadLink.removeEventListener("error", scriptLoaded, false); + + clearTimeout(loadingTimeout); + resolve(event); + }; + + preloadLink.addEventListener("load", scriptLoadedFunction, false); + preloadLink.addEventListener("error", scriptLoadedFunction, false); + + // Setup the timeout to wait for the script until the resource + // is considered loaded. The template doesn't fail loading just + // because a single script didn't load. + //Benoit: It is odd that we act as if everything was fine here... + loadingTimeout = setTimeout(function () { + documentResources.setResourcePreloaded(script.src); + resolve(); + }, documentResources._SCRIPT_TIMEOUT); + + + }); + + documentResources.setResourcePreloadedPromise(script.src, promise); + + return promise; + + } + + } + }, + + _loadSyncScripts: { + value: function (script, targetDocument, syncScriptPromises) { + var self = this, + previousPromise = syncScriptPromises[syncScriptPromises.length-1] || Promise.resolve(true); + + if(false) { + //if(currentEnvironment.supportsLinkPreload) { + + //Preload, + //require.read used XHR which doesn't work without CORS cooperation from server + // syncScriptPromises.push(require.read(script.src) + // .then( function() { + // return previousPromise + // .then( function() { + // self.loadScript(script, targetDocument); + // }, function(sriptsWithSrcLoadError) { + // return Promise.reject(sriptsWithSrcLoadError); + // }); + // }, function(sriptsWithSrcLoadError) { + // return Promise.reject(sriptsWithSrcLoadError); + // })); + + syncScriptPromises.push(this.preloadScript(script, targetDocument) + .then( function() { + return previousPromise + .then( function() { + self.loadScript(script, targetDocument); + }, function(sriptsWithSrcLoadError) { + return Promise.reject(sriptsWithSrcLoadError); + }); + }, function(sriptsWithSrcLoadError) { + return Promise.reject(sriptsWithSrcLoadError); + })); + + + } else { + //We have no choice but load/execute serially to respect the spec. + syncScriptPromises.push(previousPromise + .then( function() { + return self.loadScript(script, targetDocument); + }, function(sriptsWithSrcLoadError) { + return Promise.reject(sriptsWithSrcLoadError); + }) + ); + + } + + + } + }, loadScripts: { value: function (targetDocument) { var scripts = this.getScripts(), - ii = scripts.length; + iScript, + ii = scripts.length, + self = this; if (ii) { - var promises = []; + var promises, + asyncPromises, + syncScriptPromises, + inlineScripts = []; for (var i = 0; i < ii; i++) { - promises.push( - this.loadScript(scripts[i], targetDocument) - ); + if((iScript = scripts[i]).src) { + //If async, we load & insert at once + if(iScript.async) { + (asyncPromises || (asyncPromises = [])).push( + this.loadScript(iScript, targetDocument) + ); + } else { + this._loadSyncScripts(iScript, targetDocument, (syncScriptPromises || (syncScriptPromises = []))); + } + } else { + inlineScripts.push(iScript); + self = self || this; + } + } + + if(asyncPromises && syncScriptPromises) { + promises = asyncPromises.concat(syncScriptPromises); + } else { + promises = asyncPromises || syncScriptPromises; } - return Promise.all(promises); + return Promise.all(promises) + .then( + function(resolvedScriptsWithSrc) { + var inlineScriptsPromises = []; + + for (i = 0, ii=inlineScripts.length; i < ii; i++) { + inlineScriptsPromises.push( + self.loadScript(inlineScripts[i], targetDocument) + ); + } + + return Promise.all(inlineScriptsPromises); + + }, + function(sriptsWithSrcLoadError) { + return sriptsWithSrcLoadError; + } + ); } return this.resolvedPromise; From abb71928efeeaa5495d6007fa06c954f95a56561 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 21 Sep 2020 17:52:36 -0700 Subject: [PATCH 251/407] fix bug in _buildLanguageRegion --- core/locale.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/locale.js b/core/locale.js index 989b65ba3f..9c98dd0b1b 100644 --- a/core/locale.js +++ b/core/locale.js @@ -217,7 +217,7 @@ var Locale = exports.Locale = Montage.specialize({ //and last for country var split = this.identifier.split("-"); this._language = split[0]; - this._region = split[split.length-1] || "*"; + this._region = split.length > 1 ? split[split.length-1] : "*"; } }, /** From 3141902eb149613390c233230380ec6a2125b9ec Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 21 Sep 2020 18:00:33 -0700 Subject: [PATCH 252/407] add singletons for Promise resolve of null, undefined, true and falde --- core/promise.js | 51 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/core/promise.js b/core/promise.js index 2dd545812a..9af9b52c5f 100644 --- a/core/promise.js +++ b/core/promise.js @@ -25,4 +25,53 @@ if (Promise.prototype.hasOwnProperty('finally') === false) { }; } -exports.Promise = Promise; \ No newline at end of file +if(!Promise.resolveNull) { + /** + * A shared promise resolved with a value of null + * + * @type {external:Promise} + */ + Object.defineProperty(Promise, "resolveNull", { + value: Promise.resolve(null), + enumerable: false + }); +} + +if(!Promise.resolveUndefined) { + /** + * A shared promise resolved with a value of undefined + * + * @type {external:Promise} + */ + Object.defineProperty(Promise, "resolveUndefined", { + value: Promise.resolve(undefined), + enumerable: false + }); +} + +if(!Promise.resolveTrue) { + /** + * A shared promise resolved with a value of undefined + * + * @type {external:Promise} + */ + Object.defineProperty(Promise, "resolveTrue", { + value: Promise.resolve(true), + enumerable: false + }); +} + +if(!Promise.resolveFalse) { + /** + * A shared promise resolved with a value of undefined + * + * @type {external:Promise} + */ + Object.defineProperty(Promise, "resolveFalse", { + value: Promise.resolve(false), + enumerable: false + }); +} + + +exports.Promise = Promise; From 4a76e3678e96343ed379eed86f77414ae55ec147 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 21 Sep 2020 18:29:44 -0700 Subject: [PATCH 253/407] add fullDayTimeRangeFromDate class method --- core/range.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/range.js b/core/range.js index fa6ae86c15..63fd3fdfba 100644 --- a/core/range.js +++ b/core/range.js @@ -151,3 +151,21 @@ Object.defineProperty(Range.prototype,"length", { } } }); + +/** + * Creates a new range representing the full 24 hours of the date passed as an argument + * from midnight/0h to 23:59:59:999 of the day of the date passed as argument. + * + * @function + * @param {Date} date The date to build the range on. + * + * @returns {Range} a new Range instance. + */ + +Range.fullDayTimeRangeFromDate = function(date) { + var dayStart = new Date(date); + dayStart.setHours(0,0,0,0); + var dayEnd = new Date(dayStart); + dayEnd.setHours(23,59,59,999); + return new Range(dayStart,dayEnd); +}; From 1a6d04e5da1409d50685871af1aa36fd65b9d2b5 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 21 Sep 2020 18:30:32 -0700 Subject: [PATCH 254/407] add join method to Set, similar to Array's one --- core/collections/generic-set.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/collections/generic-set.js b/core/collections/generic-set.js index 8b3cdf32ec..aca759c864 100644 --- a/core/collections/generic-set.js +++ b/core/collections/generic-set.js @@ -79,6 +79,22 @@ GenericSet.prototype.toggle = function (value) { } }; +GenericSet.prototype.join = function (separator) { + if (this.size) { + var setIterator = this.values(), aValue, aNextValue, joinValue = ""; + while((aValue = setIterator.next().value) && (aNextValue = setIterator.next().value)) { + joinValue += aValue; + joinValue += separator; + joinValue += aNextValue; + } + joinValue += aValue; + return joinValue; + } else { + return ""; + } +}; + + var _valuesArrayFunction = function(value,key) {return value;}; GenericSet.prototype.valuesArray = function() { return this.map(_valuesArrayFunction); From 1cd51ab6ca56732ac621bf729b34a10cce464ed3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 21 Sep 2020 18:32:44 -0700 Subject: [PATCH 255/407] add inversePropertyDescriptor method --- core/meta/property-descriptor.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index 249a693651..5cc957db18 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -598,6 +598,29 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# value: undefined }, + + inversePropertyDescriptor: { + get: function() { + var self = this; + + return this.inversePropertyName + ? this.valueDescriptor.then(function (objectDescriptor) { + return self._inversePropertyDescriptor; + }) + : Promise.resolveUndefined; + + } + }, + + _inversePropertyDescriptor: { + get: function() { + return (this.inversePropertyName && this._valueDescriptorReference) + ? this._valueDescriptorReference.propertyDescriptorForName(this.inversePropertyName) + : undefined; + } + }, + + /******************************************************** * Deprecated functions */ From 8f8a2f117d001a46be8661d33c3be52fb4a4adfe Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 21 Sep 2020 18:34:08 -0700 Subject: [PATCH 256/407] override mime type to javascript only if file extension is js, needed if require used for otehr files --- core/mr/browser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/mr/browser.js b/core/mr/browser.js index d6a28d5284..ce1bc5fa22 100644 --- a/core/mr/browser.js +++ b/core/mr/browser.js @@ -184,7 +184,7 @@ bootstrap("require/browser", function (require) { if (!xhr) { xhr = new RequireRead.XMLHttpRequest(); - if (xhr.overrideMimeType) { + if (xhr.overrideMimeType && url.endsWith(".js")) { xhr.overrideMimeType(APPLICATION_JAVASCRIPT_MIMETYPE); } xhr.onload = RequireRead.onload; From ca5b86a8c69871a31d81098f4453bd5cc6578acc Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 21 Sep 2020 18:35:15 -0700 Subject: [PATCH 257/407] add first implementation of equals() method --- data/model/data-query.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/data/model/data-query.js b/data/model/data-query.js index bf155bd701..e76bacf386 100644 --- a/data/model/data-query.js +++ b/data/model/data-query.js @@ -83,6 +83,28 @@ exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { } }, + equals: { + value: function (otherQuery) { + /* + take care of the ones where we can use === first + + not including selectBindings for now, nor selectExpression as we need to see if we'll keep it + + */ + if( + (this.type === otherQuery.type) && + (this.fetchLimit === otherQuery.fetchLimit) && + (this.readExpressions && this.readExpressions.equals(otherQuery.readExpressions)) && + (this.orderings && this.orderings.equals(otherQuery.orderings)) && + (this.criteria && this.criteria.equals(otherQuery.criteria)) + ) { + return true; + } else { + return false; + } + } + }, + /** * The type of the data object to retrieve. * From 5be7a9c5f1890844c3cf3195a76e287857be2cab Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 21 Sep 2020 18:37:36 -0700 Subject: [PATCH 258/407] - fix mising ; --- data/service/data-stream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/data-stream.js b/data/service/data-stream.js index 1706fa5e1a..417f51244d 100644 --- a/data/service/data-stream.js +++ b/data/service/data-stream.js @@ -71,7 +71,7 @@ DataStream = exports.DataStream = DataProvider.specialize(/** @lends DataStream. value: value.type, enumerable: false, configurable: true - }) + }); // this.data.objectDescriptor = value.type; } } From 9a77df184ac0be07432411b201f6ae36aa805793 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 21 Sep 2020 18:49:18 -0700 Subject: [PATCH 259/407] - add automatic propagation of value to inverse properties - add ability to support mapping of polymorphic relationships when multiple raw data properties are used to hold each possible destination type's foreign key - add ability for frameworks/apps to use a subclass of DataTrigger - add the ability for data converters to know which rule they act on when executing convert() or revert() --- .../raw-foreign-value-to-object-converter.js | 190 +++++++- .../raw-value-to-object-converter.js | 8 + data/service/data-service.js | 445 +++++++++++++++++- data/service/data-trigger.js | 41 +- data/service/expression-data-mapping.js | 381 +++++++++++++-- data/service/mapping-rule.js | 106 +++-- data/service/raw-data-service.js | 62 ++- data/service/raw-data-type-mapping.js | 29 +- 8 files changed, 1147 insertions(+), 115 deletions(-) diff --git a/data/converter/raw-foreign-value-to-object-converter.js b/data/converter/raw-foreign-value-to-object-converter.js index 7439a1ab5c..99d76b29f0 100644 --- a/data/converter/raw-foreign-value-to-object-converter.js +++ b/data/converter/raw-foreign-value-to-object-converter.js @@ -1,6 +1,7 @@ var RawValueToObjectConverter = require("./raw-value-to-object-converter").RawValueToObjectConverter, Criteria = require("core/criteria").Criteria, - DataQuery = require("data/model/data-query").DataQuery, + DataQuery = require("data/model/data-query").DataQuery, + Map = require("core/collections/map").Map, Promise = require("core/promise").Promise; /** * @class RawForeignValueToObjectConverter @@ -79,9 +80,91 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( criteria.parameters.serviceIdentifier = this.serviceIdentifier; } - var query = DataQuery.withTypeAndCriteria(typeToFetch, criteria); + var query = DataQuery.withTypeAndCriteria(typeToFetch, criteria), + self = this; return this.service ? this.service.then(function (service) { + /* + When we fetch objects that have inverse relationships on each others none can complete their mapRawDataProcess because the first one's promise for mapping the relationship to the second never commpletes because the second one itself has it's raw data the foreignKey to the first and attemps to do so by default on processing operations, where the previous way was only looping on requisite proprties. If both relationships were requisite, on each side we'd end up with the same problem. + + When the second try to map it's foreignKey relationship back to the first, the first exists, and is being mapped, which we can know by checking: + if(!this.service._objectsBeingMapped.has(object)) {} + + So let's try to find a local object that we may already have. This is a specific converter to resolve foreign keys, but we should be able to run the criteria on all local instances' snapshots. We don't have right now an indexation of the snapshots by type, just by dataIdentifier. + + However, we could start by identifying if the criteria's property involves the typeToFetch's primary key. + + We also now know currentRule = this.currentRule; + + Quick draft bellow, un-tested to be refined and continued to. + + One more thought that's been on my mind. We want to leverage indexedDB anyway so the app has data offline as needed, or to be able to do edge machine learning or keep private data there. If we need to build an index to find objects known client side, we might be able to kill 2 birds with one stone to look for them in the indexedDB directly, where we wou;d build index to match foreign relationships etc... + */ + + /* + + var criteria = query.criteria; + + if(criteria.syntax.type === "equals") { + var args = criteria.syntax.args, + parameters = criteria.parameters, + parameterValue, + propertySyntax; + + // propertySyntax = args[0].type === "property" + // ? args[0] + // : args[1].type === "property" + // ? args[1] + // : null; + if(args[0].type === "property") { + if(args[1].type === "parameters") { + //parameterSyntax = args[1]; + parameterValue = parameters; + propertySyntax = args[0]; + } else if(args[1].type === "property") { + if(args[1].args[0].type === "parameters") { + parameterValue = parameters[args[1].args[1].value]; + propertySyntax = args[0]; + } else { + parameterValue = parameters[args[0].args[1].value]; + propertySyntax = args[1]; + } + } + } else if(args[1].type === "property") { + if(args[0].type === "parameters") { + //parameterSyntax = args[1]; + parameterValue = parameters; + propertySyntax = args[1]; + } else if(args[0].type === "property") { + if(args[0].args[0].type === "parameters") { + parameterValue = parameters[args[0].args[1].value]; + propertySyntax = args[1]; + } else { + parameterValue = parameters[args[1].args[1].value]; + propertySyntax = args[0]; + } + } + } + + if(propertySyntax) { + var propertyArgs = propertySyntax.args, + propertyName = propertyArgs[0].type === "literal" + ? propertyArgs[0].value + : propertyArgs[1].type === "literal" + ? propertyArgs[1].value + : null; + + if(propertyName && self._owner.rawDataPrimaryKeys.indexOf(propertyName) !== -1) { + //Our criteria is about a primary key, let's find the value: + var primaryKeyValue = parameterValue; + + } + } + } + + */ + + return service.rootService.fetchData(query); }) : null; @@ -122,6 +205,31 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( } }, + __foreignDescriptorMappingsByObjectyDescriptor: { + value: undefined + }, + _foreignDescriptorMappingsByObjectyDescriptor: { + get: function() { + if(!this.__foreignDescriptorMappingsByObjectyDescriptor) { + for(var i=0, mappings = this.foreignDescriptorMappings, countI = mappings.length, iMapping, mappingByObjectDescriptor = new Map();(i 1) { + /* + value needs to be added to the other's side: + */ + inverseValue = value[inversePropertyName]; + if(inverseValue) { + /* + We might be looping back, but in any case, we shouldn't add the same object again, so we need to check if it is there. I really don't like doinf indexOf() here, but it's not a set... + */ + if(inverseValue.indexOf(dataObject) === -1) { + inverseValue.push(dataObject) + } + } else { + //No existing array so we create one on the fly + value[inversePropertyName] = [dataObject]; + } + } else { + /* + A 1-1 then. Let's not set if it's the same... + */ + if(value[inversePropertyName] !== dataObject) { + value[inversePropertyName] = dataObject; + } + } + } + + if(previousValue) { + if(inversePropertyCardinality > 1) { + /* + previousValue needs to be removed from the other's side: + */ + inverseValue = previousValue[inversePropertyName]; + if(inverseValue) { + /* + Assuming it only exists once in the array as it should... + */ + inverseValue.delete(dataObject) + } + // else { + // //No existing array so nothing to do.... + // } + } else { + //A 1-1 then + previousValue[inversePropertyName] = null; + } + + } + + } else if(propertyDescriptor.cardinality > 1) { + //value should be an array + if(!Array.isArray(value)) { + console.warn("Something's off...., the value of propertyDescriptor:",propertyDescriptor, " of data object:",dataObject," should be an array"); + } + + this._addDataObjectPropertyDescriptorValuesForInversePropertyDescriptor(dataObject, propertyDescriptor, value, inversePropertyDescriptor); + + if(previousValue) { + this._removeDataObjectPropertyDescriptorValuesForInversePropertyDescriptor(dataObject, propertyDescriptor, previousValue, inversePropertyDescriptor); + } + // for(var i=0, countI = value.length, iValue; (i 1) { + // //many to many: + // //value needs to be added to the other's side: + // inverseValue = value[inversePropertyName]; + // if(inverseValue) { + // inverseValue.push(dataObject) + // } else { + // //No existing array so we create one on the fly + // value[inversePropertyName] = [dataObject]; + // } + + // } else { + // //A many-to-one + // iValue[inversePropertyName] = dataObject; + // } + + // } + + } + } + }, + + + /** + * handles the propagation of a values added to a property's array value to it's inverse property, fka "addToBothSidesOfRelationship..." + * + * @private + * @method + * @argument {Object} dataObject + * @returns {Promise} + + */ + _addDataObjectPropertyDescriptorValuesForInversePropertyName: { + value: function (dataObject, propertyDescriptor, value, inversePropertyName) { + //Now set the inverse if any + if(inversePropertyName) { + var inversePropertyDescriptor = propertyDescriptor._inversePropertyDescriptor /* Sync */; + if(!inversePropertyDescriptor) { + var self = this; + return propertyDescriptor.inversePropertyDescriptor.then(function(inversePropertyDescriptorResolved) { + self._addDataObjectPropertyDescriptorValuesForInversePropertyDescriptor(dataObject, propertyDescriptor, value, inversePropertyDescriptorResolved); + }) + } else { + this._addDataObjectPropertyDescriptorValuesForInversePropertyDescriptor(dataObject, propertyDescriptor, value, inversePropertyDescriptor); + return Promise.resolveTrue; + } + } + } + }, + + _addDataObjectPropertyDescriptorValuesForInversePropertyDescriptor: { + value: function (dataObject, propertyDescriptor, values, inversePropertyDescriptor) { + if(inversePropertyDescriptor) { + //value should be an array + if(!Array.isArray(values) || !(propertyDescriptor.cardinality > 0)) { + console.warn("Something's off...., values added to propertyDescriptor:",propertyDescriptor, " of data object:",dataObject," should be an array"); + } + + var inversePropertyName = inversePropertyDescriptor.name, + inversePropertyCardinality = inversePropertyDescriptor.cardinality, + i, countI; + + for(i=0, countI = values.length; (i 1) { + //many to many: + //value, if there is one, needs to be added to the other's side: + inverseValue = value[_inversePropertyName || inversePropertyDescriptor.name]; + if(inverseValue) { + /* + We shouldn't add the same object again, so we need to check if it is there. I really don't like doinf indexOf() here, but it's not a set... + */ + if(inverseValue.indexOf(dataObject) === -1) { + inverseValue.push(dataObject); + } + } else { + //No existing array so we create one on the fly + value[_inversePropertyName || inversePropertyDescriptor.name] = [dataObject]; + } + + } else { + //A many-to-one + if(value[_inversePropertyName || inversePropertyDescriptor.name] !== dataObject) { + value[_inversePropertyName || inversePropertyDescriptor.name] = dataObject; + } + } + } + } + }, + + + /** + * handles the propagation of a values added to a property's array value to it's inverse property, fka "addToBothSidesOfRelationship..." + * + * @private + * @method + * @argument {Object} dataObject + * @returns {Promise} + + */ + _removeDataObjectPropertyDescriptorValuesForInversePropertyName: { + value: function (dataObject, propertyDescriptor, value, inversePropertyName) { + //Now set the inverse if any + if(inversePropertyName) { + var inversePropertyDescriptor = propertyDescriptor._inversePropertyDescriptor /* Sync */; + if(!inversePropertyDescriptor) { + var self = this; + return propertyDescriptor.inversePropertyDescriptor.then(function(inversePropertyDescriptorResolved) { + self._removeDataObjectPropertyDescriptorValuesForInversePropertyDescriptor(dataObject, propertyDescriptor, value, inversePropertyDescriptorResolved); + }) + } else { + this._removeDataObjectPropertyDescriptorValuesForInversePropertyDescriptor(dataObject, propertyDescriptor, value, inversePropertyDescriptor); + return Promise.resolveTrue; + } + } + } + }, + + + _removeDataObjectPropertyDescriptorValuesForInversePropertyDescriptor: { + value: function (dataObject, propertyDescriptor, values, inversePropertyDescriptor) { + if(inversePropertyDescriptor) { + + //value should be an array + if(!Array.isArray(values) || !(propertyDescriptor.cardinality > 0)) { + console.warn("Something's off...., values added to propertyDescriptor:",propertyDescriptor, " of data object:",dataObject," should be an array"); + } + + var inversePropertyName = inversePropertyDescriptor.name, + inversePropertyCardinality = inversePropertyDescriptor.cardinality, + i, countI; + + for(i=0, countI = values.length; (i 1) { + /* + many to many: + value needs to be renoved to the other's side, unless it doesn't exists (which would be the case if it wasn't fetched). + */ + inverseValue = value[_inversePropertyName || inversePropertyDescriptor.name]; + if(inverseValue) { + inverseValue.delete(dataObject); + } + + } else { + //A many-to-one, sever the ties + value[_inversePropertyName || inversePropertyDescriptor.name] = null; + } + } + } + }, + registerDataObjectChangesFromEvent: { value: function (changeEvent) { + var dataObject = changeEvent.target, + key = changeEvent.key, + objectDescriptor = this.objectDescriptorForObject(dataObject), + propertyDescriptor = objectDescriptor.propertyDescriptorForName(key), + inversePropertyName = propertyDescriptor.inversePropertyName, + inversePropertyDescriptor; + + if(inversePropertyName) { + inversePropertyDescriptor = propertyDescriptor._inversePropertyDescriptor /* Sync */; + if(!inversePropertyDescriptor) { + var self = this; + return propertyDescriptor.inversePropertyDescriptor.then(function(_inversePropertyDescriptor) { + if(!_inversePropertyDescriptor) { + console.error("objectDescriptor "+objectDescriptor.name+"'s propertyDescriptor "+propertyDescriptor.name+ " declares an inverse property named "+inversePropertyName+" on objectDescriptor "+propertyDescriptor._valueDescriptorReference.name+", no matching propertyDescriptor could be found on "+propertyDescriptor._valueDescriptorReference.name); + } else { + self._registerDataObjectChangesFromEvent(changeEvent, propertyDescriptor, _inversePropertyDescriptor); + } + }); + } else { + this._registerDataObjectChangesFromEvent(changeEvent, propertyDescriptor, inversePropertyDescriptor); + } + } else { + this._registerDataObjectChangesFromEvent(changeEvent, propertyDescriptor, inversePropertyDescriptor); + } + + } + }, + + _registerDataObjectChangesFromEvent: { + value: function (changeEvent, propertyDescriptor, inversePropertyDescriptor) { var dataObject = changeEvent.target, key = changeEvent.key, keyValue = changeEvent.keyValue, addedValues = changeEvent.addedValues, - removedValues = changeEvent.addedValues, - changesForDataObject = this.dataObjectChanges.get(dataObject); + removedValues = changeEvent.removedValues, + changesForDataObject = this.dataObjectChanges.get(dataObject), + inversePropertyDescriptor, + self = this; if(!this.createdDataObjects.has(dataObject)) { this.changedDataObjects.add(dataObject); @@ -2191,6 +2561,9 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { this.dataObjectChanges.set(dataObject,changesForDataObject); } + + + /* While a single change Event should be able to model both a range change equivalent of minus/plus and a related length property change at @@ -2207,8 +2580,14 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { */ - if(changeEvent.hasOwnProperty("key") && changeEvent.hasOwnProperty("keyValue")) { - changesForDataObject.set(key,keyValue); + if( changeEvent.hasOwnProperty("key") && changeEvent.hasOwnProperty("keyValue") && key !== "length" && + /* new for blocking re-entrant */ changesForDataObject.get(key) !== keyValue) { + changesForDataObject.set(key,keyValue); + + //Now set the inverse if any + if(inversePropertyDescriptor) { + self._setDataObjectPropertyDescriptorValueForInversePropertyDescriptor(dataObject, propertyDescriptor, keyValue, inversePropertyDescriptor, changeEvent.previousKeyValue); + } } //A change event could carry both a key/value change and addedValues/remove, like a splice, where the key would be "length" @@ -2233,9 +2612,12 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { var registeredAddedValues = manyChanges.addedValues; if(!registeredAddedValues) { manyChanges.addedValues = (registeredAddedValues = new Set(addedValues)); + self._addDataObjectPropertyDescriptorValuesForInversePropertyDescriptor(dataObject, propertyDescriptor, addedValues, inversePropertyDescriptor); + } else { for(i=0, countI=addedValues.length;i} rawRules - Object whose keys are object property @@ -1445,12 +1726,12 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData */ _mapObjectMappingRules: { value: function (rawRules) { - var propertyNames = rawRules ? Object.keys(rawRules) : [], + var propertyNames = rawRules ? Object.keys(rawRules) : null, propertyName, rawRule, rule, i; //TODO Add path change listener for objectDescriptor to //account for chance that objectDescriptor is added after the rules - if (this.objectDescriptor) { + if (this.objectDescriptor && propertyNames) { for (i = 0; (propertyName = propertyNames[i]); ++i) { rawRule = rawRules[propertyName]; if (this._shouldMapRule(rawRule, true)) { @@ -1476,13 +1757,13 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData */ _mapRawDataMappingRules: { value: function (rawRules) { - var propertyNames = rawRules ? Object.keys(rawRules) : [], + var propertyNames = rawRules ? Object.keys(rawRules) : null, propertyName, rawRule, rule, i; //TODO Add path change listener for objectDescriptor to //account for chance that objectDescriptor is added after the rules - if (this.objectDescriptor) { + if (this.objectDescriptor && propertyNames) { for (i = 0; (propertyName = propertyNames[i]); ++i) { rawRule = rawRules[propertyName]; if (this._shouldMapRule(rawRule, false)) { @@ -1492,6 +1773,24 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData if (this._shouldMapRule(rawRule, true)) { rule = this._makeRuleFromRawRule(rawRule, propertyName, true, false); this._ownRawDataMappingRules.set(rule.targetPath, rule); + + //Let's add indexing for each of rule.requiremts properties if any. + if(rule.requirements && rule.requirements.length) { + var requirements = rule.requirements, + _rawDataMappingRulesByPropertyName = (this._rawDataMappingRulesByPropertyName || (this._rawDataMappingRulesByPropertyName = new Map())), + j=0, countJ = requirements.length, jRequirement, jRequirementRules; + + /* + A property in requirements could be present in more than one rule, which the case for example in the case of polymorphic associations implemented with the Exclusive Belongs To (AKA Exclusive Arc) strategy where each potential destination table has a matching foreignKey. + */ + + for(;(j stream.addData(object)"); + stream.addData(object); return object; }); @@ -464,6 +464,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot stream.addData(object); result = Promise.resolve(object); } + this._addMapDataPromiseForStream(result, stream); @@ -806,7 +807,11 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot var self = this, dataToPersist = this._streamRawData.get(stream), mappingPromises = this._streamMapDataPromises.get(stream), - dataReadyPromise = mappingPromises ? Promise.all(mappingPromises) : this.nullPromise; + dataReadyPromise = mappingPromises + ? mappingPromises.length === 1 + ? mappingPromises[0] + : Promise.all(mappingPromises) + : this.nullPromise; if (mappingPromises) { this._streamMapDataPromises.delete(stream); @@ -816,10 +821,13 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot this._streamRawData.delete(stream); } - dataReadyPromise.then(function (results) { + //console.log("rawDataDone for "+stream.query.type.name); + dataReadyPromise.then(function (results) { + // console.log("dataReadyPromise for "+stream.query.type.name); return dataToPersist ? self.writeOfflineData(dataToPersist, stream.query, context) : null; }).then(function () { + // console.log("stream.dataDone() for "+stream.query.type.name); stream.dataDone(); return null; }).catch(function (e) { @@ -1104,22 +1112,43 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot snapshot, result; + // console.log(object.dataIdentifier.objectDescriptor.name +" _mapRawDataToObject id:"+record.id); if (mapping) { - //Check if this isn't already being done, if it is, it's redundant and we bail + /* + When we fetch objects that have inverse relationships on each others none could complete their mapRawDataProcess because the first one's promise for mapping the relationship to the second never commpletes because the second one itself has it's raw data's foreignKey value to the first one converted/fetched, unique object is found, but that second mapping attenpt to map it gets stuck on the reverse to the second, etc... + + So to break the cycle, if there's a known snapshot and the object is being mapped, then we don't return a promise, knowing there's already one pending for the first pass. + */ snapshot = this.snapshotForObject(object); - if(snapshot === record && this._objectsBeingMapped.has(object)) { - return result; + if(Object.equals(snapshot,record) ) { + return undefined; + + if(this._objectsBeingMapped.has(object)) { + console.log(object.dataIdentifier.objectDescriptor.name +" _mapRawDataToObject id:"+record.id+" FOUND EXISTING MAPPING PROMISE"); + return undefined; + return Promise.resolve(object); + return this._getMapRawDataToObjectPromise(snapshot,object); + } else { + //rawData is un-changed, no point doing anything... + return undefined; + } } + //Recording snapshot even if we already had an object + //Record snapshot before we may create an object + this.recordSnapshot(object.dataIdentifier, record); this._objectsBeingMapped.add(object); result = mapping.mapRawDataToObject(record, object, context, readExpressions); + // console.log(object.dataIdentifier.objectDescriptor.name +" _mapRawDataToObject id:"+record.id+" FIRST NEW MAPPING PROMISE"); + if (result) { result = result.then(function () { result = self.mapRawDataToObject(record, object, context, readExpressions); if (!self._isAsync(result)) { + // self._deleteMapRawDataToObjectPromise(record, object); self._objectsBeingMapped.delete(object); return result; @@ -1127,11 +1156,13 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot else { result = result.then(function(resolved) { + // self._deleteMapRawDataToObjectPromise(record, object); self._objectsBeingMapped.delete(object); return resolved; }, function(failed) { + // self._deleteMapRawDataToObjectPromise(record, object); self._objectsBeingMapped.delete(object); }); @@ -1139,6 +1170,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } }, function(error) { + // self._deleteMapRawDataToObjectPromise(record, object); self._objectsBeingMapped.delete(object); throw error; }); @@ -1152,14 +1184,16 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot else { result = result.then(function(resolved) { + // self._deleteMapRawDataToObjectPromise(record, object); self._objectsBeingMapped.delete(object); return resolved; }, function(failed) { + // self._deleteMapRawDataToObjectPromise(record, object); self._objectsBeingMapped.delete(object); }); - return result; + //return result; } } } else { @@ -1171,23 +1205,27 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot if (!this._isAsync(result)) { + // self._deleteMapRawDataToObjectPromise(record, object); self._objectsBeingMapped.delete(object); return result; } else { result = result.then(function(resolved) { - + // self._deleteMapRawDataToObjectPromise(record, object); self._objectsBeingMapped.delete(object); return resolved; }, function(failed) { + // self._deleteMapRawDataToObjectPromise(record, object); self._objectsBeingMapped.delete(object); }); - return result; + //return result; } } + //this._setMapRawDataToObjectPromise(record, object, result); + return result; } diff --git a/data/service/raw-data-type-mapping.js b/data/service/raw-data-type-mapping.js index 39290c2e32..a0cd045e9c 100644 --- a/data/service/raw-data-type-mapping.js +++ b/data/service/raw-data-type-mapping.js @@ -6,7 +6,7 @@ var Montage = require("montage").Montage, /** * Instructions for a [RawDataService]{@link RawDataService} to use * to determine if a rawData object corresponds to a particular class. - * + * * @class RawDataTypeMapping * @extends Montage */ @@ -23,11 +23,11 @@ exports.RawDataTypeMapping = Montage.specialize({ } else { value = deserializer.getProperty("expression"); this.expression = value; - } - + } + } }, - + serializeSelf: { value: function (serializer) { @@ -36,8 +36,8 @@ exports.RawDataTypeMapping = Montage.specialize({ }, /** - * Criteria to evaluate against the rawData object to determine - * if it represents an instance of the class defined by the + * Criteria to evaluate against the rawData object to determine + * if it represents an instance of the class defined by the * object descriptor assigned to RawDataTypeMapping.type. * @type {Criteria} */ @@ -47,8 +47,8 @@ exports.RawDataTypeMapping = Montage.specialize({ /** - * Expression to evaluate against the rawData object to determine - * if it represents an instance of the class defined by the + * Expression to evaluate against the rawData object to determine + * if it represents an instance of the class defined by the * object descriptor assigned to RawDataTypeMapping.type. * @type {string} */ @@ -66,6 +66,13 @@ exports.RawDataTypeMapping = Montage.specialize({ }, + expressionSyntax: { + get: function() { + return this.criteria.syntax; + } + }, + + /** * Class to create an instance of when RawDataTypeMapping.criteria.evaluate * evaluates a rawData object to true @@ -79,12 +86,12 @@ exports.RawDataTypeMapping = Montage.specialize({ /** * Return whether a rawDataObject matches this.criteria * @method - * @param {Object} rawData + * @param {Object} rawData */ match: { value: function (rawData) { return !!this.criteria.evaluate(rawData); - } + } }, }, { @@ -107,4 +114,4 @@ exports.RawDataTypeMapping = Montage.specialize({ } } -}); \ No newline at end of file +}); From cc9515b0bc48e3d7e644f98a46cfb7d6606ef57a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 21 Sep 2020 18:58:02 -0700 Subject: [PATCH 260/407] hmm, this one needs another look --- .../RFC3339UTC-range-string-to-range-converter.js | 7 +++++-- testing/testpageloader.js | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/converter/RFC3339UTC-range-string-to-range-converter.js b/core/converter/RFC3339UTC-range-string-to-range-converter.js index 05348e0b09..deda1ca39e 100644 --- a/core/converter/RFC3339UTC-range-string-to-range-converter.js +++ b/core/converter/RFC3339UTC-range-string-to-range-converter.js @@ -3,10 +3,12 @@ * @requires montage/core/converter/converter */ var Converter = require("./converter").Converter, - Range = require("../range"), - Range = require("../extras/date"), + Range = require("../range").Range, singleton; + //for Date.parseRFC3339 + require("../extras/date"); + /** * @class RFC3339UTCRangeStringToRangeConverter * @classdesc Converts an RFC3339 UTC string to a date and reverts it. @@ -35,6 +37,7 @@ var RFC3339UTCRangeStringToRangeConverter = exports.RFC3339UTCRangeStringToRange */ convert: { value: function (v) { + return Range.parse(v,Date); return Range.parse(v,Date.parseRFC3339); //return Date.parseRFC3339(v); } diff --git a/testing/testpageloader.js b/testing/testpageloader.js index 3a95bc4501..707f645b27 100644 --- a/testing/testpageloader.js +++ b/testing/testpageloader.js @@ -327,7 +327,7 @@ var TestPageLoader = exports.TestPageLoader = Montage.specialize( { return new Promise(function (resolve, reject) { (function waitForDraw(done) { - + var hasDraw = component.draw.drawHappened === numDraws; if (hasDraw) { resolve(theTestPage.drawHappened); @@ -335,6 +335,7 @@ var TestPageLoader = exports.TestPageLoader = Montage.specialize( { // wait a little bit before resolving the promise // Make sure the DOM changes have been applied. setTimeout(function () { + //Hello!! resolve(theTestPage.drawHappened); }, 50); } From c575846c852f4027a2d4dd60b776663637f2b428 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 21 Sep 2020 18:59:17 -0700 Subject: [PATCH 261/407] change last display separator before id from $=# to / --- core/mr/require.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/mr/require.js b/core/mr/require.js index 8e4221cc01..6a0bccba81 100644 --- a/core/mr/require.js +++ b/core/mr/require.js @@ -557,7 +557,7 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { var aModule = modules[lookupId] = new Module(); aModule.id = id; aModule.display = (config.name || config.location); // EXTENSION - aModule.display += "#"; // EXTENSION + aModule.display += "/"; // EXTENSION aModule.display += id; // EXTENSION aModule.require = require; } From d64ffca9d4959a5c94e4877cc43e8a6ee68727cc Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 23 Sep 2020 15:59:50 -0700 Subject: [PATCH 262/407] overrides contains to make it work with both a single value, which it did as well as for another range --- core/range.js | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/core/range.js b/core/range.js index 63fd3fdfba..65cae16d26 100644 --- a/core/range.js +++ b/core/range.js @@ -42,8 +42,8 @@ exports.Range = Range; // * // * Pair | Meaning // * -----|-------- -// * `()` | open -// * `[]` | closed +// * `()` | open - excludes bounds +// * `[]` | closed - iclude bounds // * `[)` | left-closed, right-open // * `(]` | left-open, right-closed // * @@ -115,6 +115,42 @@ Range.prototype.intersection = function (other) { Range.prototype.overlaps = Range.prototype.intersects; +/** + * Check if a given value or range of values is contained within this range. + * Returns `true` or `false`. Overrides super to add support for Range as argument. + * + * @example + * new Range(0, 10).contains(5) // => true + * new Range(0, 10).contains(5) // => true + * new Range(0, 10).contains(10) // => true + * new Range(0, 10, "[)").contains(10) // => false + * + * @method contains + * @param {Object} value + */ +Range.prototype._contains = Range.prototype.contains; + +Range.prototype.contains = function(value) { + if(value instanceof Range) { + if (this.isEmpty()) return Range.empty; + if (value.isEmpty()) return Range.empty; + + /* + Range.compareBeginToBegin(new Range(0, 10), new Range(5, 15)) // => -1 + Range.compareBeginToBegin(new Range(0, 10), new Range(0, 15)) // => 0 + Range.compareBeginToBegin(new Range(0, 10), new Range(0, 15, "()")) // => 1 + + Range.compareEndToEnd(new Range(0, 10), new Range(5, 15)) // => -1 + Range.compareEndToEnd(new Range(0, 10), new Range(5, 10)) // => 0 + Range.compareEndToEnd(new Range(0, 10), new Range(5, 10, "()")) // => 1 + */ + return ((Range.compareBeginToBegin(this,value) >= 0) && (Range.compareEndToEnd(this,value) >= 0)) + ? true + : false; + } else { + return this._contains(value) + } +} Range.getInfoForObject = function(object) { return Montage.getInfoForObject(object); From 2efda1c8c3e91e08409416d4b0e393dd58d8bb8e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 28 Sep 2020 00:37:49 -0700 Subject: [PATCH 263/407] fix contains bug when argument is another range --- core/range.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/range.js b/core/range.js index 65cae16d26..b621e1210c 100644 --- a/core/range.js +++ b/core/range.js @@ -144,7 +144,7 @@ Range.prototype.contains = function(value) { Range.compareEndToEnd(new Range(0, 10), new Range(5, 10)) // => 0 Range.compareEndToEnd(new Range(0, 10), new Range(5, 10, "()")) // => 1 */ - return ((Range.compareBeginToBegin(this,value) >= 0) && (Range.compareEndToEnd(this,value) >= 0)) + return ((Range.compareBeginToBegin(this,value) <= 0) && (Range.compareEndToEnd(this,value) >= 0)) ? true : false; } else { From b20bb8791f42ecdbccb55196a66a0fa2c625e706 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 28 Sep 2020 00:39:17 -0700 Subject: [PATCH 264/407] rename path(someExpression) to evaluate(someExpression) while maintaining backward compatibility for now --- core/frb/compile-evaluator.js | 2 ++ core/frb/compile-observer.js | 1 + 2 files changed, 3 insertions(+) diff --git a/core/frb/compile-evaluator.js b/core/frb/compile-evaluator.js index 0bc29bb315..ab128b547e 100644 --- a/core/frb/compile-evaluator.js +++ b/core/frb/compile-evaluator.js @@ -279,6 +279,8 @@ var argCompilers = { }; +argCompilers.evaluate = argCompilers.path; + var operators = Object.clone(Operators, 1); Object.addEach(operators, { diff --git a/core/frb/compile-observer.js b/core/frb/compile-observer.js index 509012b238..b01813bafe 100644 --- a/core/frb/compile-observer.js +++ b/core/frb/compile-observer.js @@ -19,6 +19,7 @@ var semantics = compile.semantics = { compilers: { property: Observers.makePropertyObserver, get: Observers.makeGetObserver, + evaluate: Observers.makePathObserver, path: Observers.makePathObserver, "with": Observers.makeWithObserver, "if": Observers.makeConditionalObserver, From cac72a0454548fa66d9b57bbbdc5e83622444267 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 5 Oct 2020 15:40:50 -0700 Subject: [PATCH 265/407] avoid fetching if we don't have a valid query --- ui/data-editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/data-editor.js b/ui/data-editor.js index 8a94a514b9..03de994ffe 100644 --- a/ui/data-editor.js +++ b/ui/data-editor.js @@ -159,7 +159,7 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { this.__dataQuery = null; //If we're active for trhe user, we re-fetch - if(this.inDocument) { + if(this.inDocument && this._dataQuery) { this.fetchData(); } } From 809fe22ca445705abb665e136dd5d5c44f9aa2ae Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 5 Oct 2020 15:41:33 -0700 Subject: [PATCH 266/407] Fix bug to mirror the same done on collections project master --- core/collections/shim-array.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/collections/shim-array.js b/core/collections/shim-array.js index 5f7f2c5421..35d0b1fba3 100644 --- a/core/collections/shim-array.js +++ b/core/collections/shim-array.js @@ -34,7 +34,7 @@ if(!Array.nativeFrom ) { // } var isSymbolDefined = typeof Symbol !== "undefined"; Array.from = function (values, mapFn, thisArg) { - if(isSymbolDefined && values && typeof values[Symbol.iterator] === "function") { + if(isSymbolDefined && values && (typeof values[Symbol.iterator] === "function" || typeof mapFn === "function")) { return Array.nativeFrom(values, mapFn, thisArg); } //Now we add support for values that implement forEach: From fbef8d45c598ef54b2816f2f963386311e9cd2e3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 5 Oct 2020 15:43:04 -0700 Subject: [PATCH 267/407] delay re-setting equal until we need it --- core/collections/shim-object.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/collections/shim-object.js b/core/collections/shim-object.js index 04111f3085..2d66fda2d1 100644 --- a/core/collections/shim-object.js +++ b/core/collections/shim-object.js @@ -423,8 +423,7 @@ Object.is = function (x, y) { @returns {Boolean} whether the values are deeply equivalent @function external:Object.equals */ -Object.equals = function (a, b, equals, memo) { - equals = equals || Object.equals; +Object.equals = function ObjectEquals(a, b, equals, memo) { //console.log("Object.equals: a:",a, "b:",b, "equals:",equals); // unbox objects, but do not confuse object literals a = Object.getValueOf(a); @@ -438,6 +437,8 @@ Object.equals = function (a, b, equals, memo) { } memo.set(a, true); } + + if (Object.isObject(a) && typeof a.equals === "function") { return a.equals(b, equals, memo); } @@ -447,6 +448,9 @@ Object.equals = function (a, b, equals, memo) { } if (Object.isObject(a) && Object.isObject(b)) { if (Object.getPrototypeOf(a) === Object.prototype && Object.getPrototypeOf(b) === Object.prototype) { + + equals = equals || ObjectEquals; + for (var name in a) { if (!equals(a[name], b[name], equals, memo)) { return false; From a4cfd8013598bd5398f07afd112923d1aac90768 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 5 Oct 2020 15:44:06 -0700 Subject: [PATCH 268/407] fix bugs, improvements on calendar date, date, timezone --- core/date/calendar-date.js | 307 ++++++++++++++++++++++++++++++++---- core/date/time-zone-core.js | 104 ++++++++++++ core/date/time-zone.js | 95 +---------- core/extras/date.js | 80 ++++++++++ 4 files changed, 468 insertions(+), 118 deletions(-) create mode 100644 core/date/time-zone-core.js diff --git a/core/date/calendar-date.js b/core/date/calendar-date.js index cb0ce2fc20..f966a2e19d 100644 --- a/core/date/calendar-date.js +++ b/core/date/calendar-date.js @@ -3,10 +3,13 @@ */ var Montage = require("../core").Montage, ICAL = require('ical.js'), + TimeZone = require("./time-zone-core").TimeZone, + currentEnvironment = require("../environment").currentEnvironment, ICAL_Time = ICAL.Time, ICAL_Time_Prototype = ICAL.Time.prototype, CalendarDate = exports.CalendarDate = ICAL_Time, - CalendarDatePrototype = ICAL_Time_Prototype; + CalendarDatePrototype = ICAL_Time_Prototype, + Range = require("../range").Range; /* @@ -29,16 +32,76 @@ CalendarDate.getInfoForObject = function(object) { }; - /** - * Adds the duration to the current time. The instance is modified in - * place. - * - * @param {ICAL.Duration} aDuration The duration to add - */ -CalendarDatePrototype.adjustComponentValues = function(year, monthIndex, day, hours, minutes, seconds, milliseconds) { +/** + * Create a new . The instance is modified in + * place. + * + * @param {Number=} year The year for this date + * @param {Number=} month The month for this date - starting at 1 + * @param {Number=} day The day for this date + * @param {Number=} hours The hour for this date + * @param {Number=} minutes The minute for this date + * @param {Number=} seconds The second for this date + * @param {Number=} milliseconds The milliseconds for this date + * @param {TimeZone} timeZone The timeZone for this date + * + * borrowing from ical.js time.js fromData() that we unfortunately + * can't re-factor to extract what we need in a way it would be used + * in both places. + */ + +CalendarDate.withUTCComponentValuesInTimeZone = function(year, month, day, hours, minutes, seconds, milliseconds, timeZone) { + var calendarDate = new CalendarDate(); + + calendarDate.isDate = hours === undefined || hours === null; + + //Initialize in UTC + if(year) { + calendarDate.year = Number(year); + } + if(month) { + calendarDate.month = Number(month); + } + if(day) { + calendarDate.day = Number(day); + } + if(hours) { + calendarDate.hour = Number(hours); + } + if(minutes) { + calendarDate.minute = Number(minutes); + } + if(seconds) { + calendarDate.second = Number(seconds); + } + calendarDate.zone = TimeZone.UTCTimeZone; + + + + //Then convert to timeZone if we have one + if(timeZone) { + calendarDate.timeZone = timeZone; + } + + calendarDate._cachedUnixTime = null; + + return calendarDate; + +}; + + + + +/** + * Adds the duration to the current time. The instance is modified in + * place. + * + * @param {ICAL.Duration} aDuration The duration to add + */ +CalendarDatePrototype.adjustComponentValues = function(year, monthIndex, days, hours, minutes, seconds, milliseconds) { if(milliseconds !== undefined) { - console.warn("CalendarDate doesn't properly supports millisecons yet"); + console.warn("CalendarDate doesn't properly supports milliseconds yet"); } // because of the duration optimizations it is much @@ -52,22 +115,24 @@ CalendarDatePrototype.adjustComponentValues = function(year, monthIndex, day, ho month = this.month, myYear = this.year; - second += seconds; - minute += minutes; - hour += hours; - myDay += days; + // if(Number.isFinite(seconds)) second += seconds; + // if(Number.isFinite(minutes)) minute += minutes; + // if(Number.isFinite(hours)) hour += hours; + // if(Number.isFinite(days)) myDay += days; // day += 7 * weeks; - month += monthIndex; - myYear += year; + if(Number.isFinite(monthIndex)) month += monthIndex; + if(Number.isFinite(year)) myYear += year; - this.second = second; - this.minute = minute; - this.hour = hour; - this.day = myDay; + // this.second = second; + // this.minute = minute; + // this.hour = hour; + // this.day = myDay; this.month = month; this.year = myYear; - this._cachedUnixTime = null; + this.adjust((Number.isFinite(days) && days) || 0, (Number.isFinite(hours) && hours) || 0, (Number.isFinite(minutes) && minutes) || 0, (Number.isFinite(seconds) && seconds) || 0); + + // this._cachedUnixTime = null; }; CalendarDatePrototype.calendarDateByAdjustingComponentValues = function(year, monthIndex, day, hours, minutes, seconds, milliseconds) { @@ -84,22 +149,23 @@ CalendarDatePrototype.valueOf = function CalendarDate_valueOf() { return this.toUnixTime() * 1000; }; -CalendarDatePrototype.setComponentValues = function(year, monthIndex, day, hours, minutes, seconds, milliseconds, timeZone) { +CalendarDatePrototype.setComponentValues = function(year, month, day, hours, minutes, seconds, milliseconds, timeZone) { if(typeof arguments[0] !== "number" && arguments.length === 1) { this.fromData(arguments[0]); } else { if(milliseconds) this.millisecond = milliseconds; + if(seconds) this.seconds = seconds; if(minutes) this.minute = minutes; - if(hours) this.hour = hour; + if(hours) this.hour = hours; if(day) this.day = day; - if(monthIndex) this.month = monthIndex+1; + if(month) this.month = month; if(year) this.year = year; if(timeZone) this.timeZone = timeZone; } }; CalendarDatePrototype.takeComponentValuesFromCalendarDate = function(aCalendarDate) { - this.setComponentValues(aCalendarDate.year, aCalendarDate.month-1, aCalendarDate.day, aCalendarDate.hour, aCalendarDate.minute, aCalendarDate.second, aCalendarDate.milliseconds,aCalendarDate.timeZone); + this.setComponentValues(aCalendarDate.year, aCalendarDate.month, aCalendarDate.day, aCalendarDate.hour, aCalendarDate.minute, aCalendarDate.second, aCalendarDate.milliseconds,aCalendarDate.timeZone); } @@ -108,10 +174,26 @@ Object.defineProperty(CalendarDatePrototype,"timeZone",{ return this.zone; }, set: function(value) { - this.zone = value; + if(value !== this.zone) { + TimeZone.convertCalendarDateFromTimeZoneToTimeZone (this,this.zone,value); + this.zone = value; + } + }, + configurable: true +}); + +Object.defineProperty(CalendarDatePrototype,"fullDayRange",{ + + get: function(date) { + var dayStart = this.clone(); + dayStart.setHours(0,0,0,0); + var dayEnd = dayStart.clone(); + dayEnd.setHours(23,59,59,999); + return new Range(dayStart,dayEnd); } }); + /* Bookmark https://github.com/jakubroztocil/rrule @@ -151,6 +233,45 @@ CalendarDatePrototype.getTime = function() { return this.toUnixTime() * 1000; }; +/** + * The toLocaleString() method returns a string with a language sensitive + * representation of this date. The locales and options arguments let applications + * specify the language whose formatting conventions should be used and customize + * the behavior of the method. + * + * @method + * @argument {string||Locale||Array||Array} locales - The locale(s). + * @argument {Object} options - Options to configure the formatting of the locale string. + */ + +CalendarDatePrototype.toLocaleString = function(locales, options) { + /* + Because we can't require Locale for now because of a circular dependency, we can't test for instanceof Locale, so we work around it + */ + if(typeof locales !== "string") { + locales = locales.identifier; + } else if(Array.isArray(locales)) { + for (var i=0, countI = locales.length;(i < countI); i++) { + if(typeof locales[i] !== "string") { + locales[i] = locales[i].identifier; + } + } + } + return new Intl.DateTimeFormat(locales,options).format(this); +}; +Object.defineProperty(CalendarDatePrototype,"defaultStringDescription",{ + get: function() { + //There's a circular dependency issue between calendar, calendar-date and locale... + return this.toLocaleString(currentEnvironment.systemLocaleIdentifier); + }, + configurable: true +}); + +CalendarDatePrototype.setHours = function(hoursValue, minutesValue, secondsValue, msValue) { + this.setComponentValues(0, 0, 0, hoursValue, minutesValue, secondsValue, msValue); +} + + /* CalendarDatePrototype.getTimezoneOffset = @@ -165,7 +286,6 @@ CalendarDatePrototype.getUTCSeconds() CalendarDatePrototype.getYear() CalendarDatePrototype.setDate() CalendarDatePrototype.setFullYear() -CalendarDatePrototype.setHours() CalendarDatePrototype.setMilliseconds() CalendarDatePrototype.setMinutes() CalendarDatePrototype.setMonth() @@ -184,8 +304,6 @@ CalendarDatePrototype.toGMTString() CalendarDatePrototype.toISOString() CalendarDatePrototype.toJSON() CalendarDatePrototype.toLocaleDateString() -CalendarDatePrototype.toLocaleFormat() -CalendarDatePrototype.toLocaleString() CalendarDatePrototype.toLocaleTimeString() CalendarDatePrototype.toSource() CalendarDatePrototype.toString() @@ -193,3 +311,136 @@ CalendarDatePrototype.toTimeString() CalendarDatePrototype.toUTCString() CalendarDatePrototype.valueOf() */ + + + + +/** + * Date specific methods added to Range until we introduce a DateRange + * that would work for both native Date and CalendarDate. + */ + +function DateRangeIterator(){}; + +Object.defineProperties(DateRangeIterator.prototype, { + + initWithRangeComponentStepDirection: { + value: function(range, component, step, direction) { + this.range = range; + this.component = component; + this.step = step; + if(direction) { + this.direction = direction; + } + return self; + } + }, + range: { + value: undefined + }, + component: { + value: undefined + }, + _step: { + value: 1 + }, + step: { + get: function() { + return this._step; + }, + set: function() { + if(Number.isFinite(step)) { + this.step = step; + } else { + throw "step: "+step+" isn't a valid number"; + } + } + }, + direction: { + value: undefined + }, + _nextValue: { + value: undefined + }, + next: { + value: function() { + let result; + if (nextIndex < end) { + result = { value: nextIndex, done: false } + nextIndex += step; + iterationCount++; + return result; + } + return { value: iterationCount, done: true } + } + } +}); + +// To Satisfy both the Iterator Protocol and Iterable +DateRangeIterator.prototype[Symbol.iterator] = function() { return this; }; + +Object.defineProperty(Range.prototype,"componentIterator", { + value: function* (component, direction, step) { + var begin = this.begin, + beginComponent = begin[component], + end = this.end, + endComponent = end[component]; + + dayIterator.component = "day"; + if(step !== undefined) { + dayIterator.step = step; + } + dayIterator.range = this; + + return dayIterator; + } +}); + +Object.defineProperty(CalendarDate, "fullDayTimeRangeFrom", { + value: function(aDate) { + if(aDate instanceof Date) { + return CalendarDate.fromJSDate(aDate).fullDayRange; + } else if(aDate instanceof CalendarDate) { + return aDate.fullDayRange; + } else { + return null; + } + } +}); + + +Object.defineProperty(Range.prototype, "fullDayIterator", { + value: function* (step/*,direction*/ /* todo for later*/) { + var iterationFullDayRange = this.begin.fullDayRange, + iBegin, iEnd, + reply; + + /* + can't wait to use + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters + */ + step = step || 1; + while(Range.compareBeginToEnd(iterationFullDayRange,this) <= 0) { + /* + reply is the result of calling iterator.next(reply), which gives the power to seek in the iteration more than one step at a time + + */ + reply = yield iterationFullDayRange; + + //Create a new range for the next iteration + iBegin = iterationFullDayRange.begin.clone(); + iEnd = iterationFullDayRange.end.clone(); + + //set the new range bounds to the next day + iBegin.adjustComponentValues(0,0,(reply||1)*step); + iEnd.adjustComponentValues(0,0,(reply||1)*step); + + //Make the range + iterationFullDayRange = new Range(iBegin,iEnd); + + } + + + } +}); + diff --git a/core/date/time-zone-core.js b/core/date/time-zone-core.js new file mode 100644 index 0000000000..28d749874f --- /dev/null +++ b/core/date/time-zone-core.js @@ -0,0 +1,104 @@ +/* + Time Zones definitions at: + + https://hg.mozilla.org/comm-central/raw-file/tip/calendar/timezones/zones.json + +*/ +var Montage = require("../core").Montage, + ICAL = require("ical.js"), + ICAL_Timezone = ICAL.Timezone, + ICAL_Timezone_Prototype = ICAL.Timezone.prototype, + ICAL_Timezone_Prototype = ICAL.Timezone.prototype, + ICAL_TimezoneService = ICAL.TimezoneService, + currentEnvironment = require("../environment").currentEnvironment, + + //We really need to find a way to load only the data for the timezone we care about. + //So this file should either be split in individual files or we need to find/build an API that does so + // 1. https://developers.google.com/maps/documentation/timezone/intro //not ics + // 2. https://ipgeolocation.io/documentation/timezone-api.html + // 3. https://www.amdoren.com/time-zone-api/ + // 4. http://worldtimeapi.org + // 5. https://timezoneapi.io/developers/timezone + timeZonesData = require("./time-zone-data/zones-compiled.json"), + TimeZone = exports.TimeZone = ICAL_Timezone, + TimeZonePrototype = TimeZone.prototype; + +(function registerTimezones(timeZonesData) { + Object.keys(timeZonesData).forEach(function(key) { + var icsData = timeZonesData[key], + // dataToParse = "BEGIN:VCALENDAR\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\nVERSION:2.0\n", + parsed, + // parsed = ICAL.parse(`BEGIN:VCALENDAR\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\nVERSION:2.0\n${icsData}\nEND:VCALENDAR`), + comp, + vtimezone; + + // dataToParse += icsData; + // dataToParse += "\nEND:VCALENDAR"; + parsed = ICAL.parse(icsData); + comp = new ICAL.Component(parsed); + vtimezone = comp.getFirstSubcomponent('vtimezone'); + + + ICAL.TimezoneService.register(key, new ICAL.Timezone(vtimezone)); + }); +})(timeZonesData); + + +/** + * Returns a TimeZone a given identifier. + * + * + * @function + * @param {CalendarIdentifier} calendarIdentifier The module id of the HTML page to load. + * @returns {Calendar} a new Calendar instance. + */ + +TimeZone.withIdentifier = function(timeZoneIdentifier) { + return ICAL_TimezoneService.get(timeZoneIdentifier); +}; + +Object.defineProperties(ICAL_Timezone_Prototype, { + "identifier": { + get: function() { + return this.tzid; + } + } +}); + + + + + +/** + * Returns a TimeZone a given identifier. + * + * + * @function + * @param {CalendarIdentifier} calendarIdentifier The module id of the HTML page to load. + * @returns {Calendar} a new Calendar instance. + */ +Object.defineProperties(TimeZone, { + + "systemTimeZone": { + get: function() { + var systemLocaleIdentifier = currentEnvironment.systemLocaleIdentifier, + resolvedOptions = Intl.DateTimeFormat(systemLocaleIdentifier).resolvedOptions(), + timeZone = resolvedOptions.timeZone; /* "America/Los_Angeles" */ + return ICAL_TimezoneService.get(timeZone); + } + } + +}); + +TimeZone.UTCTimeZone = TimeZone.utcTimezone; + +/** + * Convert a calendarDate from one timeZone zone to another. + * + * @param {CalendarDate} calendarDate The calendarDate to convert + * @param {TimeZone} fromTimeZone The source zone to convert from + * @param {TimeZone} toTimeZone The target zone to convert to + * @return {CalendarDate} The converted calendarDate object + */ + +TimeZone.convertCalendarDateFromTimeZoneToTimeZone = ICAL.Timezone.convert_time; diff --git a/core/date/time-zone.js b/core/date/time-zone.js index 4c731b88a4..15d954e2c1 100644 --- a/core/date/time-zone.js +++ b/core/date/time-zone.js @@ -4,98 +4,12 @@ https://hg.mozilla.org/comm-central/raw-file/tip/calendar/timezones/zones.json */ -var Montage = require("../core").Montage, - ICAL = require("ical.js"), - ICAL_Timezone = ICAL.Timezone, - ICAL_Timezone_Prototype = ICAL.Timezone.prototype, - ICAL_Timezone_Prototype = ICAL.Timezone.prototype, - ICAL_TimezoneService = ICAL.TimezoneService, - currentEnvironment = require("../environment").currentEnvironment, - - //We really need to find a way to load only the data for the timezone we care about. - //So this file should either be split in individual files or we need to find/build an API that does so - // 1. https://developers.google.com/maps/documentation/timezone/intro //not ics - // 2. https://ipgeolocation.io/documentation/timezone-api.html - // 3. https://www.amdoren.com/time-zone-api/ - // 4. http://worldtimeapi.org - // 5. https://timezoneapi.io/developers/timezone - timeZonesData = require("./time-zone-data/zones-compiled.json"), - TimeZone = exports.TimeZone = ICAL_Timezone, +var TimeZone = require("./time-zone-core").TimeZone + CalendarDate = require("./calendar-date").CalendarDate, TimeZonePrototype = TimeZone.prototype; -(function registerTimezones(timeZonesData) { - Object.keys(timeZonesData).forEach(function(key) { - var icsData = timeZonesData[key], - // dataToParse = "BEGIN:VCALENDAR\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\nVERSION:2.0\n", - parsed, - // parsed = ICAL.parse(`BEGIN:VCALENDAR\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\nVERSION:2.0\n${icsData}\nEND:VCALENDAR`), - comp, - vtimezone; - - // dataToParse += icsData; - // dataToParse += "\nEND:VCALENDAR"; - parsed = ICAL.parse(icsData); - comp = new ICAL.Component(parsed); - vtimezone = comp.getFirstSubcomponent('vtimezone'); - - - ICAL.TimezoneService.register(key, new ICAL.Timezone(vtimezone)); - }); -})(timeZonesData); - - -/** - * Returns a TimeZone a given identifier. - * - * - * @function - * @param {CalendarIdentifier} calendarIdentifier The module id of the HTML page to load. - * @returns {Calendar} a new Calendar instance. - */ - -TimeZone.withIdentifier = function(timeZoneIdentifier) { - return ICAL_TimezoneService.get(timeZoneIdentifier); -}; - -Object.defineProperties(ICAL_Timezone_Prototype, { - "identifier": { - get: function() { - return this.tzid; - } - } -}); - - - - - -/** - * Returns a TimeZone a given identifier. - * - * - * @function - * @param {CalendarIdentifier} calendarIdentifier The module id of the HTML page to load. - * @returns {Calendar} a new Calendar instance. - */ - -TimeZone.systemTimeZone = function() { - var systemLocaleIdentifier = currentEnvironment.systemLocaleIdentifier, - resolvedOptions = Intl.DateTimeFormat(navigatorLocaleIdentifier).resolvedOptions(), - timeZone = resolvedOptions.timeZone; /* "America/Los_Angeles" */ - return ICAL_TimezoneService.get(timeZone); -}; - -/** - * Convert a calendarDate from one timeZone zone to another. - * - * @param {CalendarDate} calendarDate The calendarDate to convert - * @param {TimeZone} fromTimeZone The source zone to convert from - * @param {TimeZone} toTimeZone The target zone to convert to - * @return {CalendarDate} The converted calendarDate object - */ - -TimeZone.convertCalendarDateFromTimeZoneToTimeZone = ICAL.Timezone.convert_time; +exports.TimeZone = TimeZone; /** * Returns an equivalent CalendarDate to aDate (in UTC / local timeZone)in Calendar's timeZone. * @@ -107,7 +21,8 @@ TimeZone.convertCalendarDateFromTimeZoneToTimeZone = ICAL.Timezone.convert_time; Object.defineProperty(Date.prototype, "calendarDateInTimeZone", { value: function (timeZone) { var aCalendarDate = CalendarDate.fromJSDate(this, true /*useUTC*/); - TimeZone.convertCalendarDateFromTimeZoneToTimeZone(aCalendarDate,Timezone.utcTimezone,timeZone); + TimeZone.convertCalendarDateFromTimeZoneToTimeZone(aCalendarDate,TimeZone.utcTimezone,timeZone); + aCalendarDate.zone = timeZone; return aCalendarDate; }, enumerable: false, diff --git a/core/extras/date.js b/core/extras/date.js index 5e3ba4aba7..92444d52ef 100644 --- a/core/extras/date.js +++ b/core/extras/date.js @@ -1,3 +1,6 @@ +var Range = require("../range").Range; + + /** * Defines extensions to intrinsic `Date` object. * @@ -20,9 +23,84 @@ Object.defineProperty(Date.prototype, "clone", { return new Date(this); }, writable: true, + enumerable: false, + configurable: true +}); + +Object.defineProperty(Date.prototype,"fullDayRange",{ + + get: function(date) { + var dayStart = this.clone(); + dayStart.setHours(0,0,0,0); + var dayEnd = dayStart.clone(); + dayEnd.setHours(23,59,59,999); + return new Range(dayStart,dayEnd); + }, + enumerable: false, + configurable: true +}); +Object.defineProperty(Date.prototype,"adjustComponentValues", { + value: function(year, monthIndex, days, hours, minutes, seconds, milliseconds) { + + // because of the duration optimizations it is much + // more efficient to grab all the values up front + // then set them directly (which will avoid a normalization call). + // So we don't actually normalize until we need it. + var millisecond, + second, + minute, + hour, + myDay, + month, + myYear; + + if(Number.isFinite(milliseconds)) { + millisecond = this.millisecond; + millisecond += milliseconds; + this.millisecond = milliseconds; + } + + if(Number.isFinite(seconds)) { + second = this.second; + second += seconds; + this.second = second; + } + + if(Number.isFinite(minutes)) { + inute = this.minute; + minute += minutes; + this.minute = minute; + } + + if(Number.isFinite(hours)) { + hour = this.hour; + hour += hours; + this.hour = hour; + } + + if(Number.isFinite(days)) { + myDay = this.day; + myDay += days; + this.day = myDay; + } + + if(Number.isFinite(monthIndex)) { + month = this.month; + month += monthIndex; + this.month = month; + } + + if(Number.isFinite(year)) { + myYear = this.year; + myYear += year; + this.year = myYear; + } + }, + enumerable: false, configurable: true }); + /** * Assess if an instance a date is valid * @@ -42,6 +120,7 @@ Object.defineProperty(Date, "isValidDate", { return date && Object.prototype.toString.call(date) === "[object Date]" && !isNaN(date); }, writable: true, + enumerable: false, configurable: true }); @@ -59,6 +138,7 @@ Object.defineProperty(Date, "isValidDateString", { return date && Object.prototype.toString.call(date) === "[object Date]" && !isNaN(date); }, writable: true, + enumerable: false, configurable: true }); From e92f2f3ce20f2cb20d20a892a0b1d1ae65f48174 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 5 Oct 2020 15:45:53 -0700 Subject: [PATCH 269/407] Fix bug for when converting using foreign descriptor raw data type mapping --- data/converter/raw-foreign-value-to-object-converter.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/converter/raw-foreign-value-to-object-converter.js b/data/converter/raw-foreign-value-to-object-converter.js index 99d76b29f0..4041b617a3 100644 --- a/data/converter/raw-foreign-value-to-object-converter.js +++ b/data/converter/raw-foreign-value-to-object-converter.js @@ -289,7 +289,9 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( Keep an eye on that. */ var valueDescriptor = this.foreignDescriptorForValue(v), - aCriteria = this.convertCriteriaForValue(v); + rawDataProperty = self.rawDataPropertyForForeignDescriptor(valueDescriptor), + foreignKeyValue = v[rawDataProperty], + aCriteria = this.convertCriteriaForValue(foreignKeyValue); return this._fetchConvertedDataForObjectDescriptorCriteria(valueDescriptor, aCriteria); } From 195b47f94b95f7377c4ce5038466a22376c7f663 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 5 Oct 2020 15:46:54 -0700 Subject: [PATCH 270/407] avoid creating an ampty array when accessing Dataquery.orderings --- data/model/data-query.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/data/model/data-query.js b/data/model/data-query.js index e76bacf386..4970246db0 100644 --- a/data/model/data-query.js +++ b/data/model/data-query.js @@ -173,9 +173,12 @@ exports.DataQuery = Montage.specialize(/** @lends DataQuery.prototype */ { */ orderings: { get: function () { - if (!this._orderings) { - this._orderings = []; - } + /* + Benoit, could break backward compatibility but it doesn't look like we relied on this. No point creating an empty attay just for checking if orderings have been set on a data query. + */ + // if (!this._orderings) { + // this._orderings = []; + // } return this._orderings; }, set: function (orderings) { From 6492c45deeffb7760b8ea335b1fa5b0e2f30f0fb Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 5 Oct 2020 15:55:18 -0700 Subject: [PATCH 271/407] - record object snapshot after first mapping to allow mapping to know it's the first mapping of an object if there are no snapshot so we can focus on required proeprties for now. This should be driven only by what the app needs, which should start by exploiting expressions observed, by bindings or directly. - improve MappingRule to handle the case where foreign descriptors and raw data type mappings are used - add method and implementation to allow RawDataService to sort DataStream's data client side if it's not done server side --- data/service/expression-data-mapping.js | 19 ++++++- data/service/mapping-rule.js | 14 +++++ data/service/raw-data-service.js | 75 +++++++++++++++++++++++-- 3 files changed, 100 insertions(+), 8 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index cc32855225..4a8c2837c6 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -696,12 +696,13 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData dataMatchingRules = this.mappingRulesForRawDataProperties(Object.keys(data)), ruleIterator = dataMatchingRules.values(), requisitePropertyNames = this.requisitePropertyNames, + hasSnapshot = !!this.service.snapshotForObject(object), aRule; while ((aRule = ruleIterator.next().value)) { - if((aRule.converter && (aRule.converter instanceof RawForeignValueToObjectConverter)) && - !requisitePropertyNames.has(aRule.sourcePath) && - (readExpressions && readExpressions.indexOf(aRule.targetPath) === -1)) { + if((!hasSnapshot && !requisitePropertyNames.has(aRule.targetPath)) || ((aRule.converter && (aRule.converter instanceof RawForeignValueToObjectConverter)) && + !requisitePropertyNames.has(aRule.targetPath) && + (readExpressions && readExpressions.indexOf(aRule.targetPath) === -1))) { continue; } @@ -1458,6 +1459,18 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData Benoit: adding && value to the condition as we don't want arrays with null in it */ if(isToMany && value) { + /* + When we arrive here coming from _assignInversePropertyValue() + doing object[propertyName] causes the trigger to go fetch the value of object's propertyName, which is async. + + So we should either continue to trigger that property , with + + mainService.getObjectProperties(object, [propertyName]) + + and when that promise resolves we continue the assignment, which might be unnecessary as we'd have that data in the result as this is propagation of fecthed data, not new, + + or find a way to jut access the local state without triggering the fetch and just update it. + */ if(!Array.isArray(object[propertyName])) { value = [value]; object[propertyName] = value; diff --git a/data/service/mapping-rule.js b/data/service/mapping-rule.js index 6d6eb58b6e..f3b24c8efb 100644 --- a/data/service/mapping-rule.js +++ b/data/service/mapping-rule.js @@ -100,6 +100,20 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { (_requirements || (_requirements = [])).push(subProperty.reverse().join(".")); } else if (type === "record") { _requirements = this._parseRequirementsFromRecord(syntax, _requirements); + } else if(type === "value" && !args && this.sourcePath === "this") { + /* + added for pattern used for polymorphic relationship, where we have multiple foreignKeys for each possible destination, and we need to look at the converter and it's foreignDescriptorMappings for their expression syntax. + */ + + var converter = this.converter, + foreignDescriptorMappings = converter && converter.foreignDescriptorMappings; + + if(foreignDescriptorMappings) { + for(var i=0, countI = foreignDescriptorMappings.length, iRawDataTypeMapping;(i Date: Mon, 5 Oct 2020 16:56:05 -0700 Subject: [PATCH 272/407] remove redundant registration --- data/service/user-identity-service.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/data/service/user-identity-service.js b/data/service/user-identity-service.js index 236ed4f5ac..3cbca31054 100644 --- a/data/service/user-identity-service.js +++ b/data/service/user-identity-service.js @@ -15,7 +15,11 @@ exports.UserIdentityService = UserIdentityService = RawDataService.specialize( / constructor: { value: function UserIdentityService() { RawDataService.call(this); - UserIdentityService.userIdentityServices.push(this); + /* + This is done in DataService's constructor as well, + needs to decide where is best, but not do it twice. + */ + //UserIdentityService.userIdentityServices.push(this); } }, providesUserIdentity: { From 71d71c4dfaaa0cb5d2c210ecc04dc5a3f01faf5a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 5 Oct 2020 17:16:15 -0700 Subject: [PATCH 273/407] cache systemTimeZone --- core/date/time-zone-core.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/core/date/time-zone-core.js b/core/date/time-zone-core.js index 28d749874f..9869d19ebf 100644 --- a/core/date/time-zone-core.js +++ b/core/date/time-zone-core.js @@ -78,13 +78,23 @@ Object.defineProperties(ICAL_Timezone_Prototype, { * @returns {Calendar} a new Calendar instance. */ Object.defineProperties(TimeZone, { - - "systemTimeZone": { + "_systemTimeZone": { + value: undefined, + enumerable: false, + writable: true, + configurable: true + }, + "_createSystemTimeZone": { get: function() { var systemLocaleIdentifier = currentEnvironment.systemLocaleIdentifier, resolvedOptions = Intl.DateTimeFormat(systemLocaleIdentifier).resolvedOptions(), timeZone = resolvedOptions.timeZone; /* "America/Los_Angeles" */ - return ICAL_TimezoneService.get(timeZone); + return (this._systemTimeZone = ICAL_TimezoneService.get(timeZone)); + } + }, + "systemTimeZone": { + get: function() { + return this._systemTimeZone || this._createSystemTimeZone; } } From 5248c7603b2ff9feceda70079b5361540e2532a3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 5 Oct 2020 18:56:42 -0700 Subject: [PATCH 274/407] Add new Date formatter using Intl.DateTimeFormat() --- .../international-date-to-string-formatter.js | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 core/converter/international-date-to-string-formatter.js diff --git a/core/converter/international-date-to-string-formatter.js b/core/converter/international-date-to-string-formatter.js new file mode 100644 index 0000000000..f5ab84bcf8 --- /dev/null +++ b/core/converter/international-date-to-string-formatter.js @@ -0,0 +1,66 @@ +/** + * @module montage/core/converter/internationalDateToStringFormatter + * @requires montage/core/converter/converter + */ +var Converter = require("./converter").Converter, + Locale = require("../locale").Locale; + +/** + * Inverts the value of a boolean value. + * + * @class InvertConverter + * @extends Converter + */ +var InternationalDateToStringFormatter = exports.InternationalDateToStringFormatter = Converter.specialize({ + + _locale: { + value: Locale.systemLocale + }, + + locale: { + get: function() { + return this._locale; + }, + set: function(value) { + if(value !== this._locale) { + this._locale = value; + this.__dayDateFormatter = null; + } + } + }, + + _options: { + value: undefined + }, + + options: { + get: function() { + return this._options; + }, + set: function(value) { + if(value !== this._options) { + this._options = value; + this.__dayDateFormatter = null; + } + } + }, + + __dayDateFormatter: { + value: undefined + }, + + _dayDateFormatter: { + get: function() { + if(!this.__dayDateFormatter) { + this.__dayDateFormatter = Intl.DateTimeFormat( + this._locale.identifier, this.options); + } + return this.__dayDateFormatter; + } + }, + convert: { + value: function (v) { + return this._dayDateFormatter.format(v); + } + }, +}); From 2c99ed71904ea030b17364ab9b8507192ccabbde Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 5 Oct 2020 21:25:51 -0700 Subject: [PATCH 275/407] fix typo --- core/converter/international-date-to-string-formatter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/converter/international-date-to-string-formatter.js b/core/converter/international-date-to-string-formatter.js index f5ab84bcf8..7afe68c89e 100644 --- a/core/converter/international-date-to-string-formatter.js +++ b/core/converter/international-date-to-string-formatter.js @@ -62,5 +62,5 @@ var InternationalDateToStringFormatter = exports.InternationalDateToStringFormat value: function (v) { return this._dayDateFormatter.format(v); } - }, + } }); From f2c3fb1e3d865dce4f8b4be80981702d7b0a53f2 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 5 Oct 2020 22:27:59 -0700 Subject: [PATCH 276/407] Fix a bug preventing the use of a converter in a binding in a values block --- core/serialization/deserializer/montage-interpreter.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/serialization/deserializer/montage-interpreter.js b/core/serialization/deserializer/montage-interpreter.js index bbe9626929..72818ca9fa 100644 --- a/core/serialization/deserializer/montage-interpreter.js +++ b/core/serialization/deserializer/montage-interpreter.js @@ -285,8 +285,7 @@ var MontageContext = Montage.specialize({ value = values[key]; //An expression based property - if ((typeof value === "object" && value && - Object.keys(value).length === 1 && + if (value && (typeof value === "object" && (ONE_WAY in value || TWO_WAY in value || ONE_ASSIGNMENT in value)) || key.indexOf('.') > -1 ) { From 2521ce14019240e0315ae02329084a82ca14657d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 5 Oct 2020 22:54:17 -0700 Subject: [PATCH 277/407] - add support for fetchLimit on query - check orderings before setting on query --- ui/data-editor.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ui/data-editor.js b/ui/data-editor.js index 03de994ffe..26210a23cd 100644 --- a/ui/data-editor.js +++ b/ui/data-editor.js @@ -76,7 +76,12 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { if(!this.__dataQuery) { if(this.type) { this.__dataQuery = DataQuery.withTypeAndCriteria(this.type,this.criteria); - this.__dataQuery.orderings = this.orderings; + if(this.orderings) { + this.__dataQuery.orderings = this.orderings; + } + if(this.fetchLimit) { + this.__dataQuery.fetchLimit = this.fetchLimit; + } } } return this.__dataQuery; @@ -239,6 +244,10 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { } }, + fetchLimit: { + value: undefined + }, + /** * A RangeController, TreeController, or equivalent object that provides sorting, filtering, * selection handling of a collection of object. From cc2f164fafe3ea52634acd8b4552b6c1086540e5 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 6 Oct 2020 00:53:34 -0700 Subject: [PATCH 278/407] quick-fix / WIP to better handle triggers on propertyDescriptors that have a definition --- data/service/data-trigger.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js index 17a15ea35e..fe2302675a 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -242,8 +242,28 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy writable: true, value: function (object) { var prototype, descriptor, getter, propertyName = this._propertyName; - // Start an asynchronous fetch of the property's value if necessary. - this.getObjectProperty(object); + + /* + Experiment to see if it would make sense to avoid triggering getObjectProperty during mapping? + */ + // if(!this._service.rootService._objectsBeingMapped.has(object) + // ) { + if(!this.propertyDescriptor.definition) { + + + /* + if the trigger's property descriptor has a definition, there are 2 cases: + 1. this._service.childServiceForType(this.propertyDescriptor.owner) might know how to process the whole expression server side, in which case we don't do anything and it will be handled in teh back-end. + + 2. We have to yake care of it client side, which means we have to get all expression requirements individually before it can be evaluated, so we'd have to do the equivalent of getObjectProperties() rather than just the usual this.getObjectProperty(object) + */ + + + // Start an asynchronous fetch of the property's value if necessary. + this.getObjectProperty(object); + } + + //} // Search the prototype chain for a getter for this property, // starting just after the prototype that called this method. From ee35557b71e501a7b38bdf59d312a213384f4525 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 6 Oct 2020 08:43:56 -0700 Subject: [PATCH 279/407] remove weak-map polyfill --- core/collections/weak-map.js | 2 +- package.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/collections/weak-map.js b/core/collections/weak-map.js index 4e9ad68207..0d0631d29c 100644 --- a/core/collections/weak-map.js +++ b/core/collections/weak-map.js @@ -1,2 +1,2 @@ -module.exports = (typeof WeakMap !== 'undefined') ? WeakMap : require("weak-map"); +module.exports = WeakMap; diff --git a/package.json b/package.json index 514cbe9a38..ecd227a103 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "dependencies": { "bluebird": "~3.5.5", "htmlparser2": "~3.0.5", - "weak-map": "^1.0.5", "lodash.kebabcase": "^4.1.1", "lodash.camelcase": "^4.3.0", "lodash.trim": "^4.5.1", From c0f05e4376c81dd11fa1f623ee6823b24eb7c576 Mon Sep 17 00:00:00 2001 From: romancortes Date: Wed, 7 Oct 2020 21:12:02 +0200 Subject: [PATCH 280/407] Fix minimal regression in Flow Fix an unnoticed regression that would rarely happen during a single frame after a resize given certain circunstances. --- ui/flow.reel/flow.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/flow.reel/flow.js b/ui/flow.reel/flow.js index 2fe39166f9..01fce4c235 100644 --- a/ui/flow.reel/flow.js +++ b/ui/flow.reel/flow.js @@ -1296,7 +1296,7 @@ var Flow = exports.Flow = Component.specialize( /** @lends Flow.prototype # */ { _updateVisibleIndexes: { value: function (newVisibleIndexes, newContentIndexes) { var oldVisibleIndexes = this._visibleIndexes, - oldIndexesLength = oldVisibleIndexes && !isNaN(oldVisibleIndexes.length) ? oldVisibleIndexes.length : 0, + oldIndexesLength, holes, j, i; @@ -1305,7 +1305,7 @@ var Flow = exports.Flow = Component.specialize( /** @lends Flow.prototype # */ { this._visibleIndexes.splice(newVisibleIndexes.length, Infinity); this._needsClearVisibleIndexes = false; } - + oldIndexesLength = oldVisibleIndexes && !isNaN(oldVisibleIndexes.length) ? oldVisibleIndexes.length : 0; // Search for viable holes, leave content at the same visible index // whenever possible. for (i = 0; i < oldIndexesLength; i++) { From 197fab1bb37bd327dd2fe89a59125761d4daecbc Mon Sep 17 00:00:00 2001 From: romancortes Date: Wed, 7 Oct 2020 21:19:22 +0200 Subject: [PATCH 281/407] Fix regression in Flow not adding iterations A previous optimisation created a regression preventing new iterations to be added / displayed in Flow under certain circunstances. --- ui/flow.reel/flow.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/flow.reel/flow.js b/ui/flow.reel/flow.js index 01fce4c235..31c28b0b06 100644 --- a/ui/flow.reel/flow.js +++ b/ui/flow.reel/flow.js @@ -1332,13 +1332,15 @@ var Flow = exports.Flow = Component.specialize( /** @lends Flow.prototype # */ { } // Fill the holes - if(holes) { + if (holes) { for (i = j = 0; (j < holes.length) && (i < newVisibleIndexes.length); i++) { if (newVisibleIndexes[i] !== null) { oldVisibleIndexes.set(holes[j], newVisibleIndexes[i]); j++; } } + } else { + i = 0; } // Add new values to the end if the visible indexes have grown for (j = oldIndexesLength; i < newVisibleIndexes.length; i++) { From 8e9944c024e6075ecb420cd43764be36a84093ff Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 8 Oct 2020 14:06:43 -0700 Subject: [PATCH 282/407] Push ExpressionDataMapping's service to the converters/reverters of it's mapping rules when set, to lay the ground to remove promise around a RawValueToObjectConverter's service property --- data/service/expression-data-mapping.js | 34 ++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 4a8c2837c6..cce264e7ad 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -538,10 +538,42 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData * Used to create fetches for relationships. * @type {DataService} */ - service: { + _service: { value: undefined }, + _propagateServiceToMappingRulesConverter: { + value: function(service, mappingRules) { + if(service && mappingRules) { + var valuesIterator = mappingRules.values(), + iRule; + + while((iRule = valuesIterator.next().value)) { + if(iRule.converter) { + iRule.converter.service = service; + } + if(iRule.reverter) { + iRule.reverter.service = service; + } + } + } + } + }, + + service: { + get: function () { + return this._service; + }, + set: function (value) { + if(value !== this._service) { + this._service = value; + + //Propagate to rules one and for all + this._propagateServiceToMappingRulesConverter(value, this.objectMappingRules); + this._propagateServiceToMappingRulesConverter(value, this.rawDataMappingRules); + } + } + }, From 2507f7c8a5557bee156c381189d5c21c3eea7d92 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 8 Oct 2020 14:09:52 -0700 Subject: [PATCH 283/407] Improvements to reduce DB access: - add reuse of a fetch promise for a type/criteria if one is already sent and in-flight - check local known objects to see if we have it before attempting to fetch it --- .../raw-foreign-value-to-object-converter.js | 177 ++++++++++++++++-- 1 file changed, 158 insertions(+), 19 deletions(-) diff --git a/data/converter/raw-foreign-value-to-object-converter.js b/data/converter/raw-foreign-value-to-object-converter.js index 4041b617a3..d5e29dbc0b 100644 --- a/data/converter/raw-foreign-value-to-object-converter.js +++ b/data/converter/raw-foreign-value-to-object-converter.js @@ -61,29 +61,141 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( value: undefined }, - /********************************************************************* - * Public API - */ - /** - * Converts the fault for the relationship to an actual object that has an ObjectDescriptor. - * @function - * @param {Property} v The value to format. - * @returns {Promise} A promise for the referenced object. The promise is - * fulfilled after the object is successfully fetched. - * - */ + /* + cache: - _fetchConvertedDataForObjectDescriptorCriteria: { - value: function(typeToFetch, criteria) { - if (this.serviceIdentifier) { - criteria.parameters.serviceIdentifier = this.serviceIdentifier; + Map: ObjectDescriptor -> Map: criteriaExpression -> Map: JSON.stringify(criteria.parameters) -> Promise + + */ + _fetchPromiseByObjectDescriptorByCriteriaExpressionByCriteriaParameters: { + value: new Map() + }, + + _fetchPromiseMapForObjectDescriptor: { + value: function(objectDescriptor) { + var map = this._fetchPromiseByObjectDescriptorByCriteriaExpressionByCriteriaParameters.get(objectDescriptor); + if(!map) { + map = new Map(); + this._fetchPromiseByObjectDescriptorByCriteriaExpressionByCriteriaParameters.set(objectDescriptor,map); } + return map; + } + }, - var query = DataQuery.withTypeAndCriteria(typeToFetch, criteria), - self = this; + _fetchPromiseMapForObjectDescriptorCriteria: { + value: function(objectDescriptor, criteria) { + var objectDescriptorMap = this._fetchPromiseMapForObjectDescriptor(objectDescriptor), + criteriaExpressionMap = objectDescriptorMap.get(criteria.expression); + if(!criteriaExpressionMap) { + criteriaExpressionMap = new Map(); + objectDescriptorMap.set(criteria.expression,criteriaExpressionMap); + } + return criteriaExpressionMap; + } + }, + + _registeredFetchPromiseMapForObjectDescriptorCriteria: { + value: function(objectDescriptor, criteria) { + var criteriaExpressionMap = this._fetchPromiseMapForObjectDescriptorCriteria(objectDescriptor,criteria), + parametersKey = typeof criteria.parameters === "string" ? criteria.parameters : JSON.stringify(criteria.parameters); + + return fetchPromise = criteriaExpressionMap.get(parametersKey); + } + }, + + _registerFetchPromiseForObjectDescriptorCriteria: { + value: function(fetchPromise, objectDescriptor, criteria) { + var criteriaExpressionMap = this._fetchPromiseMapForObjectDescriptorCriteria(objectDescriptor,criteria), + parametersKey = typeof criteria.parameters === "string" ? criteria.parameters : JSON.stringify(criteria.parameters); + + return criteriaExpressionMap.set(parametersKey,fetchPromise); + } + }, + _unregisterFetchPromiseForObjectDescriptorCriteria: { + value: function(objectDescriptor, criteria) { + var criteriaExpressionMap = this._fetchPromiseMapForObjectDescriptorCriteria(objectDescriptor,criteria), + parametersKey = typeof criteria.parameters === "string" ? criteria.parameters : JSON.stringify(criteria.parameters); + + return criteriaExpressionMap.delete(parametersKey); + } + }, + _lookupExistingObjectForObjectDescriptorCriteria: { + value: function(typeToFetch, criteria, service) { + var dataIdentifier, existingObject = null; + /* + 1) dataIdentifierForTypePrimaryKey(type, primaryKey) + + 2) objectForDataIdentifier + */ + /* + Simplifying assumptions for now: + if parameters is a string, it's the primary key + if parameters is an array, it's an array of primaryKeys + */ + if(typeof criteria.parameters === "string") { + dataIdentifier = service.dataIdentifierForTypePrimaryKey(typeToFetch,criteria.parameters); + existingObject = service.rootService.objectForDataIdentifier(dataIdentifier); + } else if(Array.isArray(criteria.parameters)) { + var rootService = service.rootService, + array = criteria.parameters, i=0, countI = array.length, iObject; + + for(; (i 0) { + if(localResult.length) { + //We found some locally but not all + localPartialResultPromise = Promise.resolve(localResult); + } else { + //we didn't find anything locally + localPartialResultPromise = null; + } + } else { + //We found everything locally, we're done: + return Promise.resolve(localResult); + } + + } else { + //We found it, we're done: + return Promise.resolve(localResult); + } + } + + + if (self.serviceIdentifier) { + criteria.parameters.serviceIdentifier = this.serviceIdentifier; + } + + var fetchPromise = self._registeredFetchPromiseMapForObjectDescriptorCriteria(typeToFetch,criteria); + + if(!fetchPromise) { + var query = DataQuery.withTypeAndCriteria(typeToFetch, criteria); + /* When we fetch objects that have inverse relationships on each others none can complete their mapRawDataProcess because the first one's promise for mapping the relationship to the second never commpletes because the second one itself has it's raw data the foreignKey to the first and attemps to do so by default on processing operations, where the previous way was only looping on requisite proprties. If both relationships were requisite, on each side we'd end up with the same problem. @@ -164,8 +276,20 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( */ + fetchPromise = service.rootService.fetchData(query) + .then(function(value) { + self._unregisterFetchPromiseForObjectDescriptorCriteria(typeToFetch, criteria); + return value; + }); + + self._registerFetchPromiseForObjectDescriptorCriteria(fetchPromise, typeToFetch, criteria); + } + + if(localPartialResultPromise) { + fetchPromise = Promise.all([localPartialResultPromise,fetchPromise]); + } - return service.rootService.fetchData(query); + return fetchPromise; }) : null; } @@ -201,7 +325,9 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( convertCriteriaForValue: { value: function(value) { - return new Criteria().initWithSyntax(this.convertSyntax, value); + var criteria = new Criteria().initWithSyntax(this.convertSyntax, value); + criteria._expression = this.convertExpression; + return criteria; } }, @@ -230,6 +356,19 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( value: undefined }, + /********************************************************************* + * Public API + */ + + /** + * Converts the fault for the relationship to an actual object that has an ObjectDescriptor. + * @function + * @param {Property} v The value to format. + * @returns {Promise} A promise for the referenced object. The promise is + * fulfilled after the object is successfully fetched. + * + */ + convert: { value: function (v) { From 95a9cedcef8700275633e20642c5cc02fb2842f2 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 8 Oct 2020 23:40:17 -0700 Subject: [PATCH 284/407] Remove polyfills for ES5 Map and Set --- core/collections/_map.js | 124 +++++++------- core/collections/_set.js | 348 +++++++++++++++++++-------------------- core/collections/set.js | 212 ++++++++++++------------ 3 files changed, 342 insertions(+), 342 deletions(-) diff --git a/core/collections/_map.js b/core/collections/_map.js index 76834c4465..5ac19f947a 100644 --- a/core/collections/_map.js +++ b/core/collections/_map.js @@ -246,65 +246,65 @@ if((global.Map !== void 0) && (typeof global.Set.prototype.values === "function" var ChangeDispatchMap = Object.create(Map.prototype, observableMapProperties); } - var Set = require("./_set").CollectionsSet; - var GenericMap = require("./generic-map"); - - CollectionsMap = Map = function Map(values, equals, hash, getDefault) { - if (!(this instanceof Map)) { - return new Map(values, equals, hash, getDefault); - } - equals = equals || Object.equals; - hash = hash || Object.hash; - getDefault = getDefault || Function.noop; - this.contentEquals = equals; - this.contentHash = hash; - this.getDefault = getDefault; - this.store = new Set( - undefined, - function keysEqual(a, b) { - return equals(a.key, b.key); - }, - function keyHash(item) { - return hash(item.key); - } - ); - this.length = 0; - this.addEach(values); - } - - Map.Map = Map; // hack so require("map").Map will work in MontageJS - - Object.addEach(Map.prototype, GenericCollection.prototype); - Object.addEach(Map.prototype, GenericMap.prototype); // overrides GenericCollection - Object.defineProperty(Map.prototype,"size",GenericCollection._sizePropertyDescriptor); - - Map.from = GenericCollection.from; - - Map.prototype.constructClone = function (values) { - return new this.constructor( - values, - this.contentEquals, - this.contentHash, - this.getDefault - ); - }; - - Map.prototype.log = function (charmap, logNode, callback, thisp) { - logNode = logNode || this.logNode; - this.store.log(charmap, function (node, log, logBefore) { - logNode(node.value.value, log, logBefore); - }, callback, thisp); - }; - - Map.prototype.logNode = function (node, log) { - log(' key: ' + node.key); - log(' value: ' + node.value); - }; - - if(!GlobalMap) { - module.exports = CollectionsMap; - } - else { - module.exports = GlobalMap; - GlobalMap.CollectionsMap = CollectionsMap; - } + // var Set = require("./_set").CollectionsSet; + // var GenericMap = require("./generic-map"); + + // CollectionsMap = Map = function Map(values, equals, hash, getDefault) { + // if (!(this instanceof Map)) { + // return new Map(values, equals, hash, getDefault); + // } + // equals = equals || Object.equals; + // hash = hash || Object.hash; + // getDefault = getDefault || Function.noop; + // this.contentEquals = equals; + // this.contentHash = hash; + // this.getDefault = getDefault; + // this.store = new Set( + // undefined, + // function keysEqual(a, b) { + // return equals(a.key, b.key); + // }, + // function keyHash(item) { + // return hash(item.key); + // } + // ); + // this.length = 0; + // this.addEach(values); + // } + + // Map.Map = Map; // hack so require("map").Map will work in MontageJS + + // Object.addEach(Map.prototype, GenericCollection.prototype); + // Object.addEach(Map.prototype, GenericMap.prototype); // overrides GenericCollection + // Object.defineProperty(Map.prototype,"size",GenericCollection._sizePropertyDescriptor); + + // Map.from = GenericCollection.from; + + // Map.prototype.constructClone = function (values) { + // return new this.constructor( + // values, + // this.contentEquals, + // this.contentHash, + // this.getDefault + // ); + // }; + + // Map.prototype.log = function (charmap, logNode, callback, thisp) { + // logNode = logNode || this.logNode; + // this.store.log(charmap, function (node, log, logBefore) { + // logNode(node.value.value, log, logBefore); + // }, callback, thisp); + // }; + + // Map.prototype.logNode = function (node, log) { + // log(' key: ' + node.key); + // log(' value: ' + node.value); + // }; + + // if(!GlobalMap) { + // module.exports = CollectionsMap; + // } + // else { + // module.exports = GlobalMap; + // GlobalMap.CollectionsMap = CollectionsMap; + // } diff --git a/core/collections/_set.js b/core/collections/_set.js index 14a4b0da83..17719d7848 100644 --- a/core/collections/_set.js +++ b/core/collections/_set.js @@ -103,180 +103,180 @@ if((global.Set !== void 0) && (typeof global.Set.prototype.values === "function" - var List = require("./_list"); - var FastSet = require("./_fast-set"); - var Iterator = require("./iterator"); - - CollectionsSet = function CollectionsSet(values, equals, hash, getDefault) { - return CollectionsSet._init(CollectionsSet, this, values, equals, hash, getDefault); - } - - CollectionsSet._init = function (constructor, object, values, equals, hash, getDefault) { - if (!(object instanceof constructor)) { - return new constructor(values, equals, hash, getDefault); - } - equals = equals || Object.equals; - hash = hash || Object.hash; - getDefault = getDefault || Function.noop; - object.contentEquals = equals; - object.contentHash = hash; - object.getDefault = getDefault; - // a list of values in insertion order, used for all operations that depend - // on iterating in insertion order - object.order = new object.Order(undefined, equals); - // a set of nodes from the order list, indexed by the corresponding value, - // used for all operations that need to quickly seek value in the list - object.store = new object.Store( - undefined, - function (a, b) { - return equals(a.value, b.value); - }, - function (node) { - return hash(node.value); - } - ); - object.length = 0; - object.addEach(values); - - } - - CollectionsSet.Set = CollectionsSet; // hack so require("set").Set will work in MontageJS - CollectionsSet.CollectionsSet = CollectionsSet; - - Object.addEach(CollectionsSet.prototype, GenericCollection.prototype); - Object.addEach(CollectionsSet.prototype, GenericSet.prototype); - - CollectionsSet.from = GenericCollection.from; - - Object.defineProperty(CollectionsSet.prototype,"size",GenericCollection._sizePropertyDescriptor); - - //Overrides for consistency: - // Set.prototype.forEach = GenericCollection.prototype.forEach; - - - CollectionsSet.prototype.Order = List; - CollectionsSet.prototype.Store = FastSet; - - CollectionsSet.prototype.constructClone = function (values) { - return new this.constructor(values, this.contentEquals, this.contentHash, this.getDefault); - }; - - CollectionsSet.prototype.has = function (value) { - var node = new this.order.Node(value); - return this.store.has(node); - }; - - CollectionsSet.prototype.get = function (value, equals) { - if (equals) { - throw new Error("Set#get does not support second argument: equals"); - } - var node = new this.order.Node(value); - node = this.store.get(node); - if (node) { - return node.value; - } else { - return this.getDefault(value); - } - }; - - CollectionsSet.prototype.add = function (value) { - var node = new this.order.Node(value); - if (!this.store.has(node)) { - var index = this.length; - this.order.add(value); - node = this.order.head.prev; - this.store.add(node); - this.length++; - return true; - } - return false; - }; - - CollectionsSet.prototype["delete"] = function (value, equals) { - if (equals) { - throw new Error("Set#delete does not support second argument: equals"); - } - var node = new this.order.Node(value); - if (this.store.has(node)) { - node = this.store.get(node); - this.store["delete"](node); // removes from the set - this.order.splice(node, 1); // removes the node from the list - this.length--; - return true; - } - return false; - }; - - CollectionsSet.prototype.pop = function () { - if (this.length) { - var result = this.order.head.prev.value; - this["delete"](result); - return result; - } - }; - - CollectionsSet.prototype.shift = function () { - if (this.length) { - var result = this.order.head.next.value; - this["delete"](result); - return result; - } - }; - - CollectionsSet.prototype.one = function () { - if (this.length > 0) { - return this.store.one().value; - } - }; - - CollectionsSet.prototype.clear = function () { - this.store.clear(); - this.order.clear(); - this.length = 0; - }; - Object.defineProperty(CollectionsSet.prototype,"_clear", { - value: CollectionsSet.prototype.clear - }); - - CollectionsSet.prototype.reduce = function (callback, basis /*, thisp*/) { - var thisp = arguments[2]; - var list = this.order; - var index = 0; - return list.reduce(function (basis, value) { - return callback.call(thisp, basis, value, index++, this); - }, basis, this); - }; - - CollectionsSet.prototype.reduceRight = function (callback, basis /*, thisp*/) { - var thisp = arguments[2]; - var list = this.order; - var index = this.length - 1; - return list.reduceRight(function (basis, value) { - return callback.call(thisp, basis, value, index--, this); - }, basis, this); - }; - - CollectionsSet.prototype.iterate = function () { - return this.order.iterate(); - }; - - CollectionsSet.prototype.values = function () { - return new Iterator(this.valuesArray(), true); - }; - - CollectionsSet.prototype.log = function () { - var set = this.store; - return set.log.apply(set, arguments); - }; - - - -if(!GlobalSet) { - module.exports = CollectionsSet; -} -else { +// var List = require("./_list"); +// var FastSet = require("./_fast-set"); +// var Iterator = require("./iterator"); + +// CollectionsSet = function CollectionsSet(values, equals, hash, getDefault) { +// return CollectionsSet._init(CollectionsSet, this, values, equals, hash, getDefault); +// } + +// CollectionsSet._init = function (constructor, object, values, equals, hash, getDefault) { +// if (!(object instanceof constructor)) { +// return new constructor(values, equals, hash, getDefault); +// } +// equals = equals || Object.equals; +// hash = hash || Object.hash; +// getDefault = getDefault || Function.noop; +// object.contentEquals = equals; +// object.contentHash = hash; +// object.getDefault = getDefault; +// // a list of values in insertion order, used for all operations that depend +// // on iterating in insertion order +// object.order = new object.Order(undefined, equals); +// // a set of nodes from the order list, indexed by the corresponding value, +// // used for all operations that need to quickly seek value in the list +// object.store = new object.Store( +// undefined, +// function (a, b) { +// return equals(a.value, b.value); +// }, +// function (node) { +// return hash(node.value); +// } +// ); +// object.length = 0; +// object.addEach(values); + +// } + +// CollectionsSet.Set = CollectionsSet; // hack so require("set").Set will work in MontageJS +// CollectionsSet.CollectionsSet = CollectionsSet; + +// Object.addEach(CollectionsSet.prototype, GenericCollection.prototype); +// Object.addEach(CollectionsSet.prototype, GenericSet.prototype); + +// CollectionsSet.from = GenericCollection.from; + +// Object.defineProperty(CollectionsSet.prototype,"size",GenericCollection._sizePropertyDescriptor); + +// //Overrides for consistency: +// // Set.prototype.forEach = GenericCollection.prototype.forEach; + + +// CollectionsSet.prototype.Order = List; +// CollectionsSet.prototype.Store = FastSet; + +// CollectionsSet.prototype.constructClone = function (values) { +// return new this.constructor(values, this.contentEquals, this.contentHash, this.getDefault); +// }; + +// CollectionsSet.prototype.has = function (value) { +// var node = new this.order.Node(value); +// return this.store.has(node); +// }; + +// CollectionsSet.prototype.get = function (value, equals) { +// if (equals) { +// throw new Error("Set#get does not support second argument: equals"); +// } +// var node = new this.order.Node(value); +// node = this.store.get(node); +// if (node) { +// return node.value; +// } else { +// return this.getDefault(value); +// } +// }; + +// CollectionsSet.prototype.add = function (value) { +// var node = new this.order.Node(value); +// if (!this.store.has(node)) { +// var index = this.length; +// this.order.add(value); +// node = this.order.head.prev; +// this.store.add(node); +// this.length++; +// return true; +// } +// return false; +// }; + +// CollectionsSet.prototype["delete"] = function (value, equals) { +// if (equals) { +// throw new Error("Set#delete does not support second argument: equals"); +// } +// var node = new this.order.Node(value); +// if (this.store.has(node)) { +// node = this.store.get(node); +// this.store["delete"](node); // removes from the set +// this.order.splice(node, 1); // removes the node from the list +// this.length--; +// return true; +// } +// return false; +// }; + +// CollectionsSet.prototype.pop = function () { +// if (this.length) { +// var result = this.order.head.prev.value; +// this["delete"](result); +// return result; +// } +// }; + +// CollectionsSet.prototype.shift = function () { +// if (this.length) { +// var result = this.order.head.next.value; +// this["delete"](result); +// return result; +// } +// }; + +// CollectionsSet.prototype.one = function () { +// if (this.length > 0) { +// return this.store.one().value; +// } +// }; + +// CollectionsSet.prototype.clear = function () { +// this.store.clear(); +// this.order.clear(); +// this.length = 0; +// }; +// Object.defineProperty(CollectionsSet.prototype,"_clear", { +// value: CollectionsSet.prototype.clear +// }); + +// CollectionsSet.prototype.reduce = function (callback, basis /*, thisp*/) { +// var thisp = arguments[2]; +// var list = this.order; +// var index = 0; +// return list.reduce(function (basis, value) { +// return callback.call(thisp, basis, value, index++, this); +// }, basis, this); +// }; + +// CollectionsSet.prototype.reduceRight = function (callback, basis /*, thisp*/) { +// var thisp = arguments[2]; +// var list = this.order; +// var index = this.length - 1; +// return list.reduceRight(function (basis, value) { +// return callback.call(thisp, basis, value, index--, this); +// }, basis, this); +// }; + +// CollectionsSet.prototype.iterate = function () { +// return this.order.iterate(); +// }; + +// CollectionsSet.prototype.values = function () { +// return new Iterator(this.valuesArray(), true); +// }; + +// CollectionsSet.prototype.log = function () { +// var set = this.store; +// return set.log.apply(set, arguments); +// }; + + + +// if(!GlobalSet) { +// module.exports = CollectionsSet; +// } +// else { GlobalSet.prototype.valuesArray = GenericSet.prototype.valuesArray; GlobalSet.prototype.entriesArray = GenericSet.prototype.entriesArray; module.exports = GlobalSet; - GlobalSet.CollectionsSet = CollectionsSet; -} +// GlobalSet.CollectionsSet = CollectionsSet; +// } diff --git a/core/collections/set.js b/core/collections/set.js index ea55ab31c7..6e167ad38a 100644 --- a/core/collections/set.js +++ b/core/collections/set.js @@ -52,7 +52,7 @@ if( (global.Set !== void 0) && (typeof global.Set.prototype.values === "function if (this.dispatchesRangeChanges) { clearing = this.toArray(); this.dispatchBeforeRangeChange(this._dispatchEmptyArray, clearing, 0); - + } set_clear.call(this); @@ -138,113 +138,113 @@ if( (global.Set !== void 0) && (typeof global.Set.prototype.values === "function Object.defineEach(Set.prototype, MapChanges.prototype, false, /*configurable*/true, /*enumerable*/ false, /*writable*/true); //This is really only for testing - Object.defineProperty(Set, "_setupCollectionSet", { - value: setupCollectionSet, - writable: true, - configurable: true, - enumerable: false - }); - -} -else { - setupCollectionSet(); -} - -function setupCollectionSet() { - var _CollectionsSet = Set.CollectionsSet; - - var CollectionsSet = function CollectionsSet(values, equals, hash, getDefault) { - return _CollectionsSet._init(CollectionsSet, this, values, equals, hash, getDefault); - } - - // hack so require("set").Set will work in MontageJS - CollectionsSet.Set = CollectionsSet; - CollectionsSet.from = _CollectionsSet.from; - Set.CollectionsSet = CollectionsSet; - - CollectionsSet.prototype = new _CollectionsSet(); - CollectionsSet.prototype.constructor = CollectionsSet; - - var List = require("./list"); - var FastSet = require("./fast-set"); - CollectionsSet.prototype.Order = List; - CollectionsSet.prototype.Store = FastSet; - - Object.defineProperty(CollectionsSet.prototype,"_dispatchEmptyArray", { - value: [] - }); - - CollectionsSet.prototype.add = function (value) { - var node = new this.order.Node(value); - if (!this.store.has(node)) { - var index = this.length; - var dispatchValueArray = [value]; - this.dispatchBeforeOwnPropertyChange(SIZE, index); - if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange(dispatchValueArray, this._dispatchEmptyArray, index); - } - this.order.add(value); - node = this.order.head.prev; - this.store.add(node); - this.length++; - if (this.dispatchesRangeChanges) { - this.dispatchRangeChange(dispatchValueArray, this._dispatchEmptyArray, index); - } - this.dispatchOwnPropertyChange(SIZE, index + 1); - return true; - } - return false; - }; - CollectionsSet.prototype["delete"] = function (value, equals) { - if (equals) { - throw new Error("Set#delete does not support second argument: equals"); - } - var node = new this.order.Node(value); - if (this.store.has(node)) { - node = this.store.get(node); - var dispatchValueArray = [value]; - this.dispatchBeforeOwnPropertyChange(SIZE, this.length); - if (this.dispatchesRangeChanges) { - this.dispatchBeforeRangeChange(this._dispatchEmptyArray, dispatchValueArray, node.index); - } - this.store["delete"](node); // removes from the set - this.order.splice(node, 1); // removes the node from the list - this.length--; - if (this.dispatchesRangeChanges) { - this.dispatchRangeChange(this._dispatchEmptyArray, dispatchValueArray, node.index); - } - this.dispatchOwnPropertyChange(SIZE, this.length); - return true; - } - return false; - }; - CollectionsSet.prototype.clear = function () { - var clearing; - var length = this.length; - if (length) { - this.dispatchBeforeOwnPropertyChange(SIZE, length); - } - if (this.dispatchesRangeChanges) { - clearing = this.toArray(); - this.dispatchBeforeRangeChange(this._dispatchEmptyArray, clearing, 0); - } - this._clear(); - if (this.dispatchesRangeChanges) { - this.dispatchRangeChange(this._dispatchEmptyArray, clearing, 0); - } - if (length) { - this.dispatchOwnPropertyChange(SIZE, 0); - } - }; - - Object.addEach(Set.CollectionsSet.prototype, PropertyChanges.prototype); - Object.addEach(Set.CollectionsSet.prototype, RangeChanges.prototype); - Set.CollectionsSet.prototype.makeObservable = function () { - this.order.makeObservable(); - }; + // Object.defineProperty(Set, "_setupCollectionSet", { + // value: setupCollectionSet, + // writable: true, + // configurable: true, + // enumerable: false + // }); - module.exports = CollectionsSet; } +// else { +// setupCollectionSet(); +// } + +// function setupCollectionSet() { +// var _CollectionsSet = Set.CollectionsSet; + +// var CollectionsSet = function CollectionsSet(values, equals, hash, getDefault) { +// return _CollectionsSet._init(CollectionsSet, this, values, equals, hash, getDefault); +// } + +// // hack so require("set").Set will work in MontageJS +// CollectionsSet.Set = CollectionsSet; +// CollectionsSet.from = _CollectionsSet.from; +// Set.CollectionsSet = CollectionsSet; + +// CollectionsSet.prototype = new _CollectionsSet(); +// CollectionsSet.prototype.constructor = CollectionsSet; + +// var List = require("./list"); +// var FastSet = require("./fast-set"); +// CollectionsSet.prototype.Order = List; +// CollectionsSet.prototype.Store = FastSet; + +// Object.defineProperty(CollectionsSet.prototype,"_dispatchEmptyArray", { +// value: [] +// }); + +// CollectionsSet.prototype.add = function (value) { +// var node = new this.order.Node(value); +// if (!this.store.has(node)) { +// var index = this.length; +// var dispatchValueArray = [value]; +// this.dispatchBeforeOwnPropertyChange(SIZE, index); +// if (this.dispatchesRangeChanges) { +// this.dispatchBeforeRangeChange(dispatchValueArray, this._dispatchEmptyArray, index); +// } +// this.order.add(value); +// node = this.order.head.prev; +// this.store.add(node); +// this.length++; +// if (this.dispatchesRangeChanges) { +// this.dispatchRangeChange(dispatchValueArray, this._dispatchEmptyArray, index); +// } +// this.dispatchOwnPropertyChange(SIZE, index + 1); +// return true; +// } +// return false; +// }; +// CollectionsSet.prototype["delete"] = function (value, equals) { +// if (equals) { +// throw new Error("Set#delete does not support second argument: equals"); +// } +// var node = new this.order.Node(value); +// if (this.store.has(node)) { +// node = this.store.get(node); +// var dispatchValueArray = [value]; +// this.dispatchBeforeOwnPropertyChange(SIZE, this.length); +// if (this.dispatchesRangeChanges) { +// this.dispatchBeforeRangeChange(this._dispatchEmptyArray, dispatchValueArray, node.index); +// } +// this.store["delete"](node); // removes from the set +// this.order.splice(node, 1); // removes the node from the list +// this.length--; +// if (this.dispatchesRangeChanges) { +// this.dispatchRangeChange(this._dispatchEmptyArray, dispatchValueArray, node.index); +// } +// this.dispatchOwnPropertyChange(SIZE, this.length); +// return true; +// } +// return false; +// }; +// CollectionsSet.prototype.clear = function () { +// var clearing; +// var length = this.length; +// if (length) { +// this.dispatchBeforeOwnPropertyChange(SIZE, length); +// } +// if (this.dispatchesRangeChanges) { +// clearing = this.toArray(); +// this.dispatchBeforeRangeChange(this._dispatchEmptyArray, clearing, 0); +// } +// this._clear(); +// if (this.dispatchesRangeChanges) { +// this.dispatchRangeChange(this._dispatchEmptyArray, clearing, 0); +// } +// if (length) { +// this.dispatchOwnPropertyChange(SIZE, 0); +// } +// }; + +// Object.addEach(Set.CollectionsSet.prototype, PropertyChanges.prototype); +// Object.addEach(Set.CollectionsSet.prototype, RangeChanges.prototype); +// Set.CollectionsSet.prototype.makeObservable = function () { +// this.order.makeObservable(); +// }; + +// module.exports = CollectionsSet; +// } From 77c28317d793348c7a4f4692c6cfd0894a89427f Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 8 Oct 2020 23:41:17 -0700 Subject: [PATCH 285/407] update to additionally save each time zone in it's own file --- core/date/tools/compile-zones.js | 46 ++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/core/date/tools/compile-zones.js b/core/date/tools/compile-zones.js index 4706f90aa5..252eacd7a2 100644 --- a/core/date/tools/compile-zones.js +++ b/core/date/tools/compile-zones.js @@ -89,6 +89,39 @@ var zones = JSON.parse(zonesJson); var fetchedZones, fetchedZonesJSONString = ""; const https = require('https'); +function writeFileSyncRecursive(filename, content, charset) { + // -- normalize path separator to '/' instead of path.sep, + // -- as / works in node for Windows as well, and mixed \\ and / can appear in the path + let filepath = filename.replace(/\\/g,'/'); + + // -- preparation to allow absolute paths as well + let root = ''; + if (filepath[0] === '/') { + root = '/'; + filepath = filepath.slice(1); + } + else if (filepath[1] === ':') { + root = filepath.slice(0,3); // c:\ + filepath = filepath.slice(3); + } + + // -- create folders all the way down + const folders = filepath.split('/').slice(0, -1); // remove last item, file + folders.reduce( + (acc, folder) => { + const folderPath = acc + folder + '/'; + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath); + } + return folderPath + }, + root // first 'acc', important + ); + + // -- write file + fs.writeFileSync(root + filepath, content, charset); +} + function icsString(timeZoneId, icsData) { @@ -98,15 +131,19 @@ function icsString(timeZoneId, icsData) { function compileTimeZones(zones) { const out = {}; Object.keys(zones.zones).forEach((timeZoneId) => { - var icsData = zones.zones[timeZoneId].ics.join("\r\n"); + var icsData = zones.zones[timeZoneId].ics.join("\r\n"), + singleOut = {}; out[timeZoneId] = icsString(timeZoneId,icsData); - //fs.writeFileSync('../time-zone-data/zones-compiled.json', JSON.stringify(out)); + singleOut[timeZoneId] = out[timeZoneId]; + + writeFileSyncRecursive('../time-zone-data/'+timeZoneId+'.json', JSON.stringify(singleOut)); }); Object.keys(zones.aliases).forEach((timeZoneId) => { var previousAliasTo = zones.aliases[timeZoneId].aliasTo, nextAliasTo, + singleOut, icsData; while(zones.aliases[previousAliasTo] && (nextAliasTo = zones.aliases[previousAliasTo].aliasTo)) { previousAliasTo = nextAliasTo; @@ -114,6 +151,11 @@ function compileTimeZones(zones) { if (zones.zones[previousAliasTo]) { icsData = zones.zones[previousAliasTo].ics.join("\r\n"); out[timeZoneId] = icsString(timeZoneId,icsData); + + singleOut = {}; + singleOut[timeZoneId] = out[timeZoneId]; + + writeFileSyncRecursive('../time-zone-data/'+timeZoneId+'.json', JSON.stringify(singleOut)); } else { console.warn(`${previousAliasTo} (${timeZoneId}) not found, skipping`); } From bb38375e53ef0824b8140b880dda5cef0b0fb8b1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 8 Oct 2020 23:43:45 -0700 Subject: [PATCH 286/407] add a way to call a data object property getter and pass an argument to prevent the trigger to trigger a fetch, useful during mapping --- data/service/data-trigger.js | 16 ++++++++-------- data/service/expression-data-mapping.js | 6 ++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js index fe2302675a..9f73b1a8b6 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -240,7 +240,7 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy _getValue: { configurable: true, writable: true, - value: function (object) { + value: function (object, shouldFetch) { var prototype, descriptor, getter, propertyName = this._propertyName; /* @@ -248,7 +248,7 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy */ // if(!this._service.rootService._objectsBeingMapped.has(object) // ) { - if(!this.propertyDescriptor.definition) { + if(shouldFetch !== false && !this.propertyDescriptor.definition) { /* @@ -623,8 +623,8 @@ Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ (DataTrig trigger.propertyDescriptor = descriptor; trigger._isGlobal = descriptor.isGlobal; Montage.defineProperty(prototype, name, { - get: function () { - return trigger._getValue(this); + get: function (shouldFetch) { + return trigger._getValue(this,shouldFetch); }, set: function (value) { trigger._setValue(this, value); @@ -677,11 +677,11 @@ Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ (DataTrig trigger._isGlobal = descriptor.isGlobal; if (descriptor.definition) { var propertyDescriptor = { - get: function () { + get: function (shouldFetch) { if (!this.getBinding(descriptor.name)) { this.defineBinding(descriptor.name, {"<-": descriptor.definition}); } - return trigger._getValue(this); + return trigger._getValue(this,shouldFetch); // return (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._getValue(this); } }; @@ -694,8 +694,8 @@ Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ (DataTrig Montage.defineProperty(prototype, descriptor.name, propertyDescriptor); } else { Montage.defineProperty(prototype, descriptor.name, { - get: function () { - return trigger._getValue(this); + get: function (shouldFetch) { + return trigger._getValue(this,shouldFetch); // return (trigger||(trigger = DataTrigger._createTrigger(service, objectDescriptor, prototype, name,descriptor)))._getValue(this); }, set: function (value) { diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index cce264e7ad..ddbb7addd9 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -1503,12 +1503,14 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData or find a way to jut access the local state without triggering the fetch and just update it. */ - if(!Array.isArray(object[propertyName])) { + //We call the getter passing shouldFetch = false flag stating that it's an internal call + var objectPropertyValue = Object.getPropertyDescriptor(object,propertyName).get(/*shouldFetch*/false); + if(!Array.isArray(objectPropertyValue)) { value = [value]; object[propertyName] = value; } else { - object[propertyName].push(value); + objectPropertyValue.push(value); } } else { object[propertyName] = value; From b53c276e810243fe443f6509910f75ffa421fe42 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 8 Oct 2020 23:45:03 -0700 Subject: [PATCH 287/407] fix a regression caused by lookup of objects in memory that wasn't restricted to primary key values only --- core/frb/syntax-properties.js | 51 +++++++++++++++++++ .../raw-foreign-value-to-object-converter.js | 49 ++++++++++++------ 2 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 core/frb/syntax-properties.js diff --git a/core/frb/syntax-properties.js b/core/frb/syntax-properties.js new file mode 100644 index 0000000000..020a9f0aa2 --- /dev/null +++ b/core/frb/syntax-properties.js @@ -0,0 +1,51 @@ +/** + * The names of the properties required to evaluate .expression + * + * The raw data that .expression is evaluated against may not + * have all of the properties referenced in .expression before the + * the MappingRule is used. This array is used at the time of mapping to + * populate the raw data with any properties that are missing. + * + * @type {string[]} + */ +module.exports = function syntaxProperties(syntax) { + return _parseRequirementsFromSyntax(syntax); +}; + +function _parseRequirementsFromRecord(syntax, requirements) { + var args = syntax.args, + keys = Object.keys(args), + _requirements = requirements || null, + i, countI; + + for(i=0, countI = keys.length;(i Date: Thu, 15 Oct 2020 00:25:32 -0700 Subject: [PATCH 288/407] take .vscode files --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1cc4e0a9b9..3536b90660 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,7 @@ log.txt atlassian-ide-plugin.xml ### VisualStudioCode ### -.vscode/* +#.vscode/* .vscode-upload.json .eslintrc.json From 2298872b6dd3125335a55cb4c5e49681639a3b70 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 15 Oct 2020 00:26:05 -0700 Subject: [PATCH 289/407] remove specific node version --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 7f0d9bacc2..16d4a107d1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -55,7 +55,7 @@ "type": "node", "request": "launch", "name": "compile time zones", - "runtimeExecutable": "~/.nvm/versions/node/v12.16.1/bin/node", + "runtimeVersion": "default", "cwd": "${workspaceFolder}/core/date/tools/", "program": "${workspaceFolder}/core/date/tools/compile-zones.js" }, From 02a7d725af302ce67cad2cd89de893ebbacc3bae Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 15 Oct 2020 00:28:17 -0700 Subject: [PATCH 290/407] - test to see if it's worth loading ical.js project files individually -> apparently not - replace use of direct ICAL method by ours wrapping it --- core/date/time-zone-core.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/core/date/time-zone-core.js b/core/date/time-zone-core.js index 9869d19ebf..0b17757a19 100644 --- a/core/date/time-zone-core.js +++ b/core/date/time-zone-core.js @@ -4,7 +4,17 @@ https://hg.mozilla.org/comm-central/raw-file/tip/calendar/timezones/zones.json */ +// require("ical.js/lib/ical/helpers"); +// require("ical.js/lib/ical/component"); +// require("ical.js/lib/ical/design"); +// require("ical.js/lib/ical/property"); +// require("ical.js/lib/ical/parse"); +// require("ical.js/lib/ical/timezone"); +// require("ical.js/lib/ical/timezone_service"); +// require("ical.js/lib/ical/time"); +// require("ical.js/lib/ical/recur"); var Montage = require("../core").Montage, + ICAL = require("ical.js"), ICAL_Timezone = ICAL.Timezone, ICAL_Timezone_Prototype = ICAL.Timezone.prototype, @@ -20,6 +30,8 @@ var Montage = require("../core").Montage, // 4. http://worldtimeapi.org // 5. https://timezoneapi.io/developers/timezone timeZonesData = require("./time-zone-data/zones-compiled.json"), + // systemTimeZonesData = require("./time-zone-data/"+Intl.DateTimeFormat(navigator.languages[0]).resolvedOptions().timeZone+".json"), + TimeZone = exports.TimeZone = ICAL_Timezone, TimeZonePrototype = TimeZone.prototype; @@ -89,7 +101,7 @@ Object.defineProperties(TimeZone, { var systemLocaleIdentifier = currentEnvironment.systemLocaleIdentifier, resolvedOptions = Intl.DateTimeFormat(systemLocaleIdentifier).resolvedOptions(), timeZone = resolvedOptions.timeZone; /* "America/Los_Angeles" */ - return (this._systemTimeZone = ICAL_TimezoneService.get(timeZone)); + return (this._systemTimeZone = TimeZone.withIdentifier(timeZone)); } }, "systemTimeZone": { From 0e5d469b84bfbc15bd6dd8961cc69ed0d0a595f2 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 15 Oct 2020 00:32:55 -0700 Subject: [PATCH 291/407] log a notoe about a bug found that will need to be fixed --- core/mr/require.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/mr/require.js b/core/mr/require.js index 6a0bccba81..477b7b1ad1 100644 --- a/core/mr/require.js +++ b/core/mr/require.js @@ -1596,6 +1596,19 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { module.mappingRedirect = rest; module.mappingRequire = mappingRequire; return mappingRequire.deepLoad(rest, config.location); + /* + TODO/FixMe: + + There's a bug where if a module id contains a path + that happens to be the name of a top level package, + the rest of the path after that part is tried to be loaded + from that package, ignoring anything before that, + + This is where I discovered that bug, the actual cause + might be somewhere else. + */ + //return mappingRequire.deepLoad(rest, mappingRequire.config.location); + } ) } From b784c96d9643fdaef4d837a63ff8b97835153ed8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 15 Oct 2020 00:34:33 -0700 Subject: [PATCH 292/407] avoid exception if deserialize is called when there's no string or an empty one. --- core/serialization/deserializer/montage-deserializer.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/serialization/deserializer/montage-deserializer.js b/core/serialization/deserializer/montage-deserializer.js index d0baef935c..16819a78bf 100644 --- a/core/serialization/deserializer/montage-deserializer.js +++ b/core/serialization/deserializer/montage-deserializer.js @@ -62,6 +62,10 @@ var MontageDeserializer = exports.MontageDeserializer = Montage.specialize({ */ deserialize: { value: function (instances, element) { + if(!this._serializationString || this._serializationString === "") { + return null; + } + var context = this._module && MontageDeserializer.moduleContexts.get(this._module), circularError; if (context) { From e1a055b173a24d100b20590f10e8342c5f5599a3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 15 Oct 2020 00:48:15 -0700 Subject: [PATCH 293/407] add ability to use dataService's connection identifier property on top of name to be more consistent with the rest of the framework --- data/model/data-identifier.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/data/model/data-identifier.js b/data/model/data-identifier.js index 98633eef50..b7175606a1 100644 --- a/data/model/data-identifier.js +++ b/data/model/data-identifier.js @@ -126,7 +126,13 @@ exports.DataIdentifier = Montage.specialize(/** @lends DataIdentifier.prototype var _url = "montage-data://"; _url += this.dataService.identifier; _url += "/"; - _url += this.dataService.connectionDescriptor ? this.dataService.connectionDescriptor.name : "default"; + _url += this.dataService.connection + ? this.dataService.connection.identifier + ? this.dataService.connection.identifier + : this.dataService.connection.name + ? this.dataService.connection.name + : name + : "default"; _url += "/"; _url += this.objectDescriptor.name; _url += "/"; From 8096e79b80ce176d68d0fe9502b5577986d9bf1e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 15 Oct 2020 00:50:07 -0700 Subject: [PATCH 294/407] workaround bug caused by cyclic dependency --- data/service/user-identity-manager.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/service/user-identity-manager.js b/data/service/user-identity-manager.js index e5f28c8289..ddab973f06 100644 --- a/data/service/user-identity-manager.js +++ b/data/service/user-identity-manager.js @@ -84,8 +84,11 @@ UserIdentityManager = Montage.specialize( /** @lends AuthorizationService.protot }, set: function(value) { this._mainService = value; - this._mainService.addEventListener(DataOperation.Type.UserAuthentication, this); - this._mainService.addEventListener(DataOperation.Type.UserAuthenticationCompleted, this); + //We have a circular depency such that when mainService setter is called, DataOperation isn't yet on the exports symbol... + // this._mainService.addEventListener(DataOperation.Type.UserAuthentication, this); + // this._mainService.addEventListener(DataOperation.Type.UserAuthenticationCompleted, this); + this._mainService.addEventListener("userauthentication", this); + this._mainService.addEventListener("userauthenticationcompleted", this); } }, From 8aa04d34632861353cd6ac9c9be7dc640b676dec Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 15 Oct 2020 00:54:55 -0700 Subject: [PATCH 295/407] - add merge operation type to define a new kind of operation that is a create if it doesn't exist or an update if it does - add connect/disconnect to mirror lambda cycles on websockets - change serialization of type to string vs number to improve readability - WIP first attenpt to serialiaze a target that is not an object descriptor, but an object that exists in memory when deserialized, more work needed --- data/service/data-operation.js | 67 ++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index cbe736132d..f11d4b9835 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -1,14 +1,22 @@ var Montage = require("core/core").Montage, MutableEvent = require("core/event/mutable-event").MutableEvent, + ModuleObjectDescriptor = require("core/meta/module-object-descriptor").ModuleObjectDescriptor, Criteria = require("core/criteria").Criteria, Enum = require("core/enum").Enum, uuid = require("core/uuid"), + defaultEventManager = require("../../core/event/event-manager").defaultEventManager, DataOperationType, /* todo: we shpuld add a ...timedout for all operations. */ dataOperationTypes = [ "noop", + "connect", + "disconnect", "create", + /* + Request to cancel a previous create operation, dispatched by the actor that dispatched the matching create + */ + "createcancel", "createfailed", "createcompleted", "createcancelled", @@ -48,6 +56,16 @@ var Montage = require("core/core").Montage, "updatecancel", /* Confirmation that a Request to cancel an update data, used either by the client sending the server or vice versa*, has completed */ "updatecanceled", + + "merge", + /* + Request to cancel a previous create operation, dispatched by the actor that dispatched the matching create + */ + "mergecancel", + "mergefailed", + "mergecompleted", + "mergecancelled", + "delete", "deletecompleted", "deletefailed", @@ -167,7 +185,6 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy value: function DataOperation() { this.timeStamp = performance.now(); this.id = uuid.generate(); - this.constructionIndex = exports.DataOperation.prototype.constructionSequence++; exports.DataOperation.prototype.constructionSequence = this.constructionIndex; } @@ -177,6 +194,17 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy value: 0 }, + _mainService: { + value: 0 + }, + + mainService: { + get: function() { + return this._mainService || (this.constructor.prototype._mainService = defaultEventManager.application.mainService) + } + }, + + bubbles: { value: true }, @@ -188,9 +216,22 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy serializeSelf: { value:function (serializer) { serializer.setProperty("id", this.id); - serializer.setProperty("type", DataOperationType.intValueForMember(this.type)); + //serializer.setProperty("type", DataOperationType.intValueForMember(this.type)); + serializer.setProperty("type", this.type); serializer.setProperty("timeStamp", this.timeStamp); - serializer.setProperty("dataDescriptor", this.dataDescriptor); + + if(this.target) { + if(Array.isArray(this.target)) { + serializer.setProperty("targetModuleId", this.target.map((objectDescriptor) => {return objectDescriptor.module.id})); + } else { + if(this.target instanceof ModuleObjectDescriptor) { + serializer.setProperty("targetModuleId", this.target.module.id); + } else { + serializer.addObjectReference(this.target); + } + } + } + // serializer.setProperty("dataDescriptor", this.dataDescriptor); if(this.referrerId) { serializer.setProperty("referrerId", this.referrerId); } @@ -222,7 +263,7 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy value = deserializer.getProperty("type"); if (value !== void 0) { - this.type = DataOperationType.memberWithIntValue(value); + this.type = DataOperationType[value]; } value = deserializer.getProperty("timeStamp"); @@ -230,9 +271,13 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy this.timeStamp = value; } - value = deserializer.getProperty("dataDescriptor"); + value = deserializer.getProperty("targetModuleId") || deserializer.getProperty("dataDescriptor"); if (value !== void 0) { - this.dataDescriptor = value; + if(Array.isArray(value)) { + this.target = value.map((objectDescriptorModuleIid) => {return this.mainService.objectDescriptorWithModuleId(objectDescriptorModuleIid)}); + } else { + this.target = this.mainService.objectDescriptorWithModuleId(value); + } } value = deserializer.getProperty("referrerId"); @@ -464,7 +509,7 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy }, /** - * Benoit: This property's role is a bit fuzzy. context can be changing and arbitrary. Keep?? + * Information about the context surrounding the data operation * @type {Object} */ context: { @@ -635,6 +680,8 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy */ value: { NoOp: DataOperationType.noop, + Connect: DataOperationType.connect, + Disconnect: DataOperationType.disconnect, Create: DataOperationType.create, CreateFailed: DataOperationType.createfailed, CreateCompleted: DataOperationType.createcompleted, @@ -659,6 +706,12 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy UpdateCancel: DataOperationType.updatecancel, UpdateCanceled: DataOperationType.updatecanceled, + Merge: DataOperationType.merge, + MergeCancel: DataOperationType.mergecancel, + MergeFailed: DataOperationType.mergefailed, + MergeCompleted: DataOperationType.mergecompleted, + MergeCancelled: DataOperationType.mergecancelled, + Delete: DataOperationType.delete, DeleteCompleted: DataOperationType.deletecompleted, DeleteFailed: DataOperationType.deletefailed, From 89a77aa93be42360ceb85d4fe82fe0d0616b1a28 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 15 Oct 2020 00:56:23 -0700 Subject: [PATCH 296/407] add ability for DataService to use ObjectDescriptors as types on top of ModuleObjectDescriptor --- data/service/data-service.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index a95bff0750..e34d1311a9 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -557,7 +557,11 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } jModule = jObjectDescriptor.module; - jModuleId = [jModule.id, jObjectDescriptor.exportName].join("/"); + if(!jModule) { + jModuleId = Montage.getInfoForObject(this).moduleId; + } else { + jModuleId = [jModule.id, jObjectDescriptor.exportName].join("/"); + } map[jModuleId] = jObjectDescriptor; //Setup the event propagation chain @@ -603,9 +607,13 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } else { var self = this, module = objectDescriptor.module; - return module.require.async(module.id).then(function (exports) { - return self.__makePrototypeForType(childService, objectDescriptor, exports[objectDescriptor.exportName]); - }); + if(module) { + return module.require.async(module.id).then(function (exports) { + return self.__makePrototypeForType(childService, objectDescriptor, exports[objectDescriptor.exportName]); + }); + } else { + return Promise.resolveNull; + } } } }, From eb6fb78b03e56fc1f3afe3419ac7da3a1035b46d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 16 Oct 2020 09:55:51 -0700 Subject: [PATCH 297/407] Fix typo --- core/meta/module-object-descriptor.mjson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/meta/module-object-descriptor.mjson b/core/meta/module-object-descriptor.mjson index 06bdfd6773..e3d18b10a5 100644 --- a/core/meta/module-object-descriptor.mjson +++ b/core/meta/module-object-descriptor.mjson @@ -30,7 +30,7 @@ "@": "root" }, "mandatory": true, - "valueType": "object" + "valueType": "object", "helpKey": "" } }, From 5854476c822f84b007bb68a3cd1ccd269ab36d2b Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 16 Oct 2020 15:08:18 -0700 Subject: [PATCH 298/407] update Buffer API to newer node standards --- core/promise-io/README.md | 6 +++--- core/promise-io/buffer-stream.js | 2 +- core/promise-io/fs-common.js | 2 +- core/promise-io/fs-mock.js | 2 +- core/promise-io/reader.js | 2 +- core/promise-io/spec/fs/write-spec.js | 2 +- core/promise-io/writer.js | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/core/promise-io/README.md b/core/promise-io/README.md index 3619329d07..993806ccbe 100644 --- a/core/promise-io/README.md +++ b/core/promise-io/README.md @@ -45,7 +45,7 @@ into memory. It returns a promise for the whole file contents. By default, `read` provides a string decoded from UTF-8. With the bytewise mode flag, provides a `Buffer`. -The options argument is identical to that of `open`. +The options argument is identical to that of `open`. ```javascript return FS.read(__filename, "b") @@ -396,7 +396,7 @@ var mockFs = MockFs({ "c.txt": "Content of a/b/c.txt" } }, - "a/b/d.txt": new Buffer("Content of a/b/d.txt", "utf-8") + "a/b/d.txt": Buffer.from("Content of a/b/d.txt", "utf-8") }) ``` @@ -594,7 +594,7 @@ returns a Q writer. ```javascript var BufferStream = require("q-io/buffer-stream"); -var stream = BufferStream(new Buffer("Hello, World!\n", "utf-8"), "utf-8") +var stream = BufferStream(Buffer.from("Hello, World!\n", "utf-8"), "utf-8") ``` ## HTTP Applications diff --git a/core/promise-io/buffer-stream.js b/core/promise-io/buffer-stream.js index 517cb2e23a..83cf72c79a 100644 --- a/core/promise-io/buffer-stream.js +++ b/core/promise-io/buffer-stream.js @@ -37,7 +37,7 @@ BufferStream.prototype.read = function () { BufferStream.prototype.write = function (chunk) { if (this._charset) { - chunk = new Buffer(String(chunk), this._charset); + chunk = Buffer.from(String(chunk), this._charset); } else { if (!(chunk instanceof Buffer)) { throw new Error("Can't write strings to buffer stream without a charset: " + chunk); diff --git a/core/promise-io/fs-common.js b/core/promise-io/fs-common.js index 4fb79f8ecb..1e19524327 100644 --- a/core/promise-io/fs-common.js +++ b/core/promise-io/fs-common.js @@ -74,7 +74,7 @@ exports.update = function (exports, workingDirectory) { flags = options.flags || "w"; if (flags.indexOf("b") !== -1) { if (!(content instanceof Buffer)) { - content = new Buffer(content); + content = Buffer.from(content); } } else if (content instanceof Buffer) { flags += "b"; diff --git a/core/promise-io/fs-mock.js b/core/promise-io/fs-mock.js index f9e06bcc72..3560402eea 100644 --- a/core/promise-io/fs-mock.js +++ b/core/promise-io/fs-mock.js @@ -50,7 +50,7 @@ MockFs.prototype._init = function (files, tree) { this._init(content, path); return; } else { - content = new Buffer(String(content), "utf-8"); + content = Buffer.from(String(content), "utf-8"); } } directoryNode._entries[base] = fileNode; diff --git a/core/promise-io/reader.js b/core/promise-io/reader.js index fe9e9dbe6b..48e94c53df 100644 --- a/core/promise-io/reader.js +++ b/core/promise-io/reader.js @@ -120,7 +120,7 @@ function join(buffers) { buffer = buffers[i]; length += buffer.length; } - result = new Buffer(length); + result = Buffer.alloc(length); at = 0; for (i = 0; i < ii; i++) { buffer = buffers[i]; diff --git a/core/promise-io/spec/fs/write-spec.js b/core/promise-io/spec/fs/write-spec.js index d7642be645..222a38c983 100644 --- a/core/promise-io/spec/fs/write-spec.js +++ b/core/promise-io/spec/fs/write-spec.js @@ -22,7 +22,7 @@ describe("write", function () { it("should write bytewise", function () { var path = FS.join(__dirname, "fixtures/so-it-is-written.txt"); var content = "Good bye, cruel World!\n"; - return FS.write(path, new Buffer(content, "utf-8")) + return FS.write(path, Buffer.from(content, "utf-8")) .then(function (result) { expect(result).toBe(undefined); return FS.read(path) diff --git a/core/promise-io/writer.js b/core/promise-io/writer.js index 6b8ac9a62a..e5cf865348 100644 --- a/core/promise-io/writer.js +++ b/core/promise-io/writer.js @@ -44,7 +44,7 @@ function Writer(_stream, charset) { if (!_stream.writeable && !_stream.writable) return Q.reject(new Error("Can't write to non-writable (possibly closed) stream")); if (typeof content !== "string") { - content = new Buffer(content); + content = Buffer.from(content); } if (!_stream.write(content)) { return drained.promise; From b7afd182b0c601ed7ef38ee98882985a80a1ee02 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 16 Oct 2020 15:42:09 -0700 Subject: [PATCH 299/407] fix regression --- core/promise-io/fs-mock.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/promise-io/fs-mock.js b/core/promise-io/fs-mock.js index 3560402eea..34c7aae875 100644 --- a/core/promise-io/fs-mock.js +++ b/core/promise-io/fs-mock.js @@ -129,7 +129,11 @@ MockFs.prototype.open = function (path, flags, charset, options) { charset ); } else { - return new BufferStream(fileNode._chunks, charset); + //return new BufferStream(fileNode._chunks, charset); + // Clone chunks to avoid side effect + var bufferChunks = fileNode._chunks.slice(); + return new BufferStream(bufferChunks, charset); + } } }); @@ -531,7 +535,7 @@ LinkNode.prototype.isSymbolicLink = function () { }; LinkNode.prototype._follow = function (via, memo) { - memo = memo || Set(); + memo = memo || new Set(); if (memo.has(this)) { var error = new Error("Can't follow symbolic link cycle at " + JSON.stringify(via)); error.code = "ELOOP"; From 3354db4f035aa77b71c7c623d3591fc10b127671 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 21 Oct 2020 16:29:13 -0700 Subject: [PATCH 300/407] change to support ws, WebSocket implementation in node.js that doesn't implement addEventListener() for listeners that are not functions --- core/web-socket.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/web-socket.js b/core/web-socket.js index 89aa707395..8ff246d71e 100644 --- a/core/web-socket.js +++ b/core/web-socket.js @@ -48,8 +48,8 @@ if(_WebSocket) { _connect: { value: function () { this._webSocket = new _WebSocket(this._url, this._protocols); - this._webSocket.addEventListener("error", this, false); - this._webSocket.addEventListener("open", this, false); + this._webSocket.addEventListener("error", event => this.handleEvent(event), false); + this._webSocket.addEventListener("open", event => this.handleEvent(event), false); } }, @@ -110,8 +110,8 @@ if(_WebSocket) { case "open": this._reconnectionInterval = 100; if (this._webSocket) { - this._webSocket.addEventListener("message", this, false); - this._webSocket.addEventListener("close", this, false); + this._webSocket.addEventListener("message", event => this.handleEvent(event), false); + this._webSocket.addEventListener("close", event => this.handleEvent(event), false); } this.dispatchEvent(event); this._sendNextMessage(); From 94d54f86d30a388e5035cdfd0cdaff7ee04667e7 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 21 Oct 2020 16:31:17 -0700 Subject: [PATCH 301/407] - fix enumerability of methods added to prototype - add toLocaleCapitalized() - add removeSuffix(aPrefix) --- core/extras/string.js | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/core/extras/string.js b/core/extras/string.js index 434060c7ea..708e7c6bb7 100644 --- a/core/extras/string.js +++ b/core/extras/string.js @@ -19,6 +19,7 @@ Object.defineProperty(String.prototype, "equals", { value: function (that) { return this.valueOf() === Object.getValueOf(that); }, + enumerable: false, writable: true, configurable: true }); @@ -34,6 +35,7 @@ Object.defineProperty(String.prototype, "contains", { value: function (substring) { return this.indexOf(substring) !== -1; }, + enumerable: false, writable: true, configurable: true }); @@ -54,11 +56,28 @@ Object.defineProperty(String.prototype, "toCapitalized", { var value; return toCapitalized.cache.get(String(this)) || (toCapitalized.cache.set(String(this),(value = this[0].toUpperCase() + this.slice(1))) ? value : null); }, + enumerable: false, writable: true, configurable: true }); String.prototype.toCapitalized.cache = new Map(); +/* + TODO: implement toLocaleCapitalized + str.toLocaleCapitalized() + str.toLocaleCapitalized(locale) + str.toLocaleCapitalized([locale, locale, ...]) + +*/ +Object.defineProperty(String.prototype, "toLocaleCapitalized", { + value: function toLocaleCapitalized() { + return (this[0].toLocaleCapitalized.apply(this,arguments) + this.slice(1)); + }, + enumerable: false, + writable: true, + configurable: true +}); + if (!String.prototype.toCamelCase) { @@ -98,6 +117,7 @@ if (!String.prototype.toCamelCase) { value: function toCamelCase() { return _toCamelCase(this, toCamelCase.cache); }, + enumerable: false, writable: true, configurable: true }); @@ -109,9 +129,38 @@ if (!String.prototype.toCamelCase) { value: function toLowerCamelCase () { return _toCamelCase(this, toLowerCamelCase.cache, true); }, + enumerable: false, writable: true, configurable: true }); String.prototype.toLowerCamelCase.cache = Object.create(null); } + +if(typeof String.prototype.removeSuffix !== "function") { + Object.defineProperty(String.prototype, "removeSuffix", { + value: function (suffix) { + + if (this.length) { + if (suffix && suffix.length) { + var index = this.lastIndexOf(suffix); + + if (index !== -1) { + return this.substring(0, index); + } else { + return this; + } + } else { + return this; + } + } else { + return this; + } + }, + enumerable: false, + writable: true, + configurable: true, + }); + +} + From f4a44116057b6971b949a6ae5b8116ec0f5fa359 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 21 Oct 2020 16:32:16 -0700 Subject: [PATCH 302/407] Fix nextTarget bug --- core/meta/object-descriptor.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/meta/object-descriptor.js b/core/meta/object-descriptor.js index 57abc0c4f6..d537c2734e 100644 --- a/core/meta/object-descriptor.js +++ b/core/meta/object-descriptor.js @@ -450,10 +450,14 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends * @property {boolean} serializable * @property {Component} value */ + _nextTarget: { + value: false + }, + nextTarget: { serializable: false, get: function() { - return this.parent || ObjectDescriptor.mainService; + return this._nextTarget || (this._nextTarget = (this.parent || ObjectDescriptor.mainService.childServiceForType(this))); } }, From b76e568929fb053a491eed66c1a44fe9a1bf1280 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 21 Oct 2020 16:33:35 -0700 Subject: [PATCH 303/407] fix serialization so it uses an object's constructor when serialized rather than the .mjson it was initially desrialzed from --- .../serialization/serializer/montage-visitor.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/core/serialization/serializer/montage-visitor.js b/core/serialization/serializer/montage-visitor.js index f5f3070fb4..0c2f5ffaf3 100644 --- a/core/serialization/serializer/montage-visitor.js +++ b/core/serialization/serializer/montage-visitor.js @@ -338,8 +338,21 @@ var MontageVisitor = Montage.specialize({ value: function (object) { var moduleId = this.getObjectModuleId(object), defaultObjectName, - objectInfo = Montage.getInfoForObject(object), - objectName = objectInfo.objectName; + objectInfo, + objectName; + + /* + If the object was deserialized from an .mjson, we need to re-serialize it as it's conatructor's moduleId + */ + if(moduleId.endsWith(".mjson")) { + moduleId = this.getObjectModuleId(object.constructor); + objectInfo = Montage.getInfoForObject(object.constructor); + } else { + objectInfo = Montage.getInfoForObject(object); + } + + objectName = objectInfo.objectName; + defaultObjectName = MontageSerializerModule.MontageSerializer.getDefaultObjectNameForModuleId(moduleId); From 82f0c04c7e7a1540ae62cc02076da621cebeb7e8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 21 Oct 2020 16:36:42 -0700 Subject: [PATCH 304/407] - update operation type case - add transactionUpdated type - fix bug in deserialize/serialize --- data/service/data-operation.js | 260 +++++++++++++++++---------------- 1 file changed, 134 insertions(+), 126 deletions(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index f11d4b9835..375ebbac3f 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -16,85 +16,85 @@ var Montage = require("core/core").Montage, /* Request to cancel a previous create operation, dispatched by the actor that dispatched the matching create */ - "createcancel", - "createfailed", - "createcompleted", - "createcancelled", + "createCancel", + "createFailed", + "createCompleted", + "createCancelled", //Additional "copy", - "copyfailed", - "copycompleted", + "copyFailed", + "copyCompleted", /* Read is the first operation that models a query */ "read", /* ReadUpdated is pushed by server when a query's result changes due to data changes from others */ - "readupdated", + "readUpdated", /* ReadProgress / ReadUpdate / ReadSeek is used to instruct server that more data is required for a "live" read / query Need a better name, and a symetric? Or is ReadUpdated enough if it referes to previous operation */ - "readprogress", //ReadUpdate - "readupdate", //ReadUpdate + "readProgress", //ReadUpdate + "readUpdate", //ReadUpdate /* ReadCancel is the operation that instructs baclkend that client isn't interested by a read operastion anymore */ - "readcancel", + "readCancel", /* ReadCanceled is the operation that instructs the client that a read operation is canceled */ - "readcanceled", + "readCanceled", /* ReadFailed is the operation that instructs the client that a read operation has failed canceled */ - "readfailed", + "readFailed", /* ReadCompleted is the operation that instructs the client that a read operation has returned all available data */ - "readcompleted", + "readCompleted", /* Request to update data, used either by the client sending the server or vice versa */ "update", /* Confirmation that a Request to update data, used either by the client sending the server or vice versa*, has been completed */ - "updatecompleted", + "updateCompleted", /* Confirmation that a Request to update data, used either by the client sending the server or vice versa*, has failed */ - "updatefailed", + "updateFailed", /* Request to cancel an update, used either by the client sending the server or vice versa */ - "updatecancel", + "updateCancel", /* Confirmation that a Request to cancel an update data, used either by the client sending the server or vice versa*, has completed */ - "updatecanceled", + "updateCanceled", "merge", /* Request to cancel a previous create operation, dispatched by the actor that dispatched the matching create */ - "mergecancel", - "mergefailed", - "mergecompleted", - "mergecancelled", + "mergeCancel", + "mergeFailed", + "mergeCompleted", + "mergeCancelled", "delete", - "deletecompleted", - "deletefailed", + "deleteCompleted", + "deleteFailed", /* Lock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ "lock", - "lockcompleted", - "lockfailed", + "lockCompleted", + "lockFailed", /* Unlock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ "unlock", - "unlockcompleted", - "unlockfailed", + "unlockCompleted", + "unlockFailed", /* RemmoteProcedureCall models the ability to invoke code logic on the server-side, being a DB StoredProcedure, or an method/function in a service */ - "remoteinvocation", /* Execute ? */ - "remoteinvocationcompleted", /* ExecuteCompleted ? */ - "remoteinvocationfailed", /* ExecuteFailed ? */ + "remoteInvocation", /* Execute ? */ + "remoteInvocationCompleted", /* ExecuteCompleted ? */ + "remoteInvocationFailed", /* ExecuteFailed ? */ /* Batch models the ability to group multiple operation. If a referrer is provided to a BeginTransaction operation, then the batch will be executed within that transaction */ "batch", - "batchupdate", - "batchcompleted", - "batchfailed", + "batchUpdate", + "batchCompleted", + "batchFailed", /* A transaction is a unit of work that is performed atomically against a database. @@ -109,24 +109,25 @@ var Montage = require("core/core").Montage, so settling on create transaction and perform/rollback transaction */ - "createtransaction", + "createTransaction", /* I don't think there's such a thing, keeping for symetry for now */ - "createtransactioncompleted", + "createTransactionCompleted", /* Attempting to create a transaction within an existing one will fail */ - "createtransactionfailed", + "createTransactionFailed", - "transactioncancelled", + "transactionUpdated", + "transactionCancelled", - "createsavepoint", + "createSavePoint", - "performtransaction", - "performtransactioncompleted", - "performtransactionfailed", + "performTransaction", + "performTransactionCompleted", + "performTransactionFailed", - "rollbacktransaction", - "rollbacktransactioncompleted", - "rollbacktransactionfailed", + "rollbackTransaction", + "rollbackTransactionCompleted", + "rollbackTransactionFailed", /* operations used for the bottom of the stack to get information from a user. @@ -140,16 +141,16 @@ var Montage = require("core/core").Montage, Data components shpuld add themselves as listeners to the data service for events/ data operations like that they know how to deal with / can help with. */ - "userauthentication", - "userauthenticationupdate", - "userauthenticationcompleted", - "userauthenticationfailed", - "userauthenticationtimedout", - "userinput", - "userinputcompleted", - "userinputfailed", - "userinputcanceled", - "userinputtimedout", + "userAuthentication", + "userAuthenticationUpdate", + "userAuthenticationCompleted", + "userAuthenticationFailed", + "userAuthenticationTimedout", + "userInput", + "userInputCompleted", + "userInputFailed", + "userInputCanceled", + "userInputTimedout", /* Modeling validation operation, either executed locally or server-side. @@ -160,9 +161,9 @@ var Montage = require("core/core").Montage, */ "validate", - "validatefailed", - "validatecompleted", - "validatecancelled" + "validateFailed", + "validateCompleted", + "validateCancelled" ]; @@ -200,7 +201,7 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy mainService: { get: function() { - return this._mainService || (this.constructor.prototype._mainService = defaultEventManager.application.mainService) + return this._mainService || (this.constructor.prototype._mainService = defaultEventManager.application && defaultEventManager.application.mainService) } }, @@ -222,16 +223,17 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy if(this.target) { if(Array.isArray(this.target)) { - serializer.setProperty("targetModuleId", this.target.map((objectDescriptor) => {return objectDescriptor.module.id})); + serializer.setProperty("targetModuleId", this.target.map((objectDescriptor) => {return typeof objectDescriptor === "string" ? objectDescriptor : objectDescriptor.module.id})); } else { if(this.target instanceof ModuleObjectDescriptor) { serializer.setProperty("targetModuleId", this.target.module.id); } else { - serializer.addObjectReference(this.target); + //This is not working as I thought it would yet + //serializer.addObjectReference(this.target); + serializer.setProperty("targetModuleId", null); } } } - // serializer.setProperty("dataDescriptor", this.dataDescriptor); if(this.referrerId) { serializer.setProperty("referrerId", this.referrerId); } @@ -270,13 +272,20 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy if (value !== void 0) { this.timeStamp = value; } - + /* keeping dataDescriptor here for temporary backward compatibility */ value = deserializer.getProperty("targetModuleId") || deserializer.getProperty("dataDescriptor"); if (value !== void 0) { if(Array.isArray(value)) { this.target = value.map((objectDescriptorModuleIid) => {return this.mainService.objectDescriptorWithModuleId(objectDescriptorModuleIid)}); + } else if(this.mainService) { + if(value === null) { + this.target = this.mainService + } else { + this.target = this.mainService.objectDescriptorWithModuleId(value); + } } else { - this.target = this.mainService.objectDescriptorWithModuleId(value); + //Last resort, if we can't construct the target, let's carry the data that was supposed to help us do so + this.targetModuleId = value; } } @@ -541,10 +550,6 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy value: undefined }, - dataDescriptor: { - value: undefined - }, - /** * data is designed to carry the "meat" of an operation's specifics. For a create, it would be all properties * of a new object (I'm assuming a create operation is modeling only 1 object's creation). @@ -683,85 +688,88 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy Connect: DataOperationType.connect, Disconnect: DataOperationType.disconnect, Create: DataOperationType.create, - CreateFailed: DataOperationType.createfailed, - CreateCompleted: DataOperationType.createcompleted, - CreateCancelled: DataOperationType.createcancelled, + CreateFailed: DataOperationType.createFailed, + CreateCompleted: DataOperationType.createCompleted, + CreateCancelled: DataOperationType.createCancelled, Copy: DataOperationType.copy, - CopyFailed: DataOperationType.copyfailed, - CopyCompleted: DataOperationType.copycompleted, + CopyFailed: DataOperationType.copyFailed, + CopyCompleted: DataOperationType.copyCompleted, Read: DataOperationType.read, - ReadUpdated: DataOperationType.readupdated, - ReadProgress: DataOperationType.readprogress, //ReadUpdate - ReadUpdate: DataOperationType.readupdate, //ReadUpdate - ReadCancel: DataOperationType.readcancel, - ReadCanceled: DataOperationType.readcanceled, - ReadFailed: DataOperationType.readfailed, - ReadCompleted: DataOperationType.readcompleted, + ReadUpdated: DataOperationType.readUpdated, + ReadProgress: DataOperationType.readProgress, //ReadUpdate + ReadUpdate: DataOperationType.readUpdate, //ReadUpdate + ReadCancel: DataOperationType.readCancel, + ReadCanceled: DataOperationType.readCanceled, + ReadFailed: DataOperationType.readFailed, + ReadCompleted: DataOperationType.readCompleted, Update: DataOperationType.update, - UpdateCompleted: DataOperationType.updatecompleted, - UpdateFailed: DataOperationType.updatefailed, - UpdateCancel: DataOperationType.updatecancel, - UpdateCanceled: DataOperationType.updatecanceled, + UpdateCompleted: DataOperationType.updateCompleted, + UpdateFailed: DataOperationType.updateFailed, + UpdateCancel: DataOperationType.updateCancel, + UpdateCanceled: DataOperationType.updateCanceled, Merge: DataOperationType.merge, - MergeCancel: DataOperationType.mergecancel, - MergeFailed: DataOperationType.mergefailed, - MergeCompleted: DataOperationType.mergecompleted, - MergeCancelled: DataOperationType.mergecancelled, + MergeCancel: DataOperationType.mergeCancel, + MergeFailed: DataOperationType.mergeFailed, + MergeCompleted: DataOperationType.mergeCompleted, + MergeCancelled: DataOperationType.mergeCancelled, Delete: DataOperationType.delete, - DeleteCompleted: DataOperationType.deletecompleted, - DeleteFailed: DataOperationType.deletefailed, + DeleteCompleted: DataOperationType.deleteCompleted, + DeleteFailed: DataOperationType.deleteFailed, Lock: DataOperationType.lock, - LockCompleted: DataOperationType.lockcompleted, - LockFailed: DataOperationType.lockfailed, - - RemoteProcedureCall: DataOperationType.remoteinvocation, - RemoteProcedureCallCompleted: DataOperationType.remoteinvocationcompleted, - RemoteProcedureCallFailed: DataOperationType.remoteinvocationfailed, - RemoteInvocation: DataOperationType.remoteinvocation, - RemoteInvocationCompleted: DataOperationType.remoteinvocationcompleted, - RemoteInvocationFailed: DataOperationType.remoteinvocationfailed, - - UserAuthentication: DataOperationType.userauthentication, - UserAuthenticationUpdate: DataOperationType.userauthenticationupdate, - UserAuthenticationCompleted: DataOperationType.userauthenticationcompleted, - UserAuthenticationFailed: DataOperationType.userauthenticationfailed, - UserAuthenticationTimedout: DataOperationType.userauthenticationtimedout, - - UserInput: DataOperationType.userinput, - UserInputCompleted: DataOperationType.userinputcompleted, - UserInputFailed: DataOperationType.userinputfailed, - UserInputCanceled: DataOperationType.userinputcanceled, - UserInputTimedOut: DataOperationType.userinputtimedout, + LockCompleted: DataOperationType.lockCompleted, + LockFailed: DataOperationType.lockFailed, + + RemoteProcedureCall: DataOperationType.remoteInvocation, + RemoteProcedureCallCompleted: DataOperationType.remoteInvocationCompleted, + RemoteProcedureCallFailed: DataOperationType.remoteInvocationFailed, + RemoteInvocation: DataOperationType.remoteInvocation, + RemoteInvocationCompleted: DataOperationType.remoteInvocationCompleted, + RemoteInvocationFailed: DataOperationType.remoteInvocationFailed, + + UserAuthentication: DataOperationType.userAuthentication, + UserAuthenticationUpdate: DataOperationType.userAuthenticationUpdate, + UserAuthenticationCompleted: DataOperationType.userAuthenticationCompleted, + UserAuthenticationFailed: DataOperationType.userAuthenticationFailed, + UserAuthenticationTimedout: DataOperationType.userAuthenticationTimedout, + + UserInput: DataOperationType.userInput, + UserInputCompleted: DataOperationType.userInputCompleted, + UserInputFailed: DataOperationType.userInputFailed, + UserInputCanceled: DataOperationType.userInputCanceled, + UserInputTimedOut: DataOperationType.userInputTimedout, Validate: DataOperationType.validate, - ValidateFailed: DataOperationType.validatefailed, - validateCompleted: DataOperationType.validatecompleted, - validateCancelled: DataOperationType.validatecancelled, + ValidateFailed: DataOperationType.validateFailed, + validateCompleted: DataOperationType.validateCompleted, + validateCancelled: DataOperationType.validateCancelled, Batch: DataOperationType.batch, - BatchUpdate: DataOperationType.batchupdate, - BatchCompleted: DataOperationType.batchcompleted, - BatchFailed: DataOperationType.batchfailed, + BatchUpdate: DataOperationType.batchUpdate, + BatchCompleted: DataOperationType.batchCompleted, + BatchFailed: DataOperationType.batchFailed, + + CreateTransaction: DataOperationType.createTransaction, + CreateTransactionCompleted: DataOperationType.createTransactionCompleted, + CreateTransactionFailed: DataOperationType.createTransactionFailed, + + TransactionUpdated: DataOperationType.transactionUpdated, - CreateTransaction: DataOperationType.createtransaction, - CreateTransactionCompleted: DataOperationType.createtransactioncompleted, - CreateTransactionFailed: DataOperationType.createtransactionfailed, - CreateSavePoint: DataOperationType.createsavepoint, + CreateSavePoint: DataOperationType.createSavePoint, - PerformTransaction: DataOperationType.performtransaction, - PerformTransactionCompleted: DataOperationType.performtransactioncompleted, - PerformTransactionFailed: DataOperationType.performtransactionfailed, + PerformTransaction: DataOperationType.performTransaction, + PerformTransactionCompleted: DataOperationType.performTransactionCompleted, + PerformTransactionFailed: DataOperationType.performTransactionFailed, - RollbackTransaction: DataOperationType.rollbacktransaction, - RollbackTransactionCompleted: DataOperationType.rollbacktransactioncompleted, - RollbackTransactionFailed: DataOperationType.rollbacktransactionfailed, + RollbackTransaction: DataOperationType.rollbackTransaction, + RollbackTransactionCompleted: DataOperationType.rollbackTransactionCompleted, + RollbackTransactionFailed: DataOperationType.rollbackTransactionFailed, } } From f409a953ea63e3b57843a102373b6eb16cf49911 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 21 Oct 2020 16:38:40 -0700 Subject: [PATCH 305/407] improve support for connectionDescriptor/connection/connectionIdentifier --- data/service/raw-data-service.js | 102 +++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 5189fc708e..496630422f 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -79,6 +79,22 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot var value = deserializer.getProperty("rawDataTypeMappings"); this._registerRawDataTypeMappings(value || []); + value = deserializer.getProperty("connectionDescriptor"); + if (value) { + this.connectionDescriptor = value; + } + + /* + setting connectionIdentifier will set the current connection + based on connectionDescriptor. + + I can still be overriden by the direct setting of connection bellow + */ + value = deserializer.getProperty("connectionIdentifier"); + if (value) { + this.connectionIdentifier = value; + } + value = deserializer.getProperty("connection"); if (value) { this.connection = value; @@ -87,15 +103,101 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } }, + /* * The ConnectionDescriptor object where possible connections will be found * * @type {ConnectionDescriptor} */ + _connectionDescriptor: { + value: undefined + }, connectionDescriptor: { + get: function() { + return this._connectionDescriptor; + }, + set: function(value) { + if(value !== this._connectionDescriptor) { + this._connectionDescriptor = value; + this._registeredConnectionsByIdentifier = null; + this.registerConnections(value); + } + } + }, + /** + * Description... + * + * @method + * @argument {Array} [connectionDescription] - The different known connections to the database + * + */ + _registeredConnectionsByIdentifier: { + value: undefined, + }, + registerConnections: { + value: function(connectionDescriptor) { + + this._registeredConnectionsByIdentifier = connectionDescriptor; + + for(var i=0, connections = Object.keys(connectionDescriptor), countI = connections.length, iConnectionIdentifier, iConnection;(i Date: Wed, 21 Oct 2020 16:40:13 -0700 Subject: [PATCH 306/407] - fix bug in setting up child services if an object is deserialized more than once - improve support for using objectDescriptor --- data/service/data-service.js | 44 +++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index e34d1311a9..9e1ddc632c 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -23,6 +23,8 @@ var Montage = require("core/core").Montage, deprecate = require("../../core/deprecate"), Locale = require("core/locale").Locale; + require("core/extras/string"); + var AuthorizationPolicyType = new Montage(); AuthorizationPolicyType.NoAuthorizationPolicy = AuthorizationPolicy.NONE; AuthorizationPolicyType.UpfrontAuthorizationPolicy = AuthorizationPolicy.UP_FRONT; @@ -130,7 +132,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { value = deserializer.getProperty("childServices"); if (value) { - this._childServices = value; + this._deserializedChildServices = value; } value = deserializer.getProperty("authorizationPolicy"); @@ -147,12 +149,18 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } }, + _deserializedChildServices: { + value: undefined + }, + deserializedFromSerialization: { value: function (label) { - if(Array.isArray(this._childServices)) { - var childServices = this._childServices; - this._childServices = []; - this.addChildServices(childServices); + if(Array.isArray(this._deserializedChildServices) && this._deserializedChildServices.length > 0) { + //var childServices = this._childServices; + if(!this._childServices) { + this._childServices = []; + } + this.addChildServices(this._deserializedChildServices); } if (this.authorizationPolicy === AuthorizationPolicyType.UpfrontAuthorizationPolicy) { @@ -322,6 +330,10 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { for(i=0, countI = childServices.length;(i Date: Fri, 13 Nov 2020 13:51:32 -0800 Subject: [PATCH 307/407] optimizing for null/undefined --- .../raw-embedded-value-to-object-converter.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/data/converter/raw-embedded-value-to-object-converter.js b/data/converter/raw-embedded-value-to-object-converter.js index 7fb452faa0..ddaa1f1fbf 100644 --- a/data/converter/raw-embedded-value-to-object-converter.js +++ b/data/converter/raw-embedded-value-to-object-converter.js @@ -28,8 +28,14 @@ exports.RawEmbeddedValueToObjectConverter = RawValueToObjectConverter.specialize convertedValue, result; - - return Promise.all([this._descriptorToFetch, this.service]).then(function (values) { + /* + besides returning a default value, or a shared "Missing value" singleton, a feature we don't have, there's not much we can do here: + */ + if(v === null) { + return Promise.resolveNull; + } else if( v === undefined) { + return Promise.resolveUndefined; + } else return Promise.all([this._descriptorToFetch, this.service]).then(function (values) { var typeToFetch = values[0], service = values[1]; From 71046475cd9bf51474284c4758d9cd05873b435a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 13 Nov 2020 23:02:29 -0800 Subject: [PATCH 308/407] - fix bugs related to isObjectCreated and child services - fix bug in array changes propagation --- data/service/data-service.js | 73 ++++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 9e1ddc632c..4d63ea2c68 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -1466,7 +1466,9 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { getObjectProperties: { value: function (object, propertyNames) { - + if(!object) { + return Promise.resolveNull; + } /* Benoit: @@ -1482,7 +1484,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } else if (this.isRootService) { // Get the data, accepting property names as an array or as a list // of string arguments while avoiding the creation of any new array. - var names = Array.isArray(propertyNames) ? propertyNames : arguments, + var names = Array.isArray(propertyNames) ? propertyNames : Array.prototype.slice.call(arguments, 1), start = names === propertyNames ? 0 : 1; return this._getOrUpdateObjectProperties(object, names, start, false); } @@ -2199,7 +2201,17 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { isObjectCreated: { value: function(object) { - return this.createdDataObjects.has(object); + var isObjectCreated = this.createdDataObjects.has(object); + + if(!isObjectCreated) { + var service = this._getChildServiceForObject(object); + if(service) { + isObjectCreated = service.isObjectCreated(object); + } else { + isObjectCreated = false; + } + } + return isObjectCreated; } }, @@ -2580,6 +2592,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { value: function (changeEvent, propertyDescriptor, inversePropertyDescriptor) { var dataObject = changeEvent.target, + isCreatedObject = this.isObjectCreated(dataObject), key = changeEvent.key, keyValue = changeEvent.keyValue, addedValues = changeEvent.addedValues, @@ -2588,7 +2601,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { inversePropertyDescriptor, self = this; - if(!this.createdDataObjects.has(dataObject)) { + if(!isCreatedObject) { this.changedDataObjects.add(dataObject); } @@ -2645,30 +2658,47 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { //We later need to convert these into dataIdentifers, we could avoid a loop later //doing so right here. if(addedValues) { - var registeredAddedValues = manyChanges.addedValues; - if(!registeredAddedValues) { - manyChanges.addedValues = (registeredAddedValues = new Set(addedValues)); - self._addDataObjectPropertyDescriptorValuesForInversePropertyDescriptor(dataObject, propertyDescriptor, addedValues, inversePropertyDescriptor); + /* + In this case, the array already contains the added value and we'll save it all anyway. So we just propagate. + */ + if(Array.isArray(manyChanges) && isCreatedObject) { + self._addDataObjectPropertyDescriptorValuesForInversePropertyDescriptor(dataObject, propertyDescriptor, addedValues, inversePropertyDescriptor); } else { - for(i=0, countI=addedValues.length;i Date: Fri, 13 Nov 2020 23:05:00 -0800 Subject: [PATCH 309/407] fix bug about isObjectCreated --- data/service/data-trigger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js index 9f73b1a8b6..614dfb84a8 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -463,7 +463,7 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy getObjectProperty: { value: function (object) { //If the object is not created and not saved, we fetch the value - if(!this._service.createdDataObjects.has(object)) { + if(!this._service.isObjectCreated(object)) { var status = this._getValueStatus(object); return status ? status.promise : status === null ? this._service.nullPromise : From e44d6655f692d6799b1267dfacd9061fc54c5bce Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 15:46:29 -0800 Subject: [PATCH 310/407] add notion of stage where an app is running to support different development environment stages --- core/environment.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/core/environment.js b/core/environment.js index 085eac42ba..365d260da7 100644 --- a/core/environment.js +++ b/core/environment.js @@ -16,6 +16,13 @@ var Environment = exports.Environment = Montage.specialize({ } }, + /* + set by EventManager to avoid circular dependencies... Should environment be exposed only via application and live inside application.js? + */ + application: { + value: null + }, + systemLocaleIdentifier: { get: function () { return typeof navigator === "object" @@ -24,6 +31,41 @@ var Environment = exports.Environment = Montage.specialize({ } }, + /** + * The name of the stage the code is running. + * + * this.application.url.searchParams.get("stage"); + * + * @property {string} + */ + _stage: { + value: undefined + }, + stage: { + get: function() { + if(this._stage === undefined) { + //Check if we have an argument: + var stageArgument = this.application.url && this.application.url.searchParams.get("stage"); + + if(stageArgument) { + this._stage = stageArgument; + } else if(global.location.hostname === "127.0.0.1" || global.location.hostname === "localhost" || global.location.hostname.endsWith(".local") ) { + this._stage = "dev"; + } else { + /* + could be staging or production or anything else, we don't know and stop the guessing game. + */ + this._stage = null; + } + } + + return this._stage; + }, + set: function(value) { + this._stage = value; + } + }, + isBrowser: { value: (typeof window !== "undefined") }, From 0fe63e05a2ebf802a20341934def9138dd19050a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 15:47:47 -0800 Subject: [PATCH 311/407] add try/catch around deserialization to help with diagnostics --- montage.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/montage.js b/montage.js index 0db61f6db8..5fa5cbe065 100644 --- a/montage.js +++ b/montage.js @@ -406,7 +406,11 @@ } else { //mainDatareelModulePromise = applicationRequire.makeRequire("data/main.datareel/main.mjson").async("data/main.datareel/main.mjson"); mainDatareelLocation = "data/main.datareel/main.mjson"; - mainDatareelModulePromise = applicationRequire.async("data/main.datareel/main.mjson"); + + //Check if we have a redirection in mappings. Mr does that, when we bring mr inside montage.js, we should be able to simplify this. + // mainDatareelLocation = applicationRequire.mappings[mainDatareelLocation].location || mainDatareelLocation; + + mainDatareelModulePromise = applicationRequire.async(mainDatareelLocation); //mainDatareelModulePromise = mrPromise.resolve(); } @@ -465,7 +469,14 @@ root; module.deserializer = deserializer; deserializer.init(module.text, deserializerRequire, void 0, module, true); - root = deserializer.deserializeObject(); + + try { + root = deserializer.deserializeObject(); + } catch(error) { + console.log(module.id+" deserializeObject() failed with error:",error); + + throw error; + } // console.log("********MJSONCompilerFactory END compileMJSONFile",module.id); From 2576c24eab92ae3f89619861d6ef48e3dd4c0ff4 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 15:48:18 -0800 Subject: [PATCH 312/407] add argument to jshint to support ES6 --- node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node.js b/node.js index a7bf0c3ad0..b5fcf00d9a 100644 --- a/node.js +++ b/node.js @@ -70,7 +70,7 @@ MontageBoot.loadPackage = function (location, config) { if(!lint.JSHINT) { lint.JSHINT = require("jshint"); } - if (!lint.JSHINT.JSHINT(module.text)) { + if (!lint.JSHINT.JSHINT(module.text,{esversion: 6})) { console.warn("JSHint Error: "+module.location); lint.JSHINT.JSHINT.errors.forEach(function (error) { if (error) { From f7cef7d9942d8c2e72f2b2e693e50e8120426c41 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 16:01:09 -0800 Subject: [PATCH 313/407] - remove eventManager property as it's already in super type target - add url property to wrap document.location, if on client --- core/application.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/core/application.js b/core/application.js index 210c1e0793..e036b047f0 100644 --- a/core/application.js +++ b/core/application.js @@ -36,17 +36,6 @@ var FIRST_LOAD_KEY_SUFFIX = "-is-first-load"; */ var Application = exports.Application = Target.specialize( /** @lends Application.prototype # */ { - /** - * Provides a reference to the Montage event manager used in the - * application. - * - * @property {EventManager} value - * @default null - */ - eventManager: { - value: null - }, - /** * Provides a reference to the parent application. * @@ -71,6 +60,14 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio } }, + url: { + get: function() { + return document && document.location + ? new URL(document.location) + : null; + } + }, + /** * Provides a reference to the main application. * From eb473542cee40d406acebd9587fd9f2913997ddc Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 16:02:18 -0800 Subject: [PATCH 314/407] testing giving more control to object defining bindings --- core/core.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/core.js b/core/core.js index 56831e8cdf..88382f0cb3 100644 --- a/core/core.js +++ b/core/core.js @@ -1240,7 +1240,12 @@ var bindingPropertyDescriptors = { */ defineBindings: { value: function (descriptors, commonDescriptor) { - return Bindings.defineBindings(this, descriptors, commonDescriptor); + if (descriptors) { + for (var i=0, name, keys = Object.keys(descriptors); (name = keys[i]); i++) { + this.defineBinding(name, descriptors[name], commonDescriptor); + } + } + //return Bindings.defineBindings(this, descriptors, commonDescriptor); } }, From 42883c58b29cdc201b8fb11da764f4c819ac7205 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 16:03:29 -0800 Subject: [PATCH 315/407] WIP - needs testing --- core/counted-map.js | 59 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 core/counted-map.js diff --git a/core/counted-map.js b/core/counted-map.js new file mode 100644 index 0000000000..60d4cf28ab --- /dev/null +++ b/core/counted-map.js @@ -0,0 +1,59 @@ +// var Montage = require("./core").Montage; +Set = require("core/collections/map"); + +/** + * The CountedMap keeps a counter associated with object inserted into it. It keeps track of the number of times objects are inserted and objects are really removed when they've been removed the same number of times. + * + * @class CountedMap + * @classdesc todo + * @extends Montage + */ +var CountedMap = exports.CountedMap = function CountedMap(iterable) { + + var inst = new Map(iterable); + inst.__proto__ = CountedMap.prototype; + inst._contentCount = new Map(); + return inst; + +} + +CountedMap.prototype = Object.create(Map.prototype); + +CountedMap.prototype._set = Map.prototype.set; +CountedMap.prototype.set = function(key, value) { + var currentCount = this._contentCount.get(key) || (this.has(key) ? 1 : 0); + if(currentCount === 0) { + this._set(key, value); + } + this._contentCount.set(key,++currentCount); + return this; + //console.log("CountedMap add: countFor "+value.identifier.primaryKey+" is ",currentCount); +} +CountedMap.prototype._delete = Map.prototype.delete; +CountedMap.prototype.delete = function(key) { + var currentCount = this._contentCount.get(key) || (this.has(key) ? 1 : 0); + if(currentCount) { + this._contentCount.set(key,--currentCount); + if(currentCount === 0) { + if(!this.has(key)) { + console.error("Attempt to delete object that is actually gone, mismatch in add/delete?"); + } + this._contentCount.delete(key); + this._delete(key); + return true; + } + } + return false; + //console.log("CountedMap delete: countFor "+value.identifier.primaryKey+" is ",currentCount); +} + +CountedMap.prototype._clear = Map.prototype.clear; +CountedMap.prototype.clear = function(value) { + this._contentCount.clear(); + this._clear(); +}; + + +CountedMap.prototype.countFor = function(key) { + return this._contentCount.get(key) || 0; +} From b9ad9af047427e4049f17c140ecbd4088fc73fcc Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 21:41:36 -0800 Subject: [PATCH 316/407] - add predicateFunction property that enables the use of a criteria in a array.filter() - fix a bug in how we define methods per frb operator on criteria --- core/criteria.js | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/core/criteria.js b/core/criteria.js index e45fc18649..6bc68c83f2 100644 --- a/core/criteria.js +++ b/core/criteria.js @@ -4,7 +4,7 @@ var Montage = require("./core").Montage; var parse = require("core/frb/parse"), stringify = require("core/frb/stringify"), evaluate = require("core/frb/evaluate"), - precedence = require("core/frb/language").precedence, + operatorTypes = require("core/frb/language").operatorTypes, Scope = require("core/frb/scope"), compile = require("core/frb/compile-evaluator"); @@ -39,7 +39,6 @@ var Criteria = exports.Criteria = Montage.specialize({ return this._parameters; }, set: function(value) { - debugger; if(value !== this._parameters) { this._parameters = value; } @@ -232,6 +231,30 @@ var Criteria = exports.Criteria = Montage.specialize({ } }, + + /** + * Returns a function that performs evaluate on the criteria, allowing it to be used in array.filter for example. + * + * @method + * + * @returns {function} - boolean wether the criteri qualifies a value on propertyName. + */ + + _predicateFunction: { + value: undefined + }, + + predicateFunction: { + get: function () { + var self = this; + return this._predicateFunction || ( + this._predicateFunction = function(value) { + return self.evaluate(value); + } + ); + } + }, + /** * Walks a criteria's syntactic tree to assess if one of more an expression * involving propertyName. @@ -649,15 +672,15 @@ function _combinedCriteriaFromArguments(type, receiver, _arguments) { // generate methods on Criteria for each of the tokens of the language. // support invocation both as class and instance methods like // Criteria.and("a", "b") and aCriteria.and("b") -precedence.forEach(function (value,type, precedence) { - Montage.defineProperty(Criteria.prototype, type, { +operatorTypes.forEach(function (value,operator, operatorTypes) { + Montage.defineProperty(Criteria.prototype, operator, { value: function () { - return _combinedCriteriaFromArguments(type, this, arguments); + return _combinedCriteriaFromArguments(operator, this, arguments); } }); - Montage.defineProperty(Criteria, type, { + Montage.defineProperty(Criteria, operator, { value: function () { - return _combinedCriteriaFromArguments(type, this, arguments); + return _combinedCriteriaFromArguments(operator, this, arguments); } }); }); From fbd798364905600376a8f4f0d7ef5e7b423e028d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 21:46:12 -0800 Subject: [PATCH 317/407] - add the ability to use "global" as a module and use global types like Object, Date, Map etc... in serialization, and also require them though that's arguably less interesting in code - add mjson file basics for Object, Date and Map so to support that. Needs to be improved to actually describe their properties. --- core/collections/map.mjson | 22 ++++++++++++++++++++++ core/date.mjson | 22 ++++++++++++++++++++++ core/mr/require.js | 10 +++++++++- core/object.mjson | 22 ++++++++++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 core/collections/map.mjson create mode 100644 core/date.mjson create mode 100644 core/object.mjson diff --git a/core/collections/map.mjson b/core/collections/map.mjson new file mode 100644 index 0000000000..86c0f53c72 --- /dev/null +++ b/core/collections/map.mjson @@ -0,0 +1,22 @@ +{ + "map": { + "object": "global[Map]" + }, + "root": { + "prototype": "core/meta/module-object-descriptor", + "values": { + "name": "Map", + "propertyDescriptors": [ + ], + "propertyDescriptorGroups": { + "all": [ + ] + }, + "propertyValidationRules": {}, + "objectDescriptorModule": { "%": "core/collections/map.mjson" }, + "exportName": "Map", + "module": { "%": "global" }, + "object":{ "@": "map" } + } + } +} diff --git a/core/date.mjson b/core/date.mjson new file mode 100644 index 0000000000..0366e415f2 --- /dev/null +++ b/core/date.mjson @@ -0,0 +1,22 @@ +{ + "date": { + "object": "global[Date]" + }, + "root": { + "prototype": "core/meta/module-object-descriptor", + "values": { + "name": "Date", + "propertyDescriptors": [ + ], + "propertyDescriptorGroups": { + "all": [ + ] + }, + "propertyValidationRules": {}, + "objectDescriptorModule": { "%": "core/date.mjson" }, + "exportName": "Date", + "module": { "%": "global" }, + "object":{ "@": "date" } + } + } +} diff --git a/core/mr/require.js b/core/mr/require.js index 477b7b1ad1..0e11c19630 100644 --- a/core/mr/require.js +++ b/core/mr/require.js @@ -929,6 +929,14 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { config.requireForId = requireForId = memoize(makeRequire,config.requireById); require = makeRequire(""); + + + var globalModule = modules["global"] = new Module(); + globalModule.id = "global"; + globalModule.display = "global"; + globalModule.exports = global; + globalModule.require = require; + return require; }; @@ -1492,7 +1500,7 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { module.directory // __dirname ); - return returnValue;`` + return returnValue; }; diff --git a/core/object.mjson b/core/object.mjson new file mode 100644 index 0000000000..360346140d --- /dev/null +++ b/core/object.mjson @@ -0,0 +1,22 @@ +{ + "object": { + "object": "global[Object]" + }, + "root": { + "prototype": "core/meta/module-object-descriptor", + "values": { + "name": "Object", + "propertyDescriptors": [ + ], + "propertyDescriptorGroups": { + "all": [ + ] + }, + "propertyValidationRules": {}, + "objectDescriptorModule": { "%": "core/object.mjson" }, + "exportName": "Object", + "module": { "%": "global" }, + "object":{ "@": "object" } + } + } +} From deb8282b67b9f9688b50bb2b9c60b95291ca9cda Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 21:47:21 -0800 Subject: [PATCH 318/407] Fix bug where members wouldn't actually be built --- core/enum.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/enum.js b/core/enum.js index 5576a57095..d104be4cea 100644 --- a/core/enum.js +++ b/core/enum.js @@ -166,6 +166,8 @@ exports.Enum = Montage.specialize( /** @lends Enum# */ { value: value !== void 0 && value !== null ? value : intValue }); + (this._members || (this._members = [])).push(member); + (_membersByValue || this._membersByValue)[intValue] = member; (_membersIntValue || this._membersIntValue).set(member, intValue); } From b33b3db16236acb6bc0a5eadce8e110d385000f5 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 21:48:27 -0800 Subject: [PATCH 319/407] WIP: add missing options argument in initWithIdentifier and serializeSelf/deserializeSelf --- core/locale.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/locale.js b/core/locale.js index 9c98dd0b1b..a121a34caa 100644 --- a/core/locale.js +++ b/core/locale.js @@ -77,6 +77,8 @@ var Locale = exports.Locale = Montage.specialize({ /** * initializes a new calendar locale by a given identifier. * + * TODO: don't loose info about options.... + * * @function * @param {String} localeIdentifier The module id of the HTML page to load. * @param {Object} options An object that contains configuration for the Locale. Keys are Unicode locale tags, @@ -85,7 +87,7 @@ var Locale = exports.Locale = Montage.specialize({ * @returns {Locale} a new Locale instance. */ initWithIdentifier: { - value: function(localeIdentifier) { + value: function(localeIdentifier, options) { this.identifier = localeIdentifier; return this; } @@ -94,6 +96,22 @@ var Locale = exports.Locale = Montage.specialize({ value: new Map() }, + serializeSelf: { + value: function (serializer) { + serializer.setProperty("identifier", this.identifier); + } + }, + + deserializeSelf: { + value: function (deserializer) { + var value; + value = deserializer.getProperty("identifier"); + if (value !== void 0) { + this.identifier = value; + } + } + }, + /* Instance properties */ /** From f5b153753b6da7ccd0be9bb5bab1e282b73ec59d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 21:49:51 -0800 Subject: [PATCH 320/407] make Promise.is more robust by adding catch() to the list of checks, though in client, this code is bypassed currently, in the bootstrapping, which should be fixed --- core/promise.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/promise.js b/core/promise.js index 9af9b52c5f..fb35f9c9ec 100644 --- a/core/promise.js +++ b/core/promise.js @@ -6,7 +6,7 @@ var Promise = require("bluebird"); // Patch "Promise.is" to support native promise Promise.is = function (obj) { - return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; + return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function' && typeof obj.catch === 'function'; }; // Polyfill "Promise.prototypefinally" to support finally From 3144330a58a8a25d25729f341ab3b0f9a7de3bb5 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 21:51:44 -0800 Subject: [PATCH 321/407] - update deprecated findLast->findLastValue and find->findValue - fix some regressions --- core/range-controller.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/core/range-controller.js b/core/range-controller.js index 6428ec9f5c..dbac34fc48 100644 --- a/core/range-controller.js +++ b/core/range-controller.js @@ -113,6 +113,11 @@ Object.defineProperty(_RangeSelection.prototype, "swap_or_push", { configurable: false, value: function(start, howMany, itemsToAdd) { var content = this.rangeController.content; + + if(!content) { + return; + } + this.contentEquals = content && content.contentEquals || Object.is; start = start >= 0 ? start : this.length + start; var plus; @@ -153,13 +158,13 @@ Object.defineProperty(_RangeSelection.prototype, "swap_or_push", { } // if the same item appears twice in the add list, only add it once - if (itemsToAdd.findLast(itemsToAdd[i]) > i) { + if (itemsToAdd.findLastValue(itemsToAdd[i]) > i) { continue; } // if the item is already in the selection, don't add it // unless it's in the part that we're about to delete. - indexInSelection = this.find(itemsToAdd[i]); + indexInSelection = this.findValue(itemsToAdd[i]); if(indexInSelection < 0 || (indexInSelection >= start && indexInSelection < start + minusLength)) { plus.push(itemsToAdd[i]); @@ -773,15 +778,15 @@ var RangeController = exports.RangeController = Montage.specialize( /** @lends R if (this.selection.length) { this.selection.deleteEach(diff); - - // ensure selection always has content - if (this.selection.length === 0 && this.content && this.content.length && - this.avoidsEmptySelection && !this.allowsMultipleSelection) { - // selection can't contain previous content value as content already changed - this.selection.add(this.content[this.content.length - 1]); - } } } + + // ensure selection always has content + if (this.selection.length === 0 && this.content && this.content.length && + this.avoidsEmptySelection && !this.allowsMultipleSelection) { + // selection can't contain previous content value as content already changed + this.selection.add(this.content[this.content.length - 1]); + } } }, From 5934e833b8de320fc17941cef207b4079e37976f Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 21:52:03 -0800 Subject: [PATCH 322/407] fix bugs --- core/range.mjson | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/range.mjson b/core/range.mjson index ddec2930dd..1d989bae4b 100644 --- a/core/range.mjson +++ b/core/range.mjson @@ -27,7 +27,7 @@ } }, "range": { - "object": "core/range" + "object": "./range" }, "root": { "prototype": "core/meta/module-object-descriptor", @@ -46,9 +46,9 @@ ] }, "propertyValidationRules": {}, - "objectDescriptorModule": { "%": "core/range.meta" }, + "objectDescriptorModule": { "%": "./core/range.mjson" }, "exportName": "Range", - "module": { "%": "core/range" }, + "module": { "%": "./range" }, "object":{ "@": "range" } } } From 6db60e50d8121f25531f8be89bdd4bee774abd28 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 21:54:46 -0800 Subject: [PATCH 323/407] expose eventManager property --- core/target.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/target.js b/core/target.js index 12314f6e83..e7c95fb229 100644 --- a/core/target.js +++ b/core/target.js @@ -11,6 +11,19 @@ var Montage = require("./core").Montage, * @extends Montage */ exports.Target = Montage.specialize( /** @lends Target.prototype */{ + + /** + * Provides a reference to the Montage event manager used in the + * application. + * + * @property {EventManager} value + * @default defaultEventManager + */ + + eventManager: { + value: defaultEventManager, + serializable: false + }, /** * Whether or not this target can accept user focus and become the * activeTarget This matches up with the `document.activeElement` property From bb40fdc5ee654db227b087dd8ff24c1a3a94d6d9 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 21:56:29 -0800 Subject: [PATCH 324/407] add support to resolving relative URL in object tag's data attribute in templates --- core/template.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/template.js b/core/template.js index 8842d570be..d079cedf67 100644 --- a/core/template.js +++ b/core/template.js @@ -1283,6 +1283,12 @@ var Template = Montage.specialize( /** @lends Template# */ { if (url !== "" && !absoluteUrlRegExp.test(url)) { node.setAttribute('href', URL.resolve(baseUrl, url)); } + } else if (node.hasAttribute("data")) { + // Stylesheets + url = node.getAttribute('data'); + if (url !== "" && !absoluteUrlRegExp.test(url)) { + node.setAttribute('data', URL.resolve(baseUrl, url)); + } } } } @@ -1354,7 +1360,7 @@ var Template = Montage.specialize( /** @lends Template# */ { }, _NORMALIZED_TAG_NAMES: { - value: ["IMG", "image", "IFRAME", "link","script"] + value: ["IMG", "image", "object", "IFRAME", "link", "script"] }, __NORMALIZED_TAG_NAMES_SELECTOR: { From b94930ea489ad31e79ea28f0e622aa56b93f80f8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 21:57:15 -0800 Subject: [PATCH 325/407] fix docs and a bug --- .../international-date-to-string-formatter.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/converter/international-date-to-string-formatter.js b/core/converter/international-date-to-string-formatter.js index 7afe68c89e..3a2ed8ec1a 100644 --- a/core/converter/international-date-to-string-formatter.js +++ b/core/converter/international-date-to-string-formatter.js @@ -1,14 +1,16 @@ /** - * @module montage/core/converter/internationalDateToStringFormatter + * @module montage/core/converter/international-date-to-string-formatter * @requires montage/core/converter/converter */ var Converter = require("./converter").Converter, Locale = require("../locale").Locale; /** - * Inverts the value of a boolean value. + * Formats a Date to a String using standard Intl.DateTimeFormat. * - * @class InvertConverter + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat + * + * @class InternationalDateToStringFormatter * @extends Converter */ var InternationalDateToStringFormatter = exports.InternationalDateToStringFormatter = Converter.specialize({ @@ -60,7 +62,7 @@ var InternationalDateToStringFormatter = exports.InternationalDateToStringFormat }, convert: { value: function (v) { - return this._dayDateFormatter.format(v); + return v ? this._dayDateFormatter.format(v) : ""; } } }); From 1fcf6466f5680f70948f68cbb0650952cf5b8fd9 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 22:01:52 -0800 Subject: [PATCH 326/407] - add a property to converter to assess wether they can convert an object as well as an array containing such object - misc changes to raw-foreign-value-to-object-converter --- core/converter/converter.js | 19 ++++ .../raw-foreign-value-to-object-converter.js | 88 ++++++++++++++++--- 2 files changed, 97 insertions(+), 10 deletions(-) diff --git a/core/converter/converter.js b/core/converter/converter.js index 245a943fae..3b85f6dbbb 100644 --- a/core/converter/converter.js +++ b/core/converter/converter.js @@ -84,6 +84,25 @@ var Converter = exports.Converter = Montage.specialize( /** @lends Converter# */ value: true }, + /** + * Specifies whether the converter can convert an array of the type of values it can handle individually . + * @type {boolean} + * @default false + */ + canConvertValueArray: { + value: false + }, + + /** + * Specifies whether the converter can convert an array of the type of values it can handle individually . + * @type {boolean} + * @default false + */ + canRevertValueArray: { + value: false + }, + + /** * Converts values from the input domain into the output range. * @function diff --git a/data/converter/raw-foreign-value-to-object-converter.js b/data/converter/raw-foreign-value-to-object-converter.js index 3cd6dfe24f..d2a664cfd5 100644 --- a/data/converter/raw-foreign-value-to-object-converter.js +++ b/data/converter/raw-foreign-value-to-object-converter.js @@ -11,6 +11,13 @@ var RawValueToObjectConverter = require("./raw-value-to-object-converter").RawVa */ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( /** @lends RawForeignValueToObjectConverter# */ { + canConvertValueArray: { + value: true + }, + + canRevertValueArray: { + value: true + }, /********************************************************************* * Serialization @@ -153,9 +160,9 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( existingObject = service.rootService.objectForDataIdentifier(dataIdentifier); } else if(Array.isArray(criteria.parameters)) { var rootService = service.rootService, - array = criteria.parameters, i=0, countI = array.length, iObject; + array = criteria.parameters, i=0, iObject; - for(; (i 0) { if(localResult.length) { //We found some locally but not all - localPartialResultPromise = Promise.resolve(localResult); + localPartialResult = localResult; } else { //we didn't find anything locally - localPartialResultPromise = null; + localPartialResult = null; } } else { //We found everything locally, we're done: @@ -302,10 +315,6 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( self._registerFetchPromiseForObjectDescriptorCriteria(fetchPromise, typeToFetch, criteria); } - if(localPartialResultPromise) { - fetchPromise = Promise.all([localPartialResultPromise,fetchPromise]); - } - return fetchPromise; }) : null; @@ -474,7 +483,66 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( } } else { - return Promise.resolve(null); + /* + if we don't have a foreign key value, where we can do: + + We're already using in fetchObjectProperty: + objectCriteria = new Criteria().initWithExpression("id == $id", {id: object.dataIdentifier.primaryKey}); + on the client side to do so, id here is on the table fetched, for gettin more inline values. + + + we have the primary key, and the foreign key, so we should be able to do something with it: + + For example, with Event having a property + "respondentQuestionnaires": { + "<->": "respondentQuestionnaireIds", + "converter": {"@": "respondentQuestionnairesForeignKeyConverter"} + } + and + "convertExpression": "$.has(id)" + + if we replace $ which so far has been a value, by the type qualified symbol, it would becomes: + "$type.respondentQuestionnaireIds.has(id)", { + "type":"data/main.datareel/model/event" + } + + Adding what we do for readExpression: + Type to fetch is RespondentQuestionnaire + "$type.respondentQuestionnaireIds.has(id) && id == $id", { + "type":"data/main.datareel/model/event", + "id": object.dataIdentifier.primaryKey + } + + */ + + // var currentRule = this.currentRule, + // requirements = currentRule && currentRule.requirements, + // self = this; + + // // if(requirements && requirements.length > 0) { + // //Loop and call fetchObjectProperty + // (this.service + // ? this.service.then(function (service) { + // var promises; + + // for(var i=0, countI = requirements.length;(i Date: Wed, 16 Dec 2020 22:06:33 -0800 Subject: [PATCH 327/407] add converter that can convert an object with keys and values properties, or an array of key/value pairs, to a map. This is generic, used to be able to save a Map as 2 arrays, one of keys and one of values, each as column of array type in postgreSQL database. Opens the abilty to store native types for both keys and columns, respecting what JS can do, but jsonb can't in postgreSQL --- .../key-value-array-to-map-converter.js | 278 ++++++++++++++++++ .../key-value-array-to-map-converter.mjson | 49 +++ 2 files changed, 327 insertions(+) create mode 100644 core/converter/key-value-array-to-map-converter.js create mode 100644 core/converter/key-value-array-to-map-converter.mjson diff --git a/core/converter/key-value-array-to-map-converter.js b/core/converter/key-value-array-to-map-converter.js new file mode 100644 index 0000000000..268512280a --- /dev/null +++ b/core/converter/key-value-array-to-map-converter.js @@ -0,0 +1,278 @@ +/** + * @module montage/core/converter/key-value-array-to-map-converter + * @requires montage/core/converter/converter + */ +var Converter = require("./converter").Converter, + Promise = require("core/promise").Promise; + + +/** + * @class ArrayToMapConverter + * @classdesc Converts key/value arrays or an array of pairs to a Map. + * @extends Converter + */ +exports.KeyValueArrayToMapConverter = Converter.specialize( /** @lends ArrayToMapConverter# */ { + /********************************************************************* + * Serialization + */ + + serializeSelf: { + value: function (serializer) { + + serializer.setProperty("convertedValueDescriptor", this.convertedValueDescriptor); + serializer.setProperty("keysConverter", this.keysConverter); + serializer.setProperty("valuesConverter", this.valuesConverter); + + } + }, + deserializeSelf: { + value: function (deserializer) { + var value = deserializer.getProperty("convertedValueDescriptor"); + if (value) { + this.convertedValueDescriptor = value; + } + + value = deserializer.getProperty("keysConverter"); + if (value) { + this.keysConverter = value; + } + value = deserializer.getProperty("valuesConverter"); + if (value) { + this.valuesConverter = value; + } + } + }, + + /** + * @type {ObjectDescriptor} + * @default {Converter} undefined + */ + convertedValueDescriptor: { + value: undefined + }, + + /** + * @type {Converter} + * @default {Converter} undefined + */ + keysConverter: { + value: undefined + }, + + /** + * @type {Converter} + * @default {Converter} undefined + */ + valuesConverter: { + value: undefined + }, + + /** + * @function + * @param {object} value - object expected to have a keys and a values properties, or an array in whcih case we expect pairs. + * @returns {string} The formatted currency value. + */ + convert: { + value: function (value) { + var keys, values, + convertedValue, + convertedValueType = this.convertedValueDescriptor.object, + convertedKeys, + convertedValues, + promises; + + + if(value) { + if(Array.isArray(value)) { + /* + We expect an array ok key/value pairs, per + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map + + constructor. + + If we have a converter, we need to iterate. + For efficiency, we should group all values, convert, and then re-assign matching the pairs. + + We might need a setting to be explicit about that. + */ + if(this.keysConverter || this.valuesConverter) { + + keys = []; + values = []; + + for(var i=0, countI = value.length; (i { + var resolvedKeys = resolvedValues[0], + resolvedValues = resolvedValues[1]; + + return this._createConvertedValue(resolvedKeys,resolvedValues); + }); + } else { + return this._createConvertedValue(convertedKeys,convertedValues); + } + } + else { + return this._createConvertedValue(null,null); + } + } + }, + + _createConvertedValue: { + value: function(keys, values) { + var convertedValueType = this.convertedValueDescriptor.object, + isConvertingToObject = convertedValueType === Object, + convertedValue = isConvertingToObject ? Object.create(null) : new Map(), + i, countI; + + if(keys) { + for(i=0, countI = keys.length; (i Date: Wed, 16 Dec 2020 22:07:37 -0800 Subject: [PATCH 328/407] WIP: converter to convert each value of an array using map, more methods to be added as needed --- .../collection-iteration-converter.js | 201 ++++++++++++++++++ .../collection-iteration-converter.mjson | 37 ++++ 2 files changed, 238 insertions(+) create mode 100644 core/converter/collection-iteration-converter.js create mode 100644 core/converter/collection-iteration-converter.mjson diff --git a/core/converter/collection-iteration-converter.js b/core/converter/collection-iteration-converter.js new file mode 100644 index 0000000000..2fc0ab42e5 --- /dev/null +++ b/core/converter/collection-iteration-converter.js @@ -0,0 +1,201 @@ +/** + * @module montage/core/converter/collection-iteration-converter + * @requires montage/core/converter/converter + */ +var Converter = require("./converter").Converter, + Promise = require("core/promise").Promise; + + +/** + * @class CollectionIterationConverter + * @classdesc Converts key/value arrays or an array of pairs to a Map. + * @extends Converter + */ +exports.CollectionIterationConverter = Converter.specialize( /** @lends CollectionIterationConverter# */ { + /********************************************************************* + * Serialization + */ + + serializeSelf: { + value: function (serializer) { + + serializer.setProperty("mapConverter", this.keysConverter); + serializer.setProperty("mapReverter", this.keysConverter); + + } + }, + deserializeSelf: { + value: function (deserializer) { + + value = deserializer.getProperty("mapConverter"); + if (value) { + this.mapConverter = value; + } + + value = deserializer.getProperty("mapReverter"); + if (value) { + this.mapReverter = value; + } + } + }, + + /** + * @property {Converter|function} + * @default {Converter} undefined + */ + mapConverter: { + get: function() { + return this._iterationConverter; + }, + set: function(value) { + this._iterationConverter = value; + this._convert = this._convertElementIndexCollection; + this._revert = this._revertElementIndexCollection; + } + }, + + /** + * @property {Converter|function} + * @default {Converter} undefined + */ + mapReverter: { + get: function() { + return this._iterationReverter; + }, + set: function(value) { + this._iterationReverter = value; + this._convert = this._convertElementIndexCollection; + this._revert = this._revertElementIndexCollection; + } + }, + + /** + * @property {Converter|function} + * @default {Converter} undefined + */ + __iterationConverter: { + value: undefined + }, + _iterationConverter: { + get: function() { + return this.__iterationConverter; + }, + set: function(value) { + this.__iterationConverter = value; + } + }, + + /** + * @property {Converter|function} + * @default {Converter} undefined + */ + __iterationReverter: { + value: undefined + }, + _iterationReverter: { + get: function() { + if(!this.__iterationReverter) { + if(this.__iterationConverter && typeof this.__iterationConverter.revert === "function") { + return this.__iterationConverter; + } + } else { + return this.__iterationReverter; + } + }, + set: function(value) { + this.__iterationReverter = value; + } + }, + + _convert: { + value: undefined + }, + convert: { + get: function() { + return this._convert; + }, + set: function(value) { + this._convert = value; + } + }, + + _revert: { + value: undefined + }, + revert: { + get: function() { + return this._revert; + }, + set: function(value) { + this._revert = value; + } + }, + + /** + * @function + * @param {Collection} value - object expected to have a keys and a values properties, or an array in whcih case we expect pairs. + * @returns {array} The formatted currency value. + */ + _convertElementIndexCollection: { + value: function (value) { + + if(!this._iterationConverter) return value; + + var values = value.values(), + converter = this._iterationConverter, + isConverterFunction = typeof converter === "function", + iValue, + index = 0, + result = new value.constructor; + + while(iValue = values.next().value) { + result.add( + isConverterFunction + ? converter(iValue,index,value) + : converter.convert(iValue) + ); + index++; + } + return result; + } + }, + + + /** + * Optionally, reverts values from the output range, back into the input + * range. This may not be possible with high fidelity depending on the + * relationship between these domains. + * @function + * @default null + */ + _revertElementIndexCollection: { + enumerable: false, + value: function(value) { + + if(!this._iterationReverter) return value; + + var values = value.values(), + reverter = this._iterationReverter, + isReverterFunction = typeof reverter === "function", + iValue, + index = 0, + result = new value.constructor; + + if(!isReverterFunction && typeof reverter.revert !== "function") { + return value; + } + + while(iValue = values.next().value) { + result.add( + isReverterFunction + ? reverter(iValue,index,value) + : reverter.revert(iValue) + ); + index++; + } + return result; + } + } + +}); + diff --git a/core/converter/collection-iteration-converter.mjson b/core/converter/collection-iteration-converter.mjson new file mode 100644 index 0000000000..106c75db57 --- /dev/null +++ b/core/converter/collection-iteration-converter.mjson @@ -0,0 +1,37 @@ +{ + "converter_descriptor": { + "object": "core/converter/converter.mjson" + }, + "map": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "map", + "valueType": "object", + "helpKey": "" + } + }, + "root": { + "prototype": "core/meta/module-object-descriptor", + "values": { + "name": "CollectionIterationConverter", + "customPrototype": false, + "parent": {"@": "converter_descriptor"}, + "propertyDescriptors": [ + {"@": "map"} + ], + "propertyDescriptorGroups": { + "arrayToMapConverter": [ + {"@": "map"} + ] + }, + "propertyValidationRules": {}, + "objectDescriptorModule": { + "%": "core/converter/collection-iteration-converter.mjson" + }, + "exportName": "CollectionIterationConverter", + "module": { + "%": "core/converter/collection-iteration-converter" + } + } + } +} From 55361ea01116c115caf813f1aea3b7d9d28ff3e5 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 23:47:19 -0800 Subject: [PATCH 329/407] change to help environment acces current application --- core/event/event-manager.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/event/event-manager.js b/core/event/event-manager.js index 34719556c8..b228da45a8 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -483,6 +483,8 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan }, set: function (application) { this._application = application; + //To avoid circular depenencies, we set it on environment + this.environment.application = application; } }, From a4389f198d2b38473f126b9e7cf00ba51fb6bb65 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 23:49:45 -0800 Subject: [PATCH 330/407] add methods and fix a bug --- core/extras/date.js | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/core/extras/date.js b/core/extras/date.js index 92444d52ef..7194cf5654 100644 --- a/core/extras/date.js +++ b/core/extras/date.js @@ -12,6 +12,22 @@ var Range = require("../range").Range; * @external */ + + +/** + * Returns a date UnixTime, which is the number of seconds since the Unix Epoch. + * + * @property external:Date#unixTime + * @returns {Number} - the UnixTime +*/ +Object.defineProperty(Date.prototype, "unixTime", { + get: function () { + return this.getTime()/1000|0; + }, + enumerable: true, + configurable: true +}); + /** * Creates a copy of a date. * @@ -54,43 +70,43 @@ Object.defineProperty(Date.prototype,"adjustComponentValues", { month, myYear; - if(Number.isFinite(milliseconds)) { + if(Number.isFinite(milliseconds) && milliseconds !== 0) { millisecond = this.millisecond; millisecond += milliseconds; this.millisecond = milliseconds; } - if(Number.isFinite(seconds)) { + if(Number.isFinite(seconds) && seconds !== 0) { second = this.second; second += seconds; this.second = second; } - if(Number.isFinite(minutes)) { - inute = this.minute; + if(Number.isFinite(minutes) && minutes !== 0) { + minute = this.minute; minute += minutes; this.minute = minute; } - if(Number.isFinite(hours)) { + if(Number.isFinite(hours) && hours !== 0) { hour = this.hour; hour += hours; this.hour = hour; } - if(Number.isFinite(days)) { + if(Number.isFinite(days) && days !== 0) { myDay = this.day; myDay += days; this.day = myDay; } - if(Number.isFinite(monthIndex)) { + if(Number.isFinite(monthIndex) && monthIndex !== 0) { month = this.month; month += monthIndex; this.month = month; } - if(Number.isFinite(year)) { + if(Number.isFinite(year) && year !== 0) { myYear = this.year; myYear += year; this.year = myYear; @@ -100,6 +116,17 @@ Object.defineProperty(Date.prototype,"adjustComponentValues", { configurable: true }); +Object.defineProperty(Date.prototype,"dateByAdjustingComponentValues", { + value: function dateByAdjustingComponentValues(year, monthIndex, day, hours, minutes, seconds, milliseconds) { + var calendarDate = this.clone(); + calendarDate.adjustComponentValues(year, monthIndex, day, hours, minutes, seconds, milliseconds); + return calendarDate; + }, + writable: true, + enumerable: false, + configurable: true +}); + /** * Assess if an instance a date is valid From 33082b9ce5f004280934be5e6db3c7327438dd35 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 23:52:20 -0800 Subject: [PATCH 331/407] make code more robust to avoid duplicate values in result array --- core/frb/syntax-properties.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/frb/syntax-properties.js b/core/frb/syntax-properties.js index 020a9f0aa2..3720db2b20 100644 --- a/core/frb/syntax-properties.js +++ b/core/frb/syntax-properties.js @@ -31,11 +31,17 @@ function _parseRequirementsFromSyntax(syntax, requirements) { _requirements = requirements || null; if (type === "property" && args[0].type === "value") { - (_requirements || (_requirements = [])).push(args[1].value); + if(!_requirements || (_requirements && _requirements.indexOf(args[1].value) === -1)) { + (_requirements || (_requirements = [])).push(args[1].value); + } } else if (type === "property" && args[0].type === "property") { - var subProperty = [args[1].value]; + var subProperty = [args[1].value], + result; _parseRequirementsFromSyntax(args[0], subProperty); - (_requirements || (_requirements = [])).push(subProperty.reverse().join(".")); + result = subProperty.reverse().join("."); + if(!_requirements || (_requirements && _requirements.indexOf(result) === -1)) { + (_requirements || (_requirements = [])).push(result); + } } else if (type === "record") { _requirements = _parseRequirementsFromRecord(syntax, _requirements); } else if (args) { From 8997ce01e1ab8e668f67f25eb4f173f31c9de638 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 23:53:25 -0800 Subject: [PATCH 332/407] add property to help iterate propertyDescriptor names --- core/meta/object-descriptor.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/meta/object-descriptor.js b/core/meta/object-descriptor.js index d537c2734e..f74eadd1f7 100644 --- a/core/meta/object-descriptor.js +++ b/core/meta/object-descriptor.js @@ -678,6 +678,19 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends } }, + /* + s = new Set(m.keys()) + Set(2) {"a", "b"} + + a = Array.from(m.keys()) + (2) ["a", "b"] + */ + propertyDescriptorNamesIterator: { + get: function() { + return this._propertyDescriptorsTable.keys(); + } + }, + /** * Adds a new property descriptor to this object descriptor. * From dd3f43e85cf7448d8a09ff4623a9631ce8eab33d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 23:54:25 -0800 Subject: [PATCH 333/407] add ability to have a property that has a Map type --- core/meta/property-descriptor.js | 50 +++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index 5cc957db18..ea6d04e87a 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -41,11 +41,13 @@ var Defaults = { denyDelete: false, deleteRule: DeleteRule.NULLIFY, inversePropertyName: void 0, + keyType: void 0, valueType: "string", collectionValueType: "list", valueObjectPrototypeName: "", valueObjectModuleId: "", valueDescriptor: void 0, + keyDescriptor: void 0, enumValues: [], defaultValue: void 0, helpKey: "", @@ -137,11 +139,13 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# //and deserializing denyDelete will set the equivallent on value deleteRule // this._setPropertyWithDefaults(serializer, "denyDelete", this.denyDelete); this._setPropertyWithDefaults(serializer, "deleteRule", this.deleteRule); + this._setPropertyWithDefaults(serializer, "keyType", this.keyType); this._setPropertyWithDefaults(serializer, "valueType", this.valueType); this._setPropertyWithDefaults(serializer, "collectionValueType", this.collectionValueType); this._setPropertyWithDefaults(serializer, "valueObjectPrototypeName", this.valueObjectPrototypeName); this._setPropertyWithDefaults(serializer, "valueObjectModuleId", this.valueObjectModuleId); this._setPropertyWithDefaults(serializer, "valueDescriptor", this._valueDescriptorReference); + this._setPropertyWithDefaults(serializer, "keyDescriptor", this._valueDescriptorReference); if (this.enumValues.length > 0) { this._setPropertyWithDefaults(serializer, "enumValues", this.enumValues); } @@ -182,11 +186,13 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# this._overridePropertyWithDefaults(deserializer, "readOnly"); this._overridePropertyWithDefaults(deserializer, "denyDelete"); this._overridePropertyWithDefaults(deserializer, "deleteRule"); + this._overridePropertyWithDefaults(deserializer, "keyType"); this._overridePropertyWithDefaults(deserializer, "valueType"); this._overridePropertyWithDefaults(deserializer, "collectionValueType"); this._overridePropertyWithDefaults(deserializer, "valueObjectPrototypeName"); this._overridePropertyWithDefaults(deserializer, "valueObjectModuleId"); this._overridePropertyWithDefaults(deserializer, "_valueDescriptorReference", "valueDescriptor", "targetBlueprint"); + this._overridePropertyWithDefaults(deserializer, "_keyDescriptorReference", "keyDescriptor"); this._overridePropertyWithDefaults(deserializer, "enumValues"); this._overridePropertyWithDefaults(deserializer, "defaultValue"); this._overridePropertyWithDefaults(deserializer, "helpKey"); @@ -467,6 +473,16 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# value: null }, + /** + * @type {string} + * TODO: This is semantically similar to keyDescriptor + * We should check if keyDescriptor can do the same job and eliminate + * this. + */ + keyType: { + value: Defaults.keyType + }, + /** * @type {string} * TODO: This is semantically similar to valueDescriptor @@ -477,10 +493,11 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# value: Defaults.valueType }, + /** * @type {string} * - * This property specifies the type of collectinon this property should use. + * This property specifies the type of collection this property should use. * Default is an Array, but this could be a Set or other type of collection. */ collectionValueType: { @@ -508,6 +525,9 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * return a promise. * @type {string} */ + _valueDescriptorReference: { + value: undefined + }, valueDescriptor: { serializable: false, get: function () { @@ -525,6 +545,34 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# } }, + /** + * Promise for the descriptor of objects found as key of the property when it is a Map + * + * **Note**: The setter expects an actual descriptor but the getter will + * return a promise. + * @type {string} + */ + _keyDescriptorReference: { + value: undefined + }, + keyDescriptor: { + serializable: false, + get: function () { + // TODO: Needed for backwards compatibility with ObjectDescriptorReference. + // Remove eventually, this can become completely sync + if (this._keyDescriptorReference && typeof this._keyDescriptorReference.promise === "function") { + deprecate.deprecationWarningOnce("valueDescriptor reference via ObjectDescriptorReference", "direct reference via object syntax"); + return this._keyDescriptorReference.promise(this.require); + } else { + return this._keyDescriptorReference && Promise.resolve(this._keyDescriptorReference); + } + }, + set: function (descriptor) { + this._keyDescriptorReference = descriptor; + } + }, + + _targetObjectDescriptorReference: { value: null }, From 73427a8dc21eb763c0f26934254ff5a286fd083f Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 23:56:02 -0800 Subject: [PATCH 334/407] fix a bug and avoid swallowing an exception --- core/serialization/deserializer/montage-reviver.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/core/serialization/deserializer/montage-reviver.js b/core/serialization/deserializer/montage-reviver.js index c4c65c7f17..dd95609169 100644 --- a/core/serialization/deserializer/montage-reviver.js +++ b/core/serialization/deserializer/montage-reviver.js @@ -70,10 +70,12 @@ var ModuleLoader = Montage.specialize({ } if (moduleDescriptor.mappingRedirect !== void 0) { - return this.getExports( - moduleDescriptor.mappingRequire, - moduleDescriptor.mappingRedirect - ); + return moduleDescriptor.mappingRequire.getModuleDescriptor(moduleDescriptor.mappingRedirect); + + // return this.getExports( + // moduleDescriptor.mappingRequire, + // moduleDescriptor.mappingRedirect + // ); } return moduleDescriptor; @@ -100,6 +102,8 @@ var ModuleLoader = Montage.specialize({ if (!module && !reviver._isSync) { module = _require.async(moduleId); + } else { + throw err; } } From d7f9a1d974aa08169c6428c49ec7318004dbc2c6 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 23:57:01 -0800 Subject: [PATCH 335/407] wip - refactor --- .../raw-embedded-value-to-object-converter.js | 86 +++++++++++-------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/data/converter/raw-embedded-value-to-object-converter.js b/data/converter/raw-embedded-value-to-object-converter.js index ddaa1f1fbf..630fdbe474 100644 --- a/data/converter/raw-embedded-value-to-object-converter.js +++ b/data/converter/raw-embedded-value-to-object-converter.js @@ -94,47 +94,65 @@ exports.RawEmbeddedValueToObjectConverter = RawValueToObjectConverter.specialize */ revert: { value: function (v) { - var self = this, - revertedValue, - result; + var self = this; + + if(!v) { + return v; + } else { + return Promise.all([this._descriptorToFetch, this.service]).then(function (values) { + var revertedValue, + result, + revertedValuePromise; + - if (v) { - if (!this.compiledRevertSyntax) { - return Promise.all([this._descriptorToFetch, this.service]).then(function (values) { - var objectDescriptor = values[0], - service = values[1]; - - if(Array.isArray(v)) { - if(v.length) { - revertedValue = []; - for(var i=0, countI=v.length, promises;(i Date: Wed, 16 Dec 2020 23:58:01 -0800 Subject: [PATCH 336/407] improve foreignDescriptor setter --- data/converter/raw-value-to-object-converter.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/data/converter/raw-value-to-object-converter.js b/data/converter/raw-value-to-object-converter.js index 618d25c276..7b4b9e8e80 100644 --- a/data/converter/raw-value-to-object-converter.js +++ b/data/converter/raw-value-to-object-converter.js @@ -219,7 +219,11 @@ exports.RawValueToObjectConverter = ExpressionConverter.specialize( /** @lends R this._foreignDescriptorReference && this._foreignDescriptorReference.promise(require); }, set: function (descriptor) { - this._foreignDescriptor = descriptor; + if(descriptor !== this._foreignDescriptor) { + this._foreignDescriptor = descriptor; + //Clear the cache in case it's shared + this.__descriptorToFetch = null; + } } }, From 16250c602fb0f389ccc3ad3d73ccec9b09452fcc Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 16 Dec 2020 23:59:14 -0800 Subject: [PATCH 337/407] improve deserialization of target property --- data/service/data-operation.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 375ebbac3f..d5d2813d74 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -273,19 +273,24 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy this.timeStamp = value; } /* keeping dataDescriptor here for temporary backward compatibility */ - value = deserializer.getProperty("targetModuleId") || deserializer.getProperty("dataDescriptor"); + value = deserializer.getProperty("target") || deserializer.getProperty("targetModuleId") || deserializer.getProperty("dataDescriptor"); if (value !== void 0) { if(Array.isArray(value)) { this.target = value.map((objectDescriptorModuleIid) => {return this.mainService.objectDescriptorWithModuleId(objectDescriptorModuleIid)}); - } else if(this.mainService) { - if(value === null) { - this.target = this.mainService + } else if(typeof value === "string") { + + if(this.mainService) { + if(value === null) { + this.target = this.mainService + } else { + this.target = this.mainService.objectDescriptorWithModuleId(value); + } } else { - this.target = this.mainService.objectDescriptorWithModuleId(value); + //Last resort, if we can't construct the target, let's carry the data that was supposed to help us do so + this.targetModuleId = value; } } else { - //Last resort, if we can't construct the target, let's carry the data that was supposed to help us do so - this.targetModuleId = value; + this.target = value; } } From 47f502d304024a462d942e4c365f980ec11f566b Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 17 Dec 2020 00:10:48 -0800 Subject: [PATCH 338/407] - fix bug in mappingForType when called on rootService - regression in getObjectProperties - avoid tracking changes for properties that are derived expressions from others --- data/service/data-service.js | 51 +++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 4d63ea2c68..0b56e50e42 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -905,12 +905,22 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { */ mappingForType: { value: function (type) { - var mapping, localType = this.objectDescriptorForType(type); - while(localType && !(mapping = this._mappingByType.has(localType) && this._mappingByType.get(localType))) { - localType = localType.parent; + if(this.isRootService) { + var childService = this.childServiceForType(type); + if(childService) { + return childService.mappingForType(type); + } else { + return null; + } + } else { + var mapping, localType = this.objectDescriptorForType(type); + + while(localType && !(mapping = this._mappingByType.has(localType) && this._mappingByType.get(localType))) { + localType = localType.parent; + } + return mapping || null; } - return mapping || null; } }, @@ -1467,7 +1477,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { value: function (object, propertyNames) { if(!object) { - return Promise.resolveNull; + return Promise.resolve(null); } /* Benoit: @@ -1484,8 +1494,9 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } else if (this.isRootService) { // Get the data, accepting property names as an array or as a list // of string arguments while avoiding the creation of any new array. - var names = Array.isArray(propertyNames) ? propertyNames : Array.prototype.slice.call(arguments, 1), - start = names === propertyNames ? 0 : 1; + //var names = Array.isArray(propertyNames) ? propertyNames : Array.prototype.slice.call(arguments, 1), + var names = Array.isArray(propertyNames) ? propertyNames : arguments, + start = names === propertyNames ? 0 : 1; return this._getOrUpdateObjectProperties(object, names, start, false); } else { @@ -2328,6 +2339,20 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { if(inversePropertyCardinality > 1) { /* value needs to be added to the other's side: + + BUT - TODO - doing value[inversePropertyName] actually fires the trigger if wasn't there alredy. + In some cases, we rely on the value being there so it gets saved properly, by putting a foreignKey in for example. + It might be possible to handle that when we save only, or we could do the lookup using the property getter's secret shouldFetch argument. + + inverseValue = Object.getPropertyDescriptor(value,inversePropertyName).get(false); //<-shouldFetch false + + If we add the value and we don't know what was there (because we didn't fetch), we won't be able to do optimistic locking + We also would need to mark that property as "incommplete?", which we would need to do to able to add to a relationship without resolving it. + such that if the user actually fetch that property we can re-apply what was added/removed locally to what was actually fetched. + + Also value[inversePropertyName] does fire the trigger, but it's async, so we're likely missing the value here and we migh need to use a promise with + getObjectProperty/ies + */ inverseValue = value[inversePropertyName]; if(inverseValue) { @@ -2563,8 +2588,16 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { var dataObject = changeEvent.target, key = changeEvent.key, objectDescriptor = this.objectDescriptorForObject(dataObject), - propertyDescriptor = objectDescriptor.propertyDescriptorForName(key), - inversePropertyName = propertyDescriptor.inversePropertyName, + propertyDescriptor = objectDescriptor.propertyDescriptorForName(key); + + + //Property with definitions are read-only shortcuts, we don't want to treat these as changes the raw layers will want to know about + if(propertyDescriptor.definition) { + return; + } + + + var inversePropertyName = propertyDescriptor.inversePropertyName, inversePropertyDescriptor; if(inversePropertyName) { From 2b6dd929d4be8b6d9bf4ce3df53e8decd1c93ae2 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 17 Dec 2020 00:14:10 -0800 Subject: [PATCH 339/407] - performance improvements by adding cache - add the bility to track changes on values that are Maps using addMapChangeListener. We don't yet have the fine-grained ability to track only changes to the Map, so it becomes a change on the whole property of the host object. Not ideal, but a start. --- data/service/data-trigger.js | 223 +++++++++++++++++++++++++++++------ 1 file changed, 188 insertions(+), 35 deletions(-) diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js index 614dfb84a8..2e9389eaec 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -228,6 +228,38 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy } }, + __valueGetter: { + value: undefined, + writable: true, + configurable: true, + enumerable: false + }, + _cacheValueGetter: { + value: function() { + var prototype, descriptor, getter, propertyName = this._propertyName; + + // Search the prototype chain for a getter for this property, + // starting just after the prototype that called this method. + prototype = Object.getPrototypeOf(this._objectPrototype); + while (prototype) { + descriptor = Object.getOwnPropertyDescriptor(prototype, propertyName); + getter = descriptor && descriptor.get; + prototype = !getter && Object.getPrototypeOf(prototype); + } + + if(!getter) getter = null; + + this.__valueGetter = getter; + } + }, + _valueGetter: { + get: function() { + return this.__valueGetter !== undefined + ? this.__valueGetter + : (this._cacheValueGetter() || this.__valueGetter); + } + }, + /** * @method * @argument {Object} object @@ -241,7 +273,7 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy configurable: true, writable: true, value: function (object, shouldFetch) { - var prototype, descriptor, getter, propertyName = this._propertyName; + var prototype, descriptor, getter = this._valueGetter, propertyName = this._propertyName; /* Experiment to see if it would make sense to avoid triggering getObjectProperty during mapping? @@ -265,21 +297,58 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy //} - // Search the prototype chain for a getter for this property, - // starting just after the prototype that called this method. + // Return the property's current value. + return getter ? getter.call(object) : object[this._privatePropertyName]; + } + }, + + __valueSetter: { + value: undefined, + writable: true, + configurable: true, + enumerable: false + }, + _cacheValueSetter: { + value: function() { + var prototype, descriptor, getter, setter, writable, propertyName = this._propertyName; + + // Search the prototype chain for a setter for this trigger's + // property, starting just after the trigger prototype that caused + // this method to be called. prototype = Object.getPrototypeOf(this._objectPrototype); while (prototype) { descriptor = Object.getOwnPropertyDescriptor(prototype, propertyName); getter = descriptor && descriptor.get; - prototype = !getter && Object.getPrototypeOf(prototype); + setter = getter && descriptor.set; + writable = !descriptor || setter || descriptor.writable; + prototype = writable && !setter && Object.getPrototypeOf(prototype); } - // Return the property's current value. - return getter ? getter.call(object) : object[this._privatePropertyName]; + if(!setter) setter = null; + + this.__valueSetter = setter; + this.__isPropertyWritable = writable; } }, - _valueSetter: { - value: undefined + get: function() { + return this.__valueSetter !== undefined + ? this.__valueSetter + : (this._cacheValueSetter() || this.__valueSetter); + } + }, + + __isPropertyWritable: { + value: undefined, + writable: true, + configurable: true, + enumerable: false + }, + _isPropertyWritable: { + get: function() { + return this.__isPropertyWritable !== undefined + ? this.__isPropertyWritable + : (this._cacheValueSetter() || this.__isPropertyWritable); + } }, /** * Note that if a trigger's property value is set after that values is @@ -296,7 +365,7 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy configurable: true, writable: true, value: function (object, value, _dispatchChange) { - var status, prototype, descriptor, getter, setter = this._valueSetter, writable, currentValue, isToMany, isArray, initialValue, + var status, prototype, descriptor, getter, setter = this._valueSetter, writable, currentValue, isToMany, isArray, isMap, initialValue, dispatchChange = (arguments.length === 3) ? _dispatchChange : true; // Get the value's current status and update that status to indicate @@ -306,38 +375,49 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy status = this._getValueStatus(object); this._setValueStatus(object, null); - - //We're not changing inheritance at runtime, no need to wolk the tree everytime... - if(!setter) { - // Search the prototype chain for a setter for this trigger's - // property, starting just after the trigger prototype that caused - // this method to be called. - prototype = Object.getPrototypeOf(this._objectPrototype); - while (prototype) { - descriptor = Object.getOwnPropertyDescriptor(prototype, this._propertyName); - getter = descriptor && descriptor.get; - setter = getter && descriptor.set; - writable = !descriptor || setter || descriptor.writable; - prototype = writable && !setter && Object.getPrototypeOf(prototype); - } - this._valueSetter = setter; - } - initialValue = this._getValue(object); //If Array / to-Many isToMany = this.propertyDescriptor.cardinality !== 1; isArray = Array.isArray(initialValue); + isMap = !isArray && initialValue instanceof Map; // Set this trigger's property to the desired value, but only if // that property is writable. if (setter) { setter.call(object, value); //currentValue = value; - } else if (writable) { + } else if (this._isPropertyWritable) { + + if (isToMany) { + if(isArray) { + object[this._privatePropertyName].splice.apply(initialValue, [0, Infinity].concat(value)); + } else if(isMap) { + //We want to maintain the same map, + var map = object[this._privatePropertyName], + //iterator are "lives" until used, so we make a copy + mapIterator = new Set(map.keys()).values(), + valueIterator = value.keys(), + iKey; + + //Add what we don't have, and set if value different for same key + while(iKey = valueIterator.next().value) { + if(!map.has(iKey)) { + map.set(iKey,value.get(iKey)); + } else if(map.get(iKey) !== value.get(iKey)) { + map.set(iKey,value.get(iKey)); + } + } - if (isToMany && isArray) { - object[this._privatePropertyName].splice.apply(initialValue, [0, Infinity].concat(value)); - } + //Remove what we had that's not in value + while(iKey = mapIterator.next().value) { + if(!value.has(iKey)) { + map.delete(iKey); + } + } + } + else { + object[this._privatePropertyName] = value; + } } else { object[this._privatePropertyName] = value; } @@ -348,19 +428,27 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy if(currentValue !== initialValue) { if(isToMany) { - if(initialValue && isArray) { + if(initialValue) { var listener = this._collectionListener.get(object); if(listener) { - initialValue.removeRangeChangeListener(listener); + + if(isArray) { + initialValue.removeRangeChangeListener(listener); + } else if(isMap) { + initialValue.removeMapChangeListener(listener); + } + if(!currentValue) { this._collectionListener.delete(object); } + } + } if(currentValue) { if(Array.isArray(currentValue)) { var self = this, - listener = function _triggerCollectionListener(plus, minus, index) { + listener = function _triggerArrayCollectionListener(plus, minus, index) { //If we're not in the middle of a mapping...: if(!self._service._objectsBeingMapped.has(object)) { //Dispatch update event @@ -391,9 +479,73 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy currentValue.addRangeChangeListener(listener); } else if(currentValue instanceof Map) { - console.error("DataTrigger misses implementation to track changes on property values that are Map"); + var self = this, + listener = function _triggerMapCollectionListener(value, key) { + //If we're not in the middle of a mapping...: + if(!self._service._objectsBeingMapped.has(object)) { + //Dispatch update event + var changeEvent = new ChangeEvent; + changeEvent.target = object; + /* + Here we're saying the hosting object changed. + so maybe this should be called "property","propertyValue","previousPropertyValue" + which opens up the use of key for the content, for Map, but also key as index for an array. We only support expressing changes on 1 index for array, happy coincidence! + + !!! What if we renamed + - addedValues to added + - for Array, added contains values added to the array (plus) + - for Map, added contains one, or more pairs [key,values] as entries, when it's actually added + + - removedValues to removed + - for Array, removed contains values removed to the array (minus) + - for Map, removed contains one, or more pairs [key,values] as entries, when it's actually deleted from the map + + - key/keyValue represents a set on an object as well as a map, a mutation of something that was there. For Array it's useful to use it for length on top of added/removed + + - previousKeyValue if there contains the value before keyValue at key + + */ + changeEvent.key = self._propertyName; + //We set the whole Map() since we don't have the tools yet to express better + changeEvent.keyValue = object[self._privatePropertyName]; + + /* + Today, we'd have to listen to before change to know about the value that was previously under "key". It wouldn't be very practical for the trigger to store somewhere that value, so it can reuse it here. We probably can find a way. Another option could be to add the previous value to the arguments passed to the listener, which wouldn't break existing code and allow us to be smarter. + changeEvent.previousKeyValue = initialValue; + */ + + /* + Look like we're missing in collections listen the semantic to exparess the fact that a key is gone from the Map, vs the key being there and containing undefined? + + changeEvent.index makes no sense for a Map or an object, but it is similar to a key, it's a "slot" + + How could we express the disparition of a Key? We would need a removedKeys? + */ + //We could use "key" for arrays and the value would be an integer + // changeEvent.index = index; + // changeEvent.addedValues = plus; + // changeEvent.removedValues = minus; + + //Or this? + //changeEvent.rangeChange = [plus, minus, index]; + + //Or both with a getter/setter for index, addedValues and removedValues on top of rangeChange? + + //To deal with changes happening to an array value of that property, + //we'll need to add/cancel observing on the array itself + //and dispatch added/removed change in the array's change handler. + + //Bypass EventManager for now + self._service.rootService.handleChange(changeEvent); + } + }; + + this._collectionListener.set(object,listener); + currentValue.addMapChangeListener(listener); } - else { + else if(this.propertyDescriptor.isLocalizable) { + console.error("DataTrigger misses implementation to track changes on to-many localized property values"); + } else { console.error("DataTrigger misses implementation to track changes on property values that are neither Array nor Map"); } @@ -515,6 +667,7 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy _fetchObjectProperty: { value: function (object) { var self = this; + //console.log("data-trigger: _fetchObjectProperty "+this._propertyName,object ); this._service.fetchObjectProperty(object, this._propertyName).then(function () { return self._fulfillObjectPropertyFetch(object); }).catch(function (error) { From d6ecd8c1c84e9c7833cbce41c4416e79ac8c847d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 17 Dec 2020 00:14:46 -0800 Subject: [PATCH 340/407] Fix console.error --- data/converter/raw-foreign-value-to-object-converter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/converter/raw-foreign-value-to-object-converter.js b/data/converter/raw-foreign-value-to-object-converter.js index d2a664cfd5..da77717eec 100644 --- a/data/converter/raw-foreign-value-to-object-converter.js +++ b/data/converter/raw-foreign-value-to-object-converter.js @@ -576,7 +576,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( this._rawDataPropertyByForeignDescriptor.set(anObjectDescriptor,rawProperty); } else { - console.error("Couldn't map mapObjectPropertyToRawProperty with rawDataTypeMappingExpressionSyntax", object, property, rawDataTypeMappingExpressionSyntax); + console.error("Couldn't find raw data Property for foreign descriptor", anObjectDescriptor, "rawDataTypeMapping:",rawDataTypeMapping); } } From 746e3600590e3c2d3f4a4b5f3bdeaeb3333ccfc8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 17 Dec 2020 00:16:23 -0800 Subject: [PATCH 341/407] need more baking, refactor of requirements using "core/frb/syntax-properties" rather than original MappingRule implementation --- data/service/mapping-rule.js | 77 +++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/data/service/mapping-rule.js b/data/service/mapping-rule.js index f3b24c8efb..ef6bca0f58 100644 --- a/data/service/mapping-rule.js +++ b/data/service/mapping-rule.js @@ -2,6 +2,7 @@ var Montage = require("montage").Montage, compile = require("core/frb/compile-evaluator"), parse = require("core/frb/parse"), Promise = require("core/promise").Promise, + syntaxProperties = require("core/frb/syntax-properties"), deprecate = require("core/deprecate"); @@ -92,15 +93,7 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { type = syntax.type, _requirements = requirements || null; - if (type === "property" && args[0].type === "value") { - (_requirements || (_requirements = [])).push(args[1].value); - } else if (type === "property" && args[0].type === "property") { - var subProperty = [args[1].value]; - this._parseRequirementsFromSyntax(args[0], subProperty); - (_requirements || (_requirements = [])).push(subProperty.reverse().join(".")); - } else if (type === "record") { - _requirements = this._parseRequirementsFromRecord(syntax, _requirements); - } else if(type === "value" && !args && this.sourcePath === "this") { + if(type === "value" && !args && this.sourcePath === "this") { /* added for pattern used for polymorphic relationship, where we have multiple foreignKeys for each possible destination, and we need to look at the converter and it's foreignDescriptorMappings for their expression syntax. */ @@ -111,29 +104,69 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { if(foreignDescriptorMappings) { for(var i=0, countI = foreignDescriptorMappings.length, iRawDataTypeMapping;(i Date: Thu, 17 Dec 2020 00:18:23 -0800 Subject: [PATCH 342/407] add a method to know the property used by a RawDataTypeMapping to set types apart in their expresssion --- data/service/raw-data-type-mapping.js | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/data/service/raw-data-type-mapping.js b/data/service/raw-data-type-mapping.js index a0cd045e9c..b64c8813d3 100644 --- a/data/service/raw-data-type-mapping.js +++ b/data/service/raw-data-type-mapping.js @@ -1,3 +1,8 @@ +/** + @module montage/data/service/raw-data-type-mapping +*/ + + var Montage = require("montage").Montage, Criteria = require("core/criteria").Criteria, ObjectDescriptor = require("core/meta/object-descriptor").ObjectDescriptor; @@ -72,6 +77,29 @@ exports.RawDataTypeMapping = Montage.specialize({ } }, + _rawDataProperty: { + value: undefined + }, + + /* + Assuming the raw-data-type-mapping expressions are of the form: "aForeignKeyId.defined()" . We might have to make it mmore general? + */ + + rawDataProperty: { + get: function() { + if(this._rawDataProperty === undefined) { + var expressionSyntax = this.expressionSyntax; + + if(expressionSyntax.type === "defined" && expressionSyntax.args[0].type === "property") { + this._rawDataProperty = expressionSyntax.args[0].args[1].value; + } else { + console.error("Couldn't determine rawDataProperty from RawDataTypeMapping:",this, " expressionSyntax:", this.expressionSyntax); + this._rawDataProperty = null; + } + } + return this._rawDataProperty; + } + }, /** * Class to create an instance of when RawDataTypeMapping.criteria.evaluate From e04dc0ff7057cf2be3e9c6f76ce21dd58e0e5337 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 17 Dec 2020 00:28:08 -0800 Subject: [PATCH 343/407] - add support to have a propertyDescriptor to describe primaryKeys, so we can define the valueType (raw) and not just the name. - add the ability to use added/removed information to map object back to raw data, used by DataOperations. - add the ability to populate a raw data snapshot with the last read values of properties that changed using snapshot and added/remobed - fix a bug in mapObjectToCriteriaSourceForProperty - add support for mappings in situations where there are multiple possible types for one relationship - add support for cases where one object property could end up being saved as multiple raw properties (like a Map object mapped as one array of keys and one array of values - --- data/service/expression-data-mapping.js | 599 ++++++++++++++++++------ data/service/raw-data-service.js | 41 +- 2 files changed, 490 insertions(+), 150 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index ddbb7addd9..2ab99831cf 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -97,11 +97,20 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData this.rawDataTypeName = value; } - value = deserializer.getProperty("rawDataPrimaryKeys"); + + + value = deserializer.getProperty("primaryKeyPropertyDescriptors"); if (value) { - this.rawDataPrimaryKeys = value; + this.primaryKeyPropertyDescriptors = value; + this.rawDataPrimaryKeys = value.map((aPropertyDescriptor) => {return aPropertyDescriptor.name}); + } else { + value = deserializer.getProperty("rawDataPrimaryKeys"); + if (value) { + this.rawDataPrimaryKeys = value; + } } + if (hasReferences && !deserializer.isSync) { result = this.resolveReferences().then(function () { value = deserializer.getProperty("objectMapping"); @@ -505,8 +514,18 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData * data mapping. * @type {ObjectDescriptor} */ + _schemaDescriptor: { + value: undefined + }, schemaDescriptor: { get: function () { + // if(this._schemaDescriptor === undefined) { + // //Give service a chance to provide one, likely programmatically buit otherwise it would have been set in serialization + // var value = this.service.mappingRequestsSchemaDescriptor(this); + // if(value !== undefined) { + // this._schemaDescriptor = value; + // } + // } return this._schemaDescriptor; }, set: function (value) { @@ -569,8 +588,8 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData this._service = value; //Propagate to rules one and for all - this._propagateServiceToMappingRulesConverter(value, this.objectMappingRules); - this._propagateServiceToMappingRulesConverter(value, this.rawDataMappingRules); + //this._propagateServiceToMappingRulesConverter(value, this.objectMappingRules); + //this._propagateServiceToMappingRulesConverter(value, this.rawDataMappingRules); } } }, @@ -722,20 +741,105 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } }, + //Cache the union of all the object rules relevant to a set of RawDataKeys + __rawDataMappingRulesByObjectProperties: { + value: undefined + }, + + _rawDataMappingRulesByObjectProperties: { + get: function() { + return this.__rawDataMappingRulesByObjectProperties || (this.__rawDataMappingRulesByObjectProperties = new Map()); + } + }, + + _buildRawDataMappingRulesForObjectProperty: { + value: function (objectProperty) { + var rawDataMappingRules = this.rawDataMappingRules, + rulesIterator = rawDataMappingRules.values(), + matchingRules = new Set(), + aRule, aRulePropertyRequirements, iMatch, + i, countI; + + while ((aRule = rulesIterator.next().value)) { + aRulePropertyRequirements = aRule.requirements; + if(aRulePropertyRequirements) { + iMatch = 0; + for(i=0, countI = aRulePropertyRequirements.length;i { + this._assignMappedDiffDataToPropertyChangesObjectKey(diffData, aPropertyChanges,"addedValues"); + }); + } else { + this._assignMappedDiffDataToPropertyChangesObjectKey(diffData, aPropertyChanges,"addedValues"); + } + } + + if(removed) { + tmpExtendObject[propertyName] = Array.from(result); + + removedResult = this.__mapObjectToRawDataProperty(tmpExtendObject, diffData, rawPropertyName, _rule, lastReadSnapshot, rawDataSnapshot); + + if (this._isAsync(removedResult)) { + removedResultIsPromise = true; + removedResult = removedResult.then(() => { + this._assignMappedDiffDataToPropertyChangesObjectKey(diffData, aPropertyChanges,"addedValues"); + }); + } else { + this._assignMappedDiffDataToPropertyChangesObjectKey(diffData, aPropertyChanges,"addedValues"); + } + } + + if(addedResultIsPromise && removedResultIsPromise) { + return Promise.all([addedResultIsPromise, removedResultIsPromise]); + } else if(addedResultIsPromise) { + return addedResultIsPromise; + } else if(removedResultIsPromise) { + return removedResultIsPromise; + } + + return; + + } else { + return this.__mapObjectToRawDataProperty(object, data, rawPropertyName, _rule, lastReadSnapshot, rawDataSnapshot); + } + } + }, + + _assignMappedDiffDataToPropertyChangesObjectKey: { + value: function(diffData, aPropertyChanges, key /* addedValues/removedValues*/) { + var mappedKeys = Object.keys(diffData), + i, countI; + + for(i=0, count = mappedKeys.length; (i 1) { + return Promise.all(iPromises); + } else if(iPromises.length === 1) { + return iPromises[0]; + } else { + return; } } } + return; } else { throw new Error("No objectMappingRules found to map property "+propertyName+" of object,", object, "to raw data"); @@ -1188,117 +1458,117 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData * @argument Promise{string} propertyName - The name of the property to map. */ - mapObjectPropertyToRawProperty: { - value: function(object, property) { - var objectRule = this.objectMappingRules.get(property), - rawProperty; + // mapObjectPropertyToRawProperty: { + // value: function(object, property) { + // var objectRule = this.objectMappingRules.get(property), + // rawProperty; - if(objectRule) { - var rawDataMappingRules = this.rawDataMappingRulesForPropertyName(property); + // if(objectRule) { + // var rawDataMappingRules = this.rawDataMappingRulesForPropertyName(property); - if(rawDataMappingRules) { - if(rawDataMappingRules.length === 1) { - rawProperty = rawDataMappingRules[0].targetPath; + // if(rawDataMappingRules) { + // if(rawDataMappingRules.length === 1) { + // rawProperty = rawDataMappingRules[0].targetPath; - //Temporary sanity test - if(rawProperty !== objectRule.sourcePath ) { - console.error("Something's not right here, DEBUG ME!!!"); - } + // //Temporary sanity test + // if(rawProperty !== objectRule.sourcePath ) { + // console.error("Something's not right here, DEBUG ME!!!"); + // } - } else { - /* - We need to loop on the rules and evaluate them with object. There *should* be only one matching for currently known use-cases, but in theory there could be more. If such a real use case emerges, we'll have to revisit the caller code to deal with that. - - For now, we'll use the first rule that doesn't evaluate to undefined - */ - var propertyScope = this._scope.nest(object); - - - for(var i=0, countI = rawDataMappingRules.length, iRule, iRuleValue, iRuleValuePromises;(i Date: Thu, 17 Dec 2020 00:34:31 -0800 Subject: [PATCH 344/407] - initial support for layout properties - initial support to expose DOM layout values as component-scoped CSS custom properties - add environment property, which can be useful for richer media-query style logical layout handling criteria - expose isTemplateLoaded property --- ui/component.js | 367 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 365 insertions(+), 2 deletions(-) diff --git a/ui/component.js b/ui/component.js index 79a7bf4d1e..1f430687fa 100644 --- a/ui/component.js +++ b/ui/component.js @@ -29,7 +29,9 @@ var Montage = require("../core/core").Montage, drawLogger = require("../core/logger").logger("drawing").color.blue(), WeakMap = require("core/collections/weak-map"), Map = require("core/collections/map"), - Set = require("core/collections/set"); + Set = require("core/collections/set"), + currentEnvironment = require("core/environment").currentEnvironment, + PropertyChanges = require("core/collections/listen/property-changes"); /** * @const @@ -859,6 +861,18 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto } }, + _environment: { + value: null + }, + + environment: { + enumerable: false, + get: function () { + return this._environment || (Component.prototype._environment = currentEnvironment); + } + }, + + /** * Convenience to access the defaultEventManager object. * @type {EventManager} @@ -1327,6 +1341,12 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto enumerable: false, value: false }, + isTemplateLoaded: { + enumerable: false, + get: function() { + return this._isTemplateLoaded; + } + }, _isTemplateInstantiated: { enumerable: false, @@ -2582,6 +2602,53 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto value: Function.noop }, + + _didDraw: { + enumerable: false, + value: function(frameTime) { + if(this._updatesLayoutProperties) { + var ownerStyle = this.ownerComponent && this.ownerComponent.element && this.ownerComponent.element.style; + + if(ownerStyle) { + var boundingRect = this.element.getBoundingClientRect(), + identifier = this.identifier; + } + + + + if(this._updatesLayoutPropertyX) { + ownerStyle.setProperty(`--${identifier}X`, boundingRect.x); + } + if(this._updatesLayoutPropertyY) { + ownerStyle.setProperty(`--${identifier}Y`, boundingRect.y); + } + if(this._updatesLayoutPropertyWidth) { + ownerStyle.setProperty(`--${identifier}Width`, boundingRect.width); + } + if(this._updatesLayoutPropertyHeight) { + ownerStyle.setProperty(`--${identifier}Height`, boundingRect.height); + } + if(this._updatesLayoutPropertyTop) { + ownerStyle.setProperty(`--${identifier}Top`, boundingRect.top); + this.top = boundingRect.top; + } + if(this._updatesLayoutPropertyRight) { + ownerStyle.setProperty(`--${identifier}Right`, boundingRect.right); + this.right = boundingRect.right; + } + if(this._updatesLayoutPropertyBottom) { + ownerStyle.setProperty(`--${identifier}Bottom`, boundingRect.bottom); + this.bottom = boundingRect.bottom; + } + if(this._updatesLayoutPropertyLeft) { + ownerStyle.setProperty(`--${identifier}Left`, boundingRect.left); + this.left = boundingRect.left; + } + } + this.didDraw(frameTime); + } + }, + /** * Records whether or not we have been added to the parent's drawList. * @private @@ -3498,6 +3565,24 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto // classList this._drawClassListIntoComponent(); + + //Layout + var style = this.element && this.element.style; + if(style) { + if(this.top) { + style.setProperty("top", this.top); + } + if(this.right) { + style.setProperty("right", this.right); + } + if(this.bottom) { + style.setProperty("bottom", this.bottom); + } + if(this.left) { + style.setProperty("left", this.left); + } + } + } }, @@ -3750,7 +3835,285 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto ); }); } + }, + + /** + * As dispatching has been implemented in key setters, limit use of default method + * for corresponding keys. + * @private + */ + _superMakePropertyObservable : { + value: PropertyChanges.prototype.makePropertyObservable + }, + makePropertyObservable: { + value: function(key) { + + switch(key) { + case 'x': + this._updatesLayoutProperties = true; + this._updatesLayoutPropertyX = true; + break; + + case 'y': + this._updatesLayoutProperties = true; + this._updatesLayoutPropertyY = true; + break; + + case 'width': + this._updatesLayoutProperties = true; + this._updatesLayoutPropertyWidth = true; + break; + + case 'height': + this._updatesLayoutProperties = true; + this._updatesLayoutPropertyHeight = true; + break; + + case 'top': + this._updatesLayoutProperties = true; + this._updatesLayoutPropertyTop = true; + break; + + case 'right': + this._updatesLayoutProperties = true; + this._updatesLayoutPropertyRight = true; + break; + + case 'bottom': + this._updatesLayoutProperties = true; + this._updatesLayoutPropertyBottom = true; + break; + + case 'left': + this._updatesLayoutProperties = true; + this._updatesLayoutPropertyLeft = true; + break; + } + this._superMakePropertyObservable( key); + } + }, + + _updatesLayoutProperties: { + value: 0 + }, + _updatesLayoutPropertyX: { + value: false + }, + updatesLayoutPropertyX: { + get: function () { + return this._updatesLayoutPropertyX; + }, + set: function (value) { + if (this._updatesLayoutPropertyX !== value) { + this._updatesLayoutPropertyX = value; + if(value) { + this._updatesLayoutProperties++; + } else { + this._updatesLayoutProperties--; + } + this.needsDraw = true; + } + } + }, + + _updatesLayoutPropertyY: { + value: false + }, + updatesLayoutPropertyY: { + get: function () { + return this._updatesLayoutPropertyY; + }, + set: function (value) { + if (this._updatesLayoutPropertyY !== value) { + this._updatesLayoutPropertyY = value; + if(value) { + this._updatesLayoutProperties++; + } else { + this._updatesLayoutProperties--; + } + this.needsDraw = true; + } + } + }, + + _updatesLayoutPropertyWidth: { + value: false + }, + updatesLayoutPropertyWidth: { + get: function () { + return this._updatesLayoutPropertyWidth; + }, + set: function (value) { + if (this._updatesLayoutPropertyWidth !== value) { + this._updatesLayoutPropertyWidth = value; + if(value) { + this._updatesLayoutProperties++; + } else { + this._updatesLayoutProperties--; + } + this.needsDraw = true; + } } + }, + + _updatesLayoutPropertyHeight: { + value: false + }, + updatesLayoutPropertyHeight: { + get: function () { + return this._updatesLayoutPropertyHeight; + }, + set: function (value) { + if (this._updatesLayoutPropertyHeight !== value) { + this._updatesLayoutPropertyHeight = value; + if(value) { + this._updatesLayoutProperties++; + } else { + this._updatesLayoutProperties--; + } + this.needsDraw = true; + } + } + }, + + _updatesLayoutPropertyTop: { + value: false + }, + updatesLayoutPropertyTop: { + get: function () { + return this._updatesLayoutPropertyTop; + }, + set: function (value) { + if (this._updatesLayoutPropertyTop !== value) { + this._updatesLayoutPropertyTop = value; + if(value) { + this._updatesLayoutProperties++; + } else { + this._updatesLayoutProperties--; + } + this.needsDraw = true; + } + } + }, + + _updatesLayoutPropertyRight: { + value: false + }, + updatesLayoutPropertyRight: { + get: function () { + return this._updatesLayoutPropertyRight; + }, + set: function (value) { + if (this._updatesLayoutPropertyRight !== value) { + this._updatesLayoutPropertyRight = value; + if(value) { + this._updatesLayoutProperties++; + } else { + this._updatesLayoutProperties--; + } + this.needsDraw = true; + } + } + }, + + _updatesLayoutPropertyBottom: { + value: false + }, + updatesLayoutPropertyBottom: { + get: function () { + return this._updatesLayoutPropertyBottom; + }, + set: function (value) { + if (this._updatesLayoutPropertyBottom !== value) { + this._updatesLayoutPropertyBottom = value; + if(value) { + this._updatesLayoutProperties++; + } else { + this._updatesLayoutProperties--; + } + this.needsDraw = true; + } + } + }, + + + _updatesLayoutPropertyLeft: { + value: false + }, + updatesLayoutPropertyLeft: { + get: function () { + return this._updatesLayoutPropertyLeft; + }, + set: function (value) { + if (this._updatesLayoutPropertyLeft !== value) { + this._updatesLayoutPropertyLeft = value; + if(value) { + this._updatesLayoutProperties++; + } else { + this._updatesLayoutProperties--; + } + this.needsDraw = true; + } + } + }, + + _top: { + value: undefined + }, + top: { + get: function () { + return this._top; + }, + set: function (value) { + if (this._top !== value) { + this._top = value; + this.needsDraw = true; + } + } + }, + _right: { + value: undefined + }, + right: { + get: function () { + return this._right; + }, + set: function (value) { + if (this._right !== value) { + this._right = value; + this.needsDraw = true; + } + } + }, + _bottom: { + value: undefined + }, + bottom: { + get: function () { + return this._bottom; + }, + set: function (value) { + if (this._bottom !== value) { + this._bottom = value; + this.needsDraw = true; + } + } + }, + _left: { + value: undefined + }, + left: { + get: function () { + return this._left; + }, + set: function (value) { + if (this._left !== value) { + this._left = value; + this.needsDraw = true; + } + } + } + }, { @@ -4587,8 +4950,8 @@ var RootComponent = Component.specialize( /** @lends RootComponent.prototype */{ } for (i = 0; i < j; i++) { component = needsDrawList[i]; + component._didDraw(this._frameTime); component.dispatchEvent(this._didDrawEvent); - component.didDraw(this._frameTime); if (!component._completedFirstDraw) { firstDrawEvent = document.createEvent("CustomEvent"); firstDrawEvent.initCustomEvent("firstDraw", true, false, null); From e616a90f54dd67e07e301cc011fe923c17c88c90 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 17 Dec 2020 00:39:27 -0800 Subject: [PATCH 345/407] First iteration to make DataEditor able to orchestrate that all data needed by itself and owned components in its template are available before considering being able to draw --- ui/data-editor.js | 51 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/ui/data-editor.js b/ui/data-editor.js index 26210a23cd..9bfd22f32f 100644 --- a/ui/data-editor.js +++ b/ui/data-editor.js @@ -45,6 +45,26 @@ DataOrdering = require("data/model/data-ordering").DataOrdering; exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { + constructor: { + value: function DataEditor () { + this.super(); + this.canDrawGate.setField("dataLoaded", false); + // console.log("---------- "+this.constructor.name+" inDocument:"+this.inDocument+" —— dataLoaded: false",value); + + return this; + } + }, + defineBinding: { + value: function (targetPath, descriptor, commonDescriptor) { + var result = this.super(targetPath, descriptor, commonDescriptor); + + if(targetPath.startsWith("data")) { + console.log(this.constructor.name+" has ["+targetPath+"] bound to ["+descriptor.sourcePath+"]"+", parentComponent:",this.parentComponent); + } + return result; + } + }, + /** * A DataService used to fetch data. By default uses application.mainService * But when we support nested editing context / data services, could be a @@ -82,6 +102,10 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { if(this.fetchLimit) { this.__dataQuery.fetchLimit = this.fetchLimit; } + if(this.readExpressions) { + this.__dataQuery.readExpressions = this.readExpressions; + } + } } return this.__dataQuery; @@ -135,12 +159,13 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { fetchData: { value: function() { + + if(this._dataQuery) { var dataService = this.dataService, currentDataStream = this.dataStream, dataStream, self = this; - - + //console.log(this.constructor.name+" fetchData() >>>>> "); dataStream = dataService.fetchData(this._dataQuery); dataStream.then(function(data) { //console.log("Data fetched:",data); @@ -149,14 +174,26 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { //We need to dataService.cancelDataStream(currentDataStream); + self.didFetchData(data); + }, function(error) { console.log("fetchData failed:",error); + }) + .finally(() => { + this.canDrawGate.setField("dataLoaded", true); }); } + } }, + didFetchData: { + value: function (data) { + } + }, + + fetchDataIfNeeded: { value: function() { @@ -164,7 +201,8 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { this.__dataQuery = null; //If we're active for trhe user, we re-fetch - if(this.inDocument && this._dataQuery) { + //if(this.inDocument && this._dataQuery) { + if(this.isTemplateLoaded && this._dataQuery) { this.fetchData(); } } @@ -187,7 +225,7 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { templateDidLoad: { value: function () { - this.fetchData(); + this.fetchDataIfNeeded(); } }, @@ -287,6 +325,11 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { return this._data; }, set: function (value) { + // console.log(this.constructor.name+ " set data:",value, " inDocument:"+this.inDocument+", parentComponent:",this.parentComponent); + if(this._data === undefined && value !== undefined) { + // console.log("++++++++++ "+this.constructor.name+" inDocument:"+this.inDocument+" —— dataLoaded: true",value); + this.canDrawGate.setField("dataLoaded", true); + } if(value !== this._data) { this._data = value; } From f4402cc2ba0e8b87c2511c4d85edd4033a6c07b3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 17 Dec 2020 00:43:54 -0800 Subject: [PATCH 346/407] fix bug and inline use of css visibility property that prevents further CSS animations. We should rely on montage-Overlay--visible until we evolve it to use build-in/outs --- ui/overlay.reel/overlay.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/overlay.reel/overlay.js b/ui/overlay.reel/overlay.js index 5cf9fe0eb8..fa06e3d6b9 100644 --- a/ui/overlay.reel/overlay.js +++ b/ui/overlay.reel/overlay.js @@ -164,7 +164,7 @@ var Overlay = exports.Overlay = Component.specialize( /** @lends Overlay.prototy } } }, - + /** * Show the overlay. The overlay is displayed at the position determined by @@ -240,7 +240,7 @@ var Overlay = exports.Overlay = Component.specialize( /** @lends Overlay.prototy shouldComposerSurrenderPointerToComponent: { value: function (composer, pointer, component) { - if (component && component.element && !this.element.contains(component.element)) { + if (this.dismissOnExternalInteractioncomponent && component.element && !this.element.contains(component.element)) { this.hide(); } @@ -335,12 +335,12 @@ var Overlay = exports.Overlay = Component.specialize( /** @lends Overlay.prototy this.element.style.top = position.top + "px"; this.element.style.left = position.left + "px"; - this.element.style.visibility = "visible"; + // this.element.style.visibility = "visible"; this.callDelegateMethod("didShowOverlay", this); } else { - this.element.style.visibility = "hidden"; + // this.element.style.visibility = "hidden"; } } }, From db23c9094fd08002ee477077729e4d53e61c95f5 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 17 Dec 2020 00:45:19 -0800 Subject: [PATCH 347/407] fix to work with component that also overrides makePropertyObservable now --- ui/repetition.reel/repetition.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/repetition.reel/repetition.js b/ui/repetition.reel/repetition.js index 5818da6a94..8d8305bbcb 100644 --- a/ui/repetition.reel/repetition.js +++ b/ui/repetition.reel/repetition.js @@ -755,15 +755,15 @@ var Repetition = exports.Repetition = Component.specialize(/** @lends Repetition * for corresponding keys. * @private */ - _superMakePropertyObservable : { - value: PropertyChanges.prototype.makePropertyObservable - }, + // _superMakePropertyObservable : { + // value: PropertyChanges.prototype.makePropertyObservable + // }, makePropertyObservable: { value: function(key) { if(key === "selection") { this.isSelectionEnabled = true; } - this._superMakePropertyObservable( key); + this.super(key); } }, From d79ae8ba0fcb1f544b62cfc4515cc83312de57b3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 17 Dec 2020 17:58:38 -0800 Subject: [PATCH 348/407] - add currentEnvironment property on DataService - add languages property to better align with the fact that it should be an array, even though some browser like Safari don't expose it. We need it server side to store the result of parsing "accept-language" and we can send it back to client to inform them if needed - refactor systemLocaleIdentifier to return the first element in this.languages --- core/environment.js | 28 +++++++++++++++++++++++++--- data/service/data-service.js | 5 +++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/core/environment.js b/core/environment.js index 365d260da7..02f6c16a64 100644 --- a/core/environment.js +++ b/core/environment.js @@ -25,12 +25,34 @@ var Environment = exports.Environment = Montage.specialize({ systemLocaleIdentifier: { get: function () { - return typeof navigator === "object" - ? (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.userLanguage || navigator.language || navigator.browserLanguage || 'en' - : "en" + return this.languages[0]; } }, + _languages: { + value: undefined + }, + + languages: { + get: function() { + if(!this._languages) { + this._languages = typeof navigator === "object" + ? (navigator.languages && navigator.languages.length) + ? navigator.languages + : [navigator.userLanguage || navigator.language || navigator.browserLanguage || 'en'] + : ["en"]; + } + return this._languages; + }, + set: function(value) { + this._languages = value; + } + }, + + userAgentIPAddress: { + value: undefined + }, + /** * The name of the stage the code is running. * diff --git a/data/service/data-service.js b/data/service/data-service.js index 0b56e50e42..9b95146376 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -21,6 +21,7 @@ var Montage = require("core/core").Montage, PropertyDescriptor = require("core/meta/property-descriptor").PropertyDescriptor, DeleteRule = require("core/meta/property-descriptor").DeleteRule, deprecate = require("../../core/deprecate"), + currentEnvironment = require("core/environment").currentEnvironment, Locale = require("core/locale").Locale; require("core/extras/string"); @@ -170,6 +171,10 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } }, + currentEnvironment: { + value: currentEnvironment + }, + delegate: { value: null }, From dbf52dfdf47d96e4687972f5e3b934f3e399f2a5 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 18 Dec 2020 11:02:15 -0800 Subject: [PATCH 349/407] Fix a regression in node --- core/environment.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/environment.js b/core/environment.js index 02f6c16a64..97d645bfe8 100644 --- a/core/environment.js +++ b/core/environment.js @@ -67,11 +67,12 @@ var Environment = exports.Environment = Montage.specialize({ get: function() { if(this._stage === undefined) { //Check if we have an argument: - var stageArgument = this.application.url && this.application.url.searchParams.get("stage"); + var applicationURL = this.application.url, + stageArgument = applicationURL && applicationURL.searchParams.get("stage"); if(stageArgument) { this._stage = stageArgument; - } else if(global.location.hostname === "127.0.0.1" || global.location.hostname === "localhost" || global.location.hostname.endsWith(".local") ) { + } else if(applicationURL && (applicationURL.hostname === "127.0.0.1" || applicationURL.hostname === "localhost" || applicationURL.hostname.endsWith(".local")) ) { this._stage = "dev"; } else { /* From 21722c28553315f253873f3d34eb046eef8edad8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 18 Dec 2020 11:09:18 -0800 Subject: [PATCH 350/407] Fix potential exception --- core/environment.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/environment.js b/core/environment.js index 97d645bfe8..4609716201 100644 --- a/core/environment.js +++ b/core/environment.js @@ -99,7 +99,10 @@ var Environment = exports.Environment = Montage.specialize({ userAgent: { set: function (userAgent) { - userAgent = userAgent.toLowerCase(); + + if(userAgent) { + userAgent = userAgent.toLowerCase(); + } if (userAgent !== this._userAgent) { this._userAgent = userAgent; From eb73b1f0bfc086d03d2a2336557a19ad65f7887a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 18 Dec 2020 16:25:09 -0800 Subject: [PATCH 351/407] - fix exception in tests in raw-foreign-value-to-object-converter.js - performance tuning in deserialization to speedup start time --- core/mr/require.js | 7 +++++++ .../deserializer/montage-deserializer.js | 16 ++++++++++++---- .../deserializer/montage-interpreter.js | 11 ++++------- .../deserializer/montage-reviver.js | 13 ++++++------- .../raw-foreign-value-to-object-converter.js | 3 ++- montage.js | 3 ++- node.js | 11 +++++------ 7 files changed, 38 insertions(+), 26 deletions(-) diff --git a/core/mr/require.js b/core/mr/require.js index 0e11c19630..9c2dc08288 100644 --- a/core/mr/require.js +++ b/core/mr/require.js @@ -134,6 +134,13 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { Module.prototype.dependees = null; Module.prototype.extraDependencies = void 0; Module.prototype.uuid = null; + Module.prototype._json = undefined; + + Object.defineProperty(Module.prototype,"json", { + get: function() { + return this._json || (this._json = JSON.parse(this.text)) + } + }); var normalizePattern = /^(.*)\.js$/, normalizeIdCache = new Map(); diff --git a/core/serialization/deserializer/montage-deserializer.js b/core/serialization/deserializer/montage-deserializer.js index 16819a78bf..7ef161a26a 100644 --- a/core/serialization/deserializer/montage-deserializer.js +++ b/core/serialization/deserializer/montage-deserializer.js @@ -25,11 +25,15 @@ var MontageDeserializer = exports.MontageDeserializer = Montage.specialize({ }, init: { - value: function (serialization, _require, objectRequires, module, isSync) { + value: function (serialization, _require, objectRequires, module, isSync, useParsedSerialization) { if (typeof serialization === "string") { this._serializationString = serialization; } else { - this._serializationString = JSON.stringify(serialization); + if(useParsedSerialization) { + this._serialization = serialization; + } else { + this._serializationString = JSON.stringify(serialization); + } } this._require = _require; this._module = module; @@ -62,7 +66,7 @@ var MontageDeserializer = exports.MontageDeserializer = Montage.specialize({ */ deserialize: { value: function (instances, element) { - if(!this._serializationString || this._serializationString === "") { + if((!this._serializationString || this._serializationString === "") && !this._serialization) { return null; } @@ -86,7 +90,11 @@ var MontageDeserializer = exports.MontageDeserializer = Montage.specialize({ } try { - var serialization = JSON.parse(this._serializationString); + var serialization = this._serialization || JSON.parse(this._serializationString); + //We need a new JSON.parse every time, so if we had one, we use it, but we trash it after. + if(this._serialization) { + this._serialization = null; + } context = new MontageContext() .init(serialization, this._reviver, instances, element, this._require, this._isSync); if (this._locationId) { diff --git a/core/serialization/deserializer/montage-interpreter.js b/core/serialization/deserializer/montage-interpreter.js index 72818ca9fa..121937ab75 100644 --- a/core/serialization/deserializer/montage-interpreter.js +++ b/core/serialization/deserializer/montage-interpreter.js @@ -145,15 +145,12 @@ var MontageContext = Montage.specialize({ getObject: { value: function(label) { - var serialization = this._serialization, - reviver = this._reviver, - objects = this._objects, - object, notFoundError; + var objects = this._objects; if (label in objects) { return objects[label]; - } else if (label in serialization) { - object = reviver.reviveRootObject(serialization[label], this, label); + } else if (label in this._serialization) { + var object = this._reviver.reviveRootObject(this._serialization[label], this, label); // If no object has been set by the reviver we safe its // return, it could be a value or a promise, we need to // make sure the object won't be revived twice. @@ -163,7 +160,7 @@ var MontageContext = Montage.specialize({ return object; } else { - notFoundError = new Error("Object with label '" + label + "' was not found."); + var notFoundError = new Error("Object with label '" + label + "' was not found."); if (this._isSync) { throw notFoundError; } else { diff --git a/core/serialization/deserializer/montage-reviver.js b/core/serialization/deserializer/montage-reviver.js index dd95609169..b78364f090 100644 --- a/core/serialization/deserializer/montage-reviver.js +++ b/core/serialization/deserializer/montage-reviver.js @@ -151,7 +151,7 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont getTypeOf: { value: function (value) { - var typeOf = typeof value; + var typeOf; if (value === null) { return "null"; @@ -163,7 +163,7 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont else if(Date.parseRFC3339(value)) { return "date"; //} else if (typeOf === "object" && Object.keys(value.__proto__).length === 1) { - } else if (typeOf === "object" && Object.keys(value).length === 1) { + } else if ((typeOf = typeof value) === "object" && Object.keys(value).length === 1) { if ("@" in value) { return "reference"; } else if ("/" in value) { @@ -903,7 +903,7 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont }, reviveObjectLiteral: { - value: function(value, context, label, filterKeys) { + value: function reviveObjectLiteral(value, context, label, filterKeys) { var item, promises, propertyNames = context.propertyToReviveForObjectLiteralValue(value), @@ -977,10 +977,7 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont reviveObjectReference: { value: function(value, context, label) { - var valuePath = value["@"], - object = context.getObject(valuePath); - - return object; + return context.getObject(value["@"]); } }, @@ -1164,6 +1161,8 @@ _reviveMethodByType["array"] = "reviveArray"; _reviveMethodByType["object"] = "reviveObjectLiteral"; _reviveMethodByType["Element"] = "reviveElement"; _reviveMethodByType["binding"] = "reviveBinding"; +_reviveMethodByType["Module"] = "reviveModule"; + MontageReviverProto.reviveValue._methodByType = _reviveMethodByType; diff --git a/data/converter/raw-foreign-value-to-object-converter.js b/data/converter/raw-foreign-value-to-object-converter.js index da77717eec..eaf4476de1 100644 --- a/data/converter/raw-foreign-value-to-object-converter.js +++ b/data/converter/raw-foreign-value-to-object-converter.js @@ -134,7 +134,8 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( _areCriteriaSyntaxPropertiesRawDataPrimaryKeys: { value: function(typeToFetch, criteria, service) { if(this.__areCriteriaSyntaxPropertiesRawDataPrimaryKeys === undefined) { - this.__areCriteriaSyntaxPropertiesRawDataPrimaryKeys = service.mappingForType(typeToFetch).rawDataPrimaryKeys.equals(syntaxProperties(criteria.syntax)) + var mapping = service.mappingForType(typeToFetch); + this.__areCriteriaSyntaxPropertiesRawDataPrimaryKeys = mapping && mapping.rawDataPrimaryKeys && mapping.rawDataPrimaryKeys.equals(syntaxProperties(criteria.syntax)) } return this.__areCriteriaSyntaxPropertiesRawDataPrimaryKeys } diff --git a/montage.js b/montage.js index 5fa5cbe065..cdcda95569 100644 --- a/montage.js +++ b/montage.js @@ -469,6 +469,7 @@ root; module.deserializer = deserializer; deserializer.init(module.text, deserializerRequire, void 0, module, true); + // deserializer.init(module.json, deserializerRequire, void 0, module, true, true); try { root = deserializer.deserializeObject(); @@ -581,7 +582,7 @@ if (isMJSON) { if (typeof module.exports !== "object" && typeof module.text === "string") { try { - module.parsedText = JSON.parse(module.text); + module.parsedText = module.json; } catch (e) { if (e instanceof SyntaxError) { console.error("SyntaxError parsing JSON at "+location); diff --git a/node.js b/node.js index b5fcf00d9a..1c8ab4b75f 100644 --- a/node.js +++ b/node.js @@ -158,10 +158,9 @@ function parsePrototypeForModule(prototype) { return prototype.replace(/\[[^\]]+\]$/, ""); } -function collectSerializationDependencies(text, dependencies) { - var serialization = JSON.parse(text); - Object.keys(serialization).forEach(function (label) { - var description = serialization[label]; +function collectSerializationDependencies(serializationJSON, dependencies) { + Object.keys(serializationJSON).forEach(function (label) { + var description = serializationJSON[label]; if (description.lazy) { return; } @@ -180,7 +179,7 @@ function collectHtmlDependencies(dom, dependencies) { if (DomUtils.isTag(element)) { if (element.name === "script") { if (getAttribute(element, "type") === "text/montage-serialization") { - collectSerializationDependencies(getText(element), dependencies); + collectSerializationDependencies(JSON.parse(getText(element)), dependencies); } } else if (element.name === "link") { if (getAttribute(element, "type") === "text/montage-serialization") { @@ -231,7 +230,7 @@ MontageBoot.TemplateLoader = function (config, load) { } else if (serialization) { return load(id, module) .then(function () { - module.dependencies = collectSerializationDependencies(module.text, []); + module.dependencies = collectSerializationDependencies(module.json, []); return module; }); } else if (meta) { From 58f5c1cb64d4cc31ac0ac5769b3f9d740d932910 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 18 Dec 2020 18:22:40 -0800 Subject: [PATCH 352/407] add bluebird injection to prevent double load of bluebird on node.js --- core/mr/require.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/mr/require.js b/core/mr/require.js index 9c2dc08288..fef0bd1ae6 100644 --- a/core/mr/require.js +++ b/core/mr/require.js @@ -933,6 +933,10 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { return require; } + if ((typeof process !== "undefined") && config.name === "montage") { + inject("bluebird", Promise); + } + config.requireForId = requireForId = memoize(makeRequire,config.requireById); require = makeRequire(""); From 83ee373b167a64070e166c4794aa11a177e53eea Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 20 Dec 2020 14:59:21 -0800 Subject: [PATCH 353/407] Fix regression caused by typo/missing end of a property access --- data/service/expression-data-mapping.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 2ab99831cf..104dd5fde9 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -1329,7 +1329,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData rule = _rule ? _rule : this.rawDataMappingRules.get(propertyName), result, propertyDescriptor = rule && rule.propertyDescriptor, - isRelationship = propertyDescriptor && propertyDescriptor.value, + isRelationship = propertyDescriptor && propertyDescriptor.valueDescriptor, value; From 19b9d2cd91ddfd4fd6cde3b11506e340107157bd Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 20 Dec 2020 15:37:14 -0800 Subject: [PATCH 354/407] avoid to add undefined entries to rawDataSnapshot --- data/service/expression-data-mapping.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 104dd5fde9..2d86ea695f 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -1082,7 +1082,9 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData if(lastReadSnapshot[rawDataPropertyName] !== rawDataPropertValue) { rawData[rawDataPropertyName] = rawDataPropertValue; - rawDataSnapshot[rawDataPropertyName] = lastReadSnapshot[rawDataPropertyName]; + if(lastReadSnapshot[rawDataPropertyName] !== undefined) { + rawDataSnapshot[rawDataPropertyName] = lastReadSnapshot[rawDataPropertyName]; + } } } else { /* From 62c58f0060f467d872eb208b644c781b1fec2ff4 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 20 Dec 2020 15:55:03 -0800 Subject: [PATCH 355/407] fix missing returned promise --- data/service/expression-data-mapping.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 2d86ea695f..91cb9f0a03 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -1440,6 +1440,9 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } else { return; } + } else { + //There was only one rule, so we have one result; + return result; } } return; From 2954b4ee2abd3fea266e63b1e38175d8ade5830d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 23 Dec 2020 09:47:44 -0800 Subject: [PATCH 356/407] fix bug that had the wrong mainService being used --- core/meta/object-descriptor.js | 2 +- data/service/data-service.js | 7 +++++-- data/service/user-identity-manager.js | 12 ++++++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/core/meta/object-descriptor.js b/core/meta/object-descriptor.js index f74eadd1f7..b03f97c2e9 100644 --- a/core/meta/object-descriptor.js +++ b/core/meta/object-descriptor.js @@ -457,7 +457,7 @@ var ObjectDescriptor = exports.ObjectDescriptor = Target.specialize( /** @lends nextTarget: { serializable: false, get: function() { - return this._nextTarget || (this._nextTarget = (this.parent || ObjectDescriptor.mainService.childServiceForType(this))); + return this._nextTarget || (this._nextTarget = (this.parent || this.eventManager.application.mainService.childServiceForType(this) || this.eventManager.application.mainService)); } }, diff --git a/data/service/data-service.js b/data/service/data-service.js index 9b95146376..ced2354f67 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -69,9 +69,12 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { constructor: { value: function DataService() { - exports.DataService.mainService = ObjectDescriptor.mainService = exports.DataService.mainService || this; + + this.defineBinding("mainService", {"<-": "mainService", source: defaultEventManager.application}); + + exports.DataService.mainService = exports.DataService.mainService || this; if(this === DataService.mainService) { - UserIdentityManager.mainService = DataService.mainService; + // UserIdentityManager.mainService = DataService.mainService; //this.addOwnPropertyChangeListener("userLocales", this); this.addRangeAtPathChangeListener("userLocales", this, "handleUserLocalesRangeChange"); } diff --git a/data/service/user-identity-manager.js b/data/service/user-identity-manager.js index ddab973f06..ac1e73f4b7 100644 --- a/data/service/user-identity-manager.js +++ b/data/service/user-identity-manager.js @@ -1,5 +1,6 @@ var Montage = require("core/core").Montage, DataOperation = require("data/service/data-operation").DataOperation, + defaultEventManager = require("core/event/event-manager").defaultEventManager, UserIdentityManager; @@ -10,6 +11,11 @@ var Montage = require("core/core").Montage, * @deprecated The Authorization API was moved to DataService itself. */ UserIdentityManager = Montage.specialize( /** @lends AuthorizationService.prototype */ { + constructor: { + value: function DataService() { + this.defineBinding("mainService", {"<-": "mainService", source: defaultEventManager.application}); + } + }, registerUserIdentityService: { value: function(aService) { @@ -87,8 +93,10 @@ UserIdentityManager = Montage.specialize( /** @lends AuthorizationService.protot //We have a circular depency such that when mainService setter is called, DataOperation isn't yet on the exports symbol... // this._mainService.addEventListener(DataOperation.Type.UserAuthentication, this); // this._mainService.addEventListener(DataOperation.Type.UserAuthenticationCompleted, this); - this._mainService.addEventListener("userauthentication", this); - this._mainService.addEventListener("userauthenticationcompleted", this); + if(this._mainService) { + this._mainService.addEventListener("userauthentication", this); + this._mainService.addEventListener("userauthenticationcompleted", this); + } } }, From ee19f70c1d3487a0e91dedd9e971a5311863c6f2 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 23 Dec 2020 09:48:07 -0800 Subject: [PATCH 357/407] add note --- data/service/data-operation.js | 1 + 1 file changed, 1 insertion(+) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index d5d2813d74..e439db4121 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -229,6 +229,7 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy serializer.setProperty("targetModuleId", this.target.module.id); } else { //This is not working as I thought it would yet + //We use DataService.mainService as the target for transaction related operations. That should really be the model. //serializer.addObjectReference(this.target); serializer.setProperty("targetModuleId", null); } From d2be2964ab88bd69cfa4d84ec525a32e95484d2e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 29 Dec 2020 23:37:51 -0800 Subject: [PATCH 358/407] - fix a bug where an overriden property without a manually corresponding overriden mapping rule (which makes no sense to do) would point to the wrong valueDescriptor when the subclass changes it. - add hasRawDataRequiredValues method to assess if a rawData has the required key/values to evaluate the recipient rule --- data/service/expression-data-mapping.js | 57 +++++++++++++++++++++---- data/service/mapping-rule.js | 22 +++++++++- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 91cb9f0a03..bdc0ea7c05 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -1638,7 +1638,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData * to assign the values. */ mapObjectToRawDataProperty: { - value: function (object, data, propertyName, lastReadSnapshot, rawDataSnapshot) { + value: function (object, data, propertyName, lastReadSnapshot, rawDataSnapshot, ignoreRequiredObjectProperties) { var rule = this.rawDataMappingRules.get(propertyName), //Adding a test for rawDataPrimaryKeys. Types that are meant to be //embedded in others don't have a rawDataPrimaryKeys @@ -1646,7 +1646,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData requiredObjectProperties = (rule && this.rawDataPrimaryKeys) ? rule.requirements : null, result, self; - if(requiredObjectProperties) { + if(!ignoreRequiredObjectProperties && requiredObjectProperties) { result = this.service.rootService.getObjectPropertyExpressions(object, requiredObjectProperties); } @@ -1691,6 +1691,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData rule = this.objectMappingRules.get(propertyName), requiredRawProperties = rule ? rule.requirements : [], rawRequirementsToMap = new Set(requiredRawProperties), + ignoreRequiredObjectProperties = true, promises, key, result; while ((key = keys.next().value)) { @@ -1700,7 +1701,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData This is better to do anywaym, and it avoids to get into mapObjectToRawDataProperty trying to fecth the object property that we're precisely trying to fetch, as this method is only called by dataService._fetchObjectPropertyWithPropertyDescriptor(), which seems to lock the stack as it's logically re-entrant. So this blocks it here before it happens there */ if (rawRequirementsToMap.has(key) && !data.hasOwnProperty(key)) { - result = this.mapObjectToRawDataProperty(object, data, key); + result = this.mapObjectToRawDataProperty(object, data, key, undefined, undefined, ignoreRequiredObjectProperties); if (this._isAsync(result)) { promises = promises || []; promises.push(result); @@ -1942,10 +1943,48 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } }, + _overrideParentRuleForPropertyDescriptor: { + value: function(parentRule, ownPropertyDescriptor) { + var myRule = new MappingRule(); + myRule.sourcePath = parentRule.sourcePath; + myRule.targetPath = parentRule.targetPath; + if(parentRule.serviceIdentifier) { + myRule.serviceIdentifier = parentRule.serviceIdentifier; + } + if(parentRule.converter) { + myRule.converter = parentRule.converter; + } + if(parentRule.reverter) { + myRule.reverter = parentRule.reverter; + } + + myRule.propertyDescriptor = ownPropertyDescriptor; + + return myRule; + } + }, + _assignAllEntriesTo: { - value: function (source, target) { - var service = this.service; + value: function (source, target, isObjectMappingRule, sourceIsParent) { + var service = this.service, + myObjectDescriptor = this.objectDescriptor, + self = this; source.forEach(function (value, key) { + + if(sourceIsParent) { + var myPropertyDescriptor = myObjectDescriptor.propertyDescriptorForName( + isObjectMappingRule + ? value.targetPath + : value.sourcePath + ); + + //Check if the property was overriden: + if(myPropertyDescriptor && myPropertyDescriptor !== value.propertyDescriptor) { + //If it is, we need to clone the rule and assign it our propertyDescriptor + value = self._overrideParentRuleForPropertyDescriptor(value,myPropertyDescriptor); + } + } + target.set(key, value); /* value is a MappingRule */ @@ -1995,9 +2034,9 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData if (!this._objectMappingRules) { this._objectMappingRules = new Map(); if (this.parent) { - this._assignAllEntriesTo(this.parent.objectMappingRules, this._objectMappingRules); + this._assignAllEntriesTo(this.parent.objectMappingRules, this._objectMappingRules, /*isObjectMappingRule*/ true,/*sourceIsParent*/true); } - this._assignAllEntriesTo(this._ownObjectMappingRules, this._objectMappingRules); + this._assignAllEntriesTo(this._ownObjectMappingRules, this._objectMappingRules, /*isObjectMappingRule*/ true); } return this._objectMappingRules; } @@ -2022,9 +2061,9 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData if (!this._rawDataMappingRules) { this._rawDataMappingRules = new Map(); if (this.parent) { - this._assignAllEntriesTo(this.parent.rawDataMappingRules, this._rawDataMappingRules); + this._assignAllEntriesTo(this.parent.rawDataMappingRules, this._rawDataMappingRules, /*isObjectMappingRule*/ false,/*sourceIsParent*/true); } - this._assignAllEntriesTo(this._ownRawDataMappingRules, this._rawDataMappingRules); + this._assignAllEntriesTo(this._ownRawDataMappingRules, this._rawDataMappingRules, /*isObjectMappingRule*/ false); } return this._rawDataMappingRules; } diff --git a/data/service/mapping-rule.js b/data/service/mapping-rule.js index ef6bca0f58..470e7f6499 100644 --- a/data/service/mapping-rule.js +++ b/data/service/mapping-rule.js @@ -76,6 +76,24 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { } }, + hasRawDataRequiredValues: { + value: function(rawData) { + var requirements = this.requirements, + i, countI, iRequirenent; + + for(i=0, countI = requirements.length; (i Date: Tue, 29 Dec 2020 23:39:36 -0800 Subject: [PATCH 359/407] add qualifiedProperties that returns all properties used in the criteria's expression/syntax. All, not just first level off an object evaluated it on. --- core/criteria.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/criteria.js b/core/criteria.js index 6bc68c83f2..2eaecd4787 100644 --- a/core/criteria.js +++ b/core/criteria.js @@ -6,6 +6,7 @@ var parse = require("core/frb/parse"), evaluate = require("core/frb/evaluate"), operatorTypes = require("core/frb/language").operatorTypes, Scope = require("core/frb/scope"), + syntaxProperties = require("core/frb/syntax-properties"), compile = require("core/frb/compile-evaluator"); var Criteria = exports.Criteria = Montage.specialize({ @@ -71,6 +72,10 @@ var Criteria = exports.Criteria = Montage.specialize({ //_compiledSyntax this._compiledSyntax = null; + //Reset qualifiedProperties cache + this._qualifiedProperties = null; + + this._syntax = value; } @@ -255,6 +260,20 @@ var Criteria = exports.Criteria = Montage.specialize({ } }, + _qualifiedProperties: { + value: undefined + }, + /** + * returns all properties used in criteria's expression/syntax + * + * @property {string} + */ + qualifiedProperties: { + get: function() { + return this._qualifiedProperties || (this._qualifiedProperties = syntaxProperties(this.syntax)); + } + }, + /** * Walks a criteria's syntactic tree to assess if one of more an expression * involving propertyName. From e66e834d08bc216af6ca5690815a22b3d1e72b60 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 31 Dec 2020 09:43:50 -0800 Subject: [PATCH 360/407] update node version for running tests --- .vscode/launch.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 16d4a107d1..7b87cdf580 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,8 +14,10 @@ "type": "node", "request": "launch", "name": "npm test", - "runtimeExecutable": "~/.nvm/versions/node/v12.16.1/bin/node", - "program": "${workspaceFolder}/test/run-node.js" + "runtimeVersion": "12.18.4", + "runtimeArgs": [ + "${workspaceFolder}/test/run-node.js", + ] }, { "type": "node", @@ -48,7 +50,7 @@ "type": "node", "request": "launch", "name": "npm test mr", - "runtimeExecutable": "~/.nvm/versions/node/v12.16.1/bin/node", + "runtimeExecutable": "~/.nvm/versions/node/v12.18.4/bin/node", "program": "${workspaceFolder}/core/mr/test/run-node.js" }, { @@ -63,7 +65,7 @@ "type": "node", "request": "launch", "name": "Run promise-io Jasmine Tests", - "runtimeExecutable": "~/.nvm/versions/node/v12.16.1/bin/node", + "runtimeExecutable": "~/.nvm/versions/node/v12.18.4/bin/node", "autoAttachChildProcesses": true, "program": "${workspaceFolder}/node_modules/jasmine-node/bin/jasmine-node", "args": [ "${workspaceFolder}/core/promise-io/spec", "--verbose"], From 7f0b8bd280d03939e02d96afc52535dfab771250 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 31 Dec 2020 10:34:59 -0800 Subject: [PATCH 361/407] add check to catch that an "addedValue" would be set on an array, intially treated as a set of the proeprty, instead of an object holding aded/removedValue which would be the case if the proeprty was initially populated. This is a stop-gap measure, more exploration/changes needed to avoid being in that situation in the first place --- data/service/data-service.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/data/service/data-service.js b/data/service/data-service.js index ced2354f67..632225e102 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -2708,6 +2708,13 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } else { var registeredAddedValues = manyChanges.addedValues; if(!registeredAddedValues) { + /* + FIXME: we ended up here with manyChanges being an array, containing the same value as addedValues. And we end up setting addedValues property on that array. So let's correct it. We might not want to track toMany as set at all, and just stick to added /remove. This might happens on remove as well, we need to check further. + */ + if(Array.isArray(manyChanges) && manyChanges.equals(addedValues)) { + manyChanges = {}; + changesForDataObject.set(key, manyChanges); + } manyChanges.addedValues = (registeredAddedValues = new Set(addedValues)); self._addDataObjectPropertyDescriptorValuesForInversePropertyDescriptor(dataObject, propertyDescriptor, addedValues, inversePropertyDescriptor); From c5cd1453409bcbea465d7bb0ed5491b3876f9679 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 31 Dec 2020 10:42:43 -0800 Subject: [PATCH 362/407] change to stop using the presence of a snapshot to avoid infinite loop when mapping and use the exisitng this._objectsBeingMapped data structure instead. This make sure that values in the snapshot are associated as easrly as we know to the object, and also, we can map existing objects now when we dynamically fetch more properties, so comparing snapshots wasn't as accurate a test anymore. --- data/service/raw-data-service.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 6c08bc8475..7b793bbca0 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -555,6 +555,9 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot isUpdateToExistingObject = true; } + //Record the snapshot before we map. + this.recordSnapshot(object.dataIdentifier, rawData); + result = this._mapRawDataToObject(rawData, object, context, readExpressions); @@ -634,10 +637,13 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot //Retrieves an existing object is responsible data service is uniquing, or creates one object = this.getDataObject(type, rawData, context, dataIdentifier); + //Record snapshot before mapping + this.recordSnapshot(dataIdentifier, rawData); + result = this._mapRawDataToObject(rawData, object, context); - //Record snapshot when done mapping - this.recordSnapshot(dataIdentifier, rawData); + // //Record snapshot when done mapping + // this.recordSnapshot(dataIdentifier, rawData); if (Promise.is(result)) { return result.then(function () { @@ -1286,8 +1292,12 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot So to break the cycle, if there's a known snapshot and the object is being mapped, then we don't return a promise, knowing there's already one pending for the first pass. */ snapshot = this.snapshotForObject(object); - if(Object.equals(snapshot,record) ) { - return undefined; + //Changed order of snapshot being set before mapping so thqt doesn't work + //if(Object.equals(snapshot,record) ) { + + //Replacing with: + if(this._objectsBeingMapped.has(object)) { + return undefined; if(this._objectsBeingMapped.has(object)) { console.log(object.dataIdentifier.objectDescriptor.name +" _mapRawDataToObject id:"+record.id+" FOUND EXISTING MAPPING PROMISE"); @@ -1307,7 +1317,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot //Recording snapshot even if we already had an object //Record snapshot before we may create an object - this.recordSnapshot(object.dataIdentifier, record); + //this.recordSnapshot(object.dataIdentifier, record); // console.log(object.dataIdentifier.objectDescriptor.name +" _mapRawDataToObject id:"+record.id+" FIRST NEW MAPPING PROMISE"); From 6ebb39fcb351684f518b2b5861b0486625cfe619 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 31 Dec 2020 10:48:55 -0800 Subject: [PATCH 363/407] so far, while dataTrigger calls fetchObjectProperty, exisiting implementation relied on un-mapped raw values in the snapshot to map them, which ends up using expression converters for foreign keys, and as part of the mapping raw to object, would end up puting the value on the right property of the object the trigger intiated the logic for. We added an alternative way when the snapshot doesn't have the value, inline of relation, and it returns the value. So the code was made able to handled properly a newly returned value and put it in the right place. --- data/service/data-trigger.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js index 2e9389eaec..e74ad9e910 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -668,7 +668,15 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy value: function (object) { var self = this; //console.log("data-trigger: _fetchObjectProperty "+this._propertyName,object ); - this._service.fetchObjectProperty(object, this._propertyName).then(function () { + this._service.fetchObjectProperty(object, this._propertyName).then(function (propertyValue) { + console.log(propertyValue); + if(propertyValue && !object[self._privatePropertyName]) { + if(self.propertyDescriptor.cardinality > 1) { + object[self._propertyName] = propertyValue; + } else { + object[self._propertyName] = propertyValue[0]; + } + } return self._fulfillObjectPropertyFetch(object); }).catch(function (error) { console.error("DataTrigger Error _fetchObjectProperty for property \""+self._propertyName+"\"",error); From 6b3dc6a5e3a6ba9368446078a661de61fe556688 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 31 Dec 2020 10:49:34 -0800 Subject: [PATCH 364/407] fix bugs in mapping added/removed values --- data/service/expression-data-mapping.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index bdc0ea7c05..688ca4bc2d 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -1255,20 +1255,20 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData _mapObjectPropertyToRawDataProperty: { value: function(object, propertyName, data, rawPropertyName, added, removed, _rule, lastReadSnapshot, rawDataSnapshot) { - if(added || removed) { + if((added && added.size > 0) || (removed && removed.size > 0 )) { //We derived object so we can pretend the value of the property is alternatively added, then removed, to get the mapping done. - var tmpExtendObject = Object.createobject(), + var tmpExtendObject = Object.create(object), diffData = Object.create(null), mappedKeys, i, countI, iKey, aPropertyChanges = {}, - addedResult, addedResultIsPromise + addedResult, addedResultIsPromise, removedResult, removedResultIsPromise, result; data[rawPropertyName] = aPropertyChanges; - if(added) { + if(added && added.size > 0) { //added is a set, regular properties are array, not ideal but we need to convert to be able to map. tmpExtendObject[propertyName] = Array.from(added); addedResult = this.__mapObjectToRawDataProperty(tmpExtendObject, diffData, rawPropertyName, _rule, lastReadSnapshot, rawDataSnapshot); @@ -1283,7 +1283,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } } - if(removed) { + if(removed && removed.size > 0 ) { tmpExtendObject[propertyName] = Array.from(result); removedResult = this.__mapObjectToRawDataProperty(tmpExtendObject, diffData, rawPropertyName, _rule, lastReadSnapshot, rawDataSnapshot); @@ -1291,10 +1291,10 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData if (this._isAsync(removedResult)) { removedResultIsPromise = true; removedResult = removedResult.then(() => { - this._assignMappedDiffDataToPropertyChangesObjectKey(diffData, aPropertyChanges,"addedValues"); + this._assignMappedDiffDataToPropertyChangesObjectKey(diffData, aPropertyChanges,"removedValues"); }); } else { - this._assignMappedDiffDataToPropertyChangesObjectKey(diffData, aPropertyChanges,"addedValues"); + this._assignMappedDiffDataToPropertyChangesObjectKey(diffData, aPropertyChanges,"removedValues"); } } @@ -1319,7 +1319,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData var mappedKeys = Object.keys(diffData), i, countI; - for(i=0, count = mappedKeys.length; (i Date: Sat, 2 Jan 2021 18:50:30 -0800 Subject: [PATCH 365/407] - add performTransactionProgress - add serialization of clientId, though not sure we really need it --- data/service/data-operation.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index e439db4121..d9c1e26627 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -122,6 +122,7 @@ var Montage = require("core/core").Montage, "createSavePoint", "performTransaction", + "performTransactionProgress", "performTransactionCompleted", "performTransactionFailed", @@ -238,6 +239,9 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy if(this.referrerId) { serializer.setProperty("referrerId", this.referrerId); } + if(this.clientId) { + serializer.setProperty("clientId", this.clientId); + } serializer.setProperty("criteria", this._criteria); /* Hack: this is neededed for now to represent a query's fetchLimit @@ -299,6 +303,10 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy if (value !== void 0) { this.referrerId = value; } + value = deserializer.getProperty("clientId"); + if (value !== void 0) { + this.clientId = value; + } value = deserializer.getProperty("criteria"); if (value !== void 0) { @@ -770,6 +778,7 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy CreateSavePoint: DataOperationType.createSavePoint, PerformTransaction: DataOperationType.performTransaction, + PerformTransactionProgress: DataOperationType.performTransactionProgress, PerformTransactionCompleted: DataOperationType.performTransactionCompleted, PerformTransactionFailed: DataOperationType.performTransactionFailed, From a16a7b187336575eb80b6f8015e3a07582488145 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 3 Jan 2021 18:34:24 -0800 Subject: [PATCH 366/407] - remove serialization of clientId - add keepAlive data oepration type --- data/service/data-operation.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index d9c1e26627..33f8cd2959 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -164,7 +164,8 @@ var Montage = require("core/core").Montage, "validate", "validateFailed", "validateCompleted", - "validateCancelled" + "validateCancelled", + "keepAlive" ]; @@ -239,9 +240,6 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy if(this.referrerId) { serializer.setProperty("referrerId", this.referrerId); } - if(this.clientId) { - serializer.setProperty("clientId", this.clientId); - } serializer.setProperty("criteria", this._criteria); /* Hack: this is neededed for now to represent a query's fetchLimit @@ -303,10 +301,6 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy if (value !== void 0) { this.referrerId = value; } - value = deserializer.getProperty("clientId"); - if (value !== void 0) { - this.clientId = value; - } value = deserializer.getProperty("criteria"); if (value !== void 0) { @@ -785,6 +779,9 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy RollbackTransaction: DataOperationType.rollbackTransaction, RollbackTransactionCompleted: DataOperationType.rollbackTransactionCompleted, RollbackTransactionFailed: DataOperationType.rollbackTransactionFailed, + KeepAlive: DataOperationType.keepAlive, + + } } From d2b3da4ac66919ef56b5a933d8cadc21ff9550d3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 4 Jan 2021 10:27:12 -0800 Subject: [PATCH 367/407] add saveChangesProgress type --- data/model/data-event.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/model/data-event.js b/data/model/data-event.js index 1176349711..30d28533be 100644 --- a/data/model/data-event.js +++ b/data/model/data-event.js @@ -86,6 +86,10 @@ exports.DataEvent = MutableEvent.specialize({ /* when a change was made by someone else and it's making it's way to another user. */ update: { value: "update" + }, + + saveChangesProgress: { + value: "saveChangesProgress" } }); From 1de1634d2b6736642ed560a4bbc8184d62a68ec8 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 5 Jan 2021 22:06:10 -0800 Subject: [PATCH 368/407] remove debug log --- data/service/data-trigger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js index e74ad9e910..71c79e25ef 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -669,7 +669,7 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy var self = this; //console.log("data-trigger: _fetchObjectProperty "+this._propertyName,object ); this._service.fetchObjectProperty(object, this._propertyName).then(function (propertyValue) { - console.log(propertyValue); + // console.log(propertyValue); if(propertyValue && !object[self._privatePropertyName]) { if(self.propertyDescriptor.cardinality > 1) { object[self._propertyName] = propertyValue; From 4e37f344cf77ec9ff659980481d3348d4bd10f9a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 6 Jan 2021 11:31:26 -0800 Subject: [PATCH 369/407] add stringByDeletingLastPathComponent method --- core/extras/string.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/extras/string.js b/core/extras/string.js index 708e7c6bb7..67c9e7da6e 100644 --- a/core/extras/string.js +++ b/core/extras/string.js @@ -164,3 +164,17 @@ if(typeof String.prototype.removeSuffix !== "function") { } +if(typeof String.prototype.stringByDeletingLastPathComponent !== "function") { + Object.defineProperty(String.prototype, 'stringByDeletingLastPathComponent', { + value: function stringByRemovingPathExtension () { + var lastIndex = this.lastIndexOf("/"); + if(lastIndex !== -1 ) { + return this.substring(0,lastIndex); + } else { + return this; + } + }, + writable: true, + configurable: true + }); +} From a9ab15ac45b05986e7aa46d6232577de95c020c9 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 7 Jan 2021 09:39:25 -0800 Subject: [PATCH 370/407] - add hasSnapshot... methods to assess if there's one instead of doing a lookup - fix a bug where the addedValues/removedValues used in dataOperations would end up being added to the snapshot which created issues saving after that as that object's snapshot shape wouldn't match that of a fetch anymore --- data/service/raw-data-service.js | 98 +++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 8 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 7b793bbca0..f006b3bcec 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -551,7 +551,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot //If we're already have a snapshot, we've already fetched and //instanciated an object for that identifier previously. - if(this.snapshotForDataIdentifier(dataIdentifier)) { + if(this.hasSnapshotForDataIdentifier(dataIdentifier)) { isUpdateToExistingObject = true; } @@ -846,7 +846,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot * @argument {Object} rawData */ recordSnapshot: { - value: function (dataIdentifier, rawData) { + value: function (dataIdentifier, rawData, isFromUpdate) { if(!dataIdentifier) { return; } @@ -857,10 +857,68 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } else { var rawDataKeys = Object.keys(rawData), - i, countI; + i, countI, iUpdatedRawDataValue, iCurrentRawDataValue, iDiffValues, iRemovedValues, + j, countJ, jDiffValue, jDiffValueIndex; + + for(i=0, countI = rawDataKeys.length; (i Date: Thu, 7 Jan 2021 09:39:52 -0800 Subject: [PATCH 371/407] use new hasSnapshotForObject --- data/service/expression-data-mapping.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 688ca4bc2d..f5446f6618 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -803,7 +803,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData ruleIterator = dataMatchingRules.values(), requisitePropertyNames = this.requisitePropertyNames, isNotRequiredRule, - hasSnapshot = !!this.service.snapshotForObject(object), + hasSnapshot = this.service.hasSnapshotForObject(object), aRule, aRuleRequirements, i, countI, dataHasRuleRequirements; From e2f5de7ede2f38c26437e52a64213100ef918ad2 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 7 Jan 2021 09:52:48 -0800 Subject: [PATCH 372/407] add comments and notes on changes to make to fix an issue that happens tracking changes when a property's value isn't set from snapshot, but set synchronously progr ammatically, which means we end up best case scenario with a change that isn't one. --- data/service/data-service.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/data/service/data-service.js b/data/service/data-service.js index 632225e102..8e8a5adce6 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -2642,6 +2642,9 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { inversePropertyDescriptor, self = this; + + + if(!isCreatedObject) { this.changedDataObjects.add(dataObject); } @@ -2652,6 +2655,21 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { } + /* + + TODO / WARNING / FIX: If an object's property that has not been fetched, mapped and assigned is accessed, it will be undefined and will trigger a fetch to get it. If the business logic then assumes it's not there and set a value synchronously, when the fetch comes back, we will have a value and the set will look like an update. + + This situation is poorly handled and should be made more robust, here and in DataTrigger. + + Should we look into the snapshot to help? Then map what's there first, and then compare before acting? + + var dataObjectSnapshot = this._getChildServiceForObject(dataObject)._snapshot.get(dataObject.dataIdentifier); + + Just because it's async, doesn't mean we couldn't get it right, since we can act after the sync code action and reconciliate the 2 sides. + + */ + + /* From 8b1bb31147873749f367dcc820015aaa9fd7fe9a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 7 Jan 2021 09:54:16 -0800 Subject: [PATCH 373/407] fix code format --- ui/data-editor.js | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/ui/data-editor.js b/ui/data-editor.js index 9bfd22f32f..083d570975 100644 --- a/ui/data-editor.js +++ b/ui/data-editor.js @@ -161,30 +161,29 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { value: function() { if(this._dataQuery) { - var dataService = this.dataService, - currentDataStream = this.dataStream, - dataStream, - self = this; - //console.log(this.constructor.name+" fetchData() >>>>> "); - dataStream = dataService.fetchData(this._dataQuery); - dataStream.then(function(data) { - //console.log("Data fetched:",data); - self.dataStream = dataStream; - - //We need to - dataService.cancelDataStream(currentDataStream); - - self.didFetchData(data); - - }, - function(error) { - console.log("fetchData failed:",error); - + var dataService = this.dataService, + currentDataStream = this.dataStream, + dataStream, + self = this; + //console.log(this.constructor.name+" fetchData() >>>>> "); + dataStream = dataService.fetchData(this._dataQuery); + dataStream.then(function(data) { + //console.log("Data fetched:",data); + self.dataStream = dataStream; + + //We need to + dataService.cancelDataStream(currentDataStream); + + self.didFetchData(data); + + }, + function(error) { + console.log("fetchData failed:",error); }) .finally(() => { - this.canDrawGate.setField("dataLoaded", true); - }); - } + this.canDrawGate.setField("dataLoaded", true); + }); + } } }, From 6493934dd5bbd4b1b3add79d09f343ecfa0b42ed Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 8 Jan 2021 14:27:10 -0800 Subject: [PATCH 374/407] Fix an exception when value to convert is null --- core/converter/string-to-URL-converter.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/converter/string-to-URL-converter.js b/core/converter/string-to-URL-converter.js index dde9139495..c202a93488 100644 --- a/core/converter/string-to-URL-converter.js +++ b/core/converter/string-to-URL-converter.js @@ -20,7 +20,11 @@ exports.StringToURLConverter = Converter.specialize( /** @lends StringToURLConve */ convert: { value: function (v) { - return new URL(v); + if(v == null) { + return null; + } else { + return new URL(v); + } } }, From 1320cb3097a4f8a3451618ae479a31da620dad33 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 8 Jan 2021 14:44:16 -0800 Subject: [PATCH 375/407] - change mapping logic to map all properties in the raw data returned that don't need an extra fetch, unless they are required or eventually part of a read expresssion (I think) - added method canMapObjectDescriptorRawDataToObjectPropertyWithoutFetch to RawDataService to have a level of indirection for custom needs - fix a bug where overriden properties wouldn't be mapped correctly, (there were no fetch done to resolve a property where there should have been one) --- data/service/data-service.js | 4 ++-- data/service/expression-data-mapping.js | 18 +++++++++++++++--- data/service/raw-data-service.js | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/data/service/data-service.js b/data/service/data-service.js index 8e8a5adce6..accadfc998 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -1745,8 +1745,8 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { _fetchObjectPropertyWithPropertyDescriptor: { value: function (object, propertyName, propertyDescriptor) { var self = this, - objectDescriptor = propertyDescriptor.owner, - mapping = objectDescriptor && this.mappingForType(objectDescriptor), + objectDescriptor = this.objectDescriptorForObject(object), + mapping = objectDescriptor && this.mappingForType(objectDescriptor), data = {}, result; diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index f5446f6618..143dc78741 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -806,6 +806,8 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData hasSnapshot = this.service.hasSnapshotForObject(object), aRule, aRuleRequirements, i, countI, + service = this.service, + objectDescriptor = this.objectDescriptor, dataHasRuleRequirements; while ((aRule = ruleIterator.next().value)) { @@ -836,11 +838,21 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData */ /* - If the rule isn't required, or if we don't have what we need to fullfill, we bail out. + if we don't have what we need to fullfill, we bail out. + + Previously if the rule isn't required, we would bail out, but if it's been sent ny the server, me might as well make it useful than stay unused in the snapshot, as long as we can. */ - if(!isRequiredRule || !dataHasRuleRequirements) { + + // if(service.canMapObjectDescriptorRawDataToObjectPropertyWithoutFetch(objectDescriptor, aRule.targetPath) && dataHasRuleRequirements) { + // console.log("Now mapping property "+aRule.targetPath+" of "+objectDescriptor.name); + // } + + if((!isRequiredRule && !service.canMapObjectDescriptorRawDataToObjectPropertyWithoutFetch(objectDescriptor, aRule.targetPath)) || !dataHasRuleRequirements) { continue; } + // if(!isRequiredRule || !dataHasRuleRequirements) { + // continue; + // } result = this.mapRawDataToObjectProperty(data, object, aRule.targetPath, context); if (this._isAsync(result)) { @@ -1074,7 +1086,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData and that can't be directly compared to the structure of lastReadSnapshot.... - So we check for that. That said, it might be cleaner to pass on the relevant property descriptor which is available in the methods colling this as there might be richer semantics there. + So we check for that. That said, it might be cleaner to pass on the relevant property descriptor which is available in the methods calling this as there might be richer semantics there. */ if(lastReadSnapshot && rawDataSnapshot && diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index f006b3bcec..5db7396644 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -15,6 +15,7 @@ var DataService = require("data/service/data-service").DataService, DataOrdering = require("data/model/data-ordering").DataOrdering, DESCENDING = DataOrdering.DESCENDING, evaluate = require("core/frb/evaluate"), + RawForeignValueToObjectConverter = require("data/converter/raw-foreign-value-to-object-converter").RawForeignValueToObjectConverter, Promise = require("../../core/promise").Promise; require("core/collections/shim-object"); @@ -1796,6 +1797,23 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } }, + canMapObjectDescriptorRawDataToObjectPropertyWithoutFetch: { + + value: function(objectDescriptor, propertyName) { + var mapping = this.mappingForType(objectDescriptor), + objectRule = mapping.objectMappingRules.get(propertyName), + objectRuleConverter = objectRule && objectRule.converter, + valueDescriptor = objectRule && objectRule.propertyDescriptor._valueDescriptorReference; + + return ( + objectRule && ( + !valueDescriptor || + (valueDescriptor && objectRuleConverter && !(objectRuleConverter instanceof RawForeignValueToObjectConverter)) + ) + ); + } + }, + /*************************************************************************** * Deprecated */ From 26cedf71d89f255a5575567f1cde7712f6787e88 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Jan 2021 15:25:37 -0800 Subject: [PATCH 376/407] fix a bug in how dtataoperation target is deserialized --- data/service/data-operation.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 33f8cd2959..0c4f5ba916 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -276,23 +276,33 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy this.timeStamp = value; } /* keeping dataDescriptor here for temporary backward compatibility */ - value = deserializer.getProperty("target") || deserializer.getProperty("targetModuleId") || deserializer.getProperty("dataDescriptor"); + value = deserializer.getProperty("target"); + if(value === undefined) { + value = deserializer.getProperty("targetModuleId"); + } + if(value === undefined) { + value = deserializer.getProperty("dataDescriptor"); + } if (value !== void 0) { if(Array.isArray(value)) { this.target = value.map((objectDescriptorModuleIid) => {return this.mainService.objectDescriptorWithModuleId(objectDescriptorModuleIid)}); } else if(typeof value === "string") { if(this.mainService) { - if(value === null) { - this.target = this.mainService - } else { - this.target = this.mainService.objectDescriptorWithModuleId(value); - } + this.target = this.mainService.objectDescriptorWithModuleId(value); } else { //Last resort, if we can't construct the target, let's carry the data that was supposed to help us do so this.targetModuleId = value; } - } else { + } else if(value === null) { + if(this.mainService) { + this.target = this.mainService; + } else { + //Last resort, if we can't construct the target, let's carry the data that was supposed to help us do so + this.targetModuleId = value; + } + } + else { this.target = value; } } From 4b991127ad4dafd0b35f5f923d727d20496c3089 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Jan 2021 15:26:53 -0800 Subject: [PATCH 377/407] Fix a bug from transition from Object internal structure to Map and add a method to assess if a field is defined --- core/gate.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/core/gate.js b/core/gate.js index a170c90590..ab346e3ad7 100644 --- a/core/gate.js +++ b/core/gate.js @@ -82,7 +82,20 @@ var Gate = exports.Gate = Montage.specialize(/** @lends Gate.prototype # */ { enumerable: false, value: function (aFieldName) { var table = this.table; - return !table || table[aFieldName]; + return !table || table.get(aFieldName); + } + }, + + /** + * @function + * @param {Array} aFieldName The aFieldName array. + * @returns boolean + */ + hasField: { + enumerable: false, + value: function (aFieldName) { + var table = this.table; + return !table || table.has(aFieldName); } }, From d8fec63961ca8b8a89529d5aa39f00de53d56d6d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 12 Jan 2021 15:28:42 -0800 Subject: [PATCH 378/407] tweak to engage the use of dataLoaded field in the canDrawGate only if data setter is actually called (which can happen directly or thorugh bindings) --- ui/data-editor.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/data-editor.js b/ui/data-editor.js index 083d570975..e2fa5675e2 100644 --- a/ui/data-editor.js +++ b/ui/data-editor.js @@ -48,7 +48,7 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { constructor: { value: function DataEditor () { this.super(); - this.canDrawGate.setField("dataLoaded", false); + // this.canDrawGate.setField("dataLoaded", false); // console.log("---------- "+this.constructor.name+" inDocument:"+this.inDocument+" —— dataLoaded: false",value); return this; @@ -324,6 +324,9 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { return this._data; }, set: function (value) { + if(!this.hasOwnProperty("_data") && value === undefined) { + this.canDrawGate.setField("dataLoaded", true); + } // console.log(this.constructor.name+ " set data:",value, " inDocument:"+this.inDocument+", parentComponent:",this.parentComponent); if(this._data === undefined && value !== undefined) { // console.log("++++++++++ "+this.constructor.name+" inDocument:"+this.inDocument+" —— dataLoaded: true",value); From 6f34bc81bff24c7fbb9a09a3c0854bf548d46500 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 14 Jan 2021 17:10:08 -0800 Subject: [PATCH 379/407] Fix syntax error --- ui/scroller.reel/scroller.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/scroller.reel/scroller.html b/ui/scroller.reel/scroller.html index 7e99ac8ce1..922cacfb09 100755 --- a/ui/scroller.reel/scroller.html +++ b/ui/scroller.reel/scroller.html @@ -32,7 +32,7 @@ "minTranslateX": 0, "minTranslateY": 0, "invertAxis": true, - "listenToWheelEvent": true + "listenToWheelEvent": true, "translateX": {"<->": "@owner.scrollX"}, "translateY": {"<->": "@owner.scrollY"}, "maxTranslateX": {"<-": "@owner._maxTranslateX"}, From 171cc98de877621505c1d3f3258709a4b2dc61c3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 26 Jan 2021 08:10:18 -0800 Subject: [PATCH 380/407] WIP start renaming "path" methods to "expression" and depprecate as we progress --- core/core.js | 54 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/core/core.js b/core/core.js index 88382f0cb3..614979cdf2 100644 --- a/core/core.js +++ b/core/core.js @@ -17,9 +17,10 @@ require("./extras/weak-map"); require("proxy-polyfill/proxy.min"); -var Map = require("./collections/map"); -var WeakMap = require("./collections/weak-map"); -var Set = require("./collections/set"); +var Map = require("./collections/map"), + WeakMap = require("./collections/weak-map"), + Set = require("./collections/set"), + deprecate = require("./deprecate"); var ATTRIBUTE_PROPERTIES = "AttributeProperties", UNDERSCORE = "_", @@ -1351,19 +1352,19 @@ Object.defineProperties(PathChangeDescriptor.prototype, { var pathChangeDescriptors = new WeakMap(); -var pathPropertyDescriptors = { +var expressionPropertyDescriptors = { /** * Evaluates an FRB expression from this object and returns the value. * The evaluator does not establish any change listeners. - * @function Montage#getPath + * @function Montage#valueForExpression * @param {string} path an FRB expression * @returns the current value of the expression */ - getPath: { - value: function (path, parameters, document, components) { + valueForExpression: { + value: function (expression, parameters, document, components) { return evaluate( - path, + expression, this, parameters || this, document, @@ -1371,20 +1372,26 @@ var pathPropertyDescriptors = { ); } }, + getPath: { + value: deprecate.deprecateMethod(void 0, function (path, parameters, document, components) { + this.valueForExpression(path, parameters, document, components); + }, "getPath", "valueForExpression", true) + }, + /** * Assigns a value to the FRB expression from this object. Not all * expressions can be assigned to. Property chains will work, but will * silently fail if the target object does not exist. - * @function Montage#setPath + * @function Montage#setValueForExpression * @param {string} path an FRB expression designating the value to replace * @param value the new value */ - setPath: { - value: function (path, value, parameters, document, components) { + setValueForExpression: { + value: function (value, expression, parameters, document, components) { return assign( this, - path, + expression, value, parameters || this, document, @@ -1392,11 +1399,17 @@ var pathPropertyDescriptors = { ); } }, + setPath: { + value: deprecate.deprecateMethod(void 0, function (path, value, parameters, document, components) { + console.warn("setValueForExpression arguments 'value' and 'path' are now in reverse order of setPath()"); + this.setValueForExpression(value, path, parameters, document, components); + }, "setPath", "setValueForExpression", true) + }, /** * Observes changes to the value of an FRB expression. The content of the * emitted value may react to changes, particularly if it is an array. - * @function Montage#observePath + * @function Montage#observeExpression * @param {string} path an FRB expression * @param {function} emit a function that receives new values in response * to changes. The emitter may return a `cancel` function if it manages @@ -1405,13 +1418,18 @@ var pathPropertyDescriptors = { * change listeners, prevent new values from being observed, and prevent * previously emitted values from reacting to any further changes. */ - observePath: { - value: function (path, emit) { - var syntax = parse(path); + observeExpression: { + value: function (expression, emit) { + var syntax = parse(expression); var observe = compileObserver(syntax); return observe(autoCancelPrevious(emit), new Scope(this)); } }, + observePath: { + value: deprecate.deprecateMethod(void 0, function (path, emit) { + this.observeExpression(path, emit); + }, "observePath", "observeExpression", true) + }, /** * Observes changes to the content of the value for an FRB expression. @@ -1676,8 +1694,8 @@ var pathPropertyDescriptors = { }; -Montage.defineProperties(Montage, pathPropertyDescriptors); -Montage.defineProperties(Montage.prototype, pathPropertyDescriptors); +Montage.defineProperties(Montage, expressionPropertyDescriptors); +Montage.defineProperties(Montage.prototype, expressionPropertyDescriptors); /* * Defines the module Id for object descriptors. This is externalized so that it can be subclassed. From b3fe0e5fae431fd16a4a38087f7ba00fdae945d3 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 26 Jan 2021 08:17:48 -0800 Subject: [PATCH 381/407] add ability to mutate a criteria's expression and streamlines reset so it can be shared shared between setting expression and syntax. --- core/criteria.js | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/core/criteria.js b/core/criteria.js index 2eaecd4787..553546fd85 100644 --- a/core/criteria.js +++ b/core/criteria.js @@ -27,6 +27,12 @@ var Criteria = exports.Criteria = Montage.specialize({ ? (this._expression = stringify(this._syntax)) : this._expression ); + }, + set: function(value) { + if(value !== this._expression) { + this._reset(); + this._expression = value; + } } }, /** @@ -46,6 +52,18 @@ var Criteria = exports.Criteria = Montage.specialize({ } }, + _reset: { + value: function() { + this._expression = null; + this._compiledSyntax = null; + + //Reset qualifiedProperties cache + this._qualifiedProperties = null; + + this._syntax = null; + } + }, + /** * @private * @type {object} @@ -66,16 +84,7 @@ var Criteria = exports.Criteria = Montage.specialize({ set: function(value) { if(value !== this._syntax) { //We need to reset: - //expression if we have one: - this._expression = null; - - //_compiledSyntax - this._compiledSyntax = null; - - //Reset qualifiedProperties cache - this._qualifiedProperties = null; - - + this._reset(); this._syntax = value; } @@ -439,12 +448,17 @@ var Criteria = exports.Criteria = Montage.specialize({ // } } - else if(syntaxArg1.type === "parameters") { + else if(syntaxArg1 && syntaxArg1.type === "parameters") { this.__syntaxByAliasingSyntaxWithParameters(aliasedSyntax, syntaxArg1, 1, syntaxArg0, 0, aliasedParameters, parameterCounter, _thisParameters); } else { - aliasedSyntax.args[0] = this._syntaxByAliasingSyntaxWithParameters(syntaxArg0, aliasedParameters, parameterCounter, _thisParameters); - aliasedSyntax.args[1] = this._syntaxByAliasingSyntaxWithParameters(syntaxArg1, aliasedParameters, parameterCounter, _thisParameters); + if(syntaxArg0) { + aliasedSyntax.args[0] = this._syntaxByAliasingSyntaxWithParameters(syntaxArg0, aliasedParameters, parameterCounter, _thisParameters); + } + + if(syntaxArg1) { + aliasedSyntax.args[1] = this._syntaxByAliasingSyntaxWithParameters(syntaxArg1, aliasedParameters, parameterCounter, _thisParameters); + } } } else { aliasedSyntax[iKey] = syntax[iKey]; From a5217933f621e21dd7275680b2035dde4b216915 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 26 Jan 2021 08:19:03 -0800 Subject: [PATCH 382/407] remove one variable declaration not needed --- core/frb/parse.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/core/frb/parse.js b/core/frb/parse.js index 86f50d67a2..7be990557d 100644 --- a/core/frb/parse.js +++ b/core/frb/parse.js @@ -7,7 +7,7 @@ var memo = new Map(); // could be Dict module.exports = parse; function parse(text, options) { - var cache; + var syntax; if (Array.isArray(text)) { return { type: "tuple", @@ -15,11 +15,11 @@ function parse(text, options) { return parse(text, options); }) }; - } else if (!options && (cache = memo.get(text))) { - return cache; + } else if (!options && (syntax = memo.get(text))) { + return syntax; } else { try { - var syntax = grammar.parse(text, options || Object.empty); + syntax = grammar.parse(text, options || Object.empty); if (!options) { memo.set(text,syntax); } @@ -33,4 +33,3 @@ function parse(text, options) { } } } - From 7df157d636803cd8fe6212afe01144553dc9c892 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 26 Jan 2021 08:21:22 -0800 Subject: [PATCH 383/407] add the definitionSyntax on propertyDescriptor to cache and minimize parsing the definition expression --- core/meta/property-descriptor.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index ea6d04e87a..5d58de4fb2 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -2,6 +2,7 @@ var Montage = require("../core").Montage, Promise = require("../promise").Promise, deprecate = require("../deprecate"), Enum = require("core/enum").Enum, + parse = require("core/frb/parse"), logger = require("../logger").logger("objectDescriptor"); /* TypeDescriptor */ @@ -473,6 +474,17 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# value: null }, + _definitionSyntax: { + value: null + }, + + definitionSyntax: { + get: function() { + return this._definitionSyntax || (this._definitionSyntax = parse(this.definition)); + } + }, + + /** * @type {string} * TODO: This is semantically similar to keyDescriptor From 303c51b7f930f4fc8091415b0c0dcf2f76deef63 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 26 Jan 2021 23:35:46 -0800 Subject: [PATCH 384/407] check and add ability to add raw primary key/values to fetched rawData if it's not included by server and they are known client side, before processing as usual --- data/service/raw-data-service.js | 56 ++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 5db7396644..384f497ceb 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -16,7 +16,9 @@ var DataService = require("data/service/data-service").DataService, DESCENDING = DataOrdering.DESCENDING, evaluate = require("core/frb/evaluate"), RawForeignValueToObjectConverter = require("data/converter/raw-foreign-value-to-object-converter").RawForeignValueToObjectConverter, - Promise = require("../../core/promise").Promise; + DataOperation = require("./data-operation").DataOperation, + Promise = require("../../core/promise").Promise, + SyntaxInOrderIterator = require("core/frb/syntax-iterator").SyntaxInOrderIterator; require("core/collections/shim-object"); @@ -511,6 +513,51 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } }, + /** + * When we fetch to complete an object, we know client side for which object it is, + * so the backend may not have to send it back and save data. + * So here we check if rawData has primaryKey entries. If it doesn't, we try to find it + * in the query criteria. We can't rely on just looking up the parameters as we're + * sometine alias the criteria parameters when we combine them. Only the property value + * in the expression is reliable. + */ + + _addRawDataPrimaryKeyValuesIfNeeded: { + value: function(rawData, type, query) { + var mapping = this.mappingForObjectDescriptor(type), + rawDataPrimaryKeys = mapping.rawDataPrimaryKeys, + i, countI, iKey, + iterator, parentSyntax, currentSyntax, propertyName, propertyValue, firstArgSyntax, propertyName, secondArgSyntax, + criteriaParameters, + criteriaSyntax, + syntaxPropertyByName; + + for(i=0, countI = rawDataPrimaryKeys.length; (i Date: Tue, 26 Jan 2021 23:38:55 -0800 Subject: [PATCH 385/407] wip: first operation related utility method --- data/service/raw-data-service.js | 40 +++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 384f497ceb..0c868b6359 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -17,6 +17,7 @@ var DataService = require("data/service/data-service").DataService, evaluate = require("core/frb/evaluate"), RawForeignValueToObjectConverter = require("data/converter/raw-foreign-value-to-object-converter").RawForeignValueToObjectConverter, DataOperation = require("./data-operation").DataOperation, + DataOperationType = require("./data-operation").DataOperationType, Promise = require("../../core/promise").Promise, SyntaxInOrderIterator = require("core/frb/syntax-iterator").SyntaxInOrderIterator; @@ -936,7 +937,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot snapshot[rawDataKeys[i]] = iDiffValues; } } else { - console.error("recordSnapshot from Update: No entry in snapshot for '"+awDataKeys[i]+"' but addedValues:",iDiffValues); + console.warn("recordSnapshot from Update: No entry in snapshot for '"+rawDataKeys[i]+"' but addedValues:",iDiffValues); /* We could reconstruct from the object value, but we should relly not be here. */ @@ -1865,7 +1866,44 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot } }, + + responseOperationForReadOperation: { + value: function(readOperation, err, data, isNotLast) { + var operation = new DataOperation(); + + operation.referrerId = readOperation.id; + operation.target = readOperation.target; + + //Carry on the details needed by the coordinator to dispatch back to client + // operation.connection = readOperation.connection; + operation.clientId = readOperation.clientId; + //console.log("executed Statement err:",err, "data:",data); + + if (err) { + // an error occurred + //console.log("!!! handleRead FAILED:", err, err.stack, rawDataOperation.sql); + operation.type = DataOperation.Type.ReadFailed; + //Should the data be the error? + operation.data = err; + } + else { + // successful response + + //If we need to take care of readExpressions, we can't send a ReadCompleted until we have returnes everything that we asked for. + if(isNotLast) { + operation.type = DataOperation.Type.ReadUpdate; } else { + operation.type = DataOperation.Type.ReadCompleted; + } + + //We provide the inserted record as the operation's payload + operation.data = data; + } + return operation; + } + }, + + /*************************************************************************** * Deprecated */ From b376514b866714cacd0a5bf70ec7bcd84cf87834 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 27 Jan 2021 00:05:13 -0800 Subject: [PATCH 386/407] - add method to assess if DataService handles a type/ObjectDescriptor - move failsafe against fetching object properties of created objects from DataTrigger to DataService, allowing more finesse for fetching properties of newly created objects that can based on their current values set, still preventing cases where a typical primary key would be involved in the fetch - makes isObjectCreated more robust to just using this.createdDataObjects as some promises now involved in fetching object, properties because it's not done at the DataTrigger level, can fire after a saveChanges() may have wiped this.createdDataObjects - improve/fix bug dealing with mapping added/removed values - add method to create a criteria that supports compound primaryKeys - add ability for foreign value converter to work for properties, including if fetch depend on the values of other properties of the same object. Add readExpression to query in that case so RawDataService involved has a cue --- .../raw-foreign-value-to-object-converter.js | 27 +++++- data/service/data-service.js | 67 ++++++++++--- data/service/data-trigger.js | 38 +++++--- data/service/expression-data-mapping.js | 97 +++++++++++++++++-- 4 files changed, 196 insertions(+), 33 deletions(-) diff --git a/data/converter/raw-foreign-value-to-object-converter.js b/data/converter/raw-foreign-value-to-object-converter.js index eaf4476de1..4202297b7c 100644 --- a/data/converter/raw-foreign-value-to-object-converter.js +++ b/data/converter/raw-foreign-value-to-object-converter.js @@ -183,7 +183,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( } }, _fetchConvertedDataForObjectDescriptorCriteria: { - value: function(typeToFetch, criteria) { + value: function(typeToFetch, criteria, currentRule) { var self = this; return this.service ? this.service.then(function (service) { @@ -227,6 +227,13 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( if(!fetchPromise) { var query = DataQuery.withTypeAndCriteria(typeToFetch, criteria); + /* + Sounds twisted, but this is to deal with the case where we need to fetch to resolve a priperty of the object itself. + */ + if(currentRule && !currentRule.propertyDescriptor._valueDescriptorReference) { + query.readExpressions = [currentRule.targetPath]; + } + /* When we fetch objects that have inverse relationships on each others none can complete their mapRawDataProcess because the first one's promise for mapping the relationship to the second never commpletes because the second one itself has it's raw data the foreignKey to the first and attemps to do so by default on processing operations, where the previous way was only looping on requisite proprties. If both relationships were requisite, on each side we'd end up with the same problem. @@ -350,6 +357,13 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( } }, + /* + To open the ability to get derived values from non-saved objects, some failsafes blocking a non-saved created object to get any kind of property resolved/fetched were removed. So we need to be smarter here and do the same. + + If an object is created (which we don't know here, but we can check), fetching a property relies on the primary key and that the primarty key is one property only (like a uuid) and there's already a value (client-side generated like uuid can be), than it can't be fetched and we shoould resolve to undefined. + + */ + convertCriteriaForValue: { value: function(value) { var criteria = new Criteria().initWithSyntax(this.convertSyntax, value); @@ -401,8 +415,10 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( if((v && !(v instanceof Array )) || (v instanceof Array && v.length > 0)) { var self = this, - criteria, - query; + //We put it in a local variable so we have the right value in the closure + currentRule = this.currentRule, + criteria, + query; if(this.foreignDescriptorMappings) { /* @@ -469,7 +485,7 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( return this._descriptorToFetch.then(function (typeToFetch) { - return self._fetchConvertedDataForObjectDescriptorCriteria(typeToFetch, criteria); + return self._fetchConvertedDataForObjectDescriptorCriteria(typeToFetch, criteria, currentRule); // if (self.serviceIdentifier) { // criteria.parameters.serviceIdentifier = self.serviceIdentifier; @@ -480,6 +496,9 @@ exports.RawForeignValueToObjectConverter = RawValueToObjectConverter.specialize( // return self.service ? self.service.then(function (service) { // return service.rootService.fetchData(query); // }) : null; + }, function(error) { + console.log(error); + return error; }); } } diff --git a/data/service/data-service.js b/data/service/data-service.js index accadfc998..e50fe9e61f 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -22,6 +22,7 @@ var Montage = require("core/core").Montage, DeleteRule = require("core/meta/property-descriptor").DeleteRule, deprecate = require("../../core/deprecate"), currentEnvironment = require("core/environment").currentEnvironment, + PropertyChanges = require("../../core/collections/listen/property-changes"), Locale = require("core/locale").Locale; require("core/extras/string"); @@ -816,6 +817,12 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { value: undefined }, + handlesType: { + value: function(type) { + return (this.rootService._childServicesByType.get(type).indexOf(this) !== -1); + } + }, + _childServicesByIdentifier: { get: function () { if (!this.__childServicesByIdentifier) { @@ -1493,13 +1500,19 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { - If we create an object, properties that are not relations can't be fetched. We need to make sure we don't actually try. + + COUNTER: If an object created already has the info to make it's primary key, we should go forward. + + The real test could be that if it's mapped as a relationship, then we might be able to get something. + - If a property is a relationship and it wasn't set on the object, as an object, we can't get it either. */ - if(this.isObjectCreated(object)) { - //Not much we can do there anyway, punt - return Promise.resolve(true); - } else if (this.isRootService) { + // if(this.isObjectCreated(object)) { + // //Not much we can do there anyway, punt + // return Promise.resolve(true); + // } else + if (this.isRootService) { // Get the data, accepting property names as an array or as a list // of string arguments while avoiding the creation of any new array. //var names = Array.isArray(propertyNames) ? propertyNames : Array.prototype.slice.call(arguments, 1), @@ -1641,21 +1654,26 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { * this method. * * @method - * @argument {object} object - The object whose property value is being - * requested. - * @argument {string} name - The name of the single property whose value - * is being requested. + * @argument {object} object - The object whose property value is being + * requested. + * + * @argument {string} name - The name of the single property whose value + * is being requested. + * + * @argument {array} readExpressions - A list of object[propertyName] properties to get + * at the same time we're getting object[propertyName]. * @returns {external:Promise} - A promise fulfilled when the requested * value has been received and set on the specified property of the passed * in object. */ fetchObjectProperty: { - value: function (object, propertyName) { + value: function (object, propertyName, isObjectCreated, readExpressions) { var isHandler = this.parentService && this.parentService._getChildServiceForObject(object) === this, useDelegate = isHandler && typeof this.fetchRawObjectProperty === "function", delegateFunction = !useDelegate && isHandler && this._delegateFunctionForPropertyName(propertyName), propertyDescriptor = !useDelegate && !delegateFunction && isHandler && this._propertyDescriptorForObjectAndName(object, propertyName), childService = !isHandler && this._getChildServiceForObject(object), + isObjectCreated = this.isObjectCreated(object), debug = exports.DataService.debugProperties.has(propertyName); @@ -1670,7 +1688,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { : delegateFunction ? delegateFunction.call(this, object) : isHandler && propertyDescriptor - ? this._fetchObjectPropertyWithPropertyDescriptor(object, propertyName, propertyDescriptor) + ? this._fetchObjectPropertyWithPropertyDescriptor(object, propertyName, propertyDescriptor, isObjectCreated) : childService ? childService.fetchObjectProperty(object, propertyName) : this.nullPromise; @@ -1743,7 +1761,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { _fetchObjectPropertyWithPropertyDescriptor: { - value: function (object, propertyName, propertyDescriptor) { + value: function (object, propertyName, propertyDescriptor, isObjectCreated) { var self = this, objectDescriptor = this.objectDescriptorForObject(object), mapping = objectDescriptor && this.mappingForType(objectDescriptor), @@ -1752,6 +1770,30 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { if (mapping) { + + /* + To open the ability to get derived values from non-saved objects, some failsafes blocking a non-saved created object to get any kind of property resolved/fetched were removed. So we need to be smarter here and do the same. + + If an object is created (which we don't know here, but we can check), fetching a property relies on the primary key and that the primarty key is one property only (like a uuid) and there's already a value (client-side generated like uuid can be), than it can't be fetched and we shoould resolve to null. + + Another approch would be to map all dependencies and let the rule's converter assess if it has everytrhing it needs to do the job, but at that level, the converter doesn't know the object, but it has the primaryKey in the criteria's syntax and the value in the associated parameters, so it could find out if there's a corresponding object that is created. It might be needed, let's see if this first heuristic works first. + */ + if(isObjectCreated) { + var rule = mapping.objectMappingRules.get(propertyName), + rawDataPrimaryKeys = mapping.rawDataPrimaryKeys, + requiredRawProperties = rule && rule.requirements; + + /* + rawDataPrimaryKeys.length === 1 is to assess if it's a traditional id with no intrinsic meaning + */ + if(rawDataPrimaryKeys.length === 1 && requiredRawProperties.indexOf(rawDataPrimaryKeys[0] !== -1)){ + /* + Fetching depends on something that doesn't exists on the other side, we bail: + */ + return this.nullPromise; + } + } + /* @marchant: Why aren't we passing this.snapshotForObject(object) instead of copying everying in a new empty object? @@ -1776,6 +1818,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { result = mapping.mapRawDataToObjectProperty(data, object, propertyName); if (!self._isAsync(result)) { + result = this.nullPromise; self._objectsBeingMapped.delete(object); } else { @@ -2225,7 +2268,7 @@ exports.DataService = Target.specialize(/** @lends DataService.prototype */ { if(!isObjectCreated) { var service = this._getChildServiceForObject(object); if(service) { - isObjectCreated = service.isObjectCreated(object); + isObjectCreated = (service.isObjectCreated(object) || !service.hasSnapshotForObject(object)); } else { isObjectCreated = false; } diff --git a/data/service/data-trigger.js b/data/service/data-trigger.js index 71c79e25ef..45ea4e4150 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -614,21 +614,25 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy */ getObjectProperty: { value: function (object) { - //If the object is not created and not saved, we fetch the value - if(!this._service.isObjectCreated(object)) { + /* + If the object is not created and not saved, we fetch the value + + In some cases, a new object created in-memory, with enough data set on it might be able to read some propery that might be derived from the raw values of the property already set on it. So let's leave the bottom layers to figure that out. + */ + // if(!this._service.isObjectCreated(object)) { var status = this._getValueStatus(object); return status ? status.promise : status === null ? this._service.nullPromise : this.updateObjectProperty(object); - } else { - /* - else the object is just created, not saved, no point fetching - we wouldn't find anything anyway - */ - - this._setValueStatus(object, null); - return this._service.nullPromise; - } + // } else { + // /* + // else the object is just created, not saved, no point fetching + // we wouldn't find anything anyway + // */ + + // this._setValueStatus(object, null); + // return this._service.nullPromise; + // } } }, @@ -669,6 +673,9 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy var self = this; //console.log("data-trigger: _fetchObjectProperty "+this._propertyName,object ); this._service.fetchObjectProperty(object, this._propertyName).then(function (propertyValue) { + /* + If there's a propertyValue, it's the actual result of the fetch and bipassed the existing path where the mapping would have added the value on object by the time we get back here. So since it wasn't done, we do it here. + */ // console.log(propertyValue); if(propertyValue && !object[self._privatePropertyName]) { if(self.propertyDescriptor.cardinality > 1) { @@ -837,6 +844,15 @@ Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ (DataTrig trigger.propertyDescriptor = descriptor; trigger._isGlobal = descriptor.isGlobal; if (descriptor.definition) { + /* + As we create the binding for a definition like "propertyA.propertyB", we will endup doing fetchObjectProperty for propertyA, and later calling propertyB on the result of fetching propertyA. This is highly inneficient and we need to find a way to fetch propertyA with a readExpression of propertyB. + + By the time we reach fetchObjectProperty, that info is lost, so we need to find a way to carry it in. + + Every property change observer/listener is an opportunity to collect what "comes next", but it's only available on defineBinding, as after it's dynamically added once instances are in memory which is too late. + */ + + var propertyDescriptor = { get: function (shouldFetch) { if (!this.getBinding(descriptor.name)) { diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 143dc78741..6769004079 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -2,6 +2,7 @@ var DataMapping = require("./data-mapping").DataMapping, assign = require("core/frb/assign"), compile = require("core/frb/compile-evaluator"), DataService = require("data/service/data-service").DataService, + Criteria = require("core/criteria").Criteria, ObjectDescriptorReference = require("core/meta/object-descriptor-reference").ObjectDescriptorReference, parse = require("core/frb/parse"), Map = require("core/collections/map"), @@ -1090,7 +1091,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData */ if(lastReadSnapshot && rawDataSnapshot && - !(typeof rawDataPropertValue === "object" && (rawDataPropertValue.hasOwnProperty("addedValues") || rawDataPropertValue.hasOwnProperty("addedValues")))) { + !(typeof rawDataPropertValue === "object" && (rawDataPropertValue.hasOwnProperty("addedValues") || rawDataPropertValue.hasOwnProperty("removedValues")))) { if(lastReadSnapshot[rawDataPropertyName] !== rawDataPropertValue) { rawData[rawDataPropertyName] = rawDataPropertValue; @@ -1268,21 +1269,36 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData value: function(object, propertyName, data, rawPropertyName, added, removed, _rule, lastReadSnapshot, rawDataSnapshot) { if((added && added.size > 0) || (removed && removed.size > 0 )) { - //We derived object so we can pretend the value of the property is alternatively added, then removed, to get the mapping done. - var tmpExtendObject = Object.create(object), + var tmpExtendObject, + //We derived object so we can pretend the value of the property is alternatively added, then removed, to get the mapping done. + //tmpExtendObject = Object.create(object), diffData = Object.create(null), mappedKeys, i, countI, iKey, aPropertyChanges = {}, addedResult, addedResultIsPromise, removedResult, removedResultIsPromise, + requirements, result; data[rawPropertyName] = aPropertyChanges; if(added && added.size > 0) { - //added is a set, regular properties are array, not ideal but we need to convert to be able to map. - tmpExtendObject[propertyName] = Array.from(added); + /* + Here we have a situation where in the most common case object[propertyName] is not equal to the content of added, like if there were pre-exising values. + + Since we want to only send the diff if possible (we might need to add a flag on RawDataService to know if it can handle diffs or if needs the whole thing.). If there were a notion of order in the propertyDescriptor, that might also be a reason to not send a diff. + + First we tried to use an extension of the object, but that still modifies it. So we're going to build just a payload object with the values for _rule.requirements. + */ + requirements = _rule.requirements; + tmpExtendObject = {}; + for(i=0, countI = requirements.length; ( i 0 ) { - tmpExtendObject[propertyName] = Array.from(result); + + requirements = (requirements || _rule.requirements); + // tmpExtendObject[propertyName] = Array.from(result); + tmpExtendObject = (tmpExtendObject || {}); + + for(i=0, countI = requirements.length; ( i 0) { + expression += " && "; + } + expression += `${iKey} == $${iKey}`; + } + + this.__rawDataPrimaryKeyCriteriaSyntax = parse(expression); + } + return this.__rawDataPrimaryKeyCriteriaSyntax; + } + }, + + rawDataPrimaryKeyCriteriaForObject: { + value: function(object) { + var rawDataPrimaryKeys = this.rawDataPrimaryKeys, + isObjectCreated = this.service.isObjectCreated(object), + snapshot = !isObjectCreated && this.service.snapshotForObject(object), + criteriaParameters, + i, iKey, iKeyRawRule, countI; + + /* + If the object has been fetched, we should have the values in the snapshot, otherwise if they are natural primiary keys, we might be able to get them by mapping back + */ + for(i=0, countI = rawDataPrimaryKeys.length; (i Date: Wed, 27 Jan 2021 00:05:39 -0800 Subject: [PATCH 387/407] Fix flatten bug if receiver is already flat --- core/collections/generic-collection.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/collections/generic-collection.js b/core/collections/generic-collection.js index 7e6dcc9760..3b939e05c6 100644 --- a/core/collections/generic-collection.js +++ b/core/collections/generic-collection.js @@ -235,9 +235,13 @@ GenericCollection.prototype.concat = function () { GenericCollection.prototype.flatten = function () { var self = this; return this.reduce(function (result, array) { - array.forEach(function (value) { - this.push(value); - }, result, self); + if(array.forEach) { + array.forEach(function (value) { + this.push(value); + }, result, self); + } else { + result.push(array); + } return result; }, []); }; From 72adecf120f85164ccc6ecda1245114c04479ebe Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 27 Jan 2021 00:08:32 -0800 Subject: [PATCH 388/407] add new syntax iterators inspired from binary trees and using generator, with the ability to call .next(aType) to move through the tree with more finesse. Because syntax is built by the parser generated by Peg, we don't have a type where we could get the iterator from. --- core/frb/syntax-iterator.js | 176 ++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 core/frb/syntax-iterator.js diff --git a/core/frb/syntax-iterator.js b/core/frb/syntax-iterator.js new file mode 100644 index 0000000000..9dfc0b07c3 --- /dev/null +++ b/core/frb/syntax-iterator.js @@ -0,0 +1,176 @@ +/* + Cues from + + https://blog.mgechev.com/2014/09/12/binary-tree-iterator-with-es6-generators/ + + https://github.com/mgechev/javascript-algorithms/blob/master/src/data-structures/binary-search-tree.js#L65-L72 + +*/ + + +function SyntaxIterator(syntax, type){ + if(syntax) { + this._syntax = syntax; + this._parentBySyntax = new WeakMap(); + this._parentBySyntax.set(syntax,null); + /* + Initially, during the creation of the iterator, we need to call it because the next method is actually a generator, so by invoking it we return new instance of the generator. + */ + var _iterator = this.next(type,this._syntax); + _iterator.parent = (syntax) => { + return this._parentBySyntax.get(syntax); + }; + return _iterator; + } +}; + +function SyntaxInOrderIterator(syntax, type) { + return SyntaxIterator.call(this,syntax, type); +} +SyntaxInOrderIterator.prototype = new SyntaxIterator(); +SyntaxInOrderIterator.prototype.constructor = SyntaxInOrderIterator; + +SyntaxInOrderIterator.prototype.next = function* (type, current) { + var localType; + + if (current === undefined) { + current = this._syntax; + } + + if (current === null) { + return; + } + + if(current.args && current.args[0]) { + this._parentBySyntax.set(current.args[0],current); + localType = yield* this.next(type, current.args[0]); + } + + localType = localType === undefined + ? type + : localType; + + if(!localType || current.type === localType) { + localType = yield current; + + localType = localType === undefined + ? type + : localType; + + } + + if(current.args && current.args[1]) { + this._parentBySyntax.set(current.args[1],current); + localType = yield* this.next(type, current.args[1]); + } + + localType = localType === undefined + ? type + : localType; + +}; + +function SyntaxPostOrderIterator(syntax, type) { + return SyntaxIterator.call(this,syntax, type); +} +SyntaxPostOrderIterator.prototype = new SyntaxIterator(); +SyntaxPostOrderIterator.prototype.constructor = SyntaxPostOrderIterator; + +SyntaxPostOrderIterator.prototype.next = function* (type, current) { + var localType; + + if (current === undefined) { + current = this._syntax; + } + + if (current === null) { + return; + } + + if(current.args && current.args[0]) { + this._parentBySyntax.set(current.args[0],current); + localType = yield* this.next(type, current.args[0]); + } + + localType = localType === undefined + ? type + : localType; + + + if(current.args && current.args[1]) { + this._parentBySyntax.set(current.args[1],current); + localType = yield* this.next(type, current.args[1]); + } + + localType = localType === undefined + ? type + : localType; + + + if(!localType || current.type === localType) { + localType = yield current; + + localType = localType === undefined + ? type + : localType; + + } + +}; + + +function SyntaxPreOrderIterator(syntax, type) { + return SyntaxIterator.call(this,syntax, type); +} +SyntaxPreOrderIterator.prototype = new SyntaxIterator(); +SyntaxPreOrderIterator.prototype.constructor = SyntaxPreOrderIterator; + +SyntaxPreOrderIterator.prototype.next = function* (type, current) { + var localType; + + if (current === undefined) { + current = this._syntax; + } + + if (current === null) { + return; + } + + if(!localType || current.type === localType) { + localType = yield current; + + localType = localType === undefined + ? type + : localType; + + } + + if(current.args && current.args[0]) { + this._parentBySyntax.set(current.args[0],current); + localType = yield* this.next(type, current.args[0]); + } + + localType = localType === undefined + ? type + : localType; + + + if(current.args && current.args[1]) { + this._parentBySyntax.set(current.args[1],current); + localType = yield* this.next(type, current.args[1]); + } + + localType = localType === undefined + ? type + : localType; + + +}; + + + +exports.SyntaxIterator = SyntaxIterator; +exports.SyntaxInOrderIterator = SyntaxInOrderIterator; +exports.SyntaxPostOrderIterator = SyntaxPostOrderIterator; +exports.SyntaxPreOrderIterator = SyntaxPreOrderIterator; + From 1de2b3aabdace3d209fde61b515a63806e8f4fa0 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 2 Feb 2021 15:36:37 -0800 Subject: [PATCH 389/407] change to minimize Promise creation --- core/mr/require.js | 60 +++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/core/mr/require.js b/core/mr/require.js index fef0bd1ae6..8ea57be600 100644 --- a/core/mr/require.js +++ b/core/mr/require.js @@ -634,7 +634,8 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { // Ensures a module definition is loaded, compiled, analyzed var load = memoize(function (topId, viaId) { - var module = getModuleDescriptor(topId); + var module = getModuleDescriptor(topId), + promise; /* Equivallent to: @@ -643,32 +644,37 @@ Object.defineProperty(String.prototype, 'stringByRemovingPathExtension', { without the need to be dependent on non-standard Promise.try method */ - return new Promise(function(resolve, reject) { - resolve((function () { - // if not already loaded, already instantiated, or - // configured as a redirection to another module - if ( - module.factory === void 0 && - module.exports === void 0 && - module.redirect === void 0 - ) { - //return Promise.try(config.load, [topId, module]); - return config.load(topId, module); - } - })()); - }) - // return Promise.try(function () { - // // if not already loaded, already instantiated, or - // // configured as a redirection to another module - // if ( - // module.factory === void 0 && - // module.exports === void 0 && - // module.redirect === void 0 - // ) { - // //return Promise.try(config.load, [topId, module]); - // return config.load(topId, module); - // } - // }) + // return new Promise(function(resolve, reject) { + // resolve((function () { + // // if not already loaded, already instantiated, or + // // configured as a redirection to another module + // if ( + // module.factory === void 0 && + // module.exports === void 0 && + // module.redirect === void 0 + // ) { + // //return Promise.try(config.load, [topId, module]); + // return config.load(topId, module); + // } + // })()); + // }) + + + // if not already loaded, already instantiated, or + // configured as a redirection to another module + if ( + module.factory === void 0 && + module.exports === void 0 && + module.redirect === void 0 + ) { + promise = config.load(topId, module); + } + + if(!promise || typeof promise.then !== "function") { + promise = Promise.resolve(promise); + } + + return promise .then(function () { // compile and analyze dependencies return config.compile(module).then(function () { From 8feeed6290256e229c1774676166b07ed44a2241 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 4 Feb 2021 19:01:36 -0800 Subject: [PATCH 390/407] fix a bug --- data/service/expression-data-mapping.js | 39 +++++++++++++++++++------ 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 6769004079..8305859674 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -1627,26 +1627,47 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData } }, - mapObjectPropertyNameToRawPropertyName: { value: function(property) { - var objectRule = this.objectMappingRules.get(property), - rule = objectRule && this.rawDataMappingRules.get(objectRule.sourcePath); + var objectRule = this.objectMappingRules.get(property); - if(rule) { - //A sourcePath that's part the primary key doesn't sounds good, it has to be a relationship... - if(this.rawDataPrimaryKeys.indexOf(objectRule.sourcePath) === -1) { - return objectRule.sourcePath; - } else { - return null; + if(objectRule) { + if(objectRule.sourcePathSyntax.type === "record") { + throw "Support for objecy properties mapped to multiple columns isn't properly implemented"; } + return objectRule.sourcePath; } else { + /* + It's a bit weird, this can happens if a column is used as part of a compound source expresssion along other columns to produce an object property. This shouldn't be handled here... + */ return property; } } }, + + // mapObjectPropertyNameToRawPropertyName: { + // value: function(property) { + // var objectRule = this.objectMappingRules.get(property), + // rule = objectRule && this.rawDataMappingRules.get(objectRule.sourcePath); + + // if(rule) { + // //A sourcePath that's part the primary key doesn't sounds good, it has to be a relationship... + // if(this.rawDataPrimaryKeys.indexOf(objectRule.sourcePath) === -1) { + // return objectRule.sourcePath; + // } else { + // return null; + // } + // } + // else { + // return property; + // } + + // } + // }, + + /** * Prefetches any object properties required to map the rawData property * and maps once the fetch is complete. From 86bd607ad8d5add932209cc7994a6c56c37b335e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 4 Feb 2021 19:02:59 -0800 Subject: [PATCH 391/407] add support for locales on operations --- data/service/data-operation.js | 36 ++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 0c4f5ba916..839bd980e7 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -5,6 +5,7 @@ var Montage = require("core/core").Montage, Enum = require("core/enum").Enum, uuid = require("core/uuid"), defaultEventManager = require("../../core/event/event-manager").defaultEventManager, + Locale = require("core/locale").Locale, DataOperationType, /* todo: we shpuld add a ...timedout for all operations. */ @@ -241,14 +242,15 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy serializer.setProperty("referrerId", this.referrerId); } serializer.setProperty("criteria", this._criteria); - /* - Hack: this is neededed for now to represent a query's fetchLimit - But it's really relevant only for a read operation.... - TODO: Needs to sort this out better... + /* + Inlining locales for speed and compactness instead of letting locales serialize themselves */ - if(this.readLimit) { - serializer.setProperty("readLimit", this.readLimit); + if(this.locales) { + for(var locales = [], i=0, countI = this.locales.length; (i < countI); i++) { + locales.push(this.locales[i].identifier); + } + serializer.setProperty("locales", locales); } if(this.data) { serializer.setProperty("data", this.data); @@ -322,6 +324,18 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy this.data = value; } + /* + Inlining locales for speed and compactness instead of letting locales deserialize themselves + */ + value = deserializer.getProperty("locales"); + if (value !== void 0) { + + for(var locales = [], i=0, countI = value.length; (i < countI); i++) { + locales.push(Locale.withIdentifier(value[i])); + } + this.locales = locales; + } + value = deserializer.getProperty("snapshot"); if (value !== void 0) { this.snapshot = value; @@ -490,6 +504,16 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy value: undefined }, + /** + * The locales relevant to this operation. In an authoring mode/context, there could be multiple ones. + * + * @type {Array} + */ + locales: { + value: undefined + }, + + /** * a message about the operation meant for the user. * From 5bc9a76f89f5d8aeb3d006b249f43364e4afee2d Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 4 Feb 2021 19:03:57 -0800 Subject: [PATCH 392/407] fix a bug using superForSet/superForGet/superForValue --- core/core.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/core/core.js b/core/core.js index 614979cdf2..403b57f23b 100644 --- a/core/core.js +++ b/core/core.js @@ -2,6 +2,16 @@ * @module montage/core/core */ + /** + * The Montage constructor provides conveniences for sub-typing + * ([specialize]{@link Montage.specialize}) and common methods for Montage + * prototype chains. + * + * @class Montage + * @classdesc The basis of all types using the MontageJS framework. + */ +var Montage = exports.Montage = function Montage() {}; + require("./collections/shim"); require("./shim/object"); require("./shim/array"); @@ -71,15 +81,6 @@ var ATTRIBUTE_PROPERTIES = "AttributeProperties", }); } -/** - * The Montage constructor provides conveniences for sub-typing - * ([specialize]{@link Montage.specialize}) and common methods for Montage - * prototype chains. - * - * @class Montage - * @classdesc The basis of all types using the MontageJS framework. - */ -var Montage = exports.Montage = function Montage() {}; var PROTO_IS_SUPPORTED = {}.__proto__ === Object.prototype; var PROTO_PROPERTIES_BLACKLIST = {"_montage_metadata": 1, "__state__": 1, "_hasUserDefinedConstructor": 1}; @@ -616,7 +617,7 @@ function __findSuperMethodImplementation( method, classFn, isFunctionSuper, meth if ((property = Object.getOwnPropertyDescriptor(context, propertyName))) { func = property.value; if (func !== undefined && func !== null) { - if (func === method || func.deprecatedFunction === method) { + if (func === method || (isValueArg && methodPropertyNameArg && propertyName === methodPropertyNameArg) || func.deprecatedFunction === method) { methodPropertyName = propertyName; isValue = true; break; @@ -625,7 +626,7 @@ function __findSuperMethodImplementation( method, classFn, isFunctionSuper, meth else { func = property.get; if (func !== undefined && func !== null) { - if (func === method || func.deprecatedFunction === method) { + if (func === method || (isGetterArg && methodPropertyNameArg && propertyName === methodPropertyNameArg) || func.deprecatedFunction === method) { methodPropertyName = propertyName; isGetter = true; break; @@ -633,7 +634,7 @@ function __findSuperMethodImplementation( method, classFn, isFunctionSuper, meth } func = property.set; if (func !== undefined && func !== null) { - if (func === method || func.deprecatedFunction === method) { + if (func === method || (isSetterArg && methodPropertyNameArg && propertyName === methodPropertyNameArg) || func.deprecatedFunction === method) { methodPropertyName = propertyName; isSetter = true; break; From cfdccec42c6259f302668cd3cf70d244039610df Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Fri, 5 Feb 2021 10:12:06 -0800 Subject: [PATCH 393/407] Fix a bug in node due to files loading each other --- core/collections/map.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/collections/map.js b/core/collections/map.js index eaf009e420..3999bb11d8 100644 --- a/core/collections/map.js +++ b/core/collections/map.js @@ -1,10 +1,11 @@ "use strict"; var Map = require("./_map"); +module.exports = Map; + var PropertyChanges = require("./listen/property-changes"); var MapChanges = require("./listen/map-changes"); -module.exports = Map; if((global.Map === void 0) || (typeof global.Set.prototype.values !== "function")) { Object.addEach(Map.prototype, PropertyChanges.prototype); From 403d7f1e288629af9c9b02f90aa5063bbda91c3a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 8 Feb 2021 16:27:29 -0800 Subject: [PATCH 394/407] add suffix to most types of DataOperations to set them apart from similar events on objects, like 'create' --- data/service/data-operation.js | 275 +++++++++++++++---------------- data/service/data-stream.js | 4 +- data/service/raw-data-service.js | 6 +- 3 files changed, 139 insertions(+), 146 deletions(-) diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 839bd980e7..c6074de3b1 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -11,91 +11,92 @@ var Montage = require("core/core").Montage, /* todo: we shpuld add a ...timedout for all operations. */ dataOperationTypes = [ "noop", - "connect", - "disconnect", - "create", + "connectOperation", + "disconnectOperation", + "createOperation", /* - Request to cancel a previous create operation, dispatched by the actor that dispatched the matching create + Request to cancel a previous create operation, dispatched by the actor that dispatched the matching create ? */ - "createCancel", - "createFailed", - "createCompleted", - "createCancelled", + "cancelCreateOperation", + "createFailedOperation", + "createCompletedOperation", + "createCanceledOperation", //Additional - "copy", - "copyFailed", - "copyCompleted", + "copyOperation", + "copyFailedOperation", + "copyCompletedOperation", /* Read is the first operation that models a query */ - "read", + "readOperation", /* ReadUpdated is pushed by server when a query's result changes due to data changes from others */ - "readUpdated", + "readUpdatedOperation", /* ReadProgress / ReadUpdate / ReadSeek is used to instruct server that more data is required for a "live" read / query Need a better name, and a symetric? Or is ReadUpdated enough if it referes to previous operation */ - "readProgress", //ReadUpdate - "readUpdate", //ReadUpdate + "readProgressOperation", //ReadUpdate + "readUpdateOperation", //ReadUpdate /* ReadCancel is the operation that instructs baclkend that client isn't interested by a read operastion anymore */ - "readCancel", + "cancelReadOperation", /* ReadCanceled is the operation that instructs the client that a read operation is canceled */ - "readCanceled", + "readCanceledOperation", - /* ReadFailed is the operation that instructs the client that a read operation has failed canceled */ - "readFailed", + /* ReadFailed is the operation that instructs the client that a read operation has failed */ + "readFailedOperation", /* ReadCompleted is the operation that instructs the client that a read operation has returned all available data */ - "readCompleted", + "readCompletedOperation", /* Request to update data, used either by the client sending the server or vice versa */ - "update", + "updateOperation", /* Confirmation that a Request to update data, used either by the client sending the server or vice versa*, has been completed */ - "updateCompleted", + "updateCompletedOperation", /* Confirmation that a Request to update data, used either by the client sending the server or vice versa*, has failed */ - "updateFailed", + "updateFailedOperation", /* Request to cancel an update, used either by the client sending the server or vice versa */ - "updateCancel", + "cancelUpdateOperation", /* Confirmation that a Request to cancel an update data, used either by the client sending the server or vice versa*, has completed */ - "updateCanceled", + "updateCanceledOperation", - "merge", + "mergeOperation", /* Request to cancel a previous create operation, dispatched by the actor that dispatched the matching create */ - "mergeCancel", - "mergeFailed", - "mergeCompleted", - "mergeCancelled", + "mergeCancelOperation", + "mergeFailedOperation", + "mergeCompletedOperation", + "cancelMergeOperation", + "mergeCanceledOperation", - "delete", - "deleteCompleted", - "deleteFailed", + "deleteOperation", + "deleteCompletedOperation", + "deleteFailedOperation", /* Lock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ - "lock", - "lockCompleted", - "lockFailed", + "lockOperation", + "lockCompletedOperation", + "lockFailedOperation", /* Unlock models the ability for a client to prevent others to make changes to a set of objects described by operation's criteria */ - "unlock", - "unlockCompleted", - "unlockFailed", + "unlockOperation", + "unlockCompletedOperation", + "unlockFailedOperation", /* RemmoteProcedureCall models the ability to invoke code logic on the server-side, being a DB StoredProcedure, or an method/function in a service */ - "remoteInvocation", /* Execute ? */ - "remoteInvocationCompleted", /* ExecuteCompleted ? */ - "remoteInvocationFailed", /* ExecuteFailed ? */ + "remoteInvocationOperation", /* Execute ? */ + "remoteInvocationCompletedOperation", /* ExecuteCompleted ? */ + "remoteInvocationFailedOperation", /* ExecuteFailed ? */ /* Batch models the ability to group multiple operation. If a referrer is provided to a BeginTransaction operation, then the batch will be executed within that transaction */ - "batch", - "batchUpdate", - "batchCompleted", - "batchFailed", + "batchOperation", + "batchUpdateOperation", + "batchCompletedOperation", + "batchFailedOperation", /* A transaction is a unit of work that is performed atomically against a database. @@ -110,26 +111,26 @@ var Montage = require("core/core").Montage, so settling on create transaction and perform/rollback transaction */ - "createTransaction", + "createTransactionOperation", /* I don't think there's such a thing, keeping for symetry for now */ - "createTransactionCompleted", + "createTransactionCompletedOperation", /* Attempting to create a transaction within an existing one will fail */ - "createTransactionFailed", + "createTransactionFailedOperation", - "transactionUpdated", - "transactionCancelled", + "transactionUpdatedOperation", + "transactionCanceledOperation", - "createSavePoint", + "createSavePointOperation", - "performTransaction", - "performTransactionProgress", - "performTransactionCompleted", - "performTransactionFailed", + "performTransactionOperation", + "performTransactionProgressOperation", + "performTransactionCompletedOperation", + "performTransactionFailedOperation", - "rollbackTransaction", - "rollbackTransactionCompleted", - "rollbackTransactionFailed", + "rollbackTransactionOperation", + "rollbackTransactionCompletedOperation", + "rollbackTransactionFailedOperation", /* operations used for the bottom of the stack to get information from a user. @@ -159,14 +160,13 @@ var Montage = require("core/core").Montage, This can be used for expressing that a password value is wrong, that an account isn't confirmed with the Identity authority that a mandatory value is missing, etc... - - */ - "validate", - "validateFailed", - "validateCompleted", - "validateCancelled", - "keepAlive" + "validateOperation", + "validateFailedOperation", + "validateCompletedOperation", + "cancelValidateOperation", + "validateCanceledOperation", + "keepAliveOperation" ]; @@ -387,14 +387,6 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy name: { value: undefined }, - /** - * The type of operation, (TODO: inherited from Event). We sh - - * @type {DataOperation.Type.CREATE|DataOperation.Type.READ|DataOperation.Type.UPDATE|DataOperation.Type.DELETE} - */ - type: { - value: undefined - }, /** * BENOIT: the module for the DataService that is associated with that operation. It's expected to be the main service for Operations and a specific RawDataService for RawOperation @@ -727,52 +719,52 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy */ value: { NoOp: DataOperationType.noop, - Connect: DataOperationType.connect, - Disconnect: DataOperationType.disconnect, - Create: DataOperationType.create, - CreateFailed: DataOperationType.createFailed, - CreateCompleted: DataOperationType.createCompleted, - CreateCancelled: DataOperationType.createCancelled, - - Copy: DataOperationType.copy, - CopyFailed: DataOperationType.copyFailed, - CopyCompleted: DataOperationType.copyCompleted, - - Read: DataOperationType.read, - ReadUpdated: DataOperationType.readUpdated, - ReadProgress: DataOperationType.readProgress, //ReadUpdate - ReadUpdate: DataOperationType.readUpdate, //ReadUpdate - ReadCancel: DataOperationType.readCancel, - ReadCanceled: DataOperationType.readCanceled, - ReadFailed: DataOperationType.readFailed, - ReadCompleted: DataOperationType.readCompleted, - - Update: DataOperationType.update, - UpdateCompleted: DataOperationType.updateCompleted, - UpdateFailed: DataOperationType.updateFailed, - UpdateCancel: DataOperationType.updateCancel, - UpdateCanceled: DataOperationType.updateCanceled, - - Merge: DataOperationType.merge, - MergeCancel: DataOperationType.mergeCancel, - MergeFailed: DataOperationType.mergeFailed, - MergeCompleted: DataOperationType.mergeCompleted, - MergeCancelled: DataOperationType.mergeCancelled, - - Delete: DataOperationType.delete, - DeleteCompleted: DataOperationType.deleteCompleted, - DeleteFailed: DataOperationType.deleteFailed, - - Lock: DataOperationType.lock, - LockCompleted: DataOperationType.lockCompleted, - LockFailed: DataOperationType.lockFailed, - - RemoteProcedureCall: DataOperationType.remoteInvocation, - RemoteProcedureCallCompleted: DataOperationType.remoteInvocationCompleted, - RemoteProcedureCallFailed: DataOperationType.remoteInvocationFailed, - RemoteInvocation: DataOperationType.remoteInvocation, - RemoteInvocationCompleted: DataOperationType.remoteInvocationCompleted, - RemoteInvocationFailed: DataOperationType.remoteInvocationFailed, + ConnectOperation: DataOperationType.connectOperation, + DisconnectOperation: DataOperationType.disconnectOperation, + CreateOperation: DataOperationType.createOperation, + CreateFailedOperation: DataOperationType.createFailedOperation, + CreateCompletedOperation: DataOperationType.createCompletedOperation, + CreateCanceledOperation: DataOperationType.createCanceledOperation, + + CopyOperation: DataOperationType.copyOperation, + CopyFailedOperation: DataOperationType.copyFailedOperation, + CopyCompletedOperation: DataOperationType.copyCompletedOperation, + + ReadOperation: DataOperationType.readOperation, + ReadUpdatedOperation: DataOperationType.readUpdatedOperation, + ReadProgressOperation: DataOperationType.readProgressOperation, //ReadUpdate + ReadUpdateOperation: DataOperationType.readUpdateOperation, //ReadUpdate + CancelReadOperation: DataOperationType.cancelReadOperation, + ReadCanceledOperation: DataOperationType.readCanceledOperation, + ReadFailedOperation: DataOperationType.readFailedOperation, + ReadCompletedOperation: DataOperationType.readCompletedOperation, + + UpdateOperation: DataOperationType.updateOperation, + UpdateCompletedOperation: DataOperationType.updateCompletedOperation, + UpdateFailedOperation: DataOperationType.updateFailedOperation, + cancelUpdateOperation: DataOperationType.cancelUpdateOperation, + UpdateCanceledOperation: DataOperationType.updateCanceledOperation, + + MergeOperation: DataOperationType.mergeOperation, + MergeFailedOperation: DataOperationType.mergeFailedOperation, + MergeCompletedOperation: DataOperationType.mergeCompletedOperation, + CancelMergeOperation: DataOperationType.cancelMergeOperation, + MergeCanceledOperation: DataOperationType.mergeCanceledOperation, + + DeleteOperation: DataOperationType.deleteOperation, + DeleteCompletedOperation: DataOperationType.deleteCompletedOperation, + DeleteFailedOperation: DataOperationType.deleteFailedOperation, + + LockOperation: DataOperationType.lockOperation, + LockCompletedOperation: DataOperationType.lockCompletedOperation, + LockFailedOperation: DataOperationType.lockFailedOperation, + UnlockOperation: DataOperationType.unlockOperation, + UnlockCompletedOperation: DataOperationType.unlockCompletedOperation, + UnlockFailedOperation: DataOperationType.unlockFailedOperation, + + RemoteInvocationOperation: DataOperationType.remoteInvocationOperation, + RemoteInvocationCompletedOperation: DataOperationType.remoteInvocationCompletedOperation, + RemoteInvocationFailedOperation: DataOperationType.remoteInvocationFailedOperation, UserAuthentication: DataOperationType.userAuthentication, UserAuthenticationUpdate: DataOperationType.userAuthenticationUpdate, @@ -786,34 +778,35 @@ exports.DataOperation = MutableEvent.specialize(/** @lends DataOperation.prototy UserInputCanceled: DataOperationType.userInputCanceled, UserInputTimedOut: DataOperationType.userInputTimedout, - Validate: DataOperationType.validate, - ValidateFailed: DataOperationType.validateFailed, - validateCompleted: DataOperationType.validateCompleted, - validateCancelled: DataOperationType.validateCancelled, + ValidateOperation: DataOperationType.validateOperation, + ValidateFailedOperation: DataOperationType.validateFailedOperation, + ValidateCompletedOperation: DataOperationType.validateCompletedOperation, + CancelValidateOperation: DataOperationType.cancelValidateOperation, + ValidateCanceledOperation: DataOperationType.validateCanceledOperation, - Batch: DataOperationType.batch, - BatchUpdate: DataOperationType.batchUpdate, - BatchCompleted: DataOperationType.batchCompleted, - BatchFailed: DataOperationType.batchFailed, + BatchOperation: DataOperationType.batchOperation, + BatchUpdateOperation: DataOperationType.batchUpdateOperation, + BatchCompletedOperation: DataOperationType.batchCompletedOperation, + BatchFailedOperation: DataOperationType.batchFailedOperation, - CreateTransaction: DataOperationType.createTransaction, - CreateTransactionCompleted: DataOperationType.createTransactionCompleted, - CreateTransactionFailed: DataOperationType.createTransactionFailed, + CreateTransactionOperation: DataOperationType.createTransactionOperation, + CreateTransactionCompletedOperation: DataOperationType.createTransactionCompletedOperation, + CreateTransactionFailedOperation: DataOperationType.createTransactionFailedOperation, - TransactionUpdated: DataOperationType.transactionUpdated, + TransactionUpdatedOperation: DataOperationType.transactionUpdatedOperation, - CreateSavePoint: DataOperationType.createSavePoint, + CreateSavePointOperation: DataOperationType.createSavePointOperation, - PerformTransaction: DataOperationType.performTransaction, - PerformTransactionProgress: DataOperationType.performTransactionProgress, - PerformTransactionCompleted: DataOperationType.performTransactionCompleted, - PerformTransactionFailed: DataOperationType.performTransactionFailed, + PerformTransactionOperation: DataOperationType.performTransactionOperation, + PerformTransactionProgressOperation: DataOperationType.performTransactionProgressOperation, + PerformTransactionCompletedOperation: DataOperationType.performTransactionCompletedOperation, + PerformTransactionFailedOperation: DataOperationType.performTransactionFailedOperation, - RollbackTransaction: DataOperationType.rollbackTransaction, - RollbackTransactionCompleted: DataOperationType.rollbackTransactionCompleted, - RollbackTransactionFailed: DataOperationType.rollbackTransactionFailed, - KeepAlive: DataOperationType.keepAlive, + RollbackTransactionOperation: DataOperationType.rollbackTransactionOperation, + RollbackTransactionCompletedOperation: DataOperationType.rollbackTransactionCompletedOperation, + RollbackTransactionFailedOperation: DataOperationType.rollbackTransactionFailedOperation, + KeepAliveOperation: DataOperationType.keepAliveOperation, } diff --git a/data/service/data-stream.js b/data/service/data-stream.js index 417f51244d..60b50a0ede 100644 --- a/data/service/data-stream.js +++ b/data/service/data-stream.js @@ -358,7 +358,7 @@ DataStream = exports.DataStream = DataProvider.specialize(/** @lends DataStream. //This is probably should come from lower layers, the RawDataService in the Worker, but until then: if(!readUpdatedOperation) { var readUpdatedOperation = new DataOperation(); - readUpdatedOperation.type = DataOperation.Type.ReadUpdated; + readUpdatedOperation.type = DataOperation.Type.ReadUpdatedOperation; readUpdatedOperation.objectDescriptor = readUpdatedOperation.dataType = this.query.type; readUpdatedOperation.cursor = this._cursor; readUpdatedOperation.batchSize = this.query.batchSize; @@ -371,7 +371,7 @@ DataStream = exports.DataStream = DataProvider.specialize(/** @lends DataStream. //Kick starts the request for the next batch: if(this.hasPendingData) { var readUpdateOperation = new DataOperation(); - readUpdateOperation.type = DataOperation.Type.ReadUpdate; + readUpdateOperation.type = DataOperation.Type.ReadUpdateOperation; readUpdateOperation.objectDescriptor = readUpdateOperation.dataType = this.query.type; readUpdateOperation.cursor = this._cursor; readUpdateOperation.batchSize = this.query.batchSize; diff --git a/data/service/raw-data-service.js b/data/service/raw-data-service.js index 0c868b6359..f63a915e24 100644 --- a/data/service/raw-data-service.js +++ b/data/service/raw-data-service.js @@ -1882,7 +1882,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot if (err) { // an error occurred //console.log("!!! handleRead FAILED:", err, err.stack, rawDataOperation.sql); - operation.type = DataOperation.Type.ReadFailed; + operation.type = DataOperation.Type.ReadFailedOperation; //Should the data be the error? operation.data = err; } @@ -1891,9 +1891,9 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot //If we need to take care of readExpressions, we can't send a ReadCompleted until we have returnes everything that we asked for. if(isNotLast) { - operation.type = DataOperation.Type.ReadUpdate; + operation.type = DataOperation.Type.ReadUpdateOperation; } else { - operation.type = DataOperation.Type.ReadCompleted; + operation.type = DataOperation.Type.ReadCompletedOperation; } //We provide the inserted record as the operation's payload From ed9b98c2f040d36fa23f3ab962da3dd1fa14516a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 8 Feb 2021 23:25:35 -0800 Subject: [PATCH 395/407] add visibilitychange event definition --- core/event/event-manager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/event/event-manager.js b/core/event/event-manager.js index b228da45a8..ed11e45bdc 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -407,6 +407,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan touchmove: {bubbles: true, cancelable: true}, //TouchEvent touchstart: {bubbles: true, cancelable: true}, //TouchEvent unload: {bubbles: false, cancelable: false}, //DOM2, DOM3 + visibilitychange: {bubbles: true, cancelable: false}, //DOM2, DOM3 wheel: {bubbles: true, cancelable: true}, //DOM3 pointerdown: {bubbles: true, cancelable: true}, //PointerEvent pointerup: {bubbles: true, cancelable: true}, //PointerEvent From 70d2abec3ae38a67f9eb946e95ec7317e905139e Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 9 Feb 2021 11:27:24 -0800 Subject: [PATCH 396/407] Fix log --- core/event/event-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/event/event-manager.js b/core/event/event-manager.js index ed11e45bdc..8b6685dc1a 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -1293,7 +1293,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan var eventDefinitions = this.eventDefinitions[eventType], eventOpts; - if(eventDefinitions) { + if(!eventDefinitions) { console.debug("Event type "+eventType+" missed definition"); } From 3ded174d603f5dcc824159fe4efcec865d89676a Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 9 Feb 2021 11:28:18 -0800 Subject: [PATCH 397/407] add function name --- core/event/event-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/event/event-manager.js b/core/event/event-manager.js index 8b6685dc1a..2bf904cc72 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -2527,7 +2527,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan */ handleEvent: { enumerable: false, - value: function (event) { + value: function handleEvent(event) { if(isBrowser) { if ((window.MontageElement && event.target instanceof MontageElement) || (event instanceof UIEvent && !this._shouldDispatchEvent(event))) { From 426d67d2c6ea9fa3723e01a362ea84d1991ae7cd Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Sun, 14 Feb 2021 21:36:44 -0800 Subject: [PATCH 398/407] WIP Iteration on DataEditor regarding coordination of data loaded before drawing --- ui/data-editor.js | 115 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 90 insertions(+), 25 deletions(-) diff --git a/ui/data-editor.js b/ui/data-editor.js index e2fa5675e2..14cb0ecc7c 100644 --- a/ui/data-editor.js +++ b/ui/data-editor.js @@ -3,7 +3,13 @@ Criteria = require("core/criteria").Criteria, DataQuery = require("data/model/data-query").DataQuery, DataStream = require("data/service/data-stream").DataStream, ObjectDescriptor = require("core/meta/object-descriptor").ObjectDescriptor, -DataOrdering = require("data/model/data-ordering").DataOrdering; +DataOrdering = require("data/model/data-ordering").DataOrdering, +Montage = require("core/core").Montage, +//UUID = require("core/uuid"), +ONE_WAY = "<-", +ONE_WAY_RIGHT = "->", +TWO_WAY = "<->"; + /** @@ -51,19 +57,27 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { // this.canDrawGate.setField("dataLoaded", false); // console.log("---------- "+this.constructor.name+" inDocument:"+this.inDocument+" —— dataLoaded: false",value); - return this; - } - }, - defineBinding: { - value: function (targetPath, descriptor, commonDescriptor) { - var result = this.super(targetPath, descriptor, commonDescriptor); + // this.addPathChangeListener( + // "data", + // this, + // "handleDataChange" + // ); - if(targetPath.startsWith("data")) { - console.log(this.constructor.name+" has ["+targetPath+"] bound to ["+descriptor.sourcePath+"]"+", parentComponent:",this.parentComponent); - } - return result; + return this; } }, + // defineBinding: { + // value: function (targetPath, descriptor, commonDescriptor) { + // var result = this.super(targetPath, descriptor, commonDescriptor), + // twoWay = TWO_WAY in descriptor, + // sourcePath = !twoWay ? descriptor[ONE_WAY] : descriptor[TWO_WAY] || ""; + + // // if(targetPath.startsWith("data") || sourcePath.indexOf(".data") !== -1) { + // // console.log(Object.keys(descriptor)[0]+" "+this.constructor.name+" has ["+targetPath+"] bound to ["+descriptor.sourcePath+"]"+", parentComponent:",this.parentComponent); + // // } + // return result; + // } + // }, /** * A DataService used to fetch data. By default uses application.mainService @@ -165,7 +179,8 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { currentDataStream = this.dataStream, dataStream, self = this; - //console.log(this.constructor.name+" fetchData() >>>>> "); + console.log(this.constructor.name+" fetchData() >>>>> setField('dataLoaded', false)"); + this.canDrawGate.setField("dataLoaded", false); dataStream = dataService.fetchData(this._dataQuery); dataStream.then(function(data) { //console.log("Data fetched:",data); @@ -173,22 +188,19 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { //We need to dataService.cancelDataStream(currentDataStream); - - self.didFetchData(data); - }, function(error) { console.log("fetchData failed:",error); }) .finally(() => { - this.canDrawGate.setField("dataLoaded", true); + // this.canDrawGate.setField("dataLoaded", true); }); } } }, - didFetchData: { - value: function (data) { + dataDidChange: { + value: function () { } }, @@ -285,6 +297,10 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { value: undefined }, + readExpressions: { + value: undefined + }, + /** * A RangeController, TreeController, or equivalent object that provides sorting, filtering, * selection handling of a collection of object. @@ -316,6 +332,36 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { } }, + /** + * This is the property that makes the dataLoaded gate works hierarchically, + * as long as nested DataEditors are capable of knowing when to signal that data is loaded. + * If bindings in tenplates just do their thing organially, DataEditor component owner + * is out of the loop, besides receiving ad. To know, he possibly could (we don't have a way for this right now): + * - listen somehow for the fact that a DataTrigger gets what's needed, in cascade....? + * DataTrigger/DataService could dispacth events when they're triggered? That's equivallent of using + * addPathChangeListener/addRangeAtPathChangeListener + * + * + * - figure out what property of chid components is bound to something off their data property, and observe that + * - (might need a new dataLoadedGate that combine into the canDraw-dataLoaded now), + * - and when the value is set ( null or otherwise) then flip the flag to true + * - that doesn't help load faster + * - that adds observing that has no other purpose than coordinating display + * + * - If child component would setCanDraw dataLoaded false when they don't have data, + * like a text receiving set value of undefined when initially bound, which is a problem unsolved when + * components are used in a repetition. The ones in the template received bindings but are used as template + * and never received actual data that would unlock things. + * + * objectDescriptor's that's in dataMapping + * as dataMapping.objectDescriptor. + */ + + + _blocksOwnerComponentDraw: { + value: true + }, + _data: { value: undefined }, @@ -324,19 +370,38 @@ exports.DataEditor = Component.specialize(/** @lends DataEditor# */ { return this._data; }, set: function (value) { - if(!this.hasOwnProperty("_data") && value === undefined) { - this.canDrawGate.setField("dataLoaded", true); - } - // console.log(this.constructor.name+ " set data:",value, " inDocument:"+this.inDocument+", parentComponent:",this.parentComponent); - if(this._data === undefined && value !== undefined) { - // console.log("++++++++++ "+this.constructor.name+" inDocument:"+this.inDocument+" —— dataLoaded: true",value); + + if(value !== this._data) { + /* + By checking for readExpressions, we assess wether the DataEditor has everything it needs with it's data, or, if it has readExpressions, then it means it needs a subgraph off data. + + By default, the readExpressions are sent in the query, but some DataServices may not be able to satisfy them all in one-shot. If not, then the DataService should ideally be able to hide the multiple round-trips to one or more RawDataService(s) to get all readExpressions asked. + + The readExpressions should ideally be dynamically gathered from what components exressed they needs to display, which is typically expressed by binding their property, like "value" for Text, to expressions off the DataEditor's owner data property. This isn't done yet. + */ + if(this.readExpressions && this.readExpressions.length > 0) { + // console.log("************** "+this.constructor.name+"["+this.uuid+'].setField("dataLoaded", false)'); + this.canDrawGate.setField("dataLoaded", false); + } else { this.canDrawGate.setField("dataLoaded", true); } - if(value !== this._data) { + + this._data = value; + + this.dataDidChange(value); + } } + }, + + handleDataChange: { + value: function (data) { + } } }); + + +//Montage.defineUuidProperty(exports.DataEditor.prototype); From 5f6c6a7afb86fc993d227f09aaa72757655a566c Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 25 Nov 2019 22:44:51 -0800 Subject: [PATCH 399/407] Fix a bug in handleOrganizedContentRangeChange that impacts every listeners dispatched after handleOrganizedContentRangeChange because it modifies the passed argument. --- core/range-controller.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/range-controller.js b/core/range-controller.js index dbac34fc48..d3e6007df6 100644 --- a/core/range-controller.js +++ b/core/range-controller.js @@ -778,6 +778,16 @@ var RangeController = exports.RangeController = Montage.specialize( /** @lends R if (this.selection.length) { this.selection.deleteEach(diff); +<<<<<<< HEAD +======= + + // ensure selection always has content + if (this.selection.length === 0 && this.content && this.content.length && + this.avoidsEmptySelection && !this.allowsMultipleSelection) { + // selection can't contain previous content value as content already changed + this.selection.add(this.content[this.content.length - 1]); + } +>>>>>>> Fix a bug in handleOrganizedContentRangeChange } } From 9282328c8c2d8d49744781161585a9cbe56484b9 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Thu, 26 Mar 2020 09:26:42 -0700 Subject: [PATCH 400/407] fix deprecation warnings from methods in collection's array additions --- core/range-controller.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/range-controller.js b/core/range-controller.js index d3e6007df6..c9ed3e49ef 100644 --- a/core/range-controller.js +++ b/core/range-controller.js @@ -148,6 +148,7 @@ Object.defineProperty(_RangeSelection.prototype, "swap_or_push", { // }, this); +<<<<<<< HEAD plus = []; for(var indexInSelection, i=0, countI = itemsToAdd.length;(i index) { + return false; + } + + // if the item is already in the selection, don't add it + // unless it's in the part that we're about to delete. + var indexInSelection = this.findValue(item); + return indexInSelection < 0 || + (indexInSelection >= start && indexInSelection < start + minusLength); +>>>>>>> fix deprecation warnings from methods in collection's array additions } else { From fda51a343d843fa42ec10adf5b23d3f266a63e1f Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Mon, 6 Apr 2020 16:48:16 -0700 Subject: [PATCH 401/407] First implematatio passing specs of support for options object in addEventListener. Performance comparison with master are needed. --- core/event/event-manager.js | 873 +++++++++++++++--- core/target.js | 12 +- test/spec/events/eventmanager-spec.js | 33 +- .../montage-deserializer-element-spec.js | 6 +- 4 files changed, 782 insertions(+), 142 deletions(-) diff --git a/core/event/event-manager.js b/core/event/event-manager.js index 2bf904cc72..5c33b5c727 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -26,7 +26,153 @@ var Montage = require("../core").Montage, Event_CAPTURING_PHASE = 1, Event_AT_TARGET = 2, Event_BUBBLING_PHASE = 3, - defaultEventManager; + defaultEventManager, + browserSupportsCaptureOption = false, + browserSupportsPassiveOption = false; + + + + + +/** + * Notes to integrate coomposers in event manager: + * + * 1. Composers have to be registered. + * - Composer's code should register themeselves + * - Component relying on them today already require them + * - later the tool can have a map of event names -> moduleId and + * automatically add to the root serialization, or each template, the modules needed. + * + * 2. addEventListner("eventName") + * - if not standard, find in map the composer registered if any + * - instantiate one + * - if listener is a component, set the relationship as before + * - load, etc... which make the composer add it's own listeners + * - add the composer to the options object. + */ + + + + + +/** + * add|removeEventListener options related feature detection for older browsers + */ +try { + var options = { + get passive() { + browserSupportsPassiveOption = true; + return false; + }, + get capture() { + browserSupportsCaptureOption = true; + return false; + } + }; + addEventListener("test", null, options); + removeEventListener("test", null, options); +} catch(e) {} + + +/** + * If needed, polyfill for Event Listener with Options + * from: + * https://raw.githubusercontent.com/WICG/EventListenerOptions/gh-pages/EventListenerOptions.polyfill.js + * + */ +if (!browserSupportsPassiveOption || !browserSupportsCaptureOption) { + (function() { + var super_add_event_listener = EventTarget.prototype.addEventListener; + var super_remove_event_listener = EventTarget.prototype.removeEventListener; + var super_prevent_default = Event.prototype.preventDefault; + + function parseOptions(type, listener, options, action) { + var needsWrapping = false; + var useCapture = false; + var passive = false; + var fieldId; + if (options) { + if (typeof(options) === 'object') { + passive = options.passive ? true : false; + useCapture = options.useCapture ? true : false; + } else { + useCapture = options; + } + } + if (passive) + needsWrapping = true; + if (needsWrapping) { + fieldId = useCapture.toString(); + fieldId += passive.toString(); + } + action(needsWrapping, fieldId, useCapture, passive); + } + + Event.prototype.preventDefault = function() { + if (this.__passive) { + console.warn("Ignored attempt to preventDefault an event from a passive listener"); + return; + } + super_prevent_default.apply(this); + } + + EventTarget.prototype.addEventListener = function(type, listener, options) { + var super_this = this; + parseOptions(type, listener, options, + function(needsWrapping, fieldId, useCapture, passive) { + if (needsWrapping) { + var fieldId = useCapture.toString(); + fieldId += passive.toString(); + + if (!this.__event_listeners_options) + this.__event_listeners_options = {}; + if (!this.__event_listeners_options[type]) + this.__event_listeners_options[type] = {}; + if (!this.__event_listeners_options[type][listener]) + this.__event_listeners_options[type][listener] = []; + if (this.__event_listeners_options[type][listener][fieldId]) + return; + var wrapped = { + handleEvent: function (e) { + e.__passive = passive; + if (typeof(listener) === 'function') { + listener(e); + } else { + listener.handleEvent(e); + } + e.__passive = false; + } + }; + this.__event_listeners_options[type][listener][fieldId] = wrapped; + super_add_event_listener.call(super_this, type, wrapped, useCapture); + } else { + super_add_event_listener.call(super_this, type, listener, useCapture); + } + }); + } + + EventTarget.prototype.removeEventListener = function(type, listener, options) { + var super_this = this; + parseOptions(type, listener, options, + function(needsWrapping, fieldId, useCapture, passive) { + if (needsWrapping && + this.__event_listeners_options && + this.__event_listeners_options[type] && + this.__event_listeners_options[type][listener] && + this.__event_listeners_options[type][listener][fieldId]) { + super_remove_event_listener.call(super_this, type, this.__event_listeners_options[type][listener][fieldId], false); + delete this.__event_listeners_options[type][listener][fieldId]; + if (this.__event_listeners_options[type][listener].length == 0) + delete this.__event_listeners_options[type][listener]; + } else { + super_remove_event_listener.call(super_this, type, listener, useCapture); + } + }); + } + + })(); +} + //This is a quick polyfill for IE10 that is not exposing CustomEvent as a function. //From https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill @@ -47,6 +193,8 @@ if ( window.CustomEvent = CustomEvent; } + + // jshint -W015 /* This is to handle browsers that have TouchEvents but don't have the global constructor function Touch */ if ( @@ -226,42 +374,90 @@ var _StoredEvent = Montage.specialize({ } }); -var _serializeObjectRegisteredEventListenersForPhase = function (serializer, object, registeredEventListeners, eventListenerDescriptors, capture) { - var i, l, type, listenerRegistrations, listeners, aListener, mapIter; - mapIter = registeredEventListeners.keys(); - - while ((type = mapIter.next().value)) { - listenerRegistrations = registeredEventListeners.get(type); - listeners = listenerRegistrations && listenerRegistrations.get(object); - if (Array.isArray(listeners) && listeners.length > 0) { - for (i = 0, l = listeners.length; i < l; i++) { - aListener = listeners[i]; - eventListenerDescriptors.push({ - type: type, - listener: serializer.addObjectReference(aListener), - capture: capture - }); - } - } - else if (listeners){ - eventListenerDescriptors.push({ - type: type, - listener: serializer.addObjectReference(listeners), - capture: capture - }); - } + + +/*************************************************************************************************************** + * + * Support for Event Serialiation/Deserialization + * + **************************************************************************************************************/ + +// var _serializeObjectRegisteredEventListenersForPhase = function (serializer, object, registeredEventListeners, eventListenerDescriptors, capture) { +// var i, l, type, listenerRegistrations, listeners, aListener, mapIter; +// mapIter = registeredEventListeners.keys(); + +// while ((type = mapIter.next().value)) { +// listenerRegistrations = registeredEventListeners.get(type); +// listeners = listenerRegistrations && listenerRegistrations.get(object); +// if (Array.isArray(listeners) && listeners.length > 0) { +// for (i = 0, l = listeners.length; i < l; i++) { +// aListener = listeners[i]; +// eventListenerDescriptors.push({ +// type: type, +// listener: serializer.addObjectReference(aListener), +// capture: capture +// }); +// } +// } +// else if (listeners){ +// eventListenerDescriptors.push({ +// type: type, +// listener: serializer.addObjectReference(listeners), +// capture: capture +// }); +// } +// } +// }; + + + +var _serializedListenerEntryForType = function (listenerEntry, eventType, serializer) { + //If entry only contains listener and capture keys, we can steamline to capture only + if(Object.keys(listenerEntry) === 2) { + return { + type: eventType, + listener: serializer.addObjectReference(listenerEntry.listener), + capture: listenerEntry.capture + }; + } else { + var serializedEntry = {}; + Object.assign(serializedEntry,listenerEntry); + delete serializedEntry.listener; + return { + type: eventType, + listener: serializer.addObjectReference(listenerEntry.listener), + options: serializedEntry + }; } -}; +} Serializer.defineSerializationUnit("listeners", function listenersSerializationUnit(serializer, object) { var eventManager = defaultEventManager, eventListenerDescriptors = [], + targetEntry = eventManager._targetEventListeners.get(object), descriptors, descriptor, listener; - _serializeObjectRegisteredEventListenersForPhase(serializer, object,eventManager._registeredCaptureEventListeners,eventListenerDescriptors,true); - _serializeObjectRegisteredEventListenersForPhase(serializer, object,eventManager._registeredBubbleEventListeners,eventListenerDescriptors,false); + // _serializeObjectRegisteredEventListenersForPhase(serializer, object,eventManager._registeredCaptureEventListeners,eventListenerDescriptors,true); + // _serializeObjectRegisteredEventListenersForPhase(serializer, object,eventManager._registeredBubbleEventListeners,eventListenerDescriptors,false); + + targetEntry && targetEntry.forEach(function(targetEntryForEventType/*value*/, eventType/*key*/, map) { + targetEntryForEventType.forEach(function(listenerEntries/*value*/, phase/*key*/, map) { + var i, l, iListenerEntry, iSerializedEntry; + + if (Array.isArray(listenerEntries) && listenerEntries.length > 0) { + for (i = 0, l = listenerEntries.length; i < l; i++) { + eventListenerDescriptors.push(_serializedListenerEntryForType(listenerEntries[i],eventType,serializer)); + } + } + else if (listenerEntries){ + eventListenerDescriptors.push(_serializedListenerEntryForType(listenerEntries,eventType,serializer)); + } + + }); + }); + if (eventListenerDescriptors.length > 0) { return eventListenerDescriptors; @@ -270,10 +466,23 @@ Serializer.defineSerializationUnit("listeners", function listenersSerializationU Deserializer.defineDeserializationUnit("listeners", function listenersDeserializationUnit(deserializer, object, listeners) { for (var i = 0, listener; (listener = listeners[i]); i++) { - object.addEventListener(listener.type, listener.listener, listener.capture); + if(listener.hasOwnProperty("capture") && typeof listener.capture === "boolean") { + object.addEventListener(listener.type, listener.listener, listener.capture); + } else { + object.addEventListener(listener.type, listener.listener, listener.options); + } } }); + + + +function EventListener(){} + + + + + /** * @class EventManager * @extends Montage @@ -297,6 +506,30 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan this.environment = currentEnvironment; this._trackingTouchTimeoutIDs = new Map(); this._functionType = "function"; + + this._targetEventListeners = new Map(); + /* + _targetEventListeners -> Map( eventType -> map ( + CaptureEventListeners -> [EventListeners{ + listener: {}, + capture: true, + once: true/false, + passive: true/false, + callBack + }] + BubbleEventListeners -> [EventListeners{ + listener: {}, + capture: false, + once: true/false, + passive: true/false, + callBack + + }]) ) + + + when addEventListner is going to be called with a boolean capture, we're going to buil d a listener object with the matching defauls (those might need to change per browser?) + */ + return this; } }, @@ -625,8 +858,9 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan value: (aWindow.XMLHttpRequest.prototype.addEventListener = aWindow.Element.prototype.addEventListener = aWindow.document.addEventListener = - function addEventListener(eventType, listener, useCapture) { - return aWindow.defaultEventManager.registerEventListener(this, eventType, listener, !!useCapture); + function addEventListener(eventType, listener, optionsOrUseCapture) { + //this.nativeAddEventListener(eventType, listener, optionsOrUseCapture); + aWindow.defaultEventManager.registerTargetEventListener(this, eventType, listener, optionsOrUseCapture); }) }); if (aWindow.EventTarget) { @@ -646,8 +880,9 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan aWindow.XMLHttpRequest.prototype.removeEventListener = aWindow.Element.prototype.removeEventListener = aWindow.document.removeEventListener = - function removeEventListener(eventType, listener, useCapture) { - return aWindow.defaultEventManager.unregisterEventListener(this, eventType, listener, !!useCapture); + function removeEventListener(eventType, listener, optionsOrUseCapture) { + //this.nativeRemoveEventListener(eventType, listener, optionsOrUseCapture); + return aWindow.defaultEventManager.unregisterTargetEventListener(this, eventType, listener, optionsOrUseCapture); }) }); @@ -876,38 +1111,60 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan * @param {Event} eventType The event type. * @returns null || listeners */ + // registeredEventListenersForEventType_: { + // value: function (eventType) { + // var captureRegistration = this._registeredCaptureEventListeners.get(eventType), + // bubbleRegistration = this._registeredBubbleEventListeners.get(eventType), + // result = null; + + // if (captureRegistration) { + // captureRegistration.forEach(function(listeners, target, map) { + // if (listeners && listeners.length > 0) { + // result = result || []; + // listeners.forEach(function(aListener) { + // result.push(aListener); + // }); + // } + // }); + // } + + // if (bubbleRegistration) { + // bubbleRegistration.forEach(function(listeners, target, map) { + // if (listeners && listeners.length > 0) { + // result = result || []; + // listeners.forEach(function(aListener) { + // result.push(aListener); + // }); + // } + // }); + // } + + // return result; + // } + // }, + registeredEventListenersForEventType_: { value: function (eventType) { - var captureRegistration = this._registeredCaptureEventListeners.get(eventType), - bubbleRegistration = this._registeredBubbleEventListeners.get(eventType), - result = null; - - if (captureRegistration) { - captureRegistration.forEach(function(listeners, target, map) { - if (listeners && listeners.length > 0) { - result = result || []; - listeners.forEach(function(aListener) { - result.push(aListener); - }); - } - }); - } + var result = null, + self = this; + this._targetEventListeners.forEach(function(targetEntry/*value*/, target/*key*/, map) { - if (bubbleRegistration) { - bubbleRegistration.forEach(function(listeners, target, map) { - if (listeners && listeners.length > 0) { - result = result || []; - listeners.forEach(function(aListener) { - result.push(aListener); - }); + var forEachResult = self.registeredEventListenersForEventType_onTarget_(eventType, target); + if(forEachResult) { + if(!Array.isArray(forEachResult)) { + (result || (result = [])).push(forEachResult); + } else { + Array.prototype.push.apply((result || (result = [])), forEachResult); + } } - }); - } + }); return result; } }, + + /** * Returns the list of all listeners registered for * the specified eventType on the specified target, regardless of the phase. @@ -917,34 +1174,85 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan * @param {Event} target - The event target. * @returns {?ActionEventListener} */ + // registeredEventListenersForEventType_onTarget_: { + // enumerable: false, + // value: function (eventType, target) { + + // var captureRegistration = this._registeredCaptureEventListeners.get(eventType), + // bubbleRegistration = this._registeredBubbleEventListeners.get(eventType), + // listeners, + // result = null; + + // if (!eventType || !target || (!captureRegistration && !bubbleRegistration)) { + // return null; + // } else { + // listeners = captureRegistration ? captureRegistration.get(target) : null; + // if (listeners) { + // if (!result) { + // result = listeners; + // } + // } + // listeners = bubbleRegistration ? bubbleRegistration.get(target) : null; + // if (listeners) { + // if (!result) { + // result = listeners; + // } else { + // result = result.union(listeners); + // } + // } + // return result; + // } + // } + // }, + registeredEventListenersForEventType_onTarget_: { enumerable: false, value: function (eventType, target) { + if (!eventType || !target) { + return null; + } - var captureRegistration = this._registeredCaptureEventListeners.get(eventType), - bubbleRegistration = this._registeredBubbleEventListeners.get(eventType), - listeners, - result = null; + var targetEntry = this._targetEventListeners.get(target), + targetEntryForEventType = targetEntry && targetEntry.get(eventType), + captureListenerEntries = targetEntryForEventType && targetEntryForEventType.get(Event_CAPTURING_PHASE), + bubbleListenerEntries = targetEntryForEventType && targetEntryForEventType.get(Event_BUBBLING_PHASE), + result = null; - if (!eventType || !target || (!captureRegistration && !bubbleRegistration)) { - return null; - } else { - listeners = captureRegistration ? captureRegistration.get(target) : null; - if (listeners) { - if (!result) { - result = listeners; + if(captureListenerEntries || bubbleListenerEntries) { + if(captureListenerEntries) { + if(!Array.isArray(captureListenerEntries)) { + result = captureListenerEntries.listener; + } + else { + result = []; + Array.prototype.push.apply(result, captureListenerEntries.map(this._listenerEntryListenerMapper)); } } - listeners = bubbleRegistration ? bubbleRegistration.get(target) : null; - if (listeners) { - if (!result) { - result = listeners; - } else { - result = result.union(listeners); + if(bubbleListenerEntries) { + if(!Array.isArray(bubbleListenerEntries)) { + if(!result) { + result = bubbleListenerEntries.listener; + } else if(!Array.isArray(result)) { + result = [result,bubbleListenerEntries.listener] + } else { + result.push(bubbleListenerEntries.listener) + } + } + else { + if(!result) { + result = []; + } + Array.prototype.push.apply(result, bubbleListenerEntries.map(this._listenerEntryListenerMapper)); } } - return result; } + + return result; + } + }, + _listenerEntryListenerMapper: { + value: function(listenerEntry) { + return listenerEntry.listener; } }, @@ -957,15 +1265,25 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan * @param {Event} target - The event target. * @returns {?ActionEventListener} */ + // registeredEventListenersForEventType_onTarget_phase_: { + // enumerable: false, + // value: function (eventType, target, capture) { + // if (!target) { + // return null; + // } + // return this._registeredEventListenersForEventType_onTarget_registeredEventListeners_(eventType, target, (capture ? this._registeredCaptureEventListeners : this._registeredBubbleEventListeners)); + // } + // }, registeredEventListenersForEventType_onTarget_phase_: { enumerable: false, value: function (eventType, target, capture) { if (!target) { return null; } - return this._registeredEventListenersForEventType_onTarget_registeredEventListeners_(eventType, target, (capture ? this._registeredCaptureEventListeners : this._registeredBubbleEventListeners)); + return this._registeredEventListenersOnTarget_eventType_eventPhase(target, eventType, (capture ? Event_CAPTURING_PHASE : Event_BUBBLING_PHASE)); } }, + _registeredEventListenersForEventType_onTarget_registeredEventListeners_: { value: function (eventType, target, registeredEventListeners) { @@ -976,6 +1294,23 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan enumerable: false }, + _registeredEventListenersOnTarget_eventType_eventPhase: { + value: function (target, eventType, eventPhase) { + + var targetEntry = this._targetEventListeners.get(target), + targetEntryForEventType = targetEntry ? targetEntry.get(eventType) : null, + targetEntryForEventTypeListeners = targetEntryForEventType + ? eventPhase === Event_CAPTURING_PHASE + ? targetEntryForEventType.get(Event_CAPTURING_PHASE) + : targetEntryForEventType.get(Event_BUBBLING_PHASE) + : null; + + return targetEntryForEventTypeListeners || null; + }, + enumerable: false + }, + + /** * Returns the dictionary of all listeners registered on * the specified target, keyed by eventType. @@ -1031,18 +1366,168 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan _registeredCaptureEventListeners: { value:null }, + _registeredBubbleEventListeners: { value:null - }, - registerEventListener: { - enumerable: false, - value: function registerEventListener(target, eventType, listener, useCapture) { - var result; - result = this._registerEventListener(target, eventType, listener, useCapture ? this._registeredCaptureEventListeners : this._registeredBubbleEventListeners); - return result; + }, + + _indexOfRegisteredListener: { + enumerable: false, + value: function indexOfRegisteredListener(targetEntryForEventTypeListeners, listener, listenerEntry) { + var i=0, countI = targetEntryForEventTypeListeners.length; + + if(listenerEntry) { + + for(;i 0) { - for (i = 0, l = eventRegistration.length; i < l; i++) { - target = eventRegistration[i]; - self._stopObservingTarget_forEventType_(target, eventType); - } - } - } - } - } - }, + // _resetRegisteredEventListeners: { + // enumerable: false, + // value: function (registeredEventListeners) { + // var i, l, target, eventType, + // eventRegistration, + // self = this; + + // for (eventType in registeredEventListeners) { + // if (registeredEventListeners.hasOwnProperty(eventType)) { + // eventRegistration = registeredEventListeners.get(eventType); + + // if (eventRegistration && eventRegistration.length > 0) { + // for (i = 0, l = eventRegistration.length; i < l; i++) { + // target = eventRegistration[i]; + // self._stopObservingTarget_forEventType_(target, eventType); + // } + // } + // } + // } + // } + // }, reset: { enumerable: false, value: function () { - this._resetRegisteredEventListeners(this._registeredCaptureEventListeners); - this._resetRegisteredEventListeners(this._registeredBubbleEventListeners); + // this._resetRegisteredEventListeners(this._registeredCaptureEventListeners); + // this._resetRegisteredEventListeners(this._registeredBubbleEventListeners); + + self = this; + this._targetEventListeners.forEach(function(targetEntry/*value*/, target/*key*/, map) { + + targetEntry.forEach(function(targetEntryForEventType/*value*/, eventType/*key*/, map) { + self._stopObservingTarget_forEventType_(target, eventType); + }); + + }); + // TODO for each component claiming a pointer, force them to surrender the pointer? this._claimedPointers = new Map(); - this._registeredCaptureEventListeners = new Map(); - this._registeredBubbleEventListeners = new Map(); + this._targetEventListeners = new Map(); + // this._registeredCaptureEventListeners = new Map(); + // this._registeredBubbleEventListeners = new Map(); } }, @@ -2505,29 +3007,68 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan * * @private */ + // _processCurrentDispatchedTargetListenersToRemove: { + // value: function(target, eventType, useCapture, listeners) { + + // var registeredEventListeners, + // otherPhaseRegisteredEventListeners, + // currentDispatchedTargetListenersToRemove = this._currentDispatchedTargetListeners.get(listeners); + + // if (currentDispatchedTargetListenersToRemove && currentDispatchedTargetListenersToRemove.size > 0) { + // listeners.removeObjects(currentDispatchedTargetListenersToRemove); + // registeredEventListeners = useCapture ? this._registeredCaptureEventListeners : this._registeredBubbleEventListeners; + // otherPhaseRegisteredEventListeners = useCapture ? this._registeredBubbleEventListeners : this._registeredCaptureEventListeners; + // this._unregisterTargetForEventTypeIfNeeded(target, eventType, listeners, registeredEventListeners, otherPhaseRegisteredEventListeners); + // } + // } + // }, + _integerAscendingSortFunction: { + value: function(a, b) { + return a - b; + } + }, _processCurrentDispatchedTargetListenersToRemove: { value: function(target, eventType, useCapture, listeners) { var registeredEventListeners, otherPhaseRegisteredEventListeners, + //currentDispatchedTargetListenersToRemove is an array that contains the indexes of the listenerEntries that needs to be removed post phase distribution currentDispatchedTargetListenersToRemove = this._currentDispatchedTargetListeners.get(listeners); - if (currentDispatchedTargetListenersToRemove && currentDispatchedTargetListenersToRemove.size > 0) { - listeners.removeObjects(currentDispatchedTargetListenersToRemove); - registeredEventListeners = useCapture ? this._registeredCaptureEventListeners : this._registeredBubbleEventListeners; - otherPhaseRegisteredEventListeners = useCapture ? this._registeredBubbleEventListeners : this._registeredCaptureEventListeners; - this._unregisterTargetForEventTypeIfNeeded(target, eventType, listeners, registeredEventListeners, otherPhaseRegisteredEventListeners); + if (currentDispatchedTargetListenersToRemove && currentDispatchedTargetListenersToRemove.length > 0) { + var targetEntry = this._targetEventListeners.get(target), + targetEntryForEventType = targetEntry ? targetEntry.get(eventType) : null, + targetEntryForEventTypeListeners = targetEntryForEventType + ? useCapture + ? targetEntryForEventType.get(Event_CAPTURING_PHASE) + : targetEntryForEventType.get(Event_BUBBLING_PHASE) + : null; + + //Remove what needs to be + currentDispatchedTargetListenersToRemove.sort(this._integerAscendingSortFunction); + for(var i = 0, countI = currentDispatchedTargetListenersToRemove.length, index; i < countI; i++) { + index = currentDispatchedTargetListenersToRemove[i] - i; + targetEntryForEventTypeListeners.splice(index, 1); + } + + this._stopObservingTargeForEventTypeIfNeeded(target, eventType, targetEntryForEventType); } } }, + + /** @function @param {Event} event The handled event. */ handleEvent: { enumerable: false, - value: function handleEvent(event) { + value: function EventManager_handleEvent(event) { + // if(event.type === "pointerdown" || event.type === "pointerup" || event.type.indexOf("press") !== -1) { + // console.log("handleEvent "+event.type,event.target); + // } + //console.log("----> handleEvent "+event.type); if(isBrowser) { if ((window.MontageElement && event.target instanceof MontageElement) || (event instanceof UIEvent && !this._shouldDispatchEvent(event))) { @@ -2535,6 +3076,8 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan } } + // performance.mark('event-manager:handleEvent:start'); + if (this.monitorDOMModificationInEventHandling) { document.body.addEventListener("DOMSubtreeModified", this.domModificationEventHandler, true); document.body.addEventListener("DOMAttrModified", this.domModificationEventHandler, true); @@ -2562,7 +3105,12 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan mutableEventTarget, _currentDispatchedTargetListeners = this._currentDispatchedTargetListeners, registeredCaptureEventListeners = this._registeredCaptureEventListeners, - registeredBubbleEventListeners = this._registeredBubbleEventListeners; + registeredBubbleEventListeners = this._registeredBubbleEventListeners, + + //New stuff + CAPTURING_PHASE = Event_CAPTURING_PHASE, + BUBBLING_PHASE = Event_BUBBLING_PHASE, + targetEntry, targetEntryForEventType; if ("DOMContentLoaded" === eventType) { loadedWindow = event.target.defaultView; @@ -2611,13 +3159,22 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan this._pointerStorage.storeEvent(mutableEvent); } + + targetEntry = this._targetEventListeners.get(mutableEventTarget); + targetEntryForEventType = targetEntry ? targetEntry.get(eventType) : null; + + // if(targetEntryForEventType) { + // registeredCaptureEventListeners = targetEntryForEventType.get(Event_CAPTURING_PHASE); + // registeredCaptureEventListeners = targetEntryForEventType.get(Event_BUBBLING_PHASE); + // } + // Capture Phase Distribution - mutableEvent.eventPhase = Event_CAPTURING_PHASE; + mutableEvent.eventPhase = CAPTURING_PHASE; // The event path we generate is from bottom to top, capture needs to traverse this backwards for (i = eventPath.length - 1; !mutableEvent.propagationStopped && (iTarget = eventPath[i]); i--) { mutableEvent.currentTarget = iTarget; - listenerEntries = this._registeredEventListenersForEventType_onTarget_registeredEventListeners_(eventType, iTarget, registeredCaptureEventListeners); + listenerEntries = this._registeredEventListenersOnTarget_eventType_eventPhase(iTarget, eventType, CAPTURING_PHASE); if (!listenerEntries) { continue; } @@ -2627,13 +3184,13 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan j=0; _currentDispatchedTargetListeners.set(listenerEntries,null); while ((nextEntry = listenerEntries[j++]) && !mutableEvent.immediatePropagationStopped) { - this._invokeTargetListenerForEvent(iTarget, nextEntry, mutableEvent, currentTargetIdentifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, currentTargetIdentifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, true, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, currentTargetIdentifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, currentTargetIdentifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); } } @@ -2643,47 +3200,47 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan mutableEvent.eventPhase = Event_AT_TARGET; mutableEvent.currentTarget = iTarget = mutableEventTarget; //Capture - listenerEntries = this._registeredEventListenersForEventType_onTarget_registeredEventListeners_(eventType, iTarget, registeredCaptureEventListeners); + listenerEntries = this._registeredEventListenersOnTarget_eventType_eventPhase(iTarget, eventType, CAPTURING_PHASE); if (listenerEntries) { if (Array.isArray(listenerEntries)) { j=0; _currentDispatchedTargetListeners.set(listenerEntries,null); while ((nextEntry = listenerEntries[j++]) && !mutableEvent.immediatePropagationStopped) { - this._invokeTargetListenerForEvent(iTarget, nextEntry, mutableEvent, identifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, identifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, true, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); } } //Bubble - listenerEntries = this._registeredEventListenersForEventType_onTarget_registeredEventListeners_(eventType, iTarget, registeredBubbleEventListeners); + listenerEntries = this._registeredEventListenersOnTarget_eventType_eventPhase(iTarget, eventType, BUBBLING_PHASE); if (listenerEntries) { if (Array.isArray(listenerEntries)) { j=0; _currentDispatchedTargetListeners.set(listenerEntries,null); while ((nextEntry = listenerEntries[j++]) && !mutableEvent.immediatePropagationStopped) { - this._invokeTargetListenerForEvent(iTarget, nextEntry, mutableEvent, identifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, identifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, false, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); } } } // Bubble Phase Distribution - mutableEvent.eventPhase = Event_BUBBLING_PHASE; + mutableEvent.eventPhase = BUBBLING_PHASE; for (i = 0; eventBubbles && !mutableEvent.propagationStopped && (iTarget = eventPath[i]); i++) { mutableEvent.currentTarget = iTarget; - listenerEntries = this._registeredEventListenersForEventType_onTarget_registeredEventListeners_(eventType, iTarget, registeredBubbleEventListeners); + listenerEntries = this._registeredEventListenersOnTarget_eventType_eventPhase(iTarget, eventType, BUBBLING_PHASE); if (!listenerEntries) { continue; } @@ -2694,13 +3251,13 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan j=0; _currentDispatchedTargetListeners.set(listenerEntries,null); while ((nextEntry = listenerEntries[j++]) && !mutableEvent.immediatePropagationStopped) { - this._invokeTargetListenerForEvent(iTarget, nextEntry, mutableEvent, currentTargetIdentifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, currentTargetIdentifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, false, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, currentTargetIdentifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, currentTargetIdentifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); } } @@ -2718,6 +3275,9 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan } //console.profileEnd("handleEvent "+event.type); //console.groupTimeEnd("handleEvent"); + + // performance.mark('event-manager:handleEvent:end'); + // performance.measure('handleEvent', 'event-manager:handleEvent:start', 'event-manager:handleEvent:end'); } }, @@ -2754,6 +3314,63 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan } }, + + /** + * @private + */ + _invokeTargetListenerEntryForEvent: { + value: function _invokeTargetListenerForEvent(iTarget, listenerEntry, mutableEvent, currentTargetIdentifierSpecificPhaseMethodName, targetIdentifierSpecificPhaseMethodName, phaseMethodName) { + var listener = listenerEntry.listener, + callback; + + + //TEST, shutting currentTargetIdentifierSpecificPhaseMethodName down: + currentTargetIdentifierSpecificPhaseMethodName = null; + + if(typeof listener === this._functionType) { + listener.call(iTarget, mutableEvent); + } else { + + if(!(callback = listenerEntry.callback)) { + + // callback = (currentTargetIdentifierSpecificPhaseMethodName && typeof (callback = listener[currentTargetIdentifierSpecificPhaseMethodName]) === this._functionType) + // ? callback + // : (targetIdentifierSpecificPhaseMethodName && typeof (callback = listener[targetIdentifierSpecificPhaseMethodName]) === this._functionType) + // ? callback + // : (typeof (callback = listener[phaseMethodName]) === this._functionType) + // ? callback + // : (typeof (callback = listener.handleEvent) === this._functionType) + // ? callback + // : void 0; + + callback = (currentTargetIdentifierSpecificPhaseMethodName && typeof listener[currentTargetIdentifierSpecificPhaseMethodName] === this._functionType) + ? currentTargetIdentifierSpecificPhaseMethodName + : (targetIdentifierSpecificPhaseMethodName && typeof listener[targetIdentifierSpecificPhaseMethodName] === this._functionType) + ? targetIdentifierSpecificPhaseMethodName + : (typeof listener[phaseMethodName] === this._functionType) + ? phaseMethodName + : (typeof listener.handleEvent === this._functionType) + ? "handleEvent" + : void 0; + + if(!listenerEntry.once) { + listenerEntry.callback = callback; + } + } + + if(callback) { + //callback.call(listener, mutableEvent); + listener[callback](mutableEvent); + + if(listenerEntry.once) { + this.unregisterTargetEventListener(iTarget, mutableEvent.type, listener, listenerEntry); + } + } + } + } + }, + + /** * Ensure that any components associated with DOM elements in the hierarchy between the * original activationEvent target and the window are preparedForActionEvents diff --git a/core/target.js b/core/target.js index e7c95fb229..59c4ab81d2 100644 --- a/core/target.js +++ b/core/target.js @@ -136,12 +136,12 @@ exports.Target = Montage.specialize( /** @lends Target.prototype */{ * @function * @param {string} type The event type to listen for. * @param {object | function} listener The listener object or function. - * @param {boolean} useCapture Specifies whether to listen for the event during the bubble or capture phases. + * @param {object | boolean} useCapture Specifies whether to listen for the event during the bubble or capture phases. */ addEventListener: { - value: function addEventListener(type, listener, useCapture) { + value: function addEventListener(type, listener, optionsOrUseCapture) { if (listener) { - defaultEventManager.registerEventListener(this, type, listener, useCapture); + defaultEventManager.registerTargetEventListener(this, type, listener, optionsOrUseCapture); } } }, @@ -151,12 +151,12 @@ exports.Target = Montage.specialize( /** @lends Target.prototype */{ * @function * @param {string} type The event type. * @param {object | function} listener The listener object or function. - * @param {boolean} useCapture The phase of the event listener. + * @param {object | boolean} useCapture The phase of the event listener. */ removeEventListener: { - value: function removeEventListener(type, listener, useCapture) { + value: function removeEventListener(type, listener, optionsOrUseCapture) { if (listener) { - defaultEventManager.unregisterEventListener(this, type, listener, useCapture); + defaultEventManager.unregisterTargetEventListener(this, type, listener, optionsOrUseCapture); } } }, diff --git a/test/spec/events/eventmanager-spec.js b/test/spec/events/eventmanager-spec.js index 3685e3c068..3388aac50c 100644 --- a/test/spec/events/eventmanager-spec.js +++ b/test/spec/events/eventmanager-spec.js @@ -231,8 +231,10 @@ TestPageLoader.queueTest("eventmanagertest/eventmanagertest", function (testPage var listener = new Montage(); testDocument.addEventListener("mousedown", listener, false); - expect(eventManager.registeredEventListenersForEventType_onTarget_phase_("mousedown",testDocument,false)).toEqual(listener); + expect(eventManager.registeredEventListenersForEventType_onTarget_phase_("mousedown",testDocument,false).listener).toEqual(listener); expect(eventManager.registeredEventListenersForEventType_onTarget_phase_("mousedown",testDocument,true)).toBeNull(); + testDocument.removeEventListener("mousedown", listener, false); + }); it("should not record a listener already listening to the same event type", function () { @@ -240,7 +242,7 @@ TestPageLoader.queueTest("eventmanagertest/eventmanagertest", function (testPage testDocument.addEventListener("mousedown", listener, false); testDocument.addEventListener("mousedown", listener, false); - expect(eventManager.registeredEventListenersForEventType_onTarget_phase_("mousedown", testDocument,false)).toEqual(listener); + expect(eventManager.registeredEventListenersForEventType_onTarget_phase_("mousedown", testDocument,false).listener).toEqual(listener); }); it("should add a native event listener when the first listener for an eventType is added for a target", function () { @@ -277,6 +279,25 @@ TestPageLoader.queueTest("eventmanagertest/eventmanagertest", function (testPage }); }); + it("should automatically remove a listener after first event delivery when once:true option is used", function () { + var handleClickCalledCount = 0; + clickSpy = { + handleClick: function (event) { + handleClickCalledCount++; + } + }; + + testDocument.addEventListener("click", clickSpy, {capture: false, once: true}); + + testPage.mouseEvent(new EventInfo().initWithElement(testDocument.documentElement), "click", function () { + expect(handleClickCalledCount).toEqual(1); + }); + testPage.mouseEvent(new EventInfo().initWithElement(testDocument.documentElement), "click", function () { + expect(handleClickCalledCount).toEqual(1); + }); + }); + + it("should not interfere with inline DOM 0 event listener function", function () { var inlineCalled = false; @@ -288,6 +309,7 @@ TestPageLoader.queueTest("eventmanagertest/eventmanagertest", function (testPage var clickSpy = { handleClick: function (event) { + console.log("handleClick(",event,") called"); } }; spyOn(clickSpy, 'handleClick'); @@ -313,8 +335,8 @@ TestPageLoader.queueTest("eventmanagertest/eventmanagertest", function (testPage var bubbleTestDocumentListeners = eventManager.registeredEventListenersForEventType_onTarget_phase_("foo",testDocument,false); var bubbleTestDocumentElementListeners = eventManager.registeredEventListenersForEventType_onTarget_phase_("foo",testDocument.documentElement,false); - expect(bubbleTestDocumentListeners).toEqual(docEventSpy); - expect(bubbleTestDocumentElementListeners).toEqual(rootEventSpy); + expect(bubbleTestDocumentListeners.listener).toEqual(docEventSpy); + expect(bubbleTestDocumentElementListeners.listener).toEqual(rootEventSpy); var captureTestDocumentListeners = eventManager.registeredEventListenersForEventType_onTarget_phase_("foo",testDocument,true); var captureTestDocumentElementListeners = eventManager.registeredEventListenersForEventType_onTarget_phase_("foo",testDocument.documentElement,true); @@ -452,7 +474,8 @@ TestPageLoader.queueTest("eventmanagertest/eventmanagertest", function (testPage spyOn(mousedownCaptureSpy, 'captureMousedown').and.callThrough(); spyOn(mousedownBubbleSpy, 'handleMousedown').and.callThrough(); - testDocument.addEventListener("mousedown", mousedownCaptureSpy, true); + //Introduce test for new options standard evolution + testDocument.addEventListener("mousedown", mousedownCaptureSpy, {capture: true}); testDocument.addEventListener("mousedown", mousedownBubbleSpy, false); testPage.mouseEvent(new EventInfo().initWithElement(testDocument.documentElement), "mousedown", function () { diff --git a/test/spec/serialization/montage-deserializer-element-spec.js b/test/spec/serialization/montage-deserializer-element-spec.js index c6cf242856..a8698741f2 100644 --- a/test/spec/serialization/montage-deserializer-element-spec.js +++ b/test/spec/serialization/montage-deserializer-element-spec.js @@ -66,9 +66,9 @@ describe("serialization/montage-deserializer-element-spec", function () { deserializer.init(serializationString, require); deserializer.deserialize(null, rootEl).then(function (objects) { - expect(defaultEventManager._registeredBubbleEventListeners.has("click")).toBe(true); - var registeredEventListeners = defaultEventManager._registeredBubbleEventListeners.get("click"); - var proxyElement = registeredEventListeners.keysArray()[0]; + var registeredEventListeners = defaultEventManager.registeredEventListenersForEventType_("click"); + expect(registeredEventListeners).toBeDefined(); + var proxyElement = registeredEventListeners[0]; return new Promise(function (resolve) { expect(proxyElement).toBeTruthy(); objects.rootEl.addEventListener("action", function () { From 6c9a31aa3a838da0c378187b18219cf4b56a66e1 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 9 Feb 2021 01:15:39 -0800 Subject: [PATCH 402/407] shift determination of listener's method to call to when we actually do rather than doing upfront before we know wether there are listeners or not. --- core/event/event-manager.js | 59 +++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/core/event/event-manager.js b/core/event/event-manager.js index 5c33b5c727..f3bd3505a6 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -3092,7 +3092,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan nextEntry, eventPath, eventType = event.type, - capitalizedEventType = eventType.toCapitalized(), + // capitalizedEventType = eventType.toCapitalized(), eventBubbles = event.bubbles, captureMethodName, bubbleMethodName, @@ -3138,17 +3138,17 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan } // use most specific handler method available, possibly based upon the identifier of the event target - if (mutableEventTarget.identifier) { - capitalizedIdentifier = mutableEventTarget.identifier.toCapitalized(); - identifierSpecificCaptureMethodName = this.methodNameForCapturePhaseOfEventType(eventType, mutableEventTarget.identifier, capitalizedEventType, capitalizedIdentifier); - identifierSpecificBubbleMethodName = this.methodNameForBubblePhaseOfEventType(eventType, mutableEventTarget.identifier, capitalizedEventType, capitalizedIdentifier); - } else { - identifierSpecificCaptureMethodName = null; - identifierSpecificBubbleMethodName = null; - } + // if (mutableEventTarget.identifier) { + // capitalizedIdentifier = mutableEventTarget.identifier.toCapitalized(); + // identifierSpecificCaptureMethodName = this.methodNameForCapturePhaseOfEventType(eventType, mutableEventTarget.identifier, capitalizedEventType, capitalizedIdentifier); + // identifierSpecificBubbleMethodName = this.methodNameForBubblePhaseOfEventType(eventType, mutableEventTarget.identifier, capitalizedEventType, capitalizedIdentifier); + // } else { + // identifierSpecificCaptureMethodName = null; + // identifierSpecificBubbleMethodName = null; + // } - captureMethodName = this.methodNameForCapturePhaseOfEventType(eventType, null, capitalizedEventType); - bubbleMethodName = this.methodNameForBubblePhaseOfEventType(eventType, null, capitalizedEventType); + // captureMethodName = this.methodNameForCapturePhaseOfEventType(eventType, null, capitalizedEventType); + // bubbleMethodName = this.methodNameForBubblePhaseOfEventType(eventType, null, capitalizedEventType); // Let the delegate handle the event first if (this.delegate && typeof this.delegate.willDistributeEvent === "function") { @@ -3179,18 +3179,18 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan continue; } - currentTargetIdentifierSpecificCaptureMethodName = this.methodNameForCapturePhaseOfEventType(eventType, iTarget.identifier, capitalizedEventType); + // currentTargetIdentifierSpecificCaptureMethodName = this.methodNameForCapturePhaseOfEventType(eventType, iTarget.identifier, capitalizedEventType); if (Array.isArray(listenerEntries)) { j=0; _currentDispatchedTargetListeners.set(listenerEntries,null); while ((nextEntry = listenerEntries[j++]) && !mutableEvent.immediatePropagationStopped) { - this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, currentTargetIdentifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, undefined/*currentTargetIdentifierSpecificCaptureMethodName*/, undefined/*identifierSpecificCaptureMethodName*/, undefined/*captureMethodName*/); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, true, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, currentTargetIdentifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, undefined/*currentTargetIdentifierSpecificCaptureMethodName*/, undefined/*identifierSpecificCaptureMethodName*/, undefined/*captureMethodName*/); } } @@ -3206,13 +3206,13 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan j=0; _currentDispatchedTargetListeners.set(listenerEntries,null); while ((nextEntry = listenerEntries[j++]) && !mutableEvent.immediatePropagationStopped) { - this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, identifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, undefined/*identifierSpecificCaptureMethodName*/, undefined/*identifierSpecificCaptureMethodName*/, undefined/*captureMethodName*/); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, true, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificCaptureMethodName, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, undefined/*identifierSpecificCaptureMethodName*/, undefined/*identifierSpecificCaptureMethodName*/, undefined/*captureMethodName*/); } } @@ -3223,13 +3223,13 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan j=0; _currentDispatchedTargetListeners.set(listenerEntries,null); while ((nextEntry = listenerEntries[j++]) && !mutableEvent.immediatePropagationStopped) { - this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, identifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, undefined/*identifierSpecificBubbleMethodName*/, undefined/*identifierSpecificBubbleMethodName*/, undefined/*bubbleMethodName*/); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, false, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, undefined/*identifierSpecificBubbleMethodName*/, undefined/*identifierSpecificBubbleMethodName*/, undefined/*bubbleMethodName*/); } } @@ -3245,19 +3245,19 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan continue; } - currentTargetIdentifierSpecificBubbleMethodName = this.methodNameForBubblePhaseOfEventType(eventType, iTarget.identifier, capitalizedEventType); + //currentTargetIdentifierSpecificBubbleMethodName = this.methodNameForBubblePhaseOfEventType(eventType, iTarget.identifier, capitalizedEventType); if (Array.isArray(listenerEntries)) { j=0; _currentDispatchedTargetListeners.set(listenerEntries,null); while ((nextEntry = listenerEntries[j++]) && !mutableEvent.immediatePropagationStopped) { - this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, currentTargetIdentifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, undefined/*currentTargetIdentifierSpecificBubbleMethodName*/, undefined/*identifierSpecificBubbleMethodName*/, undefined/*bubbleMethodName*/); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, false, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, currentTargetIdentifierSpecificBubbleMethodName, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, undefined/*currentTargetIdentifierSpecificBubbleMethodName*/, undefined/*identifierSpecificBubbleMethodName*/, undefined/*bubbleMethodName*/); } } @@ -3319,7 +3319,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan * @private */ _invokeTargetListenerEntryForEvent: { - value: function _invokeTargetListenerForEvent(iTarget, listenerEntry, mutableEvent, currentTargetIdentifierSpecificPhaseMethodName, targetIdentifierSpecificPhaseMethodName, phaseMethodName) { + value: function _invokeTargetListenerEntryForEvent(iTarget, listenerEntry, mutableEvent, currentTargetIdentifierSpecificPhaseMethodName, targetIdentifierSpecificPhaseMethodName, phaseMethodName) { var listener = listenerEntry.listener, callback; @@ -3343,11 +3343,12 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan // ? callback // : void 0; - callback = (currentTargetIdentifierSpecificPhaseMethodName && typeof listener[currentTargetIdentifierSpecificPhaseMethodName] === this._functionType) + + callback = ((currentTargetIdentifierSpecificPhaseMethodName = this._currentTargetIdentifierSpecificPhaseMethodName(listenerEntry.capture, mutableEvent.type, iTarget.identifier)) && typeof listener[currentTargetIdentifierSpecificPhaseMethodName] === this._functionType) ? currentTargetIdentifierSpecificPhaseMethodName - : (targetIdentifierSpecificPhaseMethodName && typeof listener[targetIdentifierSpecificPhaseMethodName] === this._functionType) + : ((targetIdentifierSpecificPhaseMethodName = this._currentTargetIdentifierSpecificPhaseMethodName(listenerEntry.capture,mutableEvent.type,mutableEvent.target.identifier)) && typeof listener[targetIdentifierSpecificPhaseMethodName] === this._functionType) ? targetIdentifierSpecificPhaseMethodName - : (typeof listener[phaseMethodName] === this._functionType) + : (typeof listener[(phaseMethodName = this._currentTargetIdentifierSpecificPhaseMethodName(listenerEntry.capture,mutableEvent.type, null))] === this._functionType) ? phaseMethodName : (typeof listener.handleEvent === this._functionType) ? "handleEvent" @@ -3370,6 +3371,14 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan } }, + _currentTargetIdentifierSpecificPhaseMethodName: { + value: function(capture, eventType, targetIdentifier) { + return capture + ? this.methodNameForCapturePhaseOfEventType(eventType, targetIdentifier) + : this.methodNameForBubblePhaseOfEventType(eventType, targetIdentifier); + } + }, + /** * Ensure that any components associated with DOM elements in the hierarchy between the From 5acddbd02334d79a6085b840d3e4ac8d3e372eaf Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 9 Feb 2021 11:10:40 -0800 Subject: [PATCH 403/407] Remove debug log --- core/event/event-manager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/event/event-manager.js b/core/event/event-manager.js index f3bd3505a6..73a2a949f1 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -1416,10 +1416,10 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan optionsOrUseCapture.passive; } - else { - console.log(target.toString(),"registerTargetEventListener "+eventType); + // else { + // console.log(target.toString(),"registerTargetEventListener "+eventType); - } + // } // } listenerOptions.listener = listener; From 3a325163a2f6a05a3066adf9c305a342a2055de4 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 9 Feb 2021 11:11:24 -0800 Subject: [PATCH 404/407] Fix regression --- core/range-controller.js | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/core/range-controller.js b/core/range-controller.js index c9ed3e49ef..dbac34fc48 100644 --- a/core/range-controller.js +++ b/core/range-controller.js @@ -148,7 +148,6 @@ Object.defineProperty(_RangeSelection.prototype, "swap_or_push", { // }, this); -<<<<<<< HEAD plus = []; for(var indexInSelection, i=0, countI = itemsToAdd.length;(i index) { - return false; - } - - // if the item is already in the selection, don't add it - // unless it's in the part that we're about to delete. - var indexInSelection = this.findValue(item); - return indexInSelection < 0 || - (indexInSelection >= start && indexInSelection < start + minusLength); ->>>>>>> fix deprecation warnings from methods in collection's array additions } else { @@ -791,16 +778,6 @@ var RangeController = exports.RangeController = Montage.specialize( /** @lends R if (this.selection.length) { this.selection.deleteEach(diff); -<<<<<<< HEAD -======= - - // ensure selection always has content - if (this.selection.length === 0 && this.content && this.content.length && - this.avoidsEmptySelection && !this.allowsMultipleSelection) { - // selection can't contain previous content value as content already changed - this.selection.add(this.content[this.content.length - 1]); - } ->>>>>>> Fix a bug in handleOrganizedContentRangeChange } } From 18f375d1abcb71bd26a4df040424a27ecf927019 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 9 Feb 2021 14:55:46 -0800 Subject: [PATCH 405/407] delay DragManager event listenening until there's at least a drag source and a drag destination registered --- core/drag/drag-manager.js | 77 +++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/core/drag/drag-manager.js b/core/drag/drag-manager.js index 97a5d5ef47..5e452cc437 100644 --- a/core/drag/drag-manager.js +++ b/core/drag/drag-manager.js @@ -31,6 +31,22 @@ var DragManager = exports.DragManager = Montage.specialize({ } }, + _shouldListenForEvents: { + value: false + }, + + shouldListenForEvents: { + get: function() { + return this._shouldListenForEvents; + }, + set: function(value) { + if(value !== this._shouldListenForEvents) { + this._shouldListenForEvents = value; + this._registerEventsIfNeeded(); + } + } + }, + __rootComponent: { value: null }, @@ -44,11 +60,12 @@ var DragManager = exports.DragManager = Montage.specialize({ ); } - if (component) { - component.addComposerForElement( - this._translateComposer, component.element - ); - } + // if (component) { + // component.addComposerForElement( + // this._translateComposer, component.element + // ); + // } + this._eventsRegistered = false; this.__rootComponent = component; } @@ -132,21 +149,42 @@ var DragManager = exports.DragManager = Montage.specialize({ DragManager.cssTransform = "transform"; } - if (window.PointerEvent) { - element.addEventListener("pointerdown", this, true); - } else if (window.MSPointerEvent && window.navigator.msPointerEnabled) { - element.addEventListener("MSPointerDown", this, true); - } else { - element.addEventListener("touchstart", this, true); - } - - this._translateComposer.addEventListener("translateStart", this); - element.addEventListener("dragenter", this, true); + this._registerEventsIfNeeded(); return this; } }, + _eventsRegistered: { + value: false + }, + _registerEventsIfNeeded: { + value: function() { + + if(this._rootComponent && this._shouldListenForEvents && !this._eventsRegistered) { + var element = this._rootComponent.element; + + if (window.PointerEvent) { + element.addEventListener("pointerdown", this, true); + } else if (window.MSPointerEvent && window.navigator.msPointerEnabled) { + element.addEventListener("MSPointerDown", this, true); + } else { + element.addEventListener("touchstart", this, true); + } + + this._translateComposer.addEventListener("translateStart", this); + element.addEventListener("dragenter", this, true); + + this._rootComponent.addComposerForElement( + this._translateComposer, element + ); + + this._eventsRegistered = true; + } + + } + }, + /** * @public * @function @@ -215,6 +253,15 @@ var DragManager = exports.DragManager = Montage.specialize({ if (components.indexOf(component) === -1) { components.push(component); } + + /* + There's only something to do if there's at l est a source and a destination + */ + if(this._draggables.length > 0 && this._droppables.length > 0) { + this.shouldListenForEvents = true + } else { + this.shouldListenForEvents = false; + } } } }, From f20960beb032b25d592319a4dbfa08662b93e699 Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Tue, 9 Feb 2021 14:58:40 -0800 Subject: [PATCH 406/407] Further code optimization --- core/event/event-manager.js | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/core/event/event-manager.js b/core/event/event-manager.js index 73a2a949f1..a703cfb825 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -28,7 +28,8 @@ var Montage = require("../core").Montage, Event_BUBBLING_PHASE = 3, defaultEventManager, browserSupportsCaptureOption = false, - browserSupportsPassiveOption = false; + browserSupportsPassiveOption = false, + MontageElement = window ? window.MontageElement : null; @@ -3069,11 +3070,11 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan // console.log("handleEvent "+event.type,event.target); // } //console.log("----> handleEvent "+event.type); - if(isBrowser) { - if ((window.MontageElement && event.target instanceof MontageElement) || - (event instanceof UIEvent && !this._shouldDispatchEvent(event))) { + if(isBrowser && ( + (MontageElement && event.target instanceof MontageElement) || + (event instanceof UIEvent && !this._shouldDispatchEvent(event)) + )) { return void 0; - } } // performance.mark('event-manager:handleEvent:start'); @@ -3094,18 +3095,18 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan eventType = event.type, // capitalizedEventType = eventType.toCapitalized(), eventBubbles = event.bubbles, - captureMethodName, - bubbleMethodName, - identifierSpecificCaptureMethodName, - identifierSpecificBubbleMethodName, - currentTargetIdentifierSpecificCaptureMethodName, - currentTargetIdentifierSpecificBubbleMethodName, - capitalizedIdentifier, + // captureMethodName, + // bubbleMethodName, + // identifierSpecificCaptureMethodName, + // identifierSpecificBubbleMethodName, + // currentTargetIdentifierSpecificCaptureMethodName, + // currentTargetIdentifierSpecificBubbleMethodName, + // capitalizedIdentifier, mutableEvent, mutableEventTarget, _currentDispatchedTargetListeners = this._currentDispatchedTargetListeners, - registeredCaptureEventListeners = this._registeredCaptureEventListeners, - registeredBubbleEventListeners = this._registeredBubbleEventListeners, + // registeredCaptureEventListeners = this._registeredCaptureEventListeners, + // registeredBubbleEventListeners = this._registeredBubbleEventListeners, //New stuff CAPTURING_PHASE = Event_CAPTURING_PHASE, @@ -3171,8 +3172,10 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan // Capture Phase Distribution mutableEvent.eventPhase = CAPTURING_PHASE; // The event path we generate is from bottom to top, capture needs to traverse this backwards - for (i = eventPath.length - 1; !mutableEvent.propagationStopped && (iTarget = eventPath[i]); i--) { - mutableEvent.currentTarget = iTarget; + i = eventPath.length; + while (!mutableEvent.propagationStopped && (iTarget = eventPath[--i])) { + //for (i = eventPath.length - 1; !mutableEvent.propagationStopped && (iTarget = eventPath[i]); i--) { + mutableEvent.currentTarget = iTarget; listenerEntries = this._registeredEventListenersOnTarget_eventType_eventPhase(iTarget, eventType, CAPTURING_PHASE); if (!listenerEntries) { @@ -3325,7 +3328,7 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan //TEST, shutting currentTargetIdentifierSpecificPhaseMethodName down: - currentTargetIdentifierSpecificPhaseMethodName = null; + //currentTargetIdentifierSpecificPhaseMethodName = null; if(typeof listener === this._functionType) { listener.call(iTarget, mutableEvent); From 7f2ffdfae314ec58eba6330207d23562fe42a3aa Mon Sep 17 00:00:00 2001 From: Benoit Marchant Date: Wed, 5 Apr 2023 10:12:21 -0700 Subject: [PATCH 407/407] Update LICENSE.md --- LICENSE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/LICENSE.md b/LICENSE.md index 39beb40142..95073ff097 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,3 +1,6 @@ +ALL CHANGES IN THIS FORK ARE NOT OPEN SOURCE NOR AVAILABLE UNDER THE ORIGINAL LICENSE + + Montage ============ Copyright (c) 2013, Adam Solove