ajaxify

/* 
 * ajaxify.js 
 * Ajaxify - A jQuery Ajax Plugin
 * https://4nf.org/ 
 * 
 * Copyright Arvind Gupta; MIT Licensed 
 */ 
 
/* INTERFACE: See also https://4nf.org/interface/

Simplest plugin call:

jQuery('#content').ajaxify();
Ajaxifies the whole site, dynamically replacing the element with the ID '#content' across pages

If several elements should be swapped, just specify their IDs like this:
jQuery('#content, #nav').ajaxify();

The plugin can take an arbitrary amount of IDs


//Options default values
*/
var gsettings, dsettings = 

{
//	basic config parameters
	selector : "a:not(.no-ajaxy)", //Selector for elements to trigger swapping - not those to be swapped - e.g. a selection of links
	forms : "form:not(.no-ajaxy)", // jQuery selection for ajaxifying forms - set to "false" to disable
	canonical : false, // Fetch current URL from "canonical" link if given, updating the History API.  In case of a re-direct...
	refresh : false, // Refresh the page even if link clicked is current page
 
// visual effects settings
	requestDelay : 0, //in msec - Delay of Pronto request
	previewoff : true, // Plugin previews prefetched pages - set to "false" to enable or provide hints to selectively disable
	scrolltop : "s", // Smart scroll, true = always scroll to top of page, false = no scroll
	bodyClasses : false, // Copy body classes from target page, set to "true" to enable
 
// script and style handling settings, prefetch
	deltas : true, // true = deltas loaded, false = all scripts loaded
	asyncdef : false, // default async value for dynamically inserted external scripts, false = synchronous / true = asynchronous
	alwayshints : false, // strings, - separated by ", " - if matched in any external script URL - these are always loaded on every page load
	inline : true, // true = all inline scripts loaded, false = only specific inline scripts are loaded
	inlinehints : false, // strings - separated by ", " - if matched in any inline scripts - only these are executed - set "inline" to false beforehand
	inlineskip : "adsbygoogle", // strings - separated by ", " - if matched in any inline scripts - these are NOT are executed - set "inline" to true beforehand 
	inlineappend : true, // append scripts to the main content element, instead of "eval"-ing them
	style : true, // true = all style tags in the head loaded, false = style tags on target page ignored
	prefetchoff : false, // Plugin pre-fetches pages on hoverIntent - true = set off completely // strings - separated by ", " - hints to select out
 
// debugging & advanced settings
	verbosity : 0, //Debugging level to console: default off.	Can be set to 10 and higher (in case of logging enabled)
	memoryoff : false, // strings - separated by ", " - if matched in any URLs - only these are NOT executed - set to "true" to disable memory completely
	cb : null, // callback handler on completion of each Ajax request - default null
	pluginon : true // Plugin set "on" or "off" (==false) manually
};





 //Intuitively better understandable shorthand for String.indexOf() - String.iO()
String.prototype.iO = function(s) { return this.toString().indexOf(s) + 1; };

//Minified hoverIntent plugin
!function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery"],e):jQuery&&!jQuery.fn.hoverIntent&&e(jQuery)}(function(e){"use strict";var t,n,i={interval:100,sensitivity:6,timeout:0},o=0,u=function(e){t=e.pageX,n=e.pageY},r=function(e,i,o,v){if(Math.sqrt((o.pX-t)*(o.pX-t)+(o.pY-n)*(o.pY-n))<v.sensitivity)return i.off(o.event,u),delete o.timeoutId,o.isActive=!0,e.pageX=t,e.pageY=n,delete o.pX,delete o.pY,v.over.apply(i[0],[e]);o.pX=t,o.pY=n,o.timeoutId=setTimeout(function(){r(e,i,o,v)},v.interval)};e.fn.hoverIntent=function(t,n,v){var a=o++,s=e.extend({},i);e.isPlainObject(t)?(s=e.extend(s,t),e.isFunction(s.out)||(s.out=s.over)):s=e.isFunction(n)?e.extend(s,{over:t,out:n,selector:v}):e.extend(s,{over:t,out:t,selector:n});var f=function(t){var n=e.extend({},t),i=e(this),o=i.data("hoverIntent");o||i.data("hoverIntent",o={});var v=o[a];v||(o[a]=v={id:a}),v.timeoutId&&(v.timeoutId=clearTimeout(v.timeoutId));var f=v.event="mousemove.hoverIntent.hoverIntent"+a;if("mouseenter"===t.type){if(v.isActive)return;v.pX=n.pageX,v.pY=n.pageY,i.off(f,u).on(f,u),v.timeoutId=setTimeout(function(){r(n,i,v,s)},s.interval)}else{if(!v.isActive)return;i.off(f,u),v.timeoutId=setTimeout(function(){!function(e,t,n,i){var o=t.data("hoverIntent");o&&delete o[n.id],i.apply(t[0],[e])}(n,i,v,s.out)},s.timeout)}};return this.on({"mouseenter.hoverIntent":f,"mouseleave.hoverIntent":f},s.selector)}});

//Module global variables
var lvl = 0, pass = 0, currentURL = "", rootUrl = location.origin, api = window.history && window.history.pushState && window.history.replaceState,

