blob: 1c8094d91f804b3bb5ab0e79654a917dd071be7a [file] [log] [blame]
/*
Copyright (c) 2006-2007, The Xooki project
All Rights Reserved.
Licensed under the Apache License version 2.0.
For more information see:
http://www.apache.org/licenses/LICENSE-2.0.txt
Some code is largely inspired by code found in the dojo toolkit,
see http://dojotoolkit.org/ for more information.
*/
var xooki = {};
xooki.console = ""; // used for debugging purpose only, and only when the debug div is not yet created
function t(msg) {
// returns the internationalized version of the message, or the message if no translation is available
// t stands for translate
// FIXME
// if (typeof xooki.c == "object"
// && typeof xooki.c.messages == "object"
// && typeof xooki.c.messages[msg] == "string") {
// msg = xooki.c.messages[msg];
// }
var arr = [];
for (var i=1; i<arguments.length; i++) {
arr.push(arguments[i]);
}
return xooki.string.substituteParams(msg, arr);
}
function css(clss) {
// returns the css class or id configured, or the given class (or id) if no one is configured
if (typeof xooki.c.css[clss] != "undefined") {
return xooki.c.css[clss];
} else {
return clss;
}
}
function u(path) {
// convert a path relative to the root to a full URL
// u stands for Url
return xooki.c.root + path;
}
function cu(urlCfgProp) {
// get a path from a configuration path and convert it to an URL
// cu stands for Configured Url
if (typeof xooki.c.path[urlCfgProp] == "undefined") {
xooki.warn(t("path not configured in xooki: '${0}'", urlCfgProp));
return "";
}
return u(xooki.c.path[urlCfgProp]);
}
function pu(id) {
// returns the url of the page identified by id
// pu stands for Page Url
return u(id+".html");
}
xooki.p = function(path) {
// get a xooki full path from a xooki relative path
// p stands for path
return xooki.c.path.install+"/"+path;
}
xooki.u = function(path) {
// convert a path relative to the xooki installation dir to a full URL
return u(xooki.p(path));
}
xooki.cu = function(urlCfgProp) {
// get a xooki path from a configuration path and convert it to an URL
if (typeof xooki.c.path[urlCfgProp] == "undefined") {
xooki.warn(t("path not configured in xooki: '${0}'", urlCfgProp));
}
return xooki.u(xooki.c.path[urlCfgProp]);
}
xooki.util = {
isArray: function(it) {
return (it && it instanceof Array || typeof it == "array"); // Boolean
},
mix: function(src, into, override) {
if (typeof override == "undefined") {
override = true;
}
for (var k in src) {
if (typeof src[k] == "object" && !xooki.util.isArray(src[k])) {
if (override || typeof into[k] == "object" || typeof into[k] == "undefined") {
if (typeof into[k] != "object") {
into[k] = {};
}
xooki.util.mix(src[k], into[k], override);
}
} else if (override || typeof into[k] == "undefined") {
into[k] = src[k];
}
}
}
}
xooki.url = {
newXmlHttpRequest: function() {
// we first try to use ActiveX, because IE7 has a direct support for XmlHttpRequest object,
// but which doesn't work with file urls
if(window.ActiveXObject)
{
try { req = new ActiveXObject("Msxml2.XMLHTTP");
} catch(e) {
try { req = new ActiveXObject("Microsoft.XMLHTTP");
} catch(e) { req = false; }
}
}
else if(window.XMLHttpRequest) {
try { req = new XMLHttpRequest();
} catch(e) { req = false; }
}
return req;
},
loadURL: function( url ) {
req = this.newXmlHttpRequest();
if(req) {
try {
req.open("GET", url, false);
req.send("");
return req.responseText;
} catch (e) {
xooki.error(e, t("problem while loading URL ${0}", url));
}
}
return null;
},
include: function(script_filename) {
document.write('<' + 'script');
document.write(' language="javascript"');
document.write(' type="text/javascript"');
document.write(' src="' + script_filename + '">');
document.write('</' + 'script' + '>');
},
action: function(action) {
// returns the url for an action on the same page
loc = window.location.toString();
if (loc.indexOf("#") != -1) {
loc = loc.substring(0, loc.indexOf("#"));
}
return loc+"?action="+action;
}
};
xooki.string = {
substituteParams: function(/*string*/template, /* object - optional or ... */hash) {
// borrowed from dojo
// summary:
// Performs parameterized substitutions on a string. Throws an exception if any parameter is unmatched.
//
// description:
// For example,
// dojo.string.substituteParams("File '${0}' is not found in directory '${1}'.","foo.html","/temp");
// returns
// "File 'foo.html' is not found in directory '/temp'."
//
// template: the original string template with ${values} to be replaced
// hash: name/value pairs (type object) to provide substitutions. Alternatively, substitutions may be
// included as an array
var map;
if (typeof hash == "object" && hash.length) { // array
map = {};
for (var i in hash) {
map[i+""] = hash[i];
}
} else {
map = hash;
}
return template.replace(/\$\{(\w+)\}/g, function(match, key){
if(typeof(map[key]) != "undefined" && map[key] != null){
return map[key];
}
xooki.warn("Substitution not found: " + key);
return key;
}); // string
},
processTemplate: function(/*string*/template, /* object */hash) {
if (typeof template.process == "function") {
return template.process(hash);
} else {
return this.substituteParams(template, hash);
}
},
exceptionText: function(e, message) {
var s = e.description ? e.description : e.toString();
return message ? message+":\n"+s : s;
},
findXmlSection: function(str, element, from) {
return this.findSection(str, new RegExp('<'+element+'(\\s*\\w+="[\\w\\s]*")*>'), new RegExp('</'+element+'>'), from);
},
find: function(/*string*/str, /*string or regexp*/exp, /*number, optional*/from) {
// find an expression (string or regexp) in a string, from an optional index
// the object returned has two properties:
// begin: the index in str of the matching find
// end: the index in str of the end of the matching find
// returns null if no match is found
if (typeof from != "number") {
from = 0;
}
if (typeof exp == "string") {
var result = {};
result.begin = str.indexOf(exp,from);
if (result.begin >= 0) {
result.end = result.begin + exp.length;
return result;
}
} else {
var m;
if (from > 0) {
// I haven't found any other way to start from the given index
m = exp.exec(str.substring(from));
} else {
m = exp.exec(str);
}
if (m != null) {
var result = {};
result.begin = m.index + from;
result.end = result.begin + m[0].length;
return result;
}
}
return null;
},
findSection: function(/*string*/str, /*string or regexp*/open, /*string or regexp*/close, /*number, optional*/from) {
// finds a section delimited by open and close tokens in the given string
// the algorithm looks for matching open and close tokens
// the returned object has the following properties:
// outerStart: the index in str where the first open token was found
// innerStart: the index in str just after the found open token
// innerEnd: the index in str where the matching close token was found
// outerEnd: the index in str just after the matching close token
// children: an array of similar objects if nested sections where found
// if no section is found (no open token, an open token with no matching
// close token, or a close token before an open token), null is returned
//
// for instance if open=='(' and close==')' then the section will find
// a section delimited by the first found open parenthesis and the matching
// close parentethis, taking into account other opening parentethis
// examples:
// findSection("a(test)b", "(", ")") == {outerStart: 1, innerStart:2, innerEnd:6, outerEnd:7, children:[]}
// findSection("a(te(s)(t))b", "(", ")") == {outerStart: 1, innerStart:2, innerEnd:10, outerEnd:11,
// children:[
// {outerStart: 4, innerStart:5, innerEnd:6, outerEnd:7, children:[]},
// {outerStart: 7, innerStart:8, innerEnd:9, outerEnd:10, children:[]}
// ]}
var openResult = this.find(str, open, from);
if (openResult == null) {
return null;
}
var closeResult = this.find(str, close, from);
if (closeResult == null || closeResult.begin < openResult.end) {
return null;
}
if (openResult.end <= openResult.begin || closeResult.end <= closeResult.begin) {
// empty match are not allowed
return null;
}
var children = [];
var child = this.findSection(str, open, close, openResult.end);
while (child != null) {
if (child.outerEnd > closeResult.begin) {
closeResult = this.find(str, close, child.outerEnd);
if (closeResult == null) {
// unmatched open token
return null;
}
}
children.push(child);
child = this.findSection(str, open, close, child.outerEnd);
}
return {
outerStart: openResult.begin,
innerStart: openResult.end,
innerEnd: closeResult.begin,
outerEnd: closeResult.end,
children: children
};
}
};
xooki.json = {
evalJson: function (str) {
try {
return eval("("+str+")");
} catch (e) {
return null;
}
},
loadURL: function (url) {
return this.evalJson(xooki.url.loadURL(url));
}
};
// Displays an alert of an exception description with optional message
xooki.warn = function(e, message) {
xooki.display(xooki.string.exceptionText(e, message), "#eecccc");
}
// Displays an alert of an exception description with optional message
xooki.error = function(e, message) {
xooki.display(xooki.string.exceptionText(e, message), "#ffdddd");
}
xooki.info = function(message) {
xooki.display(message, "#ddddff");
}
xooki.display = function(message, background) {
var messages = document.getElementById('xooki-messages');
if (messages) {
messages.innerHTML = '<table width="100%" border="0"><tr><td align="center">'+message+'</td></tr></table>';
messages.style.background = background;
messages.style.display = "inline";
} else {
alert(message);
}
}
xooki.debug = function(message) {
var console = document.getElementById('xooki-console');
if (console) {
console.value += message + "\n";
} else {
xooki.console += message + "\n";
}
}
xooki.debugShowDetail = function (message) {
var detail = document.getElementById('xooki-debug-detail');
if (detail) {
detail.value=message;
} else {
alert(message);
}
}
xooki.html = {
hide: function(divid) {
document.getElementById(divid).style.display = 'none';
},
show: function (divid) {
document.getElementById(divid).style.display = '';
}
};
xooki.component = {
childrenList: function () {
if (xooki.page.children.length > 0) {
childrenList = '<ul class="'+css('childrenList')+'">';
for (var i in xooki.page.children) {
childrenList+='<li><a href="'+pu(xooki.page.children[i].id)+'">'+xooki.page.children[i].title+'</a></li>';
}
childrenList += "</ul>";
return childrenList;
} else {
return "";
}
},
menu: function () {
var menu = '<ul id="'+css("treemenu")+'" class="treeview">';
menu += (function (page) {
var menu = '';
for (var i in page.children) {
menu += '<li id="xooki-'+page.children[i].id+'"><a href="'+pu(page.children[i].id)+'" '+(page.children[i].id == xooki.page.id?'class="current"':'')+'>'+page.children[i].title+'</a>';
smenu = arguments.callee(page.children[i]);
if (smenu != '') {
menu += '<ul ';
if (smenu.indexOf('id="xooki-'+xooki.page.id+'"') != -1
|| page.children[i].id == xooki.page.id) {
// either a descendant or the node processed is the current page node
// we specify that the menu must be opened by default
menu += 'rel="open"';
}
menu += '>'+smenu+'</ul>';
}
menu += '</li>';
}
return menu;
})(xooki.toc);
menu += '</ul>';
return menu;
},
messages: function () {
return '<div id="xooki-messages" onclick="xooki.html.hide(\'xooki-messages\')" style="zIndex:999;display:none;position:absolute;top:30px;padding:10px;border-style:solid;background:#eeeeee;"></div>';
},
debugPanel: function () {
return '<div id="xooki-debug" style="display:none;margin-top:15px;padding:10px;border-style:solid;background:#eeeeee;"><strong>Xooki Console</strong><br/><textarea cols="100" rows="15" id="xooki-console">'+xooki.console+'</textarea><hr/><a href="javascript:xooki.debugShowDetail(document.getElementById(\'xooki-body\').innerHTML)">content</a> <a href="javascript:xooki.debugShowDetail(xooki.c.body)">xooki body</a> <a href="javascript:xooki.debugShowDetail(document.body.innerHTML)">whole body</a> <a href="javascript:xooki.action.evaluate()">evaluate</a><br/><textarea cols="100" rows="15" id="xooki-debug-detail"></textarea></div>';
},
printerFriendlyLocation: function () {
return xooki.url.action("print");
},
printerFriendlyLink: function () {
return '<a href="'+this.printerFriendlyLocation()+'">'+t('Printer Friendly')+'</a>';
}
};
xooki.render = {};
xooki.render.printerFriendly = function() {
for (var k in xooki.component) {
xooki.c[k] = xooki.component[k]();
}
xooki.c.body = xooki.c.messages
+ (function (page, level) {
var source = xooki.url.loadURL(pu(page.id));
if (source == null) {
return "";
}
var beginIndex = source.indexOf('<textarea id="xooki-source">');
beginIndex += '<textarea id="xooki-source">'.length;
var endIndex = source.lastIndexOf('</textarea>');
source = source.substring(beginIndex, endIndex);
var printerFriendly = "<h"+level+">"+page.title+"</h"+level+">";
printerFriendly += xooki.input.format.main(source);
for (var i=0; i <page.children.length; i++) {
printerFriendly += "<hr/>";
printerFriendly += arguments.callee(page.children[i], level+1);
}
return printerFriendly;
})(xooki.page, 1)
+ xooki.c.debugPanel;
document.body.innerHTML = xooki.string.processTemplate(xooki.template.body, xooki.c);
};
xooki.render.page = function() {
// realize all components available
for (var k in xooki.component) {
xooki.c[k] = xooki.component[k]();
}
xooki.input.source();
if (xooki.c.allowEdit) {
xooki.c.body = xooki.c.messages
+ xooki.c.toolbar
+ '<div id="xooki-content">'
+ '<div id="xooki-body"></div>'
+ '</div>'
+ xooki.c.editZone
+ xooki.c.debugPanel;
} else {
xooki.c.body = xooki.c.messages
+ '<div id="xooki-content">'
+ '<div id="xooki-body"></div>'
+ '</div>'
+ xooki.c.debugPanel;
}
document.body.innerHTML = xooki.string.processTemplate(xooki.template.body, xooki.c);
xooki.input.applyChanges();
// enable dynamic tree menu
ddtreemenu.createTree(css("treemenu"), false);
};
xooki.render.main = function() {
if (xooki.c.action == "print") {
// render the printer friendly version of the page
this.printerFriendly();
} else {
// render the page normally
this.page();
}
};
xooki.input = {
source: function() {
if (document.getElementById('xooki-source') != null) {
this._source = document.getElementById('xooki-source').value;
}
return this._source;
},
processed: function() {
return this.format.main(this.source());
},
format: {
getInputFilters: function (inputFormat) {
return xooki.c[inputFormat+"InputFormat"];
},
define: function (inputFormat, filters) {
// define a new inputFormat
// inputFormat: the new input format name
// filters: an array of input filter names
xooki.c[inputFormat+"InputFormat"] = filters;
},
main: function(source) {
// formats an input source
if (xooki.c.inputFormat && typeof this.getInputFilters(xooki.c.inputFormat) != "undefined") {
format = xooki.c.inputFormat;
} else {
format = xooki.c.defaultInputFormat;
}
filters = this.getInputFilters(format);
for (var i in filters) {
f = xooki.input.filters[filters[i]];
if (typeof f == "function") {
source = f(source); // process filter
} else {
xooki.error(t("unknown filter ${0} used in input format ${1}", filters[i], format));
}
}
return source;
}
},
filters: {
url: function (input) {
// handle urls
return input.replace(new RegExp("(?:file|http|https|mailto|ftp):[^\\s'\"]+(?:/|\\b)", "g"), function (str, offset, s) {
var before = s.substring(0,offset);
if (before.match(/(href|src)="$/)) {
return str;
} else {
return '<a href="'+str+'">'+str+'</a>';
}
});
},
xookiLinks: function (input) {
// handle xooki links like this:
// [[page/id]]
// [[page/id My Title]]
return input.replace(new RegExp("\\[\\[([^\\]]+)\\]\\]", "g"), function (str, code, offset, s) {
var index = code.indexOf(' ');
var id = index>0?code.substring(0,index):code;
// TODO: check page existence
var title = index>0?code.substring(index):xooki.toc.pages[id].title;
return '<a href="'+pu(id)+'">'+title+'</a>';
});
},
wikiMarkup: function (input) {
// handle bold
input = input.replace(new RegExp("\\*([^\n]+)\\*", "g"), "<b>$1</b>");
// handle italic
input = input.replace(new RegExp("\\_([^\n]+)\\_", "g"), "<em>$1</em>");
return input;
},
jira: function (input) {
// auto replace jira issue ids (like IVY-12) by a link to the issue
// needs to be configured in xooki config like this
// xooki.c.jira.ids = an array of jira projects ids (ex: ["IVY", "ANT"])
// xooki.c.jira.url = the url of the jira server (ex: "http://issues.apache.org/jira")
if (typeof xooki.c.jira != "object") {
return input;
}
input = input.replace(new RegExp("(("+xooki.c.jira.ids.join("|")+")-\\d+)([^\"\\d])", "g"), '<a href="'+xooki.c.jira.url+'/browse/$1">$1</a>$3');
return input;
},
code: function (input) {
codeSection = xooki.string.findXmlSection(input, "code");
from = 0;
while (codeSection != null) {
processedSection = "<pre>"
+ input.substring(codeSection.innerStart, codeSection.innerEnd).replace(/</g, "&lt;").replace(/>/g, "&gt;") // .replace(/\n/g, "<br/>")
+ "</pre>";
input = input.substring(0, codeSection.outerStart)
+ processedSection
+ input.substring(codeSection.outerEnd);
from = codeSection.outerStart + processedSection.length;
codeSection = xooki.string.findXmlSection(input, "code", from);
}
return input;
},
lineBreak: function (input) {
return input.replace(new RegExp("\r?\n", "g"), function (str, offset, s) {
var before = s.substring(0,offset);
var after = s.substring(offset+str.length);
if (after.match(/^<\/?(ul|table|li|pre|div)(\s*\w+="[^"]+")*\s*>/i) || (before.match(/<\/?\w+(\s*\w+="[^"]+")*\s*\/?>\s*$/i) && !before.match(/<\/?(a|b|strong|em|i|big|br class="xooki-br")(\s*\w+="[^"]+")*\s*\/?>\s*$/i))) {
return '\n';
} else {
return '<br class="xooki-br"/>'; // the class is not really necessary but allow to distinguish generated br from input one
}
});
}
},
applyChanges: function() {
document.getElementById('xooki-body').innerHTML = xooki.input.processed();
}
};
xooki.postProcess = function() {
xooki.render.main();
window.onkeypress = keyCtrl;
};
// init xooki engine
(function() {
////////////////////////////////////////////////////////////////////////////
////////////////// config init
////////////////////////////////////////////////////////////////////////////
initConfigProperty = function(prop, value, defaultValue) {
if (typeof this[prop] == "undefined") {
if (typeof value == "undefined") {
this[prop] = defaultValue;
} else if (typeof value == "function") {
this[prop] = value();
} else {
this[prop] = value;
}
}
};
xooki.config = {};
xooki.c = xooki.config;
if (typeof xookiConfig != "undefined") {xooki.util.mix(xookiConfig, xooki.config);}
xooki.c.initProperty = initConfigProperty;
xooki.c.initProperty("level", 0);
xooki.c.initProperty("root", function() {
root = window.location.toString();
// remove trailing parts of the URL to go the root depending on level
for (var i=0; i < xooki.c.level + 1; i++) {
root = root.substring(0, root.lastIndexOf('/'));
}
return root + '/';
});
var globalConfig = xooki.json.loadURL(u("config.json"));
if (globalConfig != null) {
xooki.util.mix(globalConfig, xooki.config, false);
}
xooki.c.initProperty("defaultInputFormat", "xooki");
xooki.c.initProperty("xookiInputFormat", ["xooki"]);
xooki.c.initProperty("allowEdit", document.location.toString().substr(0,5) == "file:");
xooki.input.format.define("xooki", ["url", "xookiLinks", "jira", "code", "lineBreak"]);
xooki.c.path = (typeof xooki.c.path != "undefined")?xooki.c.path:{};
xooki.c.path.initProperty = initConfigProperty;
xooki.c.path.initProperty("install", "xooki");
xooki.c.path.initProperty("messages", xooki.p("messages.json"));
xooki.c.path.initProperty("template", "template.html");
xooki.c.path.initProperty("printTemplate", "printTemplate.html");
xooki.c.path.initProperty("toc", "toc.json");
xooki.c.path.initProperty("blankPageTpl", xooki.p("blankPageTpl.html"));
xooki.c.css = (typeof xooki.c.css != "undefined")?xooki.c.css:{};
xooki.c.messages = xooki.json.loadURL(cu("messages"));
xooki.c.browser = {
NS: (window.Event) ? 1 : 0
};
// action
// TODO: better handle action extraction
xooki.c.action = window.location.search == '?action=print'?'print':xooki.c.action;
////////////////////////////////////////////////////////////////////////////
////////////////// TOC init
////////////////////////////////////////////////////////////////////////////
xooki.toc = xooki.json.loadURL(cu("toc"));
xooki.toc.pages = {}; // to store a by id map of pages objects
(function(page, parent, index) {
xooki.toc.pages[page.id] = page;
page.meta = {
parent: parent,
index: index
};
if (typeof page.children == 'undefined') {
page.children = [];
} else {
for (var i=0; i<page.children.length; i++) {
arguments.callee(page.children[i], page, i); // recurse
}
}
})(xooki.toc, null, 0);
var match = new RegExp(".*\\/((?:.*\\/){"+xooki.config.level+"}[^\\/]*)(?:\\.\\w+)", "g").exec(window.location.toString());
var curPageId;
if (match == null || match[1] == '') {
curPageId = "index";
} else {
curPageId = match[1];
}
xooki.page = xooki.toc.pages[curPageId];
if (xooki.page == null) {
xooki.warn(t('page id not found in TOC: ${0}',curPageId));
} else {
if (typeof xooki.config.title == 'undefined') {
xooki.config.title = xooki.page.title;
}
}
xooki.config.page = xooki.page;
////////////////////////////////////////////////////////////////////////////
////////////////// main template loading + head output
////////////////////////////////////////////////////////////////////////////
xooki.template = {};
xooki.template.source = xooki.url.loadURL(xooki.c.action == "print"?cu("printTemplate"):cu("template"));
if(xooki.template.source != null) {
xooki.template.head = xooki.template.source.match(/<head>([^§]*)<\/head>/im)[1];
var head = xooki.string.processTemplate(xooki.template.head, xooki.config);
head = head.replace(/href="([^\\$:"]+)"/g, 'href="'+xooki.c.root+'$1"');
document.write(head);
var body = xooki.template.source.match(/<body>([^§]*)<\/body>/im)[1];
body = body.replace(/href="([^\\$:"]+)"/g, 'href="'+xooki.c.root+'$1"');
xooki.template.body = body.replace(/src="([^\\$:"]+)"/g, 'src="'+xooki.c.root+'$1"');
}
////////////////////////////////////////////////////////////////////////////
////////////////// includes
////////////////////////////////////////////////////////////////////////////
xooki.url.include(xooki.u("tree/simpletreemenu.js"));
if (xooki.c.useTrimPath) {
xooki.url.include(xooki.u("trimpath/template.js"));
}
if (xooki.c.allowEdit) {
xooki.url.include(xooki.u("xookiEdit.js"));
}
for (var k in xooki.c) {
if (typeof xooki.c[k] == "string" || typeof xooki.c[k] == "number" || typeof xooki.c[k] == "boolean") {
xooki.debug(k+": "+xooki.c[k]);
}
}
})();
xooki.action = {}
xooki.action.toggleDebug = function() {
if (xooki.c.debug) {
if (document.getElementById('xooki-debug').style.display == 'none') {
xooki.html.show('xooki-debug');
} else {
xooki.html.hide('xooki-debug');
}
}
}
xooki.action.evaluate = function () {
var exp = prompt("Please enter javascript expression to evaluate");
xooki.debugShowDetail(eval(exp));
}
// TODO, review use registration
function keyCtrl(evt) {
var code = xooki.c.browser.NS ? evt.which : event.keyCode;
var ctrl = xooki.c.browser.NS ? evt.ctrlKey : event.ctrlKey;
var key = String.fromCharCode(code);
if (xooki.c.debug && ctrl && "d" == key) {
xooki.action.toggleDebug();
return false;
}
if (xooki.c.allowEdit && ctrl && "s" == key) {
xooki.action.saveChanges();
return false;
}
if (xooki.c.allowEdit && ctrl && "e" == key) {
xooki.action.toggleEdit();
return false;
}
}