Commit a668b969 authored by Tobias Steiner's avatar Tobias Steiner
Browse files

Initial commit

parents
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
.idea/
/dist
cache:
paths:
- node_modules/
build:
image: node:10
stage: build
script:
- npm install --progress=false
- npm run bundle
artifacts:
expire_in: 1 week
paths:
- dist
FROM node:8-alpine
# add basic libs
RUN apk --no-cache add \
bash \
g++ \
ca-certificates \
lz4-dev \
musl-dev \
cyrus-sasl-dev \
openssl-dev \
make \
python \
bash \
git
RUN apk add --no-cache --virtual .build-deps gcc zlib-dev libc-dev bsd-compat-headers py-setuptools libexecinfo libexecinfo-dev
# Create app directory
RUN mkdir -p /usr/local/app
# Move to the app directory
WORKDIR /usr/local/app
# copy app to the container
COPY package.json package-lock.json config.json tsconfig.json /usr/local/app/
COPY src /usr/local/app/src
# Install dependencies
RUN npm install
# build stuff
RUN npm run build
# run app
CMD node dist/index.js
\ No newline at end of file
# Geolinker widget
This is a small JS-client to consume data from the geolinker.histhub.ch. The query build helps to build specific queries and with a small mustache template you can customize the look and feel of the widget.
## Todo / Future
* The plan is to publish the widget as a standalone web-component. So the code will be better capsulated and we can use angular 7 with ivy-compiler to pave this.
* Remove jquery dependency
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="../dist/bundle.js"></script>
</head>
<body>
<div id="geolinkerWidget" data-url="https://dodis.ch/G8" data-endpoint="sameas" data-api-version="v1" data-language="fr">Loading widget...</div>
</body>
</html>
<div class="geolinker-widget">
<h2>{{title}}</h2>
<img src="{{image}}"/>
<p>{{description}}</p>
<ul>
{{#links}}
<li>{{link}}</li>
{{/links}}
</ul>
</div>
\ No newline at end of file
This diff is collapsed.
{
"name": "geolinker-widget",
"version": "1.0.0",
"description": "Plugin to display structured content from histhub geolinker",
"dependencies": {
"@types/jquery": "^3.3.10",
"@types/mustache": "^0.8.31",
"jquery": "^3.3.1",
"mustache": "^3.0.0",
"nconf": "^0.10.0",
"url-parse": "^1.4.3"
},
"devDependencies": {
"@types/chai": "^4.1.6",
"@types/mocha": "^5.2.5",
"@types/node": "^10.11.4",
"@types/sinon": "^5.0.3",
"chai": "^4.2.0",
"concurrently": "^3.5.1",
"jsdom": "^12.1.0",
"mocha": "^5.2.0",
"mock-require": "^3.0.2",
"sinon": "^6.3.5",
"source-map-support": "^0.5.6",
"ts-loader": "^5.2.1",
"ts-node": "^7.0.1",
"tslint": "^5.10.0",
"typescript": "^2.9.2",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.2",
"webpack-merge": "^4.2.1"
},
"scripts": {
"test": "node_modules/.bin/mocha -r ts-node/register tests/**/*.spec.ts",
"lint": "node_modules/.bin/tslint -c tslint.json 'src/**/*.ts'",
"build": "node_modules/.bin/tsc",
"watch": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run watch-node\"",
"watch-node": "nodemon dist/linker-consumer.js",
"watch-ts": "tsc -w",
"start": "npx webpack --config webpack.config.js",
"bundle": "npx webpack --config webpack.prod.js"
},
"author": "",
"license": "ISC"
}
import * as $ from 'jquery';
import jqXHR = JQuery.jqXHR;
import {InterfaceViewOptions} from './view';
/**
* Data from the geolinker service
*/
export interface InterfaceGeolinkerResponse {
id: string;
data: {
resolverneo4j?: InterfaceGeolinkerData,
resolverelasticsearch?: InterfaceGeolinkerData,
}
comment: string;
}
export interface InterfaceGeolinkerData {
links: string[];
precision: string;
}
/**
* Params for the request are key: value pair
*/
export interface InterfaceParamOption {
[key: string]: string|number;
}
const endpointMap = {
sameas: 'resolverneo4j',
similarto: 'revolverelasticsearch',
};
export class Client {
/**
* Timeout for the fetcher
* @type {number}
*/
private readonly timeout: number = 10000;
/**
* The url of the server
* @type {string}
*/
private readonly apiBaseUrl: string = 'https://api.geolinker.histhub.ch';
/**
* The version of the api
*/
private version: string = 'v1';
/**
* The endpoint
*/
private endpoint: string = 'sameas';
constructor(options){
const defaultOptions = {
timeout: 1000,
version: 'v1',
url: 'https://api.geolinker.histhub.ch',
};
// merge default options and options
options = {...defaultOptions, ...options};
this.timeout = options.timeout;
this.apiBaseUrl = options.url;
this.version = options.version;
}
/**
* Load data from the service
* @param {string} endpoint
* @param {string} url
* @returns {Promise<string[]>}
*/
public async get(url?: string, endpoint: string = 'sameas'): Promise<string[]> {
// if no url is defined guess the current url
if(typeof url === 'undefined') {
url = Client.getUrl();
}
// apply url to the server url
return $.ajax(this.getApiUrl(url, endpoint, [{depth:2}]), {
data: {timeout: this.timeout},
method: 'get',
mimeType: 'application/json',
timeout: this.timeout + 1000})
.then((data) => {
return data.data[endpointMap[endpoint]].links;
})
}
/**
* Build the api url
* @param {string} targetUri
* @param {string} endpoint
* @param {InterfaceParamOption[]} paramOptions
*/
private getApiUrl(targetUri, endpoint?: string, paramOptions?: InterfaceParamOption[]) {
// encode the url
targetUri = encodeURI(targetUri);
// build the parameter string
let paramString = '';
if(typeof paramOptions !== 'undefined' && paramOptions.length > 0){
paramString = paramOptions.map((p) => {
let key = Object.keys(p)[0];
return `${key}=${p[key]}`;
}).reduce((a, c) => {
return `${a}&${c}`
});
}
// check endpoint
if(typeof endpoint == 'undefined') {
endpoint = this.endpoint;
}
// build target url
return `${this.apiBaseUrl}/${this.version}/${endpoint}/${targetUri}?${paramString}`;
}
/**
* Get the url of the current document
* @returns {string}
*/
private static getUrl(): string {
// todo normalisation
return window.location.href;
}
}
import * as jQuery from 'jquery';
import {Widget} from './widget';
jQuery(($) => {
const template = '<div class="geolinker-widget">\n' +
' <h2>Histhub Geolinker</h2>\n' +
' <ul>' +
'{{#data}}<li><a href="{{url}}">{{provider}}</a></li>{{/data}}</ul>\n' +
'</div>';
/**
* helper function to be sure the language first letter is uppercae
* @param string
*/
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
// configuration
const targetName = '#geolinkerWidget';
const element = $(targetName);
/**
* Provide some view options
*/
const viewOptions = {
template,
templateUrl: null,
language: capitalizeFirstLetter(element.data('language')) || 'De',
target: targetName
};
/**
* Add some client options
*/
const clientOptions = {
timeout: element.data('timeout') ||5000,
url: element.data('api-url') || 'http://api.geolinker.histhub.ch',
version: element.data('api-version') || 'v1',
endpoint: element.data('endpoint') ||'sameas',
};
const url = element.data('url');
Widget.init(url, viewOptions, clientOptions);
});
const providers = JSON.parse('[{"host":"www.wikidata.org","nameDe":"wikidata","nameFr":"wikidata","nameIt":"wikidata"},{"host":"urn.bn.pt","nameDe":"PTBNP","nameFr":"identifiant Bibliothèque nationale du Portugal","nameIt":"identificativo PTBNP"},{"host":"viaf.org","nameDe":"BAV (Vatikan) ID","nameFr":"identifiant Bibliothèque apostolique vaticane","nameIt":"identificativo BAV"},{"host":"catalog.archives.gov","nameDe":"NARA-ID","nameFr":"identifiant National Archives and Records Administration","nameIt":"identificativo NARA"},{"host":"www.helveticarchives.ch","nameDe":"Helveticarchives ID","nameFr":"identifiant Helveticarchives","nameIt":"identificativo Helveticarchives"},{"host":"www.flickr.com","nameDe":"WOEID","nameFr":"identifiant Where On Earth","nameIt":"identificativo Where On Earth"},{"host":"www.britannica.com","nameDe":"Encyclopædia Britannica Online-ID","nameFr":"identifiant Encyclopædia Britannica en ligne","nameIt":"identificativo Encyclopædia Britannica Online"},{"host":"geonames.org","nameDe":"GeoNames-ID","nameFr":"identifiant GeoNames","nameIt":"identificativo GeoNames"},{"host":"vocab.getty.edu","nameDe":"TGN-ID","nameFr":"identifiant Thesaurus of Geographic Names","nameIt":"identificativo TGN"},{"host":"unlocode.rkbexplorer.com","nameDe":"UN/LOCODE","nameFr":"UN/LOCODE","nameIt":"UN/LOCODE"},{"host":"tools.wmflabs.org","nameDe":"ISNI","nameFr":"identifiant ISNI","nameIt":"identificativo ISNI"},{"host":"viaf.org","nameDe":"VIAF","nameFr":"identifiant Fichier d\'autorité international virtuel","nameIt":"identificativo VIAF"},{"host":"id.worldcat.org","nameDe":"FAST ID","nameFr":"identifiant FAST","nameIt":"identificativo FAST"},{"host":"d-nb.info","nameDe":"GND","nameFr":"identifiant Gemeinsame Normdatei","nameIt":"identificativo GND"},{"host":"id.loc.gov","nameDe":"LCAuth","nameFr":"identifiant bibliothèque du Congrès","nameIt":"identificativo LCNAF"},{"host":"babelnet.org","nameDe":"BabelNet-ID","nameFr":"identifiant BabelNet","nameIt":"identificativo BabelNet"},{"host":"catalogue.bnf.fr","nameDe":"BnF-ID","nameFr":"identifiant Bibliothèque nationale de France","nameIt":"identificativo BNF"},{"host":"www.idref.fr","nameDe":"SUDOC-Normdaten","nameFr":"identifiant IdRef","nameIt":"identificativo SUDOC"},{"host":"bigenc.ru","nameDe":"ID der Großen Russischen Enzyklopädie","nameFr":"identifiant de la Grande Encyclopédie russe en ligne","nameIt":"identificativo Grande Enciclopedia Russa"},{"host":"www.universalis.fr","nameDe":null,"nameFr":"identifiant Encyclopædia Universalis en ligne","nameIt":"identificativo Encyclopædia Universalis"},{"host":"www.newadvent.org","nameDe":null,"nameFr":"identifiant Catholic Encyclopedia","nameIt":"identificativo Catholic Encyclopedia"},{"host":"www.quora.com","nameDe":"Quora-Themenkennung","nameFr":"identifiant Quora d\'un sujet","nameIt":"identificativo argomento Quora"},{"host":"www.unifrance.org","nameDe":"Unifrance Film-ID","nameFr":"identifiant Unifrance d\'un film","nameIt":null},{"host":"www.openstreetmap.org","nameDe":"OpenStreetMap-Relations-ID","nameFr":"identifiant de relation OpenStreetMap","nameIt":"identificativo relazione OpenStreetMap"},{"host":"nla.gov.au","nameDe":"NLA Authorities","nameFr":"identifiant Bibliothèque nationale d\'Australie","nameIt":"identificativo NLA"},{"host":"ark.frantiq.fr","nameDe":null,"nameFr":"identifiant PACTOLS","nameIt":"identificativo PACTOLS"},{"host":"id.agrisemantics.org","nameDe":null,"nameFr":"identifiant Global Agricultural Concept Scheme","nameIt":null},{"host":"emlo.bodleian.ox.ac.uk","nameDe":null,"nameFr":"identifiant Early Modern Letters Online de lieu","nameIt":null},{"host":"brockhaus.de","nameDe":null,"nameFr":"identifiant Brockhaus Enzyklopädie","nameIt":null},{"host":"dd.eionet.europa.eu","nameDe":"NUTS","nameFr":"identifiant NUTS","nameIt":"codice NUTS"},{"host":"g.co","nameDe":"Freebase-Kennung","nameFr":"identifiant Freebase","nameIt":"identificativo Freebase"},{"host":"openlibrary.org","nameDe":"Open-Library-ID","nameFr":"identifiant Open Library","nameIt":"identificativo Open Library"},{"host":"www.hls-dhs-dss.ch","nameDe":"Artikelnummer im HLS","nameFr":"identifiant Dictionnaire historique de la Suisse","nameIt":"identificativo DSS"},{"host":"libris.kb.se","nameDe":"SELIBR","nameFr":"identifiant SELIBR","nameIt":"identificativo SELIBR"},{"host":"aleph.nli.org.il","nameDe":"NLI (Israel)","nameFr":"identifiant Bibliothèque nationale d\'Israël","nameIt":"identificativo NLI"},{"host":"datos.bne.es","nameDe":"BNE-ID","nameFr":"identifiant Bibliothèque nationale d\'Espagne","nameIt":"identificativo BNE"},{"host":"curlie.org","nameDe":"dmoz","nameFr":"dmoz","nameIt":"identificativo dmoz"},{"host":"dodis.ch","nameDe":"Dodis","nameFr":"Dodis","nameIt":"Dodis"},{"host":"www.ssrq-sds-fds.ch","nameDe":"Sammlung Schweizerischer Rechtsquellen (ssrq)","nameFr":"Collection des sources du droit suisse (sds)","nameIt":"Fonti del diritto svizzero (FDS)"},{"host":"www.hls-dhs-dss.ch","nameDe":"Historisches Lexikon der Schweiz","nameFr":"Dictionnaire historique de la Suisse","nameIt":"Dizionario storico della Svizzera"},{"host":"search.ortsnamen.ch","nameDe":"Ortsnamen","nameFr":"Ortsnamen","nameIt":"Ortsnamen"}]');
export default providers;
import {InterfaceGeolinkerData, InterfaceGeolinkerResponse} from './client';
import * as Mustache from 'mustache';
import * as $ from "jquery";
import providers from './providers';
export interface InterfaceViewOptions {
target?: string;
templateUrl?: string;
template?: string;
language?: string;
}
export interface InterfaceViewData {
provider: string;
url: string;
}
export class View {
options: InterfaceViewOptions;
constructor( options?: InterfaceViewOptions) {
const defaultOptions: InterfaceViewOptions = {
target: '#geolinkerWidget',
templateUrl: 'widget-template.mst',
template: null,
language: 'De',
};
// merge default options and options
this.options = {...defaultOptions, ...options};
}
/**
* Render the configured data into the template
* @param {InterfaceGeolinkerResponse} data
* @returns {Promise<void>}
*/
async render(data: string[]) {
const viewData = {data: []};
viewData.data = this.mapProviders(data);
console.log(viewData);
if(this.options.templateUrl !== null) {
$.get(this.options.templateUrl, (template) => {
const rendered = Mustache.render(template, viewData);
$(this.options.target).html(rendered);
});
} else if (this.options.template !== null) {
const rendered = Mustache.render(this.options.template, viewData);
$(this.options.target).html(rendered);
}
}
mapProviders(data: string[]): InterfaceViewData[] {
return data.map((d) => {
const url = new URL(d);
const provider = providers.find((provider) => {
return provider.host === url.host
});
return {
provider: provider[`name${this.options.language}`],
url: d,
};
}).filter((d) => {
return d.provider !== '' && d.provider !== null && typeof d.provider !== 'undefined';
});
}
}
import {Client, InterfaceGeolinkerData, InterfaceGeolinkerResponse} from './client';
import {InterfaceViewOptions, View} from './view';
import $ = require( 'jquery');
export class Widget {
static init(url?: string, viewOptions: InterfaceViewOptions = {}, clientOptions: any = {}) {
const fetcher = new Client(clientOptions);
fetcher.get(url).then(async (data: string[]) => {
const view = new View( viewOptions);
view.render(data).then(() => {
}, (error) => {
$.error(error);
});
},() =>{
$.error('Could not display the geolinker widget.');
});
}
}
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "dist",
"inlineSourceMap": true,
"declaration": false,
"typeRoots": [
"node_modules/@types"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"quotemark": [
true,
"single"
],
"object-literal-sort-keys": false,
"ordered-imports": false
},
"rulesDirectory": []
}
\ No newline at end of file
const path = require('path');
module.exports = {
entry: './src/index.ts',
watchOptions: {
aggregateTimeout: 300,
poll: 500
},
watch: true,
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
const merge = require('webpack-merge');
const common = require('./webpack.config.js');
module.exports = merge(common, {
mode: 'production',
watch: false,
});
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment