From ac46df8daea09899ce30dc8fd70986e258c746bf Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 9 Feb 2018 06:46:43 -0500 Subject: Move Add-on SDK source to toolkit/jetpack --- toolkit/jetpack/sdk/util/array.js | 123 +++++++ toolkit/jetpack/sdk/util/collection.js | 115 ++++++ toolkit/jetpack/sdk/util/contract.js | 55 +++ toolkit/jetpack/sdk/util/deprecate.js | 40 ++ toolkit/jetpack/sdk/util/dispatcher.js | 54 +++ toolkit/jetpack/sdk/util/list.js | 90 +++++ toolkit/jetpack/sdk/util/match-pattern.js | 113 ++++++ toolkit/jetpack/sdk/util/object.js | 104 ++++++ toolkit/jetpack/sdk/util/rules.js | 53 +++ toolkit/jetpack/sdk/util/sequence.js | 593 ++++++++++++++++++++++++++++++ toolkit/jetpack/sdk/util/uuid.js | 19 + 11 files changed, 1359 insertions(+) create mode 100644 toolkit/jetpack/sdk/util/array.js create mode 100644 toolkit/jetpack/sdk/util/collection.js create mode 100644 toolkit/jetpack/sdk/util/contract.js create mode 100644 toolkit/jetpack/sdk/util/deprecate.js create mode 100644 toolkit/jetpack/sdk/util/dispatcher.js create mode 100644 toolkit/jetpack/sdk/util/list.js create mode 100644 toolkit/jetpack/sdk/util/match-pattern.js create mode 100644 toolkit/jetpack/sdk/util/object.js create mode 100644 toolkit/jetpack/sdk/util/rules.js create mode 100644 toolkit/jetpack/sdk/util/sequence.js create mode 100644 toolkit/jetpack/sdk/util/uuid.js (limited to 'toolkit/jetpack/sdk/util') diff --git a/toolkit/jetpack/sdk/util/array.js b/toolkit/jetpack/sdk/util/array.js new file mode 100644 index 000000000..1d61a973e --- /dev/null +++ b/toolkit/jetpack/sdk/util/array.js @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + +/** + * Returns `true` if given `array` contain given `element` or `false` + * otherwise. + * @param {Array} array + * Target array. + * @param {Object|String|Number|Boolean} element + * Element being looked up. + * @returns {Boolean} + */ +var has = exports.has = function has(array, element) { + // shorter and faster equivalent of `array.indexOf(element) >= 0` + return !!~array.indexOf(element); +}; +var hasAny = exports.hasAny = function hasAny(array, elements) { + if (arguments.length < 2) + return false; + if (!Array.isArray(elements)) + elements = [ elements ]; + return array.some(function (element) { + return has(elements, element); + }); +}; + +/** + * Adds given `element` to the given `array` if it does not contain it yet. + * `true` is returned if element was added otherwise `false` is returned. + * @param {Array} array + * Target array. + * @param {Object|String|Number|Boolean} element + * Element to be added. + * @returns {Boolean} + */ +var add = exports.add = function add(array, element) { + var result; + if ((result = !has(array, element))) + array.push(element); + + return result; +}; + +/** + * Removes first occurrence of the given `element` from the given `array`. If + * `array` does not contain given `element` `false` is returned otherwise + * `true` is returned. + * @param {Array} array + * Target array. + * @param {Object|String|Number|Boolean} element + * Element to be removed. + * @returns {Boolean} + */ +exports.remove = function remove(array, element) { + var result; + if ((result = has(array, element))) + array.splice(array.indexOf(element), 1); + + return result; +}; + +/** + * Produces a duplicate-free version of the given `array`. + * @param {Array} array + * Source array. + * @returns {Array} + */ +function unique(array) { + return array.reduce(function(result, item) { + add(result, item); + return result; + }, []); +}; +exports.unique = unique; + +/** + * Produce an array that contains the union: each distinct element from all + * of the passed-in arrays. + */ +function union() { + return unique(Array.concat.apply(null, arguments)); +}; +exports.union = union; + +exports.flatten = function flatten(array){ + var flat = []; + for (var i = 0, l = array.length; i < l; i++) { + flat = flat.concat(Array.isArray(array[i]) ? flatten(array[i]) : array[i]); + } + return flat; +}; + +function fromIterator(iterator) { + let array = []; + if (iterator.__iterator__) { + for (let item of iterator) + array.push(item); + } + else { + for (let item of iterator) + array.push(item); + } + return array; +} +exports.fromIterator = fromIterator; + +function find(array, predicate, fallback) { + var index = 0; + var count = array.length; + while (index < count) { + var value = array[index]; + if (predicate(value)) return value; + else index = index + 1; + } + return fallback; +} +exports.find = find; diff --git a/toolkit/jetpack/sdk/util/collection.js b/toolkit/jetpack/sdk/util/collection.js new file mode 100644 index 000000000..194a29470 --- /dev/null +++ b/toolkit/jetpack/sdk/util/collection.js @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + +exports.Collection = Collection; + +/** + * Adds a collection property to the given object. Setting the property to a + * scalar value empties the collection and adds the value. Setting it to an + * array empties the collection and adds all the items in the array. + * + * @param obj + * The property will be defined on this object. + * @param propName + * The name of the property. + * @param array + * If given, this will be used as the collection's backing array. + */ +exports.addCollectionProperty = function addCollProperty(obj, propName, array) { + array = array || []; + let publicIface = new Collection(array); + + Object.defineProperty(obj, propName, { + configurable: true, + enumerable: true, + + set: function set(itemOrItems) { + array.splice(0, array.length); + publicIface.add(itemOrItems); + }, + + get: function get() { + return publicIface; + } + }); +}; + +/** + * A collection is ordered, like an array, but its items are unique, like a set. + * + * @param array + * The collection is backed by an array. If this is given, it will be + * used as the backing array. This way the caller can fully control the + * collection. Otherwise a new empty array will be used, and no one but + * the collection will have access to it. + */ +function Collection(array) { + array = array || []; + + /** + * Provides iteration over the collection. Items are yielded in the order + * they were added. + */ + this.__iterator__ = function Collection___iterator__() { + let items = array.slice(); + for (let i = 0; i < items.length; i++) + yield items[i]; + }; + + /** + * The number of items in the collection. + */ + this.__defineGetter__("length", function Collection_get_length() { + return array.length; + }); + + /** + * Adds a single item or an array of items to the collection. Any items + * already contained in the collection are ignored. + * + * @param itemOrItems + * An item or array of items. + * @return The collection. + */ + this.add = function Collection_add(itemOrItems) { + let items = toArray(itemOrItems); + for (let i = 0; i < items.length; i++) { + let item = items[i]; + if (array.indexOf(item) < 0) + array.push(item); + } + return this; + }; + + /** + * Removes a single item or an array of items from the collection. Any items + * not contained in the collection are ignored. + * + * @param itemOrItems + * An item or array of items. + * @return The collection. + */ + this.remove = function Collection_remove(itemOrItems) { + let items = toArray(itemOrItems); + for (let i = 0; i < items.length; i++) { + let idx = array.indexOf(items[i]); + if (idx >= 0) + array.splice(idx, 1); + } + return this; + }; +}; + +function toArray(itemOrItems) { + let isArr = itemOrItems && + itemOrItems.constructor && + itemOrItems.constructor.name === "Array"; + return isArr ? itemOrItems : [itemOrItems]; +} diff --git a/toolkit/jetpack/sdk/util/contract.js b/toolkit/jetpack/sdk/util/contract.js new file mode 100644 index 000000000..c689ea601 --- /dev/null +++ b/toolkit/jetpack/sdk/util/contract.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { validateOptions: valid } = require("../deprecated/api-utils"); +const method = require("method/core"); + +// Function takes property validation rules and returns function that given +// an `options` object will return validated / normalized options back. If +// option(s) are invalid validator will throw exception described by rules. +// Returned will also have contain `rules` property with a given validation +// rules and `properties` function that can be used to generate validated +// property getter and setters can be mixed into prototype. For more details +// see `properties` function below. +function contract(rules) { + const validator = (instance, options) => { + return valid(options || instance || {}, rules); + }; + validator.rules = rules + validator.properties = function(modelFor) { + return properties(modelFor, rules); + } + return validator; +} +exports.contract = contract + +// Function takes `modelFor` instance state model accessor functions and +// a property validation rules and generates object with getters and setters +// that can be mixed into prototype. Property accessors update model for the +// given instance. If you wish to react to property updates you can always +// override setters to put specific logic. +function properties(modelFor, rules) { + let descriptor = Object.keys(rules).reduce(function(descriptor, name) { + descriptor[name] = { + get: function() { return modelFor(this)[name] }, + set: function(value) { + let change = {}; + change[name] = value; + modelFor(this)[name] = valid(change, rules)[name]; + } + } + return descriptor + }, {}); + return Object.create(Object.prototype, descriptor); +} +exports.properties = properties; + +const validate = method("contract/validate"); +exports.validate = validate; diff --git a/toolkit/jetpack/sdk/util/deprecate.js b/toolkit/jetpack/sdk/util/deprecate.js new file mode 100644 index 000000000..40f236de5 --- /dev/null +++ b/toolkit/jetpack/sdk/util/deprecate.js @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + +const { get, format } = require("../console/traceback"); +const { get: getPref } = require("../preferences/service"); +const PREFERENCE = "devtools.errorconsole.deprecation_warnings"; + +function deprecateUsage(msg) { + // Print caller stacktrace in order to help figuring out which code + // does use deprecated thing + let stack = get().slice(2); + + if (getPref(PREFERENCE)) + console.error("DEPRECATED: " + msg + "\n" + format(stack)); +} +exports.deprecateUsage = deprecateUsage; + +function deprecateFunction(fun, msg) { + return function deprecated() { + deprecateUsage(msg); + return fun.apply(this, arguments); + }; +} +exports.deprecateFunction = deprecateFunction; + +function deprecateEvent(fun, msg, evtTypes) { + return function deprecateEvent(evtType) { + if (evtTypes.indexOf(evtType) >= 0) + deprecateUsage(msg); + return fun.apply(this, arguments); + }; +} +exports.deprecateEvent = deprecateEvent; diff --git a/toolkit/jetpack/sdk/util/dispatcher.js b/toolkit/jetpack/sdk/util/dispatcher.js new file mode 100644 index 000000000..67d29dfed --- /dev/null +++ b/toolkit/jetpack/sdk/util/dispatcher.js @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + +const method = require("method/core"); + +// Utility function that is just an enhancement over `method` to +// allow predicate based dispatch in addition to polymorphic +// dispatch. Unfortunately polymorphic dispatch does not quite +// cuts it in the world of XPCOM where no types / classes exist +// and all the XUL nodes share same type / prototype. +// Probably this is more generic and belongs some place else, but +// we can move it later once this will be relevant. +var dispatcher = hint => { + const base = method(hint); + // Make a map for storing predicate, implementation mappings. + let implementations = new Map(); + + // Dispatcher function goes through `predicate, implementation` + // pairs to find predicate that matches first argument and + // returns application of arguments on the associated + // `implementation`. If no matching predicate is found delegates + // to a `base` polymorphic function. + let dispatch = (value, ...rest) => { + for (let [predicate, implementation] of implementations) { + if (predicate(value)) + return implementation(value, ...rest); + } + + return base(value, ...rest); + }; + + // Expose base API. + dispatch.define = base.define; + dispatch.implement = base.implement; + dispatch.toString = base.toString; + + // Add a `when` function to allow extending function via + // predicates. + dispatch.when = (predicate, implementation) => { + if (implementations.has(predicate)) + throw TypeError("Already implemented for the given predicate"); + implementations.set(predicate, implementation); + }; + + return dispatch; +}; + +exports.dispatcher = dispatcher; diff --git a/toolkit/jetpack/sdk/util/list.js b/toolkit/jetpack/sdk/util/list.js new file mode 100644 index 000000000..6d7d2dea9 --- /dev/null +++ b/toolkit/jetpack/sdk/util/list.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +module.metadata = { + "stability": "experimental" +}; + +const { Class } = require('../core/heritage'); +const listNS = require('../core/namespace').ns(); + +const listOptions = { + /** + * List constructor can take any number of element to populate itself. + * @params {Object|String|Number} element + * @example + * List(1,2,3).length == 3 // true + */ + initialize: function List() { + listNS(this).keyValueMap = []; + + for (let i = 0, ii = arguments.length; i < ii; i++) + addListItem(this, arguments[i]); + }, + /** + * Number of elements in this list. + * @type {Number} + */ + get length() { + return listNS(this).keyValueMap.length; + }, + /** + * Returns a string representing this list. + * @returns {String} + */ + toString: function toString() { + return 'List(' + listNS(this).keyValueMap + ')'; + }, + /** + * Custom iterator providing `List`s enumeration behavior. + * We cant reuse `_iterator` that is defined by `Iterable` since it provides + * iteration in an arbitrary order. + * @see https://developer.mozilla.org/en/JavaScript/Reference/Statements/for...in + * @param {Boolean} onKeys + */ + __iterator__: function __iterator__(onKeys, onKeyValue) { + let array = listNS(this).keyValueMap.slice(0), + i = -1; + for (let element of array) + yield onKeyValue ? [++i, element] : onKeys ? ++i : element; + }, +}; +listOptions[Symbol.iterator] = function iterator() { + return listNS(this).keyValueMap.slice(0)[Symbol.iterator](); +}; +const List = Class(listOptions); +exports.List = List; + +function addListItem(that, value) { + let list = listNS(that).keyValueMap, + index = list.indexOf(value); + + if (-1 === index) { + try { + that[that.length] = value; + } + catch (e) {} + list.push(value); + } +} +exports.addListItem = addListItem; + +function removeListItem(that, element) { + let list = listNS(that).keyValueMap, + index = list.indexOf(element); + + if (0 <= index) { + list.splice(index, 1); + try { + for (let length = list.length; index < length; index++) + that[index] = list[index]; + that[list.length] = undefined; + } + catch(e){} + } +} +exports.removeListItem = removeListItem; + +exports.listNS = listNS; diff --git a/toolkit/jetpack/sdk/util/match-pattern.js b/toolkit/jetpack/sdk/util/match-pattern.js new file mode 100644 index 000000000..a0eb88b49 --- /dev/null +++ b/toolkit/jetpack/sdk/util/match-pattern.js @@ -0,0 +1,113 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { URL } = require('../url'); +const cache = {}; + +function MatchPattern(pattern) { + if (cache[pattern]) return cache[pattern]; + + if (typeof pattern.test == "function") { + // For compatibility with -moz-document rules, we require the RegExp's + // global, ignoreCase, and multiline flags to be set to false. + if (pattern.global) { + throw new Error("A RegExp match pattern cannot be set to `global` " + + "(i.e. //g)."); + } + if (pattern.multiline) { + throw new Error("A RegExp match pattern cannot be set to `multiline` " + + "(i.e. //m)."); + } + + this.regexp = pattern; + } + else { + let firstWildcardPosition = pattern.indexOf("*"); + let lastWildcardPosition = pattern.lastIndexOf("*"); + if (firstWildcardPosition != lastWildcardPosition) + throw new Error("There can be at most one '*' character in a wildcard."); + + if (firstWildcardPosition == 0) { + if (pattern.length == 1) + this.anyWebPage = true; + else if (pattern[1] != ".") + throw new Error("Expected a *. string, got: " + pattern); + else + this.domain = pattern.substr(2); + } + else { + if (pattern.indexOf(":") == -1) { + throw new Error("When not using *.example.org wildcard, the string " + + "supplied is expected to be either an exact URL to " + + "match or a URL prefix. The provided string ('" + + pattern + "') is unlikely to match any pages."); + } + + if (firstWildcardPosition == -1) + this.exactURL = pattern; + else if (firstWildcardPosition == pattern.length - 1) + this.urlPrefix = pattern.substr(0, pattern.length - 1); + else { + throw new Error("The provided wildcard ('" + pattern + "') has a '*' " + + "in an unexpected position. It is expected to be the " + + "first or the last character in the wildcard."); + } + } + } + + cache[pattern] = this; +} + +MatchPattern.prototype = { + test: function MatchPattern_test(urlStr) { + try { + var url = URL(urlStr); + } + catch (err) { + return false; + } + + // Test the URL against a RegExp pattern. For compatibility with + // -moz-document rules, we require the RegExp to match the entire URL, + // so we not only test for a match, we also make sure the matched string + // is the entire URL string. + // + // Assuming most URLs don't match most match patterns, we call `test` for + // speed when determining whether or not the URL matches, then call `exec` + // for the small subset that match to make sure the entire URL matches. + if (this.regexp && this.regexp.test(urlStr) && + this.regexp.exec(urlStr)[0] == urlStr) + return true; + + if (this.anyWebPage && /^(https?|ftp)$/.test(url.scheme)) + return true; + + if (this.exactURL && this.exactURL == urlStr) + return true; + + // Tests the urlStr against domain and check if + // wildcard submitted (*.domain.com), it only allows + // subdomains (sub.domain.com) or from the root (http://domain.com) + // and reject non-matching domains (otherdomain.com) + // bug 856913 + if (this.domain && url.host && + (url.host === this.domain || + url.host.slice(-this.domain.length - 1) === "." + this.domain)) + return true; + + if (this.urlPrefix && 0 == urlStr.indexOf(this.urlPrefix)) + return true; + + return false; + }, + + toString: () => '[object MatchPattern]' +}; + +exports.MatchPattern = MatchPattern; diff --git a/toolkit/jetpack/sdk/util/object.js b/toolkit/jetpack/sdk/util/object.js new file mode 100644 index 000000000..9d202bb51 --- /dev/null +++ b/toolkit/jetpack/sdk/util/object.js @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { flatten } = require('./array'); + +/** + * Merges all the properties of all arguments into first argument. If two or + * more argument objects have own properties with the same name, the property + * is overridden, with precedence from right to left, implying, that properties + * of the object on the left are overridden by a same named property of the + * object on the right. + * + * Any argument given with "falsy" value - commonly `null` and `undefined` in + * case of objects - are skipped. + * + * @examples + * var a = { bar: 0, a: 'a' } + * var b = merge(a, { foo: 'foo', bar: 1 }, { foo: 'bar', name: 'b' }); + * b === a // true + * b.a // 'a' + * b.foo // 'bar' + * b.bar // 1 + * b.name // 'b' + */ +function merge(source) { + let descriptor = {}; + + // `Boolean` converts the first parameter to a boolean value. Any object is + // converted to `true` where `null` and `undefined` becames `false`. Therefore + // the `filter` method will keep only objects that are defined and not null. + Array.slice(arguments, 1).filter(Boolean).forEach(function onEach(properties) { + getOwnPropertyIdentifiers(properties).forEach(function(name) { + descriptor[name] = Object.getOwnPropertyDescriptor(properties, name); + }); + }); + return Object.defineProperties(source, descriptor); +} +exports.merge = merge; + +/** + * Returns an object that inherits from the first argument and contains all the + * properties from all following arguments. + * `extend(source1, source2, source3)` is equivalent of + * `merge(Object.create(source1), source2, source3)`. + */ +function extend(source) { + let rest = Array.slice(arguments, 1); + rest.unshift(Object.create(source)); + return merge.apply(null, rest); +} +exports.extend = extend; + +function has(obj, key) { + return obj.hasOwnProperty(key); +} +exports.has = has; + +function each(obj, fn) { + for (let key in obj) has(obj, key) && fn(obj[key], key, obj); +} +exports.each = each; + +/** + * Like `merge`, except no property descriptors are manipulated, for use + * with platform objects. Identical to underscore's `extend`. Useful for + * merging XPCOM objects + */ +function safeMerge(source) { + Array.slice(arguments, 1).forEach(function onEach (obj) { + for (let prop in obj) source[prop] = obj[prop]; + }); + return source; +} +exports.safeMerge = safeMerge; + +/* + * Returns a copy of the object without omitted properties + */ +function omit(source, ...values) { + let copy = {}; + let keys = flatten(values); + for (let prop in source) + if (!~keys.indexOf(prop)) + copy[prop] = source[prop]; + return copy; +} +exports.omit = omit; + +// get object's own property Symbols and/or Names, including nonEnumerables by default +function getOwnPropertyIdentifiers(object, options = { names: true, symbols: true, nonEnumerables: true }) { + const symbols = !options.symbols ? [] : + Object.getOwnPropertySymbols(object); + const names = !options.names ? [] : + options.nonEnumerables ? Object.getOwnPropertyNames(object) : + Object.keys(object); + return [...names, ...symbols]; +} +exports.getOwnPropertyIdentifiers = getOwnPropertyIdentifiers; diff --git a/toolkit/jetpack/sdk/util/rules.js b/toolkit/jetpack/sdk/util/rules.js new file mode 100644 index 000000000..98e3109b0 --- /dev/null +++ b/toolkit/jetpack/sdk/util/rules.js @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { Class } = require('../core/heritage'); +const { MatchPattern } = require('./match-pattern'); +const { emit } = require('../event/core'); +const { EventTarget } = require('../event/target'); +const { List, addListItem, removeListItem } = require('./list'); + +// Should deprecate usage of EventEmitter/compose +const Rules = Class({ + implements: [ + EventTarget, + List + ], + add: function(...rules) { + return [].concat(rules).forEach(function onAdd(rule) { + addListItem(this, rule); + emit(this, 'add', rule); + }, this); + }, + remove: function(...rules) { + return [].concat(rules).forEach(function onRemove(rule) { + removeListItem(this, rule); + emit(this, 'remove', rule); + }, this); + }, + get: function(rule) { + let found = false; + for (let i in this) if (this[i] === rule) found = true; + return found; + }, + // Returns true if uri matches atleast one stored rule + matchesAny: function(uri) { + return !!filterMatches(this, uri).length; + }, + toString: () => '[object Rules]' +}); +exports.Rules = Rules; + +function filterMatches(instance, uri) { + let matches = []; + for (let i in instance) { + if (new MatchPattern(instance[i]).test(uri)) matches.push(instance[i]); + } + return matches; +} diff --git a/toolkit/jetpack/sdk/util/sequence.js b/toolkit/jetpack/sdk/util/sequence.js new file mode 100644 index 000000000..28e3de255 --- /dev/null +++ b/toolkit/jetpack/sdk/util/sequence.js @@ -0,0 +1,593 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + +// Disclamer: +// In this module we'll have some common argument / variable names +// to hint their type or behavior. +// +// - `f` stands for "function" that is intended to be side effect +// free. +// - `p` stands for "predicate" that is function which returns logical +// true or false and is intended to be side effect free. +// - `x` / `y` single item of the sequence. +// - `xs` / `ys` sequence of `x` / `y` items where `x` / `y` signifies +// type of the items in sequence, so sequence is not of the same item. +// - `_` used for argument(s) or variable(s) who's values are ignored. + +const { complement, flip, identity } = require("../lang/functional"); +const { isArray, isArguments, isMap, isSet, isGenerator, + isString, isBoolean, isNumber } = require("../lang/type"); + +const Sequence = function Sequence(iterator) { + if (!isGenerator(iterator)) { + throw TypeError("Expected generator argument"); + } + + this[Symbol.iterator] = iterator; +}; +exports.Sequence = Sequence; + +const polymorphic = dispatch => x => + x === null ? dispatch.null(null) : + x === void(0) ? dispatch.void(void(0)) : + isArray(x) ? (dispatch.array || dispatch.indexed)(x) : + isString(x) ? (dispatch.string || dispatch.indexed)(x) : + isArguments(x) ? (dispatch.arguments || dispatch.indexed)(x) : + isMap(x) ? dispatch.map(x) : + isSet(x) ? dispatch.set(x) : + isNumber(x) ? dispatch.number(x) : + isBoolean(x) ? dispatch.boolean(x) : + dispatch.default(x); + +const nogen = function*() {}; +const empty = () => new Sequence(nogen); +exports.empty = empty; + +const seq = polymorphic({ + null: empty, + void: empty, + array: identity, + string: identity, + arguments: identity, + map: identity, + set: identity, + default: x => x instanceof Sequence ? x : new Sequence(x) +}); +exports.seq = seq; + +// Function to cast seq to string. +const string = (...etc) => "".concat(...etc); +exports.string = string; + +// Function for casting seq to plain object. +const object = (...pairs) => { + let result = {}; + for (let [key, value] of pairs) + result[key] = value; + + return result; +}; +exports.object = object; + +// Takes `getEnumerator` function that returns `nsISimpleEnumerator` +// and creates lazy sequence of it's items. Note that function does +// not take `nsISimpleEnumerator` itslef because that would allow +// single iteration, which would not be consistent with rest of the +// lazy sequences. +const fromEnumerator = getEnumerator => seq(function* () { + const enumerator = getEnumerator(); + while (enumerator.hasMoreElements()) + yield enumerator.getNext(); +}); +exports.fromEnumerator = fromEnumerator; + +// Takes `object` and returns lazy sequence of own `[key, value]` +// pairs (does not include inherited and non enumerable keys). +const pairs = polymorphic({ + null: empty, + void: empty, + map: identity, + indexed: indexed => seq(function* () { + const count = indexed.length; + let index = 0; + while (index < count) { + yield [index, indexed[index]]; + index = index + 1; + } + }), + default: object => seq(function* () { + for (let key of Object.keys(object)) + yield [key, object[key]]; + }) +}); +exports.pairs = pairs; + +const names = polymorphic({ + null: empty, + void: empty, + default: object => seq(function*() { + for (let name of Object.getOwnPropertyNames(object)) { + yield name; + } + }) +}); +exports.names = names; + +const symbols = polymorphic({ + null: empty, + void: empty, + default: object => seq(function* () { + for (let symbol of Object.getOwnPropertySymbols(object)) { + yield symbol; + } + }) +}); +exports.symbols = symbols; + +const keys = polymorphic({ + null: empty, + void: empty, + indexed: indexed => seq(function* () { + const count = indexed.length; + let index = 0; + while (index < count) { + yield index; + index = index + 1; + } + }), + map: map => seq(function* () { + for (let [key, _] of map) + yield key; + }), + default: object => seq(function* () { + for (let key of Object.keys(object)) + yield key; + }) +}); +exports.keys = keys; + + +const values = polymorphic({ + null: empty, + void: empty, + set: identity, + indexed: indexed => seq(function* () { + const count = indexed.length; + let index = 0; + while (index < count) { + yield indexed[index]; + index = index + 1; + } + }), + map: map => seq(function* () { + for (let [_, value] of map) yield value; + }), + default: object => seq(function* () { + for (let key of Object.keys(object)) yield object[key]; + }) +}); +exports.values = values; + + + +// Returns a lazy sequence of `x`, `f(x)`, `f(f(x))` etc. +// `f` must be free of side-effects. Note that returned +// sequence is infinite so it must be consumed partially. +// +// Implements clojure iterate: +// http://clojuredocs.org/clojure_core/clojure.core/iterate +const iterate = (f, x) => seq(function* () { + let state = x; + while (true) { + yield state; + state = f(state); + } +}); +exports.iterate = iterate; + +// Returns a lazy sequence of the items in sequence for which `p(item)` +// returns `true`. `p` must be free of side-effects. +// +// Implements clojure filter: +// http://clojuredocs.org/clojure_core/clojure.core/filter +const filter = (p, sequence) => seq(function* () { + if (sequence !== null && sequence !== void(0)) { + for (let item of sequence) { + if (p(item)) + yield item; + } + } +}); +exports.filter = filter; + +// Returns a lazy sequence consisting of the result of applying `f` to the +// set of first items of each sequence, followed by applying f to the set +// of second items in each sequence, until any one of the sequences is +// exhausted. Any remaining items in other sequences are ignored. Function +// `f` should accept number-of-sequences arguments. +// +// Implements clojure map: +// http://clojuredocs.org/clojure_core/clojure.core/map +const map = (f, ...sequences) => seq(function* () { + const count = sequences.length; + // Optimize a single sequence case + if (count === 1) { + let [sequence] = sequences; + if (sequence !== null && sequence !== void(0)) { + for (let item of sequence) + yield f(item); + } + } + else { + // define args array that will be recycled on each + // step to aggregate arguments to be passed to `f`. + let args = []; + // define inputs to contain started generators. + let inputs = []; + + let index = 0; + while (index < count) { + inputs[index] = sequences[index][Symbol.iterator](); + index = index + 1; + } + + // Run loop yielding of applying `f` to the set of + // items at each step until one of the `inputs` is + // exhausted. + let done = false; + while (!done) { + let index = 0; + let value = void(0); + while (index < count && !done) { + ({ done, value } = inputs[index].next()); + + // If input is not exhausted yet store value in args. + if (!done) { + args[index] = value; + index = index + 1; + } + } + + // If none of the inputs is exhasted yet, `args` contain items + // from each input so we yield application of `f` over them. + if (!done) + yield f(...args); + } + } +}); +exports.map = map; + +// Returns a lazy sequence of the intermediate values of the reduction (as +// per reduce) of sequence by `f`, starting with `initial` value if provided. +// +// Implements clojure reductions: +// http://clojuredocs.org/clojure_core/clojure.core/reductions +const reductions = (...params) => { + const count = params.length; + let hasInitial = false; + let f, initial, source; + if (count === 2) { + [f, source] = params; + } + else if (count === 3) { + [f, initial, source] = params; + hasInitial = true; + } + else { + throw Error("Invoked with wrong number of arguments: " + count); + } + + const sequence = seq(source); + + return seq(function* () { + let started = hasInitial; + let result = void(0); + + // If initial is present yield it. + if (hasInitial) + yield (result = initial); + + // For each item of the sequence accumulate new result. + for (let item of sequence) { + // If nothing has being yield yet set result to first + // item and yield it. + if (!started) { + started = true; + yield (result = item); + } + // Otherwise accumulate new result and yield it. + else { + yield (result = f(result, item)); + } + } + + // If nothing has being yield yet it's empty sequence and no + // `initial` was provided in which case we need to yield `f()`. + if (!started) + yield f(); + }); +}; +exports.reductions = reductions; + +// `f` should be a function of 2 arguments. If `initial` is not supplied, +// returns the result of applying `f` to the first 2 items in sequence, then +// applying `f` to that result and the 3rd item, etc. If sequence contains no +// items, `f` must accept no arguments as well, and reduce returns the +// result of calling f with no arguments. If sequence has only 1 item, it +// is returned and `f` is not called. If `initial` is supplied, returns the +// result of applying `f` to `initial` and the first item in sequence, then +// applying `f` to that result and the 2nd item, etc. If sequence contains no +// items, returns `initial` and `f` is not called. +// +// Implements clojure reduce: +// http://clojuredocs.org/clojure_core/clojure.core/reduce +const reduce = (...args) => { + const xs = reductions(...args); + let x; + for (x of xs) void(0); + return x; +}; +exports.reduce = reduce; + +const each = (f, sequence) => { + for (let x of seq(sequence)) void(f(x)); +}; +exports.each = each; + + +const inc = x => x + 1; +// Returns the number of items in the sequence. `count(null)` && `count()` +// returns `0`. Also works on strings, arrays, Maps & Sets. + +// Implements clojure count: +// http://clojuredocs.org/clojure_core/clojure.core/count +const count = polymorphic({ + null: _ => 0, + void: _ => 0, + indexed: indexed => indexed.length, + map: map => map.size, + set: set => set.size, + default: xs => reduce(inc, 0, xs) +}); +exports.count = count; + +// Returns `true` if sequence has no items. + +// Implements clojure empty?: +// http://clojuredocs.org/clojure_core/clojure.core/empty_q +const isEmpty = sequence => { + // Treat `null` and `undefined` as empty sequences. + if (sequence === null || sequence === void(0)) + return true; + + // If contains any item non empty so return `false`. + for (let _ of sequence) + return false; + + // If has not returned yet, there was nothing to iterate + // so it's empty. + return true; +}; +exports.isEmpty = isEmpty; + +const and = (a, b) => a && b; + +// Returns true if `p(x)` is logical `true` for every `x` in sequence, else +// `false`. +// +// Implements clojure every?: +// http://clojuredocs.org/clojure_core/clojure.core/every_q +const isEvery = (p, sequence) => { + if (sequence !== null && sequence !== void(0)) { + for (let item of sequence) { + if (!p(item)) + return false; + } + } + return true; +}; +exports.isEvery = isEvery; + +// Returns the first logical true value of (p x) for any x in sequence, +// else `null`. +// +// Implements clojure some: +// http://clojuredocs.org/clojure_core/clojure.core/some +const some = (p, sequence) => { + if (sequence !== null && sequence !== void(0)) { + for (let item of sequence) { + if (p(item)) + return true; + } + } + return null; +}; +exports.some = some; + +// Returns a lazy sequence of the first `n` items in sequence, or all items if +// there are fewer than `n`. +// +// Implements clojure take: +// http://clojuredocs.org/clojure_core/clojure.core/take +const take = (n, sequence) => n <= 0 ? empty() : seq(function* () { + let count = n; + for (let item of sequence) { + yield item; + count = count - 1; + if (count === 0) break; + } +}); +exports.take = take; + +// Returns a lazy sequence of successive items from sequence while +// `p(item)` returns `true`. `p` must be free of side-effects. +// +// Implements clojure take-while: +// http://clojuredocs.org/clojure_core/clojure.core/take-while +const takeWhile = (p, sequence) => seq(function* () { + for (let item of sequence) { + if (!p(item)) + break; + + yield item; + } +}); +exports.takeWhile = takeWhile; + +// Returns a lazy sequence of all but the first `n` items in +// sequence. +// +// Implements clojure drop: +// http://clojuredocs.org/clojure_core/clojure.core/drop +const drop = (n, sequence) => seq(function* () { + if (sequence !== null && sequence !== void(0)) { + let count = n; + for (let item of sequence) { + if (count > 0) + count = count - 1; + else + yield item; + } + } +}); +exports.drop = drop; + +// Returns a lazy sequence of the items in sequence starting from the +// first item for which `p(item)` returns falsy value. +// +// Implements clojure drop-while: +// http://clojuredocs.org/clojure_core/clojure.core/drop-while +const dropWhile = (p, sequence) => seq(function* () { + let keep = false; + for (let item of sequence) { + keep = keep || !p(item); + if (keep) yield item; + } +}); +exports.dropWhile = dropWhile; + +// Returns a lazy sequence representing the concatenation of the +// suplied sequences. +// +// Implements clojure conact: +// http://clojuredocs.org/clojure_core/clojure.core/concat +const concat = (...sequences) => seq(function* () { + for (let sequence of sequences) + for (let item of sequence) + yield item; +}); +exports.concat = concat; + +// Returns the first item in the sequence. +// +// Implements clojure first: +// http://clojuredocs.org/clojure_core/clojure.core/first +const first = sequence => { + if (sequence !== null && sequence !== void(0)) { + for (let item of sequence) + return item; + } + return null; +}; +exports.first = first; + +// Returns a possibly empty sequence of the items after the first. +// +// Implements clojure rest: +// http://clojuredocs.org/clojure_core/clojure.core/rest +const rest = sequence => drop(1, sequence); +exports.rest = rest; + +// Returns the value at the index. Returns `notFound` or `undefined` +// if index is out of bounds. +const nth = (xs, n, notFound) => { + if (n >= 0) { + if (isArray(xs) || isArguments(xs) || isString(xs)) { + return n < xs.length ? xs[n] : notFound; + } + else if (xs !== null && xs !== void(0)) { + let count = n; + for (let x of xs) { + if (count <= 0) + return x; + + count = count - 1; + } + } + } + return notFound; +}; +exports.nth = nth; + +// Return the last item in sequence, in linear time. +// If `sequence` is an array or string or arguments +// returns in constant time. +// Implements clojure last: +// http://clojuredocs.org/clojure_core/clojure.core/last +const last = polymorphic({ + null: _ => null, + void: _ => null, + indexed: indexed => indexed[indexed.length - 1], + map: xs => reduce((_, x) => x, xs), + set: xs => reduce((_, x) => x, xs), + default: xs => reduce((_, x) => x, xs) +}); +exports.last = last; + +// Return a lazy sequence of all but the last `n` (default 1) items +// from the give `xs`. +// +// Implements clojure drop-last: +// http://clojuredocs.org/clojure_core/clojure.core/drop-last +const dropLast = flip((xs, n=1) => seq(function* () { + let ys = []; + for (let x of xs) { + ys.push(x); + if (ys.length > n) + yield ys.shift(); + } +})); +exports.dropLast = dropLast; + +// Returns a lazy sequence of the elements of `xs` with duplicates +// removed +// +// Implements clojure distinct +// http://clojuredocs.org/clojure_core/clojure.core/distinct +const distinct = sequence => seq(function* () { + let items = new Set(); + for (let item of sequence) { + if (!items.has(item)) { + items.add(item); + yield item; + } + } +}); +exports.distinct = distinct; + +// Returns a lazy sequence of the items in `xs` for which +// `p(x)` returns false. `p` must be free of side-effects. +// +// Implements clojure remove +// http://clojuredocs.org/clojure_core/clojure.core/remove +const remove = (p, xs) => filter(complement(p), xs); +exports.remove = remove; + +// Returns the result of applying concat to the result of +// `map(f, xs)`. Thus function `f` should return a sequence. +// +// Implements clojure mapcat +// http://clojuredocs.org/clojure_core/clojure.core/mapcat +const mapcat = (f, sequence) => seq(function* () { + const sequences = map(f, sequence); + for (let sequence of sequences) + for (let item of sequence) + yield item; +}); +exports.mapcat = mapcat; diff --git a/toolkit/jetpack/sdk/util/uuid.js b/toolkit/jetpack/sdk/util/uuid.js new file mode 100644 index 000000000..6d0f2de53 --- /dev/null +++ b/toolkit/jetpack/sdk/util/uuid.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { Cc, Ci, components: { ID: parseUUID } } = require('chrome'); +const { generateUUID } = Cc['@mozilla.org/uuid-generator;1']. + getService(Ci.nsIUUIDGenerator); + +// Returns `uuid`. If `id` is passed then it's parsed to `uuid` and returned +// if not then new one is generated. +exports.uuid = function uuid(id) { + return id ? parseUUID(id) : generateUUID(); +}; -- cgit v1.2.3