addon-sdk/source/lib/sdk/places/utils.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/addon-sdk/source/lib/sdk/places/utils.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,251 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +'use strict';
     1.9 +
    1.10 +module.metadata = {
    1.11 +  "stability": "experimental",
    1.12 +  "engines": {
    1.13 +    "Firefox": "*"
    1.14 +  }
    1.15 +};
    1.16 +
    1.17 +const { Cc, Ci } = require('chrome');
    1.18 +const { Class } = require('../core/heritage');
    1.19 +const { method } = require('../lang/functional');
    1.20 +const { defer, promised, all } = require('../core/promise');
    1.21 +const { send } = require('../addon/events');
    1.22 +const { EventTarget } = require('../event/target');
    1.23 +const { merge } = require('../util/object');
    1.24 +const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
    1.25 +                getService(Ci.nsINavBookmarksService);
    1.26 +
    1.27 +/*
    1.28 + * TreeNodes are used to construct dependency trees
    1.29 + * for BookmarkItems
    1.30 + */
    1.31 +let TreeNode = Class({
    1.32 +  initialize: function (value) {
    1.33 +    this.value = value;
    1.34 +    this.children = [];
    1.35 +  },
    1.36 +  add: function (values) {
    1.37 +    [].concat(values).forEach(value => {
    1.38 +      this.children.push(value instanceof TreeNode ? value : TreeNode(value));
    1.39 +    });
    1.40 +  },
    1.41 +  get length () {
    1.42 +    let count = 0;
    1.43 +    this.walk(() => count++);
    1.44 +    // Do not count the current node
    1.45 +    return --count;
    1.46 +  },
    1.47 +  get: method(get),
    1.48 +  walk: method(walk),
    1.49 +  toString: function () '[object TreeNode]'
    1.50 +});
    1.51 +exports.TreeNode = TreeNode;
    1.52 +
    1.53 +/*
    1.54 + * Descends down from `node` applying `fn` to each in order.
    1.55 + * `fn` can return values or promises -- if promise returned,
    1.56 + * children are not processed until resolved. `fn` is passed 
    1.57 + * one argument, the current node, `curr`.
    1.58 + */
    1.59 +function walk (curr, fn) {
    1.60 +  return promised(fn)(curr).then(val => {
    1.61 +    return all(curr.children.map(child => walk(child, fn)));
    1.62 +  });
    1.63 +} 
    1.64 +
    1.65 +/*
    1.66 + * Descends from the TreeNode `node`, returning
    1.67 + * the node with value `value` if found or `null`
    1.68 + * otherwise
    1.69 + */
    1.70 +function get (node, value) {
    1.71 +  if (node.value === value) return node;
    1.72 +  for (let child of node.children) {
    1.73 +    let found = get(child, value);
    1.74 +    if (found) return found;
    1.75 +  }
    1.76 +  return null;
    1.77 +}
    1.78 +
    1.79 +/*
    1.80 + * Constructs a tree of bookmark nodes
    1.81 + * returning the root (value: null);
    1.82 + */
    1.83 +
    1.84 +function constructTree (items) {
    1.85 +  let root = TreeNode(null);
    1.86 +  items.forEach(treeify.bind(null, root));
    1.87 +
    1.88 +  function treeify (root, item) {
    1.89 +    // If node already exists, skip
    1.90 +    let node = root.get(item);
    1.91 +    if (node) return node;
    1.92 +    node = TreeNode(item);
    1.93 +
    1.94 +    let parentNode = item.group ? treeify(root, item.group) : root;
    1.95 +    parentNode.add(node);
    1.96 +
    1.97 +    return node;
    1.98 +  }
    1.99 +
   1.100 +  return root;
   1.101 +}
   1.102 +exports.constructTree = constructTree;
   1.103 +
   1.104 +/*
   1.105 + * Shortcut for converting an id, or an object with an id, into
   1.106 + * an object with corresponding bookmark data
   1.107 + */
   1.108 +function fetchItem (item)
   1.109 +  send('sdk-places-bookmarks-get', { id: item.id || item })
   1.110 +exports.fetchItem = fetchItem;
   1.111 +
   1.112 +/*
   1.113 + * Takes an ID or an object with ID and checks it against
   1.114 + * the root bookmark folders
   1.115 + */
   1.116 +function isRootGroup (id) {
   1.117 +  id = id && id.id;
   1.118 +  return ~[bmsrv.bookmarksMenuFolder, bmsrv.toolbarFolder,
   1.119 +    bmsrv.unfiledBookmarksFolder
   1.120 +  ].indexOf(id);
   1.121 +}
   1.122 +exports.isRootGroup = isRootGroup;
   1.123 +
   1.124 +/*
   1.125 + * Merges appropriate options into query based off of url
   1.126 + * 4 scenarios:
   1.127 + * 
   1.128 + * 'moz.com' // domain: moz.com, domainIsHost: true
   1.129 + *    --> 'http://moz.com', 'http://moz.com/thunderbird'
   1.130 + * '*.moz.com' // domain: moz.com, domainIsHost: false
   1.131 + *    --> 'http://moz.com', 'http://moz.com/index', 'http://ff.moz.com/test'
   1.132 + * 'http://moz.com' // url: http://moz.com/, urlIsPrefix: false
   1.133 + *    --> 'http://moz.com/'
   1.134 + * 'http://moz.com/*' // url: http://moz.com/, urlIsPrefix: true
   1.135 + *    --> 'http://moz.com/', 'http://moz.com/thunderbird'
   1.136 + */
   1.137 +
   1.138 +function urlQueryParser (query, url) {
   1.139 +  if (!url) return;
   1.140 +  if (/^https?:\/\//.test(url)) {
   1.141 +    query.uri = url.charAt(url.length - 1) === '/' ? url : url + '/';
   1.142 +    if (/\*$/.test(url)) {
   1.143 +      query.uri = url.replace(/\*$/, '');
   1.144 +      query.uriIsPrefix = true;
   1.145 +    }
   1.146 +  } else {
   1.147 +    if (/^\*/.test(url)) {
   1.148 +      query.domain = url.replace(/^\*\./, '');
   1.149 +      query.domainIsHost = false;
   1.150 +    } else {
   1.151 +      query.domain = url;
   1.152 +      query.domainIsHost = true;
   1.153 +    }
   1.154 +  }
   1.155 +}
   1.156 +exports.urlQueryParser = urlQueryParser;
   1.157 +
   1.158 +/*
   1.159 + * Takes an EventEmitter and returns a promise that
   1.160 + * aggregates results and handles a bulk resolve and reject
   1.161 + */
   1.162 +
   1.163 +function promisedEmitter (emitter) {
   1.164 +  let { promise, resolve, reject } = defer();
   1.165 +  let errors = [];
   1.166 +  emitter.on('error', error => errors.push(error));
   1.167 +  emitter.on('end', (items) => {
   1.168 +    if (errors.length) reject(errors[0]);
   1.169 +    else resolve(items);
   1.170 +  });
   1.171 +  return promise;
   1.172 +}
   1.173 +exports.promisedEmitter = promisedEmitter;
   1.174 +
   1.175 +
   1.176 +// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
   1.177 +function createQuery (type, query) {
   1.178 +  query = query || {};
   1.179 +  let qObj = {
   1.180 +    searchTerms: query.query
   1.181 +  };
   1.182 +     
   1.183 +  urlQueryParser(qObj, query.url);
   1.184 +  
   1.185 +  // 0 === history
   1.186 +  if (type === 0) {
   1.187 +    // PRTime used by query is in microseconds, not milliseconds
   1.188 +    qObj.beginTime = (query.from || 0) * 1000;
   1.189 +    qObj.endTime = (query.to || new Date()) * 1000;
   1.190 +
   1.191 +    // Set reference time to Epoch
   1.192 +    qObj.beginTimeReference = 0;
   1.193 +    qObj.endTimeReference = 0;
   1.194 +  }
   1.195 +  // 1 === bookmarks
   1.196 +  else if (type === 1) {
   1.197 +    qObj.tags = query.tags;
   1.198 +    qObj.folder = query.group && query.group.id;
   1.199 +  } 
   1.200 +  // 2 === unified (not implemented on platform)
   1.201 +  else if (type === 2) {
   1.202 +
   1.203 +  }
   1.204 +
   1.205 +  return qObj;
   1.206 +}
   1.207 +exports.createQuery = createQuery;
   1.208 +
   1.209 +// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
   1.210 +
   1.211 +const SORT_MAP = {
   1.212 +  title: 1,
   1.213 +  date: 3, // sort by visit date
   1.214 +  url: 5,
   1.215 +  visitCount: 7,
   1.216 +  // keywords currently unsupported
   1.217 +  // keyword: 9,
   1.218 +  dateAdded: 11, // bookmarks only
   1.219 +  lastModified: 13 // bookmarks only
   1.220 +};
   1.221 +
   1.222 +function createQueryOptions (type, options) {
   1.223 +  options = options || {};
   1.224 +  let oObj = {};
   1.225 +  oObj.sortingMode = SORT_MAP[options.sort] || 0;
   1.226 +  if (options.descending && options.sort)
   1.227 +    oObj.sortingMode++;
   1.228 +
   1.229 +  // Resolve to default sort if ineligible based on query type
   1.230 +  if (type === 0 && // history
   1.231 +      (options.sort === 'dateAdded' || options.sort === 'lastModified'))
   1.232 +    oObj.sortingMode = 0;
   1.233 +
   1.234 +  oObj.maxResults = typeof options.count === 'number' ? options.count : 0;
   1.235 +
   1.236 +  oObj.queryType = type;
   1.237 +
   1.238 +  return oObj;
   1.239 +}
   1.240 +exports.createQueryOptions = createQueryOptions;
   1.241 +
   1.242 +
   1.243 +function mapBookmarkItemType (type) {
   1.244 +  if (typeof type === 'number') {
   1.245 +    if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark';
   1.246 +    if (bmsrv.TYPE_FOLDER === type) return 'group';
   1.247 +    if (bmsrv.TYPE_SEPARATOR === type) return 'separator';
   1.248 +  } else {
   1.249 +    if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK;
   1.250 +    if ('group' === type) return bmsrv.TYPE_FOLDER;
   1.251 +    if ('separator' === type) return bmsrv.TYPE_SEPARATOR;
   1.252 +  }
   1.253 +}
   1.254 +exports.mapBookmarkItemType = mapBookmarkItemType;

mercurial