Use page and link verdict enums
These centralise the knowledge and behaviour of these enums into a clear location, so that other classes don't have to deal with that every time. Fixes #47.
This commit is contained in:
parent
a8c05f62eb
commit
c52f690e6d
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "interlanguage-checker",
|
||||
"version": "1.13.0",
|
||||
"version": "1.13.1",
|
||||
"description": "Check the consistency of MediaWiki interlanguage links in a simple overview.",
|
||||
"author": "Felix W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-ignore
|
||||
const {stringToHtml} = window.fwdekker;
|
||||
import {InterlangNetwork, Page} from "./MediaWiki";
|
||||
import {InterlangNetwork, LinkVerdict, Page, PageVerdict} from "./MediaWiki";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -93,7 +93,7 @@ export class ValidatableInput {
|
|||
/**
|
||||
* The types of error that can be displayed by an `ErrorHandler`.
|
||||
*/
|
||||
type ErrorLevel = "warning" | "error" | null;
|
||||
export type ErrorLevel = "warning" | "error" | null;
|
||||
|
||||
/**
|
||||
* Interacts with the DOM to delegate errors to the user.
|
||||
|
@ -154,7 +154,7 @@ export class ErrorHandler {
|
|||
/**
|
||||
* The types of message that can be displayed by a `MessageHandler`.
|
||||
*/
|
||||
type MessageLevel = "complete" | "progress" | "warning" | "error" | "neutral" | null;
|
||||
export type MessageLevel = "complete" | "progress" | "warning" | "error" | "neutral" | null;
|
||||
|
||||
/**
|
||||
* Interacts with the DOM to delegate messages to the user.
|
||||
|
@ -344,13 +344,15 @@ export class InterlangTable {
|
|||
/**
|
||||
* Generates an icon element with the given title and additional classes.
|
||||
*
|
||||
* @param icon the name of the icon to display
|
||||
* @param icon the name of the icon to display, or `null` if an empty span should be returned
|
||||
* @param title the title of the icon, used for the `title` attribute
|
||||
* @param classes the additional classes to apply to the icon
|
||||
* @return an icon element with the given title and additional classes
|
||||
* @private
|
||||
*/
|
||||
private static createIcon(icon: string, title: string, classes: string[]): string {
|
||||
private static createIcon(icon: string | null, title: string, classes: string[]): string {
|
||||
if (icon === null) return `<span></span>`;
|
||||
|
||||
return `<i class="fa fa-${icon} ${(classes || []).join(" ")}" title="${title}"></i>`;
|
||||
}
|
||||
|
||||
|
@ -409,51 +411,19 @@ export class InterlangTable {
|
|||
*/
|
||||
private generateTableBody(network: InterlangNetwork): string {
|
||||
const rows = network.pages.map(srcPage => {
|
||||
const verdict = network.getPageVerdict(srcPage);
|
||||
const {self: selfVerdict, links: linkVerdicts} = network.getPageVerdict(srcPage);
|
||||
|
||||
const icons = verdict.self
|
||||
const icons = selfVerdict
|
||||
.map(state => {
|
||||
switch (state) {
|
||||
case "perfect":
|
||||
return InterlangTable.createIcon("check", "Perfect 🙂", ["success"]);
|
||||
case "not-found":
|
||||
return InterlangTable.createIcon("search", "Article does not exist 😕", ["error"]);
|
||||
case "wrongly-ordered":
|
||||
return InterlangTable.createIcon("sort-alpha-asc", "Links are in the wrong order 😕", ["warning"]);
|
||||
case "doubly-linked":
|
||||
return InterlangTable.createIcon("clone", "Links to the same wiki multiple times 😕", ["warning"]);
|
||||
case "self-linked":
|
||||
return InterlangTable.createIcon("rotate-left", "Links to its own wiki 😕", ["warning"]);
|
||||
case "unlinked":
|
||||
return InterlangTable.createIcon("chain-broken", "Misses one or more links 😕", ["error"]);
|
||||
case "redirected":
|
||||
return InterlangTable.createIcon("mail-forward", "Links to a redirect 😕", ["warning"]);
|
||||
case "wrongly-cased":
|
||||
return InterlangTable.createIcon("text-height", "Links with incorrect capitalisation 😕", ["warning"]);
|
||||
default:
|
||||
throw new Error(`Invalid page state '${state}'`);
|
||||
}
|
||||
const props = PageVerdict.props[state];
|
||||
return InterlangTable.createIcon(props.icon, props.message, props.style);
|
||||
})
|
||||
.map(it => `${it}<span> </span>`);
|
||||
const label = this.generateLabel(network.pages, srcPage);
|
||||
const cells = network.pages.map(dstPage => {
|
||||
const linkState = verdict.pages.find(it => it.page.link.equals(dstPage.link))!.verdict;
|
||||
switch (linkState) {
|
||||
case "linked":
|
||||
return InterlangTable.createIcon("check", "Linked 🙂", ["success"]);
|
||||
case "self-linked":
|
||||
return InterlangTable.createIcon("rotate-left", "Links to its own wiki 😕", ["warning"]);
|
||||
case "unlinked":
|
||||
return InterlangTable.createIcon("times", "Link is missing 😕", ["error"]);
|
||||
case "self-unlinked":
|
||||
return `<span></span>`;
|
||||
case "redirected":
|
||||
return InterlangTable.createIcon("mail-forward", "Links to a redirect 😕", ["warning"]);
|
||||
case "wrongly-cased":
|
||||
return InterlangTable.createIcon("text-height", "Links with incorrect capitalisation 😕", ["warning"]);
|
||||
default:
|
||||
throw new Error(`Invalid link state '${linkState}'`);
|
||||
}
|
||||
const linkState = linkVerdicts.get(dstPage.link)!;
|
||||
const props = LinkVerdict.props[linkState];
|
||||
return InterlangTable.createIcon(props.icon, props.message, props.style);
|
||||
});
|
||||
|
||||
return "" +
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-ignore
|
||||
const {$, doAfterLoad, footer, header, nav} = window.fwdekker;
|
||||
import {ErrorHandler, InterlangTable, MessageHandler, ValidatableInput} from "./DOM";
|
||||
import {discoverNetwork, InterlangNetwork, MediaWiki, MediaWikiManager} from "./MediaWiki";
|
||||
import {discoverNetwork, InterlangNetwork, MediaWiki, MediaWikiManager, NetworkVerdict} from "./MediaWiki";
|
||||
|
||||
|
||||
// Contains global functions for debugging
|
||||
|
@ -121,17 +121,8 @@ doAfterLoad(async () => {
|
|||
form.textContent = "";
|
||||
form.appendChild((new InterlangTable()).render("networkTable", network));
|
||||
|
||||
switch (network.getNetworkVerdict()) {
|
||||
case "perfect":
|
||||
messageHandler.handle("complete", "A perfect network! 🙂");
|
||||
break;
|
||||
case "broken":
|
||||
messageHandler.handle("warning", "The network is broken 😞<br />Hover over an icon in the left column for more information.");
|
||||
break;
|
||||
case "flawed":
|
||||
messageHandler.handle("warning", "The network is complete but flawed 😕<br />Hover over an icon in the left column for more information.");
|
||||
break;
|
||||
}
|
||||
const props = NetworkVerdict.props[network.getNetworkVerdict()];
|
||||
messageHandler.handle(props.style, props.message);
|
||||
})
|
||||
.catch(error => messageHandler.handle("error", error));
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {couldNotConnectMessage, mergeStates} from "./Shared";
|
||||
import {MessageLevel} from "./DOM";
|
||||
import {couldNotConnectMessage, mergeMaps, mergeSets} from "./Shared";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -253,29 +254,24 @@ export class InterlangNetwork {
|
|||
* @param srcPage the page to give a verdict of
|
||||
* @return the checker's verdicts of the page and its outgoing links
|
||||
*/
|
||||
getPageVerdict(srcPage: Page): { self: PageVerdict[], pages: { page: Page, verdict: LinkVerdict }[] } {
|
||||
const pageStates = this.pages.map(dstPage => ({page: dstPage, verdict: this.getLinkVerdict(srcPage, dstPage)}));
|
||||
getPageVerdict(srcPage: Page): { self: PageVerdict[], links: Map<InterlangLink, LinkVerdict> } {
|
||||
const linkVerdicts =
|
||||
new Map(this.pages.map(dstPage => ([dstPage.link, this.getLinkVerdict(srcPage, dstPage)])));
|
||||
const foundVerdicts =
|
||||
new Set([...linkVerdicts.values()]);
|
||||
|
||||
let selfStates: PageVerdict[] = [];
|
||||
if (!srcPage.exists)
|
||||
selfStates.push("not-found");
|
||||
if (!srcPage.langLinksAreOrdered())
|
||||
selfStates.push("wrongly-ordered");
|
||||
if (srcPage.hasDoubleLinks())
|
||||
selfStates.push("doubly-linked");
|
||||
if (pageStates.some(({verdict}) => verdict === "self-linked"))
|
||||
selfStates.push("self-linked");
|
||||
if (pageStates.some(({verdict}) => verdict === "unlinked"))
|
||||
selfStates.push("unlinked");
|
||||
if (pageStates.some(({verdict}) => verdict === "redirected"))
|
||||
selfStates.push("redirected");
|
||||
if (pageStates.some(({verdict}) => verdict === "wrongly-cased"))
|
||||
selfStates.push("wrongly-cased");
|
||||
let selfVerdicts: PageVerdict[] = [];
|
||||
if (!srcPage.exists) selfVerdicts.push("not-found");
|
||||
if (!srcPage.langLinksAreOrdered()) selfVerdicts.push("wrongly-ordered");
|
||||
if (srcPage.hasDoubleLinks()) selfVerdicts.push("doubly-linked");
|
||||
if (foundVerdicts.has("self-linked")) selfVerdicts.push("self-linked");
|
||||
if (foundVerdicts.has("unlinked")) selfVerdicts.push("unlinked");
|
||||
if (foundVerdicts.has("redirected")) selfVerdicts.push("redirected");
|
||||
if (foundVerdicts.has("wrongly-cased")) selfVerdicts.push("wrongly-cased");
|
||||
|
||||
if (selfStates.length === 0)
|
||||
selfStates.push("perfect");
|
||||
if (selfVerdicts.length === 0) selfVerdicts.push("perfect");
|
||||
|
||||
return {self: selfStates, pages: pageStates};
|
||||
return {self: selfVerdicts, links: linkVerdicts};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -284,18 +280,11 @@ export class InterlangNetwork {
|
|||
* @return a verdict on the network
|
||||
*/
|
||||
getNetworkVerdict(): NetworkVerdict {
|
||||
const states: NetworkVerdict[] = ["broken", "flawed", "perfect"];
|
||||
return this.pages.reduce(
|
||||
(state: NetworkVerdict, page: Page) => {
|
||||
const verdict = this.getPageVerdict(page).self;
|
||||
if (verdict.some(it => ["not-found", "unlinked"].includes(it)))
|
||||
return mergeStates<NetworkVerdict>(states, state, "broken");
|
||||
if (verdict.some(it => ["wrongly-ordered", "doubly-linked", "self-linked", "redirected", "wrongly-cased"].includes(it)))
|
||||
return mergeStates<NetworkVerdict>(states, state, "flawed");
|
||||
return mergeStates<NetworkVerdict>(states, state, "perfect");
|
||||
},
|
||||
"perfect"
|
||||
);
|
||||
const verdicts = [...mergeSets(this.pages.map(page => new Set(this.getPageVerdict(page).self)))];
|
||||
|
||||
if (verdicts.some(verdict => NetworkVerdict.brokenVerdicts.includes(verdict))) return "broken";
|
||||
if (verdicts.some(verdict => NetworkVerdict.flawedVerdicts.includes(verdict))) return "flawed";
|
||||
return "perfect";
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -609,10 +598,7 @@ export class MediaWikiManager {
|
|||
* @private
|
||||
*/
|
||||
private updateIwMap(): void {
|
||||
this.iwMap =
|
||||
[...this.mws.values()]
|
||||
.map(mw => mw.interwikiMap)
|
||||
.reduce((combined, map) => new Map([...combined, ...map]), new Map());
|
||||
this.iwMap = mergeMaps([...this.mws.values()].map(mw => mw.interwikiMap));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -703,13 +689,28 @@ export const discoverNetwork = async function(
|
|||
* The possible values are listed in decreasing order of importance, so that if a single link has multiple verdicts but
|
||||
* only one can be displayed, the one with the highest importance will be displayed.
|
||||
*/
|
||||
type LinkVerdict = "linked"
|
||||
type LinkVerdict =
|
||||
| "linked"
|
||||
| "self-linked"
|
||||
| "unlinked"
|
||||
| "self-unlinked"
|
||||
| "redirected"
|
||||
| "wrongly-cased";
|
||||
|
||||
export namespace LinkVerdict {
|
||||
/**
|
||||
* Returns UI properties for each link verdict.
|
||||
*/
|
||||
export const props = {
|
||||
"linked": {icon: "check", message: "Linked 🙂", style: ["success"]},
|
||||
"self-linked": {icon: "rotate-left", message: "Links to its own wiki 😕", style: ["warning"]},
|
||||
"unlinked": {icon: "times", message: "Link is missing 😕", style: ["error"]},
|
||||
"self-unlinked": {icon: null, message: "", style: []},
|
||||
"redirected": {icon: "mail-forward", message: "Links to a redirect 😕", style: ["warning"]},
|
||||
"wrongly-cased": {icon: "text-height", message: "Links with incorrect capitalisation 😕", style: ["warning"]},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The verdict that the checker has of a page.
|
||||
*
|
||||
|
@ -717,7 +718,7 @@ type LinkVerdict = "linked"
|
|||
* only one can be displayed, the one with the highest importance will be displayed.
|
||||
*/
|
||||
type PageVerdict =
|
||||
"perfect"
|
||||
| "perfect"
|
||||
| "not-found"
|
||||
| "wrongly-ordered"
|
||||
| "doubly-linked"
|
||||
|
@ -726,6 +727,22 @@ type PageVerdict =
|
|||
| "redirected"
|
||||
| "wrongly-cased";
|
||||
|
||||
export namespace PageVerdict {
|
||||
/**
|
||||
* Returns UI properties for each page verdict.
|
||||
*/
|
||||
export const props = {
|
||||
"perfect": {icon: "check", message: "Perfect 🙂", style: ["success"]},
|
||||
"not-found": {icon: "search", message: "Article does not exist 😕", style: ["error"]},
|
||||
"wrongly-ordered": {icon: "sort-alpha-asc", message: "Links are in the wrong order 😕", style: ["warning"]},
|
||||
"doubly-linked": {icon: "clone", message: "Links to the same wiki multiple times 😕", style: ["warning"]},
|
||||
"self-linked": {icon: "rotate-left", message: "Links to its own wiki 😕", style: ["warning"]},
|
||||
"unlinked": {icon: "chain-broken", message: "Misses one or more links 😕", style: ["error"]},
|
||||
"redirected": {icon: "mail-forward", message: "Links to a redirect 😕", style: ["warning"]},
|
||||
"wrongly-cased": {icon: "text-height", message: "Links with incorrect capitalisation 😕", style: ["warning"]},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The verdict that the checker has of a network.
|
||||
*/
|
||||
|
@ -733,3 +750,37 @@ type NetworkVerdict =
|
|||
| "perfect"
|
||||
| "flawed"
|
||||
| "broken";
|
||||
|
||||
export namespace NetworkVerdict {
|
||||
/**
|
||||
* Returns UI properties for each network verdict.
|
||||
*/
|
||||
export const props = {
|
||||
"perfect": {
|
||||
message: "A perfect network! 🙂",
|
||||
style: "complete" as MessageLevel
|
||||
},
|
||||
"flawed": {
|
||||
message: "The network is complete but flawed 😕<br />" +
|
||||
"Hover over an icon in the left column for more information.",
|
||||
style: "warning" as MessageLevel
|
||||
},
|
||||
"broken": {
|
||||
message: "The network is broken 😞<br />" +
|
||||
"Hover over an icon in the left column for more information.",
|
||||
style: "warning" as MessageLevel
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Page verdicts that cause a network to become broken.
|
||||
*/
|
||||
export const brokenVerdicts: PageVerdict[]
|
||||
= ["not-found", "unlinked"];
|
||||
|
||||
/**
|
||||
* Page verdicts that cause a network to become flawed.
|
||||
*/
|
||||
export const flawedVerdicts: PageVerdict[]
|
||||
= ["wrongly-ordered", "doubly-linked", "self-linked", "redirected", "wrongly-cased"];
|
||||
}
|
||||
|
|
|
@ -5,14 +5,23 @@ export const couldNotConnectMessage: string =
|
|||
"Could not to connect to API. Is the URL correct? Are you using a script blocker? " +
|
||||
"See the <b>About</b> section for more information.";
|
||||
|
||||
// TODO: Add a merge strategy (to prefer HTTPS)
|
||||
/**
|
||||
* Returns the status that has the lowest index in the given list of statuses.
|
||||
* Merges the given maps into a new map containing all their elements.
|
||||
*
|
||||
* @param statuses the statuses to look the given statuses up in
|
||||
* @param status1 the first status
|
||||
* @param status2 the second status
|
||||
* @return the status that has the lowest index in the given list of statuses
|
||||
* @param maps the maps to merge into a single map
|
||||
* @return the combined map
|
||||
*/
|
||||
export const mergeStates = <T>(statuses: T[], status1: T, status2: T): T => {
|
||||
return statuses[Math.min(statuses.indexOf(status1), statuses.indexOf(status2))];
|
||||
};
|
||||
export const mergeMaps = <K, V>(maps: Map<K, V>[]): Map<K, V> => {
|
||||
return maps.reduce((combined, map) => new Map([...combined, ...map]), new Map());
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the given sets into a new set containing all their elements.
|
||||
*
|
||||
* @param sets the sets to merge into a single set
|
||||
* @return the combined set
|
||||
*/
|
||||
export const mergeSets = <T>(sets: Set<T>[]): Set<T> => {
|
||||
return sets.reduce((combined, set) => new Set([...combined, ...set]), new Set());
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue