Scripting the Processing Language

I created a PAppletScriptable Applet that allows you to on the fly write javascript that will control a processing application.

Try it out here (you'll need JDK 6 and I've only tested it on Firefox so good luck, i'll update it soon to be more 'polished'): PAppletScritable test

Resources

Discussions

Processing Discourse

Scripting.dev.java.net

Processing in Javascript

I converted some of the example scripts from the processing language into javascript. The biggest differences are how classes are represented in javascript, and also the passing of the PApplet ā€œpā€ parameter, which is then used either explicitly or implicitly in a with block:

//(NOTE: not recommended to use). 
with(p){
  p.noise(val);
}

I'm still looking for a way to give the script access to the PApplet's methods and fields (static and instance), possibly through some kind of reflection mechanism that would allow me to expose those methods as globally defined functions within the script's context IE:

function noise(val) {
   //globally defined PApplet "P"
   return P.noise(val);
}

Lights Opengl

http://jddaniels.googlecode.com/svn/trunk/processing-scripting/dist/scripts/lightsOpengl.js
function setup(p) 
{
	with(p){
  size(800, 600, OPENGL);
  noStroke();
	}
}
 
function draw(p) 
{
	with(p){
  defineLights(p);
  background(0);
 
  for (var i = 0; i <= width; i += 100) {
    for (var j = 0; j <= height; j += 100) {
      pushMatrix();
      translate(i, j);
      rotateY(map(mouseX, 0, width, 0, PI));
      rotateX(map(mouseY, 0, height, 0, PI));
      box(90);
      popMatrix();
    }
  }
	}
}
 
function defineLights(p) {
 with(p){
  // Orange point light on the right
  pointLight(150, 100, 0,   // Color
             200, -150, 0); // Position
 
  // Blue directional light from the left
  directionalLight(0, 102, 255, // Color
                   1, 0, 0);    // The x-, y-, z-axis direction
 
  // Yellow spotlight from the front
  spotLight(255, 255, 109,  // Color
            0, 40, 200,     // Position
            0, -0.5, -0.5,  // Direction
            PI / 2, 2);     // Angle, concentration
 
}
}				
 

http://jddaniels.googlecode.com/svn/trunk/processing-scripting/dist/scripts/lightsOpengl.js

Space Junk

http://jddaniels.googlecode.com/svn/trunk/processing-scripting/dist/scripts/spaceJunk.js
importPackage(Packages.processing.core);
 
//used for oveall rotation
var ang = 0.0;
 
//cube count-lower/raise to test P3D/OPENGL performance
var limit = 500;
 
//array for all cubes
var cubes = new Array(limit);
 
function setup(p){
  //try substituting P3D for OPENGL 
  //argument to test performance
  p.size(640, 480, p.OPENGL); 
  p.background(0); 
 
  //instantiate cubes, passing in random vals for size and postion
  for (var i = 0; i< cubes.length; i++){
    cubes[i] = new Cube(p.random(-10, 10), p.random(-10, 10), 
    p.random(-10, 10), p.random(-140, 140), p.random(-140, 140), 
    p.random(-140, 140));
  }
}
 
function draw(p){
  p.background(0); 
  p.fill(200);
 
  //set up some different colored lights
  p.pointLight(51, 102, 255, 65, 60, 100); 
  p.pointLight(200, 40, 60, -65, -60, -150);
 
  //raise overall light in scene 
  p.ambientLight(70, 70, 10); 
 
  /*center geometry in display windwow.
   you can change 3rd argument ('0')
   to move block group closer(+)/further(-)*/
  p.translate(p.width/2, p.height/2, -200+p.mouseX);
 
  //rotate around y and x axes
  p.rotateY(PApplet.radians(ang));
  p.rotateX(PApplet.radians(ang));
 
  //draw cubes
  for (var i = 0; i< cubes.length; i++){
    cubes[i].drawCube(p);
  }
  //used in rotate function calls above
  ang++;
}
 
//simple Cube class, based on Quads
function Cube(ww,hh,dd, shiftXX, shiftYY, shiftZZ) {
 
//properties
    this.w = ww;
    this.h = hh;
    this.d = dd;
    this.shiftX = shiftXX;
    this.shiftY = shiftYY;
    this.shiftZ = shiftZZ;
 
 
  /*main cube drawing method, which looks 
   more confusing than it really is. It's 
   just a bunch of rectangles drawn for 
   each cube face*/
  this.drawCube = function(p){
    p.beginShape(p.QUADS);
    //front face
    p.vertex(-this.w/2 + this.shiftX, -this.h/2 + this.shiftY, -this.d/2 + this.shiftZ); 
    p.vertex(this.w + this.shiftX, -this.h/2 + this.shiftY, -this.d/2 + this.shiftZ); 
    p.vertex(this.w + this.shiftX, this.h + this.shiftY, -this.d/2 + this.shiftZ); 
    p.vertex(-this.w/2 + this.shiftX, this.h + this.shiftY, -this.d/2 + this.shiftZ); 
 
    //back face
    p.vertex(-this.w/2 + this.shiftX, -this.h/2 + this.shiftY, this.d + this.shiftZ); 
    p.vertex(this.w + this.shiftX, -this.h/2 + this.shiftY, this.d + this.shiftZ); 
    p.vertex(this.w + this.shiftX, this.h + this.shiftY, this.d + this.shiftZ); 
    p.vertex(-this.w/2 + this.shiftX, this.h + this.shiftY, this.d + this.shiftZ);
 
    //left face
    p.vertex(-this.w/2 + this.shiftX, -this.h/2 + this.shiftY, -this.d/2 + this.shiftZ); 
    p.vertex(-this.w/2 + this.shiftX, -this.h/2 + this.shiftY, this.d + this.shiftZ); 
    p.vertex(-this.w/2 + this.shiftX, this.h + this.shiftY, this.d + this.shiftZ); 
    p.vertex(-this.w/2 + this.shiftX, this.h + this.shiftY, -this.d/2 + this.shiftZ); 
 
    //right face
    p.vertex(this.w + this.shiftX, -this.h/2 + this.shiftY, -this.d/2 + this.shiftZ); 
    p.vertex(this.w + this.shiftX, -this.h/2 + this.shiftY, this.d + this.shiftZ); 
    p.vertex(this.w + this.shiftX, this.h + this.shiftY, this.d + this.shiftZ); 
    p.vertex(this.w + this.shiftX, this.h + this.shiftY, -this.d/2 + this.shiftZ); 
 
    //top face
    p.vertex(-this.w/2 + this.shiftX, -this.h/2 + this.shiftY, -this.d/2 + this.shiftZ); 
    p.vertex(this.w + this.shiftX, -this.h/2 + this.shiftY, -this.d/2 + this.shiftZ); 
    p.vertex(this.w + this.shiftX, -this.h/2 + this.shiftY, this.d + this.shiftZ); 
    p.vertex(-this.w/2 + this.shiftX, -this.h/2 + this.shiftY, this.d + this.shiftZ); 
 
    //bottom face
    p.vertex(-this.w/2 + this.shiftX, this.h + this.shiftY, -this.d/2 + this.shiftZ); 
    p.vertex(this.w + this.shiftX, this.h + this.shiftY, -this.d/2 + this.shiftZ); 
    p.vertex(this.w + this.shiftX, this.h + this.shiftY, this.d + this.shiftZ); 
    p.vertex(-this.w/2 + this.shiftX, this.h + this.shiftY, this.d + this.shiftZ); 
 
    p.endShape(); 
 
    //add some rotation to each box for pizazz.
    p.rotateY(PApplet.radians(1));
    p.rotateX(PApplet.radians(1));
    p.rotateZ(PApplet.radians(1));
  }
}

http://jddaniels.googlecode.com/svn/trunk/processing-scripting/dist/scripts/spaceJunk.js

Circles 2D

http://jddaniels.googlecode.com/svn/trunk/processing-scripting/dist/scripts/test.js
var spin = 0.0; 
var diameter = 84.0; 
var angle = 0.0;
 
var angle_rot = 0.0; 
var rad_points = 90;
 
//importPackage(Packages.processing.core);
print("script imported\n");
 
function setup(p) 
{
	print("SCRIPT setup()");
	  with(p) {	
		  size(400, 400, P3D);
		  noStroke();
		  smooth();
	  }
}
 
function draw(p) 
{ 
  print("SCRIPT draw()");
  with(p) {	
 
	  background(153);
 
	  translate(130, 65);
 
	  fill(255);
	  ellipse(0, 0, 16, 16);
 
	  angle_rot = 0;
	  fill(51);
 
	  for(var i=0; i<5; i++) {
	    pushMatrix();
	    rotate(angle_rot + -45);
	    ellipse(-116, 0, diameter, diameter);
	    popMatrix();
	    angle_rot += PI*2/5;
	  }
 
	  diameter = 34 * sin(angle) + 168;
 
	  angle += 0.02;
	  if (angle > TWO_PI) { angle = 0; }
  }
}

http://jddaniels.googlecode.com/svn/trunk/processing-scripting/dist/scripts/test.js

PAppletScriptable.java

This is the Applet that extends PApplet to add JSR 223 scripting support (only javascript right now). I used the Scriptlet applet example from the scripting project.

http://jddaniels.googlecode.com/svn/trunk/processing-scripting/src/net/ddaniels/processing/scripting/PAppletScriptlet.java
 
package net.ddaniels.processing.scripting;
 
import java.awt.Graphics;
import java.io.*;
import java.net.URL;
import javax.script.*;
 
import processing.core.PApplet;
 
/**
 * An applet that allows JSR 223 scripts to implement Processing functions for a PApplet.
 * 
 * Uses A. Sundararajan's Scriptlet application from Sun's scripting project.
 *
 * @author Doug Daniels,  A. Sundararajan
 */
public class PAppletScriptlet extends PApplet {
    private ScriptContext mycontext;
    // script object for this applet
    private Object appletObj;
 
    private static final String SCRIPT = "script";
    private static final String SCRIPTSRC = "scriptsrc";
    private static final String ID = "id";
    private static final String SHAREDCONTEXT = "sharedcontext";
 
    @Override public void init() {
        boolean sharedContext = Boolean.parseBoolean(getParameter(SHAREDCONTEXT)); 
        mycontext = sharedContext ? jsengine.getContext()
                                  : new SimpleScriptContext();
        mycontext.setAttribute("engine", jsengine, ScriptContext.ENGINE_SCOPE);
        initialEvalScript();
        //Initialize the PApplet
        super.init();
        invoke("init");
    }
    /**
     * Sets a script to be used as the delegate for all processing applet calls.
     */
    public void setScript(String scriptContents) {
 
        try {
 
            //stop the redrawing and updating
            noLoop();
            System.out.println("evaluating script");
            jsengine.eval(scriptContents, mycontext);
            //setup the new script
            setup();            
            //start looping again
            //FIXME: this might break a processing script that really doesn't want to loop.
            loop();
        }
        catch (ScriptException e) {
            String stackTrace = getStackTrace(e);
            System.out.println("ERROR with script:\n" + stackTrace);
        }
        catch (Exception exc) {
            String exceptionStackTrace = getStackTrace(exc);            
            System.err.println("ERROR setting script with{"+ args +"}\n");            
            System.err.println("setting script [CONTENTS]:\n" + scriptContents);
            System.err.println(exceptionStackTrace);
 
        }
    }
 
    public static String getStackTrace(Throwable t)
    {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw, true);
        t.printStackTrace(pw);
        pw.flush();
        sw.flush();
        return sw.toString();
    }    
    @Override public void setup() {
        System.out.println("invoking script setup");
        invoke("setup");
    }    
 
    @Override public void draw() {
        //System.out.println("invoking script draw");
        invoke("draw");
    }
 
    @Override public void mousePressed() {
        //System.out.println("invoking script mousePressed");
        invoke("mousePressed");
    }
 
    @Override public String getAppletInfo() {
        Object res = invoke("getAppletInfo");
        return (res != null)? res.toString() : null;
    }
 
    @Override public String[][] getParameterInfo() {
        Object res = invoke("getParameterInfo");
        if (res instanceof String[][]) {
            return (String[][]) res;
        } else {
            return null;
        }
    }
 
    // - Internals only below this point
 
    private static ScriptEngine jsengine;
    static {
        ScriptEngineManager manager = new ScriptEngineManager();
        jsengine = manager.getEngineByName("JavaScript");        
    }  
 
    private void initialEvalScript() {        
        try {
            String script = getParameter(SCRIPT);
            loadInitScript();
            if (script != null) {                
                jsengine.eval(script, mycontext);
            } else {
                String scriptSrc = getParameter(SCRIPTSRC);
                System.out.println("Loading script from URL: "+scriptSrc);
                if (scriptSrc != null) {
                    URL u = new URL(getCodeBase(), scriptSrc);                   
                    InputStream is = u.openStream();
                    Reader r = new InputStreamReader(is);
                    jsengine.eval(r, mycontext);                            
                }
            }          
            String appletId = getParameter(ID);
            //FIXME: appletId is causing issues because we're embedded in a JOGL applet
            appletId = null;
 
            if (appletId != null) {
                appletObj = mycontext.getAttribute(appletId);
            } else {
                appletObj = jsengine.eval("this", mycontext);
            }
        } catch (Exception exp) {
            throw new RuntimeException(exp);
        }
    }
 
    private Object invoke(String name) {
        return invoke(name, new Object[] { this });
    }
 
    private Object invoke(String name, Object[] args) {
        try {
            Invocable invocable = (Invocable)jsengine;
            return invocable.invokeMethod(appletObj, name, args);
        } catch (ScriptException sexp) {
            //throw new RuntimeException(sexp);
            String exceptionStackTrace = getStackTrace(sexp);
            System.err.println("ERROR invoking["+name+"] with{"+ args +"}\n" 
                    + exceptionStackTrace);
            return null;
        } catch (NoSuchMethodException nexp) {
            return null;
        }
        catch (Exception exc) {
            String exceptionStackTrace = getStackTrace(exc);
            System.err.println("ERROR invoking["+name+"] with{"+ args +"}\n" 
                    + exceptionStackTrace);            
            return null;
        }
    }
 
    private static final String INIT_SRC = "/resources/scriptlet.init.js";
 
    private void loadInitScript() 
        throws ScriptException, NoSuchMethodException {
        InputStream is = PAppletScriptlet.class.getResourceAsStream(INIT_SRC);  
        jsengine.eval(new InputStreamReader(is), mycontext);
        Object contextThis = jsengine.eval("this", mycontext);
        Object res = ((Invocable)jsengine).invokeMethod(contextThis, 
                          "wrapJSWindow", 
                          new Object[] { this });
        mycontext.setAttribute("window", res, ScriptContext.ENGINE_SCOPE);
    }
}
 