//Regexes for escaping fetched HTML of a whole page - best of Baluptons Ajaxify
//Makes it possible to pre-fetch an entire page
docType = /<\!DOCTYPE[^>]*>/i,
tagso = /<(html|head|link)([\s\>])/gi,
tagsod = /<(body)([\s\>])/gi,
tagsc = /<\/(html|head|body|link)\>/gi,

//Helper strings
div12 = '<div class="ajy-$1"$2',
divid12 = '<div id="ajy-$1"$2',
linki = '<link rel="stylesheet" href="*" />',
scri = '<script src="*"></script>',
linkr = 'link[href*="!"]', 
scrr = 'script[src*="!"]',
inlineclass = "ajy-inline";

//Module global classes
let pages, memory, cache1, getPage, fn;

//Minified pO() function - for documentation of pO() please refer to https://4nf.org/po/
var funStr,logging=!1,codedump=!1;let getParamNames=()=>funStr.slice(funStr.indexOf("(")+1,funStr.indexOf(")"));function JSON2Str(n,t){let e="";return Object.entries(n).forEach(([n,o],r)=>{e+=`${r?",\n":""}`+("function"==typeof o?`_${n} = ${iLog(o.toString(),n)}`:`${n} = ${t?'settings["':""}${t?n+'"]':JSON.stringify(o)}`)}),e?`let ${e}${0!=t?";":""}`:""}function pO(n,t,e,o,r,s){let i,l,u,g,f,$,c,a,p="",d="",O="";if(!n||!o)return console.log("Error in pO(): Missing parameter");if(funStr=iLog(funStr=o.toString(),n),i=n.substr(0,1).toUpperCase()+n.substr(1,n.length-1),g=(l=getParamNames(o)).indexOf("$this")+1,f=l.indexOf("options")+1,u=l.replace("$this, ",""),u="$this"==l?"":u,e&&!f&&(u+=""===u?"options":", options"),t&&(p=JSON2Str(t)),e&&(d=`let settings = $.extend(${JSON.stringify(e)}, options);\n${JSON2Str(e,1)}`),r&&(O=JSON2Str(r,0)),a=`\n(function ($) { class ${i} {\n        constructor(${$=e?"options":""}) {\n            ${p}\n            ${d}\n            this.a = ${funStr};\n            ${O}\n        }\n    }\n\n    $.${c=g?"fn."+n:n} = function(${u}) {${g?"let $this = $(this);":""}\n        if(!$.${c}.o) $.${c}.o = new ${i}(${$});\n        return $.${c}.o.a(${l});\n    };\n})(jQuery);`,1!=codedump&&codedump!==i.toLowerCase()||console.log(a),!s)try{jQuery.globalEval(a)}catch(n){console.log(`Error: ${n} | ${a}`)}}function showArgs(n){s="";for(var t=0;t<n.length;t++)null==n[t]?s+="null | ":s+=(null!=n[t]&&"function"!=typeof n[t]&&"object"!=typeof n[t]&&("string"!=typeof n[t]||n[t].length<=100)?n[t]:"string"==typeof n[t]?n[t].substr(0,100):typeof n[t])+" | ";return s}function iLog(n,t){if(n=n.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,""),!logging||"log"===t)return n;let e=n.indexOf("=>")<30?n.indexOf("=>")+1:0,o=n.indexOf("{")+1;e&&(n=n.replace(/(	|\r\n|\n|\r)/gm,""),(!o||o>e+5)&&(n=`${n.substr(0,e+2)}{ return ${n.substr(e+1)}}`),n="function ("+n.substr(0,n.indexOf("{")-3).trim().replace(/\(/g,"").replace(/\)/g,"")+")"+n.substr(n.indexOf("{")).trim()),o=n.indexOf("{");let r=n.substr(n.indexOf("("),n.indexOf(")")-n.indexOf("(")+1).replace(/"/g,'\\"').replace(/'/g,"\\'");return`${n.substr(0,o)}{$.log(lvl + " | ${t} | ${r} | " + showArgs(arguments)${2==logging?", -1, true, arguments":""}); try { lvl++; ${n.substr(o+1,n.length-o-2)}} finally {lvl--;}}`}pO("log",0,{verbosity:0},function(n,t,e,o){if(t>=0&&(verbosity=t),verbosity&&n&&lvl<=verbosity&&console&&1==e)return console.groupCollapsed(n),console.table(o),console.groupCollapsed("Trace"),console.trace(),console.groupEnd(),void console.groupEnd();verbosity&&n&&lvl<=verbosity&&console&&console.log(n)});


//Global helpers
function _trigger(t, e){ e = e ? e : jQuery.rq("e"); jQuery(window).trigger("pronto." + t, e); }
function _internal(url) {
	if (!url) return false;
	if (typeof(url) === "object") url = url.href;
	if (url==="") return true;
	return url.substring(0,rootUrl.length) === rootUrl || !url.iO(":");
}

function _copyAttributes(el, $S, flush) { //copy all attributes of element generically
	if (flush) [...el.attributes].forEach(e => el.removeAttribute(e.name)); //delete all old attributes
	[...$S[0].attributes].forEach(e => el.setAttribute(e.nodeName, e.nodeValue)); //low-level insertion
}

