michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: 'use strict'; michael@0: michael@0: module.metadata = { michael@0: "stability": "experimental", michael@0: "engines": { michael@0: "Firefox": "*" michael@0: } michael@0: }; michael@0: michael@0: const { Cc, Ci } = require('chrome'); michael@0: const { Class } = require('../core/heritage'); michael@0: const { method } = require('../lang/functional'); michael@0: const { defer, promised, all } = require('../core/promise'); michael@0: const { send } = require('../addon/events'); michael@0: const { EventTarget } = require('../event/target'); michael@0: const { merge } = require('../util/object'); michael@0: const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. michael@0: getService(Ci.nsINavBookmarksService); michael@0: michael@0: /* michael@0: * TreeNodes are used to construct dependency trees michael@0: * for BookmarkItems michael@0: */ michael@0: let TreeNode = Class({ michael@0: initialize: function (value) { michael@0: this.value = value; michael@0: this.children = []; michael@0: }, michael@0: add: function (values) { michael@0: [].concat(values).forEach(value => { michael@0: this.children.push(value instanceof TreeNode ? value : TreeNode(value)); michael@0: }); michael@0: }, michael@0: get length () { michael@0: let count = 0; michael@0: this.walk(() => count++); michael@0: // Do not count the current node michael@0: return --count; michael@0: }, michael@0: get: method(get), michael@0: walk: method(walk), michael@0: toString: function () '[object TreeNode]' michael@0: }); michael@0: exports.TreeNode = TreeNode; michael@0: michael@0: /* michael@0: * Descends down from `node` applying `fn` to each in order. michael@0: * `fn` can return values or promises -- if promise returned, michael@0: * children are not processed until resolved. `fn` is passed michael@0: * one argument, the current node, `curr`. michael@0: */ michael@0: function walk (curr, fn) { michael@0: return promised(fn)(curr).then(val => { michael@0: return all(curr.children.map(child => walk(child, fn))); michael@0: }); michael@0: } michael@0: michael@0: /* michael@0: * Descends from the TreeNode `node`, returning michael@0: * the node with value `value` if found or `null` michael@0: * otherwise michael@0: */ michael@0: function get (node, value) { michael@0: if (node.value === value) return node; michael@0: for (let child of node.children) { michael@0: let found = get(child, value); michael@0: if (found) return found; michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: /* michael@0: * Constructs a tree of bookmark nodes michael@0: * returning the root (value: null); michael@0: */ michael@0: michael@0: function constructTree (items) { michael@0: let root = TreeNode(null); michael@0: items.forEach(treeify.bind(null, root)); michael@0: michael@0: function treeify (root, item) { michael@0: // If node already exists, skip michael@0: let node = root.get(item); michael@0: if (node) return node; michael@0: node = TreeNode(item); michael@0: michael@0: let parentNode = item.group ? treeify(root, item.group) : root; michael@0: parentNode.add(node); michael@0: michael@0: return node; michael@0: } michael@0: michael@0: return root; michael@0: } michael@0: exports.constructTree = constructTree; michael@0: michael@0: /* michael@0: * Shortcut for converting an id, or an object with an id, into michael@0: * an object with corresponding bookmark data michael@0: */ michael@0: function fetchItem (item) michael@0: send('sdk-places-bookmarks-get', { id: item.id || item }) michael@0: exports.fetchItem = fetchItem; michael@0: michael@0: /* michael@0: * Takes an ID or an object with ID and checks it against michael@0: * the root bookmark folders michael@0: */ michael@0: function isRootGroup (id) { michael@0: id = id && id.id; michael@0: return ~[bmsrv.bookmarksMenuFolder, bmsrv.toolbarFolder, michael@0: bmsrv.unfiledBookmarksFolder michael@0: ].indexOf(id); michael@0: } michael@0: exports.isRootGroup = isRootGroup; michael@0: michael@0: /* michael@0: * Merges appropriate options into query based off of url michael@0: * 4 scenarios: michael@0: * michael@0: * 'moz.com' // domain: moz.com, domainIsHost: true michael@0: * --> 'http://moz.com', 'http://moz.com/thunderbird' michael@0: * '*.moz.com' // domain: moz.com, domainIsHost: false michael@0: * --> 'http://moz.com', 'http://moz.com/index', 'http://ff.moz.com/test' michael@0: * 'http://moz.com' // url: http://moz.com/, urlIsPrefix: false michael@0: * --> 'http://moz.com/' michael@0: * 'http://moz.com/*' // url: http://moz.com/, urlIsPrefix: true michael@0: * --> 'http://moz.com/', 'http://moz.com/thunderbird' michael@0: */ michael@0: michael@0: function urlQueryParser (query, url) { michael@0: if (!url) return; michael@0: if (/^https?:\/\//.test(url)) { michael@0: query.uri = url.charAt(url.length - 1) === '/' ? url : url + '/'; michael@0: if (/\*$/.test(url)) { michael@0: query.uri = url.replace(/\*$/, ''); michael@0: query.uriIsPrefix = true; michael@0: } michael@0: } else { michael@0: if (/^\*/.test(url)) { michael@0: query.domain = url.replace(/^\*\./, ''); michael@0: query.domainIsHost = false; michael@0: } else { michael@0: query.domain = url; michael@0: query.domainIsHost = true; michael@0: } michael@0: } michael@0: } michael@0: exports.urlQueryParser = urlQueryParser; michael@0: michael@0: /* michael@0: * Takes an EventEmitter and returns a promise that michael@0: * aggregates results and handles a bulk resolve and reject michael@0: */ michael@0: michael@0: function promisedEmitter (emitter) { michael@0: let { promise, resolve, reject } = defer(); michael@0: let errors = []; michael@0: emitter.on('error', error => errors.push(error)); michael@0: emitter.on('end', (items) => { michael@0: if (errors.length) reject(errors[0]); michael@0: else resolve(items); michael@0: }); michael@0: return promise; michael@0: } michael@0: exports.promisedEmitter = promisedEmitter; michael@0: michael@0: michael@0: // https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions michael@0: function createQuery (type, query) { michael@0: query = query || {}; michael@0: let qObj = { michael@0: searchTerms: query.query michael@0: }; michael@0: michael@0: urlQueryParser(qObj, query.url); michael@0: michael@0: // 0 === history michael@0: if (type === 0) { michael@0: // PRTime used by query is in microseconds, not milliseconds michael@0: qObj.beginTime = (query.from || 0) * 1000; michael@0: qObj.endTime = (query.to || new Date()) * 1000; michael@0: michael@0: // Set reference time to Epoch michael@0: qObj.beginTimeReference = 0; michael@0: qObj.endTimeReference = 0; michael@0: } michael@0: // 1 === bookmarks michael@0: else if (type === 1) { michael@0: qObj.tags = query.tags; michael@0: qObj.folder = query.group && query.group.id; michael@0: } michael@0: // 2 === unified (not implemented on platform) michael@0: else if (type === 2) { michael@0: michael@0: } michael@0: michael@0: return qObj; michael@0: } michael@0: exports.createQuery = createQuery; michael@0: michael@0: // https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions michael@0: michael@0: const SORT_MAP = { michael@0: title: 1, michael@0: date: 3, // sort by visit date michael@0: url: 5, michael@0: visitCount: 7, michael@0: // keywords currently unsupported michael@0: // keyword: 9, michael@0: dateAdded: 11, // bookmarks only michael@0: lastModified: 13 // bookmarks only michael@0: }; michael@0: michael@0: function createQueryOptions (type, options) { michael@0: options = options || {}; michael@0: let oObj = {}; michael@0: oObj.sortingMode = SORT_MAP[options.sort] || 0; michael@0: if (options.descending && options.sort) michael@0: oObj.sortingMode++; michael@0: michael@0: // Resolve to default sort if ineligible based on query type michael@0: if (type === 0 && // history michael@0: (options.sort === 'dateAdded' || options.sort === 'lastModified')) michael@0: oObj.sortingMode = 0; michael@0: michael@0: oObj.maxResults = typeof options.count === 'number' ? options.count : 0; michael@0: michael@0: oObj.queryType = type; michael@0: michael@0: return oObj; michael@0: } michael@0: exports.createQueryOptions = createQueryOptions; michael@0: michael@0: michael@0: function mapBookmarkItemType (type) { michael@0: if (typeof type === 'number') { michael@0: if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark'; michael@0: if (bmsrv.TYPE_FOLDER === type) return 'group'; michael@0: if (bmsrv.TYPE_SEPARATOR === type) return 'separator'; michael@0: } else { michael@0: if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK; michael@0: if ('group' === type) return bmsrv.TYPE_FOLDER; michael@0: if ('separator' === type) return bmsrv.TYPE_SEPARATOR; michael@0: } michael@0: } michael@0: exports.mapBookmarkItemType = mapBookmarkItemType;