Index: lib/pagelib.php
===================================================================
RCS file: /cvsroot/moodle/moodle/lib/pagelib.php,v
retrieving revision 1.121
diff -u -r1.121 pagelib.php
--- lib/pagelib.php	15 Jun 2009 04:03:59 -0000	1.121
+++ lib/pagelib.php	17 Jun 2009 11:49:15 -0000
@@ -40,6 +40,7 @@
  * @since Moodle 2.0
  *
  * @property-read page_requirements_manager $requires Tracks resources (for example required .css and .js files) required by this page.
+ * @property-read xhtml_container_stack $opencontainers Tracks open XHTML tags. Helps us generate well-formed XML, even in the face of errors.
  */
 class moodle_page {
     /**#@+ Tracks the where we are in the generation of the page. */
@@ -106,6 +107,8 @@
 
     protected $_othereditingcaps = array();
 
+    protected $_opencontainers = null;
+
     /**
      * This is simply to improve backwards compatability. If old code relies on
      * a page class that implements print_header, or complex logic in
@@ -297,8 +300,8 @@
     }
 
     /**
-     * Please do not call this method directly, use the ->blocks syntax. @see __get().
-     * @return blocks_manager the blocks manager object for this page.
+     * Please do not call this method directly, use the ->requires syntax. @see __get().
+     * @return page_requirements_manager tracks the JavaScript, CSS files, etc. required by this page.
      */
     public function get_requires() {
         global $CFG;
@@ -309,6 +312,18 @@
     }
 
     /**
+     * Please do not call this method directly, use the ->opencontainers syntax. @see __get().
+     * @return xhtml_container_stack Tracks the open XHTML tags on this page.
+     */
+    public function get_opencontainers() {
+        global $CFG;
+        if (is_null($this->_opencontainers)) {
+            $this->_opencontainers = new xhtml_container_stack();
+        }
+        return $this->_opencontainers;
+    }
+
+    /**
      * PHP overloading magic to make the $PAGE->course syntax work by redirecting
      * it to the corresponding $PAGE->get_course() method if there is one, and
      * throwing an exception if not.
Index: lib/outputlib.php
===================================================================
RCS file: lib/outputlib.php
diff -N lib/outputlib.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ lib/outputlib.php	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,584 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+
+/**
+ * Functions for generating the HTML that Moodle should output.
+ *
+ * Please see http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML
+ * for an overview.
+ *
+ * @package   moodlecore
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later (5)
+ */
+
+
+/**
+ * A renderer factory is just responsible for creating an appropriate renderer
+ * for any given part of Moodle.
+ *
+ * Which renderer factory to use is chose by the current theme, and an instance
+ * if created automatically when the theme is set up.
+ *
+ * A renderer factory must also have a constructor that takes a theme object and
+ * a moodle_page object. (See {@link renderer_factory_base::__construct} for an example.)
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since     Moodle 2.0
+ */
+interface renderer_factory {
+    /**
+     * Return the renderer for a particular part of Moodle.
+     *
+     * The renderer interfaces are defined by classes called moodle_..._renderer
+     * where ... is the name of the module, which, will be defined in this file
+     * for core parts of Moodle, and in a file called renderer.php for plugins.
+     *
+     * There is no separate interface definintion for renderers. Instead we
+     * take advantage of PHP being a dynamic languages. The renderer returned
+     * does not need to be a subclass of the moodle_..._renderer base class, it
+     * just needs to impmenent the same interface. This is sometimes called
+     * 'Duck typing'. For a tricky example, see {@link template_renderer} below.
+     * renderer ob
+     *
+     * @param $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
+     * @return object an object implementing the requested renderer interface.
+     */
+    public function get_renderer($module);
+}
+
+
+/**
+ * This is a base class to help you implement the renderer_factory interface.
+ *
+ * It keeps a cache of renderers that have been constructed, so you only need
+ * to construct each one once in you subclass.
+ *
+ * It also has a method to get the name of, and include the renderer.php with
+ * the definition of, the standard renderer class for a given module.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since     Moodle 2.0
+ */
+abstract class renderer_factory_base implements renderer_factory {
+    /** The theme we are rendering for. */
+    protected $theme;
+
+    /** The page we are doing output for. */
+    protected $page;
+
+    /** Used to cache renderers as they are created. */
+    protected $renderers = array();
+
+    /**
+     * Constructor.
+     * @param object $theme the theme we are rendering for.
+     * @param moodle_page $page the page we are doing output for.
+     */
+    public function __construct($theme, $page) {
+        $this->theme = $theme;
+        $this->page = $page;
+    }
+
+    /* Implement the interface method. */
+    public function get_renderer($module) {
+        // Cache the renderers by module name, and delegate the actual
+        // construction to the create_renderer method.
+        if (!array_key_exists($module, $this->renderers)) {
+            $this->renderers[$module] = $this->create_renderer($module);
+        }
+
+        return $this->renderers[$module];
+    }
+
+    /**
+     * Subclasses should override this method to actually create an instance of
+     * the appropriate renderer class, based on the module name. That is,
+     * this method should implement the same contract as
+     * {@link renderer_factory::get_renderer}.
+     *
+     * @param $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
+     * @return object an object implementing the requested renderer interface.
+     */
+    abstract public function create_renderer($module);
+
+    /**
+     * For a given module name, return the name of the standard renderer class
+     * that defines the renderer interface for that module.
+     *
+     * Also, if it exists, include the renderer.php file for that module, so
+     * the class definition of the default renderer has been loaded.
+     *
+     * @param string $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
+     * @return string the name of the standard renderer class for that module.
+     */
+    protected function standard_renderer_class_for_module($module) {
+        $pluginrenderer = get_plugin_dir($module) . '/renderer.php';
+        if (file_exists($pluginrenderer)) {
+            include_once($pluginrenderer);
+        }
+        $class = 'moodle_' . $module . '_renderer';
+        if (!class_exists($class)) {
+            throw new coding_exception('Request for an unknown renderer class ' . $class);
+        }
+        return $class;
+    }
+}
+
+
+/**
+ * This is the default renderer factory for Moodle. It simply returns an instance
+ * of the appropriate standard renderer class.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since     Moodle 2.0
+ */
+class standard_renderer_factory extends renderer_factory_base {
+    /**
+     * Constructor.
+     * @param object $theme the theme we are rendering for.
+     * @param moodle_page $page the page we are doing output for.
+     */
+    public function __construct($theme, $page) {
+        parent::__construct($theme, $page);
+        $this->renderers = array('core' => new moodle_core_renderer($this->page->opencontainers));
+    }
+
+    /* Implement the subclass method. */
+    public function create_renderer($module) {
+        $class = $this->standard_renderer_class_for_module($module);
+        return new $class($this->page->opencontainers, $this->renderers['core']);
+    }
+}
+
+
+/**
+ * This is a slight variatoin on the standard_renderer_factory that uses
+ * custom_corners_core_renderer instead of moodle_core_renderer.
+ *
+ * This generates the slightly different HTML that the custom_corners theme expects.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since     Moodle 2.0
+ */
+class custom_corners_renderer_factory extends standard_renderer_factory {
+    /**
+     * Constructor.
+     * @param object $theme the theme we are rendering for.
+     * @param moodle_page $page the page we are doing output for.
+     */
+    public function __construct($theme, $page) {
+        parent::__construct($theme, $page);
+        $this->renderers = array('core' => new custom_corners_core_renderer($this->page->opencontainers));
+    }
+}
+
+
+/**
+ * This is renderer factory that allows you to create templated themes.
+ *
+ * This should be considered an experimental proof of concept. In particular,
+ * the performance is probably not very good. Do not try to use in on a busy site
+ * without doing careful load testing first!
+ *
+ * This renderer factory returns instances of {@link template_renderer} class
+ * which which implement the corresponding renderer interface in terms of
+ * templates. To use this your theme must have a templates folder inside it.
+ * Then suppose the method moodle_core_renderer::greeting($name = 'world');
+ * exists. Then, a call to $OUTPUT->greeting() will cause the template
+ * /theme/yourtheme/templates/core/greeting.php to be rendered, with the variable
+ * $name available. The greeting.php template might contain
+ * 
+ * <pre>
+ * <h1>Hello <?php echo $name ?>!</h1>
+ * </pre>
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since     Moodle 2.0
+ */
+class template_renderer_factory extends renderer_factory_base {
+    /**
+     * An array of paths of where to search for templates. Normally this theme,
+     * the parent theme then the standardtemplate theme. (If some of these do
+     * not exist, or are the same as each other, then the list will be shorter.
+     */
+    protected $searchpaths = array();
+
+    /**
+     * Constructor.
+     * @param object $theme the theme we are rendering for.
+     * @param moodle_page $page the page we are doing output for.
+     */
+    public function __construct($theme, $page) {
+        global $CFG;
+        parent::__construct($theme, $page);
+
+        // Initialise $this->searchpaths.
+        if ($theme->name != 'standardtemplate') {
+            $templatesdir = $theme->dir . '/templates';
+            if (is_dir($templatesdir)) {
+                $this->searchpaths[] = $templatesdir;
+            }
+        }
+        if (!empty($theme->parent)) {
+            $templatesdir = $CFG->themedir .'/'. $theme->parent . '/templates';
+            if (is_dir($templatesdir)) {
+                $this->searchpaths[] = $templatesdir;
+            }
+        }
+        $this->searchpaths[] = $CFG->themedir .'/standardtemplate/templates';
+    }
+
+    /* Implement the subclass method. */
+    public function create_renderer($module) {
+        // Refine the list of search paths for this module.
+        $searchpaths = array();
+        foreach ($this->searchpaths as $rootpath) {
+            $path = $rootpath . '/' . $module;
+            if (is_dir($path)) {
+                $searchpaths[] = $path;
+            }
+        }
+
+        // Create a template_renderer that copies the API of the standard renderer.
+        $copiedclass = $this->standard_renderer_class_for_module($module);
+        return new template_renderer($copiedclass, $searchpaths, $this->page->opencontainers);
+    }
+}
+
+
+/**
+ * Simple base class for Moodle renderers.
+ *
+ * Tracks the xhtml_container_stack to use, which is passed in in the constructor.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since     Moodle 2.0
+ */
+class moodle_renderer_base {
+    /** @var xhtml_container_stack the xhtml_container_stack to use. */
+    protected $containerstack;
+
+    /**
+     * Constructor
+     * @param $containerstack the xhtml_container_stack to use. 
+     */
+    public function __construct($containerstack) {
+        $this->containerstack = $containerstack;
+    }
+}
+
+
+/**
+ * This is the templated renderer which copies the API of another class, replacing
+ * all methods calls with instantiation of a template.
+ *
+ * When the method method_name is called, this class will search for a template
+ * called method_name.php in the folders in $searchpaths, taking the frist one
+ * that it finds. Then it will set up variables for each of the arguments of that
+ * method, and render the template. This is implemented in the {@link __call()}
+ * PHP magic method.
+ *
+ * Methods like print_box_start and print_box_end are handles specially, and
+ * implemented in terms of the print_box.php method.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since     Moodle 2.0
+ */
+class template_renderer extends moodle_renderer_base {
+    /** @var ReflectionClass information about the class whose API we are copying. */
+    protected $copiedclass;
+    /** @var array of places to search for templates. */
+    protected $searchpaths;
+
+    /**
+     * Magic word used when breaking apart container templates to implement
+     * _start and _end methods.
+     */
+    const contentstoken = '-@#-Contents-go-here-#@-';
+
+    /**
+     * Constructor
+     * @param string $copiedclass the name of a class whose API we should be copying.
+     * @param $searchpaths a list of folders to search for templates in.
+     * @param $containerstack the xhtml_container_stack to use.
+     */
+    public function __construct($copiedclass, $searchpaths, $containerstack) {
+        parent::__construct($containerstack);
+        $this->copiedclass = new ReflectionClass($copiedclass);
+        $this->searchpaths = $searchpaths;
+    }
+
+    /* PHP magic method implementation. */
+    public function __call($method, $arguments) {
+        if (substr($method, -6) == '_start') {
+            return $this->process_start(substr($method, 0, -6), $arguments);
+        } else if (substr($method, -4) == '_end') {
+            return $this->process_end(substr($method, 0, -4), $arguments);
+        } else {
+            return $this->process_template($method, $arguments);
+        }
+    }
+
+    /**
+     * Render the template for a gives method of the standard renderer class,
+     * using the arguments passed.
+     * @param string $method the method that was called.
+     * @param array $arguments the arguments that were passed to it.
+     * @return string the HTML to be output.
+     */
+    protected function process_template($method, $arguments) {
+        if (!$this->copiedclass->hasMethod($method) ||
+                !$this->copiedclass->getMethod($method)->isPublic()) {
+            throw new coding_exception('Unknown method ' . $method);
+        }
+
+        // Find the template file for this method.
+        $template = $this->find_template($method);
+
+        // Use the reflection API to find out what variable names the arguments
+        // should be stored in, and fill in any missing ones with the defaults.
+        $namedarguments = array();
+        $expectedparams = $this->copiedclass->getMethod($method)->getParameters();
+        foreach ($expectedparams as $param) {
+            $paramname = $param->getName();
+            if (!empty($arguments)) {
+                $namedarguments[$paramname] = array_shift($arguments);
+            } else if ($param->isDefaultValueAvailable()) {
+                $namedarguments[$paramname] = $param->getDefaultValue();
+            } else {
+                throw new coding_exception('Missing required argument ' . $paramname);
+            }
+        }
+
+        // Actually render the template.
+        return $this->render_template($template, $namedarguments);
+    }
+
+    /**
+     * Actually do the work of rendering the template.
+     * @param $_template the full path to the template file.
+     * @param $_namedarguments an array variable name => value, the variables
+     *      that should be available to the template.
+     * @return string the HTML to be output.
+     */
+    protected function render_template($_template, $_namedarguments) {
+        // Note, we intentionally break the coding guidelines with regards to
+        // local variable names used in this function, so that they do not clash
+        // with the names of any variables being passed to the template.
+
+        // Set up the global variables that the template may wish to access.
+        global $CFG, $PAGE, $THEME;
+
+        // And the parameters from the function call.
+        extract($_namedarguments);
+
+        // Include the template, capturing the output.
+        ob_start();
+        include($_template);
+        $_result = ob_get_contents();
+        ob_end_clean();
+
+        return $_result;
+    }
+
+    /**
+     * Searches the folders in {@link $searchpaths} to try to find a template for
+     * this method name. Throws an exception if one cannot be found.
+     * @param string $method the method name.
+     * @return string the full path of the template to use.
+     */
+    protected function find_template($method) {
+        foreach ($this->searchpaths as $path) {
+            $filename = $path . '/' . $method . '.php';
+            if (file_exists($filename)) {
+                return $filename;
+            }
+        }
+        throw new coding_exception('Cannot find template for ' . $this->copiedclass->getName() . '::' . $method);
+    }
+
+    /**
+     * Handle methods like print_box_start by using the print_box template,
+     * splitting the result, pusing the end onto the stack, then returning the start.
+     * @param string $method the method that was called, with _start stripped off.
+     * @param array $arguments the arguments that were passed to it.
+     * @return string the HTML to be output.
+     */
+    protected function process_start($template, $arguments) {
+        array_unshift($arguments, self::contentstoken);
+        $html = $this->process_template($template, $arguments);
+        list($start, $end) = explode(self::contentstoken, $html, 2);
+        $this->containerstack->push($template, $end);
+        return $start;
+    }
+
+    /**
+     * Handle methods like print_box_end, we just need to pop the end HTML from
+     * the stack.
+     * @param string $method the method that was called, with _end stripped off.
+     * @param array $arguments not used. Assumed to be irrelevant.
+     * @return string the HTML to be output.
+     */
+    protected function process_end($template, $arguments) {
+        return $this->containerstack->pop($template);
+    }
+
+    /**
+     * @return array the list of paths where this class searches for templates.
+     */
+    public function get_search_paths() {
+        return $this->searchpaths;
+    }
+
+    /**
+     * @return string the name of the class whose API we are copying.
+     */
+    public function get_copied_class() {
+        return $this->copiedclass->getName();
+    }
+}
+
+
+/**
+ * This class keeps track of which HTML tags are currently open.
+ *
+ * This makes it much easier to always generate well formed XHTML output, even
+ * if execution terminates abruptly. Any time you output some opening HTML
+ * without the matching closing HTML, you should push the neccessary close tags
+ * onto the stack.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since     Moodle 2.0
+ */
+class xhtml_container_stack {
+    /** @var array stores the list of open containers. */
+    protected $opencontainsers = array();
+
+    /**
+     * Push the close HTML for a recently opened container onto the stack.
+     * @param string $type The type of container. This is checked when {@link pop()}
+     *      is called and must match, otherwise a developer debug warning is output.
+     * @param string $closehtml The HTML required to close the container.
+     */
+    public function push($type, $closehtml) {
+        $container = new stdClass;
+        $container->type = $type;
+        $container->closehtml = $closehtml;
+        array_push($this->opencontainsers, $container);
+    }
+
+    /**
+     * Pop the HTML for the next closing container from the stack. The $type
+     * must match the type passed when the container was opened, otherwise a
+     * warning will be output.
+     * @param string $type The type of container.
+     * @return string the HTML requried to close the container.
+     */
+    public function pop($type) {
+        if (empty($this->opencontainsers)) {
+            debugging('There are no more open containers. This suggests there is a nesting problem.', DEBUG_DEVELOPER);
+            return;
+        }
+
+        $container = array_pop($this->opencontainsers);
+        if ($container->type != $type) {
+            debugging('The type of container to be closed (' . $container->type .
+                    ') does not match the type of the next open container (' . $type .
+                    '). This suggests there is a nesting problem.', DEBUG_DEVELOPER);
+        }
+        return $container->closehtml;
+    }
+
+    /**
+     * Close all but the last open container. This is useful in places like error
+     * handling, where you want to close all the open containers (apart from <body>)
+     * before outputting the error message.
+     * @return string the HTML requried to close any open containers inside <body>.
+     */
+    public function pop_all_but_last() {
+        $output = '';
+        while (count($this->opencontainsers) > 1) {
+            $container = array_pop($this->opencontainsers);
+            $output .= $container->closehtml;
+        }
+        return $output;
+    }
+
+    /**
+     * You can call this function if you want to throw away an instance of this
+     * class without properly emptying the stack (for example, in a unit test).
+     * Calling this method stops the destruct method from outputting a developer
+     * debug warning. After calling this method, the instance can no longer be used.
+     */
+    public function discard() {
+        $this->opencontainsers = null;
+    }
+
+    /**
+     * Emergency fallback. If we get to the end of processing and not all
+     * containers have been closed, output the rest with a developer debug warning.
+     */
+    public function __destruct() {
+        if (empty($this->opencontainsers)) {
+            return;
+        }
+
+        debugging('Some containers were left open. This suggests there is a nesting problem.', DEBUG_DEVELOPER);
+        echo $this->pop_all_but_last();
+        $container = array_pop($this->opencontainsers);
+        echo $container->closehtml;
+    }
+}
+
+
+/**
+ * The standard implementation of the moodle_core_renderer interface.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since     Moodle 2.0
+ */
+class moodle_core_renderer extends moodle_renderer_base {
+
+    // TODO
+}
+
+
+/**
+ * A renderer for the custom corner theme, and other themes based on it.
+ *
+ * Generates the slightly different HTML that the custom corners theme wants.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since     Moodle 2.0
+ */
+class custom_corners_core_renderer extends moodle_core_renderer {
+
+    // TODO
+}
+
Index: lib/simpletest/testoutputlib.php
===================================================================
RCS file: lib/simpletest/testoutputlib.php
diff -N lib/simpletest/testoutputlib.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ lib/simpletest/testoutputlib.php	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,607 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+
+/**
+ * Unit tests for (some of) ../outputlib.php.
+ *
+ * @package   moodlecore
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later (5)
+ */
+
+if (!defined('MOODLE_INTERNAL')) {
+    die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
+}
+require_once($CFG->libdir . '/outputlib.php');
+
+
+// TODO this is needed until MDL-16438 is committed.
+function get_plugin_dir($module) {
+    global $CFG;
+    return $CFG->dirroot;
+}
+
+
+/**
+ * Subclass of renderer_factory_base for testing. Implement abstract method and
+ * count calls, so we can test caching behaviour.
+ */
+class testable_renderer_factory extends renderer_factory_base {
+    public $createcalls = array();
+
+    public function __construct() {
+        parent::__construct(null, null);
+    }
+
+    public function create_renderer($module) {
+        $this->createcalls[] = $module;
+        return new moodle_core_renderer(new xhtml_container_stack());
+    }
+
+    public function standard_renderer_class_for_module($module) {
+        return parent::standard_renderer_class_for_module($module);
+    }
+}
+
+
+/**
+ * Renderer class for testing.
+ */
+class moodle_test_renderer extends moodle_core_renderer {
+    public function __construct($containerstack) {
+        parent::__construct($containerstack);
+    }
+
+    public function greeting($name = 'world') {
+        return '<h1>Hello ' . $name . '!</h1>';
+    }
+
+    public function box($content, $id = '') {
+        return box_start($id) . $content . box_end();
+    }
+
+    public function box_start($id = '') {
+        if ($id) {
+            $id = ' id="' . $id . '"';
+        }
+        $this->containerstack->push('box', '</div>');
+        return '<div' . $id . '>';
+    }
+
+    public function box_end() {
+        return $this->containerstack->pop('box');
+    }
+}
+
+
+/**
+ * Unit tests for the requriement_base base class.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer_factory_base_test extends UnitTestCase {
+    public function test_get_calls_create() {
+        // Set up.
+        $factory = new testable_renderer_factory();
+        // Exercise SUT.
+        $renderer = $factory->get_renderer('modulename');
+        // Verify outcome
+        $this->assertEqual(array('modulename'), $factory->createcalls);
+    }
+
+    public function test_get_caches_repeat_calls() {
+        // Set up.
+        $factory = new testable_renderer_factory();
+        // Exercise SUT.
+        $renderer1 = $factory->get_renderer('modulename');
+        $renderer2 = $factory->get_renderer('modulename');
+        // Verify outcome
+        $this->assertEqual(array('modulename'), $factory->createcalls);
+        $this->assertIdentical($renderer1, $renderer2);
+    }
+
+    public function test_standard_renderer_class_for_module_core() {
+        // Set up.
+        $factory = new testable_renderer_factory();
+        // Exercise SUT.
+        $classname = $factory->standard_renderer_class_for_module('core');
+        // Verify outcome
+        $this->assertEqual('moodle_core_renderer', $classname);
+    }
+
+    public function test_standard_renderer_class_for_module_test() {
+        // Set up.
+        $factory = new testable_renderer_factory();
+        // Exercise SUT.
+        $classname = $factory->standard_renderer_class_for_module('test');
+        // Verify outcome
+        $this->assertEqual('moodle_test_renderer', $classname);
+    }
+
+    public function test_standard_renderer_class_for_module_unknown() {
+        // Set up.
+        $factory = new testable_renderer_factory();
+        $this->expectException();
+        // Exercise SUT.
+        $classname = $factory->standard_renderer_class_for_module('something_that_does_not_exist');
+    }
+}
+
+
+/**
+ * Unit tests for the standard_renderer_factory class.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class standard_renderer_factory_test extends UnitTestCase {
+    protected $factory;
+
+    public function setUp() {
+        parent::setUp();
+        $page = new stdClass;
+        $page->opencontainers = new xhtml_container_stack();
+        $this->factory = new standard_renderer_factory(null, $page);
+    }
+
+    public function tearDown() {
+        $this->factory = null;
+        parent::tearDown();
+    }
+
+    public function test_get_core_renderer() {
+        $renderer = $this->factory->get_renderer('core');
+        $this->assertIsA($renderer, 'moodle_core_renderer');
+    }
+
+    public function test_get_test_renderer() {
+        $renderer = $this->factory->get_renderer('test');
+        $this->assertIsA($renderer, 'moodle_test_renderer');
+    }
+}
+
+
+/**
+ * Unit tests for the custom_corners_renderer_factory class.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class custom_corners_renderer_factory_test extends UnitTestCase {
+    protected $factory;
+
+    public function setUp() {
+        parent::setUp();
+        $page = new stdClass;
+        $page->opencontainers = new xhtml_container_stack();
+        $this->factory = new custom_corners_renderer_factory(null, $page);
+    }
+
+    public function tearDown() {
+        $this->factory = null;
+        parent::tearDown();
+    }
+
+    public function test_get_core_renderer() {
+        $renderer = $this->factory->get_renderer('core');
+        $this->assertIsA($renderer, 'custom_corners_core_renderer');
+    }
+
+    public function test_get_test_renderer() {
+        $renderer = $this->factory->get_renderer('test');
+        $this->assertIsA($renderer, 'moodle_test_renderer');
+    }
+}
+
+
+/**
+ * Test-specific subclass that implements a getter for $searchpaths.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_template_renderer_factory extends template_renderer_factory {
+    public function get_search_paths() {
+        return $this->searchpaths;
+    }
+}
+
+
+/**
+ * Unit tests for the template_renderer_factory class.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_renderer_factory_test extends UnitTestCase {
+    protected $originalcfgthemedir;
+    protected $workspace;
+    protected $page;
+    protected $foldertocleanup = null;
+
+    public function setUp() {
+        global $CFG;
+        parent::setUp();
+        $this->originalcfgthemedir = $CFG->themedir;
+
+        $this->workspace = 'temp/template_renderer_factory_fixtures';
+        make_upload_directory($this->workspace);
+        $CFG->themedir = $CFG->dataroot . '/' . $this->workspace;
+        $this->foldertocleanup = $CFG->themedir;
+
+        $this->page = new stdClass;
+        $this->page->opencontainers = new xhtml_container_stack();
+    }
+
+    public function tearDown() {
+        global $CFG;
+        if (!empty($this->foldertocleanup)) {
+            fulldelete($this->foldertocleanup);
+            $this->foldertocleanup = null;
+        }
+        $CFG->themedir = $this->originalcfgthemedir;
+        parent::tearDown();
+    }
+
+    protected function make_theme($name) {
+        global $CFG;
+        $theme = new stdClass;
+        $theme->name = $name;
+        $theme->dir = $CFG->themedir . '/' . $name;
+        make_upload_directory($this->workspace . '/' . $name);
+        return $theme;
+    }
+
+    protected function make_theme_template_dir($name, $module = '') {
+        $path = $this->workspace . '/' . $name . '/templates';
+        if ($module) {
+            $path .= '/' . $module;
+        }
+        make_upload_directory($path);
+    }
+
+    public function test_constructor_standardtemplate() {
+        global $CFG;
+        // Set up.
+        $theme = $this->make_theme('standardtemplate');
+        $this->make_theme_template_dir('standardtemplate');
+
+        // Exercise SUT.
+        $factory = new testable_template_renderer_factory($theme, $this->page);
+
+        // Verify outcome
+        $this->assertEqual(array($CFG->themedir . '/standardtemplate/templates'),
+                $factory->get_search_paths());
+    }
+
+    public function test_constructor_mytheme() {
+        global $CFG;
+        // Set up.
+        $theme = $this->make_theme('mytheme');
+        $this->make_theme_template_dir('mytheme');
+        $this->make_theme_template_dir('standardtemplate');
+
+        // Exercise SUT.
+        $factory = new testable_template_renderer_factory($theme, $this->page);
+
+        // Verify outcome
+        $this->assertEqual(array(
+                $CFG->themedir . '/mytheme/templates',
+                $CFG->themedir . '/standardtemplate/templates'),
+                $factory->get_search_paths());
+    }
+
+    public function test_constructor_mytheme_no_templates() {
+        global $CFG;
+        // Set up.
+        $theme = $this->make_theme('mytheme');
+        $this->make_theme_template_dir('standardtemplate');
+
+        // Exercise SUT.
+        $factory = new testable_template_renderer_factory($theme, $this->page);
+
+        // Verify outcome
+        $this->assertEqual(array($CFG->themedir . '/standardtemplate/templates'),
+                $factory->get_search_paths());
+    }
+
+    public function test_constructor_mytheme_with_parent() {
+        global $CFG;
+        // Set up.
+        $theme = $this->make_theme('mytheme');
+        $theme->parent = 'parenttheme';
+        $this->make_theme_template_dir('mytheme');
+        $this->make_theme_template_dir('parenttheme');
+        $this->make_theme_template_dir('standardtemplate');
+                
+        // Exercise SUT.
+        $factory = new testable_template_renderer_factory($theme, $this->page);
+
+        // Verify outcome
+        $this->assertEqual(array(
+                $CFG->themedir . '/mytheme/templates',
+                $CFG->themedir . '/parenttheme/templates',
+                $CFG->themedir . '/standardtemplate/templates'),
+                $factory->get_search_paths());
+    }
+
+    public function test_constructor_mytheme_with_parent_no_templates() {
+        global $CFG;
+        // Set up.
+        $theme = $this->make_theme('mytheme');
+        $theme->parent = 'parenttheme';
+        $this->make_theme_template_dir('mytheme');
+        $this->make_theme_template_dir('standardtemplate');
+                
+        // Exercise SUT.
+        $factory = new testable_template_renderer_factory($theme, $this->page);
+
+        // Verify outcome
+        $this->assertEqual(array(
+                $CFG->themedir . '/mytheme/templates',
+                $CFG->themedir . '/standardtemplate/templates'),
+                $factory->get_search_paths());
+    }
+
+    public function test_get_renderer() {
+        global $CFG;
+        // Set up.
+        $theme = $this->make_theme('mytheme');
+        $theme->parent = 'parenttheme';
+        $this->make_theme_template_dir('mytheme', 'core');
+        $this->make_theme_template_dir('parenttheme', 'test');
+        $this->make_theme_template_dir('standardtemplate', 'test');
+        $factory = new testable_template_renderer_factory($theme, $this->page);
+
+        // Exercise SUT.
+        $renderer = $factory->get_renderer('test');
+
+        // Verify outcome
+        $this->assertEqual('moodle_test_renderer', $renderer->get_copied_class());
+        $this->assertEqual(array(
+                $CFG->themedir . '/parenttheme/templates/test',
+                $CFG->themedir . '/standardtemplate/templates/test'),
+                $renderer->get_search_paths());
+    }
+}
+
+
+/**
+ * Unit tests for the xhtml_container_stack class.
+ *
+ * These tests assume that developer debug mode is on, which, at the time of
+ * writing, is true. admin/report/unittest/index.php forces it on.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class xhtml_container_stack_test extends UnitTestCase {
+    protected function start_capture() {
+        ob_start();
+    }
+
+    protected function end_capture() {
+        $result = ob_get_contents();
+        ob_end_clean();
+        return $result;
+    }
+
+    public function test_push_then_pop() {
+        // Set up.
+        $stack = new xhtml_container_stack();
+        // Exercise SUT.
+        $this->start_capture();
+        $stack->push('testtype', '</div>');
+        $html = $stack->pop('testtype');
+        $errors = $this->end_capture();
+        // Verify outcome
+        $this->assertEqual('</div>', $html);
+        $this->assertEqual('', $errors);
+    }
+
+    public function test_mismatched_pop_prints_warning() {
+        // Set up.
+        $stack = new xhtml_container_stack();
+        $stack->push('testtype', '</div>');
+        // Exercise SUT.
+        $this->start_capture();
+        $html = $stack->pop('mismatch');
+        $errors = $this->end_capture();
+        // Verify outcome
+        $this->assertEqual('</div>', $html);
+        $this->assertNotEqual('', $errors);
+    }
+
+    public function test_pop_when_empty_prints_warning() {
+        // Set up.
+        $stack = new xhtml_container_stack();
+        // Exercise SUT.
+        $this->start_capture();
+        $html = $stack->pop('testtype');
+        $errors = $this->end_capture();
+        // Verify outcome
+        $this->assertEqual('', $html);
+        $this->assertNotEqual('', $errors);
+    }
+
+    public function test_correct_nesting() {
+        // Set up.
+        $stack = new xhtml_container_stack();
+        // Exercise SUT.
+        $this->start_capture();
+        $stack->push('testdiv', '</div>');
+        $stack->push('testp', '</p>');
+        $html2 = $stack->pop('testp');
+        $html1 = $stack->pop('testdiv');
+        $errors = $this->end_capture();
+        // Verify outcome
+        $this->assertEqual('</p>', $html2);
+        $this->assertEqual('</div>', $html1);
+        $this->assertEqual('', $errors);
+    }
+
+    public function test_pop_all_but_last() {
+        // Set up.
+        $stack = new xhtml_container_stack();
+        $stack->push('test1', '</h1>');
+        $stack->push('test2', '</h2>');
+        $stack->push('test3', '</h3>');
+        // Exercise SUT.
+        $this->start_capture();
+        $html = $stack->pop_all_but_last();
+        $errors = $this->end_capture();
+        // Verify outcome
+        $this->assertEqual('</h3></h2>', $html);
+        $this->assertEqual('', $errors);
+        // Tear down.
+        $stack->discard();
+    }
+
+    public function test_pop_all_but_last_only_one() {
+        // Set up.
+        $stack = new xhtml_container_stack();
+        $stack->push('test1', '</h1>');
+        // Exercise SUT.
+        $this->start_capture();
+        $html = $stack->pop_all_but_last();
+        $errors = $this->end_capture();
+        // Verify outcome
+        $this->assertEqual('', $html);
+        $this->assertEqual('', $errors);
+        // Tear down.
+        $stack->discard();
+    }
+
+    public function test_pop_all_but_last_empty() {
+        // Set up.
+        $stack = new xhtml_container_stack();
+        // Exercise SUT.
+        $this->start_capture();
+        $html = $stack->pop_all_but_last();
+        $errors = $this->end_capture();
+        // Verify outcome
+        $this->assertEqual('', $html);
+        $this->assertEqual('', $errors);
+    }
+
+    public function test_destruct() {
+        // Set up.
+        $stack = new xhtml_container_stack();
+        $stack->push('test1', '</somethingdistinctive>');
+        // Exercise SUT.
+        $this->start_capture();
+        $stack = null;
+        $errors = $this->end_capture();
+        // Verify outcome
+        $this->assertPattern('/<\/somethingdistinctive>/', $errors);
+    }
+
+    public function test_destruct_empty() {
+        // Set up.
+        $stack = new xhtml_container_stack();
+        // Exercise SUT.
+        $this->start_capture();
+        $stack = null;
+        $errors = $this->end_capture();
+        // Verify outcome
+        $this->assertEqual('', $errors);
+    }
+
+    public function test_discard() {
+        // Set up.
+        $stack = new xhtml_container_stack();
+        $stack->push('test1', '</somethingdistinctive>');
+        $stack->discard();
+        // Exercise SUT.
+        $this->start_capture();
+        $stack = null;
+        $errors = $this->end_capture();
+        // Verify outcome
+        $this->assertEqual('', $errors);
+    }
+}
+
+
+/**
+ * Unit tests for the template_renderer class.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class template_renderer_test extends UnitTestCase {
+    protected $renderer;
+    protected $templatefolder;
+    protected $savedtemplates;
+
+    public function setUp() {
+        global $CFG;
+        parent::setUp();
+        $this->templatefolder = $CFG->dataroot . '/temp/template_renderer_fixtures/test';
+        make_upload_directory('temp/template_renderer_fixtures/test');
+        $this->renderer = new template_renderer('moodle_test_renderer',
+                array($this->templatefolder), new xhtml_container_stack());
+        $this->savedtemplates = array();
+    }
+
+    public function tearDown() {
+        $this->renderer = null;
+        foreach ($this->savedtemplates as $template) {
+            unlink($template);
+        }
+        $this->savedtemplates = array();
+        parent::tearDown();
+    }
+
+    protected function save_template($name, $contents) {
+        $filename = $this->templatefolder . '/' . $name . '.php';
+        $this->savedtemplates[] = $filename;
+        file_put_contents($filename, $contents);
+    }
+
+    public function test_simple_template() {
+        $this->save_template('greeting', '<p>Hello <?php echo $name ?>!</p>');
+
+        $html = $this->renderer->greeting('Moodle');
+        $this->assertEqual('<p>Hello Moodle!</p>', $html);
+    }
+
+    public function test_simple_template_default_argument_value() {
+        $this->save_template('greeting', '<p>Hello <?php echo $name ?>!</p>');
+
+        $html = $this->renderer->greeting();
+        $this->assertEqual('<p>Hello world!</p>', $html);
+    }
+
+    public function test_box_template() {
+        $this->save_template('box', '<div class="box"<?php echo $id ?>><?php echo $content ?></div>');
+
+        $html = $this->renderer->box('This is a message in a box', 'messagediv');
+        $this->assertEqual('<div class="box"messagediv>This is a message in a box</div>', $html);
+    }
+
+    public function test_box_start_end_templates() {
+        $this->save_template('box', '<div class="box"<?php echo $id ?>><?php echo $content ?></div>');
+
+        $html = $this->renderer->box_start('messagediv');
+        $this->assertEqual('<div class="box"messagediv>', $html);
+
+        $html = $this->renderer->box_end();
+        $this->assertEqual('</div>', $html);
+    }
+}