function Hints(hints) {	 var myHints = (typeof hints === 'string' && hints.length) ? hints.split(", ") : false; //hints are passed as a comma separated string
	this.find = t => (!t || !myHints) ? false : myHints.some(h => t.iO(h)) //iterate through hints within passed text (t)
}

function lg(m){ gsettings.verbosity && console && console.log(m); }

// The stateful Cache class
// Usage - parameter "o" values: 
// none - returns currently cached page
// <URL> - returns page with specified URL
// <jQuery object> - saves the page in cache
// f - flushes the cache
class classCache1 { constructor() {
	let d = false;
            
	this.a = function (o) {
		if (!o) return d; 
	
		if (typeof o === "string") { //URL or "f" passed
			if(o === "f") { //"f" passed -> flush
				pages.a("f"); //delegate flush to $.pages
				lg("Cache flushed");
			} else d = pages.a(memory.a(o)); //URL passed -> look up page in memory

			return d; //return cached page
		}

		if (typeof o === "object") { 
			d = o; 
			return d; 
		}
	};          
 }}

// The stateful Memory class
// Usage: memory.a(<URL>) - returns the same URL if not turned off internally
class classMemory { constructor(options) {
	let hints = 0, memoryoff = gsettings.memoryoff;

	this.a = function (h) {
		if(!hints) hints = new Hints(memoryoff); 
		if (!h || memoryoff === true) return false; 
		if (memoryoff === false) return h; 
		return hints.find(h) ? false : h; 
	};           
}}

// The stateful Pages class
// Usage - parameter "h" values:
// <URL> - returns page with specified URL from internal array
// <jQuery object> - saves the passed page in internal array
// false - returns false
class classPages { constructor() {
	let d = [], i = -1;
            
    this.a = function (h) {
		if (typeof h === "string") { 
			if(h === "f") d = []; 
			else if((i=_iPage(h)) !== -1) return d[i][1]; 
		}

		if (typeof h === "object") { 
			if((i=_iPage(h)) === -1) d.push(h); 
			else d[i] = h; 
		}

		if (typeof h === "boolean") return false; 
	};
		
	let _iPage = h => d.findIndex(e => e[0] == h)
}}

// The GetPage class
// First parameter (o) is a switch: 
// empty - returns cache
// <URL> - loads HTML via Ajax, second parameter "p" must be callback
// + - pre-fetches page, second parameter "p" must be URL, third parameter "p2" must be callback 
// - - loads page into DOM and handle scripts, second parameter "p" must hold selection to load
// x - returns XHR
// otherwise - returns selection of current page to client

class classGetPage { constructor() {
	let xhr = 0, cb = 0, plus = 0, rt = "", ct = 0;
            
	this.a = function (o, p, p2) { 
		if (!o) return cache1.a(); 

		if (o.iO("/")) { 
			cb = p; 
			if(plus == o) return; 
			return _lPage(o); 
		}

		if (o === "+")	{ 
			plus = p; 
			cb = p2; 
			return _lPage(p, true); 
		}

		if (o === "a") { if (xhr && xhr.readyState !== 4) xhr.abort(); return; }
		if (o === "s") return ((xhr) ? xhr.readyState : 4) + rt; 
		if (o === "-") return _lSel(p); 
		if (o === "x") return xhr; 

		if (!cache1.a()) return;
		if (o === "body") return cache1.a().find("#ajy-" + o);
		if (o === "script") return cache1.a().find(o); 

		return cache1.a().find(o === "title" ?	"title:first" : ".ajy-" + o); 
};
let _lSel = $t => (
	pass++, 
	_lEls($t), 
	jQuery("body > script").remove("." + inlineclass), 
	jQuery.scripts(true), 
	jQuery.scripts("s"), 
	jQuery.scripts("c") 
),
	_lPage = (h, pre) => { 
		if (h.iO("#")) h = h.split("#")[0]; 
		if (jQuery.rq("is") || !cache1.a(h)) return _lAjax(h, pre); 

		plus = 0; 
		if (cb) return cb(); 
	},
	_ld = ($t, $h) => { 
		if(typeof $h[0] == "undefined") { 
			lg("Inserting placeholder for ID: " + $t.attr("id"));
			var tagN = $t.prop("tagName").toLowerCase();
			$t = $t.replaceWith("<" + tagN + " id='" + $t.attr("id") + "'></" + tagN + ">"); 
			return; 
		}

		var $c = $h.clone(); 
		$c.find("script").remove(); 
		_copyAttributes($t[0], $c, true); 
		$t.html($c.html()); 
	},
	_lEls = $t => 
		cache1.a() && !_isBody($t) && $t.each(function() { 
			_ld(jQuery(this), cache1.a().find("#" + jQuery(this).attr("id"))); 
		}),
	_isBody = $t => $t.prop("tagName").toLowerCase() == "body" 
		&& (_ld(jQuery("body"), cache1.a().find("#ajy-body")), 1),
	_lAjax = (hin, pre) => { 
		var ispost = jQuery.rq("is"); 
		if (pre) rt="p"; else rt="c"; 

		xhr = jQuery.ajax({ 
		url: hin, 
		type: ispost ? "POST" : "GET", 
		data: ispost ? jQuery.rq("d") : null, 
		success: h => { 
			if (!h || !_isHtml(xhr)) { 
				if (!pre) location.href = hin; 
			}

			cache1.a(jQuery(_parseHTML(h))); 
			pages.a([hin, cache1.a()]); 
			plus = 0; 

			if (cb) return(cb()); 
		},
		error: (jqXHR, status, error) => {	
			if (status === 'abort') {plus=0; return;} 
			try {
				xhr = jqXHR; 
				_trigger("error", error); 
				lg("Response text : " + xhr.responseText); 
				cache1.a(jQuery(_parseHTML(xhr.responseText))); 
				pages.a([hin, cache1.a()]); 
				if(cb) return cb(error);  
			} catch (e) {}
		},
		async: true 
		});
	},
	_isHtml = x => (ct = x.getResponseHeader("Content-Type")) && (ct.iO("html") || ct.iO("form-")),
	_parseHTML = h => jQuery.parseHTML(jQuery.trim(_replD(h)), null, true),
	_replD = h => String(h).replace(docType, "").replace(tagso, div12).replace(tagsod, divid12).replace(tagsc, "</div>")
}}

