|
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/. */ |
|
4 |
|
5 'use strict'; |
|
6 |
|
7 module.metadata = { |
|
8 "stability": "experimental", |
|
9 "engines": { |
|
10 "Firefox": "*" |
|
11 } |
|
12 }; |
|
13 |
|
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); |
|
23 |
|
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; |
|
49 |
|
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 } |
|
61 |
|
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 } |
|
75 |
|
76 /* |
|
77 * Constructs a tree of bookmark nodes |
|
78 * returning the root (value: null); |
|
79 */ |
|
80 |
|
81 function constructTree (items) { |
|
82 let root = TreeNode(null); |
|
83 items.forEach(treeify.bind(null, root)); |
|
84 |
|
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); |
|
90 |
|
91 let parentNode = item.group ? treeify(root, item.group) : root; |
|
92 parentNode.add(node); |
|
93 |
|
94 return node; |
|
95 } |
|
96 |
|
97 return root; |
|
98 } |
|
99 exports.constructTree = constructTree; |
|
100 |
|
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; |
|
108 |
|
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; |
|
120 |
|
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 */ |
|
134 |
|
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; |
|
154 |
|
155 /* |
|
156 * Takes an EventEmitter and returns a promise that |
|
157 * aggregates results and handles a bulk resolve and reject |
|
158 */ |
|
159 |
|
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; |
|
171 |
|
172 |
|
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 }; |
|
179 |
|
180 urlQueryParser(qObj, query.url); |
|
181 |
|
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; |
|
187 |
|
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) { |
|
199 |
|
200 } |
|
201 |
|
202 return qObj; |
|
203 } |
|
204 exports.createQuery = createQuery; |
|
205 |
|
206 // https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions |
|
207 |
|
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 }; |
|
218 |
|
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++; |
|
225 |
|
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; |
|
230 |
|
231 oObj.maxResults = typeof options.count === 'number' ? options.count : 0; |
|
232 |
|
233 oObj.queryType = type; |
|
234 |
|
235 return oObj; |
|
236 } |
|
237 exports.createQueryOptions = createQueryOptions; |
|
238 |
|
239 |
|
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; |