Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 'use strict';
7 module.metadata = {
8 "stability": "experimental",
9 "engines": {
10 "Firefox": "*"
11 }
12 };
14 const { Cc, Ci } = require('chrome');
15 const { Class } = require('../core/heritage');
16 const { method } = require('../lang/functional');
17 const { defer, promised, all } = require('../core/promise');
18 const { send } = require('../addon/events');
19 const { EventTarget } = require('../event/target');
20 const { merge } = require('../util/object');
21 const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
22 getService(Ci.nsINavBookmarksService);
24 /*
25 * TreeNodes are used to construct dependency trees
26 * for BookmarkItems
27 */
28 let TreeNode = Class({
29 initialize: function (value) {
30 this.value = value;
31 this.children = [];
32 },
33 add: function (values) {
34 [].concat(values).forEach(value => {
35 this.children.push(value instanceof TreeNode ? value : TreeNode(value));
36 });
37 },
38 get length () {
39 let count = 0;
40 this.walk(() => count++);
41 // Do not count the current node
42 return --count;
43 },
44 get: method(get),
45 walk: method(walk),
46 toString: function () '[object TreeNode]'
47 });
48 exports.TreeNode = TreeNode;
50 /*
51 * Descends down from `node` applying `fn` to each in order.
52 * `fn` can return values or promises -- if promise returned,
53 * children are not processed until resolved. `fn` is passed
54 * one argument, the current node, `curr`.
55 */
56 function walk (curr, fn) {
57 return promised(fn)(curr).then(val => {
58 return all(curr.children.map(child => walk(child, fn)));
59 });
60 }
62 /*
63 * Descends from the TreeNode `node`, returning
64 * the node with value `value` if found or `null`
65 * otherwise
66 */
67 function get (node, value) {
68 if (node.value === value) return node;
69 for (let child of node.children) {
70 let found = get(child, value);
71 if (found) return found;
72 }
73 return null;
74 }
76 /*
77 * Constructs a tree of bookmark nodes
78 * returning the root (value: null);
79 */
81 function constructTree (items) {
82 let root = TreeNode(null);
83 items.forEach(treeify.bind(null, root));
85 function treeify (root, item) {
86 // If node already exists, skip
87 let node = root.get(item);
88 if (node) return node;
89 node = TreeNode(item);
91 let parentNode = item.group ? treeify(root, item.group) : root;
92 parentNode.add(node);
94 return node;
95 }
97 return root;
98 }
99 exports.constructTree = constructTree;
101 /*
102 * Shortcut for converting an id, or an object with an id, into
103 * an object with corresponding bookmark data
104 */
105 function fetchItem (item)
106 send('sdk-places-bookmarks-get', { id: item.id || item })
107 exports.fetchItem = fetchItem;
109 /*
110 * Takes an ID or an object with ID and checks it against
111 * the root bookmark folders
112 */
113 function isRootGroup (id) {
114 id = id && id.id;
115 return ~[bmsrv.bookmarksMenuFolder, bmsrv.toolbarFolder,
116 bmsrv.unfiledBookmarksFolder
117 ].indexOf(id);
118 }
119 exports.isRootGroup = isRootGroup;
121 /*
122 * Merges appropriate options into query based off of url
123 * 4 scenarios:
124 *
125 * 'moz.com' // domain: moz.com, domainIsHost: true
126 * --> 'http://moz.com', 'http://moz.com/thunderbird'
127 * '*.moz.com' // domain: moz.com, domainIsHost: false
128 * --> 'http://moz.com', 'http://moz.com/index', 'http://ff.moz.com/test'
129 * 'http://moz.com' // url: http://moz.com/, urlIsPrefix: false
130 * --> 'http://moz.com/'
131 * 'http://moz.com/*' // url: http://moz.com/, urlIsPrefix: true
132 * --> 'http://moz.com/', 'http://moz.com/thunderbird'
133 */
135 function urlQueryParser (query, url) {
136 if (!url) return;
137 if (/^https?:\/\//.test(url)) {
138 query.uri = url.charAt(url.length - 1) === '/' ? url : url + '/';
139 if (/\*$/.test(url)) {
140 query.uri = url.replace(/\*$/, '');
141 query.uriIsPrefix = true;
142 }
143 } else {
144 if (/^\*/.test(url)) {
145 query.domain = url.replace(/^\*\./, '');
146 query.domainIsHost = false;
147 } else {
148 query.domain = url;
149 query.domainIsHost = true;
150 }
151 }
152 }
153 exports.urlQueryParser = urlQueryParser;
155 /*
156 * Takes an EventEmitter and returns a promise that
157 * aggregates results and handles a bulk resolve and reject
158 */
160 function promisedEmitter (emitter) {
161 let { promise, resolve, reject } = defer();
162 let errors = [];
163 emitter.on('error', error => errors.push(error));
164 emitter.on('end', (items) => {
165 if (errors.length) reject(errors[0]);
166 else resolve(items);
167 });
168 return promise;
169 }
170 exports.promisedEmitter = promisedEmitter;
173 // https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
174 function createQuery (type, query) {
175 query = query || {};
176 let qObj = {
177 searchTerms: query.query
178 };
180 urlQueryParser(qObj, query.url);
182 // 0 === history
183 if (type === 0) {
184 // PRTime used by query is in microseconds, not milliseconds
185 qObj.beginTime = (query.from || 0) * 1000;
186 qObj.endTime = (query.to || new Date()) * 1000;
188 // Set reference time to Epoch
189 qObj.beginTimeReference = 0;
190 qObj.endTimeReference = 0;
191 }
192 // 1 === bookmarks
193 else if (type === 1) {
194 qObj.tags = query.tags;
195 qObj.folder = query.group && query.group.id;
196 }
197 // 2 === unified (not implemented on platform)
198 else if (type === 2) {
200 }
202 return qObj;
203 }
204 exports.createQuery = createQuery;
206 // https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
208 const SORT_MAP = {
209 title: 1,
210 date: 3, // sort by visit date
211 url: 5,
212 visitCount: 7,
213 // keywords currently unsupported
214 // keyword: 9,
215 dateAdded: 11, // bookmarks only
216 lastModified: 13 // bookmarks only
217 };
219 function createQueryOptions (type, options) {
220 options = options || {};
221 let oObj = {};
222 oObj.sortingMode = SORT_MAP[options.sort] || 0;
223 if (options.descending && options.sort)
224 oObj.sortingMode++;
226 // Resolve to default sort if ineligible based on query type
227 if (type === 0 && // history
228 (options.sort === 'dateAdded' || options.sort === 'lastModified'))
229 oObj.sortingMode = 0;
231 oObj.maxResults = typeof options.count === 'number' ? options.count : 0;
233 oObj.queryType = type;
235 return oObj;
236 }
237 exports.createQueryOptions = createQueryOptions;
240 function mapBookmarkItemType (type) {
241 if (typeof type === 'number') {
242 if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark';
243 if (bmsrv.TYPE_FOLDER === type) return 'group';
244 if (bmsrv.TYPE_SEPARATOR === type) return 'separator';
245 } else {
246 if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK;
247 if ('group' === type) return bmsrv.TYPE_FOLDER;
248 if ('separator' === type) return bmsrv.TYPE_SEPARATOR;
249 }
250 }
251 exports.mapBookmarkItemType = mapBookmarkItemType;