Commit 321659ac authored by Laurent Wouters's avatar Laurent Wouters
Browse files

[clean] Refactored the LD browser

parent 8704f86970b7
......@@ -274,7 +274,9 @@ 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 url = chrome.extension.getURL(
"ldbrowser/index.html?target=" + encodeURIComponent(observation.url)
);
let callback = (openedTab: chrome.tabs.Tab) => {
allObservations[openedTab.id] = observation;
};
......
/*******************************************************************************
* Copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
* contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
*
* This file is part of CubicWeb.
*
* CubicWeb is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 2.1 of the License, or (at your option)
* any later version.
*
* CubicWeb is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import "chrome";
import * as $rdf from "rdflib";
import { application, definition, implementation } from "@logilab/libview";
import * as LANGUAGES from "../common/data.iso639.json";
import {
ResourceData,
fetchSource,
selectDataSources,
RawContent,
selectPrimaryTopic,
ObservableContent,
observeContent,
ResourceUserCommand,
DataSource
} from "../common/data";
import {
getResourceContent,
getViewRegistry,
fetchObservableAt
} from "../common/messages";
require("./view-defaults-impl");
/**
* Gets the user language selected by the browser
*/
function getBrowserLanguage(): application.Language {
let lang = navigator.language;
let i = lang.indexOf("-");
if (i > 0) lang = lang.substring(0, i);
let result = LANGUAGES.find(
(language: application.Language) =>
language.iso639_1 == lang || language.iso639_2 == lang
);
if (result != null && result != undefined) return result;
return application.NO_LANGUAGE;
}
/**
* The current language of the browser
*/
const BROWSER_LANGUAGE = getBrowserLanguage();
/**
* Event handler for an LD browser
*/
export interface LDBrowserEventHandler {
/**
* When the browser is currently loading data
* @param description The current loading state
*/
onLoad(description: string): void;
/**
* When the browser must render something
* @param registry The view registry
* @param rendering The rendering for a view
*/
onContent(
registry: definition.ViewRegistry,
rendering: application.ViewRendering
): void;
/**
* When an error occurred
* @param description The error's description
*/
onError(description: string): void;
}
/**
* The interface for an LD browser
*/
export interface LDBrowser {
/**
* When an URI has been reached
* @param uri The URI
*/
onReachedUri(uri: string): Promise<void>;
/**
* Navigates the browser to an URI
* @param uri The URI to navigate to
*/
navigateTo(uri: string): void;
/**
* Refreshes the current page
*/
refresh(): void;
/**
* Gets the current resource
*/
getCurrentResource(): ResourceData;
/**
* Sets the new user command for the current resouce
* @param command The new user command
*/
setCommand(command: ResourceUserCommand): void;
}
/**
* Creates a new LD browser
* @param handler The handler for the events
*/
export function newBrowser(handler: LDBrowserEventHandler): LDBrowser {
return new LDBrowserImpl(handler);
}
/**
* The data of an LD browser
*/
class LDBrowserImpl implements LDBrowser {
constructor(handler: LDBrowserEventHandler) {
this.handler = handler;
this.renderer = implementation.newRenderer(getResourceContent);
this.currentResource = null;
this.cacheResources = {};
this.cacheStores = {};
this.onRequestNavigateTo = this.onRequestNavigateTo.bind(this);
this.onSelectAsPrimaryTopic = this.onSelectAsPrimaryTopic.bind(this);
}
/**
* The handler for the events
*/
private handler: LDBrowserEventHandler;
/**
* The view renderer for the browser
*/
private renderer: implementation.CachingViewRenderer;
/**
* The history of resources
*/
private currentResource: ResourceData;
/**
* The data for each known resource
*/
private cacheResources: { [url: string]: ResourceData };
/**
* The cache of loaded RDF data
*/
private cacheStores: { [url: string]: $rdf.Formula };
/**
* Resolves the data about a resource
* @param uri The resource's URI
*/
private resolveResourceData(uri: string): Promise<ResourceData> {
let self = this;
return new Promise<ResourceData>(
(
resolve: (resource: ResourceData) => void,
reject: (reason: any) => void
) => {
let resource = self.cacheResources[uri];
if (resource != null && resource != undefined) return resolve(resource);
fetchObservableAt(uri)
.then((observable: ObservableContent) => {
let observations = observeContent(observable);
let resource: ResourceData = {
observations: observations,
command: new ResourceUserCommand()
};
self.cacheResources[uri] = resource;
resolve(resource);
})
.catch((reason: any) => {
reject(reason);
});
}
);
}
/**
* Gets the RDF store for a resource
* Successively try the sources until one succeed
* @param resource The resource
*/
private resolveRdfStore(resource: ResourceData): Promise<$rdf.Formula> {
let self = this;
return new Promise(
(
resolve: (store: $rdf.Formula) => void,
reject: (reason: any) => void
) => {
let cached = self.cacheStores[resource.observations.url];
if (cached != null && cached != undefined) return resolve(cached);
let sources = selectDataSources(resource);
let index = 0;
let reasons: any[] = [];
let tryNextSource = function() {
if (index >= sources.length) return reject(reasons.toString());
self
.loadRdfStoreFrom(sources[index])
.then((store: $rdf.Formula) => {
self.cacheStores[resource.observations.url] = store;
resolve(store);
})
.catch((reason: any) => {
index++;
reasons.push(reason);
tryNextSource();
});
};
tryNextSource();
}
);
}
/**
* Loads a RDF store from a data source
* @param resource The resource
*/
private loadRdfStoreFrom(source: DataSource): Promise<$rdf.Formula> {
return new Promise(
(
resolve: (store: $rdf.Formula) => void,
reject: (reason: any) => void
) => {
fetchSource(source)
.then((value: RawContent) => {
let store = $rdf.graph();
try {
$rdf.parse(
value.content,
store,
source.url,
source.contentType,
undefined
);
resolve(store);
} catch (err) {
reject(err);
}
})
.catch(reason => {
reject(reason);
});
}
);
}
/**
* Creates a new rendering context
* @param resource The resource
* @param store The RDF store
*/
private newRenderingContext(
resource: ResourceData,
store: $rdf.Formula
): application.RenderingContext {
let topic = selectPrimaryTopic(resource);
let context: application.RenderingContext = {
event: {
onRequestNavigateTo: this.onRequestNavigateTo,
onSelectAsPrimaryTopic: this.onSelectAsPrimaryTopic
},
root: topic,
store: store,
options: {},
browserLanguage: BROWSER_LANGUAGE
};
context.options[topic] = {
view: resource.command.isAutomatic ? null : resource.command.selectedView,
language: resource.command.isAutomatic
? null
: resource.command.selectedLanguage
};
return context;
}
/**
* Executes the user request of navigating to a new resource
* @param uri The URI of the resource
*/
private onRequestNavigateTo(uri: string) {
this.navigateTo(uri);
}
/**
* Reacts to a user selecting a resource as the primary topic
* @param uri The uri of the resource
*/
private onSelectAsPrimaryTopic(uri: string): void {
if (this.currentResource == null) return;
this.currentResource.command.isAutomatic = false;
this.currentResource.command.selectedTopic = uri;
this.refresh();
}
/**
* When an URI has been reached
* @param uri The URI
*/
public async onReachedUri(uri: string): Promise<void> {
this.handler.onLoad("Loading data");
let resource = await this.resolveResourceData(uri);
this.currentResource = resource;
let store = await this.resolveRdfStore(resource);
let context = this.newRenderingContext(resource, store);
this.handler.onLoad("Loading view registry");
let registry = await getViewRegistry();
this.handler.onLoad("Fetching views");
let renderer = await this.renderer.refresh(registry);
this.handler.onLoad("Rendering");
let rendering = renderer.render(context, context.root);
if (rendering != null) {
rendering.suggestedResources.forEach((uri: string) => {
// onSuggestedResource(uri);
});
this.handler.onContent(registry, rendering);
} else {
this.handler.onError("Rendering failed!");
}
}
/**
* Navigates the browser to an URI
* @param uri The URI to navigate to
*/
public navigateTo(uri: string): void {
history.pushState(
{ uri: uri },
uri,
chrome.extension.getURL("ldbrowser/index.html?target=") +
encodeURIComponent(uri)
);
this.onReachedUri(uri);
}
/**
* Refreshes the current page
*/
public refresh(): void {
if (this.currentResource == null) return;
this.onReachedUri(this.currentResource.observations.url);
}
/**
* Gets the current resource
*/
public getCurrentResource(): ResourceData {
return this.currentResource;
}
/**
* Sets the new user command for the current resouce
* @param command The new user command
*/
public setCommand(command: ResourceUserCommand): void {
if (this.currentResource == null) return;
this.currentResource.command = command;
}
}
......@@ -19,62 +19,11 @@
******************************************************************************/
import "chrome";
import * as $rdf from "rdflib";
import { application, definition, implementation } from "@logilab/libview";
import * as LANGUAGES from "../common/data.iso639.json";
import {
ResourceObservedData,
ResourceData,
fetchSource,
selectDataSources,
RawContent,
selectPrimaryTopic,
ObservableContent,
observeContent,
ResourceUserCommand,
DataSource
} from "../common/data";
import {
getResourceContent,
getViewRegistry,
getObservationsFor,
fetchObservableAt,
getMyTabId,
Message
} from "../common/messages";
import { getMyTabId, Message } from "../common/messages";
import { Viewer, newViewer } from "./viewer";
import { LDBrowser, newBrowser } from "./browser";
require("./view-defaults-impl");
/**
* The data of an LD browser
*/
class LDBrowserData {
constructor(observation: ResourceObservedData) {
this.renderer = implementation.newRenderer(getResourceContent);
this.currentResource = new ResourceData();
this.currentResource.observations = observation;
this.currentResource.command = new ResourceUserCommand();
this.byResource = {};
this.byResource[observation.url] = this.currentResource;
this.storeCache = {};
}
/**
* The view renderer for the browser
*/
renderer: implementation.CachingViewRenderer;
/**
* The history of resources
*/
currentResource: ResourceData;
/**
* The data for each known resource
*/
byResource: { [url: string]: ResourceData };
/**
* The cache of loaded RDF data
*/
storeCache: { [url: string]: $rdf.Formula };
}
/**
* Get the value of an HTTP parameter
* @param name The name of the parameter to retrieve
......@@ -88,119 +37,14 @@ function getParameterByName(name: string) {
: decodeURIComponent(results[1].replace(/\+/g, " "));
}
/**
* Loads the target URI as a resource
* @param target The target URI
*/
function loadObservationsFor(target: string): Promise<ResourceObservedData> {
return new Promise<ResourceObservedData>(
(
resolve: (resource: ResourceObservedData) => void,
reject: (reason: any) => void
) => {
fetchObservableAt(target)
.then((obsservable: ObservableContent) => {
resolve(observeContent(obsservable));
})
.catch((reason: any) => {
reject(reason);
});
}
);
}
/**
* Gets the user language selected by the browser
*/
function getBrowserLanguage(): application.Language {
let lang = navigator.language;
let i = lang.indexOf("-");
if (i > 0) lang = lang.substring(0, i);
let result = LANGUAGES.find(
(language: application.Language) =>
language.iso639_1 == lang || language.iso639_2 == lang
);
if (result != null && result != undefined) return result;
return application.NO_LANGUAGE;
}
/**
* Loads a RDF store for a resource
* Successively try the sources until one succeed
* @param resource The resource
*/
function loadRdfStore(resource: ResourceData): Promise<$rdf.Formula> {
return new Promise(
(resolve: (store: $rdf.Formula) => void, reject: (reason: any) => void) => {
let cached = currentBrowser.storeCache[resource.observations.url];
if (cached != null && cached != undefined) {
resolve(cached);
return;
}
let sources = selectDataSources(resource);
let index = 0;
let tryNextSource = function() {
if (index >= sources.length) {
reject("No available data.");
return;
}
loadRdfStoreFrom(sources[index])
.then((store: $rdf.Formula) => {
currentBrowser.storeCache[resource.observations.url] = store;
resolve(store);
})
.catch((reason: any) => {
index++;
tryNextSource();
});
};
tryNextSource();
}
);
}
/**
* Loads a RDF store from a data source
* @param resource The resource
*/
function loadRdfStoreFrom(source: DataSource): Promise<$rdf.Formula> {
return new Promise(
(resolve: (store: $rdf.Formula) => void, reject: (reason: any) => void) => {
fetchSource(source)
.then((value: RawContent) => {
let store = $rdf.graph();
try {
$rdf.parse(
value.content,
store,
source.url,
source.contentType,
undefined
);
resolve(store);
} catch (err) {
reject(err);
}
})
.catch(reason => {
reject(reason);
});
}
);
}
/**
* The current application
*/
let currentApp: MainViewer = null;
const CURRENT_VIEWER: Viewer = newViewer();
/**
* The data for the browser
*/
let currentBrowser: LDBrowserData = null;
/**
* The current language of the browser
*/
let browserLanguage = getBrowserLanguage();
const CURRENT_BROWSER: LDBrowser = newBrowser(CURRENT_VIEWER);
/**
* Listen to messages from the background
......@@ -211,10 +55,10 @@ chrome.runtime.onMessage.addListener(function(
sendResponse
) {
if (request.requestType == "GetCurrentResource") {
sendResponse(currentBrowser.currentResource);
sendResponse(CURRENT_BROWSER.getCurrentResource());
} else if (request.requestType == "UpdateCurrentCommand") {
currentBrowser.currentResource.command = request.payload;
onReachedResource(currentBrowser.currentResource);
CURRENT_BROWSER.setCommand(request.payload);
CURRENT_BROWSER.refresh();
}
});
......@@ -222,31 +66,13 @@ chrome.runtime.onMessage.addListener(function(
* Listens to history state events from the browser
*/
window.onpopstate = function(event) {
onReachedResource(currentBrowser.byResource[event.state.url]);
CURRENT_BROWSER.onReachedUri(event.state.uri);
};
/**
* Initializes the browser
*/
function main() {
// display loading
currentApp = new MainViewer();
currentApp.onUpdate();
let target = getParameterByName("target");
let promise =
target == null || target == undefined || target == ""
? getObservationsFor(null)
: loadObservationsFor(target);
promise
.then((observation: ResourceObservedData) => {
currentBrowser = new LDBrowserData(observation);
onNavigatedTo(currentBrowser.currentResource);