http://jddaniels.googlecode.com/svn/trunk/processing-scripting/src/net/ddaniels/processing/scripting/PAppletScriptlet.java

index.html

This is the HTML page that communicates to the applet passing it the script text from the textarea HTML object on the page. It also dynamically loads the most up to date script contents from the SVN when the user selects an item in the combobox.

http://jddaniels.googlecode.com/svn/trunk/processing-scripting/dist/index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
	<head>
	        <!-- charset must remain utf-8 to be handled properly by Processing -->
		<meta http-equiv="content-type" content="text/html; charset=utf-8" />
 
		<title>processing-scriptlet : Built with Processing (somewhat)</title>
 
		<script language="JavaScript">
		var clientRequests = new Array();
		function clientSideInclude(url) {
 
 
		  var req = false;
		  var responseText;
		  //Do a quick look up in cache
		  responseText = clientRequests[url];
		  //alert("HI");
		  //alert("type:"+typeof(responseText) + " value:"+responseText+ " !value:"+!responseText);
		  if(responseText != undefined) {
		  	//alert("type:"+typeof(responseText) + " value:"+responseText+ " !value:"+!responseText);
		  	return responseText;
		  }
		  // For Safari, Firefox, and other non-MS browsers
		  if (window.XMLHttpRequest) {
		    try {
		      req = new XMLHttpRequest();
		    } catch (e) {
		      req = false;
		    }
		  } else if (window.ActiveXObject) {
		    // For Internet Explorer on Windows
		    try {
		      req = new ActiveXObject("Msxml2.XMLHTTP");
		    } catch (e) {
		      try {
		        req = new ActiveXObject("Microsoft.XMLHTTP");
		      } catch (e) {
		        req = false;
		      }
		    }
		  }
		  if (req) {
		    // Synchronous request, wait till we have it all
		    req.open('GET', url, false);
		    req.send(null);
		    //save in cache
		    clientRequests[url] = req.responseText;
		    //alert("response text: " + req.responseText);
		    responseText = clientRequests[url];
		  } else {
		    responseText =
		   "Sorry, your browser does not support " +
		      "XMLHTTPRequest objects. This page requires " +
		      "Internet Explorer 5 or better for Windows, " +
		      "or Firefox for any system, or Safari. Other " +
		      "compatible browsers may also exist.";
 
		  }
		  return responseText;
		}
		function resetScript(textareaId) {
			onScriptSelect(textareaId);
		}
			function setScript(textareaId) {
				try{
					var scriptContents = document.getElementById(textareaId).value;
					var applet = document.getElementById("processingapplet");
					//When using the JOGL Applet launcher we want the subapplet
					applet = applet.getSubApplet();
 
					//alert(scriptContents);
					//alert(applet);
 
					applet.setScript(scriptContents);
				}
				catch(e) {
					alert("error setting script to applet:\n" + e);
				}
			}
			var scriptsDir = "scripts";
			function onScriptSelect(textareaId) {
			try{
				var scriptSelect = document.getElementById("scriptSelect");
				var selectedIndex = scriptSelect.selectedIndex;
				if(selectedIndex>-1) {
					var selectedScript = scriptSelect.options[selectedIndex].value;
					//alert("Selected script: "+ scriptsDir+"/"+selectedScript);
					//Give it the full path
					selectedScript = scriptsDir+"/"+selectedScript;
					var scriptTxt = clientSideInclude(selectedScript);
					var scriptTextBox = document.getElementById(textareaId);
					scriptTextBox.value = scriptTxt;
					//Will set the script to the applet on selection
					setScript("scriptContents");
				}
				}
				catch(e) {
					alert("error setting script from combobox:\n" + e);
				}				
			}
		</script>
 
		<style type="text/css">
		/* <![CDATA[ */
 
		body {
  		  margin: 60px 0px 0px 55px;
		  font-family: verdana, geneva, arial, helvetica, sans-serif; 
		  font-size: 11px; 
		  background-color: #ddddcc; 
		  text-decoration: none; 
		  font-weight: normal; 
		  line-height: normal; 
		}
 
		a          { color: #3399cc; }
		a:link     { color: #3399cc; text-decoration: underline; }
		a:visited  { color: #3399cc; text-decoration: underline; }
		a:active   { color: #3399cc; text-decoration: underline; }
		a:hover    { color: #3399cc; text-decoration: underline; }
 
		/* ]]> */
		</style>
 
	</head>
	<body>
		<div id="content">
			 <table>
			 <tr><td>
			<div id="processing_scriptlet_container">
 
			<!--[if !IE]> -->
			<!--classid="java:com.sun.opengl.util.JOGLAppletLauncher.class"-->
 
				<object id="processingapplet"
						classid="java:com.sun.opengl.util.JOGLAppletLauncher.class" 
            			type="application/x-java-applet"
            			archive="processing-scriptlet.jar,opengl.jar,jogl.jar,core.jar"
            			width="640" height="480"
            			standby="Loading Processing software..." >
 
					<param name="code" value="com.sun.opengl.util.JOGLAppletLauncher" />
					<!--  param name="code" value="net.ddaniels.processing.scripting.PAppletScriptlet" /-->
 
					<param name="mayscript" value="true" />
					<param name="scriptable" value="true" />
 
					<param name="image" value="loading.gif" />
					<param name="boxmessage" value="Loading Processing software..." />
					<param name="boxbgcolor" value="#FFFFFF" />
					<param name="progressbar" value="true" />
 
					<param name="subapplet.classname" value="net.ddaniels.processing.scripting.PAppletScriptlet" /> 
					<param name="subapplet.displayname" value="Processing Scriptlet" />
 
					<param name="test_string" value="outer" />
 
					<param name="scriptsrc" value="scripts/lightsOpengl.js"/>
			<!--<![endif]-->
				<!--param name="code" value="com.sun.opengl.util.JOGLAppletLauncher" /-->
				<!-- 
				<object classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93" 
						codebase="http://java.sun.com/update/1.4.2/jinstall-1_4_2_12-windows-i586.cab"
						width="640" height="480"
						standby="Loading Processing software..."  >
 
 
					<param name="code" value="net.ddaniels.processing.scripting.PAppletScriptlet" />
 
					<param name="archive" value="processing-scriptlet.jar,opengl.jar,jogl.jar,core.jar" />
 
					<param name="mayscript" value="true" />
					<param name="scriptable" value="true" />
 
					<param name="image" value="loading.gif" />
					<param name="boxmessage" value="Loading Processing software..." />
					<param name="boxbgcolor" value="#FFFFFF" />
					<param name="progressbar" value="true" />
 
 
					<param name="subapplet.classname" value="net.ddaniels.processing.scripting.PAppletScriptlet" /> 
					<param name="subapplet.displayname" value="PAppletScriptable" />
 
					<param name="test_string" value="inner" />
					<param name="scriptsrc" value="scripts/test.js"/>
					 -->
					<p>
						<strong>
							This browser does not have a Java Plug-in.
							<br />
							<a href="http://java.sun.com/products/plugin/downloads/index.html" title="Download Java Plug-in">
								Get the latest Java Plug-in here.
							</a>
						</strong>
					</p>
 
				</object>
 
			<!--[if !IE]> -->
				</object>
			<!--<![endif]-->
 
			</div>
			</td>
			<td valign="top">
	    <textarea style="height: 420px;" name="scriptContents" id="scriptContents" cols="80" rows="10" class="edit" tabindex="1">
function setup(p) 
{
	with(p){
  size(800, 600, OPENGL);
  noStroke();
	}
}
 
function draw(p) 
{
	with(p){
  defineLights(p);
  background(0);
 
  for (var i = 0; i <= width; i += 100) {
    for (var j = 0; j <= height; j += 100) {
      pushMatrix();
      translate(i, j);
      rotateY(map(mouseX, 0, width, 0, PI));
      rotateX(map(mouseY, 0, height, 0, PI));
      box(90);
      popMatrix();
    }
  }
	}
}
 
function defineLights(p) {
 with(p){
  // Orange point light on the right
  pointLight(150, 100, 0,   // Color
             200, -150, 0); // Position
 
  // Blue directional light from the left
  directionalLight(0, 102, 255, // Color
                   1, 0, 0);    // The x-, y-, z-axis direction
 
  // Yellow spotlight from the front
  spotLight(255, 255, 109,  // Color
            0, 40, 200,     // Position
            0, -0.5, -0.5,  // Direction
            PI / 2, 2);     // Angle, concentration
 
}
}				
</textarea>
<br/>
 
 
			<input type="button" name="Button1" value="Execute"
			onClick="setScript('scriptContents');" language="JavaScript"/>
 
			<input type="button" name="resetButton" value="Reset"
			onClick="resetScript('scriptContents')" language="JavaScript"/>
 
	           <select id="scriptSelect" NAME="scriptSelect" onchange="onScriptSelect('scriptContents');">
	               <option value="lightsOpengl.js">Lights OpenGL</option>
	               <option VALUE="test.js">Circles 2D</option>
	               <option VALUE="spaceJunk.js">spaceJunk.js</option>
	               <!--  option VALUE="esfera.js">esfera.js</option-->
	          </select>		
			</td>
			</tr>
</table>			
			<p>
			DDaniels.
 
 
			</p>
 
			<p>
			Source code: <a href="http://eventhorizongames.com/wiki/doku.php?id=articles:processing:scripting:start">PAppletScriptlet</a> 
			</p>
						<p>
			Raw SVN Source code: <a href="http://jddaniels.googlecode.com/svn/trunk/processing-scripting/src/net/ddaniels/processing/scripting/PAppletScriptlet.java">PAppletScriptlet.java</a> 
			</p>
 
			<p>
			Built with <a href="http://processing.org" title="Processing.org">Processing</a>
			</p>
		</div>
	</body>
</html>
 

http://jddaniels.googlecode.com/svn/trunk/processing-scripting/dist/index.html

 
articles/processing/scripting/start.txt · Last modified: 2009/04/30 22:56 (external edit)
 
Except where otherwise noted, content on this wiki is licensed under the following license:CC Attribution-Noncommercial-Share Alike 3.0 Unported
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki