/*
Greenstone 3 'Client-side transformer'
Performs client-side transformations of XSLT, using HTML5 local storage (simulated if the browser doesn't support it).
Currently only supports Firefox 3 and greater, since it's the only browser (at the time of writing) that can do this properly.
@author Steven McTainsh
@date 14/02/2011
*/
/* These URLs and file paths are fetched dynamically */
var gsweb = ""; // The file path to the Greenstone 3 web directory
var gsurl = ""; // The root URL for this Greenstone 3 installation
/* Misc. switches and paths */
var keyUrl = ''; // Used across methods to build up query string for text retrieval (client-side transformed version)
var on = true; // Set to false to disable operation
var index = 0; // Used for query array (to keep track of number of elements)
var deferredEls = new Array(); // Elements to defer text retrieval for until later
var queryArr = new Array(); // Text to query for (the text for the corresponding deferred element)
/* Methods */
// This is generally only called from in-line JS in the head (i.e. var text = getText(...);)
function getText(key, el) {
if(localStorage.getItem(key) != null)
return localStorage.getItem(key);
else {
push(deferredEls, el);
queryArr[el] = key; // Have it queried for too
return "";
}
}
function processEl(el, attr, key, append, isText, theText) {
// Assumes all necessary text has been loaded from the servlet
var displayText = (isText) ? theText : localStorage.getItem(key);
if(el == document && attr == 'title') {
// Handle document titles
if(append)
document.title += displayText;
else
document.title = displayText;
} else if(attr == 'innerText') {
// Handle element 'inner text'
if(append)
$(el).html($(el).html() + displayText);
else
$(el).html(displayText);
} else if(attr == 'value') {
// Handle value attribute
if(append)
$(el).val($el.val() + displayText);
else
$(el).val(displayText);
} else {
// All other cases (generic attributes)
if(append)
$(el).attr(attr, $(el).attr(attr) + displayText);
else
$(el).attr(attr, displayText);
}
}
function notSupported() {
// Set not supported cookie here
document.cookie = 'supportsXSLT=false; expires=0; path=/';
// Fall back to server version
var location = window.location.search.substring(1);
if(location == '') {
// Start with a question mark
location = window.location + "?o=server";
} else {
// Start with an ampersand
location = window.location + "&o=server";
}
window.location = location;
}
$(document).ready(function() {
if(on && isSupported()) {
if(placeholder) {
// Need to prepare page placeholders
transform(false);
}
else {
// Need to place text into the page (placeholders ready)
applyText(false);
}
}
});
function applyText(trial) {
try {
// Wait for document to be ready before propogating this URL (otherwise undefined)
keyUrl = 'grabtext?i='+$('#interface').html()+'&l='+$('#language').html()+'&k='
// First, see if the local storage should be emptied for a new language...
if(localStorage.getItem("_activeLanguage") != $('#language').html()) {
// Languages are different, clear existing strings
localStorage.clear();
localStorage.setItem("_activeLanguage", $('#language').html());
}
$('#loading').show();
var query = '';
$('.getTextFor').each(function() {
// Build up query
// Get the three parts to the class - 'getTextFor' 'element inner text to affect' 'attributes to affect'
// Proper splitting on spaces
var parts = parseAction($(this).attr('class'), ' ')
// parts[1] is this element's innerText key, parts[2] may or may not contain keys
// Can add null to the array
if(!contains(queryArr, parts[1]) && localStorage.getItem(parts[1]) == null)
// If the key has not already been queried for and is not in local storage...
// NO duplicates!
queryArr[index++] = parts[1];
if(parts.length == 3 && parts[2] != '') {
var affected = parts[2].split(',');
for(var j = 0; j < affected.length; j++) {
var els = parseAction(affected[j], ".");
// els[2] will contain key
// Ignore text, and those elements with text already in local storage and that will already be queried for
if(els[2].indexOf('text:') != 0 && !contains(queryArr, els[2]) && localStorage.getItem(els[2]) == null)
queryArr[index++] = els[2].replace("[a]", "");
}
}
});
query = queryArr.join(",");
// Fetch!
if(query != '') { // There is something to query for
$.get(keyUrl + query, function(data) {
// Process dictionary, load into local storage
$(data).find("item").each(function() {
localStorage.setItem($(this).attr('key'), $(this).attr('value'));
});
assignText();
}, 'xml');
} else { assignText(trial); }
} catch (e) { if(trial) notSupportedCookie(); else notSupported(); }
}
function assignText(trial) {
try {
$('.getTextFor').each(function() {
var parts = parseAction($(this).attr('class'), " ");
var me = parts[1];
// Process it's own inner text first
processEl($(this), 'innerText', me);
if(parts.length >= 3 && parts[2] != '') { // Otherwise 'x y ' mucks it up
// Affecting elements are listed
var affectees = parts[2];
var all = affectees.split(',');
for(var i = 0; i < all.length; i++) {
var parts = parseAction(all[i], ".");
var append = false;
var isText = false;
var theText = '';
if(parts[2].indexOf("[a]") != -1) {
parts[2] = parts[2].replace("[a]", "");
append = true;
}
if(parts[2].indexOf("text:'") != -1) {
// This action is plain text
isText = true;
theText = parts[2].substring(6, parts[2].length - 1);
}
if(parts[0] == 'parent') {
// Affects the parent
processEl($(this).parent(), parts[1], parts[2], append, isText, theText);
} else if(parts[0] == 'this') {
// Affects this element
processEl($(this), parts[1], parts[2], append, isText, theText);
} else if(parts[0] == 'document') {
// Affects document itself
processEl(document, parts[1], parts[2], append, isText, theText);
}
}
}
// Process deferred elements now
for(var def in deferredEls) {
$(def).html(localStorage[deferredEls[def]]); //dict[deferredEls[def]]);
}
});
var xsltClientCapable = document.cookie.indexOf('supportsXSLT') != -1;
// At this point, the browser has proven itself capable of XSL transformations
// So, modify all document links into JavaScript links that use AJAX to get content
if(xsltClientCapable) {
// Could set cookie here too?
$('.clientDocView').each(function() {
var link = $(this).attr('href');
var startIndex = link.indexOf("&d=");
var endIndex = link.indexOf('&', startIndex + 7);
// If the endIndex == -1, end not found; just use end index
endIndex = (endIndex == -1) ? link.length : endIndex;
var docID = link.substring(startIndex, endIndex);
docID = docID.replace("&d=", "");
$(this).attr('id', docID);
$(this).attr('onclick', "getNodeContent(this); return false;"); // .bind doesn't work for dynamically added HTML!
if(typeof initialHash != 'undefined') // Prevent errors when searching
if(docID == initialHash) {
// Load it!
getNodeContent($(this).get()[0]); // Talk about round the houses! This was the only way that produced something!
}
});
document.cookie = 'supportsXSLT=true; expires=0; path=/';
}
// Also undo escaping of necessary entities
$('body').each(function() {
var text = $(this).html();
text = text.replace(/</g, "<");
text = text.replace(/>/g, ">");
text = text.replace(/&/g, "&");
$(this).html(text);
});
// Remove dead elements
$('[xmlns]').each(function() {
if($(this).get(0).tagName.toLowerCase() != "html") {
// use toLower() in case of browser inconsistencies
$(this).hide();
}
});
//alert('Language is: ' + $('#language').html() + ', interface is: ' + $('#interface').html());
$('#loading').fadeOut();
} catch (e) { if(trial) notSupportedCookie(); else notSupported(); }
}
function transform(trial) {
try {
var queryStr = window.location.search.substring(1);
var rooturl = window.location;
//var absolute = gsurl;
var lead = queryStr == '' ? '?' : '&';
var skinurl = rooturl+lead+"o=skinandlibdoc";
var xmlurl = rooturl+lead+"o=xml";
var skindoc = "";
var xmldoc = "";
$.get(rooturl + lead + "o=clientside", function(data) {
data = data.replace(/gs3:id/g, "gs3id");
data = parseFromString(data, "text/xml");
// data contains the XML and the stylesheet too!
var skindoc = $(data).find("xslt\\:stylesheet")[0];
var xmldoc = $(data).find("page")[0];
gsurl = $($(xmldoc).find("metadata[name=siteURL]")[0]).text();
gsweb = new RegExp($($(xmldoc).find("param[name=filepath]")[0]).text().replace(/\\/g, "\\\\"), "g");
// Find xsl:include elements and update hrefs accordingly
$(skindoc).find("xsl\\:include").each(function() {
$(this).attr('href', $(this).attr('href').replace(gsweb, gsurl).replace(/\\/g, "/"));
});
$(xmldoc).find("xsl\\:include").each(function() {
$(this).attr('href', $(this).attr('href').replace(gsweb, gsurl).replace(/\\/g, "/"));
});
// Convert temporarily to text here
skindoc = convertToString(skindoc);
xmldoc = convertToString(xmldoc);
// This could just be done on the server (later)
//data = "\r\n" /*+ "\r\n"*/ + data;
// replace all!
// Be careful with regex syntax and the use of special characters!
skindoc = skindoc.replace(/util\:exists\(\$meta, ''\)/g, "$meta!=''"); // For now - use regex instead
skindoc = skindoc.replace(/%3A/g, ":"); // undo colon escaping
// Convert to XML
xmldoc = parseFromString(xmldoc, "text/xml");
skindoc = parseFromString(skindoc, "text/xml");
var output = '';
var library_name = $('/page/xsltparams/param[name=library_name]', xmldoc).text();
var interface_name = $('/page/xsltparams/param[name=interface_name]', xmldoc).text();
// And post-process...
if(window.ActiveXObject) {
// IE
var procFactory = new ActiveXObject("MSXML2.XSLTemplate");
procFactory.stylesheet = skindoc;
var proc = procFactory.createProcessor();
proc.input = xmldoc;
proc.addParameter('library_name', library_name);
proc.addParameter('interface_name', interface_name);
proc.transform();
output = proc.output;
} else {
// Fx
xsltProc = new XSLTProcessor();
// The leading slash is oh-so important here
xsltProc.setParameter(null, 'library_name', library_name);
xsltProc.setParameter(null, 'interface_name', interface_name);
xsltProc.importStylesheet(skindoc);
result = xsltProc.transformToDocument(xmldoc);
output = (new XMLSerializer()).serializeToString(result);
}
//output = output.replace('var placeholder = false;', 'var placeholder = false; var xsltClientCapable = true;');
var doc = document.open();
doc.write(output);
doc.close();
document.cookie = 'supportsXSLT=true; expires=0; path=/';
}, 'html');
} catch (e) { if(trial) notSupportedCookie(); else notSupported(); }
}
function notSupportedCookie() {
document.cookie = 'supportsXSLT=false; expires=0; path=/';
}
function getNodeContent(node) {
var hash = $(node).attr('id');
// Some document titles appear within spans, some do not, so these adjustments are necessary
$('.clientDocView').next().css('font-weight', 'normal');
$('.clientDocView').parent().css('font-weight', 'normal');
$(node).parent().css('font-weight', 'bold');
$(node).next().css('font-weight', 'bold');
var loadingimg = '';
// Fetch me some fresh XML
$('.documenttext').html(loadingimg);
var url = $(node).attr('href') + "&o=xml";
url = url.replace('&', '&');
$.get(url, function(data) {
gsurl = $($(data).find("metadata[name=siteURL]")[0]).text();
$.get(gsurl + '/interfaces/basic/transform/document_text.xsl', function(text) {
var output = '';
var result = '';
try {
// FF only
xsltProc = new XSLTProcessor();
// The leading slash is oh-so important here
xsltProc.setParameter(null, 'library_name', $('/page/xsltparams/param[name=library_name]', data).text());
xsltProc.setParameter(null, 'interface_name', $('/page/xsltparams/param[name=interface_name]', data).text());
xsltProc.importStylesheet(text);
result = xsltProc.transformToDocument(data);
output = (new XMLSerializer()).serializeToString(result);
} catch(e) { window.location = $(node).attr('href'); } // will naturally cascade to client-side, then if necessary, server-side
// Get the book name
var book = $('#documentheading').text();
var title_arr = new Array();
var temp_arr = new Array();
title_arr.push(book);
// Go up the tree, until you reach the 'Table of Contents' node
$(node).parentsUntil('#tocstart').each(function() {
if($(this).get()[0].tagName == 'LI') {
// Only list elements...
// next child = a, next sibling = text node
temp_arr.push($(this).children('span').text());
}
});
// Store these temporarily to reverse them and get it right
temp_arr = temp_arr.reverse();
title_arr = title_arr.concat(temp_arr);
document.title = title_arr.join('::');
// Fix up entity encoding first
output = output.replace(/&/g, '&');
output = output.replace(/</g, '<');
output = output.replace(/>/g, '>');
$('.documenttext').html(output);
}, 'xml');
}, 'xml');
}
function parseAction(str, chr) {
// Problem with split - seems to discard the rest of string if it encounters splitable characters but doesn't split on them
// But, then all the split characters are missing from the array! So, need to slice the substring out.
// Get additional text
var parts = str.split(chr, 3);
var trimIndex = parts[0].length + parts[1].length + 2;
parts[2] = str.substring(trimIndex);
return parts;
}
// Method equivalent to PHP's in_array method
function contains(array, value) {
for(var val in array)
if(array[val] == value)
return true;
return false;
}
// Method equivalent to PHP's own
function print_r(arr) {
var result = "";
for(var a in arr) {
result += a + " => " + arr[a] + "\r\n";
}
return result;
}
function convertToString(content) {
try {
// If this fails, it's another indication that the browser doesn't have the support we need
if(typeof XMLSerializer != 'undefined') {
return (new XMLSerializer()).serializeToString(content);
} else {
return content.xml;
}
} catch (e) { notSupported(); }
}
function parseFromString(content, contentType) {
try {
var retobj;
if(typeof window.DOMParser != 'undefined') {
// Fx
retobj = (new DOMParser()).parseFromString(content, contentType);
} else {
// IE
var retobj = new ActiveXObject("Microsoft.XMLDOM");
retobj.async = "false";
retobj.loadXML(content);
}
return retobj;
} catch(e) { var obj = new ActiveXObject('MSXML.DomDocument'); obj.async = false; obj.loadXML(content); return obj; }
}
function isSupported() {
// Are cookies enabled?
if(navigator.cookieEnabled && typeof navigator.cookieEnabled != 'undefined') {
// Is there a cookie?
if(document.cookie.indexOf('supportsXSLT=') > -1) {
// Cookie exists - don't try the transformation, as the server will
// read the cookie and determine which version to serve up.
// If it happens to be client-side, allow transformation to proceed
return (document.cookie.indexOf('supportsXSLT=true') > -1);
} else {
// Cookie doesn't exist - test!
transform(true);
applyText(true);
return (document.cookie.indexOf('supportsXSLT=true') > -1);
}
} else {
return false;
}
}
/* Simulating web storage for browsers that don't support it */
/* Credit: http://www.thewojogroup.com/2010/01/simulating-html5-local-storage/ */
(function(){var k=this;if(!k.localStorage&&navigator.cookieEnabled){var x="storageData_",y="++",z="::",l=function(a,c,b){var e=new Date;e.setTime(e.getTime()+b);b="; expires="+e.toGMTString();document.cookie=a+"="+c+b+"; path=/"},h=function(a){a=a+"=";for(var c=document.cookie.split(";"),b=0,e=c.length;ba.length?a.length-b*4E3:4E3),2592E3):m(y+b);b++}},f=k.localStorage={length:0,setItem:function(a,c){var b=i(),e=0,d=b.length,g=false;for(e=0;e