// The main plugin - Ajaxify
// Is passed the global options 
// Checks for necessary pre-conditions - otherwise gracefully degrades
// Initialises sub-plugins
// Calls Pronto
(function ($) { class Ajaxify { constructor(options) {          
	let settings = $.extend({"pluginon":true,"deltas":true,"verbosity":0}, options);
	let pluginon = settings["pluginon"],
	deltas = settings["deltas"],
	verbosity = settings["verbosity"];
	
	this.a = function ($this, options) {
		var o = options;
		if (!o || typeof(o) !== "string") {
			$(function () { 
				gsettings = Object.assign(dsettings, settings);
				pages = new classPages();
				if (_init(settings)) { 
					$this.pronto("i", settings); 
					if (deltas) $.scripts("1"); 
				}
			});
		}
		else return $().pronto(o);
	};
		let _init = s => { 
			if (!api || !pluginon) { 
				lg("Gracefully exiting...");
				return false;
			}
			
			lg("Ajaxify loaded..."); //verbosity option steers, whether this initialisation message is output
			$.scripts("i", s); 
			cache1 = new classCache1();
			memory = new classMemory();
			fn = getPage = new classGetPage();
			return true; 
		}
}}

    $.fn.ajaxify = function(options) {let $this = $(this);
        if(!$.fn.ajaxify.o) $.fn.ajaxify.o = new Ajaxify(options);
        return $.fn.ajaxify.o.a($this, options);
    };
})(jQuery);

// The stateful Scripts plugin
// First parameter "o" is switch:
// i - initailise options
// c - fetch canonical URL
// jQuery object - handle one inline script
// otherwise - delta loading
pO("scripts", { $s : false, inlhints: 0, skphints: 0, txt: 0 }, { canonical: false, inline: true, inlinehints: false, inlineskip: "adsbygoogle", inlineappend: true, style: true }, function (o) {
	if (o === "i") { //Initalise
		if(!$s) $s = $(); //Start off with empty internal jQuery object
		if(!inlhints) inlhints = new Hints(inlinehints); //create Hints object during initialisation
		if(!skphints) skphints = new Hints(inlineskip); //create Hints object during initialisation
		return true;
	}

	if (o === "s") return _allstyle($s.y); //Handle style tags

	if (o === "1") { //Initial load initialisation
		$.detScripts($s); //Fetch scripts from DOM, "pass" variable will be 0
		return _addScripts($s, settings); //Load scripts from DOM into addScripts and initialise it
	}

	if (o === "c") return canonical && $s.can ? $s.can.attr("href") : false; //Canonical URL handling - return href
	if (o === "d") return $.detScripts($s); //fetch all scripts
	if (o instanceof jQuery) return _onetxt(o); //process one inline script only

	if ($.scripts("d")) return; //fetch all scripts
	_addScripts($s, settings); //delta-loading
}, {
	allstyle: $s =>	 //Style tag handling
		!style || !$s || ( //Style shut off or selection empty -> return
		$("head").find("style").remove(), //Remove all style tags in the DOM first
		$s.each(function() { //Iterate through selection
			var d = $(this).text(); //Grab text
			_addstyle(d); //Add single style tag
		})
		)
	,
	onetxt: $s => //Add one inline JS script - pre-processing / validation
		(!(txt = $s.text()).iO(").ajaxify(") && //Extract text and type, avoid unwanted recursion
			((inline && !skphints.find(txt)) || $s.hasClass("ajaxy") || //Check hints, class "ajaxy"
			inlhints.find(txt))
		) && _addtxt($s) //Check constraints
	,
	addtxt: $s => { //Add one inline JS script - main function
		if(!txt || !txt.length) return; //Ensure input
		if(inlineappend || ($s.prop("type") && !$s.prop("type").iO("text/javascript"))) try { return _apptxt($s); } catch (e) { }

		try { $.globalEval(txt); } catch (e1) { //instead of appending, try an eval
			try { eval(txt); } catch (e2) {
				lg("Error in inline script : " + txt + "\nError code : " + e2);
			}
		}
	},
	apptxt: $s => $s.clone().addClass(inlineclass).appendTo("body"), //Add one inline script - label with inlineclass to make dynamic removal later on easy
	addstyle: t => $("head").append('<style>' + t + '</style>'), //add a single style tag
	addScripts: ($s, st) => ( $s.c.addAll("href", st), $s.j.addAll("src", st) )//Delta-loading of sylesheets("href") and external JS files("src")
});
// The DetScripts plugin - stands for "detach scripts"
// Works on "$s" jQuery object that is passed in and fills it
// Fetches all stylesheets in the head
// Fetches the canonical URL
// Fetches all external scripts on the page
// Fetches all inline scripts on the page
pO("detScripts", { head: 0, lk: 0, j: 0 }, 0, function ($s) {
	head = pass ? fn.a("head") : $("head"); //If "pass" is 0 -> fetch head from DOM, otherwise from target page
	if (!head) return true;
	lk = head.find(pass ? ".ajy-link" : "link"); //If "pass" is 0 -> fetch links from DOM, otherwise from target page
	j = pass ? fn.a("script") : $("script"); //If "pass" is 0 -> fetch JSs from DOM, otherwise from target page
	$s.c = _rel(lk, "stylesheet"); //Extract stylesheets
	$s.y = head.find("style"); //Extract style tags
	$s.can = _rel(lk, "canonical"); //Extract canonical tag
	$s.j = j; //Assign JSs to internal selection
	}, { //rel - Extract files that have specific "rel" attribute only
	rel: (lk, v) => $(lk).filter(function(){return($(this).attr("rel").iO(v));})
});


