diff --git a/_test/tests/inc/form/checkableelement.test.php b/_test/tests/inc/form/checkableelement.test.php
new file mode 100644
index 0000000000000000000000000000000000000000..a0e4173e860b470efaae56885875c4fb9003c6a7
--- /dev/null
+++ b/_test/tests/inc/form/checkableelement.test.php
@@ -0,0 +1,49 @@
+<?php
+
+use dokuwiki\Form;
+
+class form_checkableelement_test extends DokuWikiTest {
+
+    function test_defaults() {
+        $form = new Form\Form();
+        $form->addRadioButton('foo', 'label text first')->val('first')->attr('checked', 'checked');
+        $form->addRadioButton('foo', 'label text second')->val('second');
+
+        $html = $form->toHTML();
+        $pq = phpQuery::newDocumentXHTML($html);
+
+        $input = $pq->find('input[name=foo]');
+        $this->assertTrue($input->length == 2);
+
+        $label = $pq->find('label');
+        $this->assertTrue($label->length == 2);
+
+        $inputs = $pq->find('input[name=foo]');
+        $this->assertEquals('first', pq($inputs->elements[0])->val());
+        $this->assertEquals('second', pq($inputs->elements[1])->val());
+        $this->assertEquals('checked', pq($inputs->elements[0])->attr('checked'));
+        $this->assertEquals('', pq($inputs->elements[1])->attr('checked'));
+    }
+
+    /**
+     * check that posted values overwrite preset default
+     */
+    function test_prefill() {
+        global $INPUT;
+        $INPUT->post->set('foo', 'second');
+
+
+        $form = new Form\Form();
+        $form->addRadioButton('foo', 'label text first')->val('first')->attr('checked', 'checked');
+        $form->addRadioButton('foo', 'label text second')->val('second');
+
+        $html = $form->toHTML();
+        $pq = phpQuery::newDocumentXHTML($html);
+
+        $inputs = $pq->find('input[name=foo]');
+        $this->assertEquals('first', pq($inputs->elements[0])->val());
+        $this->assertEquals('second', pq($inputs->elements[1])->val());
+        $this->assertEquals('', pq($inputs->elements[0])->attr('checked'));
+        $this->assertEquals('checked', pq($inputs->elements[1])->attr('checked'));
+    }
+}
diff --git a/_test/tests/inc/form/form.test.php b/_test/tests/inc/form/form.test.php
new file mode 100644
index 0000000000000000000000000000000000000000..3ae832b2cd5d6f5aa81bb633a7a012d8209e91a9
--- /dev/null
+++ b/_test/tests/inc/form/form.test.php
@@ -0,0 +1,115 @@
+<?php
+
+use dokuwiki\Form;
+
+/**
+ * makes form internals accessible for testing
+ */
+class TestForm extends Form\Form {
+    /**
+     * @return array list of element types
+     */
+    function getElementTypeList() {
+        $list = array();
+        foreach($this->elements as $element) $list[] = $element->getType();
+        return $list;
+    }
+
+    public function balanceFieldsets() {
+        parent::balanceFieldsets();
+    }
+
+}
+
+class form_form_test extends DokuWikiTest {
+
+    /**
+     * checks that an empty form is initialized correctly
+     */
+    function test_defaults() {
+        global $INPUT;
+        global $ID;
+        $ID = 'some:test';
+        $INPUT->get->set('id', $ID);
+        $INPUT->get->set('foo', 'bar');
+
+        $form = new Form\Form();
+        $html = $form->toHTML();
+        $pq = phpQuery::newDocumentXHTML($html);
+
+        $this->assertTrue($pq->find('form')->hasClass('doku_form'));
+        $this->assertEquals(wl($ID, array('foo' => 'bar'), false, '&'), $pq->find('form')->attr('action'));
+        $this->assertEquals('post', $pq->find('form')->attr('method'));
+
+        $this->assertTrue($pq->find('input[name=sectok]')->length == 1);
+    }
+
+
+    function test_fieldsetbalance() {
+        $form = new TestForm();
+        $form->addFieldsetOpen();
+        $form->addHTML('ignored');
+        $form->addFieldsetClose();
+        $form->balanceFieldsets();
+
+        $this->assertEquals(
+            array(
+                'fieldsetopen',
+                'html',
+                'fieldsetclose'
+            ),
+            $form->getElementTypeList()
+        );
+
+        $form = new TestForm();
+        $form->addHTML('ignored');
+        $form->addFieldsetClose();
+        $form->balanceFieldsets();
+
+        $this->assertEquals(
+            array(
+                'fieldsetopen',
+                'html',
+                'fieldsetclose'
+            ),
+            $form->getElementTypeList()
+        );
+
+
+        $form = new TestForm();
+        $form->addFieldsetOpen();
+        $form->addHTML('ignored');
+        $form->balanceFieldsets();
+
+        $this->assertEquals(
+            array(
+                'fieldsetopen',
+                'html',
+                'fieldsetclose'
+            ),
+            $form->getElementTypeList()
+        );
+
+        $form = new TestForm();
+        $form->addHTML('ignored');
+        $form->addFieldsetClose();
+        $form->addHTML('ignored');
+        $form->addFieldsetOpen();
+        $form->addHTML('ignored');
+        $form->balanceFieldsets();
+
+        $this->assertEquals(
+            array(
+                'fieldsetopen',
+                'html',
+                'fieldsetclose',
+                'html',
+                'fieldsetopen',
+                'html',
+                'fieldsetclose'
+            ),
+            $form->getElementTypeList()
+        );
+    }
+
+}
diff --git a/_test/tests/inc/form/inputelement.test.php b/_test/tests/inc/form/inputelement.test.php
new file mode 100644
index 0000000000000000000000000000000000000000..7a5e6d2ea944f349c702bc1907008972c99e0d31
--- /dev/null
+++ b/_test/tests/inc/form/inputelement.test.php
@@ -0,0 +1,41 @@
+<?php
+
+use dokuwiki\Form;
+
+class form_inputelement_test extends DokuWikiTest {
+
+    function test_defaults() {
+        $form = new Form\Form();
+        $form->addTextInput('foo', 'label text')->val('this is text');
+
+        $html = $form->toHTML();
+        $pq = phpQuery::newDocumentXHTML($html);
+
+        $input = $pq->find('input[name=foo]');
+        $this->assertTrue($input->length == 1);
+        $this->assertEquals('this is text', $input->val());
+
+        $label = $pq->find('label');
+        $this->assertTrue($label->length == 1);
+        $this->assertEquals('label text', $label->find('span')->text());
+    }
+
+    /**
+     * check that posted values overwrite preset default
+     */
+    function test_prefill() {
+        global $INPUT;
+        $INPUT->post->set('foo', 'a new text');
+
+        $form = new Form\Form();
+        $form->addTextInput('foo', 'label text')->val('this is text');
+
+        $html = $form->toHTML();
+        $pq = phpQuery::newDocumentXHTML($html);
+
+        $input = $pq->find('input[name=foo]');
+        $this->assertTrue($input->length == 1);
+        $this->assertEquals('a new text', $input->val());
+    }
+
+}
diff --git a/inc/Form/CheckableElement.php b/inc/Form/CheckableElement.php
new file mode 100644
index 0000000000000000000000000000000000000000..a57bbc1f628fb230d712b901817ce2101e8e5785
--- /dev/null
+++ b/inc/Form/CheckableElement.php
@@ -0,0 +1,62 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class CheckableElement
+ *
+ * For Radio- and Checkboxes
+ *
+ * @package DokuForm
+ */
+class CheckableElement extends InputElement {
+
+    /**
+     * @param string $type The type of this element
+     * @param string $name The name of this form element
+     * @param string $label The label text for this element
+     */
+    public function __construct($type, $name, $label) {
+        parent::__construct($type, $name, $label);
+        // default value is 1
+        $this->attr('value', 1);
+    }
+
+    /**
+     * Handles the useInput flag and sets the checked attribute accordingly
+     */
+    protected function prefillInput() {
+        global $INPUT;
+        list($name, $key) = $this->getInputName();
+        $myvalue = $this->val();
+
+        if(!$INPUT->has($name)) return;
+
+        if($key === null) {
+            // no key - single value
+            $value = $INPUT->str($name);
+            if($value == $myvalue) {
+                $this->attr('checked', 'checked');
+            } else {
+                $this->rmattr('checked');
+            }
+        } else {
+            // we have an array, there might be several values in it
+            $input = $INPUT->arr($name);
+            if(isset($input[$key])) {
+                $this->rmattr('checked');
+
+                // values seem to be in another sub array
+                if(is_array($input[$key])) {
+                    $input = $input[$key];
+                }
+
+                foreach($input as $value) {
+                    if($value == $myvalue) {
+                        $this->attr('checked', 'checked');
+                    }
+                }
+            }
+        }
+    }
+
+}
diff --git a/inc/Form/Element.php b/inc/Form/Element.php
new file mode 100644
index 0000000000000000000000000000000000000000..7938ee0090434e2ba07d00ac5c4e33616ce4b703
--- /dev/null
+++ b/inc/Form/Element.php
@@ -0,0 +1,151 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class Element
+ *
+ * The basic building block of a form
+ *
+ * @package dokuwiki\Form
+ */
+abstract class Element {
+
+    /**
+     * @var array the attributes of this element
+     */
+    protected $attributes = array();
+
+    /**
+     * @var string The type of this element
+     */
+    protected $type;
+
+    /**
+     * @param string $type The type of this element
+     * @param array $attributes
+     */
+    public function __construct($type, $attributes = array()) {
+        $this->type = $type;
+        $this->attributes = $attributes;
+    }
+
+    /**
+     * Type of this element
+     *
+     * @return string
+     */
+    public function getType() {
+        return $this->type;
+    }
+
+    /**
+     * Gets or sets an attribute
+     *
+     * When no $value is given, the current content of the attribute is returned.
+     * An empty string is returned for unset attributes.
+     *
+     * When a $value is given, the content is set to that value and the Element
+     * itself is returned for easy chaining
+     *
+     * @param string $name Name of the attribute to access
+     * @param null|string $value New value to set
+     * @return string|$this
+     */
+    public function attr($name, $value = null) {
+        // set
+        if($value !== null) {
+            $this->attributes[$name] = $value;
+            return $this;
+        }
+
+        // get
+        if(isset($this->attributes[$name])) {
+            return $this->attributes[$name];
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * Removes the given attribute if it exists
+     *
+     * @param $name
+     * @return $this
+     */
+    public function rmattr($name) {
+        if(isset($this->attributes[$name])) {
+            unset($this->attributes[$name]);
+        }
+        return $this;
+    }
+
+    /**
+     * Gets or adds a all given attributes at once
+     *
+     * @param array|null $attributes
+     * @return array|$this
+     */
+    public function attrs($attributes = null) {
+        // set
+        if($attributes) {
+            foreach((array) $attributes as $key => $val) {
+                $this->attr($key, $val);
+            }
+            return $this;
+        }
+        // get
+        return $this->attributes;
+    }
+
+    /**
+     * Adds a class to the class attribute
+     *
+     * This is the preferred method of setting the element's class
+     *
+     * @param string $class the new class to add
+     * @return $this
+     */
+    public function addClass($class) {
+        $classes = explode(' ', $this->attr('class'));
+        $classes[] = $class;
+        $classes = array_unique($classes);
+        $classes = array_filter($classes);
+        $this->attr('class', join(' ', $classes));
+        return $this;
+    }
+
+    /**
+     * Get or set the element's ID
+     *
+     * This is the preferred way of setting the element's ID
+     *
+     * @param null|string $id
+     * @return string|$this
+     */
+    public function id($id = null) {
+        if(strpos($id, '__') === false) {
+            throw new \InvalidArgumentException('IDs in DokuWiki have to contain two subsequent underscores');
+        }
+
+        return $this->attr('id', $id);
+    }
+
+    /**
+     * Get or set the element's value
+     *
+     * This is the preferred way of setting the element's value
+     *
+     * @param null|string $value
+     * @return string|$this
+     */
+    public function val($value = null) {
+        return $this->attr('value', $value);
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    abstract public function toHTML();
+}
diff --git a/inc/Form/FieldsetCloseElement.php b/inc/Form/FieldsetCloseElement.php
new file mode 100644
index 0000000000000000000000000000000000000000..8f26717aa86af400523257cf359d524f29017d92
--- /dev/null
+++ b/inc/Form/FieldsetCloseElement.php
@@ -0,0 +1,30 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class FieldsetCloseElement
+ *
+ * Closes an open Fieldset
+ *
+ * @package dokuwiki\Form
+ */
+class FieldsetCloseElement extends TagCloseElement {
+
+    /**
+     * @param array $attributes
+     */
+    public function __construct($attributes = array()) {
+        parent::__construct('', $attributes);
+        $this->type = 'fieldsetclose';
+    }
+
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        return '</fieldset>';
+    }
+}
diff --git a/inc/Form/FieldsetOpenElement.php b/inc/Form/FieldsetOpenElement.php
new file mode 100644
index 0000000000000000000000000000000000000000..a7de461faa6d0a9bd9256120d88e265c48b8879e
--- /dev/null
+++ b/inc/Form/FieldsetOpenElement.php
@@ -0,0 +1,36 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class FieldsetOpenElement
+ *
+ * Opens a Fieldset with an optional legend
+ *
+ * @package dokuwiki\Form
+ */
+class FieldsetOpenElement extends TagOpenElement {
+
+    /**
+     * @param string $legend
+     * @param array $attributes
+     */
+    public function __construct($legend='', $attributes = array()) {
+        // this is a bit messy and we just do it for the nicer class hierarchy
+        // the parent would expect the tag in $value but we're storing the
+        // legend there, so we have to set the type manually
+        parent::__construct($legend, $attributes);
+        $this->type = 'fieldsetopen';
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        $html = '<fieldset '.buildAttributes($this->attrs()).'>';
+        $legend = $this->val();
+        if($legend) $html .= DOKU_LF.'<legend>'.hsc($legend).'</legend>';
+        return $html;
+    }
+}
diff --git a/inc/Form/Form.php b/inc/Form/Form.php
new file mode 100644
index 0000000000000000000000000000000000000000..625557fa1949baa1eff2324c3a1c527a79a39876
--- /dev/null
+++ b/inc/Form/Form.php
@@ -0,0 +1,367 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class Form
+ *
+ * Represents the whole Form. This is what you work on, and add Elements to
+ *
+ * @package dokuwiki\Form
+ */
+class Form extends Element {
+
+    /**
+     * @var array name value pairs for hidden values
+     */
+    protected $hidden = array();
+
+    /**
+     * @var Element[] the elements of the form
+     */
+    protected $elements = array();
+
+    /**
+     * Creates a new, empty form with some default attributes
+     *
+     * @param array $attributes
+     */
+    public function __construct($attributes = array()) {
+        global $ID;
+
+        parent::__construct('form', $attributes);
+
+        // use the current URL as default action
+        if(!$this->attr('action')) {
+            $get = $_GET;
+            if(isset($get['id'])) unset($get['id']);
+            $self = wl($ID, $get, false, '&'); //attributes are escaped later
+            $this->attr('action', $self);
+        }
+
+        // post is default
+        if(!$this->attr('method')) {
+            $this->attr('method', 'post');
+        }
+
+        // we like UTF-8
+        if(!$this->attr('accept-charset')) {
+            $this->attr('accept-charset', 'utf-8');
+        }
+
+        // add the security token by default
+        $this->setHiddenField('sectok', getSecurityToken());
+
+        // identify this as a new form based form in HTML
+        $this->addClass('doku_form');
+    }
+
+    /**
+     * Sets a hidden field
+     *
+     * @param $name
+     * @param $value
+     * @return $this
+     */
+    public function setHiddenField($name, $value) {
+        $this->hidden[$name] = $value;
+        return $this;
+    }
+
+    #region element query function
+
+    /**
+     * Returns the numbers of elements in the form
+     *
+     * @return int
+     */
+    public function elementCount() {
+        return count($this->elements);
+    }
+
+    /**
+     * Returns a reference to the element at a position.
+     * A position out-of-bounds will return either the
+     * first (underflow) or last (overflow) element.
+     *
+     * @param $pos
+     * @return Element
+     */
+    public function getElementAt($pos) {
+        if($pos < 0) $pos = count($this->elements) + $pos;
+        if($pos < 0) $pos = 0;
+        if($pos >= count($this->elements)) $pos = count($this->elements) - 1;
+        return $this->elements[$pos];
+    }
+
+    /**
+     * Gets the position of the first of a type of element
+     *
+     * @param string $type Element type to look for.
+     * @param int $offset search from this position onward
+     * @return false|int position of element if found, otherwise false
+     */
+    public function findPositionByType($type, $offset = 0) {
+        $len = $this->elementCount();
+        for($pos = $offset; $pos < $len; $pos++) {
+            if($this->elements[$pos]->getType() == $type) {
+                return $pos;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Gets the position of the first element matching the attribute
+     *
+     * @param string $name Name of the attribute
+     * @param string $value Value the attribute should have
+     * @param int $offset search from this position onward
+     * @return false|int position of element if found, otherwise false
+     */
+    public function findPositionByAttribute($name, $value, $offset = 0) {
+        $len = $this->elementCount();
+        for($pos = $offset; $pos < $len; $pos++) {
+            if($this->elements[$pos]->attr($name) == $value) {
+                return $pos;
+            }
+        }
+        return false;
+    }
+
+    #endregion
+
+    #region Element positioning functions
+
+    /**
+     * Adds or inserts an element to the form
+     *
+     * @param Element $element
+     * @param int $pos 0-based position in the form, -1 for at the end
+     * @return Element
+     */
+    public function addElement(Element $element, $pos = -1) {
+        if(is_a($element, '\dokuwiki\Form')) throw new \InvalidArgumentException('You can\'t add a form to a form');
+        if($pos < 0) {
+            $this->elements[] = $element;
+        } else {
+            array_splice($this->elements, $pos, 0, array($element));
+        }
+        return $element;
+    }
+
+    /**
+     * Replaces an existing element with a new one
+     *
+     * @param Element $element the new element
+     * @param $pos 0-based position of the element to replace
+     */
+    public function replaceElement(Element $element, $pos) {
+        if(is_a($element, '\dokuwiki\Form')) throw new \InvalidArgumentException('You can\'t add a form to a form');
+        array_splice($this->elements, $pos, 1, array($element));
+    }
+
+    /**
+     * Remove an element from the form completely
+     *
+     * @param $pos 0-based position of the element to remove
+     */
+    public function removeElement($pos) {
+        array_splice($this->elements, $pos, 1);
+    }
+
+    #endregion
+
+    #region Element adding functions
+
+    /**
+     * Adds a text input field
+     *
+     * @param $name
+     * @param $label
+     * @param int $pos
+     * @return InputElement
+     */
+    public function addTextInput($name, $label = '', $pos = -1) {
+        return $this->addElement(new InputElement('text', $name, $label), $pos);
+    }
+
+    /**
+     * Adds a password input field
+     *
+     * @param $name
+     * @param $label
+     * @param int $pos
+     * @return InputElement
+     */
+    public function addPasswordInput($name, $label = '', $pos = -1) {
+        return $this->addElement(new InputElement('password', $name, $label), $pos);
+    }
+
+    /**
+     * Adds a radio button field
+     *
+     * @param $name
+     * @param $label
+     * @param int $pos
+     * @return CheckableElement
+     */
+    public function addRadioButton($name, $label = '', $pos = -1) {
+        return $this->addElement(new CheckableElement('radio', $name, $label), $pos);
+    }
+
+    /**
+     * Adds a checkbox field
+     *
+     * @param $name
+     * @param $label
+     * @param int $pos
+     * @return CheckableElement
+     */
+    public function addCheckbox($name, $label = '', $pos = -1) {
+        return $this->addElement(new CheckableElement('checkbox', $name, $label), $pos);
+    }
+
+    /**
+     * Adds a textarea field
+     *
+     * @param $name
+     * @param $label
+     * @param int $pos
+     * @return TextareaElement
+     */
+    public function addTextarea($name, $label = '', $pos = -1) {
+        return $this->addElement(new TextareaElement($name, $label), $pos);
+    }
+
+    /**
+     * Add fixed HTML to the form
+     *
+     * @param $html
+     * @param int $pos
+     * @return HTMLElement
+     */
+    public function addHTML($html, $pos = -1) {
+        return $this->addElement(new HTMLElement($html), $pos);
+    }
+
+    /**
+     * Add a closed HTML tag to the form
+     *
+     * @param $tag
+     * @param int $pos
+     * @return TagElement
+     */
+    public function addTag($tag, $pos = -1) {
+        return $this->addElement(new TagElement($tag), $pos);
+    }
+
+    /**
+     * Add an open HTML tag to the form
+     *
+     * Be sure to close it again!
+     *
+     * @param $tag
+     * @param int $pos
+     * @return TagOpenElement
+     */
+    public function addTagOpen($tag, $pos = -1) {
+        return $this->addElement(new TagOpenElement($tag), $pos);
+    }
+
+    /**
+     * Add a closing HTML tag to the form
+     *
+     * Be sure it had been opened before
+     *
+     * @param $tag
+     * @param int $pos
+     * @return TagCloseElement
+     */
+    public function addTagClose($tag, $pos = -1) {
+        return $this->addElement(new TagCloseElement($tag), $pos);
+    }
+
+    /**
+     * Open a Fieldset
+     *
+     * @param $legend
+     * @param int $pos
+     * @return FieldsetOpenElement
+     */
+    public function addFieldsetOpen($legend = '', $pos = -1) {
+        return $this->addElement(new FieldsetOpenElement($legend), $pos);
+    }
+
+    /**
+     * Close a fieldset
+     *
+     * @param int $pos
+     * @return TagCloseElement
+     */
+    public function addFieldsetClose($pos = -1) {
+        return $this->addElement(new FieldsetCloseElement(), $pos);
+    }
+
+    #endregion
+
+    /**
+     * Adjust the elements so that fieldset open and closes are matching
+     */
+    protected function balanceFieldsets() {
+        $lastclose = 0;
+        $isopen = false;
+        $len = count($this->elements);
+
+        for($pos = 0; $pos < $len; $pos++) {
+            $type = $this->elements[$pos]->getType();
+            if($type == 'fieldsetopen') {
+                if($isopen) {
+                    //close previous fieldset
+                    $this->addFieldsetClose($pos);
+                    $lastclose = $pos + 1;
+                    $pos++;
+                    $len++;
+                }
+                $isopen = true;
+            } else if($type == 'fieldsetclose') {
+                if(!$isopen) {
+                    // make sure there was a fieldsetopen
+                    // either right after the last close or at the begining
+                    $this->addFieldsetOpen('', $lastclose);
+                    $len++;
+                    $pos++;
+                }
+                $lastclose = $pos;
+                $isopen = false;
+            }
+        }
+
+        // close open fieldset at the end
+        if($isopen) {
+            $this->addFieldsetClose();
+        }
+    }
+
+    /**
+     * The HTML representation of the whole form
+     *
+     * @return string
+     */
+    public function toHTML() {
+        $this->balanceFieldsets();
+
+        $html = '<form ' . buildAttributes($this->attrs()) . '>' . DOKU_LF;
+
+        foreach($this->hidden as $name => $value) {
+            $html .= '<input type="hidden" name="' . $name . '" value="' . formText($value) . '" />' . DOKU_LF;
+        }
+
+        foreach($this->elements as $element) {
+            $html .= $element->toHTML() . DOKU_LF;
+        }
+
+        $html .= '</form>' . DOKU_LF;
+
+        return $html;
+    }
+}
diff --git a/inc/Form/HTMLElement.php b/inc/Form/HTMLElement.php
new file mode 100644
index 0000000000000000000000000000000000000000..591cf472f97578aa829b7b19981d8b6a271b7432
--- /dev/null
+++ b/inc/Form/HTMLElement.php
@@ -0,0 +1,29 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class HTMLElement
+ *
+ * Holds arbitrary HTML that is added as is to the Form
+ *
+ * @package dokuwiki\Form
+ */
+class HTMLElement extends ValueElement {
+
+
+    /**
+     * @param string $html
+     */
+    public function __construct($html) {
+        parent::__construct('html', $html);
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        return $this->val();
+    }
+}
diff --git a/inc/Form/InputElement.php b/inc/Form/InputElement.php
new file mode 100644
index 0000000000000000000000000000000000000000..5908f7d11821a586316fe62b09ea94a677fc6926
--- /dev/null
+++ b/inc/Form/InputElement.php
@@ -0,0 +1,160 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class InputElement
+ *
+ * Base class for all input elements. Uses a wrapping label when label
+ * text is given.
+ *
+ * @todo figure out how to make wrapping or related label configurable
+ * @package dokuwiki\Form
+ */
+class InputElement extends Element {
+    /**
+     * @var Label
+     */
+    protected $label = null;
+
+    /**
+     * @var bool if the element should reflect posted values
+     */
+    protected $useInput = true;
+
+    /**
+     * @param string $type The type of this element
+     * @param string $name The name of this form element
+     * @param string $label The label text for this element
+     */
+    public function __construct($type, $name, $label = '') {
+        parent::__construct($type, array('name' => $name));
+        $this->attr('name', $name);
+        if($label) $this->label = new Label($label);
+    }
+
+    /**
+     * Returns the label element if there's one set
+     *
+     * @return Label|null
+     */
+    public function getLabel() {
+        return $this->label;
+    }
+
+    /**
+     * Should the user sent input be used to initialize the input field
+     *
+     * The default is true. Any set values will be overwritten by the INPUT
+     * provided values.
+     *
+     * @param bool $useinput
+     * @return $this
+     */
+    public function useInput($useinput) {
+        $this->useInput = (bool) $useinput;
+        return $this;
+    }
+
+    /**
+     * Get or set the element's ID
+     *
+     * @param null|string $id
+     * @return string|$this
+     */
+    public function id($id = null) {
+        if($this->label) $this->label->attr('for', $id);
+        return parent::id($id);
+    }
+
+    /**
+     * Adds a class to the class attribute
+     *
+     * This is the preferred method of setting the element's class
+     *
+     * @param string $class the new class to add
+     * @return $this
+     */
+    public function addClass($class) {
+        if($this->label) $this->label->addClass($class);
+        return parent::addClass($class);
+    }
+
+    /**
+     * Figures out how to access the value for this field from INPUT data
+     *
+     * The element's name could have been given as a simple string ('foo')
+     * or in array notation ('foo[bar]').
+     *
+     * Note: this function only handles one level of arrays. If your data
+     * is nested deeper, you should call useInput(false) and set the
+     * correct value yourself
+     *
+     * @return array name and array key (null if not an array)
+     */
+    protected function getInputName() {
+        $name = $this->attr('name');
+        parse_str("$name=1", $parsed);
+
+        $name = array_keys($parsed);
+        $name = array_shift($name);
+
+        if(is_array($parsed[$name])) {
+            $key = array_keys($parsed[$name]);
+            $key = array_shift($key);
+        } else {
+            $key = null;
+        }
+
+        return array($name, $key);
+    }
+
+    /**
+     * Handles the useInput flag and set the value attribute accordingly
+     */
+    protected function prefillInput() {
+        global $INPUT;
+
+        list($name, $key) = $this->getInputName();
+        if(!$INPUT->has($name)) return;
+
+        if($key === null) {
+            $value = $INPUT->str($name);
+        } else {
+            $value = $INPUT->arr($name);
+            if(isset($value[$key])) {
+                $value = $value[$key];
+            } else {
+                $value = '';
+            }
+        }
+        if($value !== '') {
+            $this->val($value);
+        }
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    protected function mainElementHTML() {
+        if($this->useInput) $this->prefillInput();
+        return '<input ' . buildAttributes($this->attrs()) . ' />';
+    }
+
+    /**
+     * The HTML representation of this element wrapped in a label
+     *
+     * @return string
+     */
+    public function toHTML() {
+        if($this->label) {
+            return '<label ' . buildAttributes($this->label->attrs()) . '>' . DOKU_LF .
+            '<span>' . hsc($this->label->val()) . '</span>' . DOKU_LF .
+            $this->mainElementHTML() . DOKU_LF .
+            '</label>';
+        } else {
+            return $this->mainElementHTML();
+        }
+    }
+}
diff --git a/inc/Form/Label.php b/inc/Form/Label.php
new file mode 100644
index 0000000000000000000000000000000000000000..8dcd7cd5f2fea037fa9932409b941c86f6e2dfd6
--- /dev/null
+++ b/inc/Form/Label.php
@@ -0,0 +1,27 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class Label
+ * @package dokuwiki\Form
+ */
+class Label extends ValueElement {
+
+    /**
+     * Creates a new Label
+     *
+     * @param string $label
+     */
+    public function __construct($label) {
+        parent::__construct('label', $label);
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        return '<label ' . buildAttributes($this->attrs()) . '>' . hsc($this->val()) . '</label>';
+    }
+}
diff --git a/inc/Form/LegacyForm.php b/inc/Form/LegacyForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..1b47ba204966ce9df84c4d406c8429b28b3a1e0c
--- /dev/null
+++ b/inc/Form/LegacyForm.php
@@ -0,0 +1,181 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class LegacyForm
+ *
+ * Provides a compatibility layer to the old Doku_Form API
+ *
+ * This can be used to work with the modern API on forms provided by old events for
+ * example. When you start new forms, just use Form\Form
+ *
+ * @package dokuwiki\Form
+ */
+class LegacyForm extends Form {
+
+    /**
+     * Creates a new modern form from an old legacy Doku_Form
+     *
+     * @param \Doku_Form $oldform
+     */
+    public function __construct(\Doku_Form $oldform) {
+        parent::__construct($oldform->params);
+
+        $this->hidden = $oldform->_hidden;
+
+        foreach($oldform->_content as $element) {
+            list($ctl, $attr) = $this->parseLegacyAttr($element);
+
+            if(is_array($element)) {
+                switch($ctl['elem']) {
+                    case 'wikitext':
+                        $this->addTextarea('wikitext')
+                             ->attrs($attr)
+                             ->id('wiki__text')
+                             ->val($ctl['text'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'textfield':
+                        $this->addTextInput($ctl['name'], $ctl['text'])
+                             ->attrs($attr)
+                             ->id($ctl['id'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'passwordfield':
+                        $this->addPasswordInput($ctl['name'], $ctl['text'])
+                             ->attrs($attr)
+                             ->id($ctl['id'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'checkboxfield':
+                        $this->addCheckbox($ctl['name'], $ctl['text'])
+                             ->attrs($attr)
+                             ->id($ctl['id'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'radiofield':
+                        $this->addRadioButton($ctl['name'], $ctl['text'])
+                             ->attrs($attr)
+                             ->id($ctl['id'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'tag':
+                        $this->addTag($ctl['tag'])
+                             ->attrs($attr)
+                             ->attr('name', $ctl['name'])
+                             ->id($ctl['id'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'opentag':
+                        $this->addTagOpen($ctl['tag'])
+                             ->attrs($attr)
+                             ->attr('name', $ctl['name'])
+                             ->id($ctl['id'])
+                             ->addClass($ctl['class']);
+                        break;
+                    case 'closetag':
+                        $this->addTagClose($ctl['tag']);
+                        break;
+                    case 'openfieldset':
+                        $this->addFieldsetOpen($ctl['legend'])
+                            ->attrs($attr)
+                            ->attr('name', $ctl['name'])
+                            ->id($ctl['id'])
+                            ->addClass($ctl['class']);
+                        break;
+                    case 'closefieldset':
+                        $this->addFieldsetClose();
+                        break;
+                    case 'button':
+                    case 'field':
+                    case 'fieldright':
+                    case 'filefield':
+                    case 'menufield':
+                    case 'listboxfield':
+                        throw new \UnexpectedValueException('Unsupported legacy field ' . $ctl['elem']);
+                        break;
+                    default:
+                        throw new \UnexpectedValueException('Unknown legacy field ' . $ctl['elem']);
+
+                }
+            } else {
+                $this->addHTML($element);
+            }
+        }
+
+    }
+
+    /**
+     * Parses out what is the elements attributes and what is control info
+     *
+     * @param array $legacy
+     * @return array
+     */
+    protected function parseLegacyAttr($legacy) {
+        $attributes = array();
+        $control = array();
+
+        foreach($legacy as $key => $val) {
+            if($key{0} == '_') {
+                $control[substr($key, 1)] = $val;
+            } elseif($key == 'name') {
+                $control[$key] = $val;
+            } elseif($key == 'id') {
+                $control[$key] = $val;
+            } else {
+                $attributes[$key] = $val;
+            }
+        }
+
+        return array($control, $attributes);
+    }
+
+    /**
+     * Translates our types to the legacy types
+     *
+     * @param string $type
+     * @return string
+     */
+    protected function legacyType($type) {
+        static $types = array(
+            'text' => 'textfield',
+            'password' => 'passwordfield',
+            'checkbox' => 'checkboxfield',
+            'radio' => 'radiofield',
+            'tagopen' => 'opentag',
+            'tagclose' => 'closetag',
+            'fieldsetopen' => 'openfieldset',
+            'fieldsetclose' => 'closefieldset',
+        );
+        if(isset($types[$type])) return $types[$type];
+        return $type;
+    }
+
+    /**
+     * Creates an old legacy form from this modern form's data
+     *
+     * @return \Doku_Form
+     */
+    public function toLegacy() {
+        $this->balanceFieldsets();
+
+        $legacy = new \Doku_Form($this->attrs());
+        $legacy->_hidden = $this->hidden;
+        foreach($this->elements as $element) {
+            if(is_a($element, 'dokuwiki\Form\HTMLElement')) {
+                $legacy->_content[] = $element->toHTML();
+            } elseif(is_a($element, 'dokuwiki\Form\InputElement')) {
+                /** @var InputElement $element */
+                $data = $element->attrs();
+                $data['_elem'] = $this->legacyType($element->getType());
+                $label = $element->getLabel();
+                if($label) {
+                    $data['_class'] = $label->attr('class');
+                }
+                $legacy->_content[] = $data;
+            }
+        }
+
+        return $legacy;
+    }
+}
diff --git a/inc/Form/TagCloseElement.php b/inc/Form/TagCloseElement.php
new file mode 100644
index 0000000000000000000000000000000000000000..dc0264c21ee676a772ffdedfdbd27132cfc83576
--- /dev/null
+++ b/inc/Form/TagCloseElement.php
@@ -0,0 +1,76 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class TagCloseElement
+ *
+ * Creates an HTML close tag. You have to make sure it has been opened
+ * before or this will produce invalid HTML
+ *
+ * @package dokuwiki\Form
+ */
+class TagCloseElement extends ValueElement {
+
+    /**
+     * @param string $tag
+     * @param array $attributes
+     */
+    public function __construct($tag, $attributes = array()) {
+        parent::__construct('tagclose', $tag, $attributes);
+    }
+
+    /**
+     * do not call this
+     *
+     * @param $class
+     * @return void
+     * @throws \BadMethodCallException
+     */
+    public function addClass($class) {
+        throw new \BadMethodCallException('You can\t add classes to closing tag');
+    }
+
+    /**
+     * do not call this
+     *
+     * @param $id
+     * @return void
+     * @throws \BadMethodCallException
+     */
+    public function id($id = null) {
+        throw new \BadMethodCallException('You can\t add ID to closing tag');
+    }
+
+    /**
+     * do not call this
+     *
+     * @param $name
+     * @param $value
+     * @return void
+     * @throws \BadMethodCallException
+     */
+    public function attr($name, $value = null) {
+        throw new \BadMethodCallException('You can\t add attributes to closing tag');
+    }
+
+    /**
+     * do not call this
+     *
+     * @param $attributes
+     * @return void
+     * @throws \BadMethodCallException
+     */
+    public function attrs($attributes = null) {
+        throw new \BadMethodCallException('You can\t add attributes to closing tag');
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        return '</'.$this->val().'>';
+    }
+
+}
diff --git a/inc/Form/TagElement.php b/inc/Form/TagElement.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea5144c9c8710dec92e32a02536afaee73032ccd
--- /dev/null
+++ b/inc/Form/TagElement.php
@@ -0,0 +1,29 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class TagElement
+ *
+ * Creates a self closing HTML tag
+ *
+ * @package dokuwiki\Form
+ */
+class TagElement extends ValueElement {
+
+    /**
+     * @param string $tag
+     * @param array $attributes
+     */
+    public function __construct($tag, $attributes = array()) {
+        parent::__construct('tag', $tag, $attributes);
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        return '<'.$this->val().' '.buildAttributes($this->attrs()).' />';
+    }
+}
diff --git a/inc/Form/TagOpenElement.php b/inc/Form/TagOpenElement.php
new file mode 100644
index 0000000000000000000000000000000000000000..0afe97b459a3fd85ddaea578a72e6237502e3aa2
--- /dev/null
+++ b/inc/Form/TagOpenElement.php
@@ -0,0 +1,30 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class TagOpenElement
+ *
+ * Creates an open HTML tag. You have to make sure you close it
+ * again or this will produce invalid HTML
+ *
+ * @package dokuwiki\Form
+ */
+class TagOpenElement extends ValueElement {
+
+    /**
+     * @param string $tag
+     * @param array $attributes
+     */
+    public function __construct($tag, $attributes = array()) {
+        parent::__construct('tagopen', $tag, $attributes);
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    public function toHTML() {
+        return '<'.$this->val().' '.buildAttributes($this->attrs()).'>';
+    }
+}
diff --git a/inc/Form/TextareaElement.php b/inc/Form/TextareaElement.php
new file mode 100644
index 0000000000000000000000000000000000000000..9d461fdf5b7de16db312f3d2eb95aca3044dbe93
--- /dev/null
+++ b/inc/Form/TextareaElement.php
@@ -0,0 +1,51 @@
+<?php
+namespace dokuwiki\Form;
+
+/**
+ * Class TextareaElement
+ * @package dokuwiki\Form
+ */
+class TextareaElement extends InputElement {
+
+    /**
+     * @var string the actual text within the area
+     */
+    protected $text;
+
+    /**
+     * @param string $name The name of this form element
+     * @param string $label The label text for this element
+     */
+    public function __construct($name, $label) {
+        parent::__construct('textarea', $name, $label);
+        $this->attr('dir', 'auto');
+    }
+
+    /**
+     * Get or set the element's value
+     *
+     * This is the preferred way of setting the element's value
+     *
+     * @param null|string $value
+     * @return string|$this
+     */
+    public function val($value = null) {
+        if($value !== null) {
+            $this->text = $value;
+            return $this;
+        }
+        return $this->text;
+    }
+
+    /**
+     * The HTML representation of this element
+     *
+     * @return string
+     */
+    protected function mainElementHTML() {
+        if($this->useInput) $this->prefillInput();
+        return '<textarea ' . buildAttributes($this->attrs()) . '>' .
+        formText($this->val()) . '</textarea>';
+    }
+
+}
diff --git a/inc/Form/ValueElement.php b/inc/Form/ValueElement.php
new file mode 100644
index 0000000000000000000000000000000000000000..9dc2fd0df6ad8f0817a428304af6cb2447f764ff
--- /dev/null
+++ b/inc/Form/ValueElement.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace dokuwiki\Form;
+
+/**
+ * Class ValueElement
+ *
+ * Just like an Element but it's value is not part of its attributes
+ *
+ * What the value is (tag name, content, etc) is defined by the actual implementations
+ *
+ * @package dokuwiki\Form
+ */
+abstract class ValueElement extends Element {
+
+    /**
+     * @var string holds the element's value
+     */
+    protected $value = '';
+
+    /**
+     * @param string $type
+     * @param array|string $value
+     * @param array $attributes
+     */
+    public function __construct($type, $value, $attributes = array()) {
+        parent::__construct($type, $attributes);
+        $this->val($value);
+    }
+
+    /**
+     * Get or set the element's value
+     *
+     * @param null|string $value
+     * @return string|$this
+     */
+    public function val($value = null) {
+        if($value !== null) {
+            $this->value = $value;
+            return $this;
+        }
+        return $this->value;
+    }
+
+}
diff --git a/inc/load.php b/inc/load.php
index b78a9ee38a71759c1193585ea6101043ce13f976..42a6a6362f7aa378d639d57ea1839324b30e7059 100644
--- a/inc/load.php
+++ b/inc/load.php
@@ -111,6 +111,12 @@ function load_autoload($name){
         return;
     }
 
+    // our own namespace
+    $name = str_replace('\\', '/', $name);
+    if(substr($name, 0, 9) == 'dokuwiki/') {
+        require_once(substr($name, 9) . '.php');
+    }
+
     // Plugin loading
     if(preg_match('/^(auth|helper|syntax|action|admin|renderer|remote)_plugin_('.DOKU_PLUGIN_NAME_REGEX.')(?:_([^_]+))?$/',
                   $name, $m)) {