doc import in xooki format: mere generation using ruby script, still require review, especially on images (not handled by script) and links (most links are ok, but those pointing outside the documentation book -like features for example- are not imported yet

git-svn-id: https://svn.apache.org/repos/asf/incubator/ivy/trunk/src/doc/xooki/xooki@488342 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/blankPageTpl.html b/blankPageTpl.html
new file mode 100644
index 0000000..ea3fd64
--- /dev/null
+++ b/blankPageTpl.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">

+<html>

+<head>

+	<script type="text/javascript">var xookiConfig = {level: ${level}};</script>	

+	<script type="text/javascript" src="${relroot}xooki/xooki.js"></script>

+</head>

+<body>

+	<textarea id="xooki-source">

+	</textarea>

+<script type="text/javascript">xooki.postProcess();</script>

+</body>

+</html>

diff --git a/images/addchild.gif b/images/addchild.gif
new file mode 100644
index 0000000..252d7eb
--- /dev/null
+++ b/images/addchild.gif
Binary files differ
diff --git a/images/bullet.gif b/images/bullet.gif
new file mode 100644
index 0000000..a1b1748
--- /dev/null
+++ b/images/bullet.gif
Binary files differ
diff --git a/images/closed.gif b/images/closed.gif
new file mode 100644
index 0000000..319fe8d
--- /dev/null
+++ b/images/closed.gif
Binary files differ
diff --git a/images/debug.gif b/images/debug.gif
new file mode 100644
index 0000000..afe775b
--- /dev/null
+++ b/images/debug.gif
Binary files differ
diff --git a/images/delete.gif b/images/delete.gif
new file mode 100644
index 0000000..64b4384
--- /dev/null
+++ b/images/delete.gif
Binary files differ
diff --git a/images/down.gif b/images/down.gif
new file mode 100644
index 0000000..a7c4918
--- /dev/null
+++ b/images/down.gif
Binary files differ
diff --git a/images/edit.gif b/images/edit.gif
new file mode 100644
index 0000000..0dc862c
--- /dev/null
+++ b/images/edit.gif
Binary files differ
diff --git a/images/open.gif b/images/open.gif
new file mode 100644
index 0000000..bd0be79
--- /dev/null
+++ b/images/open.gif
Binary files differ
diff --git a/images/save.gif b/images/save.gif
new file mode 100644
index 0000000..499dd0c
--- /dev/null
+++ b/images/save.gif
Binary files differ
diff --git a/images/up.gif b/images/up.gif
new file mode 100644
index 0000000..49a1461
--- /dev/null
+++ b/images/up.gif
Binary files differ
diff --git a/messages.json b/messages.json
new file mode 100644
index 0000000..bed91b9
--- /dev/null
+++ b/messages.json
@@ -0,0 +1,3 @@
+{

+        	"Impossible to save changes to ${0}": "Impossible to save changes to ${0}.\n This could be because your browser doesn't support saving (instead, use FireFox if you can), or because the pathname to your Xooki file contains illegal characters"

+}

diff --git a/tiddly/util.js b/tiddly/util.js
new file mode 100644
index 0000000..d4d21ea
--- /dev/null
+++ b/tiddly/util.js
@@ -0,0 +1,174 @@
+// utility functions borrowed from tiddly wiki: http://www.tiddlywiki.com/

+// Tiddly Wiki License:

+/*

+TiddlyWiki 2.1.3 by Jeremy Ruston, (jeremy [at] osmosoft [dot] com)

+

+Copyright (c) Osmosoft Limited 2004-2006

+

+Redistribution and use in source and binary forms, with or without modification,

+are permitted provided that the following conditions are met:

+

+Redistributions of source code must retain the above copyright notice, this

+list of conditions and the following disclaimer.

+

+Redistributions in binary form must reproduce the above copyright notice, this

+list of conditions and the following disclaimer in the documentation and/or other

+materials provided with the distribution.

+

+Neither the name of the Osmosoft Limited nor the names of its contributors may be

+used to endorse or promote products derived from this software without specific

+prior written permission.

+

+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY

+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES

+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT

+SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,

+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED

+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR

+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN

+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN

+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH

+DAMAGE.

+*/

+

+if (typeof xooki.io == "undefined") {

+    xooki.io = {};

+}

+

+xooki.io.saveFile = function (fileUrl, content) {

+	var r = null;

+	if((r == null) || (r == false))

+		r = mozillaSaveFile(fileUrl, content);

+	if((r == null) || (r == false))

+		r = ieSaveFile(fileUrl, content);

+	return(r);

+}

+

+xooki.io.loadFile = function (fileUrl) {

+	var r = null;

+	if((r == null) || (r == false))

+		r = mozillaLoadFile(fileUrl);

+	if((r == null) || (r == false))

+		r = ieLoadFile(fileUrl);

+	return(r);

+}

+

+xooki.io.getLocalPath = function (originalPath) {

+	// Remove any location or query part of the URL

+	var argPos = originalPath.indexOf("?");

+	if(argPos != -1)

+		originalPath = originalPath.substr(0,argPos);

+	var hashPos = originalPath.indexOf("#");

+	if(hashPos != -1)

+		originalPath = originalPath.substr(0,hashPos);

+	// Convert file://localhost/ to file:///

+	if(originalPath.indexOf("file://localhost/") == 0)

+		originalPath = "file://" + originalPath.substr(16);

+	// Convert to a native file format assuming

+	// "file:///x:/path/path/path..." - pc local file --> "x:\path\path\path..."

+	// "file://///server/share/path/path/path..." - FireFox pc network file --> "\\server\share\path\path\path..."

+	// "file:///path/path/path..." - mac/unix local file --> "/path/path/path..."

+	// "file://server/share/path/path/path..." - pc network file --> "\\server\share\path\path\path..."

+	var localPath;

+	if(originalPath.charAt(9) == ":") // pc local file

+		localPath = unescape(originalPath.substr(8)).replace(new RegExp("/","g"),"\\");

+	else if(originalPath.indexOf("file://///") == 0) // FireFox pc network file

+		localPath = "\\\\" + unescape(originalPath.substr(10)).replace(new RegExp("/","g"),"\\");

+	else if(originalPath.indexOf("file:///") == 0) // mac/unix local file

+		localPath = unescape(originalPath.substr(7));

+	else if(originalPath.indexOf("file:/") == 0) // mac/unix local file

+		localPath = unescape(originalPath.substr(5));

+	else // pc network file

+		localPath = "\\\\" + unescape(originalPath.substr(7)).replace(new RegExp("/","g"),"\\");

+	return localPath;

+}

+

+

+// Returns null if it can't do it, false if there's an error, true if it saved OK

+function ieSaveFile(filePath, content)

+{

+	try

+		{

+		var fso = new ActiveXObject("Scripting.FileSystemObject");

+		}

+	catch(e)

+		{

+		//alert("Exception while attempting to save\n\n" + e.toString());

+		return(null);

+		}

+	var file = fso.OpenTextFile(filePath,2,-1,0);

+	file.Write(content);

+	file.Close();

+	return(true);

+}

+

+// Returns null if it can't do it, false if there's an error, or a string of the content if successful

+function ieLoadFile(filePath)

+{

+	try

+		{

+		var fso = new ActiveXObject("Scripting.FileSystemObject");

+		var file = fso.OpenTextFile(filePath,1);

+		var content = file.ReadAll();

+		file.Close();

+		}

+	catch(e)

+		{

+		//alert("Exception while attempting to load\n\n" + e.toString());

+		return(null);

+		}

+	return(content);

+}

+

+// Returns null if it can't do it, false if there's an error, true if it saved OK

+function mozillaSaveFile(filePath, content)

+{

+	if(window.Components)

+		try

+			{

+			netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");

+			var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);

+			file.initWithPath(filePath);

+			if (!file.exists())

+				file.create(0, 0664);

+			var out = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream);

+			out.init(file, 0x20 | 0x02, 00004,null);

+			out.write(content, content.length);

+			out.flush();

+			out.close();

+			return(true);

+			}

+		catch(e)

+			{

+			//alert("Exception while attempting to save\n\n" + e);

+			return(false);

+			}

+	return(null);

+}

+

+// Returns null if it can't do it, false if there's an error, or a string of the content if successful

+function mozillaLoadFile(filePath)

+{

+	if(window.Components)

+		try

+			{

+			netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");

+			var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);

+			file.initWithPath(filePath);

+			if (!file.exists())

+				return(null);

+			var inputStream = Components.classes["@mozilla.org/network/file-input-stream;1"].createInstance(Components.interfaces.nsIFileInputStream);

+			inputStream.init(file, 0x01, 00004, null);

+			var sInputStream = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream);

+			sInputStream.init(inputStream);

+			return(sInputStream.read(sInputStream.available()));

+			}

+		catch(e)

+			{

+			//alert("Exception while attempting to load\n\n" + e);

+			return(false);

+			}

+	return(null);

+}

+

+// end tiddly wiki borrowed code

diff --git a/tree/closed.gif b/tree/closed.gif
new file mode 100644
index 0000000..8f37341
--- /dev/null
+++ b/tree/closed.gif
Binary files differ
diff --git a/tree/list.gif b/tree/list.gif
new file mode 100644
index 0000000..a06a6ca
--- /dev/null
+++ b/tree/list.gif
Binary files differ
diff --git a/tree/open.gif b/tree/open.gif
new file mode 100644
index 0000000..fb9ed97
--- /dev/null
+++ b/tree/open.gif
Binary files differ
diff --git a/tree/simpletree.css b/tree/simpletree.css
new file mode 100644
index 0000000..33bfb07
--- /dev/null
+++ b/tree/simpletree.css
@@ -0,0 +1,26 @@
+.treeview ul{ /*CSS for Simple Tree Menu*/

+margin: 0;

+padding: 0;

+}

+

+.treeview li{ /*Style for LI elements in general (excludes an LI that contains sub lists)*/

+background: white url(../images/bullet.gif) no-repeat left center;

+list-style-type: none;

+padding-left: 22px;

+margin-bottom: 3px;

+}

+

+.treeview li.submenu{ /* Style for LI that contains sub lists (other ULs). */

+background: white url(../images/closed.gif) no-repeat left 1px;

+cursor: hand !important;

+cursor: pointer !important;

+}

+

+

+.treeview li.submenu ul{ /*Style for ULs that are children of LIs (submenu) */

+display: none; /*Hide them by default. Don't delete. */

+}

+

+.treeview .submenu ul li{ /*Style for LIs of ULs that are children of LIs (submenu) */

+cursor: default;

+}

diff --git a/tree/simpletreemenu.js b/tree/simpletreemenu.js
new file mode 100644
index 0000000..74bf96d
--- /dev/null
+++ b/tree/simpletreemenu.js
@@ -0,0 +1,139 @@
+/***********************************************

+* Simple Tree Menu- © Dynamic Drive DHTML code library (www.dynamicdrive.com)

+* This notice MUST stay intact for legal use

+* Visit Dynamic Drive at http://www.dynamicdrive.com/ for full source code

+***********************************************/

+

+var persisteduls=new Object()

+var ddtreemenu=new Object()

+

+ddtreemenu.closefolder=xooki.u("images/closed.gif") //set image path to "closed" folder image

+ddtreemenu.openfolder=xooki.u("images/open.gif") //set image path to "open" folder image

+

+//////////No need to edit beyond here///////////////////////////

+

+ddtreemenu.createTree=function(treeid, enablepersist, persistdays){

+var ultags=document.getElementById(treeid).getElementsByTagName("ul")

+if (typeof persisteduls[treeid]=="undefined")

+persisteduls[treeid]=(enablepersist==true && ddtreemenu.getCookie(treeid)!="")? ddtreemenu.getCookie(treeid).split(",") : ""

+for (var i=0; i<ultags.length; i++)

+ddtreemenu.buildSubTree(treeid, ultags[i], i)

+if (enablepersist==true){ //if enable persist feature

+var durationdays=(typeof persistdays=="undefined")? 1 : parseInt(persistdays)

+ddtreemenu.dotask(window, function(){ddtreemenu.rememberstate(treeid, durationdays)}, "unload") //save opened UL indexes on body unload

+}

+}

+