// The AddAll plugin
// Works on a new selection of scripts to apply delta-loading to it 
// pk parameter:
// href - operate on stylesheets in the new selection
// src - operate on JS scripts
pO("addAll", { $scriptsO: [], $sCssO: [], $sO: [], PK: 0, url: 0, hints: 0 }, { deltas: true, asyncdef: false, alwayshints: false }, function ($this, pk) {
	if(!hints) hints = new Hints(alwayshints); //create Hints object during first pass
	if(!$this.length) return; //ensure input
	if(deltas === "n") return true; //Delta-loading completely disabled

	PK = pk; //Copy "primary key" into internal variable

	if(!deltas) return _allScripts($this); //process all scripts
	//deltas presumed to be "true" -> proceed with normal delta-loading

	$scriptsO = PK == "href" ? $sCssO : $sO; //Copy old.  Stylesheets or JS

	if(!pass) _newArray($this); //Fill new array on initial load, nothing more
	else $this.each(function() { //Iterate through selection
		var $t = $(this);
		url = $t.attr(PK);

		if(_classAlways($t)) { //Class always handling
			_removeScript(); //remove from DOM
			_iScript($t); //insert back single external script in the head
			return;
		}
		if(url) { //URL?
			if(!$scriptsO.some(e => e == url)) { // Test, whether new
				$scriptsO.push(url); //If yes: Push to old array
				_iScript($t);
			}
			//Otherwise nothing to do
			return;
		}

		if(PK != "href") $.scripts($t); //Inline JS script? -> inject into DOM
	});
}, {
	allScripts: $t => 
		$t.each(function() { //Iterate through scripts
			_iScript($(this)); //Write out single script
		})
	,
	newArray: $t =>	 //Fill new array on initial load
		$t.each(function() { //Iterate through selection
			if(url = $(this).attr(PK)) $scriptsO.push(url); //Copy over external sheet URLs only
		})
	,
	classAlways: $t => $t.attr("data-class") == "always" || hints.find(url), //Check for data-class = "always" and alwayshints
	iScript: $S => { //insert single script - pre-processing
		url = $S.attr(PK);

		if(PK == "href") return $(linki.replace("*", url)).appendTo("head"); //insert single stylesheet
		if(!url) return $.scripts($S); //insert single inline script

		//Insert single external JS script - we have to go low level to avoid a warning coming from jQuery append()
		//But we'll do our best to support all salient attributes
		var script = document.createElement("script");
		script.async = asyncdef; //initialise with asyncdef - may be overwritten in _copyAttributes
		_copyAttributes(script, $S); //copy all attributes of script element generically
		document.head.appendChild(script); //append to head because some come from the head
	},
	removeScript: () => $((PK == "href" ? linkr : scrr).replace("!", url)).remove() //Remove script (stylesheet or external JS) from DOM
});


