diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php
new file mode 100644
index 0000000000..b3b2a5ae42
--- /dev/null
+++ b/lib/htmloutputter.php
@@ -0,0 +1,353 @@
+.
+ *
+ * @category Output
+ * @package Laconica
+ * @author Evan Prodromou
+ * @author Sarven Capadisli
+ * @copyright 2008 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/lib/xmloutputter.php';
+
+define('PAGE_TYPE_PREFS',
+ 'text/html,application/xhtml+xml,'.
+ 'application/xml;q=0.3,text/xml;q=0.2');
+
+/**
+ * Low-level generator for HTML
+ *
+ * Abstracts some of the code necessary for HTML generation. Especially
+ * has methods for generating HTML form elements. Note that these have
+ * been created kind of haphazardly, not with an eye to making a general
+ * HTML-creation class.
+ *
+ * @category Output
+ * @package Laconica
+ * @author Evan Prodromou
+ * @author Sarven Capadisli
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ * @see Action
+ * @see HTMLOutputter
+ */
+
+class HTMLOutputter extends XMLOutputter
+{
+ /**
+ * Constructor
+ *
+ * Just wraps the XMLOutputter constructor.
+ *
+ * @param string $output URI to output to, default = stdout
+ * @param boolean $indent Whether to indent output, default true
+ */
+
+ function __construct($output='php://output', $indent=true)
+ {
+ parent::__construct($output, $indent);
+ }
+
+ /**
+ * Start an HTML document
+ *
+ * If $type isn't specified, will attempt to do content negotiation.
+ *
+ * Attempts to do content negotiation for language, also.
+ *
+ * @param string $type MIME type to use; default is to do negotation.
+ *
+ * @todo extract content negotiation code to an HTTP module or class.
+ *
+ * @return void
+ */
+
+ function startHTML($type=null)
+ {
+ if (!$type) {
+ $httpaccept = isset($_SERVER['HTTP_ACCEPT']) ?
+ $_SERVER['HTTP_ACCEPT'] : null;
+
+ // XXX: allow content negotiation for RDF, RSS, or XRDS
+
+ $cp = common_accept_to_prefs($httpaccept);
+ $sp = common_accept_to_prefs(PAGE_TYPE_PREFS);
+
+ $type = common_negotiate_type($cp, $sp);
+
+ if (!$type) {
+ common_user_error(_('This page is not available in a '.
+ 'media type you accept'), 406);
+ exit(0);
+ }
+ }
+
+ header('Content-Type: '.$type);
+
+ $this->startXML('html',
+ '-//W3C//DTD XHTML 1.0 Strict//EN',
+ 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
+
+ // FIXME: correct language for interface
+
+ $language = common_language();
+
+ $this->elementStart('html', array('xmlns' => 'http://www.w3.org/1999/xhtml',
+ 'xml:lang' => $language,
+ 'lang' => $language));
+ }
+
+ /**
+ * Output an HTML text input element
+ *
+ * Despite the name, it is specifically for outputting a
+ * text input element, not other elements. It outputs
+ * a cluster of elements, including a