+ddtreemenu.buildSubTree=function(treeid, ulelement, index){

+ulelement.parentNode.className="submenu"

+if (typeof persisteduls[treeid]=="object"){ //if cookie exists (persisteduls[treeid] is an array versus "" string)

+if (ddtreemenu.searcharray(persisteduls[treeid], index)){

+ulelement.setAttribute("rel", "open")

+ulelement.style.display="block"

+ulelement.parentNode.style.backgroundImage="url("+ddtreemenu.openfolder+")"

+}

+else /* start patch Xavier Hanin */ if (ulelement.getAttribute("rel") != "open") /* end patch Xavier Hanin */

+ulelement.setAttribute("rel", "closed")

+} //end cookie persist code

+else if (ulelement.getAttribute("rel")==null || ulelement.getAttribute("rel")==false) //if no cookie and UL has NO rel attribute explicted added by user

+ulelement.setAttribute("rel", "closed")

+else if (ulelement.getAttribute("rel")=="open") //else if no cookie and this UL has an explicit rel value of "open"

+ddtreemenu.expandSubTree(treeid, ulelement) //expand this UL plus all parent ULs (so the most inner UL is revealed!)

+ulelement.parentNode.onclick=function(e){

+var submenu=this.getElementsByTagName("ul")[0]

+if (submenu.getAttribute("rel")=="closed"){

+submenu.style.display="block"

+submenu.setAttribute("rel", "open")

+ulelement.parentNode.style.backgroundImage="url("+ddtreemenu.openfolder+")"

+}

+else if (submenu.getAttribute("rel")=="open"){

+submenu.style.display="none"

+submenu.setAttribute("rel", "closed")

+ulelement.parentNode.style.backgroundImage="url("+ddtreemenu.closefolder+")"

+}

+ddtreemenu.preventpropagate(e)

+}

+ulelement.onclick=function(e){

+ddtreemenu.preventpropagate(e)

+}

+}

+

+ddtreemenu.expandSubTree=function(treeid, ulelement){ //expand a UL element and any of its parent ULs

+var rootnode=document.getElementById(treeid)

+var currentnode=ulelement

+currentnode.style.display="block"

+currentnode.parentNode.style.backgroundImage="url("+ddtreemenu.openfolder+")"

+while (currentnode!=rootnode){

+if (currentnode.tagName=="UL"){ //if parent node is a UL, expand it too

+currentnode.style.display="block"

+currentnode.setAttribute("rel", "open") //indicate it's open

+currentnode.parentNode.style.backgroundImage="url("+ddtreemenu.openfolder+")"

+}

+currentnode=currentnode.parentNode

+}

+}

+

+ddtreemenu.flatten=function(treeid, action){ //expand or contract all UL elements

+var ultags=document.getElementById(treeid).getElementsByTagName("ul")

+for (var i=0; i<ultags.length; i++){

+ultags[i].style.display=(action=="expand")? "block" : "none"

+var relvalue=(action=="expand")? "open" : "closed"

+ultags[i].setAttribute("rel", relvalue)

+ultags[i].parentNode.style.backgroundImage=(action=="expand")? "url("+ddtreemenu.openfolder+")" : "url("+ddtreemenu.closefolder+")"

+}

+}

+

+ddtreemenu.rememberstate=function(treeid, durationdays){ //store index of opened ULs relative to other ULs in Tree into cookie

+var ultags=document.getElementById(treeid).getElementsByTagName("ul")

+var openuls=new Array()

+for (var i=0; i<ultags.length; i++){

+if (ultags[i].getAttribute("rel")=="open")

+openuls[openuls.length]=i //save the index of the opened UL (relative to the entire list of ULs) as an array element

+}

+if (openuls.length==0) //if there are no opened ULs to save/persist

+openuls[0]="none open" //set array value to string to simply indicate all ULs should persist with state being closed

+ddtreemenu.setCookie(treeid, openuls.join(","), durationdays) //populate cookie with value treeid=1,2,3 etc (where 1,2... are the indexes of the opened ULs)

+}

+

+////A few utility functions below//////////////////////

+

+ddtreemenu.getCookie=function(Name){ //get cookie value

+var re=new RegExp(Name+"=[^;]+", "i"); //construct RE to search for target name/value pair

+if (document.cookie.match(re)) //if cookie found

+return document.cookie.match(re)[0].split("=")[1] //return its value

+return ""

+}

+

+ddtreemenu.setCookie=function(name, value, days){ //set cookei value

+var expireDate = new Date()

+//set "expstring" to either future or past date, to set or delete cookie, respectively

+var expstring=expireDate.setDate(expireDate.getDate()+parseInt(days))

+document.cookie = name+"="+value+"; expires="+expireDate.toGMTString()+"; path=/";

+}

+

+ddtreemenu.searcharray=function(thearray, value){ //searches an array for the entered value. If found, delete value from array

+var isfound=false

+for (var i=0; i<thearray.length; i++){

+if (thearray[i]==value){

+isfound=true

+thearray.shift() //delete this element from array for efficiency sake

+break

+}

+}

+return isfound

+}

+

+ddtreemenu.preventpropagate=function(e){ //prevent action from bubbling upwards

+if (typeof e!="undefined")

+e.stopPropagation()

+else

+event.cancelBubble=true

+}

+

+ddtreemenu.dotask=function(target, functionref, tasktype){ //assign a function to execute to an event handler (ie: onunload)

+var tasktype=(window.addEventListener)? tasktype : "on"+tasktype

+if (target.addEventListener)

+target.addEventListener(tasktype, functionref, false)

+else if (target.attachEvent)

+target.attachEvent(tasktype, functionref)

+}