// The Rq plugin - stands for request
// Stores all kinds of and manages data concerning the pending request
// Simplifies the Pronto plugin by managing request data separately, instead of passing it around...
// Second parameter (p) : data
// First parameter (o) values:
// = - check whether internally stored "href" ("h") variable is the same as the global currentURL
// ! - update last request ("l") variable with passed href
// ? - Edin's intelligent plausibility check - can spawn an external XHR abort
// v - validate value passed in "p", which is expected to be a click event value - also performs "i" afterwards
// i - initialise request defaults and return "c" (currentTarget)
// h - access internal href hard
// e - set / get internal "e" (event)
// p - set / get internal "p" (push flag)
// is - set / get internal "ispost" (flag whether request is a POST)
// d - set / get internal "d" (data for central $.ajax())
// C - set / get internal "can" ("href" of canonical URL)
// c - check whether simple canonical URL is given and return, otherwise return value passed in "p"
pO("rq", { ispost: 0, data: 0, push: 0, can: 0, e: 0, c: 0, h: 0, l: false}, 0, function (o, p) {
	if(o === "=") { 
		if(p) return h === currentURL //check whether internally stored "href" ("h") variable is the same as the global currentURL
		|| h === l; //or href of last request ("l")
		return h === currentURL; //for click requests
	}

	if(o === "!") return l = h; //store href in "l" (last request)

	if(o === "?") { //Edin previously called this "isOK" - powerful intelligent plausibility check
		let xs=fn.a("s");
		if (!xs.iO("4") && !p) fn.a("a"); //if xhr is not idle and new request is standard one, do xhr.abort() to set it free
		if (xs==="1c" && p) return false; //if xhr is processing standard request and new request is prefetch, cancel prefetch until xhr is finished
		if (xs==="1p" && p) return true; //if xhr is processing prefetch request and new request is prefetch do nothing (see [options] comment below)
		//([semaphore options for requests] fn.a("a") -> abort previous, proceed with new | return false -> leave previous, stop new | return true -> proceed)
		return true;
	}

	if(o === "v") { //validate value passed in "p", which is expected to be a click event value - also performs "i" afterwards
		if(!p) return false; //ensure data
		_setE(p); //Set event and href in one go
		if(!_internal(h)) return false; //if not internal -> report failure
		o = "i"; //continue with "i"
	}

	if(o === "i") { //initialise request defaults and return "c" (currentTarget)
		ispost = false; //GET assumed
		data = null; //reset data
		push = true; //assume we want to push URL to the History API
		can = false; //reset can (canonical URL)
		return h; //return "h" (href)
	}

	if(o === "h") { // Access href hard
		if(p) {
			if (typeof p === "string") e = 0; // Reset e -> default handler
			h = (p.href) ? p.href : p;	// Poke in href hard
		}

		return h; //href
	}

	if(o === "e") { //set / get internal "e" (event)
		if(p) _setE(p);	//Set event and href in one go
		return e ? e : h; // Return "e" or if not given "h"
	}

	if(o === "p") { //set / get internal "p" (push flag)
		if(p !== undefined) push = p;
		return push;
	}

	if(o === "is") { //set / get internal "ispost" (flag whether request is a POST)
		if(p !== undefined) ispost = p;
		return ispost;
	}

	if(o === "d") { //set / get internal "d" (data for central $.ajax())
		if(p) data = p;
		return data;
	}

	if(o === "C") { //set internal "can" ("href" of canonical URL)
		if(p !== undefined) can = p;
		return can;
	}

	if(o === "c") return can && can !== p && !p.iO("#") && !p.iO("?") ? can : p; //get internal "can" ("href" of canonical URL)
}, {
	setE: p =>	//Set event and href in one go
		h = typeof (e = p) !== "string" ? e.currentTarget.href || e.currentTarget.action || e.originalEvent.state.url : e //extract href (link/form submit/history pop)
});

// The Frms plugin - stands for forms
// Ajaxify all forms in the specified divs
// Switch (o) values:
// d - set divs variable
// a - Ajaxify all forms in divs
pO("frms", { fm: 0, divs: 0}, { forms: "form:not(.no-ajaxy)" }, function (o, p) {
	if (!forms || !o) return; //ensure data

	if(o === "d") divs = p; //set divs variable
	if(o === "a") divs.find(forms).filter(function() { //Ajaxify all forms in divs
		let c = $(this).attr("action");
		return(_internal(c && c.length > 0 ? c : currentURL)); //ensure "action"
	}).submit( q => { //override submit handler
		fm = $(q.target); // fetch target
		if (!fm.is("form")) { //is form? -> found
			fm = fm.filter("input[type=submit]").parents("form:first"); //for multiple fields 
			if (fm.length === 0) { //failed?
				return(true); //degrade to default handler
			}
		}

		p = _k(); //Serialise data
		var g = "get", //assume GET
		m = fm.attr("method"); //fetch method attribute
		if (m.length > 0 && m.toLowerCase() == "post") g = "post"; //Override with "post"

		var h, a = fm.attr("action"); //fetch action attribute
		if (a && a.length > 0) h = a; //found -> store
		else h = currentURL; //not found -> select current URL

		$.rq("v", q); //validate request

		if (g == "get") h = _b(h, p); //GET -> copy URL parameters
		else {
			$.rq("is", true); //set is POST in request data
			$.rq("d", p); //save data in request data
		}

		_trigger("submit", h); //raise pronto.submit event
		$().pronto({ href: h }); //programmatically change page

		return(false); //success -> disable default behaviour
	});
}, {
	k: () => { //Serialise data
		let o = fm.serialize(), 
			n = $("input[name][type=submit]", fm);

		if (!n.length) return o; else n = `${n.attr("name")}=${n.val()}`;
		return (o.length) ? `${o}&${n}` : n;
	},
	b: (m, n) => { //copy URL parameters
		if (m.iO("?")) m = m.substring(0, m.iO("?"));
		return `${m}?${n}`;
	}
});

