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:
Florine W. Dekker 2021-05-14 19:29:09 +02:00
parent a8c05f62eb
commit c52f690e6d
Signed by: FWDekker
GPG Key ID: 78B3EAF58145AF25
5 changed files with 125 additions and 104 deletions

View File

@ -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",

View File

@ -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 "" +

View File

@ -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));
};

View File

@ -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"];
}

View File

@ -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());
}