зеркало из https://github.com/mozilla/gecko-dev.git
Bug 583890 - Tab Title Abridger module and tests. r=ttaubert
This commit is contained in:
Родитель
d39f96fcbf
Коммит
3b8ae4eb13
|
@ -389,6 +389,7 @@ pref("browser.tabs.tabClipWidth", 140);
|
|||
pref("browser.tabs.animate", true);
|
||||
pref("browser.tabs.onTop", true);
|
||||
pref("browser.tabs.drawInTitlebar", true);
|
||||
pref("browser.tabs.cropTitleRedundancy", true);
|
||||
|
||||
// Where to show tab close buttons:
|
||||
// 0 on active tab only
|
||||
|
|
|
@ -157,6 +157,12 @@ XPCOMUtils.defineLazyGetter(this, "gBrowserNewTabPreloader", function () {
|
|||
return new tmp.BrowserNewTabPreloader();
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "TabTitleAbridger", function() {
|
||||
let tmp = {};
|
||||
Cu.import("resource:///modules/TabTitleAbridger.jsm", tmp);
|
||||
return new tmp.TabTitleAbridger(window);
|
||||
});
|
||||
|
||||
let gInitialPages = [
|
||||
"about:blank",
|
||||
"about:newtab",
|
||||
|
@ -1413,6 +1419,7 @@ var gBrowserInit = {
|
|||
|
||||
gBrowserThumbnails.init();
|
||||
TabView.init();
|
||||
TabTitleAbridger.init();
|
||||
|
||||
setUrlAndSearchBarWidthForConditionalForwardButton();
|
||||
window.addEventListener("resize", function resizeHandler(event) {
|
||||
|
@ -1608,6 +1615,7 @@ var gBrowserInit = {
|
|||
TabView.uninit();
|
||||
gBrowserThumbnails.uninit();
|
||||
FullZoom.destroy();
|
||||
TabTitleAbridger.destroy();
|
||||
|
||||
Services.obs.removeObserver(gSessionHistoryObserver, "browser:purge-session-history");
|
||||
Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled");
|
||||
|
|
|
@ -134,6 +134,7 @@ _BROWSER_FILES = \
|
|||
browser_bug581242.js \
|
||||
browser_bug581253.js \
|
||||
browser_bug581947.js \
|
||||
browser_bug583890.js \
|
||||
browser_bug583890_label.js \
|
||||
browser_bug585785.js \
|
||||
browser_bug585830.js \
|
||||
|
|
|
@ -0,0 +1,359 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Call the aCallback when aTab's label is equal to the aExpectedLabel.
|
||||
* Either happens immediately, or after a series of TabAttrModified events.
|
||||
* In the case of failure, this will cause timeout of the test.
|
||||
*
|
||||
* @param aTab the tab whose label is being tested
|
||||
* @param aExpectedLabel the value the tab's label must match
|
||||
* @param aCallback the callback for use upon success
|
||||
*/
|
||||
function waitForTabLabel(aTab, aExpectedLabel, aCallback) {
|
||||
if (aTab.visibleLabel == aExpectedLabel) {
|
||||
executeSoon(aCallback);
|
||||
} else {
|
||||
executeSoon(function () { waitForTabLabel(aTab, aExpectedLabel, aCallback); });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the aCallback after adding aCount tabs.
|
||||
*
|
||||
* @param aCount the number of tabs to add
|
||||
* @param aCallback the callback for use upon success
|
||||
*/
|
||||
function addTabs(aCount, aCallback) {
|
||||
let addedTabs = [];
|
||||
for (let i = aCount; i > 0; i--) {
|
||||
addedTabs.push(gBrowser.addTab());
|
||||
}
|
||||
executeSoon(function () { aCallback(addedTabs); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the aCallback after updating aTab's title and waiting for a label update
|
||||
* In the case of failure, this will cause timeout of the test.
|
||||
*
|
||||
* @param aTab the tab whose title is set
|
||||
* @param aTitle the value to give the tab title
|
||||
* @param aExpectedLabel the value the tab's label must match
|
||||
* @param aCallback the callback for use upon success
|
||||
*/
|
||||
function setTitleForTab(aTab, aTitle, aExpectedLabel, aCallback) {
|
||||
aTab.linkedBrowser.contentDocument.title = aTitle;
|
||||
waitForTabLabel(aTab, aExpectedLabel, aCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the aCallback after updating aTab's label and waiting for a label update
|
||||
* In the case of failure, this will cause timeout of the test.
|
||||
*
|
||||
* @param aTab the tab whose title is set
|
||||
* @param aTitle the value to give the tab title
|
||||
* @param aExpectedLabel the value the tab's label must match
|
||||
* @param aCallback the callback for use upon success
|
||||
*/
|
||||
function setLabelForTab(aTab, aTitle, aExpectedLabel, aCallback) {
|
||||
aTab.label = aTitle;
|
||||
waitForTabLabel(aTab, aExpectedLabel, aCallback);
|
||||
}
|
||||
|
||||
function GroupTest() {
|
||||
this.groupNumber = 0;
|
||||
this.tabs = [];
|
||||
}
|
||||
|
||||
GroupTest.prototype = {
|
||||
groups: [
|
||||
[
|
||||
/*
|
||||
* Test proxying and suffix protection
|
||||
*/
|
||||
[
|
||||
"Foo - Bar - Baz",
|
||||
"Foo - Baz - Baz",
|
||||
"Foo - Baz - Baz",
|
||||
"Foo - Baz - Qux"
|
||||
],
|
||||
[
|
||||
[
|
||||
"Bar - Baz",
|
||||
"Baz - Baz"
|
||||
],
|
||||
[
|
||||
"Bar - Baz",
|
||||
"Baz - Baz",
|
||||
"Baz - Baz"
|
||||
],
|
||||
[
|
||||
"Bar - Baz",
|
||||
"Baz",
|
||||
"Baz",
|
||||
"Qux"
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
/*
|
||||
* Test pathmode
|
||||
*/
|
||||
[
|
||||
"http://example.com/foo.html",
|
||||
"http://example.com/foo/bar.html",
|
||||
"Browse - ftp://example.com/pub/",
|
||||
"Browse - ftp://example.com/pub/src/"
|
||||
],
|
||||
[
|
||||
[
|
||||
"foo.html",
|
||||
"foo/bar.html"
|
||||
],
|
||||
[
|
||||
"foo.html",
|
||||
"foo/bar.html",
|
||||
"Browse - ftp://example.com/pub/"
|
||||
],
|
||||
[
|
||||
"foo.html",
|
||||
"foo/bar.html",
|
||||
"pub/",
|
||||
"src/"
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
/*
|
||||
* Test that we don't leave a lone suffix
|
||||
*/
|
||||
[
|
||||
"'Zilla and the Foxes - Singles - Musical Monkey",
|
||||
"'Zilla and the Foxes - Biography - Musical Monkey",
|
||||
"'Zilla and the Foxes - Musical Monkey",
|
||||
"'Zilla and the Foxes - Interviews - Musical Monkey"
|
||||
],
|
||||
[
|
||||
[
|
||||
"Singles - Musical Monkey",
|
||||
"Biography - Musical Monkey"
|
||||
],
|
||||
[
|
||||
"Singles - Musical Monkey",
|
||||
"Biography - Musical Monkey",
|
||||
"'Zilla and the Foxes - Musical Monkey"
|
||||
],
|
||||
[
|
||||
"Singles - Musical Monkey",
|
||||
"Biography - Musical Monkey",
|
||||
"'Zilla and the Foxes - Musical Monkey",
|
||||
"Interviews - Musical Monkey"
|
||||
]
|
||||
]
|
||||
],
|
||||
/*
|
||||
* Test short endings for MIN_CHOP
|
||||
*/
|
||||
[
|
||||
[
|
||||
"Foo - Bar - 0",
|
||||
"Foo - Bar - 0 - extra - 0",
|
||||
"Foo - Bar - 1",
|
||||
"Foo - Bar - 2 - extra",
|
||||
"Foo - Bar - 3"
|
||||
],
|
||||
[
|
||||
[
|
||||
"Bar - 0",
|
||||
"0 - extra - 0"
|
||||
],
|
||||
[
|
||||
"Bar - 0",
|
||||
"0 - extra - 0",
|
||||
"Bar - 1"
|
||||
],
|
||||
[
|
||||
"Bar - 0",
|
||||
"0 - extra - 0",
|
||||
"Bar - 1",
|
||||
"2 - extra"
|
||||
],
|
||||
[
|
||||
"Bar - 0",
|
||||
"0 - extra - 0",
|
||||
"Bar - 1",
|
||||
"2 - extra",
|
||||
"Bar - 3"
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
/*
|
||||
* Test multiple whitespace
|
||||
*/
|
||||
[
|
||||
"Foo - Bar - Baz",
|
||||
"Foo - Bar - Baz",
|
||||
"Foo - Bar - Baz",
|
||||
"Foo - Baz - Baz"
|
||||
],
|
||||
[
|
||||
[
|
||||
"Foo - Bar - Baz",
|
||||
"Foo - Bar - Baz"
|
||||
],
|
||||
[
|
||||
"Foo - Bar - Baz",
|
||||
"Foo - Bar - Baz",
|
||||
"Foo - Bar - Baz"
|
||||
],
|
||||
[
|
||||
"Bar - Baz",
|
||||
"Bar - Baz",
|
||||
"Bar - Baz",
|
||||
"Baz - Baz"
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
/**
|
||||
* Either proceed with the next group, or finish group tests
|
||||
*/
|
||||
nextGroup: function GroupTest_nextGroup() {
|
||||
while (this.tabs.length) {
|
||||
gBrowser.removeTab(this.tabs.pop());
|
||||
}
|
||||
if (this.groups.length) {
|
||||
this.groupNumber++;
|
||||
[this.labels, this.expectedLabels] = this.groups.shift();
|
||||
this.nextTab();
|
||||
} else {
|
||||
runNextTest();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Runs tests for existing tabs, and adds the next tab (if group isn't empty)
|
||||
* If the group is empty, starts the next group
|
||||
*/
|
||||
nextTab: function GroupTest_nextTab() {
|
||||
if (this.tabs.length > 1) {
|
||||
let ourExpected = this.expectedLabels.shift();
|
||||
for (let i = 0; i < this.tabs.length; i++) {
|
||||
is(this.tabs[i].visibleLabel, ourExpected[i],
|
||||
"Tab " + this.groupNumber + "." + (i + 1) + " has correct visibleLabel");
|
||||
}
|
||||
}
|
||||
if (this.labels.length) {
|
||||
this.tabs.push(gBrowser.addTab(
|
||||
"data:text/html,<title>" + this.labels.shift() + "</title>"));
|
||||
if (this.tabs.length > 1) {
|
||||
waitForTabLabel(this.tabs[this.tabs.length - 1],
|
||||
this.expectedLabels[0][this.expectedLabels[0].length - 1],
|
||||
this.nextTab.bind(this));
|
||||
} else {
|
||||
this.nextTab();
|
||||
}
|
||||
} else {
|
||||
this.nextGroup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let TESTS = [
|
||||
function test_about_blank() {
|
||||
let tab1 = gBrowser.selectedTab;
|
||||
let tab2;
|
||||
let tab3;
|
||||
addTabs(2, setup1);
|
||||
function setup1(aTabs) {
|
||||
[tab2, tab3] = aTabs
|
||||
waitForTabLabel(tab3, "New Tab", setupComplete);
|
||||
}
|
||||
function setupComplete() {
|
||||
is(tab1.visibleLabel, "New Tab", "First tab has original label");
|
||||
is(tab2.visibleLabel, "New Tab", "Second tab has original label");
|
||||
is(tab3.visibleLabel, "New Tab", "Third tab has original label");
|
||||
runNextTest();
|
||||
}
|
||||
},
|
||||
|
||||
function test_two_tabs() {
|
||||
let tab1 = gBrowser.selectedTab;
|
||||
addTabs(1, setup1);
|
||||
let tab2;
|
||||
function setup1(aTabs) {
|
||||
tab2 = aTabs[0];
|
||||
setTitleForTab(tab1, "Foo - Bar - Baz", "Foo - Bar - Baz", setup2);
|
||||
}
|
||||
function setup2() {
|
||||
setTitleForTab(tab2, "Foo - Baz - Baz", "Baz - Baz", setupComplete);
|
||||
}
|
||||
function setupComplete() {
|
||||
is(tab1.visibleLabel, "Bar - Baz", "Removed exactly two tokens");
|
||||
is(tab2.visibleLabel, "Baz - Baz", "Removed exactly two tokens");
|
||||
gBrowser.removeTab(tab2);
|
||||
waitForTabLabel(tab1, "Foo - Bar - Baz", afterRemoval);
|
||||
}
|
||||
function afterRemoval() {
|
||||
is (tab1.visibleLabel, "Foo - Bar - Baz", "Single tab has full title");
|
||||
runNextTest();
|
||||
}
|
||||
},
|
||||
|
||||
function test_direct_label() {
|
||||
let tab1 = gBrowser.selectedTab;
|
||||
addTabs(2, setup1);
|
||||
let tab2;
|
||||
let tab3;
|
||||
function setup1(aTabs) {
|
||||
tab2 = aTabs[0];
|
||||
tab3 = aTabs[1];
|
||||
setLabelForTab(tab1, "Foo - Bar - Baz", "Foo - Bar - Baz", setup2);
|
||||
}
|
||||
function setup2() {
|
||||
setLabelForTab(tab2, "Foo - Baz - Baz", "Foo - Baz - Baz", setup3);
|
||||
}
|
||||
function setup3() {
|
||||
setLabelForTab(tab3, "Foo - Baz - Baz", "Baz - Baz", setupComplete);
|
||||
}
|
||||
function setupComplete() {
|
||||
is(tab1.visibleLabel, "Bar - Baz", "Removed exactly two tokens");
|
||||
is(tab2.visibleLabel, "Foo - Baz - Baz", "Irregular spaces mean no match");
|
||||
is(tab3.visibleLabel, "Baz - Baz", "Removed exactly two tokens");
|
||||
gBrowser.removeTab(tab3);
|
||||
waitForTabLabel(tab1, "Foo - Bar - Baz", afterRemoval);
|
||||
}
|
||||
function afterRemoval() {
|
||||
is (tab1.visibleLabel, "Foo - Bar - Baz", "Single tab has full title");
|
||||
gBrowser.removeTab(tab2);
|
||||
runNextTest();
|
||||
}
|
||||
},
|
||||
|
||||
function test_groups() {
|
||||
let g = new GroupTest();
|
||||
g.nextGroup();
|
||||
}
|
||||
];
|
||||
|
||||
function runNextTest() {
|
||||
if (TESTS.length == 0) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
while (gBrowser.tabs.length > 1) {
|
||||
gBrowser.removeTab(gBrowser.tabs[1]);
|
||||
}
|
||||
|
||||
info("Running " + TESTS[0].name);
|
||||
TESTS.shift()();
|
||||
};
|
||||
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
runNextTest();
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ EXTRA_JS_MODULES = \
|
|||
NewTabUtils.jsm \
|
||||
offlineAppCache.jsm \
|
||||
SignInToWebsite.jsm \
|
||||
TabTitleAbridger.jsm \
|
||||
TelemetryTimestamps.jsm \
|
||||
Social.jsm \
|
||||
webappsUI.jsm \
|
||||
|
|
|
@ -0,0 +1,604 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
let EXPORTED_SYMBOLS = ["TabTitleAbridger"];
|
||||
|
||||
const Cu = Components.utils;
|
||||
const ABRIDGMENT_PREF = "browser.tabs.cropTitleRedundancy";
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gETLDService",
|
||||
"@mozilla.org/network/effective-tld-service;1",
|
||||
"nsIEffectiveTLDService");
|
||||
|
||||
function TabTitleAbridger(aBrowserWin) {
|
||||
this._tabbrowser = aBrowserWin.gBrowser;
|
||||
}
|
||||
|
||||
TabTitleAbridger.prototype = {
|
||||
/*
|
||||
* Events we listen to. We specifically do not listen for TabCreate, as we
|
||||
* get TabLabelModified at the appropriate times.
|
||||
*/
|
||||
_eventNames: [
|
||||
"TabPinned",
|
||||
"TabUnpinned",
|
||||
"TabShow",
|
||||
"TabHide",
|
||||
"TabClose",
|
||||
"TabLabelModified"
|
||||
],
|
||||
|
||||
init: function TabTitleAbridger_Initialize() {
|
||||
this._cropTitleRedundancy = Services.prefs.getBoolPref(ABRIDGMENT_PREF);
|
||||
Services.prefs.addObserver(ABRIDGMENT_PREF, this, false);
|
||||
if (this._cropTitleRedundancy) {
|
||||
this._domainSets = new DomainSets();
|
||||
this._addListeners();
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function TabTitleAbridger_Destroy() {
|
||||
Services.prefs.removeObserver(ABRIDGMENT_PREF, this);
|
||||
if (this._cropTitleRedundancy) {
|
||||
this._dropListeners();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Preference observer
|
||||
*/
|
||||
observe: function TabTitleAbridger_PrefObserver(aSubject, aTopic, aData) {
|
||||
let val = Services.prefs.getBoolPref(aData);
|
||||
if (this._cropTitleRedundancy && !val) {
|
||||
this._dropListeners();
|
||||
this._domainSets.destroy();
|
||||
delete this._domainSets;
|
||||
this._resetTabTitles();
|
||||
} else if (!this._cropTitleRedundancy && val) {
|
||||
this._addListeners();
|
||||
// We're just turned on, so we want to abridge everything
|
||||
this._domainSets = new DomainSets();
|
||||
let domains = this._domainSets.bootstrap(this._tabbrowser.visibleTabs);
|
||||
this._abridgeTabTitles(domains);
|
||||
}
|
||||
this._cropTitleRedundancy = val;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds all the necessary event listeners and listener-supporting objects for
|
||||
* the instance.
|
||||
*/
|
||||
_addListeners: function TabTitleAbridger_addListeners() {
|
||||
let tabContainer = this._tabbrowser.tabContainer;
|
||||
for (let eventName of this._eventNames) {
|
||||
tabContainer.addEventListener(eventName, this, false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes event listeners and listener-supporting objects for the instance.
|
||||
*/
|
||||
_dropListeners: function TabTitleAbridger_dropListeners() {
|
||||
let tabContainer = this._tabbrowser.tabContainer;
|
||||
for (let eventName of this._eventNames) {
|
||||
tabContainer.removeEventListener(eventName, this, false);
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent: function TabTitleAbridger_handler(aEvent) {
|
||||
let tab = aEvent.target;
|
||||
let updateSets;
|
||||
|
||||
switch (aEvent.type) {
|
||||
case "TabUnpinned":
|
||||
case "TabShow":
|
||||
updateSets = this._domainSets.addTab(tab);
|
||||
break;
|
||||
case "TabPinned":
|
||||
case "TabHide":
|
||||
case "TabClose":
|
||||
updateSets = this._domainSets.removeTab(tab);
|
||||
tab.visibleLabel = tab.label;
|
||||
break;
|
||||
case "TabLabelModified":
|
||||
if (!tab.hidden && !tab.pinned) {
|
||||
aEvent.preventDefault();
|
||||
updateSets = this._domainSets.updateTab(tab);
|
||||
}
|
||||
break;
|
||||
}
|
||||
this._abridgeTabTitles(updateSets);
|
||||
},
|
||||
|
||||
/**
|
||||
* Make all tabs have their visibleLabels be their labels.
|
||||
*/
|
||||
_resetTabTitles: function TabTitleAbridger_resetTabTitles() {
|
||||
// We're freshly disabled, so reset unpinned, visible tabs (see handleEvent)
|
||||
for (let tab of this._tabbrowser.visibleTabs) {
|
||||
if (!tab.pinned && tab.visibleLabel != tab.label) {
|
||||
tab.visibleLabel = tab.label;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply abridgment for the given tabset and chop list.
|
||||
* @param aTabSet Array of tabs to abridge
|
||||
* @param aChopList Corresponding array of chop points for the tabs
|
||||
*/
|
||||
_applyAbridgment: function TabTitleAbridger_applyAbridgment(aTabSet,
|
||||
aChopList) {
|
||||
for (let i = 0; i < aTabSet.length; i++) {
|
||||
let tab = aTabSet[i];
|
||||
let label = tab.label || "";
|
||||
if (label.length > 0) {
|
||||
let chop = aChopList[i] || 0;
|
||||
if (chop > 0) {
|
||||
label = label.substr(chop);
|
||||
}
|
||||
}
|
||||
if (label != tab.visibleLabel) {
|
||||
tab.visibleLabel = label;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Abridges the tabs sets of tabs in the aTabSets array.
|
||||
* @param aTabSets Array of tab sets needing abridgment
|
||||
*/
|
||||
_abridgeTabTitles: function TabTitleAbridger_abridgeTabtitles(aTabSets) {
|
||||
// Process each set
|
||||
for (let tabSet of aTabSets) {
|
||||
// Get a chop list for the set and apply it
|
||||
let chopList = AbridgmentTools.getChopsForSet(tabSet);
|
||||
this._applyAbridgment(tabSet, chopList);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Maintains a mapping between tabs and domains, so that only the tabs involved
|
||||
* in a TabLabelModified event need to be modified by the TabTitleAbridger.
|
||||
*/
|
||||
function DomainSets() {
|
||||
this._domainSets = {};
|
||||
this._tabsMappedToDomains = new WeakMap();
|
||||
}
|
||||
|
||||
DomainSets.prototype = {
|
||||
_noHostSchemes: {
|
||||
chrome: true,
|
||||
file: true,
|
||||
resource: true,
|
||||
data: true,
|
||||
about: true
|
||||
},
|
||||
|
||||
destroy: function DomainSets_destroy() {
|
||||
delete this._domainSets;
|
||||
delete this._tabsMappedToDomains;
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to build the domainsets when enabled in mid-air, as opposed to when
|
||||
* the window is coming up.
|
||||
* @param The visibleTabs for the browser, or a set of tabs to check.
|
||||
* @return An array containing the tabs in the domains they belong to, or
|
||||
* an empty array if none of the tabs belonged to domains.
|
||||
*/
|
||||
bootstrap: function DomainSets_bootstrap(aVisibleTabs) {
|
||||
let needAbridgment = [];
|
||||
for (let tab of aVisibleTabs) {
|
||||
let domainSet = this.addTab(aTab)[0] || null;
|
||||
if (domainSet && needAbridgment.indexOf(domainSet) == -1) {
|
||||
needAbridgment.push(domainSet);
|
||||
}
|
||||
}
|
||||
return needAbridgment;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a tab, include it in the domain sets.
|
||||
* @param aTab The tab to include in the domain sets
|
||||
* @param aTabDomain [optional] The known domain for the tab
|
||||
* @return An array containing the tabs in the domain the tab was added to.
|
||||
*/
|
||||
addTab: function DomainSets_addTab(aTab, aTabDomain) {
|
||||
let tabDomain = aTabDomain || this._getDomainForTab(aTab);
|
||||
if (!this._domainSets.hasOwnProperty(tabDomain)) {
|
||||
this._domainSets[tabDomain] = [];
|
||||
}
|
||||
this._domainSets[tabDomain].push(aTab);
|
||||
this._tabsMappedToDomains.set(aTab, tabDomain);
|
||||
return [this._domainSets[tabDomain]];
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a tab, remove it from the domain sets.
|
||||
* @param aTab The tab to remove from the domain sets
|
||||
* @param aTabDomain [optional] The known domain for the tab
|
||||
* @return An array containing the tabs in the domain the tab was removed
|
||||
* from, or an empty array if the tab was not removed from a domain set.
|
||||
*/
|
||||
removeTab: function DomainSets_removeTab(aTab, aTabDomain) {
|
||||
let oldTabDomain = aTabDomain || this._tabsMappedToDomains.get(aTab);
|
||||
if (!this._domainSets.hasOwnProperty(oldTabDomain)) {
|
||||
return [];
|
||||
}
|
||||
let index = this._domainSets[oldTabDomain].indexOf(aTab);
|
||||
if (index == -1) {
|
||||
return [];
|
||||
}
|
||||
this._domainSets[oldTabDomain].splice(index, 1);
|
||||
this._tabsMappedToDomains.delete(aTab);
|
||||
if (!this._domainSets[oldTabDomain].length) {
|
||||
// Keep the sets clean of empty domains
|
||||
delete this._domainSets[oldTabDomain];
|
||||
return [];
|
||||
}
|
||||
return [this._domainSets[oldTabDomain]];
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a tab, update the domain set it belongs to.
|
||||
* @param aTab The tab to update the domain set for
|
||||
* @return An array containing the tabs in the domain the tab belongs to, and
|
||||
* (if changed) the domain the tab was removed from.
|
||||
*/
|
||||
updateTab: function DomainSets_updateTab(aTab) {
|
||||
let tabDomain = this._getDomainForTab(aTab);
|
||||
let oldTabDomain = this._tabsMappedToDomains.get(aTab);
|
||||
if (oldTabDomain != tabDomain) {
|
||||
let needAbridgment = [];
|
||||
// Probably swapping domain sets out; we pass the domains along to avoid
|
||||
// re-getting them in addTab/removeTab
|
||||
if (oldTabDomain) {
|
||||
needAbridgment = needAbridgment.concat(
|
||||
this.removeTab(aTab, oldTabDomain));
|
||||
}
|
||||
return needAbridgment.concat(this.addTab(aTab, tabDomain));
|
||||
}
|
||||
// No change was needed
|
||||
return [this._domainSets[tabDomain]];
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a tab, determine the URI scheme or host to categorize it.
|
||||
* @param aTab The tab to get the domain for
|
||||
* @return The domain or scheme for the tab
|
||||
*/
|
||||
_getDomainForTab: function DomainSets_getDomainForTab(aTab) {
|
||||
let browserURI = aTab.linkedBrowser.currentURI;
|
||||
if (browserURI.scheme in this._noHostSchemes) {
|
||||
return browserURI.scheme;
|
||||
}
|
||||
|
||||
// throws for empty URI, host is IP, and disallowed characters
|
||||
try {
|
||||
return gETLDService.getBaseDomain(browserURI);
|
||||
}
|
||||
catch (e) {}
|
||||
|
||||
// this nsIURI may not be an nsStandardURL nsIURI, which means it
|
||||
// might throw for the host
|
||||
try {
|
||||
return browserURI.host;
|
||||
}
|
||||
catch (e) {}
|
||||
|
||||
// Treat this URI as unique
|
||||
return browserURI.spec;
|
||||
}
|
||||
};
|
||||
|
||||
let AbridgmentTools = {
|
||||
/**
|
||||
* Constant for the minimum remaining length allowed if a label is abridged.
|
||||
* I.e., original:"abc - de" might be chopped to just "de", which is too
|
||||
* small, so the label would be reverted to the next-longest version.
|
||||
*/
|
||||
MIN_CHOP: 3,
|
||||
|
||||
/**
|
||||
* Helper to determine if aStr is URI-like
|
||||
* \s? optional leading space
|
||||
* [^\s\/]* optional scheme or relative path component
|
||||
* ([^\s\/]+:\/)? optional scheme separator, with at least one scheme char
|
||||
* \/ at least one slash
|
||||
* \/? optional second (or third for eg, file scheme on UNIX) slash
|
||||
* [^\s\/]* optional path component
|
||||
* ([^\s\/]+\/?)* optional more path components with optional end slash
|
||||
* @param aStr the string to check for URI-likeness
|
||||
* @return boolean value of whether aStr matches
|
||||
*/
|
||||
_titleIsURI: function AbridgmentTools_titleIsURI(aStr) {
|
||||
return /^\s?[^\s\/]*([^\s\/]+:\/)?\/\/?[^\s\/]*([^\s\/]+\/?)*$/.test(aStr);
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds the proper abridgment indexes for the given tabs.
|
||||
* @param aTabSet the array of tabs to find abridgments for
|
||||
* @return an array of abridgment indexes corresponding to the tabs
|
||||
*/
|
||||
getChopsForSet: function AbridgmentTools_getChopsForSet(aTabSet) {
|
||||
let chopList = [];
|
||||
let pathMode = false;
|
||||
|
||||
aTabSet.sort(function(aTab, bTab) {
|
||||
let aLabel = aTab.label;
|
||||
let bLabel = bTab.label;
|
||||
return (aLabel < bLabel) ? -1 : (aLabel > bLabel) ? 1 : 0;
|
||||
});
|
||||
|
||||
// build and apply the chopList for the set
|
||||
for (let i = 0, next = 1; next < aTabSet.length; i = next++) {
|
||||
next = this._abridgePair(aTabSet, i, next, chopList);
|
||||
}
|
||||
return chopList;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the abridgment between aIndex and aNext, or in the case where the
|
||||
* label at aNext is the same as at aIndex, moves aNext forward appropriately.
|
||||
* @param aTabSet Sorted array of tabs that the indices refer to
|
||||
* @param aIndex First tab index to use in abridgment
|
||||
* @param aNext Second tab index to use as the an initial comparison
|
||||
* @param aChopList Array to add chop points to for the given tabs
|
||||
* @return Index to replace aNext with, that is the index of the tab that was
|
||||
* used in abridging the tab at aIndex
|
||||
*/
|
||||
_abridgePair: function TabTitleAbridger_abridgePair(aTabSet, aIndex, aNext,
|
||||
aChopList) {
|
||||
let tabStr = aTabSet[aIndex].label;
|
||||
let pathMode = this._titleIsURI(tabStr);
|
||||
let chop = RedundancyFinder.indexOfSep(pathMode, tabStr);
|
||||
|
||||
// Default to no chop
|
||||
if (!aChopList[aIndex]) {
|
||||
aChopList[aIndex] = 0;
|
||||
}
|
||||
|
||||
// Siblings with same label get proxied by the first
|
||||
let nextStr;
|
||||
aNext = this._nextUnproxied(aTabSet, tabStr, aNext);
|
||||
if (aNext < aTabSet.length) {
|
||||
nextStr = aTabSet[aNext].label;
|
||||
}
|
||||
|
||||
// Bail on these strings early, using the first as the basis
|
||||
if (chop == -1 || aNext == aTabSet.length ||
|
||||
!nextStr.startsWith(tabStr.substr(0, chop + 1))) {
|
||||
chop = aChopList[aIndex];
|
||||
if (aNext != aTabSet.length) {
|
||||
aChopList[aNext] = 0;
|
||||
}
|
||||
} else {
|
||||
[pathMode, chop] = this._getCommonChopPoint(pathMode, tabStr, nextStr,
|
||||
chop);
|
||||
[chop, aChopList[aNext]] = this._adjustChops(pathMode, tabStr, nextStr,
|
||||
chop);
|
||||
aChopList[aIndex] = chop;
|
||||
}
|
||||
|
||||
// Mark chop on the relevant tabs
|
||||
for (let j = aIndex; j < aNext; j++) {
|
||||
let oldChop = aChopList[j];
|
||||
if (!oldChop || oldChop < chop) {
|
||||
aChopList[j] = chop;
|
||||
}
|
||||
}
|
||||
return aNext;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the index in aTabSet of the next tab that's not equal to aStr.
|
||||
* @param aTabSet Sorted set of tabs to check
|
||||
* @param aStr Label string to check against
|
||||
* @param aStart First item to check for proxying
|
||||
* @return The index of the next different tab.
|
||||
*/
|
||||
_nextUnproxied: function AbridgmentTools_nextUnproxied(aTabSet, aTabStr,
|
||||
aStart) {
|
||||
let nextStr = aTabSet[aStart].label;
|
||||
while (aStart < aTabSet.length && aTabStr == nextStr) {
|
||||
aStart += 1;
|
||||
if (aStart < aTabSet.length) {
|
||||
nextStr = aTabSet[aStart].label;
|
||||
}
|
||||
}
|
||||
return aStart;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the common index where the aTabStr and aNextStr diverge.
|
||||
* @param aPathMode Whether to use path mode
|
||||
* @param aTabStr Tab label
|
||||
* @param aNextStr Second tab label
|
||||
* @param aChop Current chop point being considered (index of aTabStr's
|
||||
* first separator)
|
||||
* @return An array containing the resulting path mode (in case it changes)
|
||||
* and the diverence index for the labels.
|
||||
*/
|
||||
_getCommonChopPoint: function AbridgmentTools_getCommonChopPoint(aPathMode,
|
||||
aTabStr,
|
||||
aNextStr,
|
||||
aChop) {
|
||||
aChop = RedundancyFinder.findCommonPrefix(aPathMode, aTabStr, aNextStr,
|
||||
aChop);
|
||||
// Does a URI remain?
|
||||
if (!aPathMode) {
|
||||
aPathMode = this._titleIsURI(aTabStr.substr(aChop));
|
||||
if (aPathMode) {
|
||||
aChop = RedundancyFinder.findCommonPrefix(aPathMode, aTabStr, aNextStr,
|
||||
aChop);
|
||||
}
|
||||
}
|
||||
|
||||
return [aPathMode, aChop + 1];
|
||||
},
|
||||
|
||||
/**
|
||||
* Adjusts the chop points based on their suffixes and lengths.
|
||||
* @param aPathMode Whether to use path mode
|
||||
* @param aTabStr Tab label
|
||||
* @param aNextStr Second tab label
|
||||
* @param aChop Current chop point being considered
|
||||
* @return An array containing the chop point for the two labels.
|
||||
*/
|
||||
_adjustChops: function AbridgmentTools_adjustChops(aPathMode, aTabStr,
|
||||
aNextStr, aChop) {
|
||||
let suffix = RedundancyFinder.findCommonSuffix(aPathMode, aTabStr,
|
||||
aNextStr);
|
||||
let sufPos = aTabStr.length - suffix;
|
||||
let nextSufPos = aNextStr.length - suffix;
|
||||
let nextChop = aChop;
|
||||
|
||||
// Adjust the chop based on the suffix.
|
||||
if (sufPos < aChop) {
|
||||
// Only revert based on suffix for tab and any identicals
|
||||
aChop = RedundancyFinder.lastIndexOfSep(aPathMode, aTabStr,
|
||||
sufPos - 1)[1] + 1;
|
||||
} else if (nextSufPos < aChop) {
|
||||
// Only revert based on suffix for 'next'
|
||||
nextChop = RedundancyFinder.lastIndexOfSep(aPathMode, aNextStr,
|
||||
nextSufPos - 1)[1] + 1;
|
||||
}
|
||||
|
||||
if (aTabStr.length - aChop < this.MIN_CHOP) {
|
||||
aChop = RedundancyFinder.lastIndexOfSep(aPathMode, aTabStr,
|
||||
aChop - 2)[1] + 1;
|
||||
}
|
||||
if (aNextStr.length - nextChop < this.MIN_CHOP) {
|
||||
nextChop = RedundancyFinder.lastIndexOfSep(aPathMode, aNextStr,
|
||||
nextChop - 2)[1] + 1;
|
||||
}
|
||||
return [aChop, nextChop];
|
||||
}
|
||||
};
|
||||
|
||||
let RedundancyFinder = {
|
||||
/**
|
||||
* Finds the first index of a matched separator after aStart.
|
||||
* Separators will either be space-padded punctuation or slashes (in pathmode)
|
||||
*
|
||||
* ^.+? at least one character, non-greedy match
|
||||
* \s+ one or more whitespace characters
|
||||
* [-:>\|]+ one or more separator characters
|
||||
* \s+ one or more whitespace characters
|
||||
*
|
||||
* @param aPathMode true for path mode, false otherwise
|
||||
* @param aStr the string to look for a separator in
|
||||
* @param aStart (optional) an index to start the search from
|
||||
* @return the next index of a separator or -1 for none
|
||||
*/
|
||||
indexOfSep: function RedundancyFinder_indexOfSep(aPathMode, aStr, aStart) {
|
||||
if (aPathMode) {
|
||||
return aStr.indexOf('/', aStart);
|
||||
}
|
||||
|
||||
let match = aStr.slice(aStart).match(/^.+?\s+[-:>\|]+\s+/);
|
||||
if (match) {
|
||||
return (aStart || 0) + match[0].length - 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Compares a pair of strings, seeking an index where their redundancy ends
|
||||
* @param aPathMode true for pathmode, false otherwise
|
||||
* @param aStr the string to decide an abridgment for
|
||||
* @param aNextStr the lexicographically next string to compare with
|
||||
* @param aChop the basis index, a best-known index to begin comparison
|
||||
* @return the index at which aStr's abridged title should begin
|
||||
*/
|
||||
findCommonPrefix: function RedundancyFinder_findCommonPrefix(aPathMode, aStr,
|
||||
aNextStr,
|
||||
aChop) {
|
||||
// Advance until the end of the title or the pair diverges
|
||||
do {
|
||||
aChop = this.indexOfSep(aPathMode, aStr, aChop + 1);
|
||||
} while (aChop != -1 && aNextStr.startsWith(aStr.substr(0, aChop + 1)));
|
||||
|
||||
if (aChop < 0) {
|
||||
aChop = aStr.length;
|
||||
}
|
||||
|
||||
// Return the last valid spot
|
||||
return this.lastIndexOfSep(aPathMode, aStr, aChop - 1)[1];
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds the range of a separator earlier than aEnd in aStr
|
||||
* The range is required by findCommonSuffix() needing to know the beginning
|
||||
* of the separator.
|
||||
* Separators will either be space-padded punctuation or slashes (in pathmode)
|
||||
*
|
||||
* .+ one or more initial characters
|
||||
* ( first group
|
||||
* ( second group
|
||||
* \s+ one or more whitespace characters
|
||||
* [-:>\|]+ one or more separator characters
|
||||
* \s+ one or more whitespace characters
|
||||
* ) end first group
|
||||
* .*? zero or more characters, non-greedy match
|
||||
* ) end second group
|
||||
* $ end of input
|
||||
*
|
||||
* @param aPathMode true for pathmode, false otherwise
|
||||
* @param aStr the string to look for a separator in
|
||||
* @param aEnd (optional) an index to start the backwards search from
|
||||
* @return an array containing the endpoints of a separator (-1, -1 for none)
|
||||
*/
|
||||
lastIndexOfSep: function RedundancyFinder_lastIndexOfSep(aPathMode, aStr,
|
||||
aEnd) {
|
||||
if (aPathMode) {
|
||||
let path = aStr.lastIndexOf('/', aEnd);
|
||||
return [path, path];
|
||||
}
|
||||
|
||||
let string = aStr.slice(0, aEnd);
|
||||
let match = string.match(/.+((\s+[-:>\|]+\s+).*?)$/);
|
||||
if (match) {
|
||||
let index = string.length - match[1].length;
|
||||
return [index, index + match[2].length - 1];
|
||||
}
|
||||
|
||||
return [-1, -1];
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds a common suffix (redundancy at the end of) a pair of strings.
|
||||
* @param aPathMode true for pathmode, false otherwise
|
||||
* @param aStr a base string to look for a suffix in
|
||||
* @param aNextStr a string that may share a common suffix with aStr
|
||||
* @return an index indicating the divergence between the strings
|
||||
*/
|
||||
findCommonSuffix: function RedundancyFinder_findCommonSuffix(aPathMode, aStr,
|
||||
aNextStr) {
|
||||
let last = this.lastIndexOfSep(aPathMode, aStr)[0];
|
||||
|
||||
// Is there any suffix match?
|
||||
if (!aNextStr.endsWith(aStr.slice(last))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Move backwards on the main string until the suffix diverges
|
||||
let oldLast;
|
||||
do {
|
||||
oldLast = last;
|
||||
last = this.lastIndexOfSep(aPathMode, aStr, last - 1)[0];
|
||||
} while (last != -1 && aNextStr.endsWith(aStr.slice(last)));
|
||||
|
||||
return aStr.length - oldLast;
|
||||
}
|
||||
};
|
||||
|
Загрузка…
Ссылка в новой задаче