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 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..7b87cdf580 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,80 @@ +{ + // 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", + "runtimeVersion": "12.18.4", + "runtimeArgs": [ + "${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.18.4/bin/node", + "program": "${workspaceFolder}/core/mr/test/run-node.js" + }, + { + "type": "node", + "request": "launch", + "name": "compile time zones", + "runtimeVersion": "default", + "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.18.4/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/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 diff --git a/composer/composer.js b/composer/composer.js index e962d2b703..5d0a88f80b 100644 --- a/composer/composer.js +++ b/composer/composer.js @@ -3,7 +3,7 @@ * @requires montage/core/target */ var Target = require("../core/target").Target, - defaultEventManager = require("../core/event/event-manager").defaultEventManager; + defaultEventManager = require("../core/event/event-manager").defaultEventManager; /** * The `Composer` helps to keep event normalization and calculation out of @@ -70,6 +70,36 @@ exports.Composer = Target.specialize( /** @lends Composer# */ { } }, + /** + * @private + * @property {Target} value + */ + _nextTarget: { + value: null + }, + + /** + * The next Target to consider in the event target chain + * + * Currently, components themselves do not allow this chain to be broken; + * setting a component's nextTarget to a falsy value will cause nextTarget + * to resolve as the parentComponent. + * + * To interrupt the propagation path a Target that accepts a falsy + * nextTarget needs to be set at a component's nextTarget. + * + * @param {Target} value + * @returns {Target} + */ + nextTarget: { + get: function () { + return this._nextTarget || this._component; + }, + set: function (value) { + this._nextTarget = value; + } + }, + _shawdowRoot: { value: null }, diff --git a/core/application.js b/core/application.js index 89bdacc76c..e036b047f0 100644 --- a/core/application.js +++ b/core/application.js @@ -13,6 +13,10 @@ var Target = require("./target").Target, Template = require("./template"), MontageWindow = require("../window-loader/montage-window").MontageWindow, + Criteria = require("core/criteria").Criteria, + DataQuery = require("data/model/data-query").DataQuery, + UserIdentityService = undefined, + UserIdentityManager = require("data/service/user-identity-manager").UserIdentityManager, Slot; require("./dom"); @@ -32,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. * @@ -67,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. * @@ -428,6 +429,8 @@ var Application = exports.Application = Target.specialize( /** @lends Applicatio ) { this.parentApplication = window.loadInfo.parent.document.application; } + + UserIdentityManager.delegate = this; } }, @@ -443,14 +446,86 @@ 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 + */ + + //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, + authenticationPromise, + // userObjectDescriptor = this. + selfUserCriteria, + userIdentityQuery; - rootComponent = exports.__root__; + //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") { + // 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/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/.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..eb7c7085ec --- /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..1d9a62819f --- /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": "file:../../.." + }, + "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..305a686a64 --- /dev/null +++ b/core/collections/test/spec/array-spec.js @@ -0,0 +1,341 @@ +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"); +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..5f3718986a --- /dev/null +++ b/core/collections/test/spec/clone-spec.js @@ -0,0 +1,21 @@ + +var Set = require("montage/core/collections/set"); +var Map = require("montage/core/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..070d062ce2 --- /dev/null +++ b/core/collections/test/spec/deque-fuzz.js @@ -0,0 +1,92 @@ + +var Deque = require("montage/core/collections/deque"); +require("montage/core/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..17b41b9fcc --- /dev/null +++ b/core/collections/test/spec/deque-spec.js @@ -0,0 +1,123 @@ + +var Deque = require("montage/core/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..7fac8cb72b --- /dev/null +++ b/core/collections/test/spec/dict-spec.js @@ -0,0 +1,27 @@ + +var Dict = require("montage/core/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..f0358840c3 --- /dev/null +++ b/core/collections/test/spec/fast-map-spec.js @@ -0,0 +1,12 @@ + +var FastMap = require("montage/core/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..1411dd1351 --- /dev/null +++ b/core/collections/test/spec/fast-set-spec.js @@ -0,0 +1,172 @@ +"use strict"; + +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"); +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..c94ea764c0 --- /dev/null +++ b/core/collections/test/spec/heap-spec.js @@ -0,0 +1,107 @@ + +var Heap = require("montage/core/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..7c0fcf0af1 --- /dev/null +++ b/core/collections/test/spec/iterator-spec.js @@ -0,0 +1,649 @@ + +var Iterator = require("montage/core/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..614d3a41c5 --- /dev/null +++ b/core/collections/test/spec/lfu-map-spec.js @@ -0,0 +1,85 @@ + +var LfuMap = require("montage/core/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..4843d29b85 --- /dev/null +++ b/core/collections/test/spec/lfu-set-spec.js @@ -0,0 +1,60 @@ +var LfuSet = require("montage/core/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..97d4efdd3c --- /dev/null +++ b/core/collections/test/spec/list-spec.js @@ -0,0 +1,214 @@ + +var List = require("montage/core/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..47e655cec9 --- /dev/null +++ b/core/collections/test/spec/listen/array-changes-spec.js @@ -0,0 +1,559 @@ + +require("montage/core/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..dc90991a94 --- /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("montage/core/collections/shim"); +var PropertyChanges = require("montage/core/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..d6fe700a7b --- /dev/null +++ b/core/collections/test/spec/lru-map-spec.js @@ -0,0 +1,84 @@ + +var LruMap = require("montage/core/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..5dd864a39b --- /dev/null +++ b/core/collections/test/spec/lru-set-spec.js @@ -0,0 +1,55 @@ + +var LruSet = require("montage/core/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..fd19e5d480 --- /dev/null +++ b/core/collections/test/spec/map-spec.js @@ -0,0 +1,15 @@ +// TODO test insertion order + +var Map = require("montage/core/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..4a324444f9 --- /dev/null +++ b/core/collections/test/spec/order.js @@ -0,0 +1,377 @@ + +var GenericCollection = require("montage/core/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..5737ced76b --- /dev/null +++ b/core/collections/test/spec/regexp-spec.js @@ -0,0 +1,34 @@ + +require("montage/core/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..5e0f852cb7 --- /dev/null +++ b/core/collections/test/spec/set-spec.js @@ -0,0 +1,258 @@ + +var Set = require("montage/core/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..23d9790fd3 --- /dev/null +++ b/core/collections/test/spec/set.js @@ -0,0 +1,134 @@ + +var Iterator = require("montage/core/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..a4a6971e05 --- /dev/null +++ b/core/collections/test/spec/shim-array-spec.js @@ -0,0 +1,51 @@ + +require("montage/core/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..d154170793 --- /dev/null +++ b/core/collections/test/spec/shim-functions-spec.js @@ -0,0 +1,49 @@ + +require("montage/core/collections/shim-object"); +require("montage/core/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..e9bc1d3f64 --- /dev/null +++ b/core/collections/test/spec/shim-object-spec.js @@ -0,0 +1,608 @@ +"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("montage/core/collections/shim"); +var Dict = require("montage/core/collections/dict"); +var Set = require("montage/core/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); + }); + }); + }); + }); + }); + + 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 () { + + 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..31169c85a6 --- /dev/null +++ b/core/collections/test/spec/sorted-array-map-spec.js @@ -0,0 +1,14 @@ + +var SortedArrayMap = require("montage/core/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..e49844fc1d --- /dev/null +++ b/core/collections/test/spec/sorted-array-set-spec.js @@ -0,0 +1,20 @@ + +var SortedArraySet = require("montage/core/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..18221d608c --- /dev/null +++ b/core/collections/test/spec/sorted-array-spec.js @@ -0,0 +1,95 @@ + +var SortedArray = require("montage/core/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..a0037bbcf9 --- /dev/null +++ b/core/collections/test/spec/sorted-map-spec.js @@ -0,0 +1,43 @@ + +var SortedMap = require("montage/core/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..ebd7efc548 --- /dev/null +++ b/core/collections/test/spec/sorted-set-spec.js @@ -0,0 +1,457 @@ + +require("montage/core/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..0d0631d29c --- /dev/null +++ b/core/collections/weak-map.js @@ -0,0 +1,2 @@ + +module.exports = WeakMap; 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..deda1ca39e --- /dev/null +++ b/core/converter/RFC3339UTC-range-string-to-range-converter.js @@ -0,0 +1,73 @@ +/** + * @module montage/core/converter/RFC3339UTC-range-string-to-range-converter + * @requires montage/core/converter/converter + */ +var Converter = require("./converter").Converter, + 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. + */ +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); + 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" + } + } + } +} 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" + } + } + } +} 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" + } + } + } +} 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/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; diff --git a/core/converter/expression-converter.js b/core/converter/expression-converter.js index 5a9e1840db..eca91e6fab 100644 --- a/core/converter/expression-converter.js +++ b/core/converter/expression-converter.js @@ -3,9 +3,10 @@ * @requires montage/core/converter/converter */ var Converter = require("./converter").Converter, - parse = require("frb/parse"), - Scope = require("frb/scope"), - compile = require("frb/compile-evaluator"); + parse = require("core/frb/parse"), + Scope = require("core/frb/scope"), + compile = require("core/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); } } 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..3a2ed8ec1a --- /dev/null +++ b/core/converter/international-date-to-string-formatter.js @@ -0,0 +1,68 @@ +/** + * @module montage/core/converter/international-date-to-string-formatter + * @requires montage/core/converter/converter + */ +var Converter = require("./converter").Converter, + Locale = require("../locale").Locale; + +/** + * Formats a Date to a String using standard Intl.DateTimeFormat. + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat + * + * @class InternationalDateToStringFormatter + * @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 v ? this._dayDateFormatter.format(v) : ""; + } + } +}); 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||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 = +CalendarDatePrototype.getUTCDate = +CalendarDatePrototype.getUTCDay = +CalendarDatePrototype.getUTCFullYear = +CalendarDatePrototype.getUTCHours = +CalendarDatePrototype.getUTCMilliseconds() +CalendarDatePrototype.getUTCMinutes() +CalendarDatePrototype.getUTCMonth() +CalendarDatePrototype.getUTCSeconds() +CalendarDatePrototype.getYear() +CalendarDatePrototype.setDate() +CalendarDatePrototype.setFullYear() +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.toLocaleTimeString() +CalendarDatePrototype.toSource() +CalendarDatePrototype.toString() +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/calendar.js b/core/date/calendar.js new file mode 100644 index 0000000000..b8419faeb3 --- /dev/null +++ b/core/date/calendar.js @@ -0,0 +1,225 @@ +var Montage = require("../core").Montage, + Enum = require("../enum").Enum, + //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/calendar.mjson b/core/date/calendar.mjson new file mode 100644 index 0000000000..dccfb46a91 --- /dev/null +++ b/core/date/calendar.mjson @@ -0,0 +1,68 @@ +{ + "root": { + "prototype": "core/meta/module-object-descriptor", + "values": { + "name": "Calendar", + "propertyDescriptors": [ + { "@": "identifier" }, + { "@": "location" }, + { "@": "tznames" }, + { "@": "latitude" }, + { "@": "longitude" } + ], + "propertyDescriptorGroups": { + "all": [ + { "@": "identifier" }, + { "@": "location" }, + { "@": "tznames" }, + { "@": "latitude" }, + { "@": "longitude" } + ] + }, + "propertyValidationRules": {}, + "objectDescriptorModule": { "%": "core/date/calendar.mjson" }, + "exportName": "Calendar", + "module": { "%": "core/date/calendar" }, + "modulePrototype":{"@": "CalendarPrototype"}, + "object":{ "@": "CalendarPrototype" } + } + }, + "CalendarPrototype": { + "object": "core/date/calendar" + }, + "identifier": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "identifier", + "valueType": "string" + } + }, + "location": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "location", + "valueType": "string" + } + }, + "tznames": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "tznames", + "valueType": "string" + } + }, + "latitude": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "latitude", + "valueType": "number" + } + }, + "longitude": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "latitude", + "valueType": "number" + } + } +} diff --git a/core/date/ical-expander.js b/core/date/ical-expander.js new file mode 100644 index 0000000000..44b7a378bb --- /dev/null +++ b/core/date/ical-expander.js @@ -0,0 +1,155 @@ +'use strict'; + +/* + + "repository": { + "type": "git", + "url": "git+https://github.com/mifi/ical-expander.git" + }, + "author": "Mikael Finstad ", + "license": "MIT", + +*/ + +const ICAL = require('ical.js'); + +// Copied from https://dxr.mozilla.org/comm-central/source/calendar/timezones/zones.json +// And compiled using node compile-zones.js +// See also https://github.com/mozilla-comm/ical.js/issues/195 +const timezones = require('./zones-compiled.json'); + +class IcalExpander { + constructor(opts) { + this.maxIterations = opts.maxIterations != null ? opts.maxIterations : 1000; + this.skipInvalidDates = opts.skipInvalidDates != null ? opts.skipInvalidDates : false; + + this.jCalData = ICAL.parse(opts.ics); + this.component = new ICAL.Component(this.jCalData); + this.events = this.component.getAllSubcomponents('vevent').map(vevent => new ICAL.Event(vevent)); + + if (this.skipInvalidDates) { + this.events = this.events.filter((evt) => { + try { + evt.startDate.toJSDate(); + evt.endDate.toJSDate(); + return true; + } catch (err) { + // skipping events with invalid time + return false; + } + }); + } + } + + between(after, before) { + function isEventWithinRange(startTime, endTime) { + return (!after || endTime >= after.getTime()) && + (!before || startTime <= before.getTime()); + } + + function getTimes(eventOrOccurrence) { + const startTime = eventOrOccurrence.startDate.toJSDate().getTime(); + let endTime = eventOrOccurrence.endDate.toJSDate().getTime(); + + // If it is an all day event, the end date is set to 00:00 of the next day + // So we need to make it be 23:59:59 to compare correctly with the given range + if (eventOrOccurrence.endDate.isDate && (endTime > startTime)) { + endTime -= 1; + } + + return { startTime, endTime }; + } + + const exceptions = []; + + this.events.forEach((event) => { + if (event.isRecurrenceException()) exceptions.push(event); + }); + + const ret = { + events: [], + occurrences: [], + }; + + this.events.filter(e => !e.isRecurrenceException()).forEach((event) => { + const exdates = []; + + event.component.getAllProperties('exdate').forEach((exdateProp) => { + const exdate = exdateProp.getFirstValue(); + exdates.push(exdate.toJSDate().getTime()); + }); + + // Recurring event is handled differently + if (event.isRecurring()) { + const iterator = event.iterator(); + + let next; + let i = 0; + + do { + i += 1; + next = iterator.next(); + if (next) { + const occurrence = event.getOccurrenceDetails(next); + + const { startTime, endTime } = getTimes(occurrence); + + const isOccurrenceExcluded = exdates.indexOf(startTime) !== -1; + + // TODO check that within same day? + const exception = exceptions.find(ex => ex.uid === event.uid && ex.recurrenceId.toJSDate().getTime() === occurrence.startDate.toJSDate().getTime()); + + // We have passed the max date, stop + if (before && startTime > before.getTime()) break; + + // Check that we are within our range + if (isEventWithinRange(startTime, endTime)) { + if (exception) { + ret.events.push(exception); + } else if (!isOccurrenceExcluded) { + ret.occurrences.push(occurrence); + } + } + } + } + while (next && (!this.maxIterations || i < this.maxIterations)); + + return; + } + + // Non-recurring event: + const { startTime, endTime } = getTimes(event); + + if (isEventWithinRange(startTime, endTime)) ret.events.push(event); + }); + + return ret; + } + + before(before) { + return this.between(undefined, before); + } + + after(after) { + return this.between(after); + } + + all() { + return this.between(); + } +} + +function registerTimezones() { + Object.keys(timezones).forEach((key) => { + const icsData = timezones[key]; + const parsed = ICAL.parse(`BEGIN:VCALENDAR\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\nVERSION:2.0\n${icsData}\nEND:VCALENDAR`); + const comp = new ICAL.Component(parsed); + const vtimezone = comp.getFirstSubcomponent('vtimezone'); + + ICAL.TimezoneService.register(key, new ICAL.Timezone(vtimezone)); + }); +} + +registerTimezones(); + +module.exports = IcalExpander; diff --git a/core/date/time-zone-core.js b/core/date/time-zone-core.js new file mode 100644 index 0000000000..0b17757a19 --- /dev/null +++ b/core/date/time-zone-core.js @@ -0,0 +1,126 @@ +/* + Time Zones definitions at: + + 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, + 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"), + // systemTimeZonesData = require("./time-zone-data/"+Intl.DateTimeFormat(navigator.languages[0]).resolvedOptions().timeZone+".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": { + 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 (this._systemTimeZone = TimeZone.withIdentifier(timeZone)); + } + }, + "systemTimeZone": { + get: function() { + return this._systemTimeZone || this._createSystemTimeZone; + } + } + +}); + +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-data/zones-compiled.json b/core/date/time-zone-data/zones-compiled.json new file mode 100644 index 0000000000..ba90468acb --- /dev/null +++ b/core/date/time-zone-data/zones-compiled.json @@ -0,0 +1 @@ +{"Africa/Abidjan":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Abidjan\r\nX-LIC-LOCATION:Africa/Abidjan\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Accra":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Accra\r\nX-LIC-LOCATION:Africa/Accra\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Addis_Ababa":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Addis_Ababa\r\nX-LIC-LOCATION:Africa/Addis_Ababa\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Algiers":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Algiers\r\nX-LIC-LOCATION:Africa/Algiers\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Asmara":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Asmara\r\nX-LIC-LOCATION:Africa/Asmara\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Bamako":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Bamako\r\nX-LIC-LOCATION:Africa/Bamako\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Bangui":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Bangui\r\nX-LIC-LOCATION:Africa/Bangui\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Banjul":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Banjul\r\nX-LIC-LOCATION:Africa/Banjul\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Bissau":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Bissau\r\nX-LIC-LOCATION:Africa/Bissau\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Blantyre":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Blantyre\r\nX-LIC-LOCATION:Africa/Blantyre\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Brazzaville":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Brazzaville\r\nX-LIC-LOCATION:Africa/Brazzaville\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Bujumbura":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Bujumbura\r\nX-LIC-LOCATION:Africa/Bujumbura\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Cairo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Cairo\r\nX-LIC-LOCATION:Africa/Cairo\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Casablanca":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Casablanca\r\nX-LIC-LOCATION:Africa/Casablanca\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20180325T020000\r\nRDATE:20180325T020000\r\nRDATE:20180617T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:20180513T030000\r\nRDATE:20180513T030000\r\nEND:STANDARD\r\nBEGIN: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\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20181028T030000\r\nRDATE:20181028T030000\r\nEND:STANDARD\r\nBEGIN: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\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Ceuta":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Ceuta\r\nX-LIC-LOCATION:Africa/Ceuta\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Conakry":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Conakry\r\nX-LIC-LOCATION:Africa/Conakry\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Dakar":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Dakar\r\nX-LIC-LOCATION:Africa/Dakar\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Dar_es_Salaam":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Dar_es_Salaam\r\nX-LIC-LOCATION:Africa/Dar_es_Salaam\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Djibouti":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Djibouti\r\nX-LIC-LOCATION:Africa/Djibouti\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Douala":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Douala\r\nX-LIC-LOCATION:Africa/Douala\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/El_Aaiun":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/El_Aaiun\r\nX-LIC-LOCATION:Africa/El_Aaiun\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20180325T020000\r\nRDATE:20180325T020000\r\nRDATE:20180617T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:20180513T030000\r\nRDATE:20180513T030000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20181028T030000\r\nRDATE:20181028T030000\r\nEND:STANDARD\r\nBEGIN: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\r\nBEGIN: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\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Freetown":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Freetown\r\nX-LIC-LOCATION:Africa/Freetown\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Gaborone":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Gaborone\r\nX-LIC-LOCATION:Africa/Gaborone\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Harare":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Harare\r\nX-LIC-LOCATION:Africa/Harare\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Johannesburg":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Johannesburg\r\nX-LIC-LOCATION:Africa/Johannesburg\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:SAST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Juba":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Juba\r\nX-LIC-LOCATION:Africa/Juba\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Kampala":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Kampala\r\nX-LIC-LOCATION:Africa/Kampala\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Khartoum":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Khartoum\r\nX-LIC-LOCATION:Africa/Khartoum\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Kigali":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Kigali\r\nX-LIC-LOCATION:Africa/Kigali\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Kinshasa":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Kinshasa\r\nX-LIC-LOCATION:Africa/Kinshasa\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Lagos":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Lagos\r\nX-LIC-LOCATION:Africa/Lagos\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Libreville":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Libreville\r\nX-LIC-LOCATION:Africa/Libreville\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Lome":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Lome\r\nX-LIC-LOCATION:Africa/Lome\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Luanda":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Luanda\r\nX-LIC-LOCATION:Africa/Luanda\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Lubumbashi":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Lubumbashi\r\nX-LIC-LOCATION:Africa/Lubumbashi\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Lusaka":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Lusaka\r\nX-LIC-LOCATION:Africa/Lusaka\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Malabo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Malabo\r\nX-LIC-LOCATION:Africa/Malabo\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Maputo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Maputo\r\nX-LIC-LOCATION:Africa/Maputo\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Maseru":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Maseru\r\nX-LIC-LOCATION:Africa/Maseru\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:SAST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Mbabane":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Mbabane\r\nX-LIC-LOCATION:Africa/Mbabane\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:SAST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Mogadishu":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Mogadishu\r\nX-LIC-LOCATION:Africa/Mogadishu\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Monrovia":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Monrovia\r\nX-LIC-LOCATION:Africa/Monrovia\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Nairobi":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Nairobi\r\nX-LIC-LOCATION:Africa/Nairobi\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Ndjamena":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Ndjamena\r\nX-LIC-LOCATION:Africa/Ndjamena\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Niamey":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Niamey\r\nX-LIC-LOCATION:Africa/Niamey\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Nouakchott":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Nouakchott\r\nX-LIC-LOCATION:Africa/Nouakchott\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Ouagadougou":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Ouagadougou\r\nX-LIC-LOCATION:Africa/Ouagadougou\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Porto-Novo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Porto-Novo\r\nX-LIC-LOCATION:Africa/Porto-Novo\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Sao_Tome":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Sao_Tome\r\nX-LIC-LOCATION:Africa/Sao_Tome\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:20180101T010000\r\nRDATE:20180101T010000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:20190101T020000\r\nRDATE:20190101T020000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Tripoli":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Tripoli\r\nX-LIC-LOCATION:Africa/Tripoli\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Tunis":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Tunis\r\nX-LIC-LOCATION:Africa/Tunis\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Windhoek":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Windhoek\r\nX-LIC-LOCATION:Africa/Windhoek\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Adak":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Adak\r\nX-LIC-LOCATION:America/Adak\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-1000\r\nTZOFFSETTO:-0900\r\nTZNAME:HDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-1000\r\nTZNAME:HST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Anchorage":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Anchorage\r\nX-LIC-LOCATION:America/Anchorage\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Anguilla":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Anguilla\r\nX-LIC-LOCATION:America/Anguilla\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Antigua":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Antigua\r\nX-LIC-LOCATION:America/Antigua\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Araguaina":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Araguaina\r\nX-LIC-LOCATION:America/Araguaina\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Argentina/Buenos_Aires":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Argentina/Buenos_Aires\r\nX-LIC-LOCATION:America/Argentina/Buenos_Aires\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Argentina/Catamarca":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Argentina/Catamarca\r\nX-LIC-LOCATION:America/Argentina/Catamarca\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Argentina/Cordoba":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Argentina/Cordoba\r\nX-LIC-LOCATION:America/Argentina/Cordoba\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Argentina/Jujuy":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Argentina/Jujuy\r\nX-LIC-LOCATION:America/Argentina/Jujuy\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Argentina/La_Rioja":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Argentina/La_Rioja\r\nX-LIC-LOCATION:America/Argentina/La_Rioja\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Argentina/Mendoza":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Argentina/Mendoza\r\nX-LIC-LOCATION:America/Argentina/Mendoza\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Argentina/Rio_Gallegos":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Argentina/Rio_Gallegos\r\nX-LIC-LOCATION:America/Argentina/Rio_Gallegos\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Argentina/Salta":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Argentina/Salta\r\nX-LIC-LOCATION:America/Argentina/Salta\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Argentina/San_Juan":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Argentina/San_Juan\r\nX-LIC-LOCATION:America/Argentina/San_Juan\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Argentina/San_Luis":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Argentina/San_Luis\r\nX-LIC-LOCATION:America/Argentina/San_Luis\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Argentina/Tucuman":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Argentina/Tucuman\r\nX-LIC-LOCATION:America/Argentina/Tucuman\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Argentina/Ushuaia":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Argentina/Ushuaia\r\nX-LIC-LOCATION:America/Argentina/Ushuaia\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Aruba":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Aruba\r\nX-LIC-LOCATION:America/Aruba\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Asuncion":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Asuncion\r\nX-LIC-LOCATION:America/Asuncion\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19701004T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700322T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=4SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Atikokan":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Atikokan\r\nX-LIC-LOCATION:America/Atikokan\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Bahia":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Bahia\r\nX-LIC-LOCATION:America/Bahia\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Bahia_Banderas":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Bahia_Banderas\r\nX-LIC-LOCATION:America/Bahia_Banderas\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Barbados":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Barbados\r\nX-LIC-LOCATION:America/Barbados\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Belem":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Belem\r\nX-LIC-LOCATION:America/Belem\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Belize":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Belize\r\nX-LIC-LOCATION:America/Belize\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Blanc-Sablon":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Blanc-Sablon\r\nX-LIC-LOCATION:America/Blanc-Sablon\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Boa_Vista":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Boa_Vista\r\nX-LIC-LOCATION:America/Boa_Vista\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Bogota":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Bogota\r\nX-LIC-LOCATION:America/Bogota\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Boise":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Boise\r\nX-LIC-LOCATION:America/Boise\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Cambridge_Bay":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Cambridge_Bay\r\nX-LIC-LOCATION:America/Cambridge_Bay\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Campo_Grande":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Campo_Grande\r\nX-LIC-LOCATION:America/Campo_Grande\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20181104T000000\r\nRDATE:20181104T000000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20180218T000000\r\nRDATE:20180218T000000\r\nRDATE:20190217T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Cancun":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Cancun\r\nX-LIC-LOCATION:America/Cancun\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Caracas":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Caracas\r\nX-LIC-LOCATION:America/Caracas\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Cayenne":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Cayenne\r\nX-LIC-LOCATION:America/Cayenne\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Cayman":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Cayman\r\nX-LIC-LOCATION:America/Cayman\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Chicago":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Chicago\r\nX-LIC-LOCATION:America/Chicago\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Chihuahua":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Chihuahua\r\nX-LIC-LOCATION:America/Chihuahua\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Costa_Rica":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Costa_Rica\r\nX-LIC-LOCATION:America/Costa_Rica\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Creston":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Creston\r\nX-LIC-LOCATION:America/Creston\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Cuiaba":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Cuiaba\r\nX-LIC-LOCATION:America/Cuiaba\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20181104T000000\r\nRDATE:20181104T000000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20180218T000000\r\nRDATE:20180218T000000\r\nRDATE:20190217T000000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Curacao":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Curacao\r\nX-LIC-LOCATION:America/Curacao\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Danmarkshavn":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Danmarkshavn\r\nX-LIC-LOCATION:America/Danmarkshavn\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Dawson":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Dawson\r\nX-LIC-LOCATION:America/Dawson\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Dawson_Creek":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Dawson_Creek\r\nX-LIC-LOCATION:America/Dawson_Creek\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Denver":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Denver\r\nX-LIC-LOCATION:America/Denver\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Detroit":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Detroit\r\nX-LIC-LOCATION:America/Detroit\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Dominica":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Dominica\r\nX-LIC-LOCATION:America/Dominica\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Edmonton":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Edmonton\r\nX-LIC-LOCATION:America/Edmonton\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Eirunepe":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Eirunepe\r\nX-LIC-LOCATION:America/Eirunepe\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/El_Salvador":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/El_Salvador\r\nX-LIC-LOCATION:America/El_Salvador\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Fort_Nelson":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Fort_Nelson\r\nX-LIC-LOCATION:America/Fort_Nelson\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Fortaleza":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Fortaleza\r\nX-LIC-LOCATION:America/Fortaleza\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Glace_Bay":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Glace_Bay\r\nX-LIC-LOCATION:America/Glace_Bay\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:ADT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Godthab":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Godthab\r\nX-LIC-LOCATION:America/Godthab\r\nBEGIN: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\r\nBEGIN: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\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Goose_Bay":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Goose_Bay\r\nX-LIC-LOCATION:America/Goose_Bay\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:ADT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Grand_Turk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Grand_Turk\r\nX-LIC-LOCATION:America/Grand_Turk\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:20181104T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:20190310T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:20180311T020000\r\nRDATE:20180311T020000\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Grenada":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Grenada\r\nX-LIC-LOCATION:America/Grenada\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Guadeloupe":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Guadeloupe\r\nX-LIC-LOCATION:America/Guadeloupe\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Guatemala":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Guatemala\r\nX-LIC-LOCATION:America/Guatemala\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Guayaquil":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Guayaquil\r\nX-LIC-LOCATION:America/Guayaquil\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Guyana":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Guyana\r\nX-LIC-LOCATION:America/Guyana\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Halifax":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Halifax\r\nX-LIC-LOCATION:America/Halifax\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:ADT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Havana":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Havana\r\nX-LIC-LOCATION:America/Havana\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:CST\r\nDTSTART:19701101T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:CDT\r\nDTSTART:19700308T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Hermosillo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Hermosillo\r\nX-LIC-LOCATION:America/Hermosillo\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Indiana/Indianapolis":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Indiana/Indianapolis\r\nX-LIC-LOCATION:America/Indiana/Indianapolis\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Indiana/Knox":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Indiana/Knox\r\nX-LIC-LOCATION:America/Indiana/Knox\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Indiana/Marengo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Indiana/Marengo\r\nX-LIC-LOCATION:America/Indiana/Marengo\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Indiana/Petersburg":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Indiana/Petersburg\r\nX-LIC-LOCATION:America/Indiana/Petersburg\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Indiana/Tell_City":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Indiana/Tell_City\r\nX-LIC-LOCATION:America/Indiana/Tell_City\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Indiana/Vevay":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Indiana/Vevay\r\nX-LIC-LOCATION:America/Indiana/Vevay\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Indiana/Vincennes":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Indiana/Vincennes\r\nX-LIC-LOCATION:America/Indiana/Vincennes\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Indiana/Winamac":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Indiana/Winamac\r\nX-LIC-LOCATION:America/Indiana/Winamac\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Inuvik":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Inuvik\r\nX-LIC-LOCATION:America/Inuvik\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Iqaluit":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Iqaluit\r\nX-LIC-LOCATION:America/Iqaluit\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Jamaica":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Jamaica\r\nX-LIC-LOCATION:America/Jamaica\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Juneau":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Juneau\r\nX-LIC-LOCATION:America/Juneau\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Kentucky/Louisville":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Kentucky/Louisville\r\nX-LIC-LOCATION:America/Kentucky/Louisville\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Kentucky/Monticello":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Kentucky/Monticello\r\nX-LIC-LOCATION:America/Kentucky/Monticello\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Kralendijk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Kralendijk\r\nX-LIC-LOCATION:America/Kralendijk\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/La_Paz":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/La_Paz\r\nX-LIC-LOCATION:America/La_Paz\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Lima":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Lima\r\nX-LIC-LOCATION:America/Lima\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Los_Angeles":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Los_Angeles\r\nX-LIC-LOCATION:America/Los_Angeles\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Lower_Princes":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Lower_Princes\r\nX-LIC-LOCATION:America/Lower_Princes\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Maceio":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Maceio\r\nX-LIC-LOCATION:America/Maceio\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Managua":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Managua\r\nX-LIC-LOCATION:America/Managua\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Manaus":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Manaus\r\nX-LIC-LOCATION:America/Manaus\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Marigot":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Marigot\r\nX-LIC-LOCATION:America/Marigot\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Martinique":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Martinique\r\nX-LIC-LOCATION:America/Martinique\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Matamoros":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Matamoros\r\nX-LIC-LOCATION:America/Matamoros\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Mazatlan":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Mazatlan\r\nX-LIC-LOCATION:America/Mazatlan\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Menominee":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Menominee\r\nX-LIC-LOCATION:America/Menominee\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Merida":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Merida\r\nX-LIC-LOCATION:America/Merida\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Metlakatla":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Metlakatla\r\nX-LIC-LOCATION:America/Metlakatla\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:20191103T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:20181104T020000\r\nRDATE:20181104T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:20190120T020000\r\nRDATE:20190120T020000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Mexico_City":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Mexico_City\r\nX-LIC-LOCATION:America/Mexico_City\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Miquelon":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Miquelon\r\nX-LIC-LOCATION:America/Miquelon\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Moncton":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Moncton\r\nX-LIC-LOCATION:America/Moncton\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:ADT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Monterrey":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Monterrey\r\nX-LIC-LOCATION:America/Monterrey\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Montevideo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Montevideo\r\nX-LIC-LOCATION:America/Montevideo\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Montserrat":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Montserrat\r\nX-LIC-LOCATION:America/Montserrat\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Nassau":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Nassau\r\nX-LIC-LOCATION:America/Nassau\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/New_York":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/New_York\r\nX-LIC-LOCATION:America/New_York\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Nipigon":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Nipigon\r\nX-LIC-LOCATION:America/Nipigon\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Nome":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Nome\r\nX-LIC-LOCATION:America/Nome\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Noronha":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Noronha\r\nX-LIC-LOCATION:America/Noronha\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/North_Dakota/Beulah":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/North_Dakota/Beulah\r\nX-LIC-LOCATION:America/North_Dakota/Beulah\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/North_Dakota/Center":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/North_Dakota/Center\r\nX-LIC-LOCATION:America/North_Dakota/Center\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/North_Dakota/New_Salem":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/North_Dakota/New_Salem\r\nX-LIC-LOCATION:America/North_Dakota/New_Salem\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Ojinaga":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Ojinaga\r\nX-LIC-LOCATION:America/Ojinaga\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Panama":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Panama\r\nX-LIC-LOCATION:America/Panama\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Pangnirtung":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Pangnirtung\r\nX-LIC-LOCATION:America/Pangnirtung\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Paramaribo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Paramaribo\r\nX-LIC-LOCATION:America/Paramaribo\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Phoenix":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Phoenix\r\nX-LIC-LOCATION:America/Phoenix\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Port-au-Prince":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Port-au-Prince\r\nX-LIC-LOCATION:America/Port-au-Prince\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Port_of_Spain":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Port_of_Spain\r\nX-LIC-LOCATION:America/Port_of_Spain\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Porto_Velho":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Porto_Velho\r\nX-LIC-LOCATION:America/Porto_Velho\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Puerto_Rico":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Puerto_Rico\r\nX-LIC-LOCATION:America/Puerto_Rico\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Punta_Arenas":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Punta_Arenas\r\nX-LIC-LOCATION:America/Punta_Arenas\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Rainy_River":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Rainy_River\r\nX-LIC-LOCATION:America/Rainy_River\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Rankin_Inlet":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Rankin_Inlet\r\nX-LIC-LOCATION:America/Rankin_Inlet\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Recife":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Recife\r\nX-LIC-LOCATION:America/Recife\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Regina":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Regina\r\nX-LIC-LOCATION:America/Regina\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Resolute":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Resolute\r\nX-LIC-LOCATION:America/Resolute\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Rio_Branco":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Rio_Branco\r\nX-LIC-LOCATION:America/Rio_Branco\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Santarem":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Santarem\r\nX-LIC-LOCATION:America/Santarem\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Santiago":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Santiago\r\nX-LIC-LOCATION:America/Santiago\r\nBEGIN: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\r\nBEGIN: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\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20180812T000000\r\nRDATE:20180812T000000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20180513T000000\r\nRDATE:20180513T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Santo_Domingo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Santo_Domingo\r\nX-LIC-LOCATION:America/Santo_Domingo\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Sao_Paulo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Sao_Paulo\r\nX-LIC-LOCATION:America/Sao_Paulo\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:20181104T000000\r\nRDATE:20181104T000000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20180218T000000\r\nRDATE:20180218T000000\r\nRDATE:20190217T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:19700101T000000\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Scoresbysund":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Scoresbysund\r\nX-LIC-LOCATION:America/Scoresbysund\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19700329T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:-0100\r\nTZNAME:-01\r\nDTSTART:19701025T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Sitka":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Sitka\r\nX-LIC-LOCATION:America/Sitka\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/St_Barthelemy":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/St_Barthelemy\r\nX-LIC-LOCATION:America/St_Barthelemy\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/St_Johns":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/St_Johns\r\nX-LIC-LOCATION:America/St_Johns\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0230\r\nTZOFFSETTO:-0330\r\nTZNAME:NST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0330\r\nTZOFFSETTO:-0230\r\nTZNAME:NDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/St_Kitts":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/St_Kitts\r\nX-LIC-LOCATION:America/St_Kitts\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/St_Lucia":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/St_Lucia\r\nX-LIC-LOCATION:America/St_Lucia\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/St_Thomas":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/St_Thomas\r\nX-LIC-LOCATION:America/St_Thomas\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/St_Vincent":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/St_Vincent\r\nX-LIC-LOCATION:America/St_Vincent\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Swift_Current":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Swift_Current\r\nX-LIC-LOCATION:America/Swift_Current\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Tegucigalpa":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Tegucigalpa\r\nX-LIC-LOCATION:America/Tegucigalpa\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Thule":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Thule\r\nX-LIC-LOCATION:America/Thule\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:ADT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Thunder_Bay":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Thunder_Bay\r\nX-LIC-LOCATION:America/Thunder_Bay\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Tijuana":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Tijuana\r\nX-LIC-LOCATION:America/Tijuana\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Toronto":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Toronto\r\nX-LIC-LOCATION:America/Toronto\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Tortola":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Tortola\r\nX-LIC-LOCATION:America/Tortola\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Vancouver":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Vancouver\r\nX-LIC-LOCATION:America/Vancouver\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Whitehorse":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Whitehorse\r\nX-LIC-LOCATION:America/Whitehorse\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Winnipeg":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Winnipeg\r\nX-LIC-LOCATION:America/Winnipeg\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Yakutat":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Yakutat\r\nX-LIC-LOCATION:America/Yakutat\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Yellowknife":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Yellowknife\r\nX-LIC-LOCATION:America/Yellowknife\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Antarctica/Casey":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Antarctica/Casey\r\nX-LIC-LOCATION:Antarctica/Casey\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:20180311T040000\r\nRDATE:20180311T040000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Antarctica/Davis":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Antarctica/Davis\r\nX-LIC-LOCATION:Antarctica/Davis\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Antarctica/DumontDUrville":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Antarctica/DumontDUrville\r\nX-LIC-LOCATION:Antarctica/DumontDUrville\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:+10\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Antarctica/Macquarie":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Antarctica/Macquarie\r\nX-LIC-LOCATION:Antarctica/Macquarie\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Antarctica/Mawson":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Antarctica/Mawson\r\nX-LIC-LOCATION:Antarctica/Mawson\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Antarctica/McMurdo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Antarctica/McMurdo\r\nX-LIC-LOCATION:Antarctica/McMurdo\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1300\r\nTZNAME:NZDT\r\nDTSTART:19700927T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1200\r\nTZNAME:NZST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Antarctica/Palmer":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Antarctica/Palmer\r\nX-LIC-LOCATION:Antarctica/Palmer\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Antarctica/Rothera":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Antarctica/Rothera\r\nX-LIC-LOCATION:Antarctica/Rothera\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Antarctica/Syowa":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Antarctica/Syowa\r\nX-LIC-LOCATION:Antarctica/Syowa\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Antarctica/Troll":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Antarctica/Troll\r\nX-LIC-LOCATION:Antarctica/Troll\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0200\r\nTZNAME:+02\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Antarctica/Vostok":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Antarctica/Vostok\r\nX-LIC-LOCATION:Antarctica/Vostok\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Arctic/Longyearbyen":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Arctic/Longyearbyen\r\nX-LIC-LOCATION:Arctic/Longyearbyen\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Aden":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Aden\r\nX-LIC-LOCATION:Asia/Aden\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Almaty":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Almaty\r\nX-LIC-LOCATION:Asia/Almaty\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Amman":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Amman\r\nX-LIC-LOCATION:Asia/Amman\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700326T235959\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1TH\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701030T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1FR\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Anadyr":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Anadyr\r\nX-LIC-LOCATION:Asia/Anadyr\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Aqtau":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Aqtau\r\nX-LIC-LOCATION:Asia/Aqtau\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Aqtobe":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Aqtobe\r\nX-LIC-LOCATION:Asia/Aqtobe\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Ashgabat":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Ashgabat\r\nX-LIC-LOCATION:Asia/Ashgabat\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Atyrau":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Atyrau\r\nX-LIC-LOCATION:Asia/Atyrau\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Baghdad":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Baghdad\r\nX-LIC-LOCATION:Asia/Baghdad\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Bahrain":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Bahrain\r\nX-LIC-LOCATION:Asia/Bahrain\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Baku":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Baku\r\nX-LIC-LOCATION:Asia/Baku\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Bangkok":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Bangkok\r\nX-LIC-LOCATION:Asia/Bangkok\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Barnaul":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Barnaul\r\nX-LIC-LOCATION:Asia/Barnaul\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Beirut":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Beirut\r\nX-LIC-LOCATION:Asia/Beirut\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Bishkek":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Bishkek\r\nX-LIC-LOCATION:Asia/Bishkek\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Brunei":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Brunei\r\nX-LIC-LOCATION:Asia/Brunei\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Chita":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Chita\r\nX-LIC-LOCATION:Asia/Chita\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:+09\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Choibalsan":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Choibalsan\r\nX-LIC-LOCATION:Asia/Choibalsan\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Colombo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Colombo\r\nX-LIC-LOCATION:Asia/Colombo\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0530\r\nTZOFFSETTO:+0530\r\nTZNAME:+0530\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Damascus":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Damascus\r\nX-LIC-LOCATION:Asia/Damascus\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701030T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1FR\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700327T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1FR\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Dhaka":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Dhaka\r\nX-LIC-LOCATION:Asia/Dhaka\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Dili":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Dili\r\nX-LIC-LOCATION:Asia/Dili\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:+09\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Dubai":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Dubai\r\nX-LIC-LOCATION:Asia/Dubai\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Dushanbe":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Dushanbe\r\nX-LIC-LOCATION:Asia/Dushanbe\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Famagusta":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Famagusta\r\nX-LIC-LOCATION:Asia/Famagusta\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:20180325T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Gaza":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Gaza\r\nX-LIC-LOCATION:Asia/Gaza\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701031T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SA\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:20190329T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1FR\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:20180324T010000\r\nRDATE:20180324T010000\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Hebron":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Hebron\r\nX-LIC-LOCATION:Asia/Hebron\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701031T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SA\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:20190329T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1FR\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:20180324T010000\r\nRDATE:20180324T010000\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Ho_Chi_Minh":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Ho_Chi_Minh\r\nX-LIC-LOCATION:Asia/Ho_Chi_Minh\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Hong_Kong":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Hong_Kong\r\nX-LIC-LOCATION:Asia/Hong_Kong\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:HKT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Hovd":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Hovd\r\nX-LIC-LOCATION:Asia/Hovd\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Irkutsk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Irkutsk\r\nX-LIC-LOCATION:Asia/Irkutsk\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Istanbul":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Istanbul\r\nX-LIC-LOCATION:Asia/Istanbul\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Jakarta":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Jakarta\r\nX-LIC-LOCATION:Asia/Jakarta\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:WIB\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Jayapura":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Jayapura\r\nX-LIC-LOCATION:Asia/Jayapura\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:WIT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Jerusalem":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Jerusalem\r\nX-LIC-LOCATION:Asia/Jerusalem\r\nBEGIN: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\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:IST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Kabul":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Kabul\r\nX-LIC-LOCATION:Asia/Kabul\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0430\r\nTZOFFSETTO:+0430\r\nTZNAME:+0430\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Kamchatka":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Kamchatka\r\nX-LIC-LOCATION:Asia/Kamchatka\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Karachi":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Karachi\r\nX-LIC-LOCATION:Asia/Karachi\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:PKT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Kathmandu":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Kathmandu\r\nX-LIC-LOCATION:Asia/Kathmandu\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0545\r\nTZOFFSETTO:+0545\r\nTZNAME:+0545\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Khandyga":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Khandyga\r\nX-LIC-LOCATION:Asia/Khandyga\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:+09\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Kolkata":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Kolkata\r\nX-LIC-LOCATION:Asia/Kolkata\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0530\r\nTZOFFSETTO:+0530\r\nTZNAME:IST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Krasnoyarsk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Krasnoyarsk\r\nX-LIC-LOCATION:Asia/Krasnoyarsk\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Kuala_Lumpur":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Kuala_Lumpur\r\nX-LIC-LOCATION:Asia/Kuala_Lumpur\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Kuching":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Kuching\r\nX-LIC-LOCATION:Asia/Kuching\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Kuwait":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Kuwait\r\nX-LIC-LOCATION:Asia/Kuwait\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Macau":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Macau\r\nX-LIC-LOCATION:Asia/Macau\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Magadan":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Magadan\r\nX-LIC-LOCATION:Asia/Magadan\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Makassar":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Makassar\r\nX-LIC-LOCATION:Asia/Makassar\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:WITA\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Manila":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Manila\r\nX-LIC-LOCATION:Asia/Manila\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:PST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Muscat":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Muscat\r\nX-LIC-LOCATION:Asia/Muscat\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Nicosia":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Nicosia\r\nX-LIC-LOCATION:Asia/Nicosia\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Novokuznetsk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Novokuznetsk\r\nX-LIC-LOCATION:Asia/Novokuznetsk\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Novosibirsk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Novosibirsk\r\nX-LIC-LOCATION:Asia/Novosibirsk\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Omsk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Omsk\r\nX-LIC-LOCATION:Asia/Omsk\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Oral":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Oral\r\nX-LIC-LOCATION:Asia/Oral\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Phnom_Penh":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Phnom_Penh\r\nX-LIC-LOCATION:Asia/Phnom_Penh\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Pontianak":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Pontianak\r\nX-LIC-LOCATION:Asia/Pontianak\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:WIB\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Pyongyang":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Pyongyang\r\nX-LIC-LOCATION:Asia/Pyongyang\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0830\r\nTZNAME:KST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0830\r\nTZOFFSETTO:+0900\r\nTZNAME:KST\r\nDTSTART:20180504T233000\r\nRDATE:20180504T233000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Qatar":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Qatar\r\nX-LIC-LOCATION:Asia/Qatar\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Qostanay":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Qostanay\r\nX-LIC-LOCATION:Asia/Qostanay\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Qyzylorda":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Qyzylorda\r\nX-LIC-LOCATION:Asia/Qyzylorda\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:20181221T000000\r\nRDATE:20181221T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Riyadh":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Riyadh\r\nX-LIC-LOCATION:Asia/Riyadh\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Sakhalin":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Sakhalin\r\nX-LIC-LOCATION:Asia/Sakhalin\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Samarkand":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Samarkand\r\nX-LIC-LOCATION:Asia/Samarkand\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Seoul":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Seoul\r\nX-LIC-LOCATION:Asia/Seoul\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:KST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Shanghai":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Shanghai\r\nX-LIC-LOCATION:Asia/Shanghai\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Singapore":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Singapore\r\nX-LIC-LOCATION:Asia/Singapore\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Srednekolymsk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Srednekolymsk\r\nX-LIC-LOCATION:Asia/Srednekolymsk\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Taipei":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Taipei\r\nX-LIC-LOCATION:Asia/Taipei\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Tashkent":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Tashkent\r\nX-LIC-LOCATION:Asia/Tashkent\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Tbilisi":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Tbilisi\r\nX-LIC-LOCATION:Asia/Tbilisi\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Tehran":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Tehran\r\nX-LIC-LOCATION:Asia/Tehran\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0330\r\nTZNAME:+0330\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN: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\r\nBEGIN: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\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Thimphu":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Thimphu\r\nX-LIC-LOCATION:Asia/Thimphu\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Tokyo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Tokyo\r\nX-LIC-LOCATION:Asia/Tokyo\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:JST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Tomsk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Tomsk\r\nX-LIC-LOCATION:Asia/Tomsk\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Ulaanbaatar":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Ulaanbaatar\r\nX-LIC-LOCATION:Asia/Ulaanbaatar\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Urumqi":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Urumqi\r\nX-LIC-LOCATION:Asia/Urumqi\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Ust-Nera":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Ust-Nera\r\nX-LIC-LOCATION:Asia/Ust-Nera\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:+10\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Vientiane":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Vientiane\r\nX-LIC-LOCATION:Asia/Vientiane\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Vladivostok":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Vladivostok\r\nX-LIC-LOCATION:Asia/Vladivostok\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:+10\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Yakutsk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Yakutsk\r\nX-LIC-LOCATION:Asia/Yakutsk\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:+09\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Yangon":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Yangon\r\nX-LIC-LOCATION:Asia/Yangon\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0630\r\nTZOFFSETTO:+0630\r\nTZNAME:+0630\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Yekaterinburg":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Yekaterinburg\r\nX-LIC-LOCATION:Asia/Yekaterinburg\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Yerevan":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Yerevan\r\nX-LIC-LOCATION:Asia/Yerevan\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Atlantic/Azores":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Atlantic/Azores\r\nX-LIC-LOCATION:Atlantic/Azores\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19700329T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:-0100\r\nTZNAME:-01\r\nDTSTART:19701025T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Atlantic/Bermuda":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Atlantic/Bermuda\r\nX-LIC-LOCATION:Atlantic/Bermuda\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:ADT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Atlantic/Canary":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Atlantic/Canary\r\nX-LIC-LOCATION:Atlantic/Canary\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:WEST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:WET\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Atlantic/Cape_Verde":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Atlantic/Cape_Verde\r\nX-LIC-LOCATION:Atlantic/Cape_Verde\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0100\r\nTZOFFSETTO:-0100\r\nTZNAME:-01\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Atlantic/Faroe":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Atlantic/Faroe\r\nX-LIC-LOCATION:Atlantic/Faroe\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:WEST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:WET\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Atlantic/Madeira":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Atlantic/Madeira\r\nX-LIC-LOCATION:Atlantic/Madeira\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:WEST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:WET\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Atlantic/Reykjavik":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Atlantic/Reykjavik\r\nX-LIC-LOCATION:Atlantic/Reykjavik\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Atlantic/South_Georgia":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Atlantic/South_Georgia\r\nX-LIC-LOCATION:Atlantic/South_Georgia\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Atlantic/St_Helena":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Atlantic/St_Helena\r\nX-LIC-LOCATION:Atlantic/St_Helena\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Atlantic/Stanley":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Atlantic/Stanley\r\nX-LIC-LOCATION:Atlantic/Stanley\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Australia/Adelaide":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Australia/Adelaide\r\nX-LIC-LOCATION:Australia/Adelaide\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1030\r\nTZOFFSETTO:+0930\r\nTZNAME:ACST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0930\r\nTZOFFSETTO:+1030\r\nTZNAME:ACDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Australia/Brisbane":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Australia/Brisbane\r\nX-LIC-LOCATION:Australia/Brisbane\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Australia/Broken_Hill":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Australia/Broken_Hill\r\nX-LIC-LOCATION:Australia/Broken_Hill\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1030\r\nTZOFFSETTO:+0930\r\nTZNAME:ACST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0930\r\nTZOFFSETTO:+1030\r\nTZNAME:ACDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Australia/Currie":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Australia/Currie\r\nX-LIC-LOCATION:Australia/Currie\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1100\r\nTZNAME:AEDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Australia/Darwin":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Australia/Darwin\r\nX-LIC-LOCATION:Australia/Darwin\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0930\r\nTZOFFSETTO:+0930\r\nTZNAME:ACST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Australia/Eucla":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Australia/Eucla\r\nX-LIC-LOCATION:Australia/Eucla\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0845\r\nTZOFFSETTO:+0845\r\nTZNAME:+0845\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Australia/Hobart":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Australia/Hobart\r\nX-LIC-LOCATION:Australia/Hobart\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1100\r\nTZNAME:AEDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Australia/Lindeman":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Australia/Lindeman\r\nX-LIC-LOCATION:Australia/Lindeman\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Australia/Lord_Howe":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Australia/Lord_Howe\r\nX-LIC-LOCATION:Australia/Lord_Howe\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1030\r\nTZNAME:+1030\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1030\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Australia/Melbourne":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Australia/Melbourne\r\nX-LIC-LOCATION:Australia/Melbourne\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1100\r\nTZNAME:AEDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Australia/Perth":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Australia/Perth\r\nX-LIC-LOCATION:Australia/Perth\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:AWST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Australia/Sydney":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Australia/Sydney\r\nX-LIC-LOCATION:Australia/Sydney\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1100\r\nTZNAME:AEDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Amsterdam":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Amsterdam\r\nX-LIC-LOCATION:Europe/Amsterdam\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Andorra":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Andorra\r\nX-LIC-LOCATION:Europe/Andorra\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Astrakhan":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Astrakhan\r\nX-LIC-LOCATION:Europe/Astrakhan\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Athens":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Athens\r\nX-LIC-LOCATION:Europe/Athens\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Belgrade":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Belgrade\r\nX-LIC-LOCATION:Europe/Belgrade\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Berlin":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Bratislava":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Bratislava\r\nX-LIC-LOCATION:Europe/Bratislava\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Brussels":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Brussels\r\nX-LIC-LOCATION:Europe/Brussels\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Bucharest":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Bucharest\r\nX-LIC-LOCATION:Europe/Bucharest\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Budapest":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Budapest\r\nX-LIC-LOCATION:Europe/Budapest\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Busingen":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Busingen\r\nX-LIC-LOCATION:Europe/Busingen\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Chisinau":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Chisinau\r\nX-LIC-LOCATION:Europe/Chisinau\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Copenhagen":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Copenhagen\r\nX-LIC-LOCATION:Europe/Copenhagen\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Dublin":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Dublin\r\nX-LIC-LOCATION:Europe/Dublin\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:IST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Gibraltar":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Gibraltar\r\nX-LIC-LOCATION:Europe/Gibraltar\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Guernsey":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Guernsey\r\nX-LIC-LOCATION:Europe/Guernsey\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:BST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Helsinki":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Helsinki\r\nX-LIC-LOCATION:Europe/Helsinki\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Isle_of_Man":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Isle_of_Man\r\nX-LIC-LOCATION:Europe/Isle_of_Man\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:BST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Istanbul":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Istanbul\r\nX-LIC-LOCATION:Europe/Istanbul\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Jersey":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Jersey\r\nX-LIC-LOCATION:Europe/Jersey\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:BST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Kaliningrad":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Kaliningrad\r\nX-LIC-LOCATION:Europe/Kaliningrad\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Kiev":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Kiev\r\nX-LIC-LOCATION:Europe/Kiev\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Kirov":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Kirov\r\nX-LIC-LOCATION:Europe/Kirov\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Lisbon":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Lisbon\r\nX-LIC-LOCATION:Europe/Lisbon\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:WET\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:WEST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Ljubljana":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Ljubljana\r\nX-LIC-LOCATION:Europe/Ljubljana\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/London":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/London\r\nX-LIC-LOCATION:Europe/London\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:BST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Luxembourg":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Luxembourg\r\nX-LIC-LOCATION:Europe/Luxembourg\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Madrid":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Madrid\r\nX-LIC-LOCATION:Europe/Madrid\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Malta":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Malta\r\nX-LIC-LOCATION:Europe/Malta\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Mariehamn":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Mariehamn\r\nX-LIC-LOCATION:Europe/Mariehamn\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Minsk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Minsk\r\nX-LIC-LOCATION:Europe/Minsk\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Monaco":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Monaco\r\nX-LIC-LOCATION:Europe/Monaco\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Moscow":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Moscow\r\nX-LIC-LOCATION:Europe/Moscow\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:MSK\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Nicosia":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Nicosia\r\nX-LIC-LOCATION:Europe/Nicosia\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Oslo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Oslo\r\nX-LIC-LOCATION:Europe/Oslo\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Paris":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Paris\r\nX-LIC-LOCATION:Europe/Paris\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Podgorica":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Podgorica\r\nX-LIC-LOCATION:Europe/Podgorica\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Prague":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Prague\r\nX-LIC-LOCATION:Europe/Prague\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Riga":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Riga\r\nX-LIC-LOCATION:Europe/Riga\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Rome":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Rome\r\nX-LIC-LOCATION:Europe/Rome\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Samara":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Samara\r\nX-LIC-LOCATION:Europe/Samara\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/San_Marino":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/San_Marino\r\nX-LIC-LOCATION:Europe/San_Marino\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Sarajevo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Sarajevo\r\nX-LIC-LOCATION:Europe/Sarajevo\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Saratov":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Saratov\r\nX-LIC-LOCATION:Europe/Saratov\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Simferopol":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Simferopol\r\nX-LIC-LOCATION:Europe/Simferopol\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:MSK\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Skopje":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Skopje\r\nX-LIC-LOCATION:Europe/Skopje\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Sofia":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Sofia\r\nX-LIC-LOCATION:Europe/Sofia\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Stockholm":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Stockholm\r\nX-LIC-LOCATION:Europe/Stockholm\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Tallinn":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Tallinn\r\nX-LIC-LOCATION:Europe/Tallinn\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Tirane":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Tirane\r\nX-LIC-LOCATION:Europe/Tirane\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Ulyanovsk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Ulyanovsk\r\nX-LIC-LOCATION:Europe/Ulyanovsk\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Uzhgorod":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Uzhgorod\r\nX-LIC-LOCATION:Europe/Uzhgorod\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Vaduz":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Vaduz\r\nX-LIC-LOCATION:Europe/Vaduz\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Vatican":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Vatican\r\nX-LIC-LOCATION:Europe/Vatican\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Vienna":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Vienna\r\nX-LIC-LOCATION:Europe/Vienna\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Vilnius":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Vilnius\r\nX-LIC-LOCATION:Europe/Vilnius\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Volgograd":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Volgograd\r\nX-LIC-LOCATION:Europe/Volgograd\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:20181028T020000\r\nRDATE:20181028T020000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Warsaw":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Warsaw\r\nX-LIC-LOCATION:Europe/Warsaw\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Zagreb":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Zagreb\r\nX-LIC-LOCATION:Europe/Zagreb\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Zaporozhye":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Zaporozhye\r\nX-LIC-LOCATION:Europe/Zaporozhye\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Zurich":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Zurich\r\nX-LIC-LOCATION:Europe/Zurich\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Indian/Antananarivo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Indian/Antananarivo\r\nX-LIC-LOCATION:Indian/Antananarivo\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Indian/Chagos":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Indian/Chagos\r\nX-LIC-LOCATION:Indian/Chagos\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Indian/Christmas":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Indian/Christmas\r\nX-LIC-LOCATION:Indian/Christmas\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Indian/Cocos":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Indian/Cocos\r\nX-LIC-LOCATION:Indian/Cocos\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0630\r\nTZOFFSETTO:+0630\r\nTZNAME:+0630\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Indian/Comoro":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Indian/Comoro\r\nX-LIC-LOCATION:Indian/Comoro\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Indian/Kerguelen":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Indian/Kerguelen\r\nX-LIC-LOCATION:Indian/Kerguelen\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Indian/Mahe":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Indian/Mahe\r\nX-LIC-LOCATION:Indian/Mahe\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Indian/Maldives":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Indian/Maldives\r\nX-LIC-LOCATION:Indian/Maldives\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Indian/Mauritius":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Indian/Mauritius\r\nX-LIC-LOCATION:Indian/Mauritius\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Indian/Mayotte":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Indian/Mayotte\r\nX-LIC-LOCATION:Indian/Mayotte\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Indian/Reunion":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Indian/Reunion\r\nX-LIC-LOCATION:Indian/Reunion\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Apia":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Apia\r\nX-LIC-LOCATION:Pacific/Apia\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1400\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:19700405T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1400\r\nTZNAME:+14\r\nDTSTART:19700927T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Auckland":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Auckland\r\nX-LIC-LOCATION:Pacific/Auckland\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1300\r\nTZNAME:NZDT\r\nDTSTART:19700927T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1200\r\nTZNAME:NZST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Bougainville":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Bougainville\r\nX-LIC-LOCATION:Pacific/Bougainville\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Chatham":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Chatham\r\nX-LIC-LOCATION:Pacific/Chatham\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1245\r\nTZOFFSETTO:+1345\r\nTZNAME:+1345\r\nDTSTART:19700927T024500\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1345\r\nTZOFFSETTO:+1245\r\nTZNAME:+1245\r\nDTSTART:19700405T034500\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Chuuk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Chuuk\r\nX-LIC-LOCATION:Pacific/Chuuk\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:+10\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Easter":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Easter\r\nX-LIC-LOCATION:Pacific/Easter\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:-06\r\nDTSTART:20190406T220000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SA\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:20190907T220000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=1SA\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:-06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:20180811T220000\r\nRDATE:20180811T220000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:-06\r\nDTSTART:20180512T220000\r\nRDATE:20180512T220000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Efate":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Efate\r\nX-LIC-LOCATION:Pacific/Efate\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Enderbury":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Enderbury\r\nX-LIC-LOCATION:Pacific/Enderbury\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Fakaofo":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Fakaofo\r\nX-LIC-LOCATION:Pacific/Fakaofo\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Fiji":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Fiji\r\nX-LIC-LOCATION:Pacific/Fiji\r\nBEGIN: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\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:20191110T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:20181104T020000\r\nRDATE:20181104T020000\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Funafuti":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Funafuti\r\nX-LIC-LOCATION:Pacific/Funafuti\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Galapagos":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Galapagos\r\nX-LIC-LOCATION:Pacific/Galapagos\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:-06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Gambier":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Gambier\r\nX-LIC-LOCATION:Pacific/Gambier\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0900\r\nTZNAME:-09\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Guadalcanal":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Guadalcanal\r\nX-LIC-LOCATION:Pacific/Guadalcanal\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Guam":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Guam\r\nX-LIC-LOCATION:Pacific/Guam\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:ChST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Honolulu":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Honolulu\r\nX-LIC-LOCATION:Pacific/Honolulu\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-1000\r\nTZOFFSETTO:-1000\r\nTZNAME:HST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Kiritimati":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Kiritimati\r\nX-LIC-LOCATION:Pacific/Kiritimati\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1400\r\nTZOFFSETTO:+1400\r\nTZNAME:+14\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Kosrae":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Kosrae\r\nX-LIC-LOCATION:Pacific/Kosrae\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Kwajalein":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Kwajalein\r\nX-LIC-LOCATION:Pacific/Kwajalein\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Majuro":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Majuro\r\nX-LIC-LOCATION:Pacific/Majuro\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Marquesas":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Marquesas\r\nX-LIC-LOCATION:Pacific/Marquesas\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0930\r\nTZOFFSETTO:-0930\r\nTZNAME:-0930\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Midway":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Midway\r\nX-LIC-LOCATION:Pacific/Midway\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-1100\r\nTZOFFSETTO:-1100\r\nTZNAME:SST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Nauru":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Nauru\r\nX-LIC-LOCATION:Pacific/Nauru\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Niue":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Niue\r\nX-LIC-LOCATION:Pacific/Niue\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-1100\r\nTZOFFSETTO:-1100\r\nTZNAME:-11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Norfolk":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Norfolk\r\nX-LIC-LOCATION:Pacific/Norfolk\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:20191006T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:20200405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1130\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:20190701T000000\r\nRDATE:20190701T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Noumea":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Noumea\r\nX-LIC-LOCATION:Pacific/Noumea\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Pago_Pago":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Pago_Pago\r\nX-LIC-LOCATION:Pacific/Pago_Pago\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-1100\r\nTZOFFSETTO:-1100\r\nTZNAME:SST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Palau":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Palau\r\nX-LIC-LOCATION:Pacific/Palau\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:+09\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Pitcairn":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Pitcairn\r\nX-LIC-LOCATION:Pacific/Pitcairn\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0800\r\nTZNAME:-08\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Pohnpei":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Pohnpei\r\nX-LIC-LOCATION:Pacific/Pohnpei\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Port_Moresby":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Port_Moresby\r\nX-LIC-LOCATION:Pacific/Port_Moresby\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:+10\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Rarotonga":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Rarotonga\r\nX-LIC-LOCATION:Pacific/Rarotonga\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-1000\r\nTZOFFSETTO:-1000\r\nTZNAME:-10\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Saipan":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Saipan\r\nX-LIC-LOCATION:Pacific/Saipan\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:ChST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Tahiti":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Tahiti\r\nX-LIC-LOCATION:Pacific/Tahiti\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-1000\r\nTZOFFSETTO:-1000\r\nTZNAME:-10\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Tarawa":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Tarawa\r\nX-LIC-LOCATION:Pacific/Tarawa\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Tongatapu":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Tongatapu\r\nX-LIC-LOCATION:Pacific/Tongatapu\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Wake":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Wake\r\nX-LIC-LOCATION:Pacific/Wake\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Wallis":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Wallis\r\nX-LIC-LOCATION:Pacific/Wallis\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","AUS Central Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:AUS Central Standard Time\r\nX-LIC-LOCATION:AUS Central Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0930\r\nTZOFFSETTO:+0930\r\nTZNAME:ACST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","AUS Eastern Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:AUS Eastern Standard Time\r\nX-LIC-LOCATION:AUS Eastern Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1100\r\nTZNAME:AEDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Afghanistan Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Afghanistan Standard Time\r\nX-LIC-LOCATION:Afghanistan Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0430\r\nTZOFFSETTO:+0430\r\nTZNAME:+0430\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Asmera":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Asmera\r\nX-LIC-LOCATION:Africa/Asmera\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Africa/Timbuktu":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Africa/Timbuktu\r\nX-LIC-LOCATION:Africa/Timbuktu\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Alaskan Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Alaskan Standard Time\r\nX-LIC-LOCATION:Alaskan Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0900\r\nTZOFFSETTO:-0800\r\nTZNAME:AKDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0900\r\nTZNAME:AKST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Argentina/ComodRivadavia":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Argentina/ComodRivadavia\r\nX-LIC-LOCATION:America/Argentina/ComodRivadavia\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Buenos_Aires":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Buenos_Aires\r\nX-LIC-LOCATION:America/Buenos_Aires\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Louisville":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Louisville\r\nX-LIC-LOCATION:America/Louisville\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Montreal":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Montreal\r\nX-LIC-LOCATION:America/Montreal\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","America/Santa_Isabel":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:America/Santa_Isabel\r\nX-LIC-LOCATION:America/Santa_Isabel\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Arab Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Arab Standard Time\r\nX-LIC-LOCATION:Arab Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Arabian Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Arabian Standard Time\r\nX-LIC-LOCATION:Arabian Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Arabic Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Arabic Standard Time\r\nX-LIC-LOCATION:Arabic Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Argentina Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Argentina Standard Time\r\nX-LIC-LOCATION:Argentina Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Calcutta":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Calcutta\r\nX-LIC-LOCATION:Asia/Calcutta\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0530\r\nTZOFFSETTO:+0530\r\nTZNAME:IST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Katmandu":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Katmandu\r\nX-LIC-LOCATION:Asia/Katmandu\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0545\r\nTZOFFSETTO:+0545\r\nTZNAME:+0545\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Rangoon":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Rangoon\r\nX-LIC-LOCATION:Asia/Rangoon\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0630\r\nTZOFFSETTO:+0630\r\nTZNAME:+0630\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Asia/Saigon":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Asia/Saigon\r\nX-LIC-LOCATION:Asia/Saigon\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Atlantic Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Atlantic Standard Time\r\nX-LIC-LOCATION:Atlantic Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:ADT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:AST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Atlantic/Faeroe":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Atlantic/Faeroe\r\nX-LIC-LOCATION:Atlantic/Faeroe\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:WEST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:WET\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Atlantic/Jan_Mayen":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Atlantic/Jan_Mayen\r\nX-LIC-LOCATION:Atlantic/Jan_Mayen\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Azerbaijan Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Azerbaijan Standard Time\r\nX-LIC-LOCATION:Azerbaijan Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Azores Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Azores Standard Time\r\nX-LIC-LOCATION:Azores Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19700329T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:-0100\r\nTZNAME:-01\r\nDTSTART:19701025T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Bahia Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Bahia Standard Time\r\nX-LIC-LOCATION:Bahia Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Bangladesh Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Bangladesh Standard Time\r\nX-LIC-LOCATION:Bangladesh Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Belarus Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Belarus Standard Time\r\nX-LIC-LOCATION:Belarus Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Canada Central Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Canada Central Standard Time\r\nX-LIC-LOCATION:Canada Central Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Cape Verde Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Cape Verde Standard Time\r\nX-LIC-LOCATION:Cape Verde Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0100\r\nTZOFFSETTO:-0100\r\nTZNAME:-01\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Caucasus Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Caucasus Standard Time\r\nX-LIC-LOCATION:Caucasus Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Cen. Australia Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Cen. Australia Standard Time\r\nX-LIC-LOCATION:Cen. Australia Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1030\r\nTZOFFSETTO:+0930\r\nTZNAME:ACST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0930\r\nTZOFFSETTO:+1030\r\nTZNAME:ACDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Central America Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Central America Standard Time\r\nX-LIC-LOCATION:Central America Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Central Asia Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Central Asia Standard Time\r\nX-LIC-LOCATION:Central Asia Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0600\r\nTZOFFSETTO:+0600\r\nTZNAME:+06\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Central Brazilian Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Central Brazilian Standard Time\r\nX-LIC-LOCATION:Central Brazilian Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20181104T000000\r\nRDATE:20181104T000000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20180218T000000\r\nRDATE:20180218T000000\r\nRDATE:20190217T000000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Central Europe Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Central Europe Standard Time\r\nX-LIC-LOCATION:Central Europe Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Central European Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Central European Standard Time\r\nX-LIC-LOCATION:Central European Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Central Pacific Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Central Pacific Standard Time\r\nX-LIC-LOCATION:Central Pacific Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Central Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Central Standard Time\r\nX-LIC-LOCATION:Central Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Central Standard Time (Mexico)":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Central Standard Time (Mexico)\r\nX-LIC-LOCATION:Central Standard Time (Mexico)\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","China Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:China Standard Time\r\nX-LIC-LOCATION:China Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","E. Africa Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:E. Africa Standard Time\r\nX-LIC-LOCATION:E. Africa Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:EAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","E. Australia Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:E. Australia Standard Time\r\nX-LIC-LOCATION:E. Australia Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","E. South America Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:E. South America Standard Time\r\nX-LIC-LOCATION:E. South America Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:20181104T000000\r\nRDATE:20181104T000000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20180218T000000\r\nRDATE:20180218T000000\r\nRDATE:20190217T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0200\r\nTZOFFSETTO:-0200\r\nTZNAME:-02\r\nDTSTART:19700101T000000\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Eastern Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Eastern Standard Time\r\nX-LIC-LOCATION:Eastern Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Egypt Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Egypt Standard Time\r\nX-LIC-LOCATION:Egypt Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Ekaterinburg Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Ekaterinburg Standard Time\r\nX-LIC-LOCATION:Ekaterinburg Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Europe/Belfast":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Belfast\r\nX-LIC-LOCATION:Europe/Belfast\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:BST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","FLE Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:FLE Standard Time\r\nX-LIC-LOCATION:FLE Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Fiji Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Fiji Standard Time\r\nX-LIC-LOCATION:Fiji Standard Time\r\nBEGIN: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\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:20191110T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:20181104T020000\r\nRDATE:20181104T020000\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","GMT Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:GMT Standard Time\r\nX-LIC-LOCATION:GMT Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:BST\r\nDTSTART:19700329T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","GTB Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:GTB Standard Time\r\nX-LIC-LOCATION:GTB Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Georgian Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Georgian Standard Time\r\nX-LIC-LOCATION:Georgian Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Greenland Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Greenland Standard Time\r\nX-LIC-LOCATION:Greenland Standard Time\r\nBEGIN: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\r\nBEGIN: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\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Greenwich Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Greenwich Standard Time\r\nX-LIC-LOCATION:Greenwich Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:GMT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Hawaiian Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Hawaiian Standard Time\r\nX-LIC-LOCATION:Hawaiian Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-1000\r\nTZOFFSETTO:-1000\r\nTZNAME:HST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","India Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:India Standard Time\r\nX-LIC-LOCATION:India Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0530\r\nTZOFFSETTO:+0530\r\nTZNAME:IST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Iran Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Iran Standard Time\r\nX-LIC-LOCATION:Iran Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0330\r\nTZNAME:+0330\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN: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\r\nBEGIN: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\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Israel Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Israel Standard Time\r\nX-LIC-LOCATION:Israel Standard Time\r\nBEGIN: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\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:IST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Jordan Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Jordan Standard Time\r\nX-LIC-LOCATION:Jordan Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700326T235959\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1TH\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701030T010000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1FR\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Kaliningrad Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Kaliningrad Standard Time\r\nX-LIC-LOCATION:Kaliningrad Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Korea Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Korea Standard Time\r\nX-LIC-LOCATION:Korea Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:KST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Libya Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Libya Standard Time\r\nX-LIC-LOCATION:Libya Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Line Islands Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Line Islands Standard Time\r\nX-LIC-LOCATION:Line Islands Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1400\r\nTZOFFSETTO:+1400\r\nTZNAME:+14\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Magadan Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Magadan Standard Time\r\nX-LIC-LOCATION:Magadan Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Mauritius Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Mauritius Standard Time\r\nX-LIC-LOCATION:Mauritius Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Middle East Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Middle East Standard Time\r\nX-LIC-LOCATION:Middle East Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700329T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701025T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Montevideo Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Montevideo Standard Time\r\nX-LIC-LOCATION:Montevideo Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Morocco Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Morocco Standard Time\r\nX-LIC-LOCATION:Morocco Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0000\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20180325T020000\r\nRDATE:20180325T020000\r\nRDATE:20180617T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0000\r\nTZNAME:+00\r\nDTSTART:20180513T030000\r\nRDATE:20180513T030000\r\nEND:STANDARD\r\nBEGIN: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\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:+01\r\nDTSTART:20181028T030000\r\nRDATE:20181028T030000\r\nEND:STANDARD\r\nBEGIN: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\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Mountain Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Mountain Standard Time\r\nX-LIC-LOCATION:Mountain Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Mountain Standard Time (Mexico)":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Mountain Standard Time (Mexico)\r\nX-LIC-LOCATION:Mountain Standard Time (Mexico)\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Myanmar Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Myanmar Standard Time\r\nX-LIC-LOCATION:Myanmar Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0630\r\nTZOFFSETTO:+0630\r\nTZNAME:+0630\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","N. Central Asia Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:N. Central Asia Standard Time\r\nX-LIC-LOCATION:N. Central Asia Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Namibia Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Namibia Standard Time\r\nX-LIC-LOCATION:Namibia Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:CAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Nepal Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Nepal Standard Time\r\nX-LIC-LOCATION:Nepal Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0545\r\nTZOFFSETTO:+0545\r\nTZNAME:+0545\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","New Zealand Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:New Zealand Standard Time\r\nX-LIC-LOCATION:New Zealand Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1300\r\nTZNAME:NZDT\r\nDTSTART:19700927T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1200\r\nTZNAME:NZST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Newfoundland Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Newfoundland Standard Time\r\nX-LIC-LOCATION:Newfoundland Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0230\r\nTZOFFSETTO:-0330\r\nTZNAME:NST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0330\r\nTZOFFSETTO:-0230\r\nTZNAME:NDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","North Asia East Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:North Asia East Standard Time\r\nX-LIC-LOCATION:North Asia East Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","North Asia Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:North Asia Standard Time\r\nX-LIC-LOCATION:North Asia Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific SA Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific SA Standard Time\r\nX-LIC-LOCATION:Pacific SA Standard Time\r\nBEGIN: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\r\nBEGIN: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\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:20180812T000000\r\nRDATE:20180812T000000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:20180513T000000\r\nRDATE:20180513T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific Standard Time\r\nX-LIC-LOCATION:Pacific Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific Standard Time (Mexico)":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific Standard Time (Mexico)\r\nX-LIC-LOCATION:Pacific Standard Time (Mexico)\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pacific/Johnston":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pacific/Johnston\r\nX-LIC-LOCATION:Pacific/Johnston\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-1000\r\nTZOFFSETTO:-1000\r\nTZNAME:HST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Pakistan Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Pakistan Standard Time\r\nX-LIC-LOCATION:Pakistan Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:PKT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Paraguay Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Paraguay Standard Time\r\nX-LIC-LOCATION:Paraguay Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19701004T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700322T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=4SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Romance Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Romance Standard Time\r\nX-LIC-LOCATION:Romance Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Russia Time Zone 10":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Russia Time Zone 10\r\nX-LIC-LOCATION:Russia Time Zone 10\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1100\r\nTZNAME:+11\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Russia Time Zone 11":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Russia Time Zone 11\r\nX-LIC-LOCATION:Russia Time Zone 11\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1200\r\nTZOFFSETTO:+1200\r\nTZNAME:+12\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Russia Time Zone 3":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Russia Time Zone 3\r\nX-LIC-LOCATION:Russia Time Zone 3\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0400\r\nTZOFFSETTO:+0400\r\nTZNAME:+04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Russian Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Russian Standard Time\r\nX-LIC-LOCATION:Russian Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:MSK\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","SA Eastern Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:SA Eastern Standard Time\r\nX-LIC-LOCATION:SA Eastern Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0300\r\nTZOFFSETTO:-0300\r\nTZNAME:-03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","SA Pacific Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:SA Pacific Standard Time\r\nX-LIC-LOCATION:SA Pacific Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0500\r\nTZNAME:-05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","SA Western Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:SA Western Standard Time\r\nX-LIC-LOCATION:SA Western Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","SE Asia Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:SE Asia Standard Time\r\nX-LIC-LOCATION:SE Asia Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0700\r\nTZOFFSETTO:+0700\r\nTZNAME:+07\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Samoa Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Samoa Standard Time\r\nX-LIC-LOCATION:Samoa Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1400\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:19700405T040000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1400\r\nTZNAME:+14\r\nDTSTART:19700927T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Singapore Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Singapore Standard Time\r\nX-LIC-LOCATION:Singapore Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","South Africa Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:South Africa Standard Time\r\nX-LIC-LOCATION:South Africa Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0200\r\nTZNAME:SAST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Sri Lanka Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Sri Lanka Standard Time\r\nX-LIC-LOCATION:Sri Lanka Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0530\r\nTZOFFSETTO:+0530\r\nTZNAME:+0530\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Syria Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Syria Standard Time\r\nX-LIC-LOCATION:Syria Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nTZNAME:EET\r\nDTSTART:19701030T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1FR\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nTZNAME:EEST\r\nDTSTART:19700327T000000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1FR\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Taipei Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Taipei Standard Time\r\nX-LIC-LOCATION:Taipei Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Tasmania Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Tasmania Standard Time\r\nX-LIC-LOCATION:Tasmania Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1100\r\nTZNAME:AEDT\r\nDTSTART:19701004T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1100\r\nTZOFFSETTO:+1000\r\nTZNAME:AEST\r\nDTSTART:19700405T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Tokyo Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Tokyo Standard Time\r\nX-LIC-LOCATION:Tokyo Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:JST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Tonga Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Tonga Standard Time\r\nX-LIC-LOCATION:Tonga Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1300\r\nTZOFFSETTO:+1300\r\nTZNAME:+13\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Turkey Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Turkey Standard Time\r\nX-LIC-LOCATION:Turkey Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0300\r\nTZNAME:+03\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","US Eastern Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:US Eastern Standard Time\r\nX-LIC-LOCATION:US Eastern Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","US Mountain Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:US Mountain Standard Time\r\nX-LIC-LOCATION:US Mountain Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","US/Central":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:US/Central\r\nX-LIC-LOCATION:US/Central\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","US/Eastern":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:US/Eastern\r\nX-LIC-LOCATION:US/Eastern\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","US/Mountain":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:US/Mountain\r\nX-LIC-LOCATION:US/Mountain\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0600\r\nTZNAME:MDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0700\r\nTZNAME:MST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","US/Pacific":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:US/Pacific\r\nX-LIC-LOCATION:US/Pacific\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","US/Pacific-New":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:US/Pacific-New\r\nX-LIC-LOCATION:US/Pacific-New\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0800\r\nTZOFFSETTO:-0700\r\nTZNAME:PDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0700\r\nTZOFFSETTO:-0800\r\nTZNAME:PST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Ulaanbaatar Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Ulaanbaatar Standard Time\r\nX-LIC-LOCATION:Ulaanbaatar Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:+08\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Venezuela Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Venezuela Standard Time\r\nX-LIC-LOCATION:Venezuela Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0400\r\nTZNAME:-04\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Vladivostok Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Vladivostok Standard Time\r\nX-LIC-LOCATION:Vladivostok Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:+10\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","W. Australia Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:W. Australia Standard Time\r\nX-LIC-LOCATION:W. Australia Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0800\r\nTZOFFSETTO:+0800\r\nTZNAME:AWST\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","W. Central Africa Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:W. Central Africa Standard Time\r\nX-LIC-LOCATION:W. Central Africa Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nTZNAME:WAT\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","W. Europe Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:W. Europe Standard Time\r\nX-LIC-LOCATION:W. Europe Standard Time\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","West Asia Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:West Asia Standard Time\r\nX-LIC-LOCATION:West Asia Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0500\r\nTZOFFSETTO:+0500\r\nTZNAME:+05\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","West Pacific Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:West Pacific Standard Time\r\nX-LIC-LOCATION:West Pacific Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+1000\r\nTZOFFSETTO:+1000\r\nTZNAME:+10\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR","Yakutsk Standard Time":"BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Yakutsk Standard Time\r\nX-LIC-LOCATION:Yakutsk Standard Time\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0900\r\nTZOFFSETTO:+0900\r\nTZNAME:+09\r\nDTSTART:19700101T000000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR"} \ No newline at end of file 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..15d954e2c1 --- /dev/null +++ b/core/date/time-zone.js @@ -0,0 +1,31 @@ +/* + Time Zones definitions at: + + https://hg.mozilla.org/comm-central/raw-file/tip/calendar/timezones/zones.json + +*/ +var TimeZone = require("./time-zone-core").TimeZone + CalendarDate = require("./calendar-date").CalendarDate, + TimeZonePrototype = TimeZone.prototype; + + +exports.TimeZone = TimeZone; +/** + * 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); + aCalendarDate.zone = timeZone; + return aCalendarDate; + }, + enumerable: false, + writable: true, + configurable: true +}); diff --git a/core/date/time-zone.mjson b/core/date/time-zone.mjson new file mode 100644 index 0000000000..c529c3700e --- /dev/null +++ b/core/date/time-zone.mjson @@ -0,0 +1,68 @@ +{ + "root": { + "prototype": "core/meta/module-object-descriptor", + "values": { + "name": "TimeZone", + "propertyDescriptors": [ + { "@": "identifier" }, + { "@": "location" }, + { "@": "tznames" }, + { "@": "latitude" }, + { "@": "longitude" } + ], + "propertyDescriptorGroups": { + "all": [ + { "@": "identifier" }, + { "@": "location" }, + { "@": "tznames" }, + { "@": "latitude" }, + { "@": "longitude" } + ] + }, + "propertyValidationRules": {}, + "objectDescriptorModule": { "%": "core/date/time-zone.mjson" }, + "exportName": "TimeZone", + "module": { "%": "core/date/time-zone" }, + "modulePrototype":{"@": "TimeZonePrototype"}, + "object":{ "@": "TimeZonePrototype" } + } + }, + "TimeZonePrototype": { + "object": "core/date/time-zone" + }, + "identifier": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "identifier", + "valueType": "string" + } + }, + "location": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "location", + "valueType": "string" + } + }, + "tznames": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "tznames", + "valueType": "string" + } + }, + "latitude": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "latitude", + "valueType": "number" + } + }, + "longitude": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "latitude", + "valueType": "number" + } + } +} diff --git a/core/date/tools/compile-zones.js b/core/date/tools/compile-zones.js new file mode 100644 index 0000000000..252eacd7a2 --- /dev/null +++ b/core/date/tools/compile-zones.js @@ -0,0 +1,203 @@ +'use strict'; + +/* + From https://github.com/mifi/ical-expander/ + + MIT License + + Copyright (c) 2016 Mikael Finstad + +*/ + + +/* + zones.js from Mozilla when written: + + "America/New_York": { + "ics": "BEGIN:VTIMEZONE\r\nTZID:America/New_York\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nTZNAME:EDT\r\nDTSTART:19700308T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nTZNAME:EST\r\nDTSTART:19701101T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE", + "latitude": "+0404251", + "longitude": "-0740023" + }, + + zones.js from Mozilla now: + + "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" + }, + + + "BEGIN:DAYLIGHT + TZOFFSETFROM:-0500 + TZOFFSETTO:-0400 + TZNAME:EDT + DTSTART:19700308T020000 + RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU + END:DAYLIGHT" + + "BEGIN:STANDARD + TZOFFSETFROM:-0400 + TZOFFSETTO:-0500 + TZNAME:EST + DTSTART:19701101T020000 + RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU + END:STANDARD" + + + + + what ical.js expects: + New_York.ics: + BEGIN:VCALENDAR + PRODID:-//tzurl.org//NONSGML Olson 2012h//EN + VERSION:2.0 + BEGIN:VTIMEZONE + TZID:America/New_York + X-LIC-LOCATION:America/New_York + BEGIN:DAYLIGHT + TZOFFSETFROM:-0500 + TZOFFSETTO:-0400 + TZNAME:EDT + DTSTART:19700308T020000 + RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU + END:DAYLIGHT + BEGIN:STANDARD + TZOFFSETFROM:-0400 + TZOFFSETTO:-0500 + TZNAME:EST + DTSTART:19701101T020000 + RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU + END:STANDARD + END:VTIMEZONE + END:VCALENDAR + + +/* +parsed = ICAL.parse(`BEGIN:VCALENDAR\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID:${timeZoneId}\nX-LIC-LOCATION:${timeZoneId}\n${icsData}/nEND:VTIMEZONE\nEND:VCALENDAR`), +*/ + + +/* eslint-disable no-console */ + +const fs = require('fs'); +const zonesJson = fs.readFileSync('../time-zone-data/zones.json'); +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) { + return `BEGIN:VCALENDAR\r\nPRODID:-//tzurl.org//NONSGML Olson 2012h//EN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:${timeZoneId}\r\nX-LIC-LOCATION:${timeZoneId}\r\n${icsData}\r\nEND:VTIMEZONE\r\nEND:VCALENDAR`; +} + +function compileTimeZones(zones) { + const out = {}; + Object.keys(zones.zones).forEach((timeZoneId) => { + var icsData = zones.zones[timeZoneId].ics.join("\r\n"), + singleOut = {}; + out[timeZoneId] = icsString(timeZoneId,icsData); + 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; + } + 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`); + } + }); + + fs.writeFileSync('../time-zone-data/zones-compiled.json', JSON.stringify(out)); +}; + + + +//https://hg.mozilla.org/comm-central/raw-file/tip/calendar/timezones/zones.json +const options = { + hostname: 'hg.mozilla.org', + port: 443, + path: '/comm-central/raw-file/tip/calendar/timezones/zones.json', + method: 'GET' +}; + +const req = https.request(options, res => { + console.log(`statusCode: ${res.statusCode}`) + + res.on('data', d => { + fetchedZonesJSONString += d; + }); + + res.on("end", () => { + try { + fetchedZones = JSON.parse(fetchedZonesJSONString); + compileTimeZones(fetchedZones); + } catch (error) { + console.error(error.message); + compileTimeZones(zones); + }; + }); +}); + +req.on('error', error => { + console.error(error); + compileTimeZones(zones); +}); + +req.end(); + + + diff --git a/core/deprecate.js b/core/deprecate.js index 744ef1470e..32ef46168f 100644 --- a/core/deprecate.js +++ b/core/deprecate.js @@ -1,6 +1,6 @@ /* global console */ var Montage = require("./core").Montage; - Map = require("collections/map"); + Map = require("./collections/map"); var deprecatedFeaturesOnceMap = new Map(); @@ -79,7 +79,7 @@ exports.deprecateMethod = function deprecate(scope, deprecatedFunction, name, al } else { deprecationWarning(name, alternative, 3); } - + return deprecatedFunction.apply(scope ? scope : this, arguments); }; deprecationWrapper.deprecatedFunction = deprecatedFunction; 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" }, diff --git a/core/drag/drag-manager.js b/core/drag/drag-manager.js index af30800db4..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; } @@ -121,7 +138,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) { @@ -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; + } } } }, @@ -243,7 +290,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 +311,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 +385,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 +408,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 +470,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 +500,7 @@ var DragManager = exports.DragManager = Montage.specialize({ value: function (positionX, positionY) { return this._findRegisteredComponentAtPosistion( positionX, - positionY, + positionY, DRAGGABLE ); } @@ -471,7 +518,7 @@ var DragManager = exports.DragManager = Montage.specialize({ value: function (positionX, positionY) { return this._findRegisteredComponentAtPosistion( positionX, - positionY, + positionY, DROPABBLE ); } @@ -494,7 +541,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 +573,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 +663,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 +685,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 +735,7 @@ var DragManager = exports.DragManager = Montage.specialize({ this._draggingOperationContext = (draggingOperationContext = ( this._createDraggingOperationContextWithDraggableAndPosition( - null, + null, event ) )); @@ -702,12 +749,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 +792,7 @@ var DragManager = exports.DragManager = Montage.specialize({ value: function (event) { event.preventDefault(); event.stopPropagation(); - + this._draggingOperationContext.deltaX = ( event.pageX - this._draggingOperationContext.startPositionX ); @@ -757,7 +804,7 @@ var DragManager = exports.DragManager = Montage.specialize({ this._draggingOperationContext.dataTransfer = ( DataTransfer.fromDataTransfer(event.dataTransfer) ); - + this._removeDragListeners(); this.handleTranslateEnd(); } @@ -786,13 +833,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 +886,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 +896,7 @@ var DragManager = exports.DragManager = Montage.specialize({ * Draw Cycles Management */ - willDraw: { + willDraw: { value: function () { var draggingOperationContext; @@ -859,14 +906,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 +925,7 @@ var DragManager = exports.DragManager = Montage.specialize({ draggingOperationContext.positionY ); - if (droppable && + if (droppable && !draggingOperationContext.dropTargetCandidates.has( droppable ) @@ -890,13 +937,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 +1006,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 +1062,7 @@ var DragManager = exports.DragManager = Montage.specialize({ draggedImage.style[DragManager.cssTransform] = translate; this._scrollIfNeeded( - draggingOperationContext.positionX, + draggingOperationContext.positionX, draggingOperationContext.positionY ); } @@ -1037,7 +1084,7 @@ var DragManager = exports.DragManager = Montage.specialize({ this._dispatchDrop( draggingOperationContext ); - + this._resetTranslateContext(); draggingOperationContext.currentDropTarget = null; this._dispatchDragEnd(draggingOperationContext); @@ -1049,9 +1096,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 +1154,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 +1169,7 @@ var DragManager = exports.DragManager = Montage.specialize({ ); draggableElement.parentNode.insertBefore( - placeholderElement, + placeholderElement, draggableElement ); @@ -1145,8 +1192,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 +1210,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 +1219,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 +1240,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 +1249,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 +1266,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 +1275,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 +1291,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 +1300,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 +1317,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 +1325,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; diff --git a/core/enum.js b/core/enum.js index effd8f0311..d104be4cea 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 @@ -26,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 @@ -68,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"); @@ -79,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 @@ -86,14 +156,20 @@ 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 }); + + (this._members || (this._members = [])).push(member); + + (_membersByValue || this._membersByValue)[intValue] = member; + (_membersIntValue || this._membersIntValue).set(member, intValue); } } }, @@ -125,7 +201,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) { @@ -137,7 +213,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"); } diff --git a/core/environment.js b/core/environment.js index 413a248309..4609716201 100644 --- a/core/environment.js +++ b/core/environment.js @@ -16,13 +16,93 @@ 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 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. + * + * 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 applicationURL = this.application.url, + stageArgument = applicationURL && applicationURL.searchParams.get("stage"); + + if(stageArgument) { + this._stage = stageArgument; + } else if(applicationURL && (applicationURL.hostname === "127.0.0.1" || applicationURL.hostname === "localhost" || applicationURL.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") + }, + _userAgent: { value: null }, userAgent: { set: function (userAgent) { - userAgent = userAgent.toLowerCase(); + + if(userAgent) { + userAgent = userAgent.toLowerCase(); + } if (userAgent !== this._userAgent) { this._userAgent = userAgent; @@ -55,6 +135,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/event/change-event.js b/core/event/change-event.js new file mode 100644 index 0000000000..1cf579a9d6 --- /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 = performance.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 + } + +}); diff --git a/core/event/event-manager.js b/core/event/event-manager.js index e4aef1c155..a703cfb825 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -15,14 +15,165 @@ */ 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"), - WeakMap = require("collections/weak-map"), - currentEnvironment = require("../environment").currentEnvironment; + Map = require("core/collections/map"), + WeakMap = require("core/collections/weak-map"), + currentEnvironment = require("../environment").currentEnvironment, + isBrowser = currentEnvironment.isBrowser, + Event_NONE = 0, + Event_CAPTURING_PHASE = 1, + Event_AT_TARGET = 2, + Event_BUBBLING_PHASE = 3, + defaultEventManager, + browserSupportsCaptureOption = false, + browserSupportsPassiveOption = false, + MontageElement = window ? window.MontageElement : null; + + + + + +/** + * 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); + } + }); + } + + })(); +} -var 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 @@ -43,6 +194,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 ( @@ -222,42 +375,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; @@ -266,10 +467,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 @@ -292,6 +506,31 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan this._elementEventHandlerByElement = new WeakMap(); 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; } }, @@ -361,6 +600,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 +616,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,11 +635,13 @@ 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 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 @@ -473,6 +718,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; } }, @@ -554,7 +801,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, @@ -612,8 +859,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) { @@ -633,8 +881,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); }) }); @@ -759,7 +1008,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 +1050,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; @@ -863,38 +1112,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. @@ -904,34 +1175,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; } }, @@ -944,15 +1266,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) { @@ -963,6 +1295,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. @@ -1018,16 +1367,166 @@ 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(); } }, @@ -1778,7 +2302,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))) || @@ -2481,34 +3008,77 @@ 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 (event) { - if ((window.MontageElement && event.target instanceof MontageElement) || - (event instanceof UIEvent && !this._shouldDispatchEvent(event))) { - return void 0; + 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 && ( + (MontageElement && event.target instanceof MontageElement) || + (event instanceof UIEvent && !this._shouldDispatchEvent(event)) + )) { + return void 0; } + // performance.mark('event-manager:handleEvent:start'); + if (this.monitorDOMModificationInEventHandling) { document.body.addEventListener("DOMSubtreeModified", this.domModificationEventHandler, true); document.body.addEventListener("DOMAttrModified", this.domModificationEventHandler, true); @@ -2523,18 +3093,25 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan nextEntry, eventPath, eventType = event.type, - capitalizedEventType = eventType.toCapitalized(), + // capitalizedEventType = eventType.toCapitalized(), eventBubbles = event.bubbles, - captureMethodName, - bubbleMethodName, - identifierSpecificCaptureMethodName, - identifierSpecificBubbleMethodName, - 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, + BUBBLING_PHASE = Event_BUBBLING_PHASE, + targetEntry, targetEntryForEventType; if ("DOMContentLoaded" === eventType) { loadedWindow = event.target.defaultView; @@ -2555,24 +3132,24 @@ 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); } // 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; - } - - captureMethodName = this.methodNameForCapturePhaseOfEventType(eventType, null, capitalizedEventType); - bubbleMethodName = this.methodNameForBubblePhaseOfEventType(eventType, null, capitalizedEventType); + // 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); // Let the delegate handle the event first if (this.delegate && typeof this.delegate.willDistributeEvent === "function") { @@ -2583,96 +3160,111 @@ 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; + 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._registeredEventListenersForEventType_onTarget_registeredEventListeners_(eventType, iTarget, registeredCaptureEventListeners); + listenerEntries = this._registeredEventListenersOnTarget_eventType_eventPhase(iTarget, eventType, CAPTURING_PHASE); 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._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, undefined/*currentTargetIdentifierSpecificCaptureMethodName*/, undefined/*identifierSpecificCaptureMethodName*/, undefined/*captureMethodName*/); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, true, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, undefined/*currentTargetIdentifierSpecificCaptureMethodName*/, undefined/*identifierSpecificCaptureMethodName*/, undefined/*captureMethodName*/); } } // 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); + 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, captureMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, undefined/*identifierSpecificCaptureMethodName*/, undefined/*identifierSpecificCaptureMethodName*/, undefined/*captureMethodName*/); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, true, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificCaptureMethodName, captureMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, undefined/*identifierSpecificCaptureMethodName*/, undefined/*identifierSpecificCaptureMethodName*/, undefined/*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, bubbleMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, undefined/*identifierSpecificBubbleMethodName*/, undefined/*identifierSpecificBubbleMethodName*/, undefined/*bubbleMethodName*/); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, false, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, undefined/*identifierSpecificBubbleMethodName*/, undefined/*identifierSpecificBubbleMethodName*/, undefined/*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; } + //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._invokeTargetListenerEntryForEvent(iTarget, nextEntry, mutableEvent, undefined/*currentTargetIdentifierSpecificBubbleMethodName*/, undefined/*identifierSpecificBubbleMethodName*/, undefined/*bubbleMethodName*/); } this._processCurrentDispatchedTargetListenersToRemove(iTarget, eventType, false, listenerEntries); _currentDispatchedTargetListeners.delete(listenerEntries); } else { - this._invokeTargetListenerForEvent(iTarget, listenerEntries, mutableEvent, identifierSpecificBubbleMethodName, bubbleMethodName); + this._invokeTargetListenerEntryForEvent(iTarget, listenerEntries, mutableEvent, undefined/*currentTargetIdentifierSpecificBubbleMethodName*/, undefined/*identifierSpecificBubbleMethodName*/, undefined/*bubbleMethodName*/); } } - mutableEvent.eventPhase = Event.NONE; + mutableEvent.eventPhase = Event_NONE; mutableEvent.currentTarget = null; if (this._isStoringPointerEvents) { @@ -2686,6 +3278,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'); } }, @@ -2693,28 +3288,107 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan * @private */ _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); + value: function _invokeTargetListenerForEvent(iTarget, jListener, mutableEvent, currentTargetIdentifierSpecificPhaseMethodName, targetIdentifierSpecificPhaseMethodName, 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); + // } + (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; + + } + }, + + + /** + * @private + */ + _invokeTargetListenerEntryForEvent: { + value: function _invokeTargetListenerEntryForEvent(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 = this._currentTargetIdentifierSpecificPhaseMethodName(listenerEntry.capture, mutableEvent.type, iTarget.identifier)) && typeof listener[currentTargetIdentifierSpecificPhaseMethodName] === this._functionType) + ? currentTargetIdentifierSpecificPhaseMethodName + : ((targetIdentifierSpecificPhaseMethodName = this._currentTargetIdentifierSpecificPhaseMethodName(listenerEntry.capture,mutableEvent.type,mutableEvent.target.identifier)) && typeof listener[targetIdentifierSpecificPhaseMethodName] === this._functionType) + ? targetIdentifierSpecificPhaseMethodName + : (typeof listener[(phaseMethodName = this._currentTargetIdentifierSpecificPhaseMethodName(listenerEntry.capture,mutableEvent.type, null))] === 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); + } + } } + } + }, + _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 * original activationEvent target and the window are preparedForActionEvents * + * Benoit todo: Make this more generic and configurable to be applicable to different trees. + * * @function * @private */ diff --git a/core/event/mutable-event.js b/core/event/mutable-event.js index d11d4a5150..78146051c6 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,14 +86,26 @@ if (typeof window !== "undefined") { }, /** - * @function + * @function - deprecated */ 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; + } + }, + + defaultPrevented: { + value: function () { + return (typeof this._event.defaultPrevented === "boolean") + ? this._event.defaultPrevented + : this.getPreventDefault(); } }, @@ -163,7 +175,11 @@ if (typeof window !== "undefined") { */ 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; @@ -178,7 +194,11 @@ if (typeof window !== "undefined") { */ 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; @@ -193,7 +213,11 @@ if (typeof window !== "undefined") { */ 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; @@ -223,7 +247,7 @@ if (typeof window !== "undefined") { */ 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; @@ -235,7 +259,7 @@ if (typeof window !== "undefined") { */ touches: { get: function () { - return this._event.touches; + return this._event ? this._event.touches : null; }, set: function (value) { this._event.touches = value; @@ -247,7 +271,7 @@ if (typeof window !== "undefined") { */ changedTouches: { get: function () { - return this._event.changedTouches; + return this._event ? this._event.changedTouches : null; }, set: function (value) { this._event.changedTouches = value; @@ -259,19 +283,36 @@ if (typeof window !== "undefined") { */ targetTouches: { get: function () { - return this._event.targetTouches; + 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 }, /** * @type {Property} - * @default {Element} null + * @default {boolean} false */ 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; @@ -286,11 +327,30 @@ if (typeof window !== "undefined") { */ 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; } + }, + _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; + } } }, { @@ -328,11 +388,20 @@ if (typeof window !== "undefined") { * @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})); } } }); -} // client-side +//} // client-side 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; diff --git a/core/extras/date.js b/core/extras/date.js index 3540ef7a3d..7194cf5654 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. * @@ -9,6 +12,22 @@ * @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. * @@ -20,6 +39,493 @@ 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) && milliseconds !== 0) { + millisecond = this.millisecond; + millisecond += milliseconds; + this.millisecond = milliseconds; + } + + if(Number.isFinite(seconds) && seconds !== 0) { + second = this.second; + second += seconds; + this.second = second; + } + + if(Number.isFinite(minutes) && minutes !== 0) { + minute = this.minute; + minute += minutes; + this.minute = minute; + } + + if(Number.isFinite(hours) && hours !== 0) { + hour = this.hour; + hour += hours; + this.hour = hour; + } + + if(Number.isFinite(days) && days !== 0) { + myDay = this.day; + myDay += days; + this.day = myDay; + } + + if(Number.isFinite(monthIndex) && monthIndex !== 0) { + month = this.month; + month += monthIndex; + this.month = month; + } + + if(Number.isFinite(year) && year !== 0) { + myYear = this.year; + myYear += year; + this.year = myYear; + } + }, + enumerable: false, + 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 + * + * - 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, + enumerable: false, + 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, + enumerable: false, 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" ); + */ + function _parseRFC3339(dString) { + if (typeof dString != 'string' || !_parseRFC3339.endsByZ.test(dString)) return; + var result, + d = dString.match(_parseRFC3339.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; + }; + _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; + + + if(!Object.getOwnPropertyDescriptor(Date.prototype, 'year')) { + + Object.defineProperties(Date.prototype,{ + "year": { + get: function() { + return this.getFullYear(); + }, + set: function(value) { + return this.setFullYear(value); + }, + configurable: true + }, + "month": { + get: function() { + //Date API is 0 based + return this.getMonth()+1; + }, + set: function(value) { + //Date API is 0 based + return this.setMonth(value-1); + }, + configurable: true + }, + "day": { + get: function() { + return this.getDate(); + }, + set: function(value) { + return this.setDate(value); + }, + configurable: true + }, + "hour": { + get: function() { + return this.getHours(); + }, + set: function(value) { + return this.setHours(value); + }, + configurable: true + }, + "minute": { + get: function() { + return this.getMinutes(); + }, + set: function(value) { + return this.setMinutes(value); + }, + configurable: true + }, + "second": { + get: function() { + return this.getSeconds(); + }, + set: function(value) { + return this.setSeconds(value); + }, + configurable: true + }, + "millisecond": { + get: function() { + return this.getMilliseconds(); + }, + set: function(value) { + return this.setMilliseconds(value); + }, + configurable: true + } + }); +} + + +Object.defineProperty(Date.prototype, "isToday", { + get: function () { + var today = new Date(); + return this.day === today.day && + this.month === today.month && + this.year === today.year; + }, + 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, + * 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 +*/ + +/* +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 + +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 +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' + +exports- const FORMAT_DEFAULT = 'YYYY-MM-DDTHH:mm:ssZ' + +exports- const INVALID_DATE_STRING = 'Invalid Date' + +// regex +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 +*/ + +/* +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/core/extras/object.js b/core/extras/object.js index 9eefa95aec..e2496a5dac 100644 --- a/core/extras/object.js +++ b/core/extras/object.js @@ -82,7 +82,7 @@ Object.defineProperty(Object.prototype, "clear", { if (Object.hasOwnProperty('deleteBinding') === false) { Object.defineProperty(Object, "deleteBinding", { value: function (target, targetPath) { - var Bindings = require("frb"); + var Bindings = require("core/frb/bindings"); Bindings.cancelBinding(target, targetPath); }, writable: true, @@ -95,7 +95,7 @@ if (Object.hasOwnProperty('deepFreeze') === false) { value: function (object) { var propertyNames = Object.getOwnPropertyNames(object), property; - + for (var i = 0, length = propertyNames.length; i < length; i++) { if ((property = object[propertyNames[i]]) !== null && typeof property === 'object' @@ -103,7 +103,7 @@ if (Object.hasOwnProperty('deepFreeze') === false) { Object.deepFreeze(property); } } - + return Object.freeze(object); }, writable: true, diff --git a/core/extras/string.js b/core/extras/string.js index 58eb57c0ec..67c9e7da6e 100644 --- a/core/extras/string.js +++ b/core/extras/string.js @@ -1,4 +1,4 @@ -var Map = require("collections/map"); +var Map = require("../collections/map"); /** @@ -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,7 +56,125 @@ 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) { + 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); + }, + enumerable: false, + 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); + }, + 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, + }); + +} + +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 + }); +} 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..156462adf8 --- /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("core/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("montage/core/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("core/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("core/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("core/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("core/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("core/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("core/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("core/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("core/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("core/frb/bindings"); +require("core/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("core/frb/bindings"); + +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("core/frb/bindings"); +``` + +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("core/frb/bindings"); + +// 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("core/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("core/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("core/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("core/frb/parse"); +var compile = require("core/frb/compile-evaluator"); +var Scope = require("core/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("core/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("core/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("core/frb/parse"); +var compileObserver = require("core/frb/compile-observer"); +var compileBinder = require("core/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..e80b7cc14a --- /dev/null +++ b/core/frb/algebra.js @@ -0,0 +1,158 @@ + +// 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 = { + + memo: new Map(), + + _cacheSolve: function (target, source, targetMemo) { + var targetSourceMemo, + simplification, + canRotateTargetToSource, + canRotateSourceToTarget; + + while (true) { + // simplify the target + while (this.simplifiers.hasOwnProperty(target.type)) { + simplification = this.simplifiers[target.type](target); + if (simplification) { + target = simplification; + } else { + break; + } + } + canRotateTargetToSource = this.rotateTargetToSource.hasOwnProperty(target.type); + 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]; + } + } + //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 (targetMemo && targetMemo.get(source)) || this._cacheSolve(target, source, targetMemo); + }, + + 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..a4b4780c2c --- /dev/null +++ b/core/frb/bind.js @@ -0,0 +1,274 @@ + +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, + 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 = 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) { + 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..31e6f6630b --- /dev/null +++ b/core/frb/bindings.js @@ -0,0 +1,119 @@ + +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) { + for (var i=0, name, keys = Object.keys(descriptors); (name = keys[i]); i++) { + 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..58a725efb0 --- /dev/null +++ b/core/frb/compile-assigner.js @@ -0,0 +1,213 @@ + +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 = [], + semantics = this.compileEvaluator.semantics; + for(var i=0, args = syntax.args, countI = args.length;i + + + + 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..94770fe3a0 --- /dev/null +++ b/core/frb/language-temp.js @@ -0,0 +1,4 @@ +exports.precedence = require("core/frb/language").precedence; +exports.precedenceLevels = require("core/frb/language").precedenceLevels; +exports.operatorTokens = require("core/frb/language").operatorTokens; +exports.operatorTypes = require("core/frb/language").operatorTypes; diff --git a/core/frb/language.js b/core/frb/language.js new file mode 100644 index 0000000000..8b60ed9f2f --- /dev/null +++ b/core/frb/language.js @@ -0,0 +1,68 @@ + +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..0ddd6c02b3 --- /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..adc9b891df --- /dev/null +++ b/core/frb/observers.js @@ -0,0 +1,1853 @@ + +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) { + for(var plusMapped = [], j = 0, countJ = plus.length;(j [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..987210dfca --- /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..09bc40ca4b --- /dev/null +++ b/core/frb/parse-temp.js @@ -0,0 +1 @@ +exports = require("core/frb/parse"); diff --git a/core/frb/parse.js b/core/frb/parse.js new file mode 100644 index 0000000000..7be990557d --- /dev/null +++ b/core/frb/parse.js @@ -0,0 +1,35 @@ + +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 syntax; + if (Array.isArray(text)) { + return { + type: "tuple", + args: text.map(function (text) { + return parse(text, options); + }) + }; + } else if (!options && (syntax = memo.get(text))) { + return syntax; + } else { + try { + 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..4a8044cf0c --- /dev/null +++ b/core/frb/samples/temperature-converter/temperature-converter.js @@ -0,0 +1,34 @@ + +var Bindings = require("core/frb/bindings"); +var PrecisionConverter = require("./precision-converter"); + +// the DOM module adds support for property change listeners to the DOM element +// prototypes +require("core/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..f497ad40c9 --- /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..682989eb19 --- /dev/null +++ b/core/frb/spec/assign-spec.js @@ -0,0 +1,174 @@ + +var assign = require("../assign"); +var Map = require("../../../core/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..0357516edd --- /dev/null +++ b/core/frb/spec/bind-spec.js @@ -0,0 +1,617 @@ + +var bind = require("../bind"); +var SortedSet = require("../../../core/collections/sorted-set"); +var Map = require("../../../core/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..a41f81b491 --- /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..a1ad16c468 --- /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..097e58c5c1 --- /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..430441b0e6 --- /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..d12df59103 --- /dev/null +++ b/core/frb/spec/language.js @@ -0,0 +1,1203 @@ +// 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: "2 + 2 == $", + syntax: {type: "equals", args: [ + {type: "add", args: [ + {type: "literal", value: 2}, + {type: "literal", value: 2} + ]}, + {type: "parameters"} + ]} + }, + + { + 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..bac18ccfe8 --- /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..dfdcff7ad9 --- /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..996ce25c13 --- /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..ebe970df03 --- /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/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; + diff --git a/core/frb/syntax-properties.js b/core/frb/syntax-properties.js new file mode 100644 index 0000000000..3720db2b20 --- /dev/null +++ b/core/frb/syntax-properties.js @@ -0,0 +1,57 @@ +/** + * 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": "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); +} diff --git a/core/gate.js b/core/gate.js index a78845806e..ab346e3ad7 100644 --- a/core/gate.js +++ b/core/gate.js @@ -6,7 +6,7 @@ */ var Montage = require("./core").Montage, logger = require("./logger").logger("gate"), - Map = require("collections/map"); + Map = require("core/collections/map"); /** * @class Gate @@ -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; } @@ -84,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); } }, @@ -100,7 +111,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 +125,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 +141,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 +190,7 @@ var Gate = exports.Gate = Montage.specialize(/** @lends Gate.prototype # */ { reset: { enumerable: false, value: function () { - this.table = {}; + this.table = null; this.count = 0; } }, @@ -190,13 +201,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; } } diff --git a/core/locale.js b/core/locale.js new file mode 100644 index 0000000000..a121a34caa --- /dev/null +++ b/core/locale.js @@ -0,0 +1,349 @@ +var Montage = require("./core").Montage, +Calendar = require("./date/calendar").Calendar, +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'; +} + + +*/ + +/* +a valid source of all locales: + +https://github.com/unicode-cldr/cldr-core/blob/master/availableLocales.json + +*/ + + +/** + 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. + + + References: + - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation + - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat + - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator + - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat + - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules (x Safari) + - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/getCanonicalLocales + + + @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({ + identifier: { + value: undefined + }, + + /** + * 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, + * values are valid Unicode tag values. + * + * @returns {Locale} a new Locale instance. + */ + initWithIdentifier: { + value: function(localeIdentifier, options) { + this.identifier = localeIdentifier; + return this; + } + }, + _localeByIdentifier: { + 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 */ + + /** + * 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 + }, + language: { + get: function() { + if(!this._language) { + this._buildLanguageRegion(); + } + return this._language; + } + }, + _buildLanguageRegion: { + value: function() { + //Could be "en-Latn-US" in a browser? so we pick first for language + //and last for country + var split = this.identifier.split("-"); + this._language = split[0]; + this._region = split.length > 1 ? split[split.length-1] : "*"; + } + }, + /** + * 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 + }, + region: { + get: function() { + if(!this._region) { + this._buildLanguageRegion(); + } + return this._region; + } + }, + + /** + * 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(systemLocaleIdentifier).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._systemLocale; + }, + 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); + } + } +}); + +Locale.Calendar = Calendar; 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" } + } + } +} diff --git a/core/localizer.js b/core/localizer.js index 30e0fc8cad..f21ac7774c 100644 --- a/core/localizer.js +++ b/core/localizer.js @@ -17,11 +17,11 @@ var Montage = require("./core").Montage, Deserializer = require("./serialization/deserializer/montage-deserializer").MontageDeserializer, Promise = require("./promise").Promise, Bindings = require("./core").Bindings, - FrbBindings = require("frb/bindings"), - stringify = require("frb/stringify"), - expand = require("frb/expand"), - Map = require("collections/map"), - Scope = require("frb/scope"); + FrbBindings = require("core/frb/bindings"), + stringify = require("core/frb/stringify"), + expand = require("core/frb/expand"), + Map = require("core/collections/map"), + Scope = require("core/frb/scope"); // Add all locales to MessageFormat object MessageFormat.locale = require("./messageformat-locale"); @@ -1165,7 +1165,7 @@ var Message = exports.Message = Montage.specialize( /** @lends Message.prototype var scope, syntax = input.sourceSyntax; - + if (input.source !== object) { var reference = serializer.addObjectReference(input.source); scope = new Scope({ @@ -1221,7 +1221,7 @@ var createMessageBinding = function (object, prop, key, defaultMessage, data, de Bindings.defineBinding(dataMap, ".get('" + d + "')", property, { components: deserializer }); - } + } } } diff --git a/core/meta/module-object-descriptor.js b/core/meta/module-object-descriptor.js index 3318edb8af..f2683adbc6 100644 --- a/core/meta/module-object-descriptor.js +++ b/core/meta/module-object-descriptor.js @@ -112,7 +112,7 @@ var ModuleObjectDescriptor = exports.ModuleObjectDescriptor = ObjectDescriptor.s /** - * The name of the export this object descriptor is for. + * The name of the export. this object descriptor is for. * @type {string} */ exportName: { @@ -123,6 +123,18 @@ var ModuleObjectDescriptor = exports.ModuleObjectDescriptor = ObjectDescriptor.s serializable: false, value: null } + /* + , + + prepareToHandleDataEvents: { + value: function (event) { + if(this.object) { + this.object.prepareToHandleEvent(event); + } + } + }, + */ + }, /** @lends ModuleObjectDescriptor. */ { diff --git a/core/meta/module-object-descriptor.mjson b/core/meta/module-object-descriptor.mjson new file mode 100644 index 0000000000..e3d18b10a5 --- /dev/null +++ b/core/meta/module-object-descriptor.mjson @@ -0,0 +1,76 @@ +{ + "module_descriptor": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "module", + "objectDescriptor": { + "@": "root" + }, + "mandatory": true, + "valueType": "object", + "valueObjectPrototypeName": "ModuleReference", + "valueObjectModuleId": "core/module-reference", + "helpKey": "" + } + }, + "exportName_descriptor": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "exportName", + "objectDescriptor": { "@": "root" }, + "valueType": "string", + "helpKey": "" + } + }, + "object_descriptor": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "object", + "objectDescriptor": { + "@": "root" + }, + "mandatory": true, + "valueType": "object", + "helpKey": "" + } + }, + "root": { + "prototype": "core/meta/module-object-descriptor", + "values": { + "name": "ModuleObjectDescriptor", + "customPrototype": false, + "propertyDescriptors": [ + { + "@": "module_descriptor" + }, + { + "@": "exportName_descriptor" + }, + { + "@": "object_descriptor" + } + ], + "propertyDescriptorGroups": { + "associations": [ + { + "@": "module_descriptor" + }, + { + "@": "exportName_descriptor" + }, + { + "@": "object_descriptor" + } + ] + }, + "propertyValidationRules": {}, + "objectDescriptorModule": { + "%": "core/meta/module-object-descriptor.mjson" + }, + "exportName": "ModuleObjectDescriptor", + "module": { + "%": "core/meta/module-object-descriptor" + } + } + } +} diff --git a/core/meta/object-descriptor.js b/core/meta/object-descriptor.js index 5b6a927b61..b03f97c2e9 100644 --- a/core/meta/object-descriptor.js +++ b/core/meta/object-descriptor.js @@ -1,12 +1,13 @@ var Montage = require("../core").Montage, + Target = require("core/target").Target, DerivedDescriptor = require("./derived-descriptor").DerivedDescriptor, EventDescriptor = require("./event-descriptor").EventDescriptor, - Map = require("collections/map"), + Map = require("../collections/map"), ModelModule = require("./model"), Promise = require("../promise").Promise, PropertyDescriptor = require("./property-descriptor").PropertyDescriptor, PropertyValidationRule = require("./validation-rule").PropertyValidationRule, - Set = require("collections/set"), + Set = require("../collections/set"), deprecate = require("../deprecate"); @@ -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" @@ -33,6 +34,7 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.specialize( /** @lends this._propertyDescriptorGroups = {}; this._eventPropertyDescriptorsTable = new Map(); this.defineBinding("eventDescriptors", {"<-": "_eventDescriptors.concat(parent.eventDescriptors)"}); + this.defineBinding("localizablePropertyNames", {"<-": "localizablePropertyDescriptors.name"}); } }, @@ -112,7 +114,7 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.specialize( /** @lends deprecate.deprecationWarningOnce("parent reference via ObjectDescriptorReference", "direct reference with object syntax"); this._parentReference = parentReference; } else { - this._parent = parentReference; + this.parent = parentReference; } } @@ -252,7 +254,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: { @@ -304,10 +306,11 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.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("_"); } }, @@ -350,11 +353,115 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.specialize( /** @lends get: function () { return this._parent; }, - set: function (objectDescriptor) { - this._parent = objectDescriptor; + set: function (value) { + if(this._parent !== value) { + + //Remove from current parent + if(this._parent) { + this._parent.removeChildObjectDescriptor(this); + } + + this._parent = value; + + //Add to new parent + if(value) { + value.addChildObjectDescriptor(this); + } + } + } + }, + + _childObjectDescriptors: { + value: undefined + }, + childObjectDescriptors: { + get: function () { + return this._childObjectDescriptors || (this._childObjectDescriptors = []); + }, + set: function (value) { + if(value !== this._childObjectDescriptors) { + this._childObjectDescriptors = value; + } + } + }, + addChildObjectDescriptor: { + value: function(value) { + if(value) { + if(this.childObjectDescriptors.indexOf(value) === -1) { + this.childObjectDescriptors.push(value); + this._clearDescendants(); + } + } + } + }, + removeChildObjectDescriptor: { + value: function(value) { + var index = this.childObjectDescriptors.indexOf(value); + + if(index !== -1) { + this.childObjectDescriptors.splice(index,1); + this._clearDescendants(); + } + + } + }, + _descendantDescriptors: { + value: undefined + }, + _clearDescendants: { + value: function() { + this._descendantDescriptors = undefined; + if(this._parent) { + this._parent._clearDescendants(); + } + } + }, + descendantDescriptors: { + get: function () { + if(this._descendantDescriptors === undefined) { + + var descendants = null, + push = Array.prototype.push, + currentChildren = this._childObjectDescriptors, + i, countI, iChild, iDescendants; + + if(currentChildren) { + for(i=0, countI = currentChildren.length; (iChild = currentChildren[i]);i++) { + (descendants || (descendants = [])).push(iChild); + iDescendants = iChild.descendantDescriptors; + if(iDescendants) { + push.apply(descendants, iDescendants); + } + } + } + this._descendantDescriptors = descendants; + } + return this._descendantDescriptors; + }, + set: function (value) { + if(value !== this._descendantDescriptors) { + this._descendantDescriptors = value; + } + } + }, + + /** + * An ObjectDescriptor's next target is it's parent or in the end the mainService. + * @property {boolean} serializable + * @property {Component} value + */ + _nextTarget: { + value: false + }, + + nextTarget: { + serializable: false, + get: function() { + return this._nextTarget || (this._nextTarget = (this.parent || this.eventManager.application.mainService.childServiceForType(this) || this.eventManager.application.mainService)); } }, + /** * Defines whether the object descriptor should use custom prototype for new * instances. @@ -386,6 +493,14 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.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); + this.localizablePropertyDescriptors.delete(descriptor); + } } } @@ -399,11 +514,34 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.specialize( /** @lends this._propertyDescriptorsTable.set(descriptor.name, descriptor); } descriptor._owner = descriptor._owner || this; + + if(descriptor.isLocalizable) { + //this.localizablePropertyDescriptors.push(descriptor); + this.localizablePropertyDescriptors.add(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); + this.localizablePropertyDescriptors.add(object); + } else { + //this.localizablePropertyDescriptors.splice(this.localizablePropertyDescriptors.indexOf(object), 1); + this.localizablePropertyDescriptors.delete(object); + } + } + }, + _preparePropertyDescriptorsCache: { value: function () { var ownDescriptors = this._ownPropertyDescriptors, @@ -425,6 +563,15 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.specialize( /** @lends descriptor._owner = this; this._propertyDescriptors.push(descriptor); this._propertyDescriptorsTable.set(descriptor.name, descriptor); + + if(descriptor.isLocalizable) { + + //this.localizablePropertyDescriptors.push(descriptor); + this.localizablePropertyDescriptors.add(descriptor); + descriptor.addOwnPropertyChangeListener("isLocalizable", this); + + } + } this.addRangeAtPathChangeListener("_ownPropertyDescriptors", this, "_handlePropertyDescriptorsRangeChange"); this.addRangeAtPathChangeListener("parent.propertyDescriptors", this, "_handlePropertyDescriptorsRangeChange"); @@ -433,6 +580,7 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.specialize( /** @lends } }, + /** * PropertyDescriptors for this object descriptor, not including those * provided by this.parent @@ -451,6 +599,27 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.specialize( /** @lends value: false }, + _localizablePropertyDescriptors: { + value: undefined + }, + + /** + * A Set of an ObjectDescriptor's Property Descriptors that are localizable + * + * @private + * @property {Set} + */ + localizablePropertyDescriptors: { + get: function() { + //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. @@ -509,6 +678,19 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.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. * @@ -635,6 +817,7 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.specialize( /** @lends }, /** + * FIXME: Should probably be named propertyDescriptorNamed or propertyDescriptorWithName * @function * @param {string} name * @returns {PropertyDescriptor} @@ -973,6 +1156,64 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.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 @@ -980,20 +1221,36 @@ var ObjectDescriptor = exports.ObjectDescriptor = Montage.specialize( /** @lends * array otherwise. */ evaluateRules: { - value: function (objectInstance) { + value: deprecate.deprecateMethod(void 0, function (objectInstance) { var name, rule, messages = []; 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; - } + }, "addEventBlueprint", "addEventDescriptor") }, objectDescriptorModuleId: require("../core")._objectDescriptorModuleIdDescriptor, diff --git a/core/meta/property-descriptor.js b/core/meta/property-descriptor.js index c22f891889..5d58de4fb2 100644 --- a/core/meta/property-descriptor.js +++ b/core/meta/property-descriptor.js @@ -1,28 +1,10 @@ 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"); -// TODO change Defaults[*] to Defaults.* throughout. Needless performance -// degradations. -var Defaults = { - name: "default", - cardinality: 1, - mandatory: false, - readOnly: false, - denyDelete: false, - inversePropertyName: void 0, - valueType: "string", - collectionValueType: "list", - valueObjectPrototypeName: "", - valueObjectModuleId: "", - valueDescriptor: void 0, - enumValues: [], - defaultValue: void 0, - helpKey: "" -}; - - /* TypeDescriptor */ /* DeleteRules */ @@ -42,13 +24,68 @@ var Defaults = { For example, if you delete a department, fire all the employees in that department at the same time. - No Action + 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", + cardinality: 1, + isMandatory: false, + readOnly: false, + 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: "", + isLocalizable: false, + isSearchable: false, + isOrdered: false, + isUnique: false, + isSerializable: true, + hasUniqueValues: false, +}; + + +/* +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. + +*/ + + /** @@ -66,9 +103,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; } }, @@ -97,14 +134,19 @@ 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); - 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, "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); } @@ -112,6 +154,13 @@ 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, "isLocalizable", this.isLocalizable); + 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); } }, @@ -134,19 +183,29 @@ 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"); + 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"); this._overridePropertyWithDefaults(deserializer, "definition"); this._overridePropertyWithDefaults(deserializer, "inversePropertyName"); + this._overridePropertyWithDefaults(deserializer, "isLocalizable"); + this._overridePropertyWithDefaults(deserializer, "isSerializable"); + this._overridePropertyWithDefaults(deserializer, "isSearchable"); + this._overridePropertyWithDefaults(deserializer, "isOrdered"); + this._overridePropertyWithDefaults(deserializer, "isUnique"); + this._overridePropertyWithDefaults(deserializer, "hasUniqueValues"); + this._overridePropertyWithDefaults(deserializer, "description"); } }, @@ -191,15 +250,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; } }, @@ -227,12 +287,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 @@ -257,19 +328,24 @@ 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 */ cardinality: { - value: Defaults["cardinality"] + value: Defaults.cardinality }, /** * @type {boolean} * @default false */ - mandatory: { - value: Defaults["mandatory"] + isMandatory: { + value: Defaults.isMandatory }, /** @@ -277,7 +353,20 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * @default false */ denyDelete: { - value: Defaults["denyDelete"] + get: function() { + return this.deleteRule === DeleteRule.DENY; + }, + set: function(value) { + this.deleteRule = DeleteRule.DENY; + } + }, + + /** + * @type {boolean} + * @default false + */ + deleteRule: { + value: Defaults.deleteRule }, /** @@ -285,7 +374,7 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * @default false */ readOnly: { - value: Defaults["readOnly"] + value: Defaults.readOnly }, /** @@ -306,10 +395,69 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# */ isDerived: { get: function () { - return false; + return !!this.definition; } }, + /** + * 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 + */ + isSearchable: { + value: Defaults.isSearchable + }, + + /** + * 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 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 + * 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 @@ -326,6 +474,27 @@ 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 + * 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 @@ -333,28 +502,32 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * this. */ valueType: { - value: Defaults["valueType"] + value: Defaults.valueType }, + /** * @type {string} + * + * 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: { - value: Defaults["collectionValueType"] + value: Defaults.collectionValueType }, /** * @type {string} */ valueObjectPrototypeName: { - value: Defaults["valueObjectPrototypeName"] + value: Defaults.valueObjectPrototypeName }, /** * @type {string} */ valueObjectModuleId: { - value: Defaults["valueObjectModuleId"] + value: Defaults.valueObjectModuleId }, /** @@ -364,6 +537,9 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * return a promise. * @type {string} */ + _valueDescriptorReference: { + value: undefined + }, valueDescriptor: { serializable: false, get: function () { @@ -381,6 +557,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 }, @@ -408,11 +612,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, @@ -423,25 +627,60 @@ exports.PropertyDescriptor = Montage.specialize( /** @lends PropertyDescriptor# * possible values are: "reference" | "value" | "auto" | true | false, * @default false */ - serializable: { - value: true + isSerializable: { + value: Defaults.isSerializable + }, + + /** + * @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 + */ + isLocalizable: { + 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 }, + + 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 */ @@ -472,6 +711,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..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,7 +225,43 @@ "valueType": "boolean", "enumValues": [], "helpKey": "", - "serializable": true + "isSerializable": true + } + }, + + "property_isLocalizable": { + "prototype": "core/meta/property-descriptor", + "values": { + "name": "isLocalizable", + "objectDescriptor": { + "@": "root" + }, + "cardinality": 1, + "mandatory": false, + "denyDelete": false, + "readOnly": false, + "valueType": "boolean", + "enumValues": [], + "helpKey": "", + "isSerializable": 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": "", + "isSerializable": true } }, @@ -247,6 +283,9 @@ { "@": "property_valueType" }, + { + "@": "property_valueDescriptor" + }, { "@": "property_collectionValueType" }, @@ -268,6 +307,12 @@ { "@": "property_enumValues" }, + { + "@": "property_isSerializable" + }, + { + "@": "property_isLocalizable" + }, { "@": "property_helpKey" } @@ -286,6 +331,9 @@ { "@": "property_valueType" }, + { + "@": "property_valueDescriptor" + }, { "@": "property_collectionValueType" }, @@ -308,6 +356,12 @@ "@": "property_enumValues" }, { + "@": "property_isSerializable" + }, + { + "@": "property_isLocalizable" + }, + { "@": "property_helpKey" } ] 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" } + } + } +} 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..739edbf85c --- /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..b377734caa --- /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..dc0f411ccd --- /dev/null +++ b/core/mr/test/spec/main/node_modules/dot.js/a.js @@ -0,0 +1 @@ +module.exports = 50; 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..9320b557ea --- /dev/null +++ b/core/mr/test/spec/main/node_modules/js-ext/a.js @@ -0,0 +1 @@ +module.exports = 20; 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..9dbb4c8b09 --- /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..cb7803908b --- /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..6a825f4750 --- /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("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'); 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; + } + }; + 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" } + } + } +} 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..993806ccbe --- /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": Buffer.from("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(Buffer.from("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..83cf72c79a --- /dev/null +++ b/core/promise-io/buffer-stream.js @@ -0,0 +1,59 @@ + +var Q = require("../q"); +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 = 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); + } + } + 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..c39652c16e --- /dev/null +++ b/core/promise-io/coverage-report.js @@ -0,0 +1,44 @@ + +require("../collections/shim"); +var Q = require("../q"); +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..1e19524327 --- /dev/null +++ b/core/promise-io/fs-common.js @@ -0,0 +1,492 @@ +var Q = require("../q"); +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 = Buffer.from(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..34c7aae875 --- /dev/null +++ b/core/promise-io/fs-mock.js @@ -0,0 +1,559 @@ + +var Q = require("../q"); +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("core/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 = Buffer.from(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); + // Clone chunks to avoid side effect + var bufferChunks = fileNode._chunks.slice(); + return new BufferStream(bufferChunks, 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 || new 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..97ed5d61f3 --- /dev/null +++ b/core/promise-io/fs-root.js @@ -0,0 +1,145 @@ + +var Q = require("../q"); +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..feaf7d492e --- /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"); +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..4c241d991a --- /dev/null +++ b/core/promise-io/fs2http.js @@ -0,0 +1,65 @@ + +var Q = require("../q"); +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..8d7f383520 --- /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"); +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..9745a43500 --- /dev/null +++ b/core/promise-io/http-apps/content.js @@ -0,0 +1,87 @@ +var Q = require("../../q"); +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..534067d375 --- /dev/null +++ b/core/promise-io/http-apps/cookie.js @@ -0,0 +1,177 @@ + +var Q = require("../../q"); +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..8982cb4693 --- /dev/null +++ b/core/promise-io/http-apps/decorators.js @@ -0,0 +1,178 @@ + +var Q = require("../../q"); +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..768c1fe745 --- /dev/null +++ b/core/promise-io/http-apps/fs.js @@ -0,0 +1,417 @@ + +var Q = require("../../q"); +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..532b3ff50f --- /dev/null +++ b/core/promise-io/http-apps/html.js @@ -0,0 +1,58 @@ + +var Q = require("../../q"); +// 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..ffa0998d93 --- /dev/null +++ b/core/promise-io/http-apps/json.js @@ -0,0 +1,78 @@ +var Q = require("../../q"); +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..f97a384930 --- /dev/null +++ b/core/promise-io/http-apps/negotiate.js @@ -0,0 +1,120 @@ + +var Q = require("../../q"); +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..e1b5a5f907 --- /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"); + +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..02119ea098 --- /dev/null +++ b/core/promise-io/http-apps/redirect.js @@ -0,0 +1,209 @@ + +var Q = require("../../q"); +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..863f72faee --- /dev/null +++ b/core/promise-io/http-apps/route.js @@ -0,0 +1,125 @@ + +var Q = require("../../q"); +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..ab2fe9a999 --- /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"); +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..48e94c53df --- /dev/null +++ b/core/promise-io/reader.js @@ -0,0 +1,133 @@ + +var Q = require("../q"); + +/** + * 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 = Buffer.alloc(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..3f1f900911 --- /dev/null +++ b/core/promise-io/spec/fs/issues/1-spec.js @@ -0,0 +1,33 @@ + +require("../../lib/jasmine-promise"); +var Q = require("../../../../q"); +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..c1fe73628f --- /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"); +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..0dcb2fa684 --- /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"); +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..3c1a8fe7d4 --- /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"); +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..079b94f383 --- /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"); +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..af633d75e6 --- /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"); +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..cc6fde0c55 --- /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"); +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..b07dfbd944 --- /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"); +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..9761313790 --- /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"); +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..a530fbf3f9 --- /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"); +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..95b3cb2b4a --- /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"); +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..5d8afb4f94 --- /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"); +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..217f2589d3 --- /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"); +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..2c0d8bbc5b --- /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"); +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..0aac78e968 --- /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"); +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..abfedae177 --- /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"); +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..756bce7723 --- /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"); +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..222a38c983 --- /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, Buffer.from(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..be3b293c11 --- /dev/null +++ b/core/promise-io/spec/http-apps/cookie-spec.js @@ -0,0 +1,52 @@ + +require("../lib/jasmine-promise"); +var Q = require("../../../q"); +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..7094742a3d --- /dev/null +++ b/core/promise-io/spec/http-apps/hosts-spec.js @@ -0,0 +1,49 @@ + +require("../lib/jasmine-promise"); +var Q = require("../../../q"); +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..7ee950a5d6 --- /dev/null +++ b/core/promise-io/spec/http-apps/proxy-spec.js @@ -0,0 +1,82 @@ + +require("../lib/jasmine-promise"); +var Q = require("../../../q"); +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..439f8d26c0 --- /dev/null +++ b/core/promise-io/spec/http/agent-spec.js @@ -0,0 +1,97 @@ +require("../lib/jasmine-promise"); +var Q = require("../../../q"); +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..879982efbf --- /dev/null +++ b/core/promise-io/spec/http/basic-spec.js @@ -0,0 +1,107 @@ + +require("../lib/jasmine-promise"); +var Q = require("../../../q"); +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..5d9a72a65e --- /dev/null +++ b/core/promise-io/spec/lib/jasmine-promise.js @@ -0,0 +1,42 @@ +"use strict"; + +var Q = require("../../../q"); + +/** + * 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..e5cf865348 --- /dev/null +++ b/core/promise-io/writer.js @@ -0,0 +1,113 @@ + +var Q = require("../q"); + +/** + * 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 = Buffer.from(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 +} + diff --git a/core/promise.js b/core/promise.js index 2dd545812a..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 @@ -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; 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); +}); diff --git a/core/range-controller.js b/core/range-controller.js index e59482dc84..dbac34fc48 100644 --- a/core/range-controller.js +++ b/core/range-controller.js @@ -2,8 +2,8 @@ * @module montage/core/range-controller */ var Montage = require("./core").Montage; -var GenericCollection = require("collections/generic-collection"); -var observableArrayProperties = require("collections/listen/array-changes").observableArrayProperties; +var GenericCollection = require("core/collections/generic-collection"); +var observableArrayProperties = require("core/collections/listen/array-changes").observableArrayProperties; // The content controller is responsible for determining which content from a // source collection are visible, their order of appearance, and whether they @@ -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; @@ -123,24 +128,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.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.findValue(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; @@ -309,6 +345,9 @@ var RangeController = exports.RangeController = Montage.specialize( /** @lends R * "name.startsWith('A')" to see only names starting with 'A'. * If the `filterPath` is null, all content is accepted. * + * TODO: this needs to be at least renamed to filterExpression, + * but it should really be a criteria object. + * * @property {string} value */ filterPath: {value: null}, @@ -664,6 +703,27 @@ var RangeController = exports.RangeController = Montage.specialize( /** @lends R } }, + + /** + * @private + */ + _contentDescriptor: { + value: null + }, + contentDescriptor: { + get: function () { + return this._contentDescriptor; + }, + set: function (value) { + this._contentDescriptor = value; + } + }, + + /** + * TODO: reconciliate contentDescriptor's module with + * _contentConstructor when contentDescriptor is used. + */ + /** * @private */ @@ -709,22 +769,24 @@ 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); - - // 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]); - } + 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]); + } } }, @@ -772,7 +834,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); } } }, diff --git a/core/range.js b/core/range.js new file mode 100644 index 0000000000..b621e1210c --- /dev/null +++ b/core/range.js @@ -0,0 +1,207 @@ +/* + 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"), +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 - excludes bounds +// * `[]` | closed - iclude bounds +// * `[)` | 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; + } 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; + +/** + * 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); +}; + +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; + } +}; + +Object.defineProperty(Range.prototype,"length", { + get: function (serializer) { + if(this.isFinite) { + return this.end - this.begin; + } else { + return Infinity; + } + } +}); + +/** + * 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); +}; diff --git a/core/range.mjson b/core/range.mjson new file mode 100644 index 0000000000..1d989bae4b --- /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": "./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.mjson" }, + "exportName": "Range", + "module": { "%": "./range" }, + "object":{ "@": "range" } + } + } +} diff --git a/core/serialization/bindings.js b/core/serialization/bindings.js index 2551cfe573..ba02d7e665 100644 --- a/core/serialization/bindings.js +++ b/core/serialization/bindings.js @@ -1,9 +1,9 @@ -var Bindings = require("frb"), - stringify = require("frb/stringify"), - assign = require("frb/assign"), - evaluate = require("frb/evaluate"), - expand = require("frb/expand"), - Scope = require("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]; @@ -98,7 +100,7 @@ var deserializeObjectBindings = exports.deserializeObjectBindings = function (de if (ONE_ASSIGNMENT in descriptor) { var value = descriptor[ONE_ASSIGNMENT]; - + assign( object, 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); } } }; diff --git a/core/serialization/deserializer/montage-deserializer.js b/core/serialization/deserializer/montage-deserializer.js index c1b5cc7b3d..7ef161a26a 100644 --- a/core/serialization/deserializer/montage-deserializer.js +++ b/core/serialization/deserializer/montage-deserializer.js @@ -2,8 +2,8 @@ var Montage = require("../../core").Montage, MontageContext = require("./montage-interpreter").MontageContext, MontageReviver = require("./montage-reviver").MontageReviver, BindingsModule = require("../bindings"), - Map = require("collections/map").Map, - Promise = require("core/promise").Promise, + Map = require("core/collections/map").Map, + Promise = require("../../promise").Promise, deprecate = require("../../deprecate"); var MontageDeserializer = exports.MontageDeserializer = Montage.specialize({ @@ -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,6 +66,10 @@ var MontageDeserializer = exports.MontageDeserializer = Montage.specialize({ */ deserialize: { value: function (instances, element) { + if((!this._serializationString || this._serializationString === "") && !this._serialization) { + return null; + } + var context = this._module && MontageDeserializer.moduleContexts.get(this._module), circularError; if (context) { @@ -82,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) { @@ -144,7 +156,7 @@ var MontageDeserializer = exports.MontageDeserializer = Montage.specialize({ ); } locationDesc = MontageReviver.parseObjectLocationId(locationId); - module = moduleLoader.getModule(locationDesc.moduleId, label); + module = moduleLoader.getModule(locationDesc.moduleId, label, this); if (Promise.is(module)) { (promises || (promises = [])).push(module); } diff --git a/core/serialization/deserializer/montage-interpreter.js b/core/serialization/deserializer/montage-interpreter.js index 219f066d38..121937ab75 100644 --- a/core/serialization/deserializer/montage-interpreter.js +++ b/core/serialization/deserializer/montage-interpreter.js @@ -2,7 +2,7 @@ var Montage = require("../../core").Montage, MontageReviver = require("./montage-reviver").MontageReviver, Promise = require("../../promise").Promise, deprecate = require("../../deprecate"), - Set = require("collections/set"), + Set = require("../../collections/set"), ONE_ASSIGNMENT = "=", ONE_WAY = "<-", TWO_WAY = "<->"; @@ -47,26 +47,25 @@ var MontageInterpreter = Montage.specialize({ locationId, locationDesc, module, - promises = []; - - for (var label in serialization) { - if (serialization.hasOwnProperty(label)) { - object = serialization[label]; - locationId = object.prototype || object.object; - - if (locationId) { - if (typeof locationId !== "string") { - throw new Error( - "Property 'object' of the object with the label '" + - label + "' must be a module id" - ); - } - locationDesc = MontageReviver.parseObjectLocationId(locationId); - module = moduleLoader.getModule( - locationDesc.moduleId, label); - if (Promise.is(module)) { - promises.push(module); - } + promises = [], + i, keys, label; + + for (i =0, keys = Object.keys(serialization);(label = keys[i]); i++) { + object = serialization[label]; + locationId = object.prototype || object.object; + + if (locationId) { + if (typeof locationId !== "string") { + throw new Error( + "Property 'object' of the object with the label '" + + label + "' must be a module id" + ); + } + locationDesc = MontageReviver.parseObjectLocationId(locationId); + module = moduleLoader.getModule( + locationDesc.moduleId, label); + if (Promise.is(module)) { + promises.push(module); } } } @@ -106,13 +105,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; @@ -138,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. @@ -156,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 { @@ -278,8 +282,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 ) { diff --git a/core/serialization/deserializer/montage-reviver.js b/core/serialization/deserializer/montage-reviver.js index a81710803f..b78364f090 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(); @@ -69,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; @@ -80,7 +83,7 @@ var ModuleLoader = Montage.specialize({ }, getModule: { - value: function (moduleId, label) { + value: function (moduleId, label, reviver) { var objectRequires = this._objectRequires, _require, module; @@ -97,8 +100,10 @@ var ModuleLoader = Montage.specialize({ module = this.getModuleDescriptor(_require, moduleId).text; } - if (!module) { + if (!module && !reviver._isSync) { module = _require.async(moduleId); + } else { + throw err; } } @@ -146,13 +151,19 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont getTypeOf: { value: function (value) { - var typeOf = typeof value; + var typeOf; if (value === null) { return "null"; } else if (Array.isArray(value)) { return "array"; - } else if (typeOf === "object" && Object.keys(value).length === 1) { + } + //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.__proto__).length === 1) { + } else if ((typeOf = typeof value) === "object" && Object.keys(value).length === 1) { if ("@" in value) { return "reference"; } else if ("/" in value) { @@ -464,7 +475,7 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont objectName = locationId; } else if (locationId) { locationDesc = MontageReviver.parseObjectLocationId(locationId); - module = this.moduleLoader.getModule(locationDesc.moduleId, label); + module = this.moduleLoader.getModule(locationDesc.moduleId, label, this); objectName = locationDesc.objectName; } @@ -473,7 +484,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 + @@ -655,6 +671,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. } }, @@ -848,27 +870,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 === "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); @@ -878,6 +882,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) { @@ -889,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), @@ -948,19 +962,29 @@ var MontageReviver = exports.MontageReviver = Montage.specialize(/** @lends Mont } }, - reviveObjectReference: { + reviveDate: { value: function(value, context, label) { - var valuePath = value["@"], - object = context.getObject(valuePath); - return object; + var date = Date.parseRFC3339(value); + + if (label) { + context.setObjectLabel(date, label); + } + + return date; + } + }, + + reviveObjectReference: { + value: function(value, context, label) { + return context.getObject(value["@"]); } }, reviveArray: { value: function(value, context, label) { var item, - promises = []; + promises; if (label) { context.setObjectLabel(value, label); @@ -970,7 +994,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 { @@ -978,7 +1002,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() { @@ -1126,6 +1150,25 @@ 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"; +_reviveMethodByType["Module"] = "reviveModule"; + + +MontageReviverProto.reviveValue._methodByType = _reviveMethodByType; + +MontageReviverProto = _reviveMethodByType = null; + + MontageReviver.findProxyForElement = function (element) { return PROXY_ELEMENT_MAP.get(element); }; @@ -1167,7 +1210,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/deserializer/serialization-extractor.js b/core/serialization/deserializer/serialization-extractor.js index cdfb122a9f..f546b7f224 100644 --- a/core/serialization/deserializer/serialization-extractor.js +++ b/core/serialization/deserializer/serialization-extractor.js @@ -1,6 +1,6 @@ var Montage = require("../../core").Montage, MontageReviver = require("./montage-reviver").MontageReviver, - parse = require("frb/parse"); + parse = require("core/frb/parse"); var SerializationExtractor = Montage.specialize( { _serialization: {value: null}, @@ -99,7 +99,7 @@ var SerializationExtractor = Montage.specialize( { if (unitSerialization.hasOwnProperty(propertyName)) { binding = unitSerialization[propertyName]; sourcePath = binding["<-"] || binding["<->"]; - this._collectLabelsInBindingPath(sourcePath, labels); + this._collectLabelsInBindingPath(sourcePath, labels); } } } diff --git a/core/serialization/serialization.js b/core/serialization/serialization.js index f767b5c616..5269d6354d 100644 --- a/core/serialization/serialization.js +++ b/core/serialization/serialization.js @@ -6,8 +6,8 @@ var Montage = require("../core").Montage, MontageLabeler = require("./serializer/montage-labeler").MontageLabeler, MontageReviver = require("./deserializer/montage-reviver").MontageReviver, - parse = require("frb/parse"), - stringify = require("frb/stringify"); + parse = require("core/frb/parse"), + stringify = require("core/frb/stringify"); /** * @class Serialization @@ -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); } @@ -112,7 +113,7 @@ var Serialization = Montage.specialize( /** @lends Serialization.prototype # */ this._serializationString = null; } - + return this; } }, @@ -120,15 +121,15 @@ var Serialization = Montage.specialize( /** @lends Serialization.prototype # */ removeObjectsWithLabels: { value: function (labels) { var serializationObject = this.getSerializationObject(); - + if (serializationObject && labels) { - for (var i = 0, length = labels.length; i < length; i++) { + for (var i = 0, length = labels.length; i < length; i++) { this.removeObjectWithLabel(labels[i], serializationObject); } this._serializationString = null; } - + return this; } }, @@ -386,7 +387,7 @@ var SerializationMerger = Montage.specialize(null, /** @lends SerializationMerge inDestination = this._isLabelValidInSerialization( newLabel, serialization1); if (!inDestination) { - renameLabel = !this._isLabelValidInSerialization(newLabel, serialization2) && + renameLabel = !this._isLabelValidInSerialization(newLabel, serialization2) && collisionLabels.indexOf(newLabel) === -1; } @@ -460,7 +461,7 @@ var SerializationMerger = Montage.specialize(null, /** @lends SerializationMerge */ _createCollisionTable: { value: function (labels1, labels2, collisionTable, labeler) { - + labeler = labeler || new MontageLabeler(); var foundCollisions = false, componentLabel, @@ -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); } diff --git a/core/serialization/serializer/montage-ast.js b/core/serialization/serializer/montage-ast.js index 9c6a0f410c..c11b031629 100644 --- a/core/serialization/serializer/montage-ast.js +++ b/core/serialization/serializer/montage-ast.js @@ -45,20 +45,21 @@ 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); } else { result[label] = object; - } + } } - } return result; } @@ -301,3 +302,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-labeler.js b/core/serialization/serializer/montage-labeler.js index eaeb8933f0..4b7685e06c 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(); @@ -94,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; } } }, diff --git a/core/serialization/serializer/montage-malker.js b/core/serialization/serializer/montage-malker.js index feabc1dcca..d239b4d0d3 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,19 +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.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); } }, @@ -40,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 { @@ -69,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); @@ -77,6 +91,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") { @@ -176,6 +192,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); @@ -206,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); + } } } } diff --git a/core/serialization/serializer/montage-serializer.js b/core/serialization/serializer/montage-serializer.js index 3c7b55f798..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; @@ -68,6 +68,12 @@ var MontageSerializer = Montage.specialize({ 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]); @@ -80,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..0c2f5ffaf3 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); @@ -331,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); @@ -355,7 +375,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 +401,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 +452,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 +485,7 @@ var MontageVisitor = Montage.specialize({ this.setObjectCustomUnit(malker, object, unitName); } - } + } } }, @@ -607,6 +627,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); @@ -675,7 +701,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 +709,7 @@ var MontageVisitor = Montage.specialize({ } else { return new Error("Visitor '" + methodName + "' is already registered."); } - } + } } } diff --git a/core/target.js b/core/target.js index ee9bf84ab2..59c4ab81d2 100644 --- a/core/target.js +++ b/core/target.js @@ -4,13 +4,26 @@ 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 * @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 @@ -123,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); } } }, @@ -138,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/core/template.js b/core/template.js index 09bd07a833..d079cedf67 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; /** @@ -68,11 +69,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 +318,7 @@ var Template = Montage.specialize( /** @lends Template# */ { part.objects = objects; self._invokeDelegates(part, instances); part.stopActingAsTopComponent(); - + return part; }); } @@ -488,11 +490,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 +1009,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 +1026,7 @@ var Template = Montage.specialize( /** @lends Template# */ { /* jshint forin: false */ argumentElementsCollisionTable[key] = collisionTable[key]; } - } + } } } @@ -1282,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)); + } } } } @@ -1353,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: { @@ -1451,21 +1458,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; + } } - return Promise.all(promises); + if(asyncPromises && syncScriptPromises) { + promises = asyncPromises.concat(syncScriptPromises); + } else { + promises = asyncPromises || syncScriptPromises; + } + + 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; @@ -1551,7 +1710,7 @@ var TemplateResources = Montage.specialize( /** @lends TemplateResources# */ { documentResources = DocumentResources.getInstanceForDocument(targetDocument); return documentResources.preloadResource(url); } - + return this.resolvedPromise; } }, diff --git a/core/tree-controller.js b/core/tree-controller.js index 1b39abf49f..b72e161573 100644 --- a/core/tree-controller.js +++ b/core/tree-controller.js @@ -1,7 +1,7 @@ var Montage = require("./core").Montage, - WeakMap = require("collections/weak-map"), - parse = require("frb/parse"), - evaluate = require("frb/evaluate"); + WeakMap = require("core/collections/weak-map"), + parse = require("core/frb/parse"), + evaluate = require("core/frb/evaluate"); var TreeNode = exports.TreeNode = Montage.specialize({ diff --git a/core/undo-manager.js b/core/undo-manager.js index 69a22d7907..8e1ee63d0d 100644 --- a/core/undo-manager.js +++ b/core/undo-manager.js @@ -6,8 +6,8 @@ var Montage = require("./core").Montage, Target = require("./target").Target, Promise = require("./promise").Promise, - Map = require("collections/map"), - List = require("collections/list"); + Map = require("core/collections/map"), + List = require("core/collections/list"); var UNDO_OPERATION = 0, REDO_OPERATION = 1; 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", diff --git a/core/web-socket.js b/core/web-socket.js new file mode 100644 index 0000000000..8ff246d71e --- /dev/null +++ b/core/web-socket.js @@ -0,0 +1,247 @@ + +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; + +if(_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", event => this.handleEvent(event), false); + this._webSocket.addEventListener("open", event => this.handleEvent(event), 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", event => this.handleEvent(event), false); + this._webSocket.addEventListener("close", event => this.handleEvent(event), 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. + } + }); + +} +else { + console.error("no native WebSocket implementation available"); +} + 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; diff --git a/data/converter/raw-embedded-value-to-object-converter.js b/data/converter/raw-embedded-value-to-object-converter.js index 654277428e..630fdbe474 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]; @@ -52,7 +58,7 @@ exports.RawEmbeddedValueToObjectConverter = RawValueToObjectConverter.specialize } else { if(v) { - return this._convertOneValue(v,typeToFetch, service); + return self._convertOneValue(v,typeToFetch, service); } } }); @@ -88,18 +94,86 @@ exports.RawEmbeddedValueToObjectConverter = RawValueToObjectConverter.specialize */ revert: { value: function (v) { - if (v) { - if (!this.compiledRevertSyntax) { - return Promise.resolve(v); - } else { - var scope = this.scope; - //Parameter is what is accessed as $ in expressions - scope.value = v; - return Promise.resolve(this.compiledRevertSyntax(scope)); - } + var self = this; + + if(!v) { + return v; + } else { + return Promise.all([this._descriptorToFetch, this.service]).then(function (values) { + var revertedValue, + result, + revertedValuePromise; + + + var objectDescriptor = values[0], + service = values[1]; + + if(Array.isArray(v)) { + if(v.length) { + revertedValue = []; + for(var i=0, countI=v.length, promises;(i} + * */ + + foreignDescriptorMappings: { + value: undefined + }, + + + /* + cache: + + 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; + } + }, + + _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); + } + }, + __areCriteriaSyntaxPropertiesRawDataPrimaryKeys: { + value: undefined + }, + _areCriteriaSyntaxPropertiesRawDataPrimaryKeys: { + value: function(typeToFetch, criteria, service) { + if(this.__areCriteriaSyntaxPropertiesRawDataPrimaryKeys === undefined) { + var mapping = service.mappingForType(typeToFetch); + this.__areCriteriaSyntaxPropertiesRawDataPrimaryKeys = mapping && mapping.rawDataPrimaryKeys && mapping.rawDataPrimaryKeys.equals(syntaxProperties(criteria.syntax)) + } + return this.__areCriteriaSyntaxPropertiesRawDataPrimaryKeys + } + }, + _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(this._areCriteriaSyntaxPropertiesRawDataPrimaryKeys(typeToFetch, criteria, service)) { + + 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, iObject; + + while( i < array.length ) { + dataIdentifier = service.dataIdentifierForTypePrimaryKey(typeToFetch,array[i]); + iObject = rootService.objectForDataIdentifier(dataIdentifier); + if(iObject) { + //Add to result + (existingObject || (existingObject = [])).push(iObject); + //remove from criteria since found + array.splice(i,1); + } else { + i++; + } + } + + } + } + + return existingObject; + } + }, + _fetchConvertedDataForObjectDescriptorCriteria: { + value: function(typeToFetch, criteria, currentRule) { + var self = this; + + return this.service ? this.service.then(function (service) { + + var localResult = self._lookupExistingObjectForObjectDescriptorCriteria(typeToFetch, criteria, service), + //var localResult, + localPartialResult; + + /* + + Leaving a trace of localPartialResult here. Unless I'm missing something, the problem with a partial result is that we don't really have an easy way to return a partial result with the promise-based API, and we still need to get the rest, and that will brinf all values back, the mapping will be faster for the objects that already are in memnory thoygh + */ + if(localResult) { + if(Array.isArray(localResult)) { + if(criteria.parameters.length > 0) { + if(localResult.length) { + //We found some locally but not all + localPartialResult = localResult; + } else { + //we didn't find anything locally + localPartialResult = 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); + + /* + 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. + + 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; + + } + } + } + + */ + + fetchPromise = service.rootService.fetchData(query) + .then(function(value) { + self._unregisterFetchPromiseForObjectDescriptorCriteria(typeToFetch, criteria); + return value; + }); + + self._registerFetchPromiseForObjectDescriptorCriteria(fetchPromise, typeToFetch, criteria); + } + + return fetchPromise; + }) : 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), - 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) { + /* + 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": "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 {return typeof objectDescriptor === "string" ? objectDescriptor : objectDescriptor.module.id})); + } else { + if(this.target instanceof ModuleObjectDescriptor) { + 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); + } + } + } + if(this.referrerId) { + serializer.setProperty("referrerId", this.referrerId); + } + serializer.setProperty("criteria", this._criteria); + + /* + Inlining locales for speed and compactness instead of letting locales serialize themselves + */ + 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); + } + if(this.snapshot) { + serializer.setProperty("snapshot", this.snapshot); + } + } + }, + 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 = DataOperationType[value]; + } + + value = deserializer.getProperty("timeStamp"); + if (value !== void 0) { + this.timeStamp = value; + } + /* keeping dataDescriptor here for temporary backward compatibility */ + 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) { + 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 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; + } + } + + 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; + } + + /* + 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; + } + + } }, /*************************************************************************** @@ -42,6 +361,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 @@ -62,14 +387,6 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ name: { 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". - - * @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 @@ -104,6 +421,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} @@ -121,33 +451,30 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ 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. + * An operation that preceded and this one is related to. For a ReadUpdated, it would be the Read operation. * - * @type {number} + * @type {DataOperation} */ - creationTime: { + referrer: { 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} */ - referrer: { + referrerIdentifier: { value: undefined }, @@ -161,11 +488,30 @@ 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 + }, + + /** + * 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. + * + * @type {Object} + */ + userMessage: { value: undefined }, @@ -201,12 +547,12 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ * * @type {number} */ - index: { + constructionIndex: { value: undefined }, /** - * 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: { @@ -231,6 +577,9 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ value: undefined }, + /* + Might be more straightforward to name this objectDescriptor + */ dataType: { value: undefined }, @@ -301,6 +650,50 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ * } * * + * 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 + * //to the transaction + * + * * @type {Object} */ data: { @@ -309,34 +702,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 */ { @@ -347,49 +712,103 @@ exports.DataOperation = Montage.specialize(/** @lends DataOperation.prototype */ */ Type: { + + /* + Search: is a read operation + + */ value: { - Create: {isCreate: true}, - CreateFailed: {isCreate: true}, - CreateCompleted: {isCreate: true}, - //Additional - Copy: {isCreate: true}, - CopyFailed: {isCreate: true}, - CopyCompleted: {isCreate: true}, - /* Read is the first operation that mnodels a query */ - Read: {isRead: true}, - - /* ReadUpdated is pushed by server when a query's result changes due to data changes from others */ - ReadUpdated: {isRead: true}, - - /* 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 - - /* ReadCancel is the operation that instructs baclkend that client isn't interested by a read operastion anymore */ - ReadCancel: {isRead: true}, - - /* ReadCanceled is the operation that instructs the client that a read operation is canceled */ - ReadCanceled: {isRead: true}, - - /* ReadFailed is the operation that instructs the client that a read operation has failed canceled */ - ReadFailed: {isRead: true}, - /* 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}, - Delete: {isDelete: true}, - DeleteCompleted: {isDelete: true}, - DeleteFailed: {isDelete: true}, - /* 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}, - /* 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} + NoOp: DataOperationType.noop, + 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, + UserAuthenticationCompleted: DataOperationType.userAuthenticationCompleted, + UserAuthenticationFailed: DataOperationType.userAuthenticationFailed, + UserAuthenticationTimedout: DataOperationType.userAuthenticationTimedout, + + UserInput: DataOperationType.userInput, + UserInputCompleted: DataOperationType.userInputCompleted, + UserInputFailed: DataOperationType.userInputFailed, + UserInputCanceled: DataOperationType.userInputCanceled, + UserInputTimedOut: DataOperationType.userInputTimedout, + + ValidateOperation: DataOperationType.validateOperation, + ValidateFailedOperation: DataOperationType.validateFailedOperation, + ValidateCompletedOperation: DataOperationType.validateCompletedOperation, + CancelValidateOperation: DataOperationType.cancelValidateOperation, + ValidateCanceledOperation: DataOperationType.validateCanceledOperation, + + BatchOperation: DataOperationType.batchOperation, + BatchUpdateOperation: DataOperationType.batchUpdateOperation, + BatchCompletedOperation: DataOperationType.batchCompletedOperation, + BatchFailedOperation: DataOperationType.batchFailedOperation, + + CreateTransactionOperation: DataOperationType.createTransactionOperation, + CreateTransactionCompletedOperation: DataOperationType.createTransactionCompletedOperation, + CreateTransactionFailedOperation: DataOperationType.createTransactionFailedOperation, + + TransactionUpdatedOperation: DataOperationType.transactionUpdatedOperation, + + + CreateSavePointOperation: DataOperationType.createSavePointOperation, + + PerformTransactionOperation: DataOperationType.performTransactionOperation, + PerformTransactionProgressOperation: DataOperationType.performTransactionProgressOperation, + PerformTransactionCompletedOperation: DataOperationType.performTransactionCompletedOperation, + PerformTransactionFailedOperation: DataOperationType.performTransactionFailedOperation, + + RollbackTransactionOperation: DataOperationType.rollbackTransactionOperation, + RollbackTransactionCompletedOperation: DataOperationType.rollbackTransactionCompletedOperation, + RollbackTransactionFailedOperation: DataOperationType.rollbackTransactionFailedOperation, + KeepAliveOperation: DataOperationType.keepAliveOperation, + + } } diff --git a/data/service/data-service.js b/data/service/data-service.js index 992379b932..e50fe9e61f 100644 --- a/data/service/data-service.js +++ b/data/service/data-service.js @@ -1,16 +1,31 @@ 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, + 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, - Map = require("collections/map"), + Map = require("core/collections/map"), Promise = require("core/promise").Promise, ObjectDescriptor = require("core/meta/object-descriptor").ObjectDescriptor, - Set = require("collections/set"), - WeakMap = require("collections/weak-map"); - + Set = require("core/collections/set"), + CountedSet = require("core/counted-set").CountedSet, + WeakMap = require("core/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, + 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"); var AuthorizationPolicyType = new Montage(); AuthorizationPolicyType.NoAuthorizationPolicy = AuthorizationPolicy.NONE; @@ -18,6 +33,17 @@ 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; + + + +/** + * Future: Look at https://www.npmjs.com/package/broadcast-channel to implement cross-tab event distribution? + */ + /** * Provides data objects and manages changes to them. * @@ -32,10 +58,11 @@ AuthorizationPolicyType.OnFirstFetchAuthorizationPolicy = AuthorizationPolicy.ON * 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 */ -exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { +exports.DataService = Target.specialize(/** @lends DataService.prototype */ { /*************************************************************************** * Initializing @@ -43,8 +70,27 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { constructor: { value: function DataService() { + + this.defineBinding("mainService", {"<-": "mainService", source: defaultEventManager.application}); + exports.DataService.mainService = exports.DataService.mainService || this; - this._initializeAuthorization(); + if(this === DataService.mainService) { + // UserIdentityManager.mainService = DataService.mainService; + //this.addOwnPropertyChangeListener("userLocales", this); + this.addRangeAtPathChangeListener("userLocales", this, "handleUserLocalesRangeChange"); + } + + //Deprecated now + //this._initializeAuthorization(); + + if (this.providesAuthorization) { + exports.DataService.authorizationManager.registerAuthorizationService(this); + } + + if(this.providesUserIdentity === true) { + UserIdentityManager.registerUserIdentityService(this); + } + this._initializeOffline(); } }, @@ -91,23 +137,48 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { value = deserializer.getProperty("childServices"); if (value) { - this._childServices = value; + this._deserializedChildServices = value; + } + + value = deserializer.getProperty("authorizationPolicy"); + if (value) { + this.authorizationPolicy = value; + } + + value = deserializer.getProperty("userAuthenticationPolicy"); + if (value) { + this.userAuthenticationPolicy = value; } return this; } }, + _deserializedChildServices: { + value: undefined + }, + deserializedFromSerialization: { - value: function () { - if(Array.isArray(this._childServices)) { - var childServices = this._childServices; - this._childServices = []; - this.addChildServices(childServices); + value: function (label) { + 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) { + exports.DataService.authorizationManager.registerServiceWithUpfrontAuthorizationPolicy(this); } + } }, + currentEnvironment: { + value: currentEnvironment + }, + delegate: { value: null }, @@ -255,6 +326,11 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { }, + /** + * Adds child Services to the receiving service. + * + * @param {Array.} childServices. childServices to add. + */ addChildServices: { value: function (childServices) { @@ -263,16 +339,20 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { for(i=0, countI = childServices.length;(i} + */ + 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 * 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,24 +2306,565 @@ 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. * + * The key are the objects, the value is a Set containing the property changed + * + * @type {Map.} + */ + dataObjectChanges: { + get: function () { + if (this.isRootService) { + return this._dataObjectChanges || (this._dataObjectChanges = new Map()); + } + else { + return this.rootService.dataObjectChanges; + } + } + }, + + _dataObjectChanges: { + 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.dataObjectChanges.get(dataObject); + } + }, + + /** + * handles the propagation of a value set to a property's inverse property, fka "addToBothSidesOfRelationship..." + * This doesn't handle range changes, which is done in another method. + * + * @private + * @method + * @argument {Object} dataObject + * @returns {Promise} + + */ + _setDataObjectPropertyDescriptorValueForInversePropertyName: { + 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._setDataObjectPropertyDescriptorValueForInversePropertyDescriptor(dataObject, propertyDescriptor, value, inversePropertyDescriptorResolved); + }) + } else { + this._setDataObjectPropertyDescriptorValueForInversePropertyDescriptor(dataObject, propertyDescriptor, value, inversePropertyDescriptor); + return Promise.resolveTrue; + } + } + } + }, + + _setDataObjectPropertyDescriptorValueForInversePropertyDescriptor: { + value: function (dataObject, propertyDescriptor, value, inversePropertyDescriptor, previousValue) { + if(!inversePropertyDescriptor) { + return; + } + + var inversePropertyName = inversePropertyDescriptor.name, + inversePropertyCardinality = inversePropertyDescriptor.cardinality, + inverseValue; + + if(propertyDescriptor.cardinality === 1) { + //value should not be an array + if(Array.isArray(value)) { + console.warn("Something's off...., the value of propertyDescriptor:",propertyDescriptor, " of data object:",dataObject," should not be an array"); + } + + if(value) { + 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) { + /* + 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); + + + //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) { + 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, + isCreatedObject = this.isObjectCreated(dataObject), + key = changeEvent.key, + keyValue = changeEvent.keyValue, + addedValues = changeEvent.addedValues, + removedValues = changeEvent.removedValues, + changesForDataObject = this.dataObjectChanges.get(dataObject), + inversePropertyDescriptor, + self = this; + + + + + if(!isCreatedObject) { + this.changedDataObjects.add(dataObject); + } + + if(!changesForDataObject) { + changesForDataObject = new Map(); + this.dataObjectChanges.set(dataObject,changesForDataObject); + } + + + /* + + 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. + + */ + + + + + /* + 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( 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" + + 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) { + + /* + 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 { + 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); + + } else { + + for(i=0, countI=addedValues.length;i} */ - changedDataObjects: { + deletedDataObjects: { get: function () { if (this.isRootService) { - this._changedDataObjects = this._changedDataObjects || new Set(); - return this._changedDataObjects; + this._deletedDataObjects = this._deletedDataObjects || new Set(); + return this._deletedDataObjects; } else { - return this.rootService.changedDataObjects(); + return this.rootService.deletedDataObjects; } } }, - _changedDataObjects: { + _deletedDataObjects: { value: undefined }, + /*************************************************************************** * Fetching Data */ @@ -1835,6 +2922,11 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ fetchData: { value: function (queryOrType, optionalCriteria, optionalStream) { + + if(!queryOrType) { + return Promise.resolveNull; + } + var self = this, isSupportedType = !(queryOrType instanceof DataQuery), type = isSupportedType && queryOrType, @@ -1843,7 +2935,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(); @@ -1892,29 +2984,183 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { if (childService) { childService._fetchRawData(stream); } else { - if (this.authorizationPolicy === AuthorizationPolicy.ON_DEMAND) { - var prefetchAuthorization = typeof this.shouldAuthorizeForQuery === "function" && this.shouldAuthorizeForQuery(stream.query); - if (prefetchAuthorization || !this.authorization) { - this.authorizationPromise = exports.DataService.authorizationManager.authorizeService(this, prefetchAuthorization).then(function(authorization) { - self.authorization = authorization; - return authorization; + //this is the new path for services with a userAuthenticationPolicy + if (this.userAuthenticationPolicy) { + var userIdentityPromise, + shouldAuthenticate; + //If this is the service providing providesUserIdentity for the query's type: UserIdentityService for the UserIdentity type required. + //the query is either for the UserIdentity itself, or something else. + //If it's for the user identity itself, it's a simple fetch + //but if it's for something else, we may need to fetch the identity first, and then move on to the query at hand. + if(this.providesUserIdentity) { + //Regardless of the policy, we're asked to fetch a user identity + var streamQuery = stream.query; + if(stream.query.criteria) { + stream.query = self.mapSelectorToRawDataQuery(streamQuery); + } + this.fetchRawData(stream); + //Switch it back + stream.query = streamQuery; + } + else { + if(!this.userIdentity) { + if(this.userIdentityPromise) { + userIdentityPromise = this.userIdentityPromise; + } + else if ((this.authenticationPolicy === AuthenticationPolicyType.UpfrontAuthenticationPolicy) || + ( + (this.authenticationPolicy === AuthenticationPolicy.ON_DEMAND) && + (shouldAuthenticate = typeof this.queryRequireAuthentication === "function") && this.queryRequireAuthentication(stream.query) + )) { + + this.userIdentityPromise = userIdentityPromise = new Promise(function(resolve,reject) { + var userIdentityServices = this.userIdentityServices, + userIdentityObjectDescriptors, + selfUserCriteria, + userIdentityQuery; + + + //Shortcut, there could be multiple one we need to flatten. + userIdentityObjectDescriptors = userIdentityServices[0].types; + //selfUserCriteria = new Criteria().initWithExpression("identity == $", "self"); + userIdentityQuery = DataQuery.withTypeAndCriteria(userIdentityObjectDescriptors[0]); + + this.rootService.fetchData(userIdentityQuery) + .then(function(userIdenties) { + self.userIdentity = userIdenties[0]; + resolve(self.userIdentity); + }, + function(error) { + console.error(error); + reject(error); + }); + + }); + + } + else userIdentityPromise = Promise.resolve(true); + } + else { + userIdentityPromise = Promise.resolve(true); + } + + userIdentityPromise.then(function (authorization) { + var streamSelector = stream.query; + stream.query = self.mapSelectorToRawDataQuery(streamSelector); + self.fetchRawData(stream); + stream.query = streamSelector; + }).catch(function (e) { + stream.dataError(e); + self.userIdentityPromise = Promise.resolve(null); }); } } - this.authorizationPromise.then(function (authorization) { - var streamSelector = stream.query; - stream.query = self.mapSelectorToRawDataQuery(streamSelector); - self.fetchRawData(stream); - stream.query = streamSelector; - }).catch(function (e) { - stream.dataError(e); - self.authorizationPromise = Promise.resolve(null); - }); + else { + if (this.authorizationPolicy === AuthorizationPolicy.ON_DEMAND) { + var prefetchAuthorization = typeof this.shouldAuthorizeForQuery === "function" && this.shouldAuthorizeForQuery(stream.query); + if (prefetchAuthorization || !this.authorization) { + this.authorizationPromise = exports.DataService.authorizationManager.authorizeService(this, prefetchAuthorization).then(function(authorization) { + self.authorization = authorization; + return authorization; + }); + + } + } + + this.authorizationPromise.then(function (authorization) { + var streamSelector = stream.query; + stream.query = self.mapSelectorToRawDataQuery(streamSelector); + self.fetchRawData(stream); + stream.query = streamSelector; + }).catch(function (e) { + stream.dataError(e); + self.authorizationPromise = Promise.resolve(null); + }); + } } } }, + + /* + 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.handleRead(dataOperation); + } catch (e) { + stream.dataError(e); + } + + }) + + // Return the passed in or created stream. + return stream; + } + }, + + _childServiceForQuery: { value: function (query) { var serviceModuleID = this._serviceIdentifierForQuery(query), @@ -1932,12 +3178,12 @@ exports.DataService = Montage.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; if (!serviceModuleID) { - mapping = this.mappingWithType(query.type); + mapping = this.mappingForType(query.type); propertyName = mapping && parameters && parameters.propertyName; serviceModuleID = propertyName && mapping.serviceIdentifierForProperty(propertyName); } @@ -1997,6 +3243,32 @@ 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) { + //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); + //} + } + }, + /*************************************************************************** * Saving Data */ @@ -2011,7 +3283,8 @@ exports.DataService = Montage.specialize(/** @lends DataService.prototype */ { */ deleteDataObject: { value: function (object) { - var saved = !this.createdDataObjects.has(object); + var saved = !this.isObjectCreated(object); + this.deletedDataObjects.add(object); return this._updateDataObject(object, saved && "deleteDataObject"); } }, @@ -2048,6 +3321,13 @@ exports.DataService = Montage.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, @@ -2070,7 +3350,23 @@ 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.log(error); + return Promise.reject(error); + }); + } } else { return promise; @@ -2133,6 +3429,49 @@ exports.DataService = Montage.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) { + + } + }, + + + /** + * 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 () { + var self = this, + service, + promise = this.nullPromise, + + service = this.childServices[0]; + if (service && typeof service.saveChanges === "function") { + return service.saveChanges(); + } + else { + return promise; + } + } + }, + + + /*************************************************************************** * Offline */ @@ -2511,6 +3850,57 @@ exports.DataService = Montage.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 */ @@ -2611,11 +4001,116 @@ exports.DataService = Montage.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) + * + * @property Array {Locale} + */ + + _userLocales: { + value: undefined + }, + + userLocales: { + get: function() { + return this.isRootService + ? this._userLocales || ((this._userLocales = [Locale.systemLocale]) && this._userLocales) + : this.rootService.userLocales; + }, + set: function(value) { + if(value !== this._userLocales) { + this._userLocales = value; + } + } + }, + + _userLocalesCriteria: { + value: undefined + }, + + userLocalesCriteria: { + get: function() { + return this.isRootService + ? this._userLocalesCriteria || (this._userLocalesCriteria = this._createUserLocalesCriteria()) + : this.rootService.userLocalesCriteria; + + }, + set: function(value) { + if(value !== this._userLocalesCriteria) { + this._userLocalesCriteria = value; + } + } + }, + + _createUserLocalesCriteria: { + value: function() { + return new Criteria().initWithExpression("locales == $DataServiceUserLocales", { + DataServiceUserLocales: this.userLocales + }); + } + }, + + handleUserLocalesChange: { + value: function (value, key, object) { + this.userLocalesCriteria = this._createUserLocalesCriteria(); + } + }, + handleUserLocalesRangeChange: { + value: function (plus, minus){ + this.userLocalesCriteria = this._createUserLocalesCriteria(); + } + }, + + /** + * Returns the locales for a specific object's locale. Default implementation + * returns userLocales. Subclasses can override to offer a per-object + * localization to be different than the session's one. + * Subclasses have the opporyunity to oveorrides to get useLocale + * from more specific data objects (DO) + * + * @returns Array{Locale} + */ + + localesForObject: { + value: function(object) { + return this.userLocales; + } + }, + + /** + * Returns a criteria the locales for a specific object. Default implementation + * returns userLocales. Subclasses can override to offer a per-object + * localization to be different than the session's one. + * Subclasses have the opporyunity to oveorrides to get useLocale + * from more specific data objects (DO) + * + * @returns Array{Locale} + */ + + localesCriteriaForObject: { + value: function(object) { + return this.userLocalesCriteria; + } + }, + + /** + * The DataTrigger class used by the montage data stack + * + * @property {DataTrigger} + */ + + DataTrigger: { + value: DataTrigger + }, }, /** @lends DataService */ { + /*************************************************************************** * Service Hierarchy */ @@ -2673,3 +4168,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..60b50a0ede 100644 --- a/data/service/data-stream.js +++ b/data/service/data-stream.js @@ -4,9 +4,11 @@ var DataProvider = require("data/service/data-provider").DataProvider, DataQuery = require("data/model/data-query").DataQuery, Promise = require("core/promise").Promise, deprecate = require("core/deprecate"), - parse = require("frb/parse"), - Scope = require("frb/scope"), - compile = require("frb/compile-evaluator"); + parse = require("core/frb/parse"), + Scope = require("core/frb/scope"), + compile = require("core/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 @@ -52,9 +54,28 @@ exports.DataStream = DataProvider.specialize(/** @lends DataStream.prototype */ * * @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) { + Object.defineProperty(this.data,"objectDescriptor", { + value: value.type, + enumerable: false, + configurable: true + }); + // this.data.objectDescriptor = value.type; + } + } + }, /** * The selector defining the data returned in this stream. @@ -96,6 +117,9 @@ exports.DataStream = DataProvider.specialize(/** @lends DataStream.prototype */ * 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}. @@ -199,6 +223,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,9 +292,13 @@ 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)); } + if (data && Array.isArray(data)) { this.data.push.apply(this.data, data); } else if (data) { @@ -265,6 +323,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.ReadUpdatedOperation; + 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.ReadUpdateOperation; + 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..45ea4e4150 100644 --- a/data/service/data-trigger.js +++ b/data/service/data-trigger.js @@ -1,7 +1,11 @@ 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"), + WeakMap = require("core/collections/weak-map"), + Map = require("core/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; /** @@ -136,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(); } } }, @@ -157,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: @@ -213,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 @@ -225,24 +272,84 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy _getValue: { configurable: true, 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); + value: function (object, shouldFetch) { + var prototype, descriptor, getter = this._valueGetter, propertyName = this._propertyName; - // Search the prototype chain for a getter for this property, - // starting just after the prototype that called this method. + /* + Experiment to see if it would make sense to avoid triggering getObjectProperty during mapping? + */ + // if(!this._service.rootService._objectsBeingMapped.has(object) + // ) { + if(shouldFetch !== false && !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); + } + + //} + + // 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: { + 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 * requested but before it is obtained from the trigger's service the @@ -257,44 +364,216 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy _setValue: { configurable: true, writable: true, - value: function (object, value) { - var status, prototype, descriptor, getter, setter, writable, currentValue, isToMany; + value: function (object, value, _dispatchChange) { + 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 // 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); - } + 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); - } else if (writable) { + //currentValue = value; + } 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 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)); - } + //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; } } + + currentValue = this._getValue(object); + if(currentValue !== initialValue) { + + if(isToMany) { + if(initialValue) { + var listener = this._collectionListener.get(object); + if(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 _triggerArrayCollectionListener(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; + + //This + 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.addRangeChangeListener(listener); + } + else if(currentValue instanceof 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 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"); + } + + } + } + } + + +//addRangeChangeListener + + //If we're not in the middle of a mapping...: + if(currentValue !== initialValue && dispatchChange && !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 +581,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. * @@ -325,10 +614,25 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy */ getObjectProperty: { value: function (object) { - var status = this._getValueStatus(object); - return status ? status.promise : - status === null ? this._service.nullPromise : - this.updateObjectProperty(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; + // } } }, @@ -367,10 +671,22 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy _fetchObjectProperty: { value: function (object) { var self = this; - this._service.fetchObjectProperty(object, this._propertyName).then(function () { + //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) { + object[self._propertyName] = propertyValue; + } else { + object[self._propertyName] = propertyValue[0]; + } + } 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); }); } @@ -392,7 +708,8 @@ exports.DataTrigger.prototype = Object.create({}, /** @lends DataTrigger.prototy }); -Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ { +var DataTriggerClassMethods; +Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ (DataTriggerClassMethods = { /** * @method @@ -474,8 +791,8 @@ Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ { 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); @@ -527,23 +844,35 @@ Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ { trigger.propertyDescriptor = descriptor; trigger._isGlobal = descriptor.isGlobal; if (descriptor.definition) { - Montage.defineProperty(prototype, descriptor.name, { - get: function () { + /* + 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)) { 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); - }, - 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 () { - 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) { @@ -608,4 +937,7 @@ Object.defineProperties(exports.DataTrigger, /** @lends DataTrigger */ { } } -}); +})); + +//Temporary share on exports for subclasses to use. +exports._DataTriggerClassMethods = DataTriggerClassMethods; diff --git a/data/service/expression-data-mapping.js b/data/service/expression-data-mapping.js index 35d1bf29b8..8305859674 100644 --- a/data/service/expression-data-mapping.js +++ b/data/service/expression-data-mapping.js @@ -1,15 +1,19 @@ var DataMapping = require("./data-mapping").DataMapping, - assign = require("frb/assign"), - compile = require("frb/compile-evaluator"), + 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("frb/parse"), - Map = require("collections/map"), + parse = require("core/frb/parse"), + Map = require("core/collections/map"), MappingRule = require("data/service/mapping-rule").MappingRule, Promise = require("core/promise").Promise, - Scope = require("frb/scope"), - Set = require("collections/set"), - deprecate = require("core/deprecate"); + Scope = require("core/frb/scope"), + Set = require("core/collections/set"), + deprecate = require("core/deprecate"), + RawForeignValueToObjectConverter = require("data/converter/raw-foreign-value-to-object-converter").RawForeignValueToObjectConverter, + DataOperation = require("./data-operation").DataOperation; + var Montage = require("montage").Montage; @@ -89,11 +93,25 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData this.addRequisitePropertyName.apply(this, value); } - value = deserializer.getProperty("rawDataPrimaryKeys"); + value = deserializer.getProperty("rawDataTypeName"); if (value) { - this.rawDataPrimaryKeys = value; + this.rawDataTypeName = value; + } + + + + value = deserializer.getProperty("primaryKeyPropertyDescriptors"); + if (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"); @@ -181,7 +199,7 @@ exports.ExpressionDataMapping = DataMapping.specialize(/** @lends ExpressionData _scope: { get: function() { - return this.__scope || new Scope(); + return this.__scope || new Scope(this.service); } }, @@ -227,12 +245,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 @@ -334,6 +430,65 @@ 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); + if(objectRule) { + /* + Test for polymorphic Associations with the Exclusive Belongs To (AKA Exclusive Arc) strategy where each potential destination table + gets it's matching foreignKeyId + */ + var objectRuleConverter = objectRule.converter, + objectRuleConverterForeignDescriptorMappings = objectRuleConverter && objectRuleConverter.foreignDescriptorMappings, + j, countJ; + + if(objectRuleConverterForeignDescriptorMappings && objectRule.sourcePath === "this") { + + for(j=0, countJ = objectRuleConverterForeignDescriptorMappings.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 + + } + } + */ + + // _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 0) || (removed && removed.size > 0 )) { + 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) { + /* + 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 { + this._assignMappedDiffDataToPropertyChangesObjectKey(diffData, aPropertyChanges,"addedValues"); + }); + } else { + this._assignMappedDiffDataToPropertyChangesObjectKey(diffData, aPropertyChanges,"addedValues"); + } + } + + if(removed && removed.size > 0 ) { + + requirements = (requirements || _rule.requirements); + // tmpExtendObject[propertyName] = Array.from(result); + tmpExtendObject = (tmpExtendObject || {}); + + for(i=0, countI = requirements.length; ( i { + this._assignMappedDiffDataToPropertyChangesObjectKey(diffData, aPropertyChanges,"removedValues"); + }); + } else { + this._assignMappedDiffDataToPropertyChangesObjectKey(diffData, aPropertyChanges,"removedValues"); + } + } + + 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, countI = mappedKeys.length; (i 1) { + return Promise.all(iPromises); + } else if(iPromises.length === 1) { + return iPromises[0]; + } else { + return; + } + } else { + //There was only one rule, so we have one result; + return result; + } + } + return; + } + else { + throw new Error("No objectMappingRules found to map property "+propertyName+" of object,", object, "to raw data"); + } + } + }, + + + /** + * Maps the property name of a single object property to it's raw level + * property equivallent. + * + * @method + * @argument {Object} object - A data object. + * @argument Promise{string} propertyName - The name of the property to map. + */ + + // mapObjectPropertyToRawProperty: { + // value: function(object, property) { + // var objectRule = this.objectMappingRules.get(property), + // rawProperty; + + // if(objectRule) { + // var rawDataMappingRules = this.rawDataMappingRulesForPropertyName(property); + + // 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!!!"); + // } + + // } 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} rawRules - Object whose keys are object property @@ -975,12 +2181,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)) { @@ -1006,13 +2212,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)) { @@ -1022,6 +2228,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 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 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); + } + } +}); diff --git a/data/service/http-service.js b/data/service/http-service.js index b89a0e10c6..82583318fe 100644 --- a/data/service/http-service.js +++ b/data/service/http-service.js @@ -1,12 +1,12 @@ var RawDataService = require("data/service/raw-data-service").RawDataService, DataQuery = require("data/model/data-query").DataQuery, Enumeration = require("data/model/enumeration").Enumeration, - Map = require("collections/map"), + Map = require("core/collections/map"), Montage = require("montage").Montage, - parse = require("frb/parse"), - compile = require("frb/compile-evaluator"), - evaluate = require("frb/evaluate"), - Scope = require("frb/scope"), + parse = require("core/frb/parse"), + compile = require("core/frb/compile-evaluator"), + evaluate = require("core/frb/evaluate"), + Scope = require("core/frb/scope"), Promise = require("core/promise").Promise; @@ -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); diff --git a/data/service/indexed-d-b-data-service.js b/data/service/indexed-d-b-data-service.js index f564c364c1..8abc49d4f3 100644 --- a/data/service/indexed-d-b-data-service.js +++ b/data/service/indexed-d-b-data-service.js @@ -5,8 +5,8 @@ var PersistentDataService = require("data/service/persistent-data-service").Pers uuid = require("core/uuid"), DataOrdering = require("data/model/data-ordering").DataOrdering, DESCENDING = DataOrdering.DESCENDING, - evaluate = require("frb/evaluate"), - Map = require("collections/map"), + evaluate = require("core/frb/evaluate"), + Map = require("core/collections/map"), OfflineService; /** * TODO: Document diff --git a/data/service/mapping-rule.js b/data/service/mapping-rule.js index 3eb6084eba..470e7f6499 100644 --- a/data/service/mapping-rule.js +++ b/data/service/mapping-rule.js @@ -1,6 +1,8 @@ var Montage = require("montage").Montage, - compile = require("frb/compile-evaluator"), - parse = require("frb/parse"), + 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"); @@ -29,6 +31,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) { @@ -38,28 +43,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 @@ -81,18 +64,41 @@ exports.MappingRule = Montage.specialize(/** @lends MappingRule.prototype */ { */ requirements: { get: function () { - if (!this._requirements && this.sourcePathSyntax) { - this._requirements = this._parseRequirementsFromSyntax(this.sourcePathSyntax); + return this._requirements || ( + this._requirements === undefined && this.sourcePathSyntax + ? (this._requirements = this._parseRequirementsFromSyntax(this.sourcePathSyntax)) + : this._requirements = null + ); + // if (!this._requirements && this.sourcePathSyntax) { + // this._requirements = this._parseRequirementsFromSyntax(this.sourcePathSyntax); + // } + // return this._requirements; + } + }, + + hasRawDataRequiredValues: { + value: function(rawData) { + var requirements = this.requirements, + i, countI, iRequirenent; + + for(i=0, countI = requirements.length; (i stream.addData(object)"); + stream.addData(object); return object; }); @@ -433,6 +625,7 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot stream.addData(object); result = Promise.resolve(object); } + this._addMapDataPromiseForStream(result, stream); @@ -493,14 +686,18 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot //Retrieves an existing object is responsible data service is uniquing, or creates one object, result; - //Record snapshot before we may create an object - this.recordSnapshot(dataIdentifier, rawData); //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); + if (Promise.is(result)) { return result.then(function () { return object; @@ -514,11 +711,26 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot objectForTypeRawData: { value:function(type, rawData, context) { - var dataIdentifier = this.dataIdentifierForTypeRawData(type,rawData); + // var dataIdentifier = this.dataIdentifierForTypeRawData(type,rawData); + + // return this.rootService.objectForDataIdentifier(dataIdentifier) || + // this.getDataObject(type, rawData, context, dataIdentifier); + + + var dataIdentifier = this.dataIdentifierForTypeRawData(type,rawData), + object = this.rootService.objectForDataIdentifier(dataIdentifier); + + //Consolidation, recording snapshot even if we already had an object //Record snapshot before we may create an object - this.recordSnapshot(dataIdentifier, rawData); - //iDataIdentifier argument should be all we need later on - return this.getDataObject(type, rawData, context, dataIdentifier); + //Benoit: commenting out, done twice when fetching now + //this.recordSnapshot(dataIdentifier, rawData); + + if(!object) { + //iDataIdentifier argument should be all we need later on + return this.getDataObject(type, rawData, context, dataIdentifier); + } + return object; + } }, @@ -526,11 +738,49 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot value: undefined }, - //This should belong on the - //Gives us an indirection layer to deal with backward compatibility. - dataIdentifierForTypeRawData: { + /** + * 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) { - var mapping = this.mappingWithType(type), + var mapping = this.mappingForType(type), rawDataPrimaryKeys = mapping ? mapping.rawDataPrimaryKeyExpressions : null, scope = new Scope(rawData), rawDataPrimaryKeysValues, @@ -538,21 +788,78 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot if(rawDataPrimaryKeys && rawDataPrimaryKeys.length) { - dataIdentifierMap = this._typeIdentifierMap.get(type); - - if(!dataIdentifierMap) { - this._typeIdentifierMap.set(type,(dataIdentifierMap = new Map())); - } - for(var i=0, expression; (expression = rawDataPrimaryKeys[i]); i++) { rawDataPrimaryKeysValues = rawDataPrimaryKeysValues || []; rawDataPrimaryKeysValues[i] = expression(scope); } if(rawDataPrimaryKeysValues) { primaryKey = rawDataPrimaryKeysValues.join("/"); - dataIdentifier = dataIdentifierMap.get(primaryKey); + // dataIdentifier = dataIdentifierMap.get(primaryKey); } + return primaryKey; + } + return undefined; + } + }, + + registerDataIdentifierForTypeRawData: { + value: function (dataIdentifier, type, rawData) { + var primaryKey = this.primaryKeyForTypeRawData(type, rawData); + + this.registerDataIdentifierForTypePrimaryKey(dataIdentifier, type, primaryKey); + } + }, + + //This should belong on the + //Gives us an indirection layer to deal with backward compatibility. + dataIdentifierForTypeRawData: { + value: function (type, rawData) { + var primaryKey = this.primaryKeyForTypeRawData(type, rawData); + + if(primaryKey) { + return this.dataIdentifierForTypePrimaryKey(type,primaryKey); + } + return undefined; + } + }, + + /** + * In most cases a RawDataService will register a dataIdentifier created during + * the mapping process, but in some cases where an object created by the upper + * layers fitst, this can be used direcly to reconcilate things. + * + * @method + * @argument {DataIdentifier} dataIdentifier - The dataIdentifier representing the type's rawData. + * @argument {ObjectDescriptor} type - the type of the raw data. + * @argument {?} primaryKey - An arbitrary value that that is the primary key + * + * + * + * @returns {Promise} - 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; + + dataIdentifier = dataIdentifierMap + ? dataIdentifierMap.get(primaryKey) + : null; + if(!dataIdentifier) { var typeName = type.typeName /*DataDescriptor*/ || type.name; //This should be done by ObjectDescriptor/blueprint using primaryProperties @@ -562,14 +869,15 @@ 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); + // dataIdentifierMap.set(primaryKey,dataIdentifier); + this.registerDataIdentifierForTypePrimaryKey(dataIdentifier,type, primaryKey); } return dataIdentifier; - } - return undefined; } + }, __snapshot: { @@ -591,8 +899,81 @@ exports.RawDataService = DataService.specialize(/** @lends RawDataService.protot * @argument {Object} rawData */ recordSnapshot: { - value: function (dataIdentifier, rawData) { - this._snapshot.set(dataIdentifier, rawData); + value: function (dataIdentifier, rawData, isFromUpdate) { + if(!dataIdentifier) { + return; + } + + var snapshot = this._snapshot.get(dataIdentifier); + if(!snapshot) { + this._snapshot.set(dataIdentifier, rawData); + } + else { + var rawDataKeys = Object.keys(rawData), + i, countI, iUpdatedRawDataValue, iCurrentRawDataValue, iDiffValues, iRemovedValues, + j, countJ, jDiffValue, jDiffValueIndex; + + for(i=0, countI = rawDataKeys.length; (i=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": "^1.13.3", - "mr": "montagejs/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" - }, - "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": { + "bluebird": "~3.5.5", + "htmlparser2": "~3.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": { + "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", + "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";" + }, + "bin": { + "mr": "core/mr/bin/mr" + }, + "exclude": [ + "demo", + "report", + "doc", + "test", + "tools" + ] } diff --git a/test/all.js b/test/all.js index aa8281b168..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 @@ -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}, @@ -35,6 +36,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", @@ -74,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", diff --git a/test/mocks/component.js b/test/mocks/component.js index 479af67b7b..5c4d9cb065 100644 --- a/test/mocks/component.js +++ b/test/mocks/component.js @@ -1,4 +1,4 @@ -var Set = require("montage/collections/set"), +var Set = require("montage/core/collections/set"), Event = require("mocks/event"), Component = require("montage/ui/component").Component, defaultEventManager = require("montage/core/event/event-manager").defaultEventManager; 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' ) + ]; } }, diff --git a/test/mocks/dom.js b/test/mocks/dom.js index 6615b6b2ff..0264b356be 100644 --- a/test/mocks/dom.js +++ b/test/mocks/dom.js @@ -1,4 +1,4 @@ -var Set = require("montage/collections/set"), +var Set = require("montage/core/collections/set"), defaultKeyManager = require("montage/core/event/key-manager").defaultKeyManager, Event = require("mocks/event"), Component = require("mocks/component"); @@ -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); @@ -196,7 +197,7 @@ exports.document = function () { result.rootElement = exports.element(result); result.body = exports.element(result); - + // configure html element result.querySelectorAll = result.rootElement.querySelectorAll; result.querySelector = result.rootElement.querySelector; @@ -253,7 +254,7 @@ exports.keyPressEvent = function (keys, target) { customEvent = {}; for (key in event) { if (event.hasOwnProperty(key)) { - customEvent[key] = event[key]; + customEvent[key] = event[key]; } } customEvent.charCode = modifiersAndKeyCode.keyCode; diff --git a/test/mocks/event.js b/test/mocks/event.js index a0a79cb58f..0430d46bcd 100644 --- a/test/mocks/event.js +++ b/test/mocks/event.js @@ -1,4 +1,4 @@ -var Set = require("montage/collections/set"); +var Set = require("montage/core/collections/set"); var MockEvent = exports.MockEvent = function MockEvent() {}; diff --git a/test/package.json b/test/package.json index 9247899cef..7a7cc4882c 100644 --- a/test/package.json +++ b/test/package.json @@ -3,8 +3,8 @@ "version": "0.0.0", "mappings": { "montage": "../", - "montage-testing": "../node_modules/montage-testing", - "collections": "../node_modules/collections", + "ical.js": "../node_modules/ical.js", + "montage-testing": "../testing", "package-a": { "name": "package-a", "location": "package-a" 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); 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/core-spec.js b/test/spec/core/core-spec.js index a64a05875b..0b35f6ab39 100644 --- a/test/spec/core/core-spec.js +++ b/test/spec/core/core-spec.js @@ -30,8 +30,8 @@ POSSIBILITY OF SUCH DAMAGE. */ var Montage = require("montage").Montage; // TODO [June 20 2011 PJYF] This s temporary implementation of WeakMap to let the browser catch up. -var WeakMap = require("collections/weak-map"); -var Map = require("collections/map"); +var WeakMap = require("montage/core/collections/weak-map"); +var Map = require("montage/core/collections/map"); var UUID = require("montage/core/uuid"); describe("core/core-spec", function () { 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); + }); + + +}); diff --git a/test/spec/core/date-spec.js b/test/spec/core/date-spec.js new file mode 100644 index 0000000000..4bfaec1934 --- /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(resultDate.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; + }); + +}); + 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/core/localizer-spec.js b/test/spec/core/localizer-spec.js index 618c7e36bf..05167a6c94 100644 --- a/test/spec/core/localizer-spec.js +++ b/test/spec/core/localizer-spec.js @@ -33,7 +33,7 @@ var Montage = require("montage").Montage, Localizer = require("montage/core/localizer"), Promise = require("montage/core/promise").Promise, Bindings = require("montage/core/core").Bindings, - Map = require("montage/collections/map"); + Map = require("montage/core/collections/map"); describe("core/localizer-spec", function () { @@ -318,7 +318,7 @@ describe("core/localizer-spec", function () { }); describe("loadMessages", function () { - + it("fails when package.json has no localizer/no-package-manifest/manifest", function (done) { require.loadPackage(module.directory + "localizer/no-package-manifest/", {}).then(function (r){ l.require = r; diff --git a/test/spec/core/localizer/serialization-spec.js b/test/spec/core/localizer/serialization-spec.js index d193815d53..ab18f3190c 100644 --- a/test/spec/core/localizer/serialization-spec.js +++ b/test/spec/core/localizer/serialization-spec.js @@ -35,9 +35,9 @@ var Montage = require("montage").Montage, Serializer = require("montage/core/serialization/serializer/montage-serializer").MontageSerializer, Deserializer = require("montage/core/serialization/deserializer/montage-deserializer").MontageDeserializer, TestPageLoader = require("montage-testing/testpageloader").TestPageLoader, - Map = require("montage/collections/map"), + 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, ""); diff --git a/test/spec/core/range-controller-spec.js b/test/spec/core/range-controller-spec.js index 4ec83c6de6..e0e6b58263 100644 --- a/test/spec/core/range-controller-spec.js +++ b/test/spec/core/range-controller-spec.js @@ -1,6 +1,6 @@ var Montage = require("montage").Montage; var RangeController = require("montage/core/range-controller").RangeController; -var Set = require("montage/collections/set"); +var Set = require("montage/core/collections/set"); describe("core/range-controller-spec", function () { var rangeController; diff --git a/test/spec/core/set-spec.js b/test/spec/core/set-spec.js index 033b8ac75d..c26e7b4c2f 100644 --- a/test/spec/core/set-spec.js +++ b/test/spec/core/set-spec.js @@ -1,5 +1,5 @@ -var Set = require("montage/collections/set"); +var Set = require("montage/core/collections/set"); describe("test/core/set-spec", function () { diff --git a/test/spec/core/undo-manager-spec.js b/test/spec/core/undo-manager-spec.js index cad1b4bbda..d6aaba9c3b 100644 --- a/test/spec/core/undo-manager-spec.js +++ b/test/spec/core/undo-manager-spec.js @@ -1,5 +1,5 @@ var Montage = require("montage").Montage, - Set = require("montage/collections/set"), + Set = require("montage/core/collections/set"), UndoManager = require("montage/core/undo-manager").UndoManager, Promise = require("montage/core/promise").Promise, WAITS_FOR_TIMEOUT = 2500; @@ -97,9 +97,9 @@ var Roster = Montage.specialize( { deferredAddResolve = resolve; }); deferredAdd.resolve = deferredAddResolve; - + this.members.add(member); - + this.undoManager.register("Add Member", deferredAdd); return deferredAdd; diff --git a/test/spec/data/authorization-manager.js b/test/spec/data/authorization-manager.js index 33828797b5..feeba6dedd 100644 --- a/test/spec/data/authorization-manager.js +++ b/test/spec/data/authorization-manager.js @@ -6,7 +6,7 @@ var AuthorizationManager = require("montage/data/service/authorization-manager") OnDemandService = require("spec/data/logic/authorization/on-demand-service").OnDemandService, OnFirstFetchService = require("spec/data/logic/authorization/on-first-fetch-service").OnFirstFetchService, UpFrontService = require("spec/data/logic/authorization/up-front-service").UpFrontService, - Map = require("collections/map"); + Map = require("montage/core/collections/map"); describe("An Authorization Manager", function () { @@ -38,13 +38,13 @@ describe("An Authorization Manager", function () { expect(authorizationManager.hasPendingServices).toBe(false); }); - + describe("can skip authorization", function () { it("for service with AuthorizationPolicy.NONE", function (done) { var service = new NoneService(); - + authorizationManager.authorizeService(service).then(function (result) { expect(result).toBeNull(); done(); @@ -53,7 +53,7 @@ describe("An Authorization Manager", function () { it("for service with AuthorizationPolicy.ON_DEMAND without failure", function (done) { var service = new OnDemandService(); - + authorizationManager.authorizeService(service).then(function (result) { expect(result).toBeNull(); done(); @@ -64,17 +64,17 @@ describe("An Authorization Manager", function () { describe("can authorize individual service", function () { it("with AuthorizationPolicy.ON_FIRST_FETCH", function (done) { var service = new OnFirstFetchService(); - - + + authorizationService.resolve(); authorizationManager.authorizeService(service).then(function (result) { expect(Array.isArray(result)).toBeTruthy(); expect(result[0] instanceof Authorization).toBeTruthy(); done(); }); - + }); - + it("with AuthorizationPolicy.ON_DEMAND with failure", function (done) { var service = new OnDemandService() authorizationService.resolve(); @@ -83,9 +83,9 @@ describe("An Authorization Manager", function () { expect(result[0] instanceof Authorization).toBeTruthy(); done(); }); - + }); - + it("with AuthorizationPolicy.UP_FRONT", function (done) { var service = new UpFrontService(); authorizationService.resolve(); @@ -94,8 +94,8 @@ describe("An Authorization Manager", function () { expect(result[0] instanceof Authorization).toBeTruthy(); done(); }); - - + + }); describe("after authorization expiration", function () { @@ -108,7 +108,7 @@ describe("An Authorization Manager", function () { var service = new OnFirstFetchService(), didFail = false, authorizations; - + authorizationService.reject(); authorizationManager.authorizeService(service).catch(function (e) { didFail = true; @@ -134,7 +134,7 @@ describe("An Authorization Manager", function () { it("with all of multiple authorization services", function (done) { var service = new OnFirstFetchService(), authorizations; - + service.authorizationServices = [ "spec/data/logic/authorization/authorization-service", @@ -156,7 +156,7 @@ describe("An Authorization Manager", function () { xit("with one of multiple authorization services", function (done) { var service = new OnFirstFetchService(), authorizations; - + service.authorizationServices = [ "spec/data/logic/authorization/authorization-service", @@ -177,14 +177,14 @@ describe("An Authorization Manager", function () { describe("can authorize multiple data services", function () { - // Array of descriptions of DataServices with a particular AuthorizationPolicy. + // Array of descriptions of DataServices with a particular AuthorizationPolicy. // Each of the descriptions has the following structure // - // constructor: Constructor function to create the DataService - // name: Human Readable name of the AuthorizationPolicy of this dataService + // constructor: Constructor function to create the DataService + // name: Human Readable name of the AuthorizationPolicy of this dataService // didFailResult: Boolean stating whether a result is expected after a preceding auth failure // initialResult: Boolean stating whether a result is expected the first time Authorization is requested - // + // // var serviceDescriptors = [ { @@ -256,7 +256,7 @@ describe("An Authorization Manager", function () { }); } - + describe("with same authorization service", function () { @@ -347,7 +347,7 @@ describe("An Authorization Manager", function () { expect(authorizationManager._authorizationsForDataService(serviceB).length).toBe(0); done(); }); - + }); }); @@ -375,9 +375,9 @@ describe("An Authorization Manager", function () { expect(authorizationManager._authorizationsForDataService(service).length).toBe(0); done(); }); - + }); }); - + }); 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(); }); }); diff --git a/test/spec/events/eventmanager-spec.js b/test/spec/events/eventmanager-spec.js index 0bce5404ee..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 () { @@ -508,12 +531,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 +948,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 +986,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 +1004,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; 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); 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"}, diff --git a/test/spec/paths-spec.js b/test/spec/paths-spec.js index 923e21713b..c9ddf3ae28 100644 --- a/test/spec/paths-spec.js +++ b/test/spec/paths-spec.js @@ -1,5 +1,5 @@ var Montage = require("montage").Montage; -var Map = require("montage/collections/map"); +var Map = require("montage/core/collections/map"); describe("paths-spec", function () { @@ -422,8 +422,8 @@ describe("paths-spec", function () { spy = jasmine.createSpy(); object.array = [1, 2]; - expect(spy).toHaveBeenCalledWith([2, 4], [], 0); - }) + expect(spy).toHaveBeenCalledWith([2, 4], [], 0); + }) }); diff --git a/test/spec/serialization/montage-deserializer-element-spec.js b/test/spec/serialization/montage-deserializer-element-spec.js index 0731856ecd..a8698741f2 100644 --- a/test/spec/serialization/montage-deserializer-element-spec.js +++ b/test/spec/serialization/montage-deserializer-element-spec.js @@ -4,7 +4,7 @@ var Montage = require("montage").Montage, Deserializer = require("montage/core/serialization/deserializer/montage-deserializer").MontageDeserializer, deserialize = require("montage/core/serialization/deserializer/montage-deserializer").deserialize, Alias = require("montage/core/serialization/alias").Alias, - Bindings = require("montage/frb"), + Bindings = require("montage/core/frb/bindings"), defaultEventManager = require("montage/core/event/event-manager").defaultEventManager, Promise = require("montage/core/promise").Promise, objects = require("spec/serialization/testobjects-v2").objects; @@ -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 () { diff --git a/test/spec/serialization/montage-deserializer-spec.js b/test/spec/serialization/montage-deserializer-spec.js index 465a8a4039..c62af9aff9 100644 --- a/test/spec/serialization/montage-deserializer-spec.js +++ b/test/spec/serialization/montage-deserializer-spec.js @@ -34,7 +34,7 @@ var Montage = require("montage").Montage, Deserializer = require("montage/core/serialization/deserializer/montage-deserializer").MontageDeserializer, deserialize = require("montage/core/serialization/deserializer/montage-deserializer").deserialize, Alias = require("montage/core/serialization/alias").Alias, - Bindings = require("montage/frb"), + Bindings = require("montage/core/frb/bindings"), Promise = require("montage/core/promise").Promise, objects = require("spec/serialization/testobjects-v2").objects; @@ -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); @@ -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 }, 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/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 () { 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 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..707f645b27 --- /dev/null +++ b/testing/testpageloader.js @@ -0,0 +1,1060 @@ +/* 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 () { + //Hello!! + 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; +}; 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..746e949945 --- /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("core/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" + } + +}); diff --git a/ui/authorization-manager-panel.reel/authorization-manager-panel.js b/ui/authorization-manager-panel.reel/authorization-manager-panel.js index f73bcbd9d9..b1fa5e9b95 100644 --- a/ui/authorization-manager-panel.reel/authorization-manager-panel.js +++ b/ui/authorization-manager-panel.reel/authorization-manager-panel.js @@ -1,7 +1,7 @@ var Component = require("ui/component").Component, Promise = require("core/promise").Promise, application = require("core/application").application, - Map = require("collections/map"), + Map = require("core/collections/map"), Montage = require("montage").Montage; /** @@ -96,7 +96,7 @@ exports.AuthorizationManagerPanel = Component.specialize({ if (panel) { this._deregisterPanel(panel); } - + if (application.applicationModal) { application.applicationModal.hide(this); } diff --git a/ui/base/abstract-control.js b/ui/base/abstract-control.js index 2b3bbf68f0..82b4805ba9 100644 --- a/ui/base/abstract-control.js +++ b/ui/base/abstract-control.js @@ -6,7 +6,7 @@ * @requires collections/map */ var Component = require("../component").Component, - Map = require("collections/map"); + Map = require("core/collections/map"); /** * @class AbstractControl diff --git a/ui/base/abstract-image.js b/ui/base/abstract-image.js index 734ff99f18..8e8fa5f9e6 100644 --- a/ui/base/abstract-image.js +++ b/ui/base/abstract-image.js @@ -5,7 +5,7 @@ */ var Component = require("../component").Component, Url = require("../../core/mini-url"), - Map = require("collections/map"); + Map = require("core/collections/map"); if (typeof window !== "undefined") { // client-side Map = window.Map || Map; 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"} 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, diff --git a/ui/component.js b/ui/component.js index 713877f54f..1f430687fa 100644 --- a/ui/component.js +++ b/ui/component.js @@ -27,9 +27,11 @@ var Montage = require("../core/core").Montage, drawListLogger = require("../core/logger").logger("drawing list").color.blue(), needsDrawLogger = require("../core/logger").logger("drawing needsDraw").color.violet(), drawLogger = require("../core/logger").logger("drawing").color.blue(), - WeakMap = require("collections/weak-map"), - Map = require("collections/map"), - Set = require("collections/set"); + WeakMap = require("core/collections/weak-map"), + Map = require("core/collections/map"), + Set = require("core/collections/set"), + currentEnvironment = require("core/environment").currentEnvironment, + PropertyChanges = require("core/collections/listen/property-changes"); /** * @const @@ -747,7 +749,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto dragManager: { get: function () { - return _defaultDragManager || + return _defaultDragManager || ((_defaultDragManager = new DragManager()).initWithComponent(this.rootComponent)); } }, @@ -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} @@ -1204,6 +1218,14 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto }, _inDocument: { + get: function() { + return this.inDocument; + }, + set: function(value) { + this.inDocument = value; + } + }, + inDocument: { value: false }, @@ -1217,9 +1239,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 +1263,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto } } - if (component._inDocument) { + if (component.inDocument) { component.__exitDocument(); } }; @@ -1319,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, @@ -1699,6 +1727,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); @@ -1901,7 +1944,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto }, deserializedFromSerialization: { - value: function () { + value: function (label) { this.attachToParentComponent(); } }, @@ -2384,7 +2427,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto for (i = 0; (component = components[i]); i++) { component.attachToParentComponent(); } - } + } } } } @@ -2559,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 @@ -2975,6 +3065,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, + * isDroppable would be a more consistent name while close to standard, + * acceptDrop/acceptsDrop could work too */ registerDroppable: { value: function () { @@ -3076,7 +3169,7 @@ var Component = exports.Component = Target.specialize(/** @lends Component.proto } } this._shouldBuildOut = false; - if (this._inDocument) { + if (this.inDocument) { this._buildIn(); } } @@ -3096,7 +3189,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(); } } @@ -3292,7 +3385,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(); } @@ -3344,6 +3437,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) { @@ -3366,6 +3463,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) { @@ -3389,7 +3511,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; - } + } } } } @@ -3443,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); + } + } + } }, @@ -3586,47 +3726,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]; @@ -3672,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; + } + } + } + }, { @@ -3734,7 +4175,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 +4496,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 +4506,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 +4532,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 +4541,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 +4551,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 +4561,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 +4593,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 +4655,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++) { @@ -4509,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); @@ -4574,7 +5015,7 @@ var RootComponent = Component.specialize( /** @lends RootComponent.prototype */{ }); - + exports.__root__ = rootComponent = new RootComponent().init(); //https://github.com/kangax/html-minifier/issues/63 diff --git a/ui/control.js b/ui/control.js index bcbb4260d8..514327c5dc 100644 --- a/ui/control.js +++ b/ui/control.js @@ -4,7 +4,7 @@ var Component = require("ui/component").Component, deprecate = require("core/deprecate"), - Map = require("collections/map"); + Map = require("core/collections/map"); /** Base component for all native components, such as RadioButton and Checkbox. @@ -254,7 +254,7 @@ var Control = exports.Control = Component.specialize(/** @lends module:montage/u /** Specifies whether the button should receive focus or not. @type {boolean} - @event longpress + @event longpress */ preventFocus: { get: function () { 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..14cb0ecc7c --- /dev/null +++ b/ui/data-editor.js @@ -0,0 +1,407 @@ +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, +Montage = require("core/core").Montage, +//UUID = require("core/uuid"), +ONE_WAY = "<-", +ONE_WAY_RIGHT = "->", +TWO_WAY = "<->"; + + + +/** + * 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# */ { + constructor: { + value: function DataEditor () { + this.super(); + // this.canDrawGate.setField("dataLoaded", false); + // console.log("---------- "+this.constructor.name+" inDocument:"+this.inDocument+" —— dataLoaded: false",value); + + // this.addPathChangeListener( + // "data", + // this, + // "handleDataChange" + // ); + + 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 + * 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); + if(this.orderings) { + this.__dataQuery.orderings = this.orderings; + } + if(this.fetchLimit) { + this.__dataQuery.fetchLimit = this.fetchLimit; + } + if(this.readExpressions) { + this.__dataQuery.readExpressions = this.readExpressions; + } + + } + } + 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() { + + if(this._dataQuery) { + var dataService = this.dataService, + currentDataStream = this.dataStream, + dataStream, + self = this; + 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); + self.dataStream = dataStream; + + //We need to + dataService.cancelDataStream(currentDataStream); + }, + function(error) { + console.log("fetchData failed:",error); + }) + .finally(() => { + // this.canDrawGate.setField("dataLoaded", true); + }); + } + } + }, + + dataDidChange: { + value: function () { + } + }, + + + fetchDataIfNeeded: { + value: function() { + + //Blow the cache: + this.__dataQuery = null; + + //If we're active for trhe user, we re-fetch + //if(this.inDocument && this._dataQuery) { + if(this.isTemplateLoaded && this._dataQuery) { + 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.fetchDataIfNeeded(); + } + }, + + /** + * 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; + } + } + }, + + fetchLimit: { + value: undefined + }, + + readExpressions: { + value: undefined + }, + + /** + * 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; + } + } + }, + + /** + * 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 + }, + data: { + get: function () { + return this._data; + }, + set: function (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); + } + + + this._data = value; + + this.dataDidChange(value); + + } + } + }, + + handleDataChange: { + value: function (data) { + } + } + + +}); + + +//Montage.defineUuidProperty(exports.DataEditor.prototype); 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# */ { + + +}); diff --git a/ui/flow.reel/flow.js b/ui/flow.reel/flow.js index edf3f293db..31c28b0b06 100644 --- a/ui/flow.reel/flow.js +++ b/ui/flow.reel/flow.js @@ -4,7 +4,7 @@ * @module "montage/ui/flow.reel" */ var Component = require("../component").Component, - observeProperty = require("frb/observers").observeProperty, + observeProperty = require("core/frb/observers").observeProperty, FlowBezierSpline = require("./flow-bezier-spline").FlowBezierSpline, RangeController = require("../../core/range-controller").RangeController; @@ -252,7 +252,7 @@ var Flow = exports.Flow = Component.specialize( /** @lends Flow.prototype # */ { densities[i] = iPathKnot.previousDensity; // TODO: implement previous/next density for (j in pathUnits) { if (pathUnits.hasOwnProperty(j)) { - splinePathParameters[j].data.push(iPathKnot[j]); + splinePathParameters[j].data.push(iPathKnot[j]); } } } @@ -333,7 +333,7 @@ var Flow = exports.Flow = Component.specialize( /** @lends Flow.prototype # */ { parametersLength = iSplinePath.parameters[j].data.length; for (k = 0; k < parametersLength; k++) { path.knots[k][j] = iSplinePath.parameters[j].data[k]; - } + } } } if (this._paths[i].hasOwnProperty("headOffset")) { @@ -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++) { @@ -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++) { @@ -1494,7 +1496,7 @@ var Flow = exports.Flow = Component.specialize( /** @lends Flow.prototype # */ { // a good balance between precision and performance. time = timestamp; iterations = 6; - + var interval1 = this.lastDrawTime ? (time - this.lastDrawTime) * 0.018 * this._elasticScrollingSpeed : 0, interval = 1 - (interval1 / iterations), offset1, offset2, resultOffset, diff --git a/ui/list-item.reel/list-item.js b/ui/list-item.reel/list-item.js index 24ab2579ad..25566eb7b4 100755 --- a/ui/list-item.reel/list-item.js +++ b/ui/list-item.reel/list-item.js @@ -1,6 +1,6 @@ var Component = require("../component").Component, PressComposer = require("../../composer/press-composer").PressComposer, - assign = require("frb/assign"), + assign = require("core/frb/assign"), Montage = require("../../core/core").Montage; /** @@ -61,7 +61,7 @@ exports.ListItem = Component.specialize({ "(userInterfaceDescriptor.listItemIsExpandable || " + "isExpandable) : isExpandable" } - }); + }); //FIXME: not safe! this._templateDidLoad = true; this._loadDataUserInterfaceDescriptorIfNeeded(); @@ -75,7 +75,7 @@ exports.ListItem = Component.specialize({ value: { set: function (value) { this._value = !!value; - }, + }, get: function () { return this._value; } @@ -174,7 +174,7 @@ exports.ListItem = Component.specialize({ this.__pressComposer = new PressComposer(); this.__pressComposer.delegate = this; this.addComposerForElement( - this.__pressComposer, + this.__pressComposer, this._valuePlaceholderComponent.element ); } @@ -185,7 +185,7 @@ exports.ListItem = Component.specialize({ shouldComposerSurrenderPointerToComponent: { value: function (pressComposer, pointer, component) { - if (pressComposer === this.__pressComposer && + if (pressComposer === this.__pressComposer && this.element.contains(component.element) ) { return false; @@ -248,7 +248,7 @@ exports.ListItem = Component.specialize({ checked ); } - + this.value = checked; } }, @@ -258,7 +258,7 @@ exports.ListItem = Component.specialize({ if (this.data && this._templateDidLoad) { var self = this, infoDelegate; - + return this.loadUserInterfaceDescriptor(this.data).then(function (UIDescriptor) { self.userInterfaceDescriptor = UIDescriptor || self.userInterfaceDescriptor; // trigger biddings. @@ -354,7 +354,7 @@ exports.ListItem = Component.specialize({ self.list ) || self._isExpandable; // default value }); - } + } } } 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"; } } }, diff --git a/ui/repetition.reel/repetition.js b/ui/repetition.reel/repetition.js index debc101b2b..8d8305bbcb 100644 --- a/ui/repetition.reel/repetition.js +++ b/ui/repetition.reel/repetition.js @@ -7,9 +7,9 @@ var RangeController = require("../../core/range-controller").RangeController; var Promise = require("../../core/promise").Promise; var PressComposer = require("../../composer/press-composer").PressComposer; -var Map = require("collections/map"); -var Set = require("collections/set"); -var PropertyChanges = require("collections/listen/property-changes"); +var Map = require("core/collections/map"); +var Set = require("core/collections/set"); +var PropertyChanges = require("core/collections/listen/property-changes"); var logger = require("../../core/logger").logger("repetition").color.magenta(); @@ -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); + } + //} + } }, @@ -586,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); } } } @@ -638,7 +650,7 @@ var Repetition = exports.Repetition = Component.specialize(/** @lends Repetition */ initWithContent: { value: function (content) { - this.object = content; + this.content = content; return this; } }, @@ -711,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 @@ -728,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.super(key); + } + }, + + + allowsMultipleSelection: { set: function (allowsMultipleSelection) { allowsMultipleSelection = !!allowsMultipleSelection; @@ -1126,7 +1167,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 = []; @@ -2018,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); } }, @@ -2098,6 +2148,8 @@ var Repetition = exports.Repetition = Component.specialize(/** @lends Repetition } } + //this.dispatchEvent("press"); + this._ignoreSelection(); } }, 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"}, diff --git a/ui/slider.reel/slider.js b/ui/slider.reel/slider.js index ec8edb7dba..aaa2de9bbb 100644 --- a/ui/slider.reel/slider.js +++ b/ui/slider.reel/slider.js @@ -7,8 +7,8 @@ var Control = require("ui/control").Control, TranslateComposer = require("../../composer/translate-composer").TranslateComposer, KeyComposer = require("../../composer/key-composer").KeyComposer, - Map = require("collections/map"), - WeakMap = require("collections/weak-map"), + Map = require("core/collections/map"), + WeakMap = require("core/collections/weak-map"), MONTAGE_SLIDER_THUMB_CLASS = "montage-Slider--thumb"; /** @@ -327,9 +327,9 @@ var Slider = exports.Slider = Control.specialize({ dimensionLength = this._spacer.offsetHeight - parseFloat(computedStyle.getPropertyValue("padding-top")) - parseFloat(computedStyle.getPropertyValue("padding-bottom")); - } else { - dimensionLength = this._spacer.offsetWidth - - parseFloat(computedStyle.getPropertyValue("padding-left")) - + } else { + dimensionLength = this._spacer.offsetWidth - + parseFloat(computedStyle.getPropertyValue("padding-left")) - parseFloat(computedStyle.getPropertyValue("padding-right")); } @@ -672,7 +672,7 @@ Should introduce a validate method } else if (this._value < this._min) { return this._min; } - + return this._value; }, set: function (value) { @@ -727,7 +727,7 @@ Should introduce a validate method value: null }, - /* Axis should be renamed orientation and a setter should be put in place for + /* Axis should be renamed orientation and a setter should be put in place for backward compatibility */ diff --git a/ui/succession.info/sample/ui/main.reel/main.js b/ui/succession.info/sample/ui/main.reel/main.js index e1b4a6e5da..af14a30f62 100644 --- a/ui/succession.info/sample/ui/main.reel/main.js +++ b/ui/succession.info/sample/ui/main.reel/main.js @@ -42,8 +42,8 @@ exports.Main = Component.specialize({ //Object.observe(component, function (changes) { // changes.forEach(function (change) { - // if (change.name === '_inDocument') { - // console.log("Property " + '_inDocument' + " changed"); + // if (change.name === 'inDocument') { + // console.log("Property " + 'inDocument' + " changed"); // console.log(change); // } // }); 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) { diff --git a/ui/tree-list.reel/tree-list.js b/ui/tree-list.reel/tree-list.js index ca0428fd79..c78251f606 100644 --- a/ui/tree-list.reel/tree-list.js +++ b/ui/tree-list.reel/tree-list.js @@ -5,7 +5,7 @@ var Component = require("../component").Component, TreeNode = require("../../core/tree-controller").TreeNode, TranslateComposer = require("../../composer/translate-composer").TranslateComposer, - WeakMap = require("collections/weak-map"); + WeakMap = require("core/collections/weak-map"); var PLACEHOLDER_POSITION = { @@ -766,7 +766,7 @@ var TreeList = exports.TreeList = Component.specialize(/** @lends TreeList.proto _getPlaceholderPositionOnTreeNode: { value: function (treeNode, pointerPositionX, pointerPositionY, canBeOver) { var treeNodeElement = this._findTreeNodeElementWithNode(treeNode), - placeholderRect = this._placeholderBoundingClientRect, + placeholderRect = this._placeholderBoundingClientRect, rowRect = treeNodeElement.getBoundingClientRect(), thresholdHeight = this._placeholderThreshold, maxBottom = rowRect.bottom + thresholdHeight, @@ -781,7 +781,7 @@ var TreeList = exports.TreeList = Component.specialize(/** @lends TreeList.proto } else if (pointerPositionY >= minTop && pointerPositionY <= maxTop) { return PLACEHOLDER_POSITION.BEFORE_NODE; } - + return PLACEHOLDER_POSITION.OVER_NODE; } }, @@ -822,7 +822,7 @@ var TreeList = exports.TreeList = Component.specialize(/** @lends TreeList.proto treeNodeOverDataObject = treeNodeOver.data, nodeOverAcceptChild = this._nodeAcceptChild(treeNodeOver), overNodeIndex = -1; - + this._placerholderPosition = this._getPlaceholderPositionOnTreeNode( treeNodeOver, positionX, @@ -928,7 +928,7 @@ var TreeList = exports.TreeList = Component.specialize(/** @lends TreeList.proto drawnIterations = this.repetition._drawnIterations, rootCondition, marginTop, object, iteration, element, rowHeight, i, length; - + this.element.classList.add(TreeList.PLACEHOLDER_OVER); for (i = 0, length = drawnIterations.length; i < length; i++) {