Commit 3dabfc86 authored by Laurent Wouters's avatar Laurent Wouters
Browse files

Refactoring the extension

parent 5d2d0b2149e4
......@@ -36,7 +36,6 @@
"@types/react-dom": "^16.0.6",
"awesome-typescript-loader": "^5.2.0",
"copy-webpack-plugin": "^4.5.2",
"html-webpack-plugin": "^3.0.0",
"source-map-loader": "^0.2.3",
"typescript": "^2.9.2",
"web-ext": "^2.7.0",
......
......@@ -31,10 +31,13 @@ import {
MimeInfo,
refersToPrimaryTopic,
refersToData,
TabRegistry,
resolveTabData
ObservedResourceRegistry,
resolveObservationsForTab,
selectDataSource,
hasDetectedData,
ResourceObservedData
} from "../common/data";
import { Message, updateTab, navigateTabTo } from "../common/messages";
import { Message } from "../common/messages";
/// <reference path="./fallback.d.ts"/>
let F = require("./fallback");
import "chrome";
......@@ -47,7 +50,7 @@ import {
/**
* The data about the tabs
*/
let allTabs: TabRegistry = {};
let allObservations: ObservedResourceRegistry = {};
/**
* The reference view registry
*/
......@@ -64,60 +67,18 @@ reloadRegistryFromStorage(registry)
});
/**
* Updates a tab
* When the observed data about a tab has been updated
* @param id The identifier of a tab
*/
function onTabUpdated(tabId: number): void {
updateTabIcon(tabId);
updateTab(tabId, resolveTabData(allTabs, tabId));
}
/**
* Updates the icon of a tab
* @param id The identifier of a tab
*/
function updateTabIcon(tabId: number): void {
let data = resolveTabData(allTabs, tabId);
chrome.browserAction.setIcon({
path: data.command.isActive
? {
48: "icons/app.png"
}
: {
48: "icons/app_disabled.png"
},
tabId: tabId
});
chrome.browserAction.setBadgeBackgroundColor({
color: [217, 0, 0, 255],
tabId: tabId
});
var badge = "";
if (data.mainSource != NO_DATA) badge += "\u2605";
if (data.primaryTopic != null) badge += "\u2605";
chrome.browserAction.setBadgeText({
text: badge,
tabId: tabId
});
}
/**
* When a tab has been activated
* @param tabId The identifier of a tab
*/
function onTabActivated(tabId: number): void {
updateTabIcon(tabId);
function onObservedTabUpdated(tabId: number): void {
let observation = resolveObservationsForTab(allObservations, tabId);
if (hasDetectedData(observation)) {
chrome.pageAction.show(tabId);
} else {
chrome.pageAction.hide(tabId);
}
}
// listen to tab URL changes
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
onTabActivated(tabId);
});
// listen to tab switching
chrome.tabs.onActivated.addListener(function(activeInfo) {
onTabActivated(activeInfo.tabId);
});
/**
* Parses the tags for a link
* @param content The link description
......@@ -269,10 +230,8 @@ function doPreempt(
): chrome.webRequest.BlockingResponse {
let headers = details.responseHeaders;
setHeader(headers, "Content-Type", "text/plain");
let data = resolveTabData(allTabs, details.tabId);
let data = resolveObservationsForTab(allObservations, details.tabId);
data.sources.push(new DataSourcePage(details.url, mime.mime));
data.command.isActive = true; // activate by default
data.selectMain();
return { responseHeaders: headers };
}
......@@ -292,6 +251,7 @@ function onHeadersReceived(
details.type != "main_frame" // not the top-level document
)
return {};
let index = details.url.indexOf("?protocol=ldviews&uri=");
if (index >= 0) {
// preempts the protocol ldviews
......@@ -303,37 +263,38 @@ function onHeadersReceived(
redirectUrl: chrome.extension.getURL("ldviews/index.html") + "?uri=" + uri
};
}
let data = resolveTabData(allTabs, details.tabId);
if (data.mainSource != NO_DATA)
// already detected something
return {};
let observation = resolveObservationsForTab(allObservations, details.tabId);
observation.url = details.url;
// try to detect the primary topic in HTTP headers' links
let links = getAllLinks(details.responseHeaders);
data.primaryTopic = detectTopicOnlinks(links);
if (observation.primaryTopic == "")
observation.primaryTopic = detectTopicOnlinks(links);
// determine if we shall preempt the content due to its type (raw RDF data)
let mime = shouldPreempt(details);
/*let mime = shouldPreempt(details);
if (mime != null) {
// preempts this request
return doPreempt(details, mime);
}
}*/
// try to detect alternate sources of content
data.sources.push(detectDataOnContent(details, details.responseHeaders));
data.sources = data.sources.concat(detectDataOnLinks(links));
data.selectMain();
observation.sources.push(
detectDataOnContent(details, details.responseHeaders)
);
observation.sources = observation.sources.concat(detectDataOnLinks(links));
// if there are still nothing, try to probe with HTTP content negotiation
if (data.mainSource == NO_DATA) {
if (!hasDetectedData(observation)) {
tryNegotiateData(details.url)
.then((source: DataSourceLinked) => {
data.sources.push(source);
data.selectMain();
onTabUpdated(details.tabId);
observation.sources.push(source);
onObservedTabUpdated(details.tabId);
})
.catch((reason: string) => {
// do nothing
onObservedTabUpdated(details.tabId);
});
} else {
onObservedTabUpdated(details.tabId);
}
updateTabIcon(details.tabId);
return {};
}
......@@ -345,10 +306,10 @@ function onBeforeNavigate(
details: chrome.webNavigation.WebNavigationFramedCallbackDetails
): void {
if (details.tabId == -1 || details.frameId != 0) return;
let data = resolveTabData(allTabs, details.tabId);
data.clear();
data.url = details.url;
updateTabIcon(details.tabId);
let observation = new ResourceObservedData();
observation.url = details.url;
allObservations[details.tabId] = observation;
chrome.pageAction.hide(details.tabId);
}
/**
......@@ -359,7 +320,7 @@ function onCompleted(
details: chrome.webNavigation.WebNavigationFramedCallbackDetails
): void {
if (details.tabId == -1 || details.frameId != 0) return;
onTabUpdated(details.tabId);
onObservedTabUpdated(details.tabId);
}
// listen to received headers
......@@ -374,31 +335,25 @@ F.registerNavigations(onBeforeNavigate, onCompleted);
chrome.runtime.onMessage.addListener(
(request: Message<any>, sender, sendResponse) => {
if (request.requestType == "SubmitHtmlLinks") {
let data = resolveTabData(allTabs, sender.tab.id);
if (data.primaryTopic == null) {
data.primaryTopic = detectTopicOnlinks(request.payload);
let observation = resolveObservationsForTab(
allObservations,
sender.tab.id
);
if (observation.primaryTopic == null || observation.primaryTopic == "") {
observation.primaryTopic = detectTopicOnlinks(request.payload);
}
data.sources = data.sources.concat(detectDataOnLinks(request.payload));
data.selectMain();
onTabUpdated(sender.tab.id);
} else if (request.requestType == "GetTabId") {
sendResponse(sender.tab.id);
} else if (request.requestType == "GetTabData") {
observation.sources = observation.sources.concat(
detectDataOnLinks(request.payload)
);
onObservedTabUpdated(sender.tab.id);
} else if (request.requestType == "GetObservations") {
let tabId = request.payload;
if (tabId == null || tabId == undefined) {
// deduce from the sender
tabId = sender.tab.id;
}
let data = resolveTabData(allTabs, tabId);
sendResponse(data);
} else if (request.requestType == "SetTabData") {
let data = resolveTabData(allTabs, request.payload.tabId);
data.updateWith(request.payload.tabData);
onTabUpdated(request.payload.tabId);
sendResponse(data);
} else if (request.requestType == "NavigateTo") {
// trampoline the request back to the tab
navigateTabTo(request.payload.tabId, request.payload.uri);
let observation = resolveObservationsForTab(allObservations, tabId);
sendResponse(observation);
} else if (request.requestType == "GetViewRegistry") {
sendResponse(registry);
} else if (request.requestType == "AddNewSource") {
......@@ -442,3 +397,24 @@ chrome.runtime.onMessage.addListener(
}
}
);
chrome.pageAction.onClicked.addListener((tab: chrome.tabs.Tab) => {
let observation = resolveObservationsForTab(allObservations, tab.id);
let url = chrome.extension.getURL("ldbrowser/index.html");
let callback = (openedTab: chrome.tabs.Tab) => {
allObservations[openedTab.id] = observation;
};
let promise: any = chrome.tabs.create(
{
windowId: tab.windowId,
index: tab.index + 1,
url: url,
active: true,
openerTabId: tab.id
},
callback
);
if (promise != undefined && promise != null) {
promise.then(callback);
}
});
......@@ -315,137 +315,150 @@ function compareSources(x: DataSource, y: DataSource): number {
export const NO_DATA = new DataSourceNone();
/**
* The user command for a tab
* The observed data about a web resource
*/
export class TabCommand {
export class ResourceObservedData {
constructor() {
this.isActive = false;
this.isAutomatic = true;
this.selectedSource = "";
this.selectedView = "";
this.selectedLanguage = application.NO_LANGUAGE;
this.url = "";
this.sources = [];
this.primaryTopic = "";
}
/**
* Whether the user activated the extension for this tab
*/
isActive: boolean;
/**
* Whether the user is using automatic selection of data sources and views
* The resource's url
*/
isAutomatic: boolean;
/**
* The identifier of the selected data source (if not automatic)
*/
selectedSource: string;
url: string;
/**
* The identifier of the selected view (if not automatic)
* All the alternative data sources
*/
selectedView: string;
sources: DataSource[];
/**
* The selected language (if not automatic)
* The detected primary topic if any
*/
selectedLanguage: application.Language;
primaryTopic: string;
}
/**
* Updates this command with the specified one
* @param command The updated command
*/
updateWith(command: TabCommand) {
this.isActive = command.isActive;
this.isAutomatic = command.isAutomatic;
this.selectedSource = command.selectedSource;
this.selectedView = command.selectedView;
this.selectedLanguage = command.selectedLanguage;
}
/**
* Gets whether data has been detected
* @param observation The current observation
*/
export function hasDetectedData(observation: ResourceObservedData): boolean {
let main = observation.sources.find(
(source: DataSource) => source != NO_DATA
);
return main != undefined && main != null && main != NO_DATA;
}
/**
* Data about a tab
* The user command for a specific resource for the linked data browser
*/
export class TabData {
export class ResourceUserCommand {
constructor() {
this.command = new TabCommand();
this.mainSource = NO_DATA;
this.sources = [];
this.primaryTopic = null;
this.url = null;
this.isAutomatic = true;
this.selectedSource = "";
this.selectedTopic = "";
this.selectedView = "";
this.selectedLanguage = application.NO_LANGUAGE;
}
/**
* The user command for this tab
* Whether the command use the defaults
*/
command: TabCommand;
isAutomatic: boolean;
/**
* The data associated to this tab
* The identifier of the user-selected data source
*/
mainSource: DataSource;
selectedSource: string;
/**
* All the alternative data sources
* The URI of the user-selected topic, if any
*/
sources: DataSource[];
selectedTopic: string;
/**
* The primary topic on this tab
* The identifier of the user-selected view
*/
primaryTopic: string;
selectedView: string;
/**
* The current url for the tab
* The user-selected language
*/
url: string;
selectedLanguage: application.Language;
}
/**
* The data about a resource for the linked data browser
*/
export class ResourceData {
/**
* Updates this data with the specified one
* @param data The updated data
* The observed data
*/
updateWith(data: TabData) {
this.command.updateWith(data.command);
this.selectMain();
}
observations: ResourceObservedData;
/**
* Clears this data
* The associated user command
*/
clear(): void {
this.command.isAutomatic = true;
this.command.selectedSource = "";
this.command.selectedView = "";
this.command.selectedLanguage = application.NO_LANGUAGE;
this.mainSource = NO_DATA;
this.sources = [];
this.primaryTopic = null;
command: ResourceUserCommand;
}
/**
* Selects the main resource among the provided one
* @param resource The current data about a resource
*/
export function selectDataSource(resource: ResourceData): DataSource {
if (
resource.command.isAutomatic ||
resource.command.selectedSource == null ||
resource.command.selectedSource == ""
) {
if (resource.observations.sources.length == 0) {
return NO_DATA;
}
let sources = resource.observations.sources
.filter((source: DataSource) => {
return source != NO_DATA;
})
.sort(compareSources);
return sources.length == 0 ? NO_DATA : sources[0];
} else {
let source = resource.observations.sources.find(
(source: DataSource) => source.url == resource.command.selectedSource
);
return source != null && source != undefined ? source : NO_DATA;
}
}
/**
* Selects the relevant primay topic
* @param resource The current data about a resource
*/
export function selectPrimaryTopic(resource: ResourceData): string {
if (
resource.command.isAutomatic ||
resource.command.selectedTopic == null ||
resource.command.selectedTopic == ""
) {
return resource.observations.primaryTopic;
} else {
return resource.command.selectedTopic;
}
}
/**
* The data of an LD browser
*/
export class LDBrowserData {
constructor(observation: ResourceObservedData) {
let init = new ResourceData();
init.observations = observation;
init.command = new ResourceUserCommand();
this.history = [init];
this.byResource = {};
this.byResource[observation.url] = init;
}
/**
* Selects the main resource among the provided one
* The history of resources
*/
selectMain(): DataSource {
if (
this.command.isAutomatic ||
this.command.selectedSource == null ||
this.command.selectedSource == ""
) {
if (this.sources.length == 0) {
this.mainSource = NO_DATA;
} else {
this.sources = this.sources
.filter((source: DataSource) => {
return source != NO_DATA;
})
.sort(compareSources);
this.mainSource = this.sources.length == 0 ? NO_DATA : this.sources[0];
}
} else {
this.mainSource = this.sources.reduce(
(acc: DataSource, current: DataSource) => {
if (acc != null) return acc;
if (current.url == this.command.selectedSource) return current;
return null;
},
null
);
if (this.mainSource == null) this.mainSource = NO_DATA;
}
return this.mainSource;
}
history: ResourceData[];
/**
* The data for each known resource
*/
byResource: { [url: string]: ResourceData };
}
/**
......@@ -488,20 +501,23 @@ export function tryNegotiateData(target: string): Promise<DataSourceLinked> {
}
/**
* A registry of tab data
* A registry for observed data
*/
export interface TabRegistry {
[index: number]: TabData;
export interface ObservedResourceRegistry {
[index: number]: ResourceObservedData;
}
/**
* Gets the data about a tab
* Gets the observed data for a tab
* @param registry The registry of tab data
* @param id The identifier of a tab
*/
export function resolveTabData(registry: TabRegistry, id: number): TabData {
export function resolveObservationsForTab(
registry: ObservedResourceRegistry,
id: number
): ResourceObservedData {
if (!registry.hasOwnProperty(id)) {
registry[id] = new TabData();
registry[id] = new ResourceObservedData();
}
return registry[id];
}
......@@ -18,7 +18,7 @@
* with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { TabData } from "./data";
import { ResourceObservedData } from "./data";
import { definition } from "@logilab/libview";
import "chrome";
......@@ -153,130 +153,24 @@ export function getResourceContent(
}
/**
* Gets the identifier of the current tab from the background
*/
export function getTabId(): Promise<number> {
return new Promise<number>(
(resolve: (tabId: number) => void, reject: (reason: any) => void) => {
chrome.runtime.sendMessage(
{
requestType: "GetTabId",
payload: null
},
(tabId: number) => {
resolve(tabId);
}
);
}
);
}
/**
* Gets data about the current tab to the background
* Gets the observations about a tab
* @param tabId The corresponding tab number
*/
export function getTabData(tabId: number): Promise<TabData> {
return new Promise<TabData>(
(resolve: (tabData: TabData) => void, reject: (reason: any) => void) => {
export function getObservationsFor(
tabId: number
): Promise<ResourceObservedData> {
return new Promise<ResourceObservedData>(
(
resolve: (data: ResourceObservedData) => void,
reject: (reason: any) => void
) => {
chrome.runtime.sendMessage(
{
requestType: "GetTabData",
requestType: "GetObservations",
payload: tabId
},