wikiroute

Finds the shortest routes between two wikipedia pages and marks the corresponding links

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         wikiroute
// @namespace    https://wikiroute.revig.nl/
// @version      0.5.2
// @description  Finds the shortest routes between two wikipedia pages and marks the corresponding links
// @author       Revi
// @license      MIT
// @match        https://*.wikipedia.org/wiki/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';
    let defaultServ = "https://wikiroute.revig.nl/wikiroute?"
    let servOverwritesByLang = new Map()
    let isWikipedia = location.hostname.indexOf("wikipedia")!==-1
    let lang = isWikipedia ? location.hostname.split(".")[0] : "en-text"
    let wikirouteServ = servOverwritesByLang.get(lang) ?? defaultServ

    GM_registerMenuCommand("route", getRoute, "r");
    async function getRoute() {
        let dest = prompt("Destination","");
        if (dest==null) {
            sessionStorage.route=null;
            sessionStorage.articlesVisited=null;
            return;
        }
        let source = me;
        try {
            let res = await fetch(wikirouteServ+"source="+encodeURIComponent(source)+"&dest="+encodeURIComponent(dest)+"&lang="+encodeURIComponent(lang));
            sessionStorage.route = await res.text();
            sessionStorage.articlesVisited=JSON.stringify([]);
        } catch(err) {
            alert(err);
            sessionStorage.route = null;
            sessionStorage.articlesVisited=null;
        }
        start();
    }
    function inMainNamespace(link) {
        let namespaces = ["media","special","user","wikipedia","file","mediawiki","template","help","category","portal","draft","timedtext","module","gadget","gadget definition"];
        let talks = namespaces.map(a=>a+" talk");
        talks.push("talk");
        namespaces = namespaces.concat(talks);
        link = link.toLowerCase();
        for (let i=0;i<namespaces.length;i++) {
            if (link.startsWith(namespaces[i]+":")) return false;
        }
        return true;
    }
    function getLinksInArticle() {
        return Array.from((document.getElementById("bodyContent")||document.getElementsByClassName("mw-parser-output")[0]).getElementsByTagName("a")).map(a=>a.dataset?.url?.split("_").join(" ")||a.href.split("#")[0].split("/wiki/")[1]&&decodeURIComponent(a.href.split("#")[0].split("/wiki/")[1])).filter(a=>a!=undefined)
    }
    async function updateRoute() {
        let route = JSON.parse(sessionStorage.route);
        let dests = route.destinations;
        let sources = Array.from(new Set(getLinksInArticle().map(a=>a.split("_").join(" ")).filter(a=>a!==me&&inMainNamespace(a))));
        if (sources.some(source=>dests.includes(source))) {
            route.route[me] = sources.filter(source=>dests.includes(source));
            console.log("Direct link");
        } else {
            let exclude;
            try { exclude = JSON.parse(sessionStorage.articlesVisited) } catch(e) { exclude=[];console.log("failed to parse articlesVisited") }
            let res = await fetch(wikirouteServ+"lang="+encodeURIComponent(lang), {method:"POST",body:JSON.stringify({sources,dests,exclude})});
            route = await res.json();
            if (route.route==null) {
                let res = await fetch(wikirouteServ+"lang="+encodeURIComponent(lang), {method:"POST",body:JSON.stringify({sources,dests})});
                route = await res.json();
            }
            console.log("source find ratio: "+route.sources.length/sources.length)
            if (route.route==null) {
                alert("No route found when trying to update route");
                // maybe return?
            }
            route.route[me] = route.sources.filter(a=>route.route[a]&&route.route[a].length);
            if (marked.length&&!route.route[(marked[0].dataset.url||getPlainLink(marked[0])).split("_").join(" ")]) {
                console.log("Found faster route");
                marked.forEach(a=>{a.style=""}) // TODO: make better, such that old styles are kept
                marked=[];
                markedI=0;
            }
        }
        sessionStorage.route = JSON.stringify(route);
        console.log("Updated route", route.route[me]);
        routeUpdated = true;
        let urls = route.route[me].map(a=>a.split(" ").join("_"));
        urls.forEach(url=>Array.from(document.getElementsByTagName("a")).forEach(a=>{if((getPlainLink(a)===url||a.dataset.url===url)&&!marked.includes(a)) { a.style.color = "white";a.style.backgroundColor="green";marked.push(a);console.log(a) }}));
    }
    function delay(ms) {
        return new Promise(res=>setTimeout(res, ms));
    }
    var marked = [];
    var markedI = 0;
    let redirectsResolved = false;
    let routeUpdated = false;
    let active = false;
    let me = decodeURIComponent(document.querySelector('link[rel="canonical"]').href.split("#")[0].split("_").join(" ").split("/wiki/")[1]);
    async function resolveRedirects(callback) {
        let as = document.getElementsByTagName("a");
        console.time("Redirect resolution");
        let toResolve = [];
        let redirectas = [];
        for (let i=0;i<as.length;i++) {
            let a = as[i];
            if (!a.classList.contains("mw-redirect")) continue;
            let link = getPlainLink(a);
            if (!link) continue;
            link = link.split("_").join(" ");
            redirectas.push({a,link});
            if (!toResolve.includes(link)) toResolve.push(link);
        }
        let redirectMap = await getRedirectMap(toResolve,redirectMap=>{
            for (let i=0;i<redirectas.length;i++) {
                let link = redirectas[i].link;
                if (!redirectMap.has(link)) continue
                let a = redirectas[i].a;
                a.dataset.url = redirectMap.get(link).split(" ").join("_");
                callback(a);
                redirectas.splice(i,1);i--;
            }
        });
        if (redirectas.length) {
            console.log("Unable to resolve the following redirects:",redirectas,redirectMap);
        }
        console.timeEnd("Redirect resolution");
        redirectsResolved = true;
    }
    async function getRedirectMap(toResolve, callback) {
        let redirectMap = new Map();
        if (!isWikipedia) return redirectMap;
        for (let i=0;i<toResolve.length;i+=50) {
            let requestURL = "/w/api.php?action=query&titles="+toResolve.slice(i,Math.min(i+50,toResolve.length)).map(encodeURIComponent).join("|")+"&formatversion=2&redirects=1&format=json";
            console.time("fetch");
            let res = await fetch(requestURL, {headers:{"api-user-agent":"wikiroute.revig.nl/wikiroute.user.js"}});
            console.timeEnd("fetch");
            let redirects = (await res.json()).query.redirects;
            for (let j=0;j<redirects.length;j++) {
                redirectMap.set(redirects[j].from, redirects[j].to);
            }
            callback(redirectMap);
        }
        return redirectMap;
    }
    function getPlainLink(a) {
        let link = a.href.split("#")[0].split("/wiki/")[1];
        if (!link) return undefined;
        return decodeURIComponent(link);
    }
    function markURLS(urls) {
      Array.from(document.getElementsByTagName("a")).forEach(a=>{if(!a.classList.contains("mw-redirect")&&urls.includes(getPlainLink(a))) { a.style.color = "white";a.style.backgroundColor="black";marked.push(a);console.log(a) }});
    }

    function focusMarkedLink() {
        if (!marked.length) {
            if (!redirectsResolved) {
                alert("Please wait for redirect resolution");
            } else if (!routeUpdated) {
                alert("Please wait for route update");
            } else {
                alert("Fail");
            }
            return;
        }
        if (!marked[markedI].checkVisibility()) uncollapseAll()
        marked[markedI].focus();
        if (marked[markedI].offsetParent==null) {
            console.log(marked,markedI);
            Array.from(document.getElementsByClassName("mw-collapsible-text")).forEach(a=>a.click());
            marked[markedI].focus()
        }
        markedI = (markedI+1)%marked.length;
    }
    function uncollapseAll() {
        Array.from(document.getElementsByClassName("mw-collapsible-toggle-collapsed")).forEach(e=>e.click())
        Array.from(document.getElementsByClassName("mf-icon-expand")).forEach(e=>e.click())
    }
    start();
    document.body.addEventListener("keydown", e=>{if(e.key=="r"&&(e.altKey||e.target===document.body)) getRoute()});
    function start() {
        let route ;
        try { route=JSON.parse(window.sessionStorage.route); } catch {console.log("No valid current route");return;}
        if (!route) return;
        if (route.sources.length===0) {
            console.log("Source not found");
            route.route={};
            route.route[me] = [];
            sessionStorage.route=JSON.stringify(route);
            //no return
        } else if (route.route==null) {
            if (route.destinations.length===0) {
                alert("Destination not found");
            } else {
                alert("No route found");
            }
            window.sessionStorage.route=null;
            window.sessionStorage.articlesVisited=null;
            return;
        }
        if (route.destinations.includes(me)) {
            sessionStorage.route=null;
            sessionStorage.articlesVisited=null;
            return;
        }
        console.log(me, route.route[me]);
        let urls = route.route[me]
        if (!urls) {
            console.log("Page not on route");
            sessionStorage.articlesVisited="[]"; // maybe store two seperate articlesVisited arrays?
            urls=[];
        }
        urls = urls.map(a=>decodeURIComponent(a).split(" ").join("_"));
        markURLS(urls);
        active = true
        redirectsResolved = false;
        routeUpdated = false;

        resolveRedirects(a=>{
          urls.forEach(url=>{
            if (a.dataset.url===url) {a.style.color = "white";a.style.backgroundColor="gray";marked.push(a);console.log(a)}
          })
        }).then(updateRoute)

        GM_registerMenuCommand("link", focusMarkedLink);
        document.body.addEventListener("keydown", function(e) {
            if (e.target!==document.body&&e.target.tagName!=="A") return;
            if (e.key==="k") {
                focusMarkedLink()
            } else if (e.key==="c") {
                uncollapseAll()
            } else if (e.key==="d") {debugger;}
        });
        try { let visited=JSON.parse(window.sessionStorage.articlesVisited);visited.push(me);sessionStorage.articlesVisited=JSON.stringify(visited) } catch(e) { console.log("error updating articlesvisited",e) }
    }
})();