diff --git a/trimpath/template.js b/trimpath/template.js
new file mode 100644
index 0000000..ffc40e9
--- /dev/null
+++ b/trimpath/template.js
@@ -0,0 +1,397 @@
+/**
+ * TrimPath Template. Release 1.0.38.
+ * Copyright (C) 2004, 2005 Metaha.
+ * 
+ * TrimPath Template is licensed under the GNU General Public License
+ * and the Apache License, Version 2.0, as follows:
+ *
+ * This program is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * 
+ * This program is distributed WITHOUT ANY WARRANTY; without even the 
+ * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+ * See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+var TrimPath;

+

+// TODO: Debugging mode vs stop-on-error mode - runtime flag.

+// TODO: Handle || (or) characters and backslashes.
+// TODO: Add more modifiers.
+

+(function() {               // Using a closure to keep global namespace clean.

+    if (TrimPath == null)

+        TrimPath = new Object();

+    if (TrimPath.evalEx == null)

+        TrimPath.evalEx = function(src) { return eval(src); };

+
+    var UNDEFINED;
+    if (Array.prototype.pop == null)  // IE 5.x fix from Igor Poteryaev.
+        Array.prototype.pop = function() {
+            if (this.length === 0) {return UNDEFINED;}
+            return this[--this.length];
+        };
+    if (Array.prototype.push == null) // IE 5.x fix from Igor Poteryaev.
+        Array.prototype.push = function() {
+            for (var i = 0; i < arguments.length; ++i) {this[this.length] = arguments[i];}
+            return this.length;
+        };
+
+    TrimPath.parseTemplate = function(tmplContent, optTmplName, optEtc) {

+        if (optEtc == null)

+            optEtc = TrimPath.parseTemplate_etc;

+        var funcSrc = parse(tmplContent, optTmplName, optEtc);
+        var func = TrimPath.evalEx(funcSrc, optTmplName, 1);
+        if (func != null)
+            return new optEtc.Template(optTmplName, tmplContent, funcSrc, func, optEtc);
+        return null;
+    }

+    

+    try {

+        String.prototype.process = function(context, optFlags) {

+            var template = TrimPath.parseTemplate(this, null);
+            if (template != null)
+                return template.process(context, optFlags);
+            return this;

+        }

+    } catch (e) { // Swallow exception, such as when String.prototype is sealed.

+    }

+    

+    TrimPath.parseTemplate_etc = {};            // Exposed for extensibility.

+    TrimPath.parseTemplate_etc.statementTag = "forelse|for|if|elseif|else|var|macro";

+    TrimPath.parseTemplate_etc.statementDef = { // Lookup table for statement tags.
+        "if"     : { delta:  1, prefix: "if (", suffix: ") {", paramMin: 1 },
+        "else"   : { delta:  0, prefix: "} else {" },
+        "elseif" : { delta:  0, prefix: "} else if (", suffix: ") {", paramDefault: "true" },
+        "/if"    : { delta: -1, prefix: "}" },
+        "for"    : { delta:  1, paramMin: 3, 
+                     prefixFunc : function(stmtParts, state, tmplName, etc) {
+                        if (stmtParts[2] != "in")
+                            throw new etc.ParseError(tmplName, state.line, "bad for loop statement: " + stmtParts.join(' '));
+                        var iterVar = stmtParts[1];
+                        var listVar = "__LIST__" + iterVar;
+                        return [ "var ", listVar, " = ", stmtParts[3], ";",
+                             // Fix from Ross Shaull for hash looping, make sure that we have an array of loop lengths to treat like a stack.
+                             "var __LENGTH_STACK__;",
+                             "if (typeof(__LENGTH_STACK__) == 'undefined' || !__LENGTH_STACK__.length) __LENGTH_STACK__ = new Array();", 
+                             "__LENGTH_STACK__[__LENGTH_STACK__.length] = 0;", // Push a new for-loop onto the stack of loop lengths.
+                             "if ((", listVar, ") != null) { ",
+                             "var ", iterVar, "_ct = 0;",       // iterVar_ct variable, added by B. Bittman     
+                             "for (var ", iterVar, "_index in ", listVar, ") { ",
+                             iterVar, "_ct++;",
+                             "if (typeof(", listVar, "[", iterVar, "_index]) == 'function') {continue;}", // IE 5.x fix from Igor Poteryaev.
+                             "__LENGTH_STACK__[__LENGTH_STACK__.length - 1]++;",
+                             "var ", iterVar, " = ", listVar, "[", iterVar, "_index];" ].join("");
+                     } },
+        "forelse" : { delta:  0, prefix: "} } if (__LENGTH_STACK__[__LENGTH_STACK__.length - 1] == 0) { if (", suffix: ") {", paramDefault: "true" },
+        "/for"    : { delta: -1, prefix: "} }; delete __LENGTH_STACK__[__LENGTH_STACK__.length - 1];" }, // Remove the just-finished for-loop from the stack of loop lengths.
+        "var"     : { delta:  0, prefix: "var ", suffix: ";" },
+        "macro"   : { delta:  1, 
+                      prefixFunc : function(stmtParts, state, tmplName, etc) {
+                          var macroName = stmtParts[1].split('(')[0];
+                          return [ "var ", macroName, " = function", 
+                                   stmtParts.slice(1).join(' ').substring(macroName.length),
+                                   "{ var _OUT_arr = []; var _OUT = { write: function(m) { if (m) _OUT_arr.push(m); } }; " ].join('');
+                     } }, 
+        "/macro"  : { delta: -1, prefix: " return _OUT_arr.join(''); };" }
+    }

+    TrimPath.parseTemplate_etc.modifierDef = {

+        "eat"        : function(v)    { return ""; },

+        "escape"     : function(s)    { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); },

+        "capitalize" : function(s)    { return String(s).toUpperCase(); },

+        "default"    : function(s, d) { return s != null ? s : d; }

+    }
+    TrimPath.parseTemplate_etc.modifierDef.h = TrimPath.parseTemplate_etc.modifierDef.escape;
+

+    TrimPath.parseTemplate_etc.Template = function(tmplName, tmplContent, funcSrc, func, etc) {
+        this.process = function(context, flags) {

+            if (context == null)

+                context = {};

+            if (context._MODIFIERS == null)

+                context._MODIFIERS = {};
+            if (context.defined == null)
+                context.defined = function(str) { return (context[str] != undefined); };

+            for (var k in etc.modifierDef) {

+                if (context._MODIFIERS[k] == null)

+                    context._MODIFIERS[k] = etc.modifierDef[k];

+            }

+            if (flags == null)

+                flags = {};

+            var resultArr = [];
+            var resultOut = { write: function(m) { resultArr.push(m); } };

+            try {
+                func(resultOut, context, flags);

+            } catch (e) {

+                if (flags.throwExceptions == true)

+                    throw e;

+                var result = new String(resultArr.join("") + "[ERROR: " + e.toString() + (e.message ? '; ' + e.message : '') + "]");
+                result["exception"] = e;
+                return result;

+            }
+            return resultArr.join("");
+        }

+        this.name       = tmplName;

+        this.source     = tmplContent; 

+        this.sourceFunc = funcSrc;

+        this.toString   = function() { return "TrimPath.Template [" + tmplName + "]"; }
+    }
+    TrimPath.parseTemplate_etc.ParseError = function(name, line, message) {

+        this.name    = name;
+        this.line    = line;
+        this.message = message;
+    }
+    TrimPath.parseTemplate_etc.ParseError.prototype.toString = function() { 
+        return ("TrimPath template ParseError in " + this.name + ": line " + this.line + ", " + this.message);
+    }

+    
+    var parse = function(body, tmplName, etc) {
+        body = cleanWhiteSpace(body);
+        var funcText = [ "var TrimPath_Template_TEMP = function(_OUT, _CONTEXT, _FLAGS) { with (_CONTEXT) {" ];
+        var state    = { stack: [], line: 1 };                              // TODO: Fix line number counting.
+        var endStmtPrev = -1;
+        while (endStmtPrev + 1 < body.length) {
+            var begStmt = endStmtPrev;
+            // Scan until we find some statement markup.
+            begStmt = body.indexOf("{", begStmt + 1);
+            while (begStmt >= 0) {
+                var endStmt = body.indexOf('}', begStmt + 1);
+                var stmt = body.substring(begStmt, endStmt);
+                var blockrx = stmt.match(/^\{(cdata|minify|eval)/); // From B. Bittman, minify/eval/cdata implementation.
+                if (blockrx) {
+                    var blockType = blockrx[1]; 
+                    var blockMarkerBeg = begStmt + blockType.length + 1;
+                    var blockMarkerEnd = body.indexOf('}', blockMarkerBeg);
+                    if (blockMarkerEnd >= 0) {
+                        var blockMarker;
+                        if( blockMarkerEnd - blockMarkerBeg <= 0 ) {
+                            blockMarker = "{/" + blockType + "}";
+                        } else {
+                            blockMarker = body.substring(blockMarkerBeg + 1, blockMarkerEnd);
+                        }                        
+                        
+                        var blockEnd = body.indexOf(blockMarker, blockMarkerEnd + 1);
+                        if (blockEnd >= 0) {                            
+                            emitSectionText(body.substring(endStmtPrev + 1, begStmt), funcText);
+                            
+                            var blockText = body.substring(blockMarkerEnd + 1, blockEnd);
+                            if (blockType == 'cdata') {
+                                emitText(blockText, funcText);
+                            } else if (blockType == 'minify') {
+                                emitText(scrubWhiteSpace(blockText), funcText);
+                            } else if (blockType == 'eval') {
+                                if (blockText != null && blockText.length > 0) // From B. Bittman, eval should not execute until process().
+                                    funcText.push('_OUT.write( (function() { ' + blockText + ' })() );');
+                            }
+                            begStmt = endStmtPrev = blockEnd + blockMarker.length - 1;
+                        }
+                    }                        
+                } else if (body.charAt(begStmt - 1) != '$' &&               // Not an expression or backslashed,
+                           body.charAt(begStmt - 1) != '\\') {              // so check if it is a statement tag.
+                    var offset = (body.charAt(begStmt + 1) == '/' ? 2 : 1); // Close tags offset of 2 skips '/'.
+                                                                            // 10 is larger than maximum statement tag length.
+                    if (body.substring(begStmt + offset, begStmt + 10 + offset).search(TrimPath.parseTemplate_etc.statementTag) == 0) 
+                        break;                                              // Found a match.
+                }
+                begStmt = body.indexOf("{", begStmt + 1);
+            }
+            if (begStmt < 0)                              // In "a{for}c", begStmt will be 1.
+                break;
+            var endStmt = body.indexOf("}", begStmt + 1); // In "a{for}c", endStmt will be 5.
+            if (endStmt < 0)
+                break;
+            emitSectionText(body.substring(endStmtPrev + 1, begStmt), funcText);
+            emitStatement(body.substring(begStmt, endStmt + 1), state, funcText, tmplName, etc);
+            endStmtPrev = endStmt;
+        }
+        emitSectionText(body.substring(endStmtPrev + 1), funcText);
+        if (state.stack.length != 0)
+            throw new etc.ParseError(tmplName, state.line, "unclosed, unmatched statement(s): " + state.stack.join(","));
+        funcText.push("}}; TrimPath_Template_TEMP");
+        return funcText.join("");
+    }
+    
+    var emitStatement = function(stmtStr, state, funcText, tmplName, etc) {
+        var parts = stmtStr.slice(1, -1).split(' ');
+        var stmt = etc.statementDef[parts[0]]; // Here, parts[0] == for/if/else/...
+        if (stmt == null) {                    // Not a real statement.
+            emitSectionText(stmtStr, funcText);
+            return;
+        }
+        if (stmt.delta < 0) {
+            if (state.stack.length <= 0)
+                throw new etc.ParseError(tmplName, state.line, "close tag does not match any previous statement: " + stmtStr);
+            state.stack.pop();
+        } 
+        if (stmt.delta > 0)
+            state.stack.push(stmtStr);
+
+        if (stmt.paramMin != null &&
+            stmt.paramMin >= parts.length)
+            throw new etc.ParseError(tmplName, state.line, "statement needs more parameters: " + stmtStr);
+        if (stmt.prefixFunc != null)
+            funcText.push(stmt.prefixFunc(parts, state, tmplName, etc));
+        else 
+            funcText.push(stmt.prefix);
+        if (stmt.suffix != null) {
+            if (parts.length <= 1) {
+                if (stmt.paramDefault != null)
+                    funcText.push(stmt.paramDefault);
+            } else {
+                for (var i = 1; i < parts.length; i++) {
+                    if (i > 1)
+                        funcText.push(' ');
+                    funcText.push(parts[i]);
+                }
+            }
+            funcText.push(stmt.suffix);
+        }
+    }
+

+    var emitSectionText = function(text, funcText) {
+        if (text.length <= 0)
+            return;
+        var nlPrefix = 0;               // Index to first non-newline in prefix.
+        var nlSuffix = text.length - 1; // Index to first non-space/tab in suffix.

+        while (nlPrefix < text.length && (text.charAt(nlPrefix) == '\n'))
+            nlPrefix++;
+        while (nlSuffix >= 0 && (text.charAt(nlSuffix) == ' ' || text.charAt(nlSuffix) == '\t'))
+            nlSuffix--;
+        if (nlSuffix < nlPrefix)
+            nlSuffix = nlPrefix;
+        if (nlPrefix > 0) {
+            funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("');

+            var s = text.substring(0, nlPrefix).replace('\n', '\\n'); // A macro IE fix from BJessen.
+            if (s.charAt(s.length - 1) == '\n')
+            	s = s.substring(0, s.length - 1);
+            funcText.push(s);
+            funcText.push('");');
+        }
+        var lines = text.substring(nlPrefix, nlSuffix + 1).split('\n');
+        for (var i = 0; i < lines.length; i++) {
+            emitSectionTextLine(lines[i], funcText);
+            if (i < lines.length - 1)
+                funcText.push('_OUT.write("\\n");\n');
+        }

+        if (nlSuffix + 1 < text.length) {
+            funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("');
+            var s = text.substring(nlSuffix + 1).replace('\n', '\\n');
+            if (s.charAt(s.length - 1) == '\n')
+            	s = s.substring(0, s.length - 1);
+            funcText.push(s);
+            funcText.push('");');
+        }
+    }
+    
+    var emitSectionTextLine = function(line, funcText) {
+        var endMarkPrev = '}';
+        var endExprPrev = -1;
+        while (endExprPrev + endMarkPrev.length < line.length) {
+            var begMark = "${", endMark = "}";
+            var begExpr = line.indexOf(begMark, endExprPrev + endMarkPrev.length); // In "a${b}c", begExpr == 1
+            if (begExpr < 0)
+                break;
+            if (line.charAt(begExpr + 2) == '%') {
+                begMark = "${%";
+                endMark = "%}";
+            }
+            var endExpr = line.indexOf(endMark, begExpr + begMark.length);         // In "a${b}c", endExpr == 4;
+            if (endExpr < 0)
+                break;
+            emitText(line.substring(endExprPrev + endMarkPrev.length, begExpr), funcText);                
+            // Example: exprs == 'firstName|default:"John Doe"|capitalize'.split('|')
+            var exprArr = line.substring(begExpr + begMark.length, endExpr).replace(/\|\|/g, "#@@#").split('|');
+            for (var k in exprArr) {
+                if (exprArr[k].replace) // IE 5.x fix from Igor Poteryaev.
+                    exprArr[k] = exprArr[k].replace(/#@@#/g, '||');
+            }
+            funcText.push('_OUT.write(');
+            emitExpression(exprArr, exprArr.length - 1, funcText); 
+            funcText.push(');');
+            endExprPrev = endExpr;
+            endMarkPrev = endMark;
+        }
+        emitText(line.substring(endExprPrev + endMarkPrev.length), funcText); 
+    }
+    
+    var emitText = function(text, funcText) {
+        if (text == null ||
+            text.length <= 0)
+            return;
+        text = text.replace(/\\/g, '\\\\');
+        text = text.replace(/\n/g, '\\n');
+        text = text.replace(/"/g,  '\\"');
+        funcText.push('_OUT.write("');
+        funcText.push(text);
+        funcText.push('");');
+    }
+    
+    var emitExpression = function(exprArr, index, funcText) {
+        // Ex: foo|a:x|b:y1,y2|c:z1,z2 is emitted as c(b(a(foo,x),y1,y2),z1,z2)
+        var expr = exprArr[index]; // Ex: exprArr == [firstName,capitalize,default:"John Doe"]
+        if (index <= 0) {          // Ex: expr    == 'default:"John Doe"'

+            funcText.push(expr);
+            return;
+        }
+        var parts = expr.split(':');
+        funcText.push('_MODIFIERS["');
+        funcText.push(parts[0]); // The parts[0] is a modifier function name, like capitalize.
+        funcText.push('"](');
+        emitExpression(exprArr, index - 1, funcText);
+        if (parts.length > 1) {
+            funcText.push(',');
+            funcText.push(parts[1]);
+        }
+        funcText.push(')');
+    }

+
+    var cleanWhiteSpace = function(result) {
+        result = result.replace(/\t/g,   "    ");
+        result = result.replace(/\r\n/g, "\n");
+        result = result.replace(/\r/g,   "\n");
+        result = result.replace(/^(\s*\S*(\s+\S+)*)\s*$/, '$1'); // Right trim by Igor Poteryaev.
+        return result;
+    }
+
+    var scrubWhiteSpace = function(result) {
+        result = result.replace(/^\s+/g,   "");
+        result = result.replace(/\s+$/g,   "");
+        result = result.replace(/\s+/g,   " ");
+        result = result.replace(/^(\s*\S*(\s+\S+)*)\s*$/, '$1'); // Right trim by Igor Poteryaev.
+        return result;
+    }
+
+    // The DOM helper functions depend on DOM/DHTML, so they only work in a browser.
+    // However, these are not considered core to the engine.
+    //
+    TrimPath.parseDOMTemplate = function(elementId, optDocument, optEtc) {
+        if (optDocument == null)
+            optDocument = document;
+        var element = optDocument.getElementById(elementId);
+        var content = element.value;     // Like textarea.value.
+        if (content == null)
+            content = element.innerHTML; // Like textarea.innerHTML.
+        content = content.replace(/&lt;/g, "<").replace(/&gt;/g, ">");
+        return TrimPath.parseTemplate(content, elementId, optEtc);
+    }
+
+    TrimPath.processDOMTemplate = function(elementId, context, optFlags, optDocument, optEtc) {
+        return TrimPath.parseDOMTemplate(elementId, optDocument, optEtc).process(context, optFlags);
+    }
+}) ();
diff --git a/xooki.js b/xooki.js
new file mode 100644
index 0000000..248351a
--- /dev/null
+++ b/xooki.js
@@ -0,0 +1,793 @@
+/*

+	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 curPageId = new RegExp(".*\\/((?:.*\\/){"+xooki.config.level+"}[^\\/]*)(?:\\.\\w+)", "g").exec(window.location.toString())[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;

+	}

+}

diff --git a/xookiEdit.js b/xookiEdit.js
new file mode 100644
index 0000000..ab66882
--- /dev/null
+++ b/xookiEdit.js
@@ -0,0 +1,372 @@
+/*

+	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.

+*/

