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