Resolve language links from relative sites

This commit is contained in:
Florine W. Dekker 2020-04-09 16:15:16 +02:00
parent c8d4925841
commit 89a7224816
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
1 changed files with 135 additions and 75 deletions

View File

@ -70,8 +70,14 @@
<script src="https://static.fwdekker.com/js/common.js" crossorigin="anonymous"></script>
<script src="MediawikiJS.js" crossorigin="anonymous"></script>
<script>
"use strict";
/**
* A data class for combining a language and page title to identify a page.
*
* @property lang {string} the language of the wiki this page is of
* @property title {string} the title of the page
*/
class InterlangLink {
/**
@ -117,10 +123,67 @@
}
}
/**
* A map of interwiki links.
*
* @property map {Object.<string, string>} maps interwiki abbreviations to URLs
*/
class InterwikiMap {
/**
* Constructs a new interwiki map.
*
* @param map {Object.<string, string>} the mapping from interwiki abbreviations to URLs to store in this map
*/
constructor(map) {
this.map = Object.assign({}, map);
}
/**
* Returns the URL for the given abbreviation, or `undefined` if the abbreviation could not be found.
*
* @param abbr {string} the abbreviation to return the URL of
* @returns {string} the URL for the given abbreviation, or `undefined` if the abbreviation could not be found
*/
getUrl(abbr) {
return this.map[abbr];
}
/**
* Returns `true` if and only if this map has a URL for the given abbreviation.
*
* @param abbr {string} the abbreviation to check for
* @returns {boolean} `true` if and only if this map has a URL for the given abbreviation
*/
hasUrl(abbr) {
return this.map[abbr] !== undefined;
}
/**
* Returns a new `InterwikiMap` with all entries from both this map and the given map.
*
* @param other {InterwikiMap} the map to merge with
* @return a new `InterwikiMap` with all entries from both this map and the given map
*/
mergeWith(other) {
const conflicts = Object.keys(this.map)
.filter(key => other.map.hasOwnProperty(key))
.filter(key => this.map[key] !== other.map[key]);
if (conflicts.length !== 0)
console.warn("iw map merge conflict(s)", conflicts);
return new InterwikiMap({...this.map, ...other.map});
}
}
/**
* Interacts with the API in an asynchronous manner.
*
* Wraps around the MediawikiJS library.
*
* @property baseUrl {string} the origin of the wiki's API
* @property apiPath {string} the path relative to the wiki's API
*/
class MediaWiki {
/**
@ -130,7 +193,9 @@
*/
constructor(baseUrl) {
const urlObj = new URL(baseUrl);
this.mwjs = MediaWikiJS({baseURL: urlObj.origin, apiPath: urlObj.pathname});
this._mwjs = MediaWikiJS({baseURL: urlObj.origin, apiPath: urlObj.pathname});
this.baseUrl = this._mwjs.baseURL;
this.apiPath = this._mwjs.apiPath;
}
@ -141,7 +206,8 @@
* @return {Promise<Object>} the API's response
*/
request(params) {
return new Promise(resolve => this.mwjs.send(params, it => resolve(it)));
console.debug(`Requesting from ${this.baseUrl}/${this.apiPath} with param`, params);
return new Promise(resolve => this._mwjs.send(params, it => resolve(it)));
}
/**
@ -170,7 +236,7 @@
/**
* Requests all interwiki abbreviations available on this wiki.
*
* @return {Promise<Object.<string, string>>} a mapping from
* @return {Promise<InterwikiMap>} a mapping from
*/
getIwMap() {
return this
@ -180,40 +246,61 @@
map[mapping["prefix"]] = mapping["url"];
return map;
}, {})
);
)
.then(map => new InterwikiMap(map));
}
}
/**
* Manages a `MediaWiki` instance for different languages, caching retrieved information for re-use.
*
* @property mws {Object.<string, MediaWiki>} the cached `MediaWiki` instances
*/
class MediaWikiManager {
/**
* Constructs a new `MediaWikiManager`.
*
* The `#init` method **must** be called before invoking any other function. Behavior is undefined otherwise.
*/
constructor() {
this.mws = {};
this.iwMaps = {};
this._iwMap = new InterwikiMap({});
}
/**
* Stores the given `MediaWiki` in this manager.
* Initializes this `MediaWikiManager`.
*
* @param lang {string} the language of the `MediaWiki`
* @param mw {MediaWiki} the `MediaWiki` to store
* @param baseLang {string} the language for the `MediaWiki` that is used as a starting point
* @param baseMw {MediaWiki} the `MediaWiki` that is used as a starting point
* @return {MediaWikiManager} this `MediaWikiManager`
*/
addMw(lang, mw) {
this.mws[lang] = mw;
async init(baseLang, baseMw) {
this.apiPath = baseMw.apiPath;
this.mws[baseLang] = baseMw;
await this._importIwMap(baseMw);
return this;
}
/**
* Returns the `MediaWiki` for the given language, or `undefined` if it has not been stored.
* Returns the `MediaWiki` for the given language, creating it if necessary, or `undefined` if it it could not
* be created.
*
* @param lang {string} the language of the `MediaWiki` to return
* @returns {MediaWiki} the `MediaWiki` for the given language, or `undefined` if it has not been stored
* @returns {MediaWiki} the `MediaWiki` for the given language, or `undefined` if it could not be created
*/
getMw(lang) {
async getMw(lang) {
if (this.hasMw(lang))
return this.mws[lang];
if (!this._iwMap.hasUrl(lang))
return undefined;
const url = this._iwMap.getUrl(lang);
this.mws[lang] = new MediaWiki(url.slice(0, -7) + "/" + this.apiPath);
await this._importIwMap(this.mws[lang]);
return this.mws[lang];
}
@ -224,33 +311,17 @@
* @returns {boolean} `true` if and only if this manager has a `MediaWiki` for the given language
*/
hasMw(lang) {
return this.getMw(lang) !== undefined;
return this.mws[lang] !== undefined;
}
/**
* Returns the interwiki map for the given language, fetching it from the server if necessary.
* Imports the interwiki map from the given wiki, fetching it from the server and merging it into this manager's
* main interwiki map.
*
* @param lang {string} the language to return the interwiki map for
* @return {Promise<Object.<string, number>>} the interwiki map for the given language
* @param mw {MediaWiki} the `MediaWiki` to import the interwiki map of
*/
getIwMap(lang) {
if (!this.hasMw(lang))
return null;
if (!this.hasIwMap(lang))
this.iwMaps[lang] = this.getMw(lang).getIwMap();
return this.iwMaps[lang];
}
/**
* Returns `true` if and only if this manager has an interwiki table for the given language.
*
* @param lang {string} the language of the interwiki table to check presence of
* @returns {boolean} `true` if and only if this manager has an interwiki table for the given language
*/
hasIwMap(lang) {
return this.iwMaps[lang] !== undefined;
async _importIwMap(mw) {
this._iwMap = this._iwMap.mergeWith(await mw.getIwMap());
}
}
@ -261,65 +332,54 @@
/**
* Constructs a new `InterlangNetwork`.
*
* @param mappings {Object.<InterlangLink, InterlangLink[]>} a mapping from source page to all pages it links to
* @param mappings {Object.<string, InterlangLink[]>} a mapping from source page to all pages it links to
*/
constructor(mappings) {
this.mappings = mappings || {};
}
static async discoverNetwork(mw, title) {
static async discoverNetwork(mwm, link) {
const mappings = {};
const mwm = new MediaWikiManager();
mwm.addMw("en", mw); // TODO Support non-English `mw`
return mwm.getIwMap("en").then(async map => {
const entries = [];
const history = [];
const queue = [link];
while (queue.length > 0) {
const next = queue.pop();
if (history.some(it => it.equals(next)))
continue;
const queue = [new InterlangLink("en", title)];
while (queue.length > 0) {
const next = queue.pop();
if (entries.some(it => it.equals(next)))
continue;
history.push(next);
const nextMw = await mwm.getMw(next.lang);
await nextMw
.getLangLinks([next.title], undefined)
.then(langlinks => langlinks[next.title])
.then(langlinks => {
if (langlinks === undefined)
return;
entries.push(next);
if (!mwm.hasMw(next.lang)) {
if (!map.hasOwnProperty(next.lang))
continue;
mappings[next.toString()] = langlinks;
queue.push(...langlinks);
});
}
mwm.addMw(next.lang, new MediaWiki(map[next.lang].slice(0, -7) + "/api.php"));
}
const nextMw = mwm.getMw(next.lang);
await nextMw.getLangLinks([next.title], undefined)
.then(langlinks => langlinks[next.title])
.then(langlinks => {
if (langlinks === undefined)
return;
mappings[next.toString()] = langlinks;
queue.push(...(langlinks));
});
}
return new InterlangNetwork(mappings);
});
return new InterlangNetwork(mappings);
}
}
const url = "https://fallout.fandom.com/api.php";
const mw = new MediaWiki(url);
InterlangNetwork.discoverNetwork(mw, "Felix").then(it => console.log(it));
// noinspection JSUnresolvedFunction Defined in `common.js`
doAfterLoad(() => {
doAfterLoad(async () => {
const url = "https://fallout.fandom.com/api.php";
const mw = new MediaWiki(url);
const mwm = await new MediaWikiManager().init("en", mw);
const pageInput = $("#page");
const checkButton = $("#check");
const submit = () => {
window.alert("SUBMIT");
InterlangNetwork.discoverNetwork(mwm, new InterlangLink("en", pageInput.value))
.then(it => console.log(it));
};
pageInput.addEventListener("keypress", (event) => {