+

+// this file is included only in edit mode

+

+xooki.url.include(xooki.u("tiddly/util.js"));

+

+if (typeof xooki.io == "undefined") {

+    xooki.io = {};

+}

+

+xooki.io.removeFile = function (filePath) {

+	var r = null;

+	if((r == null) || (r == false))

+		r = xooki.io.mozillaRemoveFile(filePath);

+	if((r == null) || (r == false))

+		r = xooki.io.ieRemoveFile(filePath);

+	return(r);

+}

+

+xooki.io.mozillaRemoveFile = function(filePath) {

+	if(window.Components)

+		try

+			{

+			netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");

+			var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);

+			file.initWithPath(filePath);

+			if (!file.exists())

+				return null;

+			file.remove(false);

+			return(true);

+			}

+		catch(e)

+			{

+			return(false);

+			}

+	return(null);

+}

+

+xooki.io.ieRemoveFile = function(filePath) {

+	try

+		{

+		var fso = new ActiveXObject("Scripting.FileSystemObject");

+		}

+	catch(e)

+		{

+		//alert("Exception while attempting to save\n\n" + e.toString());

+		return(null);

+		}

+	fso.DeleteFile(filePath, false);

+	return(true);

+}

+

+

+if (typeof xooki.string == "undefined") {

+    xooki.string = {};

+}