// The stateful Offsets plugin
// Usage:
// 1) $.offsets(<URL>) - returns offset of specified URL from internal array
// 2) $.offsets() - saves the current URL + offset in internal array
pO("offsets", { d: [], i: -1 }, 0, function (h) {
	if (typeof h === "string") { //Lookup page offset
		h = h.iO("?") ? h.split("?")[0] : h; //Handle root URL only from dynamic pages
		i = _iOffset(h); //Fetch offset
		if(i === -1) return 0; // scrollTop if not found
		return d[i][1]; //Return offset that was found
	}

	//Add page offset
	var u = currentURL, us1 = u.iO("?") ? u.split("?")[0] : u, us = us1.iO("#") ? us1.split("#")[0] : us1, os = [us, $(window).scrollTop()];
	i = _iOffset(us); //get page index
	if(i === -1) d.push(os); //doesn't exist -> push to array
	else d[i] = os; //exists -> overwrite
}, {
  iOffset: h => d.findIndex(e => e[0] == h) //find index of page within array
}
);

// The Scrolly plugin - manages scroll effects centrally
// scrolltop values: "s" - "smart" (default), true - always scroll to top, false - no scroll
// Switch (o) values:
// + - add current page to offsets
// ! - scroll to current page offset
pO("scrolly", 0, { scrolltop: "s" }, function (o) {
	if(!o) return; //ensure operator

	var op = o; //cache operator

	if(o === "+" || o === "!") o = currentURL; //fetch currentURL for "+" and "-" operators

	if(op !== "+" && o.iO("#") && (o.iO("#") < o.length - 1)) { //if hash in URL and not standalone hash
		var $el = $("#" + o.split("#")[1]); //fetch the element
		if (!$el.length) return; //nothing found -> return quickly
		_scrll($el.offset().top); // ...animate to ID
		return;
	}

	if(scrolltop === "s") { //smart scroll enabled
		if(op === "+") $.offsets(); //add page offset
		if(op === "!") _scrll($.offsets(o)); //scroll to stored position of page

		return;
	}

	if(op !== "+" && scrolltop) _scrll(0); //otherwise scroll to top of page

	//default -> do nothing

}, {
	scrll: o => $(window).scrollTop(o)
});

// The hApi plugin - manages operatios on the History API centrally
// Second parameter (p) - set global currentURL
// Switch (o) values:
// = - perform a replaceState, using currentURL
// otherwise - perform a pushState, using currentURL
pO("hApi", 0, 0, function (o, p) {
	if(!o) return; //ensure operator
	if(p) currentURL = p; //if p given -> update current URL

	if(o === "=") history.replaceState({ url: currentURL }, "state-" + currentURL, currentURL); //perform replaceState
	else if (currentURL !== window.location.href) history.pushState({ url: currentURL }, "state-" + currentURL, currentURL); //perform pushState
});

