diff options
Diffstat (limited to 'toolkit/jetpack/sdk/places/utils.js')
-rw-r--r-- | toolkit/jetpack/sdk/places/utils.js | 268 |
1 files changed, 268 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/places/utils.js b/toolkit/jetpack/sdk/places/utils.js new file mode 100644 index 000000000..44366d2aa --- /dev/null +++ b/toolkit/jetpack/sdk/places/utils.js @@ -0,0 +1,268 @@ +/* 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", + "engines": { + "Firefox": "*", + "SeaMonkey": "*" + } +}; + +const { Cc, Ci, Cu } = require('chrome'); +const { Class } = require('../core/heritage'); +const { method } = require('../lang/functional'); +const { defer, promised, all } = require('../core/promise'); +const { send } = require('../addon/events'); +const { EventTarget } = require('../event/target'); +const { merge } = require('../util/object'); +const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + +Cu.importGlobalProperties(["URL"]); + +/* + * TreeNodes are used to construct dependency trees + * for BookmarkItems + */ +var TreeNode = Class({ + initialize: function (value) { + this.value = value; + this.children = []; + }, + add: function (values) { + [].concat(values).forEach(value => { + this.children.push(value instanceof TreeNode ? value : TreeNode(value)); + }); + }, + get length () { + let count = 0; + this.walk(() => count++); + // Do not count the current node + return --count; + }, + get: method(get), + walk: method(walk), + toString: () => '[object TreeNode]' +}); +exports.TreeNode = TreeNode; + +/* + * Descends down from `node` applying `fn` to each in order. + * `fn` can return values or promises -- if promise returned, + * children are not processed until resolved. `fn` is passed + * one argument, the current node, `curr`. + */ +function walk (curr, fn) { + return promised(fn)(curr).then(val => { + return all(curr.children.map(child => walk(child, fn))); + }); +} + +/* + * Descends from the TreeNode `node`, returning + * the node with value `value` if found or `null` + * otherwise + */ +function get (node, value) { + if (node.value === value) return node; + for (let child of node.children) { + let found = get(child, value); + if (found) return found; + } + return null; +} + +/* + * Constructs a tree of bookmark nodes + * returning the root (value: null); + */ + +function constructTree (items) { + let root = TreeNode(null); + items.forEach(treeify.bind(null, root)); + + function treeify (root, item) { + // If node already exists, skip + let node = root.get(item); + if (node) return node; + node = TreeNode(item); + + let parentNode = item.group ? treeify(root, item.group) : root; + parentNode.add(node); + + return node; + } + + return root; +} +exports.constructTree = constructTree; + +/* + * Shortcut for converting an id, or an object with an id, into + * an object with corresponding bookmark data + */ +function fetchItem (item) { + return send('sdk-places-bookmarks-get', { id: item.id || item }); +} +exports.fetchItem = fetchItem; + +/* + * Takes an ID or an object with ID and checks it against + * the root bookmark folders + */ +function isRootGroup (id) { + id = id && id.id; + return ~[bmsrv.bookmarksMenuFolder, bmsrv.toolbarFolder, + bmsrv.unfiledBookmarksFolder + ].indexOf(id); +} +exports.isRootGroup = isRootGroup; + +/* + * Merges appropriate options into query based off of url + * 4 scenarios: + * + * 'moz.com' // domain: moz.com, domainIsHost: true + * --> 'http://moz.com', 'http://moz.com/thunderbird' + * '*.moz.com' // domain: moz.com, domainIsHost: false + * --> 'http://moz.com', 'http://moz.com/index', 'http://ff.moz.com/test' + * 'http://moz.com' // uri: http://moz.com/ + * --> 'http://moz.com/' + * 'http://moz.com/*' // uri: http://moz.com/, domain: moz.com, domainIsHost: true + * --> 'http://moz.com/', 'http://moz.com/thunderbird' + */ + +function urlQueryParser (query, url) { + if (!url) return; + if (/^https?:\/\//.test(url)) { + query.uri = url.charAt(url.length - 1) === '/' ? url : url + '/'; + if (/\*$/.test(url)) { + // Wildcard searches on URIs are not supported, so try to extract a + // domain and filter the data later. + url = url.replace(/\*$/, ''); + try { + query.domain = new URL(url).hostname; + query.domainIsHost = true; + // Unfortunately here we cannot use an expando to store the wildcard, + // cause the query is a wrapped native XPCOM object, so we reuse uri. + // We clearly don't want to query for both uri and domain, thus we'll + // have to handle this in host-query.js::execute() + query.uri = url; + } catch (ex) { + // Cannot extract an host cause it's not a valid uri, the query will + // just return nothing. + } + } + } else { + if (/^\*/.test(url)) { + query.domain = url.replace(/^\*\./, ''); + query.domainIsHost = false; + } else { + query.domain = url; + query.domainIsHost = true; + } + } +} +exports.urlQueryParser = urlQueryParser; + +/* + * Takes an EventEmitter and returns a promise that + * aggregates results and handles a bulk resolve and reject + */ + +function promisedEmitter (emitter) { + let { promise, resolve, reject } = defer(); + let errors = []; + emitter.on('error', error => errors.push(error)); + emitter.on('end', (items) => { + if (errors.length) reject(errors[0]); + else resolve(items); + }); + return promise; +} +exports.promisedEmitter = promisedEmitter; + + +// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions +function createQuery (type, query) { + query = query || {}; + let qObj = { + searchTerms: query.query + }; + + urlQueryParser(qObj, query.url); + + // 0 === history + if (type === 0) { + // PRTime used by query is in microseconds, not milliseconds + qObj.beginTime = (query.from || 0) * 1000; + qObj.endTime = (query.to || new Date()) * 1000; + + // Set reference time to Epoch + qObj.beginTimeReference = 0; + qObj.endTimeReference = 0; + } + // 1 === bookmarks + else if (type === 1) { + qObj.tags = query.tags; + qObj.folder = query.group && query.group.id; + } + // 2 === unified (not implemented on platform) + else if (type === 2) { + + } + + return qObj; +} +exports.createQuery = createQuery; + +// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions + +const SORT_MAP = { + title: 1, + date: 3, // sort by visit date + url: 5, + visitCount: 7, + // keywords currently unsupported + // keyword: 9, + dateAdded: 11, // bookmarks only + lastModified: 13 // bookmarks only +}; + +function createQueryOptions (type, options) { + options = options || {}; + let oObj = {}; + oObj.sortingMode = SORT_MAP[options.sort] || 0; + if (options.descending && options.sort) + oObj.sortingMode++; + + // Resolve to default sort if ineligible based on query type + if (type === 0 && // history + (options.sort === 'dateAdded' || options.sort === 'lastModified')) + oObj.sortingMode = 0; + + oObj.maxResults = typeof options.count === 'number' ? options.count : 0; + + oObj.queryType = type; + + return oObj; +} +exports.createQueryOptions = createQueryOptions; + + +function mapBookmarkItemType (type) { + if (typeof type === 'number') { + if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark'; + if (bmsrv.TYPE_FOLDER === type) return 'group'; + if (bmsrv.TYPE_SEPARATOR === type) return 'separator'; + } else { + if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK; + if ('group' === type) return bmsrv.TYPE_FOLDER; + if ('separator' === type) return bmsrv.TYPE_SEPARATOR; + } +} +exports.mapBookmarkItemType = mapBookmarkItemType; |