+

+xooki.string.escapeString = function(str) {

+// borrowed from dojo

+//summary:

+//	Adds escape sequences for non-visual characters, double quote and backslash

+//	and surrounds with double quotes to form a valid string literal.

+	return ('"' + str.replace(/(["\\])/g, '\\$1') + '"'

+		).replace(/[\f]/g, "\\f"

+		).replace(/[\b]/g, "\\b"

+		).replace(/[\n]/g, "\\n"

+		).replace(/[\t]/g, "\\t"

+		).replace(/[\r]/g, "\\r"); // string

+};

+

+if (typeof xooki.json == "undefined") {

+    xooki.json = {};

+}

+

+xooki.json.serialize = function (o, indent) {

+        // borrowed and adapted from dojo

+    	// summary:

+    	//		Create a JSON serialization of the object.

+    	// return:

+    	//		a String representing the serialized version of the passed object

+    	if (!indent) {

+    	   indent = "";

+        }

+		var objtype = typeof(o);

+		if(objtype == "undefined"){

+			return "undefined";

+		}else if((objtype == "number")||(objtype == "boolean")){

+			return o + "";

+		}else if(o === null){

+			return "null";

+		}

+		if (objtype == "string") { return xooki.string.escapeString(o); }

+		if(objtype == "function"){

+		    // do not encode functions

+			return null;

+		}

+		// recurse

+		var me = arguments.callee;

+		// short-circuit for objects that support "json" serialization

+		// if they return "self" then just pass-through...

+		var newObj;

+		// array

+		if(objtype != "function" && typeof(o.length) == "number"){

+			var res = [];

+			for(var i = 0; i < o.length; i++){

+				var val = me(o[i], indent+"  ");

+				

+				if(typeof(val) != "string"){

+					val = "undefined";

+				}

+				res.push(val);

+			}

+			return " [\n" + res.join(",\n") + "\n"+indent+"]\n";

+		}

+		// generic object code path

+		res = [];

+		for (var k in o){

+		    if ("meta" == k) {

+		      continue;

+            }

+			var useKey;

+			if (typeof(k) == "number"){

+				useKey = '"' + k + '"';

+			}else if (typeof(k) == "string"){

+				useKey = xooki.string.escapeString(k);

+			}else{

+				// skip non-string or number keys

+				continue;

+			}

+			val = me(o[k], indent+"    ");

+			if(typeof(val) != "string"){

+				// skip non-serializable values

+				continue;

+			}

+			res.push(indent+"  "+useKey + ":" + val);

+		}

+		return indent+"{\n" + res.join(",\n") + indent+"}";

+};

+

+xooki.component.toolbar = function () {

+	return	'<div style="position:absolute;top:2px;right:30px;" id="xooki-toolbar">'

+				+ '<a href="javascript:xooki.action.toggleEdit()"><img src="'+xooki.u('images/edit.gif')+'" title="toggle edit'+(xooki.c.browser.NS?' (CTRL+E)':'')+'"/></a>'

+				+ '<a href="javascript:xooki.action.saveChanges()"><img src="'+xooki.u('images/save.gif')+'" title="save'+(xooki.c.browser.NS?' (CTRL+S)':'')+'"/></a>'

+				+ '<a href="javascript:xooki.action.remove()"><img src="'+xooki.u('images/delete.gif')+'" title="remove"/></a>'

+				+ '<a href="javascript:xooki.action.createChild()"><img src="'+xooki.u('images/addchild.gif')+'" title="create child"/></a>'

+				+ '<a href="javascript:xooki.action.movePageUp()"><img src="'+xooki.u('images/up.gif')+'" title="move page up in TOC"/></a>'

+				+ '<a href="javascript:xooki.action.movePageDown()"><img src="'+xooki.u('images/down.gif')+'" title="move page down in TOC"/></a>'

+				+ (xooki.c.debug?'<a href="javascript:xooki.action.toggleDebug()"><img src="'+xooki.u('images/debug.gif')+'" title="toggle xooki debug mode"/></a>':'')

+			+ '</div>';

+};

+    

+xooki.component.editZone = function () {

+    return '<div id="xooki-edit" style="display:none">'

+        + '<table border="0"><tr><td valign="top">Title</td><td><input id="xooki-input-title" type="text" value="'+xooki.page.title+'"></input></td></tr>'

+        + '<tr><td valign="top">Content</td><td><textarea rows="20" cols="80" id="xooki-source">'+xooki.input.source()+'</textarea></td></tr>'

+        + '<tr><td colspan="2" align="right"><input type="button" value="Save" onclick="javascript:xooki.action.saveChanges()"/> <input type="button" value="Preview" onclick="javascript:xooki.action.previewChanges()"/> <input type="button" value="Discard" onclick="javascript:xooki.action.discardChanges()"/></td></tr></table></div>';

+}

+

+xooki.url.reload = function() {

+    window.location = window.location;

+}

+

+

+

+xooki.action.quitEdit = function () {

+    xooki.input.applyChanges();

+	xooki.html.hide('xooki-edit');

+	xooki.html.show('xooki-content');

+}

+xooki.action.edit = function () {

+	xooki.html.hide('xooki-content');

+	xooki.html.show('xooki-edit');

+}

+xooki.action.toggleEdit = function () {

+	if (document.getElementById('xooki-edit').style.display == 'none') {

+		xooki.action.edit();

+	} else {

+		xooki.action.quitEdit();

+	}

+}

+xooki.action.remove = function () {

+    if (confirm(t("The current page will be removed from the table of content, and deleted on file system.\nNote that all children will be removed from the table of content too, but not from the file system!\nAre you sure you want to delete the current page?"))) {

+

+        // the page to which we'll be redirected...

+        var nextPage;

+        var index = xooki.page.meta.index;

+        var parent = xooki.page.meta.parent;

+        if (index > 0) {

+            // ... will be the previous sibling if there is one ...

+            nextPage = parent.children[index - 1];

+        } else if (parent.children.length > 1) {

+            // ... or the next sibling if there is one ...

+            nextPage = parent.children[index + 1];        

+        } else if (parent != xooki.toc) {

+            // ... or the parent if the parent is not the toc root ...

+            nextPage = parent;        

+        } else {

+            // ... otherwise it s a problem

+            xooki.error(t("Cannot delete the sole page!"));

+            return;

+        }

+        

+        parent.children.splice(index, 1); // remove node from toc

+        xooki.toc.save();

+        

+        xooki.io.removeFile(xooki.io.getLocalPath(window.location.toString()));

+        

+        window.location = pu(nextPage.id);

+    }

+}

+xooki.action.discardChanges = function () {

+    xooki.url.reload();

+}

+xooki.action.previewChanges = function () {

+    xooki.action.quitEdit();

+}

+xooki.action.saveChanges = function () {

+	var originalPath = document.location.toString();

+	var localPath = xooki.io.getLocalPath(originalPath);

+	

+	// Load the original file

+	var original = xooki.io.loadFile(localPath);

+	if(original == null) {

+		xooki.error(t("Impossible to load original file: ${0}", localPath));

+		return;

+	}

+	

+	var startSaveArea = '<textarea id="xooki-source">';

+	var posOpeningArea = original.indexOf(startSaveArea);

+	var posClosingArea = original.indexOf('</textarea>');

+	

+	xooki.page.title = document.getElementById('xooki-input-title').value;

+	xooki.toc.save();

+	

+	var save;

+	try {

+		// Save new file

+		var revised = original.substr(0,posOpeningArea + startSaveArea.length) + "\n" +

+					xooki.input.source() +

+					original.substr(posClosingArea);

+					

+		save = xooki.io.saveFile(localPath,revised);

+	} catch (e) {

+		xooki.error(e);

+	} 

+    if(save) {

+		xooki.info(t("saved to ${0}",localPath));

+		

+		// TODO: see if we are able to apply title change without reloading

+		setTimeout(function() {xooki.url.reload();}, 800);

+	} else

+		xooki.error(t("Impossible to save changes to ${0}", localPath));

+}

+

+

+xooki.action.movePageUp = function () {

+    xooki.action.movePage(-1);

+}

+xooki.action.movePageDown = function () {

+    xooki.action.movePage(1);

+}

+

+xooki.action.movePage = function (delta) {

+    var index = xooki.page.meta.index;

+    var parent = xooki.page.meta.parent;

+

+    // check if node can move

+    if (index + delta < 0) {

+        xooki.info(t("Can't move first page up"));

+        return;

+    }

+    if (index + delta >= parent.children.length) {

+        xooki.info(t("Can't move last page down"));

+        return;

+    }

+

+    // move node in toc    

+    parent.children.splice(index, 1);

+    parent.children.splice(index+delta, 0, xooki.page);

+    

+    xooki.toc.save();

+    xooki.url.reload();

+}

+

+xooki.action.createChild = function () {

+    titleToId = function (title) {

+        return title.replace(/\s+/g, '');

+    }

+

+    var title = prompt("Child page title?", "");

+    var id = prompt("Child page path?", titleToId(title));

+    

+    xooki.action.createChildPage({"id": id, "title": title, "children": []});

+}

+

+xooki.action.createChildPage = function (child) {

+    if (typeof child.level == 'undefined') {

+        child.level = child.id.split('/').length - 1;

+    }

+        

+    var pagetpl = xooki.url.loadURL(cu("blankPageTpl"));

+    if (pagetpl != null) {

+        var childURL = pu(child.id);

+        var localPath = xooki.io.getLocalPath(childURL);

+        var original = xooki.io.loadFile(localPath);

+        if (original != null) {

+            if (!window.confirm(t("File for child page:\n${0}\nalready exists.\nAre you sure you want to continue and overwrite it?", localPath))) 

+                return;

+        }

+        

+        xooki.page.children.push(child);

+        xooki.toc.save();

+        

+        // usually used by templates

+        if (typeof child.relroot == 'undefined') {

+            child.relroot = "";

+        	for (var i=0; i < child.level; i++) {

+        		child.relroot += "../";

+        	}

+        }

+		var revised = xooki.string.processTemplate(pagetpl, child);

+    	var save;

+    	try {

+    		// Save new file

+    		save = xooki.io.saveFile(localPath,revised);

+    	} catch (e) {

+    		xooki.error(e);

+    	} 

+        if(save) {

+    		// go to child page

+            window.location = childURL;

+    	} else

+    		xooki.error(t("Impossible to save changes to ${0}", localPath));

+    }

+}

+

+

+xooki.toc.save = function (revised) {

+    if (!revised) {

+        revised = xooki.json.serialize({children: this.children});

+    }

+	var save;

+	var tocPath = xooki.io.getLocalPath(cu("toc"));

+	try {

+		save = xooki.io.saveFile(tocPath, revised);

+	} catch (e) {

+		xooki.error(e);

+	} 

+    if(!save) 

+		xooki.error(t("Impossible to save changes to ${0}", tocPath));

+}

+

+

+xooki.util = {}

+xooki.util.copy = function(o) {

+    var copy = {};

+    for (var k in o) {

+        copy[k] = o[k];

+    }   

+    return copy;

+}