User:HudgynS/common.js

From JoJo's Bizarre Encyclopedia - JoJo Wiki
< User:HudgynS
Revision as of 18:28, 5 October 2024 by HudgynS (talk | contribs)
Jump to navigation Jump to search
Exclamation.png Note: This is a user's personal page attached to their profile!
This is not an actual article, may not be related to JoJo or Araki, and is not associated with the wiki. As such, it may not adhere to the policies.

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/* <pre> */
/* smth like mw:Extension:Popups */
/* popup on link:hover */
/* classes: main: npage-preview, wide image: npage-preview-vertical, */
/* image only: npage-preview-file, image not found: npage-preview-noimage */
/* img: <img>, text: <div> */
(function wrapper ($) {
    var parser = new DOMParser();
    var urlVars = new URLSearchParams(location.search);
    var Settings = window.pPreview || {},
        mwc = mw.config.get(['wgScriptPath', 'wgSassParams', 'wgArticlePath']);
    Settings.debug = urlVars.get('debug') || urlVars.get('debug1') || (Settings.debug !== undefined ? Settings.debug : false);

    // killswitch
    Settings.dontrun = urlVars.get('nolp');
    if (Settings.dontrun) return;

    // default values
    var Defaults = {
        dock: '#mw-content-text, #article-comments',
        defimage: 'https://jojowiki.com/extensions/InterviewManager/resources/Loading-Koichi.png',
        noimage : 'https://static.jojowiki.com/images/7/71/latest/20230228042347/JJBESymbol.png',
    };// defaults
    var pp = {};
    pp.sync = []; // synchronization element
    var ncache = []; // {href, data}
    var loc = {lefts: 5, tops: 5}; // left: x, top: y, lefts: left-shift, clientx
    var currentEl = {}; // {href, ?data}
    // var api = new mw.Api();
    var apiUri;
    // exports
    Settings.wrapper = wrapper;
    Settings.context = this;
    Settings.f = {init: init, main: main, createuri: createUri, getpreview: ngetPreview,
                showpreview: nshowPreview, hidepreview: nhidePreview, cache: ncache,
                ignoreimage: nignoreImage, ignorepage: nignorePage, ignorelink: nignoreLink,
                cacheof: ncacheOf, chkimagesrc: chkImageSrc, preprocess: preprocess,
                elvalidate: elValidate, processtext: processText, resettimer: resetTimer,
                callapi: callApi, splitnamespace: splitNamespace};

    mw.loader.using(['mediawiki.util', 'mediawiki.Uri'], init);

    function log () {
        var a = [].slice.call(arguments);
        a.unshift('pp');
        if (Settings.debug) console.log.apply(this, a);
    }// log

    pp.start = function (e) {
        // allows (true) processing for element e
        if (e) {
            if (pp.sync.indexOf(e) > -1) {
                return false;
            }
        }
        Settings.process = true;
        pp.sync.push(e || Settings.process);
        return true;
    };// start
    
    pp.stop = function (e) {
        hlpaHover();
        var epos = pp.sync.indexOf(e);
        if (epos !== -1) {
            // remove e from sync array
            pp.sync.splice(epos, 1);
        } else {
            // remove something; stack presumed
            pp.sync.splice(0, 1);
        }
        if (pp.sync.length === 0) {
            Settings.process = false;
        }
    };// stop

    pp.cachedupl = function () {
        // check cache for href duplication
        var el = null;
        outer:
        for (var i = 0, len = ncache.length; i < len; i++) {
            for (var k = i + 1; k < len; k++) {
                if (ncache[i].href === ncache[k].href) {
                    el = {v: ncache[i].href, i: i, k: k};
                    break outer;
                }
            }// k inner loop
        }// i outer loop
        if (el) {
            console.log('pp.cachedupl found', el.v, el.i, el.k);
        }
    };// cachedupl

    function init () {
        if (window.pPreview && window.pPreview.version) {
            log('init dbl run protection triggered');
            return;
        }
        Settings.version = '1.70';
        log('init vrsn:', Settings.version);
        apiUri = new mw.Uri({path: mwc.wgScriptPath + '/api.php'});
        // use api.v1/article/details
        Settings.apid = Settings.apid !== undefined ? Settings.apid : false;
        // show preview delay, ms
        Settings.delay = Settings.delay !== undefined ? Settings.delay : 200;
        // suppress hover events for x ms
        Settings.ttl = Settings.ttl !== undefined ? Settings.ttl : 100;
        // Settings.throttling = timeout until x
        Settings.throttle = Settings.throttle !== undefined ? Settings.throttle : 100;
        Settings.throttling = false;
        Settings.process = false;// processing data
        Settings.tlen = Settings.tlen !== undefined ? Settings.tlen : 800; // max text length
        // do not remove portable infobox on preprocess stage
        Settings.pibox = Settings.pibox !== undefined ? Settings.pibox : false;
        // do not remove infobox siblings
        Settings.piboxkeepprev = Settings.piboxkeepprev !== undefined ? Settings.piboxkeepprev : false;
        // cache size
        Settings.csize = Settings.csize !== undefined ? Settings.csize : 100;
        Settings.defimage = Settings.defimage !== undefined ? Settings.defimage : Defaults.defimage; // default image path
        // no image found. class: npage-preview-noimage
        Settings.noimage = Settings.noimage !== undefined ? Settings.noimage : Defaults.noimage;
        // request to perform scaling
        Settings.scale = Settings.scale !== undefined ? Settings.scale : {r: '?', t: '/scale-to-width-down/350?'};
        // container (#WikiaMainContent, #mw-content-text etc)
        Settings.dock = !!Settings.dock ? Settings.dock : Defaults.dock;
        // parse whole page. debug purposes mainly
        Settings.wholepage = urlVars.get('wholepage') || (Settings.wholepage !== undefined ? Settings.wholepage : false);
        Settings.RegExp = Settings.RegExp || {}; // regexps
        // images 2 ignore
        Settings.RegExp.iimages = Settings.RegExp.iimages || [];
        // pages 2 ignore
        Settings.RegExp.ipages = Settings.RegExp.ipages || [];
        // links 2 ignore
        Settings.RegExp.ilinks = Settings.RegExp.ilinks || [];
        // parents to ignore
        Settings.RegExp.iparents = Settings.RegExp.iparents || ['[id^=flytabs] .tabs'];
        // classes to ignore
        Settings.RegExp.iclasses = Settings.RegExp.iclasses || [];
        // content to process. non-exclusive inclusion
        Settings.RegExp.onlyinclude = Settings.RegExp.onlyinclude || [];
        // content to remove (css-style targets)
        Settings.RegExp.noinclude = Settings.RegExp.noinclude || [];
        // Settings.RegExp.hash = Settings.RegExp.hash || new RegExp('#.*');
        Settings.RegExp.wiki = Settings.RegExp.wiki || new RegExp('^.*?\/wiki\/', 'i');
        // delete tags
        Settings.RegExp.dtag = Settings.RegExp.dtag || new RegExp(/<\/*([^\/bius]|s[^p]|b[^r>\s]+|[iu][^>\s]+).*?>/, 'gm');
        // preprocess data (remove scripts)
        Settings.RegExp.prep = Settings.RegExp.prep || [];
        // set len restriction for apid.abstract
        if (Settings.apid) {
            Settings.tlen = (Settings.tlen > 500) ? 500 : Settings.tlen;
        }
        // ensure #mw-content-text is processed
        Settings.fixContentHook = Settings.fixContentHook !== undefined ? Settings.fixContentHook : true;
        Settings.emptytext = Settings.emptytext !== undefined ? Settings.emptytext : /^([\n\s]|<[^>]+?>|<!--.*?-->|\.\.\.)*?$/;
        
        window.pPreview = Settings;
        var thisPage = (createUri(location) || {}).truepath;
        // should i ignore this page
        if (!thisPage || nignorePage(thisPage)) {
            mw.hook('wikipage.content').remove(main);
            log('ignore', thisPage);
            return;
        }
        // run once
        // dump sass params
        var sasses = '';
        $.each(mwc.wgSassParams, function(k, v) {
            sasses = sasses + '--sass-' + k + ':' + v + ';\n';
        });// each sassparam
        if (sasses.length) {
            sasses = ':root {\n' + sasses + '}';
            mw.util.addCSS(sasses);
        }
        log('sasses', {sasses: sasses});
        log('rmain');
        if (Settings.debug) {
            Settings.cache = ncache;
        }
        Settings.RegExp.ilinks.push(thisPage); // ignore this page
        Settings.RegExp.ilinks.push(new RegExp(apiUri.path)); // ignore unknown
        var r;
        if (Settings.RegExp.prep instanceof RegExp) {
            r = Settings.RegExp.prep;
            Settings.RegExp.prep = [r];
        }// if regexp.prep is regexp
        if (!(Settings.RegExp.prep instanceof Array)) {
            Settings.RegExp.prep = [];
        }// if regexp.prep is not array
        Settings.RegExp.prep.push(/<script>[\s\S]*?<\/script>/igm);
        Settings.RegExp.prep.push(/<ref>[\s\S]*?<\/ref>/igm);
        Settings.defimage = chkImageSrc(Settings.defimage) ? Settings.defimage : Defaults.defimage;
        Settings.noimage = chkImageSrc(Settings.noimage) ? Settings.noimage : Defaults.noimage;
        Settings.f.pp = pp;
        // ajaxrc support
        window.ajaxCallAgain = window.ajaxCallAgain || [];
        window.ajaxCallAgain.push(main);
        mw.hook('wikipage.content').add(main);
        mw.hook('ppreview.ready').fire(Settings);

        // load localization, if no local (wiki\user-specific) noimage defined
        if (Settings.noimage === Defaults.noimage) {
	        log('i18n load');
	        mw.hook('dev.i18n').add(function (i18n) {
	        	i18n.loadMessages('LinkPreview').done(function (i18n) {
		        	log('i18n loaded', i18n);
		        	i18n.useContentLang();
		        	var img = i18n.msg('no-image').plain();
		        	Settings.noimage = chkImageSrc(img) ? img : Settings.noimage;
		        	log('i18n noimage', Settings.noimage, img);
	        	});
	        });
        }
        // main();
    } // init
    
    function main ($cont) {
        // main
        log('main', $cont);
        if (Settings.fixContentHook && $cont && $cont.length) {
            Settings.fixContentHook = false;
            if ($cont.selector !== '#mw-content-text') {
                log('main fixcontent', $cont);
                main($('#mw-content-text'));
            }
        }
        var $content, arr = [];
        // gather dock sites to one array
        Settings.dock.split(',').forEach(function (v) {
            var $c = {};
            if ($cont) {
                // if $cont belongs to dock container
                $c = ($cont.is(v) || $cont.parents(v).length) ? $cont : {};
            } else {
                // get whole dock. if main() called w\o params
                $c = $(v);
            }// if $cont. instead of $cont ? .is || .len ? : :
            $.merge(arr, $c);
        });// each dock
        $content = $(arr);
        log('main.c:', $content);
        $content.find('a').each(function() {
            var $el = $(this);
            if (elValidate($el)) { // internal link
                // $el.hover(aHover, resetTimer);
                $el.off('mouseover.pp blur.pp mouseout.pp');
                $el.on('mouseover.pp', aHover);
            } // if internal link
        }); // each a
    } // main
    
    function splitNamespace(pagename) {
        // returns array containing namespace and pagename without namespace
        var divider = pagename.search(/:[^_\s]/);
        if (divider) {
            var namespace = pagename.substring(0, divider);
            var namespaceless = pagename.substring(divider+1);
            return new Array(namespace, namespaceless);
        }
        return new Array('', pagename);
    } // splitNamespace

    function callApi(nuri, withD) {
        // calls the site API and returns array containing text and image
        return new Promise(function (resolve, reject) {
            var text = '';
            var img = '';
            var orient = 'horizontal';
            var extend;
            var splitName = splitNamespace(nuri.apititle);
            var apipage = new mw.Uri({path: mwc.wgScriptPath + '/api.php'});
            var datafunc;
            if (!splitName) reject();
            switch (splitName[0]) {
                case 'Category': // include additional text option listing members
                    apipage.extend({
                        action: 'query', titles: nuri.apititle, prop: 'extracts|pageimages', format: 'json', formatversion: 2,
                        redirects: false, exchars: 1200, piprop: 'thumbnail', list: 'categorymembers', cmtitle: nuri.apititle,
                        cmlimit: 50, pithumbsize: 200, pilicense: 'any', smaxage: 600, maxage: 600, uselang: 'content'
                    });
                    datafunc = function (item) {
                        if (item.categorymembers) {
                            text = '<b>' + splitName[1].replace(/_/g,' ') + ':</b> ';
                            $.each(item.categorymembers, function(index, value) {
                                text = text + value.title;
                                if (index < item.categorymembers.length-1) {
                                    text = text + ', ';
                                }
                            });
                            resolve(new Array(text, img, orient));
                        }
                    };
                    break;
                case 'File': // get higher-quality file in case there's no text
                    apipage.extend({
                        action: 'query', titles: nuri.apititle, prop: 'extracts|pageimages', format: 'json',
                        formatversion: 2, redirects: false, exchars: 1200, piprop: 'thumbnail',
                        pithumbsize: 300, pilicense: 'any', smaxage: 600, maxage: 600, uselang: 'content'
                    });
                    break;
                case 'User':
                case 'UserWiki':
                    apipage.extend({
                        action: 'query', titles: nuri.apititle, prop: 'extracts', format: 'json',
                        formatversion: 2, redirects: false, exchars: 1200, list: 'users', ususers: nuri.apititle,
                        usprop: 'gender|registration|editcount', smaxage: 600, maxage: 600, uselang: 'content'
                    });
                    datafunc = function (item) {
                        if (item.users && item.users[0]) {
                            text = '<b>' + item.users[0].name + '</b>';
                            if (item.users[0].gender) {
                                switch (item.users[0].gender) {
                                    case 'male':
                                        text = text + ' (♂)';
                                        break;
                                    case 'female':
                                        text = text + ' (♀)';
                                        break;
                                } // ideally pronouns would be a field
                            }
                            if (item.users[0].registration) {
                                var registered = new Date(item.users[0].registration);
                                text = text + '<br><b>Registered:</b> ' + registered.toLocaleString();
                            }
                            if (item.users[0].editcount) {
                                text = text + '<br><b>Edits:</b> ' + item.users[0].editcount.toLocaleString();
                            }
                        }
                    };
                    break;
                case 'Interview':
                case 'Poll':
                case 'Blog':
                case 'Help':
                case 'JoJo_Wiki':
                case '':
                    if (withD) {
                        apipage.extend({
                            action: 'query', titles: nuri.apititle, prop: 'extracts|pageimages', format: 'json',
                            formatversion: 2, redirects: false, exchars: 1200, explaintext: true,
                            exsectionformat: 'plain', piprop: 'thumbnail', pithumbsize: 250,
                            pilicense: 'any', smaxage: 600, maxage: 600, uselang: 'content'
                        });
                    }
                    else {
                        apipage.extend({
                            action: 'query', titles: nuri.apititle, prop: 'extracts|pageimages', format: 'json',
                            formatversion: 2, redirects: false, exchars: 1200, piprop: 'thumbnail',
                            pithumbsize: 250, pilicense: 'any', smaxage: 600, maxage: 600, uselang: 'content'
                        });
                    }
                    datafunc = function (item) {};
                    break;
                default:
                    reject();
                    break;
            }

            $.getJSON(apipage).done(function(data) {
                var item = data.query;
                datafunc(item);
                if (item.pages && item.pages[0]) {
                    if (item.pages[0].thumbnail) {
                        img = item.pages[0].thumbnail.source;
                        if (item.pages[0].thumbnail.width > item.pages[0].thumbnail.height) {
                            orient = 'vertical';
                        }
                    }
                    else {
                        if (item.pages[0].extract && !Settings.emptytext.test(item.pages[0].extract)) {
                            text = item.pages[0].extract;
                            if (text.search('<div class="introSwitch">') > -1 || Settings.wholepage) {
                                apipage = new mw.Uri({path: mwc.wgScriptPath + '/api.php'});
                                apipage.extend({
                                    action: 'parse', page: nuri.apititle, prop: 'text', format: 'json',
                                    formatversion: 2, redirects: false, smaxage: 600, maxage: 600, uselang: 'content'
                                });
                                $.getJSON(apipage).done(function(data) {
                                    var item2 = data.parse;
                                    if (item2.text) text = item2.text;
                                    resolve(new Array(text, img, orient));
                                })
                                .fail(function(data) {
                                    resolve(new Array(text, img, orient));
                                });// apid.fail
                            }
                            else resolve(new Array(text, img, orient));
                        }
                        else resolve(new Array(text, img, orient));
                    }
                }
            })// apid.done
            .fail(function(data) {
                log('gp apid.fail', nuri, data);
                Settings.RegExp.ilinks.push(nuri.truepath); // and ignore it
                pp.stop(nuri.truepath);
                reject();
            });// apid.fail
        });
    } // callApi

    function resetTimer (ev) {
        // resets timer until preview ends
        if (ev.data.el) {
            if (ev.data.el.timer) clearTimeout(ev.data.el.timer);
            ev.data.el.timer = setTimeout(function() {nhidePreview(ev.data.el)}, Settings.ttl);
        }
    } // resetTimer

    function processText (text, nuri) {
        // various text cleaning operations
        if (text && text.length > 0) {
            // preprocess (cleanup)
            text = preprocess(text);
            var newimg;
            
            var switcherTabber = text.match(/<div class="switcherTabber".*?">/); // TabberSwitch functionality
            if (switcherTabber) {
                var rawtext = text.replace(/<span class="noexcerpt">/g,'<span style="display:none;">'); // this is HTML, don't forget
                text = rawtext.substring(rawtext.search('\n<div class="introSwitch">')+1); // cut the beginning templates (hopefully!)
                var ns = splitNamespace(nuri.apititle);
                if (ns[1]) ns[1] = ns[1].replace(/_/g,' '); // convert url to proper name

                var imagetabs = rawtext.match(/role="tab.*?(class="tab|<\/div><\/div>)/g);
                imagetabs = $.grep(imagetabs, function (value, index) {
                    if (imagetabs.length > 3 && !value.match(/src=".+?\.(jpg|jpeg|png|gif)"/)) return false;
                    return true;
                });
                // cut any tabs that don't have images

                var switcherTabs = switcherTabber[0].match(/data-url[1-4]=".*?"/g); // find our possible names
                $.each(switcherTabs, function (index, value) {
                    var matchName = value.substring(11, value.length-1); // get the value of the attribute
                    if (matchName === ns[1] && index > 0) { // if it's the first tab it's default
                        var tabimage = imagetabs[index].match(/src=".+?\.(jpg|jpeg|png|gif)"/); // find image
                        if (tabimage) {
                            newimg = tabimage[0].substring(5, tabimage[0].length-1); // return image
                        }
                        text = text.replace(/<div class="introSwitch">(.|\n)*?<\/div>/g,'');
                        text = text.replace(/<div class="introSwitch2" style="display:none;">((.|\n)*?)<\/div>/g,'<span>$1</span>');
                        return false; // we change the div to a span so it won't be cut down in the coming purge
                    }
                    else {
                        if (index == switcherTabs.length-1) { // if this is the last data-url
                            text = text.replace(/<div class="introSwitch">((.|\n)*?)<\/div>/g,'<span>$1</span>');
                        } // same as above
                    }
                });
            }
            // text clean up
            // cut everything until the first paragraph
            var para = text.search(/(<p>|<span>)/);
            if (para > -1) {
                //text = text.substring(para);
            }
            text = text.replace(/<a href="#.*?">\[.+?\]<\/a>/g, ''); // remove references
            text = text.replace(Settings.RegExp.dtag, ''); // and misc. tags

            // then cut everything past the first header
            var header = text.search(/([^\/]>|[^>\n])\n\n/);
            if (header > -1) {
                text = text.substring(0,header+2);
            }
            text = text.replace(/\n/g, ''); // we can now toss out natural line breaks (unless we put them there!)

            // then cut everything in any incomplete sentence
            if (text.endsWith('...')) {
                text = text.substring(0,text.length-3);
            }

            if (text.length > 0 && text.substring(0,4) != '<!--') {
                if (newimg) {
                    return new Array(text, newimg);
                }
                else return text;
            }
        }
        return "";
    }// processText
    
    function elValidate ($el) {
        // returns false if element should be ignored
        var ahref = $el.attr('href'),
            bstop = false;
        // log('elValidate. el.h:', ahref);
        if (!ahref) return false;
        ahref = createUri(ahref);
        // log('elValidate.uri:', ahref);
        if (!ahref || (ahref.hostname !== apiUri.host) || nignoreLink(ahref.truepath)) {
            return false;
        }

        // chk classes
        if ($.isArray(Settings.RegExp.iclasses)) {
            Settings.RegExp.iclasses.forEach(function(v) {
                if ($el.hasClass(v)) {
                    log('elValidate classes', v, ahref.truepath);
                    // Settings.RegExp.ilinks.push(ahref.truepath);
                    bstop = true;
                }
            });
        }
        // log('elValidate classes', bstop);
        if (bstop) return false;

        // chk parents
        if ($.isArray(Settings.RegExp.iparents)) {
            Settings.RegExp.iparents.forEach(function(v) {
                if ($el.parents(v).length) {
                    log('elValidate parents', v, ahref.truepath);
                    // Settings.RegExp.ilinks.push(ahref.truepath);
                    bstop = true;
                }
            });
        }
        // log('elValidate parents', bstop);
        if (bstop) return false;
        return true;
    }// elValidate
    
    function chkImageSrc (src) {
        // is src belongs to wiki
        if (!src) return false;
        var url;
        try {
            url = new mw.Uri(src);
            return (/(\.jojowiki\.com)$/.test(url.host));
        }
        catch (e) {
            return false;
        }
        return false;
    }// chkimagesrc
    
    function preprocess (text) {
        // prep must be non-empty array (script removing at least, added in the init)
        if (!(Settings.RegExp.prep instanceof Array) || Settings.RegExp.prep.length < 1) return '';
        var s = text,
            $s = $('<div>').html(s);

        // remove noinclude items
        if (Settings.RegExp.noinclude && (Settings.RegExp.noinclude instanceof Array)) {
            Settings.RegExp.noinclude.forEach(function(v){$s.find(v).remove();});
        }// if RegExp.noinclude
        s = $s.html();
        // process exclusive items
        // must be done before trash tag processing. because of reasons
        if (Settings.RegExp.onlyinclude && (Settings.RegExp.onlyinclude instanceof Array)) {
            /* exclusive
            Settings.RegExp.onlyinclude.forEach(function (v) {
                var $v = $s.find(v);
                if ($v.length) $s = $v;// call it exclusive
            });
            s = $s.html();
            */
            /* non-exclusive set */
            s = Settings.RegExp.onlyinclude.map(function(v) {
                var $v = $s.find(v);
                if ($v.length) {
                    $s.find(v).remove();
                    return $v.map(function() {return this.outerHTML}).toArray().join();
                } else {
                    return false;
                }
            })
            .filter(Boolean).join() || s;
        }// if RegExp.onlyinclude
        
        Settings.RegExp.prep.forEach(function (v) {
            s = s.replace(v, '');
        });
        return s;
    }// preprocess
    
    function createUri (href, base) {
        var h;
        try {
            h = new mw.Uri(href.toString());
            h.pathname = h.path;
            h.hostname = h.host;
        } catch (e) {
            h = undefined;
            log('createUrl.e', e);
        }
        if (h) {
            try {
                h.truepath = decodeURIComponent(h.pathname.replace(Settings.RegExp.wiki, ''));
                h.interwiki = h.path.split('/wiki/')[0];
                h.islocal = mwc.wgArticlePath.split('/wiki/')[0] === h.interwiki;
            }
            catch (e) {
                h = undefined;
                log('createuri decode.e', e, h, String(h));
            }
        }
        return h;
    } // createUri
        
    function escapeRegExp(str) {
        return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
    } // escapeRegExp
    
    function hlpaHover () {
        // aHover helper
        if (Settings.throttling) {
            clearTimeout(Settings.throttling);
            Settings.throttling = false;
        }
    }// hlpaHover
    
    function aHover (ev) {
        // a hover handler
        ev.stopPropagation();
        log('ahover ', Settings.throttling, currentEl.href);
        // suppress some events
        if (Settings.throttling || Settings.process) {
            return false;
        }
        Settings.throttling = setTimeout(hlpaHover, Settings.throttle);
        var hel = createUri($(ev.currentTarget).attr('href')) || {};
        // if link already in process
        var preview = $('.npage-preview');
        if (currentEl.source !== ev.currentTarget) {
            currentEl.source = ev.currentTarget;
        }
        if (hel && hel.truepath && currentEl.href == hel.truepath) {
            if (preview.timer) clearTimeout(preview.timer);
            return false;
        }
        nhidePreview(preview);
        currentEl.href = hel.truepath;
        currentEl.islocal = hel.islocal;
        currentEl.interwiki = hel.interwiki;
        currentEl.source = ev.currentTarget;
        // if link determined be ignored
        if (nignoreLink(currentEl.href)) {
            return true;
        } // if ignore link
        // set coords
        loc.left = ev.pageX;
        loc.top = ev.pageY;
        loc.clientX = ev.clientX;
        loc.clientY = ev.clientY;
        log('ahover ev:', ev, 'cel:', currentEl);
        setTimeout(ngetPreview.bind(this, ev), Settings.delay);
        return false;
    } // ahover
    
    function getObj (data, key) {
        // traverse through object tree
        var ret = [], r;
        for (var k in data) {
            if (data[k] instanceof Object) {
                if (k === key) {
                    ret.push(data[k]);
                }
                r=getObj(data[k], key);
                if (r) ret=ret.concat(r);
            } // if obj
        } // for k in data
        return ret;
    } // getObj
    
    function getVal (data, key) {
        // travers through object tree
        var ret = [], r;
        for (var k in data) {
            if (data[k] instanceof Object) {
                r=getVal(data[k], key);
                if (r) {
                    ret=ret.concat(r);
                }
            } else {
                if (k === key) {
                    ret.push(data[k]);
                }
            } // if obj
        } // for k in data
        return ret;
    } // getVal
    
    function hlpPreview (uri, div, img, force, withD) {
        // preview helper
        // load img and add to div
        var im, d;
        im = $('img', div);
        if (!Settings.apid && !withD) {
            if (img) {
                // let vignette do scale
                im.attr('src', Settings.scale ? img.replace(Settings.scale.r, Settings.scale.t) : img);
            } else {
                im.attr('src', Settings.noimage);
                im.addClass('npage-preview-noimage');
            } // if img
        }// if !apid
        d = {href: uri.truepath, data: div, uri: uri};
        ncache.push(d);
        if (Settings.debug) window.pPreview.pdiv = d.data;
        nshowPreview(d.data, d.uri, force);
        pp.stop(d.href);
    } // hlpPreview
    
    function ngetPreview (ev, forcepath, withD) {
        var nuri = createUri($(currentEl.source).attr('href')) || {};
        nuri.truepath = forcepath || nuri.truepath;
        nuri.apititle = nuri.truepath.replace(/^\/+/g, '') || '';
        if (!nuri || !nuri.truepath || nuri.apititle.length <= 0 || nuri.apititle === 'index.php'
            || nuri.apititle.substring(nuri.apititle.length-3) === '.js'
            || nuri.apititle.substring(nuri.apititle.length-4) === '.css') {
            log('gp no href', ev, forcepath);
            return;
        }
        if (!pp.start(nuri.truepath)) {
            // this href already started to process
            log('gp suppressed dbl processing for', nuri);
            return;
        }
        // save bandwidth
        log('gp uri: ', nuri, ' curel.href: ', currentEl.href, nuri.truepath === currentEl.href, 'd:', withD);
        // withd means fallback request, that should not be cancelled early
        if (!forcepath && !withD && (nuri.truepath != currentEl.href)) {
            pp.stop(nuri.truepath);
            return;
        }
        var div = $('<div>', {class: 'mw-body npage-preview'});
        var ndata = ncacheOf(nuri.truepath);
        log('gp x:', loc.left, 'y:', loc.top);
        if (ndata) {
            div.html(ndata.data.html()); // get content from cache
            div.addClass(ndata.data.attr('class'));
        } // if data
        else div.timer = null;

        div.source = currentEl.source;
        $(div).off('mouseover.pp blur.pp mouseout.pp');
        $(div).on('mouseover.pp', function() {
            if (div.timer) clearTimeout(div.timer);
        });
        $(div).on('blur.pp mouseout.pp', {el: div}, resetTimer);
        $(currentEl.source).off('blur.pp mouseout.pp');
        $(currentEl.source).on('blur.pp mouseout.pp', {el: div}, resetTimer);

        if (ndata) {
            log('gp show preview', ndata);
            nshowPreview(div, nuri, forcepath ? true : false);
            pp.stop(nuri.truepath);
            return false;
        } // if data

        // get data
        var apicall,
            iwrap = $('<img>', {src: Settings.defimage}),
            twrap = $('<div>', {class: 'npage-text'}),
            awrap = $('<a>', {href: ''});
        callApi(nuri, withD).then(function(data) {
            log('gp apip', apicall);
            if (!data) {
                log('gp apid.error', nuri, apicall);
                Settings.RegExp.ilinks.push(nuri.truepath); // and ignore it
                pp.stop(nuri.truepath);
                return this;
            }
            if (data.length < 1) {
                log('gp apid.noitem', nuri, apicall);
                Settings.RegExp.ilinks.push(nuri.truepath); // and ignore it
                pp.stop(nuri.truepath);
                return this;
            }
            if (data[1] && !nignoreImage(0)) {
                var img = data[1];
            }
            if (data[0]) {
                text = processText(data[0], nuri);

                if (text.length == 2) {
                    img = text[1];
                    text = text[0];
                }
                if (text && !Settings.emptytext.test(text)) {
                    text = text.replace(/\n/g,'<br>');
                    $(twrap).html(text);
                    div.append(twrap);
                }
                else div.addClass('npage-preview-file');
            }
            else div.addClass('npage-preview-file'); // add file class (show at 200px) if there is no text
            if (!img && !text) {
                pp.stop(nuri.truepath);
                if (Settings.apid || withD) {
                    Settings.RegExp.ilinks.push(nuri.truepath); // and ignore it
                    return this;
                } else {
                    // last try; via api.v1
                    return ngetPreview(ev, null, true);
                }
            }
            awrap.html(iwrap);
            awrap.attr('href',nuri.truepath);
            div.prepend(awrap);

            if (data[2] === 'vertical') div.addClass('npage-preview-vertical');
            if (img) {
                hlpPreview(nuri, div, img, forcepath ? true : false);
            } else { // if img
                hlpPreview(nuri, div, false, forcepath ? true : false);
            }// no img
            // pp.stop();
            return false;
        })
        .catch(function() {
            return false;
        });
        return;
    } // getpreview
    
    function nshowPreview (data, target, force) {
        log('sp', data, target, force);
        currentEl.href = ''; // this makes sure it can pop back up on the same link!
        if (!($(data.source).is(":hover"))) { nhidePreview(data); return false; } // in case the cursor already left
        log('sp data:', data);
        
        // nhidePreview();
        $('.npage-preview').remove(); // remove artefacts
        $('body').append($(data));
        $(data.source).off('mouseover.pp');
        $(data.source).on('mouseover.pp', function() {
            if (data.timer) clearTimeout(data.timer);
        });

        // prehide data
        $(data).css({left: -10000, top: -10000});
        $(data).show(200, function() { // ;// fadeIn('fast');
            // reposition works well with pre-set fixed data bounds
            if ((loc.clientY + $(data).height()) > $(window).height()) {
                loc.top -= ($(data).height() + loc.tops);
            } else {
                loc.top += loc.tops;
            }// if top>window
            if ((loc.clientX + $(data).width()) > $(window).width()) {
                loc.left -= ($(data).width() + loc.lefts);
            } else {
                loc.left += loc.lefts;
            }// if left>window
        
            // move preview to target location
            log('sp loc', loc);
            loc.left = loc.left > 0 ? loc.left : 0;
            loc.top = loc.top > 0 ? loc.top : 0;
            $(data).css({
                left: force ? $('body').scrollLeft() : loc.left,
                top: force ? $('body').scrollTop() : loc.top});
            mw.hook('ppreview.show').fire(data);
        });// data.show.done
    } // showpreview
    
    function nhidePreview (data) {
        clearTimeout(data.timer);
        $(data.source).off('mouseover.pp mouseout.pp');
        $(data.source).on('mouseover.pp', aHover);
        $(data).remove();
        // clear throttling
        hlpaHover();
    } // hidepreview
    
    function nignoreImage (name) {
        // true if image should be ignore
        // name = name.replace(/(file):/im, '');
        // name = name.charAt(0).toUpperCase() + name.slice(1);
        for (var i = 0, len = Settings.RegExp.iimages.length; i < len; i++) {
            if (Settings.RegExp.iimages[i] instanceof RegExp) {
                if (Settings.RegExp.iimages[i].test(name)) return true;
            } else {
                if (name === Settings.RegExp.iimages[i]) return true;
            } // if regexp
        }
        return false;
    } // nignoreimage
    
    function nignorePage (name) {
        // true if page should be ignore
        var a = Settings.RegExp.ipages;
        for (var i = 0, len = a.length; i < len; i++) {
            if (a[i] instanceof RegExp) {
                if (a[i].test(name)) return true;
            } else {
                if (name === a[i]) return true;
            } // if regexp
        }
        return false;
    } // nignorepage
    
    function nignoreLink (name) {
        // true if link should be ignore
        var a = Settings.RegExp.ilinks;
        for (var i = 0, len = a.length; i < len; i++) {
            if (a[i] instanceof RegExp) {
                if (a[i].test(name)) return true;
            } else {
                if (name === a[i]) return true;
            } // if regexp
        }
        return false;
    } // nignorelink
    
    function ncacheOf (href) {
        // returns cached obj or null
        if (ncache.length > Settings.csize) ncache = []; // clear cache
        for (var i = 0, len = ncache.length; i < len; i++) {
            if (ncache[i].href === href) {
                log('cache found:', href, 'data:', ncache[i].data);
                // window.ppcdata = ncache[i];
                return ncache[i];
            }
        }
        return null;
    } // ncacheof
})(jQuery);