// The Pronto plugin - Pronto variant of Ben Plum's Pronto plugin - low level event handling in general
// Works on a selection, passed to Pronto by the selection, which specifies, which elements to Ajaxify
// Switch (h) values:
// i - initialise Pronto
// <object> - fetch href part and continue with _request()
// <URL> - set "h" variable of $.rq hard and continue with _request()
pO("pronto", { $gthis: 0, requestTimer: 0, pfohints: 0, pvohints: 0 }, { selector: "a:not(.no-ajaxy)", prefetchoff: false, refresh: false, previewoff: true, cb: 0, bodyClasses: false, requestDelay: 0, passCount: false }, function ($this, h) {
	if(!h) return; //ensure data

	if(h === "i") { //request to initialise
		var s = settings; //abbreviation
		if(!$this.length) $this = $("body");
		$gthis = $this; //copy selection to global selector
		if(!pfohints) pfohints = new Hints(prefetchoff); //create Hints object during initialisation
		if(!pvohints) pvohints = new Hints(previewoff); //create Hints object during initialisation
		$.frms(0, 0, s); //initialise forms sub-plugin
		if($.slides) $.slides(0, s); //initialise optional slideshow sub-plugin
		$.scrolly(0, s); //initialise scroll effects sub-plugin
		_init_p(); //initialise Pronto sub-plugin
		return $this; //return jQuery selector for chaining
	}

	if(typeof(h) === "object") { //jump to internal page programmatically -> handler for forms sub-plugin
		$.rq("h", h);
		_request();
		return;
	}

	if(h.iO("/")) { //jump to internal page programmatically -> default handler
		$.rq("h", h);				 
		_request(true);
	}
}, { 
	init_p: () => {
		$.hApi("=", window.location.href); // Set initial state
		$(window).on("popstate", _onPop); // Set handler for popState
		if (prefetchoff !== true) {
			$(document).hoverIntent(_prefetch, () => {}, selector); //this type of call also handles dynamically inserted links
			$(document).on("touchstart", selector, _prefetch); // for touchscreens - same thing
		}

		var $body = $("body"); //abbreviation
		$body.on("click.pronto", selector, _click); // Real click handler -> _click()
		$.frms("d", $body); // Select forms in whole body
		$.frms("a"); // Ajaxify forms
		$.frms("d", $gthis); // Every further pass - select forms in content div(s) only
		if($.slides) $.slides("i"); // Init slideshow
	}, 
	prefetch: e => { //...target page on hoverIntent
		if(prefetchoff === true) return;
		if (!$.rq("?", true)) return; //semaphore check for prefetch requests
		var href = $.rq("v", e); // validate internal URL
		if ($.rq("=", true) || !href || pfohints.find(href)) return; //same page, no data or selected out
		fn.a("+", href, () => { //prefetch page
				if (previewoff === true) return(false);
				if (!_isInDivs() && (previewoff === false || !pvohints.find(href))) _click(e, true);
		});
	},
	isInDivs: () => {
		var is = false;
		$gthis.each(function() {
			if ($($.rq("e")).parents("#" + $(this).attr("id")).length > 0) is = true;
		});	

		return is;
	},
	stopBubbling: e => ( // Stop "bubbling-up"
		e.preventDefault(),
		e.stopPropagation(),
		e.stopImmediatePropagation()
	),
	click: (e, notPush) => { //...handler for normal clicks
		if(!$.rq("?")) return; //semaphore check for click requests
		var href = $.rq("v", e);  // validate internal URL
		if(!href || _exoticKey()) return; // Ignore everything but normal click
		if(href.substr(-1) ==="#") return true;
		if(_hashChange()) { // only hash part has changed
			$.hApi("=", href); // commit new URL to History API
			return true; // Enable default behaviour and return - does not invoke a full page load!
		}

		$.scrolly("+"); // Capture old vertical position of scroll bar
		_stopBubbling(e); // preventDefault and stop bubbling-up from here on, no matter what comes next
		if($.rq("=")) $.hApi("="); // if new URL is same as old URL, commit to History API
		if(refresh || !$.rq("=")) _request(notPush); // Continue with _request() when not the same URL or "refresh" parameter set hard
	}, 
	request: notPush => { // ... new url
		$.rq("!"); //we're serious about this request - disable further fetches on same URL
		if(notPush) $.rq("p", false); // mode for hApi - replaceState / pushState
		_trigger("request"); // Fire request event
		fn.a($.rq("h"), err => { // Call "fn" - handler of parent
			if (err) { 
				lg("Error in _request : " + err); 
				_trigger("error", err); 
			}

			_render(); // continue with _render()
		});
	},
	render: () => { // Clear and set timer for requestDelay
		_trigger("beforeload");
		if(requestDelay) { //only needs handling if requestDelay set (not 0)
			if(requestTimer) clearTimeout(requestTimer); // Clear
			requestTimer = setTimeout(_doRender, requestDelay); // Set - unconditionally
		} else _doRender(); //requestDelay is 0 -> continue
	},
	onPop: e => { // Handle back/forward navigation
		$.rq("i"); //Initialise request in general
		$.rq("e", e); //Initialise request event
		$.rq("p", false); //We don't want to re-push
		$.scrolly("+");

		var data = e.originalEvent.state, url = data ? data.url : 0;

		if (!url || url === currentURL) return; // Check if data exists
		_trigger("request"); // Fire request event
		fn.a(url, _render); // Call "fn" - handler of parent, continue with _render()
	},
	doRender: () => { // Render HTML
		_trigger("load");  // Fire load event
		if(bodyClasses) { var classes = fn.a("body").attr("class"); $("body").attr("class", classes ? classes : null); } //Replace body classes from target page

		var href = $.rq("h"), // Retrieve href 
		href = $.rq("c", href); // Fetch canonical if no hash or parameters in URL

		$.hApi($.rq("p") ? "+" : "=", href); // Push new state to the stack on new url
		if (fn.a("title")) $("title").html(fn.a("title").html()); // Update title
		$.rq("C", fn.a("-", $gthis)); // Update DOM and fetch canonical URL
		$.frms("a"); // Ajaxify forms - in content divs only

		// Stop animations + finishing off
		$.scrolly("!"); // Scroll to respective ID if hash in URL, or previous position on page
		_gaCaptureView(href); // Trigger analytics page view
		_trigger("render"); // Fire render event
		if(passCount) $("#" + passCount).html("Pass: " + pass);
		if(cb) cb(); // Callback users handler, if specified
	},
	gaCaptureView: href => { // Google Analytics support
		href = "/" + href.replace(rootUrl,"");
		if (typeof window.ga !== "undefined") window.ga("send", "pageview", href); // the new analytics API
		else if (typeof window._gaq !== "undefined") window._gaq.push(["_trackPageview", href]);  // the old API					
	},
	exoticKey: () => { //not a real click, or target = "_blank", or WP-Admin link
		var href = $.rq("h"), e = $.rq("e"); //Shorthands for href and event
		return (e.which > 1 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.currentTarget.target === "_blank"
			|| href.iO("wp-login") || href.iO("wp-admin"));
	},
	hashChange: () => { // only hash has changed
		var e = $.rq("e");
		return (e.hash && e.href.replace(e.hash, "") === window.location.href.replace(location.hash, "") || e.href === window.location.href + "#");
	}
});

//var fn = getPage; //fn is passed to Pronto as a jQuery sub-plugin, that is a callback