Browse Source

[REVIEW] Promoting Zend_Markup to trunk

git-svn-id: http://framework.zend.com/svn/framework/standard/trunk@19720 44c647ce-9c0f-0410-b52a-842ac1e357ba
matthew 16 years ago
parent
commit
c76c13d437

+ 86 - 0
documentation/manual/en/module_specs/Zend_Markup-Getting-Started.xml

@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Reviewed: no -->
+<sect1 id="zend.markup.getting-started">
+    <title>Getting Started With Zend_Markup</title>
+
+    <para>
+        This guide to get you started with <classname>Zend_Markup</classname> uses the BBCode parser
+        and <acronym>HTML</acronym> renderer. The priciples discussed can be adapted to other
+        parsers and renderers.
+    </para>
+    
+    <example id="zend.markup.getting-started.basic-usage">
+        <title>Basic Zend_Markup Usage</title>
+        
+        <para>
+            We will first instantiate a <classname>Zend_Markup_Renderer_Html</classname> object
+            using the <methodname>Zend_Markup::factory()</methodname> method.  This will also create
+            a <classname>Zend_Markup_Parser_Bbcode</classname> object which will be added to the
+            renderer object.
+        </para>
+        
+        <para>
+            Afther that, we will use the <methodname>render()</methodname> method to convert a piece
+            of BBCode to <acronym>HTML</acronym>.
+        </para>
+        
+        <programlisting language="php"><![CDATA[
+// Creates instance of Zend_Markup_Renderer_Html,
+// with Zend_Markup_Parser_BbCode as its parser
+$bbcode = Zend_Markup::factory('Bbcode');
+
+echo $bbcode->render('[b]bold text[/b] and [i]cursive text[/i]');
+// Outputs: '<strong>bold text</strong> and <em>cursive text</em>'
+]]></programlisting>
+    </example>
+    
+    <example id="zend.markup.getting-started.complicated-example">
+        <title>A more complicated example of Zend_Markup</title>
+        
+        <para>
+            This time, we will do exactly the same as above, but with more complicated BBCode
+            markup.
+        </para>
+    
+        <programlisting language="php"><![CDATA[
+$bbcode = Zend_Markup::factory('Bbcode');
+
+$input = <<<EOT
+[list]
+[*]Zend Framework
+[*]Foobar
+[/list]
+EOT;
+
+echo $bbcode->render($input);
+/*
+Should output something like:
+<ul>
+<li>Zend Framework</li>
+<li>Foobar</li>
+</ul>
+*/
+]]></programlisting>
+    </example>
+    
+    <example id="zend.markup.getting-started.incorrect-input">
+        <title>Processing incorrect input</title>
+
+        <para>
+            Besides simply parsing and rendering markup such as BBCode,
+            <classname>Zend_Markup</classname> is also able to handle incorrect input. Most BBCode
+            processors are not able to render all input to <acroym>XHTML</acroym> valid output.
+            <classname>Zend_Markup</classname> corrects input that is nested incorrectly, and also
+            closes tags that were not closed:
+        </para>
+        
+        <programlisting language="php"><![CDATA[
+$bbcode = Zend_Markup::factory('Bbcode');
+
+echo $bbcode->render('some [i]wrong [b]sample [/i] text');
+// Note that the '[b]' tag is never closed, and is also incorrectly
+// nested; regardless, Zend_Markup renders it correctly as:
+// some <em>wrong <strong>sample </strong></em><strong> text</strong>
+]]></programlisting>
+    </example>
+</sect1>

+ 225 - 0
documentation/manual/en/module_specs/Zend_Markup-Parsers.xml

@@ -0,0 +1,225 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Reviewed: no -->
+<sect1 id="zend.markup.parsers">
+    <title>Zend_Markup Parsers</title>
+
+    <para>
+        <classname>Zend_Markup</classname> is currently shipped with two parsers, a BBCode parser
+        and a Textile parser.
+    </para>
+    
+    <sect2 id="zend.markup.parsers.theory">
+        <title>Theory of Parsing</title>
+        
+        <para>
+            The parsers of <classname>Zend_Markup</classname> are classes that convert text with
+            markup to a token tree. Although we are using the BBCode parser as example here, the
+            idea of the token tree remains the same across all parsers. We will start with this
+            piece of BBCode for example:
+        </para>
+        
+        <programlisting><![CDATA[
+[b]foo[i]bar[/i][/b]baz
+]]></programlisting>
+
+        <para>
+            Then the BBCode parser will take that value, tear it apart and create the following
+            tree:
+        </para>
+        
+        <itemizedlist>
+            <listitem>
+                <para>[b]</para>
+
+                <itemizedlist>
+                    <listitem>
+                        <para>foo</para>
+                    </listitem>
+
+                    <listitem>
+                        <para>[i]</para>
+
+                        <itemizedlist>
+                            <listitem>
+                                <para>bar</para>
+                            </listitem>
+                        </itemizedlist>
+                    </listitem>
+                </itemizedlist>
+            </listitem>
+
+            <listitem>
+                <para>baz</para>
+            </listitem>
+        </itemizedlist>
+        
+        <para>
+            You will notice that the closing tags are gone, they don't show up as content in the
+            tree structure. This is because the closing tag isn't part of the actual content.
+            Although, this does not mean that the closing tag is just lost, it is stored inside the
+            tag information for the tag itself. Also, please note that this is just a simplified
+            view of the tree itself. The actual tree contains a lot more information, like the tag's
+            attributes and its name. 
+        </para>
+    </sect2>
+    
+    <sect2 id="zend.markup.parsers.bbcode">
+        <title>The BBCode parser</title>
+        
+        <para>
+            The BBCode parser is a <classname>Zend_Markup</classname> parser that converts BBCode to
+            a token tree. The syntax of all BBCode tags is:
+        </para>
+        
+        <programlisting language="text"><![CDATA[
+[name(=(value|"value"))( attribute=(value|"value"))*]        
+]]></programlisting>
+
+        <para>
+            Some examples of valid BBCode tags are:
+        </para>
+        
+        <programlisting><![CDATA[
+[b]
+[list=1]
+[code file=Zend/Markup.php]
+[url="http://framework.zend.com/" title="Zend Framework!"]
+]]></programlisting>
+        
+        <para>
+            By default, all tags are closed by using the format '[/tagname]'.
+        </para>
+    </sect2>
+    
+    <sect2 id="zend.markup.parsers.textile">
+        <title>The Textile parser</title>
+        
+        <para>
+            The Textile parser is a <classname>Zend_Markup</classname> parser that converts Textile
+            to a token tree. Because Textile doesn't have a tag structure, the following is a list
+            of example tags:
+        </para>
+        
+        <table id="zend.markup.parsers.textile.tags">
+            <title>List of basic Textile tags</title>
+
+            <tgroup cols="2" align="left" colsep="1" rowsep="1">
+	            <thead>
+	                <row>
+	                    <entry>Sample input</entry>
+
+	                    <entry>Sample output</entry>
+	                </row>
+	            </thead>
+	            
+	            <tbody>
+	                <row>
+	                    <entry>*foo*</entry>
+
+	                    <entry><![CDATA[<strong>foo</strong>]]></entry>
+	                </row>
+
+	                <row>
+                        <entry>_foo_</entry>
+
+                        <entry><![CDATA[<em>foo</em>]]></entry>
+                    </row>
+
+                    <row>
+                        <entry>??foo??</entry>
+
+                        <entry><![CDATA[<cite>foo</cite>]]></entry>
+                    </row>
+
+                    <row>
+                        <entry>-foo-</entry>
+
+                        <entry><![CDATA[<del>foo</del>]]></entry>
+                    </row>
+
+                    <row>
+                        <entry>+foo+</entry>
+
+                        <entry><![CDATA[<ins>foo</ins>]]></entry>
+                    </row>
+
+                    <row>
+                        <entry>^foo^</entry>
+
+                        <entry><![CDATA[<sup>foo</sup>]]></entry>
+                    </row>
+
+                    <row>
+                        <entry>~foo~</entry>
+
+                        <entry><![CDATA[<sub>foo</sub>]]></entry>
+                    </row>
+
+                    <row>
+                        <entry>%foo%</entry>
+
+                        <entry><![CDATA[<span>foo</span>]]></entry>
+                    </row>
+
+                    <row>
+                        <entry>PHP(PHP Hypertext Preprocessor)</entry>
+
+                        <entry><![CDATA[<acronym title="PHP Hypertext Preprocessor">PHP</acronym>]]></entry>
+                    </row>
+
+                    <row>
+                        <entry>"Zend Framework":http://framework.zend.com/</entry>
+
+                        <entry><![CDATA[<a href="http://framework.zend.com/">Zend Framework</a>]]></entry>
+                    </row>
+
+                    <row>
+                        <entry>h1. foobar</entry>
+
+                        <entry><![CDATA[<h1>foobar</h1>]]></entry>
+                    </row>
+
+                    <row>
+                        <entry>h6. foobar</entry>
+
+                        <entry><![CDATA[<h6>foobar</h6>]]></entry>
+                    </row>
+
+                    <row>
+                        <entry>!http://framework.zend.com/images/logo.gif!</entry>
+
+                        <entry><![CDATA[<img src="http://framework.zend.com/images/logo.gif" />]]></entry>
+                    </row>
+	            </tbody>
+            </tgroup>
+        </table>
+        
+        <sect3 id="zend.markup.parsers.textile.lists">
+            <title>Lists</title>
+            
+            <para>
+                The Textile parser also supports two types of lists. The numeric type, using the "#"
+                character and bullit-lists using the "*" character. An example of both lists:
+            </para>
+            
+            <programlisting><![CDATA[
+# Item 1
+# Item 2
+
+* Item 1
+* Item 2
+]]></programlisting>
+
+            <para>
+                The above will generate two lists: the first, numbered; and the second, bulleted.
+                Inside list items, you can use normal tags like strong (*), and emphasized (_). Tags
+                that need to start on a new line (like 'h1' etc.) cannot be used inside lists.
+            </para>
+        </sect3>
+
+        <para>
+            Also, the Textile parser wraps all tags into paragraphs; a paragraph ends with two
+            newlines, and if there are more tags, a new paragraph will be added.
+        </para>
+    </sect2>
+</sect1>

+ 52 - 0
documentation/manual/en/module_specs/Zend_Markup-Renderers.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Reviewed: no -->
+<sect1 id="zend.markup.renderers">
+    <title>Zend_Markup Renderers</title>
+
+    <para>
+        <classname>Zend_Markup</classname> is currently shipped with one renderer, the
+        <acronym>HTML</acronym> renderer.
+    </para>
+    
+    <sect2 id="zend.markup.renderers.add">
+        <title>Adding your own tags</title>
+        
+        <para>
+            By adding your own tags, you can add your own functionality to the
+            <classname>Zend_Markup</classname> renderers. With the tag structure, you can add about
+            any functionality you want. From simple tags, to complicated tag structures. A simple
+            example for a 'foo' tag:
+        </para>
+        
+        <programlisting language="php"><![CDATA[
+// Creates instance of Zend_Markup_Renderer_Html,
+// with Zend_Markup_Parser_BbCode as its parser
+$bbcode = Zend_Markup::factory('Bbcode');
+
+// this will create a simple 'foo' tag
+// The first parameter defines the tag's name.
+// The second parameter takes an integer that defines the tags type.
+// The third parameter is an array that defines other things about a
+// tag, like the tag's group, and (in this case) a start and end tag.
+$bbcode->addTag(
+    'foo',
+    Zend_Markup_Renderer_RendererAbstract::TYPE_REPLACE
+        | Zend_Markup_Renderer_RendererAbstract::TAG_NORMAL,
+    array(
+        'start' => '-bar-', 
+        'end'   => '-baz-', 
+        'group' => 'inline',
+    )
+);
+
+// now, this will output: 'my -bar-tag-baz-'
+echo $bbcode->render('my [foo]tag[/foo]');
+]]></programlisting>
+        
+        <para>
+            Please note that creating your own tags only makes sense when your parser also supports
+            it with a tag structure. Currently, only BBCode supports this. Textile doesn't have
+            support for custom tags.
+        </para>
+    </sect2>
+</sect1>

+ 21 - 0
documentation/manual/en/module_specs/Zend_Markup.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Reviewed: no -->
+<sect1 id="zend.markup.introduction">
+    <title>Introduction</title>
+
+    <para>
+        The <classname>Zend_Markup</classname> component provides an extensible
+        way for parsing text and rendering lightweight markup languages like
+        BBcode and Textile. It is available as of Zend Framework version 1.10. 
+    </para>
+
+    <para>
+        <classname>Zend_Markup</classname> uses a factory method to instantiate
+        an instance of a renderer that extends
+        <classname>Zend_Markup_Renderer_Abstract</classname>. The factory
+        method accepts three arguments. The first one is the parser used to
+        tokenize the text (e.g. BbCode). The second (optional) parameter is
+        the renderer to use, Html by default. Thirdly an array with options
+        to use for the renderer can be specified.
+    </para>
+</sect1>

+ 134 - 0
library/Zend/Markup.php

@@ -0,0 +1,134 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Loader_PluginLoader
+ */
+require_once 'Zend/Loader/PluginLoader.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Markup
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup
+{
+    const CALLBACK = 'callback';
+    const REPLACE  = 'replace';
+
+
+    /**
+     * The parser loader
+     *
+     * @var Zend_Loader_PluginLoader
+     */
+    protected static $_parserLoader;
+
+    /**
+     * The renderer loader
+     *
+     * @var Zend_Loader_PluginLoader
+     */
+    protected static $_rendererLoader;
+
+
+    /**
+     * Disable instantiation of Zend_Markup
+     */
+    private function __construct() { }
+
+    /**
+     * Get the parser loader
+     *
+     * @return Zend_Loader_PluginLoader
+     */
+    public static function getParserLoader()
+    {
+        if (!(self::$_parserLoader instanceof Zend_Loader_PluginLoader)) {
+            self::$_parserLoader = new Zend_Loader_PluginLoader(array(
+                'Zend_Markup_Parser' => 'Zend/Markup/Parser/',
+            ));
+        }
+
+        return self::$_parserLoader;
+    }
+
+    /**
+     * Get the renderer loader
+     *
+     * @return Zend_Loader_PluginLoader
+     */
+    public static function getRendererLoader()
+    {
+        if (!(self::$_rendererLoader instanceof Zend_Loader_PluginLoader)) {
+            self::$_rendererLoader = new Zend_Loader_PluginLoader(array(
+                'Zend_Markup_Renderer' => 'Zend/Markup/Renderer/',
+            ));
+        }
+
+        return self::$_rendererLoader;
+    }
+
+    /**
+     * Add a parser path
+     *
+     * @param  string $prefix
+     * @param  string $path
+     * @return Zend_Loader_PluginLoader
+     */
+    public static function addParserPath($prefix, $path)
+    {
+        return self::getParserLoader()->addPrefixPath($prefix, $path);
+    }
+
+    /**
+     * Add a renderer path
+     *
+     * @param  string $prefix
+     * @param  string $path
+     * @return Zend_Loader_PluginLoader
+     */
+    public static function addRendererPath($prefix, $path)
+    {
+        return self::getRendererLoader()->addPrefixPath($prefix, $path);
+    }
+
+    /**
+     * Factory pattern
+     *
+     * @param  string $parser
+     * @param  string $renderer
+     * @param  array $options
+     * @return Zend_Markup_Renderer_RendererAbstract
+     */
+    public static function factory($parser, $renderer = 'Html', array $options = array())
+    {
+        $parserClass   = self::getParserLoader()->load($parser);
+        $rendererClass = self::getRendererLoader()->load($renderer);
+
+        $parser            = new $parserClass();
+        $options['parser'] = $parser;
+        $renderer          = new $rendererClass($options);
+
+        return $renderer;
+    }
+}

+ 38 - 0
library/Zend/Markup/Exception.php

@@ -0,0 +1,38 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Exception
+ */
+require_once 'Zend/Exception.php';
+
+/**
+ * Exception class for Zend_Markup
+ *
+ * @category   Zend 
+ * @uses       Zend_Exception
+ * @package    Zend_Markup
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Exception extends Zend_Exception
+{
+}

+ 580 - 0
library/Zend/Markup/Parser/Bbcode.php

@@ -0,0 +1,580 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Parser
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Markup_TokenList
+ */
+require_once 'Zend/Markup/TokenList.php';
+
+/**
+ * @see Zend_Markup_Parser_ParserInterface
+ */
+require_once 'Zend/Markup/Parser/ParserInterface.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Parser
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Parser_Bbcode implements Zend_Markup_Parser_ParserInterface
+{
+    const TAG_START = '[';
+    const TAG_END   = ']';
+    const NEWLINE   = "[newline\0]";
+
+    // there is a parsing difference between the default tags and single tags
+    const TYPE_DEFAULT = 'default';
+    const TYPE_SINGLE  = 'single';
+
+    /**
+     * Token tree
+     *
+     * @var Zend_Markup_TokenList
+     */
+    protected $_tree;
+
+    /**
+     * Current token
+     *
+     * @var Zend_Markup_Token
+     */
+    protected $_current;
+
+    /**
+     * Source to tokenize
+     *
+     * @var string
+     */
+    protected $_value = '';
+
+    /**
+     * Length of the value
+     *
+     * @var int
+     */
+    protected $_valueLen = 0;
+
+    /**
+     * Current pointer
+     *
+     * @var int
+     */
+    protected $_pointer = 0;
+
+    /**
+     * The buffer
+     *
+     * @var string
+     */
+    protected $_buffer = '';
+
+    /**
+     * The current tag we are working on
+     *
+     * @var string
+     */
+    protected $_tag = '';
+
+    /**
+     * The current tag name
+     *
+     * @var string
+     */
+    protected $_name;
+
+    /**
+     * Attributes of the tag we are working on
+     *
+     * @var array
+     */
+    protected $_attributes = array();
+
+    /**
+     * Stoppers that we are searching for
+     *
+     * @var array
+     */
+    protected $_searchedStoppers = array();
+
+    /**
+     * Tag information
+     *
+     * @var array
+     */
+    protected $_tags = array(
+        'Zend_Markup_Root' => array(
+            'type'     => self::TYPE_DEFAULT,
+            'stoppers' => array(),
+        ),
+        '*' => array(
+            'type'     => self::TYPE_DEFAULT,
+            'stoppers' => array(self::NEWLINE),
+        ),
+        'hr' => array(
+            'type'     => self::TYPE_SINGLE,
+            'stoppers' => array(),
+        ),
+    );
+
+
+    /**
+     * Prepare the parsing of a bbcode string, the real parsing is done in {@link _parse()}
+     *
+     * @param  string $value
+     * @return Zend_Markup_TokenList
+     */
+    public function parse($value)
+    {
+        if (!is_string($value)) {
+            /**
+             * @see Zend_Markup_Parser_Exception
+             */
+            require_once 'Zend/Markup/Parser/Exception.php';
+            throw new Zend_Markup_Parser_Exception('Value to parse should be a string.');
+        }
+
+        if (empty($value)) {
+            /**
+             * @see Zend_Markup_Parser_Exception
+             */
+            require_once 'Zend/Markup/Parser/Exception.php';
+            throw new Zend_Markup_Parser_Exception('Value to parse cannot be left empty.');
+        }
+
+        // first make we only have LF newlines
+        $this->_value = str_replace(array("\r\n", "\r", "\n"), self::NEWLINE, $value);
+
+        // initialize variables
+        $this->_tree             = new Zend_Markup_TokenList();
+        $this->_valueLen         = strlen($this->_value);
+        $this->_pointer          = 0;
+        $this->_buffer           = '';
+        $this->_temp             = array();
+        $this->_searchedStoppers = array();
+        $this->_current          = new Zend_Markup_Token(
+            '',
+            Zend_Markup_Token::TYPE_NONE,
+            'Zend_Markup_Root'
+        );
+
+        $this->_tree->addChild($this->_current);
+
+        // start the parsing process
+        $this->_parse();
+
+        return $this->_tree;
+    }
+
+    /**
+     * Parse a bbcode string
+     *
+     * @return void
+     */
+    protected function _parse()
+    {
+        // just keep looping until the parsing is done
+        do {
+            $this->_parseTagStart();
+        } while ($this->_pointer < $this->_valueLen);
+
+        if (!empty($this->_buffer)) {
+            // no tag start found, add the buffer to the current tag and stop parsing
+            $token = new Zend_Markup_Token(
+                $this->_buffer,
+                Zend_Markup_Token::TYPE_NONE,
+                $this->_current
+            );
+            $this->_current->addChild($token);
+            $this->_buffer  = '';
+        }
+    }
+
+    /**
+     * Parse the start of a tag
+     *
+     * @return void
+     */
+    protected function _parseTagStart()
+    {
+        $start = strpos($this->_value, self::TAG_START, $this->_pointer);
+
+        if ($start === false) {
+            if ($this->_valueLen > $this->_pointer) {
+                $this->_buffer .= substr($this->_value, $this->_pointer);
+                $this->_pointer = $this->_valueLen;
+            }
+            return;
+        }
+
+        // add the prepended text to the buffer
+        if ($start > $this->_pointer) {
+            $this->_buffer .= substr($this->_value, $this->_pointer, $start - $this->_pointer);
+        }
+
+        $this->_pointer = $start;
+
+        // we have the start of this tag, now we need its name
+        $this->_parseTag();
+    }
+
+    /**
+     * Get the tag information
+     *
+     * @return void
+     */
+    protected function _parseTag()
+    {
+        // get the tag's name
+        $len         = strcspn($this->_value, " \n\r\t=" . self::TAG_END, $this->_pointer + 1);
+        $this->_name = substr($this->_value, $this->_pointer + 1, $len);
+
+        $this->_tag      = self::TAG_START . $this->_name;
+        $this->_pointer += $len + 1;
+
+        if (!isset($this->_value[$this->_pointer])) {
+            // this is not a tag
+            $this->_buffer .= $this->_tag;
+
+            return;
+        }
+
+        switch ($this->_value[$this->_pointer]) {
+            case self::TAG_END:
+                // ending the tag
+                $this->_tag .= self::TAG_END;
+                $this->_endTag();
+                return;
+                break;
+            case '=':
+                // we are dealing with an name-attribute
+                $this->_tag .= '=';
+                ++$this->_pointer;
+                $value = $this->_parseAttributeValue();
+
+                if (false === $value) {
+                    // this isn't a tag, just end it right here, right now
+                    $this->_buffer .= $this->_tag;
+                    return;
+                }
+
+                $this->_attributes[$this->_name] = $value;
+                break;
+            default:
+                // the tag didn't end, so get the rest of the tag.
+                break;
+        }
+
+        $this->_parseAttributes();
+    }
+
+    /**
+     * Parse attributes
+     *
+     * @return void
+     */
+    protected function _parseAttributes()
+    {
+        while ($this->_pointer < $this->_valueLen) {
+            // we are looping until we find something
+            switch ($this->_value[$this->_pointer]) {
+                case self::TAG_END:
+                    // end the tag and return
+                    $this->_tag .= self::TAG_END;
+                    $this->_endTag();
+                    return;
+                    break;
+                default:
+                    // just go further
+                    if (ctype_space($this->_value[$this->_pointer])) {
+                        //@TODO: implement this speedhack later
+                        $len             = strspn($this->_value, " \n\r\t", $this->_pointer + 1);
+                        $this->_tag     .= substr($this->_value, $this->_pointer, $len - 1);
+                        $this->_tag .= $this->_value[$this->_pointer];
+                        ++$this->_pointer;
+                    } else {
+                        $this->_parseAttribute();
+                    }
+                    break;
+            }
+        }
+
+        // end tags without ']'
+        $this->_endTag();
+    }
+
+    /**
+     * Parse an attribute
+     *
+     * @return void
+     */
+    protected function _parseAttribute()
+    {
+        // first find the =, or a ] when the attribute is empty
+        $len = strcspn($this->_value, "=" . self::TAG_END, $this->_pointer);
+
+        // get the name and value
+        $name = substr($this->_value, $this->_pointer, $len);
+        $this->_pointer += $len;
+
+        if (isset($this->_value[$this->_pointer]) && ($this->_value[$this->_pointer] == '=')) {
+            ++$this->_pointer;
+            // ending attribute
+            $this->_tag .= $name . '=';
+
+            $value = $this->_parseAttributeValue();
+
+            $this->_attributes[trim($name)] = $value;
+        } else {
+            // empty attribute
+            $this->_tag .= $name;
+        }
+    }
+
+    /**
+     * Parse the value from an attribute
+     *
+     * @return string
+     */
+    protected function _parseAttributeValue()
+    {
+        //$delimiter = $this->_value[$this->_pointer];
+        $delimiter = substr($this->_value, $this->_pointer, 1);
+        if (($delimiter == "'") || ($delimiter == '"')) {
+            $delimiter = $this->_value[$this->_pointer];
+
+            // just find the delimiter
+            $len   = strcspn($this->_value, $delimiter, $this->_pointer + 1);
+            $value = substr($this->_value, $this->_pointer + 1, $len);
+
+            if ($this->_pointer + $len + 1 >= $this->_valueLen) {
+                // i think we just ran out of gas....
+                $this->_pointer++;
+                $this->_tag .= $delimiter;
+                return false;
+            }
+
+            $this->_pointer += $len + 2;
+
+            $this->_tag .= $delimiter . $value . $delimiter;
+        } else {
+            // find a tag end or a whitespace
+            $len   = strcspn($this->_value, " \n\r\t" . self::TAG_END, $this->_pointer);
+            $value = substr($this->_value, $this->_pointer, $len);
+
+            $this->_pointer += $len;
+
+            $this->_tag .= $value;
+        }
+        return $value;
+    }
+
+    /**
+     * End the found tag
+     *
+     * @return void
+     */
+    protected function _endTag()
+    {
+        // rule out empty tags (just '[]')
+        if (strlen($this->_name) == 0) {
+            $this->_buffer .= $this->_tag;
+            $this->_pointer++;
+            return;
+        }
+
+        // first check if the tag is a newline or a stopper without a tag
+        if (!$this->_isStopper($this->_tag, true)) {
+            if ($this->_tag == self::NEWLINE) {
+                $this->_buffer .= "\n";
+                ++$this->_pointer;
+                return;
+            } elseif ($this->_name[0] == '/') {
+                $this->_buffer .= $this->_tag;
+                ++$this->_pointer;
+                return;
+            }
+        }
+
+        // first add the buffer as token and clear the buffer
+        if (!empty($this->_buffer)) {
+            $token = new Zend_Markup_Token(
+                $this->_buffer,
+                Zend_Markup_Token::TYPE_NONE,
+                '',
+                array(),
+                $this->_current
+            );
+            $this->_current->addChild($token);
+            $this->_buffer = '';
+        }
+
+        $attributes = $this->_attributes;
+
+        // check if this tag is a stopper
+        if ($this->_isStopper($this->_tag)) {
+            // we got a stopper, end the current tag and get back to the parent
+            $this->_current->setStopper($this->_tag);
+            $this->_current = $this->_current->getParent();
+
+            $this->_removeFromSearchedStoppers($this->_current);
+        } elseif (!empty($this->_searchedStoppers[$this->_tag])) {
+            // hell has broken loose, these stoppers are searched somewere
+            // lower in the tree
+            $oldItems = array();
+
+            while (!in_array($this->_tag, $this->_tags[$this->_current->getName()]['stoppers'])) {
+                $oldItems[]     = clone $this->_current;
+                $this->_current = $this->_current->getParent();
+            }
+
+            // ladies and gentlemen... WE GOT HIM!
+            $this->_current->setStopper($this->_tag);
+            $this->_removeFromSearchedStoppers($this->_current);
+            $this->_current = $this->_current->getParent();
+
+            // add those old items again
+            foreach (array_reverse($oldItems) as $token) {
+                /* @var $token Zend_Markup_Token */
+                $this->_current->addChild($token);
+                $token->setParent($this->_current);
+                $this->_current = $token;
+            }
+        } elseif ($this->_getType($this->_name) == self::TYPE_SINGLE) {
+            $token = new Zend_Markup_Token(
+                $this->_tag,
+                Zend_Markup_Token::TYPE_TAG,
+                $this->_name,
+                $attributes,
+                $this->_current
+            );
+            $this->_current->addChild($token);
+        } else {
+            // add the tag and jump into it
+            $token = new Zend_Markup_Token(
+                $this->_tag,
+                Zend_Markup_Token::TYPE_TAG,
+                $this->_name,
+                $attributes,
+                $this->_current
+            );
+            $this->_current->addChild($token);
+            $this->_current = $token;
+
+            $this->_addToSearchedStoppers($token);
+        }
+        ++$this->_pointer;
+        $this->_attributes = array();
+    }
+
+    /**
+     * Check the tag's type
+     *
+     * @param  string $name
+     * @return string
+     */
+    protected function _getType($name)
+    {
+        // first check if the current tag has a row for this
+        if (!isset($this->_tags[$name])) {
+            $this->_tags[$name] = array(
+                'type'     => self::TYPE_DEFAULT,
+                'stoppers' => array(
+                    self::TAG_START . '/' . $name . self::TAG_END,
+                    self::TAG_START . '/' . self::TAG_END
+                )
+            );
+        }
+
+        return $this->_tags[$name]['type'];
+    }
+
+    /**
+     * Check if the tag is a stopper
+     *
+     * @param  string $tag
+     * @return bool
+     */
+    protected function _isStopper($tag, $searched = false)
+    {
+        // first check if the current tag has registered stoppers
+        if (!isset($this->_tags[$this->_current->getName()])) {
+            $this->_tags[$this->_current->getName()] = array(
+                'type'     => self::TYPE_DEFAULT,
+                'stoppers' => array(
+                    self::TAG_START . '/' . $this->_current->getName() . self::TAG_END,
+                    self::TAG_START . '/' . self::TAG_END
+                )
+            );
+        }
+
+        // and now check if it is a stopper
+        $tags = $this->_tags[$this->_current->getName()]['stoppers'];
+        if (in_array($tag, $tags)
+            || (!empty($this->_searchedStoppers[$this->_tag]) && $searched)
+        ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Add to searched stoppers
+     *
+     * @param  Zend_Markup_Token $token
+     * @return void
+     */
+    protected function _addToSearchedStoppers(Zend_Markup_Token $token)
+    {
+        if (!isset($this->_tags[$token->getName()])) {
+            $this->_tags[$token->getName()] = array(
+                'type'     => self::TYPE_DEFAULT,
+                'stoppers' => array(
+                    self::TAG_START . '/' . $token->getName() . self::TAG_END,
+                    self::TAG_START . '/' . self::TAG_END
+                )
+            );
+        }
+
+        foreach ($this->_tags[$token->getName()]['stoppers'] as $stopper) {
+            if (!isset($this->_searchedStoppers[$stopper])) {
+                $this->_searchedStoppers[$stopper] = 0;
+            }
+            ++$this->_searchedStoppers[$stopper];
+        }
+    }
+
+    /**
+     * Remove from searched stoppers
+     *
+     * @param  Zend_Markup_Token $token
+     * @return void
+     */
+    protected function _removeFromSearchedStoppers(Zend_Markup_Token $token)
+    {
+        foreach ($this->_tags[$token->getName()]['stoppers'] as $stopper) {
+            --$this->_searchedStoppers[$stopper];
+        }
+    }
+}

+ 40 - 0
library/Zend/Markup/Parser/Exception.php

@@ -0,0 +1,40 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Parser
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Markup_Exception
+ */
+require_once 'Zend/Markup/Exception.php';
+
+/**
+ * Exception class for Zend_Markup_Parser
+ *
+ * @category   Zend 
+ * @uses       Zend_Markup_Exception
+ * @package    Zend_Markup
+ * @subpackage Parser
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Parser_Exception extends Zend_Markup_Exception
+{
+}

+ 67 - 0
library/Zend/Markup/Parser/ParserInterface.php

@@ -0,0 +1,67 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Parser
+ * @copyright  Copyright (c) 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Parser
+ * @copyright  Copyright (c) 2005-2007 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_Markup_Parser_ParserInterface
+{
+    /**
+     * Parse a string
+     *
+     * This should output something like this:
+     *
+     * <code>
+     * array(
+     *     array(
+     *         'tag'        => '[tag="a" attr=val]',
+     *         'type'       => Zend_Markup::TYPE_TAG,
+     *         'name'       => 'tag',
+     *         'stoppers'   => array('[/]', '[/tag]'),
+     *         'attributes' => array(
+     *             'tag'  => 'a',
+     *             'attr' => 'val'
+     *         )
+     *     ),
+     *     array(
+     *         'tag'   => 'value',
+     *         'type'  => Zend_Markup::TYPE_NONE
+     *     ),
+     *     array(
+     *         'tag'        => '[/tag]',
+     *         'type'       => Zend_Markup::TYPE_STOPPER,
+     *         'name'       => 'tag',
+     *         'stoppers'   => array(),
+     *         'attributes' => array()
+     *     )
+     * )
+     * </code>
+     *
+     * @param  string $value
+     * @return array
+     */
+    public function parse($value);
+}

+ 919 - 0
library/Zend/Markup/Parser/Textile.php

@@ -0,0 +1,919 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Parser
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Markup_TokenList
+ */
+require_once 'Zend/Markup/TokenList.php';
+
+/**
+ * @see Zend_Markup_Parser_ParserInterface
+ */
+require_once 'Zend/Markup/Parser/ParserInterface.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Parser
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Parser_Textile implements Zend_Markup_Parser_ParserInterface
+{
+    /**
+     * Token tree
+     *
+     * @var Zend_Markup_TokenList
+     */
+    protected $_tree;
+
+    /**
+     * Current token
+     *
+     * @var Zend_Markup_Token
+     */
+    protected $_current;
+
+    /**
+     * Source to tokenize
+     *
+     * @var string
+     */
+    protected $_value = '';
+
+    /**
+     * Length of the value
+     *
+     * @var int
+     */
+    protected $_valueLen = 0;
+
+    /**
+     * Current pointer
+     *
+     * @var int
+     */
+    protected $_pointer = 0;
+
+    /**
+     * The buffer
+     *
+     * @var string
+     */
+    protected $_buffer = '';
+
+    /**
+     * Simple tag translation
+     *
+     * @var array
+     */
+    protected $_simpleTags = array(
+        '*'  => 'strong',
+        '**' => 'bold',
+        '_'  => 'emphasized',
+        '__' => 'italic',
+        '??' => 'citation',
+        '-'  => 'deleted',
+        '+'  => 'insert',
+        '^'  => 'superscript',
+        '~'  => 'subscript',
+        '%'  => 'span',
+        // these are a little more complicated
+        '@'  => 'code',
+        '!'  => 'img',
+    );
+
+    /**
+     * The list's level
+     *
+     * @var int
+     */
+    protected $_listLevel = 0;
+
+
+    /**
+     * Prepare the parsing of a Textile string, the real parsing is done in {@link _parse()}
+     *
+     * @param  string $value
+     * @return array
+     */
+    public function parse($value)
+    {
+        if (!is_string($value)) {
+            /**
+             * @see Zend_Markup_Parser_Exception
+             */
+            require_once 'Zend/Markup/Parser/Exception.php';
+            throw new Zend_Markup_Parser_Exception('Value to parse should be a string.');
+        }
+        if (empty($value)) {
+            /**
+             * @see Zend_Markup_Parser_Exception
+             */
+            require_once 'Zend/Markup/Parser/Exception.php';
+            throw new Zend_Markup_Parser_Exception('Value to parse cannot be left empty.');
+        }
+
+        // first make we only have LF newlines
+        $this->_value = str_replace(array("\r\n", "\r"), "\n", $value);
+
+        // trim and add a leading LF to make sure that headings on the first line
+        // are parsed correctly
+        $this->_value = trim($this->_value);
+
+        // initialize variables
+        $this->_tree     = new Zend_Markup_TokenList();
+        $this->_valueLen = strlen($this->_value);
+        $this->_pointer  = 0;
+        $this->_buffer   = '';
+        $this->_temp     = array();
+        $this->_current  = new Zend_Markup_Token('', Zend_Markup_Token::TYPE_NONE, 'Zend_Markup_Root');
+        $this->_tree->addChild($this->_current);
+
+
+        $info = array(
+            'tag'        => '',
+            'attributes' => array()
+        );
+
+        // check if the base paragraph has some extra information
+        if ($this->_value[$this->_pointer] == 'p') {
+            $this->_pointer++;
+            $info = $this->_parseTagInfo('p', '.', true);
+
+            // check if the paragraph definition is correct
+            if (substr($this->_value, $this->_pointer, 2) != '. ') {
+                $this->_pointer = 0;
+            } else {
+                $this->_pointer += 2;
+            }
+        }
+
+        // add the base paragraph
+        $paragraph = new Zend_Markup_Token(
+            $info['tag'],
+            Zend_Markup_Token::TYPE_TAG,
+            'p',
+            $info['attributes'],
+            $this->_current
+        );
+        $this->_current->addChild($paragraph);
+        $this->_current = $paragraph;
+
+        // start the parsing process
+        $this->_parse(true);
+
+        return $this->_tree;
+    }
+
+    /**
+     * Parse a Textile string
+     *
+     * @return void
+     */
+    protected function _parse($checkStartTags = false)
+    {
+        // just keep looping until the parsing is done
+        if ($checkStartTags) {
+            // check the starter tags (newlines)
+            switch ($this->_value[$this->_pointer]) {
+                case 'h':
+                    $this->_parseHeading();
+                    break;
+                case '*':
+                case '#':
+                    $this->_parseList();
+                    break;
+            }
+        }
+        while ($this->_pointer < $this->_valueLen) {
+            $this->_parseTag();
+        }
+
+        $this->_processBuffer();
+    }
+
+    /**
+     * Parse an inline string
+     *
+     * @param  string $value
+     * @param  Zend_Markup_Token $token
+     * @return Zend_Markup_Token
+     */
+    protected function _parseInline($value, Zend_Markup_Token $token)
+    {
+        // save the old values
+        $oldValue    = $this->_value;
+        $oldValueLen = $this->_valueLen;
+        $oldPointer  = $this->_pointer;
+        $oldTree     = $this->_tree;
+        $oldCurrent  = $this->_current;
+        $oldBuffer   = $this->_buffer;
+
+        // set the new values
+        $this->_value    = $value;
+        $this->_valueLen = strlen($value);
+        $this->_pointer  = 0;
+        $this->_tree     = $token;
+        $this->_current  = $token;
+        $this->_buffer   = '';
+
+        // parse
+        $this->_parse();
+
+        // set the old values
+        $this->_value    = $oldValue;
+        $this->_valueLen = $oldValueLen;
+        $this->_pointer  = $oldPointer;
+        $this->_tree     = $oldTree;
+        $this->_current  = $oldCurrent;
+        $this->_buffer   = $oldBuffer;
+
+        return $token;
+    }
+
+    /**
+     * Parse a tag
+     *
+     * @return void
+     */
+    protected function _parseTag()
+    {
+        switch ($this->_value[$this->_pointer]) {
+            case '*':
+            case '_':
+            case '?':
+            case '-':
+            case '+':
+            case '^':
+            case '~':
+            case '%':
+                // simple tags like bold, italic
+                $this->_parseSimpleTag($this->_value[$this->_pointer]);
+                break;
+            case '@':
+            case '!':
+                // simple tags, only they don't have anything inside them
+                $this->_parseEmptyTag($this->_value[$this->_pointer]);
+                break;
+            case '"':
+                $this->_parseLink();
+                break;
+            case '(':
+                $this->_parseAcronym();
+                break;
+            case "\n":
+                // this could mean multiple things, let this function check what
+                $this->_parseNewline();
+                break;
+            default:
+                // just add this token to the buffer
+                $this->_buffer .= $this->_value[$this->_pointer++];
+                break;
+        }
+    }
+
+    /**
+     * Parse tag information
+     *
+     * @todo use $tagEnd property for identation
+     * @param  string $tag
+     * @param  string $tagEnd
+     * @param  bool $block
+     * @return array
+     */
+    protected function _parseTagInfo($tag = '', $tagEnd = '', $block = false)
+    {
+        $info = array(
+            'attributes' => array(),
+            'tag'        => $tag
+        );
+
+        if ($this->_pointer >= $this->_valueLen) {
+            return $info;
+        }
+
+        // check which attribute
+        switch ($this->_value[$this->_pointer]) {
+            case '(':
+                // class or id
+                $attribute = $this->_parseAttributeEnd(')', $tagEnd);
+
+                if (!$attribute) {
+                    break;
+                }
+
+                $info['tag'] .= '(' . $attribute . ')';
+
+                if ($attribute[0] == '#') {
+                    $info['attributes']['id'] = substr($attribute, 1);
+                } elseif (($len = strpos($attribute, '#')) !== false) {
+                    $info['attributes']['class'] = substr($attribute, 0, $len);
+                    $info['attributes']['id']    = substr($attribute, $len + 1);
+                } else {
+                    $info['attributes']['class'] = $attribute;
+                }
+
+                $this->_pointer++;
+                break;
+            case '{':
+                // style
+                $attribute = $this->_parseAttributeEnd('}', $tagEnd);
+
+                if (!$attribute) {
+                    break;
+                }
+
+                $info['tag'] .= '{' . $attribute . '}';
+
+                $info['attributes']['style'] = $attribute;
+
+                $this->_pointer++;
+                break;
+            case '[':
+                // style
+                $attribute = $this->_parseAttributeEnd(']', $tagEnd);
+
+                if (!$attribute) {
+                    break;
+                }
+
+                $info['tag'] .= '[' . $attribute . ']';
+
+                $info['attributes']['lang'] = $attribute;
+
+                $this->_pointer++;
+                break;
+            case '<':
+                if ($block) {
+                    $info['tag'] .= '<';
+                    if (($this->_value[++$this->_pointer] == '>')) {
+                        $info['attributes']['align'] = 'justify';
+                        $info['tag'] .= '>';
+                        $this->_pointer++;
+                    } else {
+                        $info['attributes']['align'] = 'left';
+                    }
+                }
+                break;
+            case '>':
+                if ($block) {
+                    $info['attributes']['align'] = 'right';
+                    $info['tag'] .= '>';
+                    $this->_pointer++;
+                }
+                break;
+            case '=':
+                if ($block) {
+                    $info['attributes']['align'] = 'center';
+                    $info['tag'] .= '=';
+                    $this->_pointer++;
+                }
+                break;
+            default:
+                // simply do nothing, there are no attributes
+                break;
+        }
+
+        return $info;
+    }
+
+    /**
+     * Parse the attribute's end
+     *
+     * @todo use $tagEnd property for indentation
+     * @param  string $endToken
+     * @param  string $tagEnd
+     * @param  bool $block
+     * @return string|bool
+     */
+    protected function _parseAttributeEnd($endToken, $tagEnd = '', $block = false)
+    {
+        $value = '';
+
+        $oldPointer = $this->_pointer;
+
+        while ($this->_pointer < $this->_valueLen) {
+            if ($this->_pointer + 1 >= $this->_valueLen) {
+                $this->_pointer = $oldPointer;
+                return false;
+            }
+            if ($this->_value[++$this->_pointer] == $endToken) {
+                return $value;
+            }
+            $value .= $this->_value[$this->_pointer];
+        }
+    }
+
+    /**
+     * Parse a simple markup tag
+     *
+     * @param  string $tag
+     * @return void
+     */
+    protected function _parseSimpleTag($tag)
+    {
+        if (++$this->_pointer >= $this->_valueLen) {
+	        // could be a stopper
+	        if ($this->_isSimpleStopper($tag)) {
+	            $this->_processBuffer();
+	            $this->_current->setStopper($tag);
+	            $this->_current = $this->_current->getParent();
+	        } else {
+	        	$this->_buffer .= $tag;
+	        }
+            return;
+        }
+
+        if ($this->_value[$this->_pointer] == $tag) {
+            $tag = $tag . $tag;
+            $this->_pointer++;
+        }
+
+        // check if this is a stopper
+        if ($this->_isSimpleStopper($tag)) {
+            $this->_processBuffer();
+            $this->_current->setStopper($tag);
+            $this->_current = $this->_current->getParent();
+            return;
+        }
+
+        // check if this is a tag
+        if (isset($this->_simpleTags[$tag])) {
+            $name = $this->_simpleTags[$tag];
+
+            // process the buffer and add the tag
+            $this->_processBuffer();
+
+            // parse a possible attribute
+            $info = $this->_parseTagInfo($tag);
+
+            $token = new Zend_Markup_Token(
+                $info['tag'],
+                Zend_Markup_Token::TYPE_TAG,
+                $name,
+                $info['attributes'],
+                $this->_current
+            );
+            $this->_current->addChild($token);
+            $this->_current = $token;
+        } else {
+            $this->_buffer .= $tag;
+        }
+    }
+
+    /**
+     * Parse an 'empty' markup tag
+     *
+     * @todo implement support for attributes
+     * @param  string $tag
+     * @return void
+     */
+    protected function _parseEmptyTag($tag)
+    {
+        if (!isset($this->_simpleTags[$tag])) {
+            $this->_buffer .= $tag;
+            return;
+        }
+
+        // add the tag
+        $this->_processBuffer();
+
+        $this->_pointer++;
+
+        $info = $this->_parseTagInfo($tag);
+
+        $name = $this->_simpleTags[$tag];
+
+        $token = new Zend_Markup_Token(
+            $info['tag'],
+            Zend_Markup_Token::TYPE_TAG,
+            $name,
+            $info['attributes'],
+            $this->_current
+        );
+        $this->_current->addChild($token);
+        $this->_current = $token;
+
+        // find the stopper
+        while ($this->_valueLen > $this->_pointer) {
+            if ($this->_value[$this->_pointer] == $tag) {
+                // found the stopper, set it and return
+                $this->_pointer++;
+
+                $this->_processBuffer();
+                $this->_current->setStopper($tag);
+                $this->_current = $this->_current->getParent();
+                return;
+            } else {
+                // not yet found, add the character to the buffer and go to the next one
+                $this->_buffer .= $this->_value[$this->_pointer++];
+            }
+        }
+    }
+
+    /**
+     * Parse a link
+     *
+     * @return void
+     */
+    protected function _parseLink()
+    {
+        // first find the other "
+        $len  = strcspn($this->_value, '"', ++$this->_pointer);
+        $text = substr($this->_value, $this->_pointer, $len);
+
+        // not a link tag
+        if (($this->_pointer + $len >= $this->_valueLen) || ($this->_value[$this->_pointer + $len++] != '"')) {
+            $this->_buffer  .= '"' . $text;
+            $this->_pointer += $len;
+            return;
+        }
+        // not a link tag
+        if (($this->_pointer + $len >= $this->_valueLen) || ($this->_value[$this->_pointer + $len++] != ':')) {
+            $this->_buffer  .= '"' . $text . '"';
+            $this->_pointer += $len;
+            return;
+        }
+
+        // update the pointer
+        $this->_pointer += $len;
+
+        // now, get the URL
+        $len = strcspn($this->_value, "\n\t ", $this->_pointer);
+        $url = substr($this->_value, $this->_pointer, $len);
+
+        $this->_pointer += $len;
+
+        // gather the attributes
+        $attributes = array(
+            'url' => $url,
+        );
+
+        // add the tag
+        $this->_processBuffer();
+        $token = new Zend_Markup_Token(
+            '"',
+            Zend_Markup_Token::TYPE_TAG,
+            'url',
+            $attributes,
+            $this->_current
+        );
+        $token->addChild(new Zend_Markup_Token(
+            $text,
+            Zend_Markup_Token::TYPE_NONE,
+            '',
+            array(),
+            $token
+        ));
+        $token->setStopper('":');
+
+        $this->_current->addChild($token);
+    }
+
+    /**
+     * Parse a newline
+     *
+     * A newline could mean multiple things:
+     * - Heading {@link _parseHeading()}
+     * - List {@link _parseList()}
+     * - Paragraph {@link _parseParagraph()}
+     *
+     * @return void
+     */
+    protected function _parseNewline()
+    {
+        if (!empty($this->_buffer) && ($this->_buffer[strlen($this->_buffer) - 1] == "\n")) {
+            $this->_parseParagraph();
+        } else {
+            switch ($this->_value[++$this->_pointer]) {
+                case 'h':
+                    $this->_parseHeading();
+                    break;
+                case '*':
+                case '#':
+                    $this->_parseList();
+                    break;
+                default:
+                    $this->_buffer .= "\n";
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Parse a paragraph declaration
+     *
+     * @return void
+     */
+    protected function _parseParagraph()
+    {
+        // remove the newline from the buffer and increase the pointer
+        $this->_buffer = substr($this->_buffer, 0, -1);
+        $this->_pointer++;
+
+        // check if we are in the current paragraph
+        if ($this->_current->getName() == 'p') {
+            $this->_processBuffer();
+
+            $this->_current->setStopper("\n");
+            $this->_current = $this->_current->getParent();
+
+            $info = array(
+                'tag'        => "\n",
+                'attributes' => array()
+            );
+
+            $oldPointer = $this->_pointer;
+
+            if ($this->_value[$this->_pointer] == 'p') {
+                $this->_pointer++;
+                $info = $this->_parseTagInfo("\np", '.', true);
+
+                if (substr($this->_value, $this->_pointer, 2) == '. ') {
+                    $this->_pointer += 2;
+                } else {
+                    // incorrect declaration of paragraph, reset the pointer and use default info
+                    $this->_pointer = $oldPointer;
+                    $info = array(
+                        'tag'        => "\n",
+                        'attributes' => array()
+                    );
+                }
+            }
+
+            // create a new one and jump onto it
+            $paragraph = new Zend_Markup_Token(
+                $info['tag'],
+                Zend_Markup_Token::TYPE_TAG,
+                'p',
+                $info['attributes'],
+                $this->_current
+            );
+            $this->_current->addChild($paragraph);
+            $this->_current = $paragraph;
+        } else {
+            /**
+             * @todo Go down in the tree until you find the paragraph
+             * while remembering every step. After that, close the
+             * paragraph, add a new one and climb back up by re-adding
+             * every step
+             */
+        }
+    }
+
+    /**
+     * Parse a heading
+     *
+     * @todo implement support for attributes
+     * @return void
+     */
+    protected function _parseHeading()
+    {
+        // check if it is a valid heading
+        if (in_array($this->_value[++$this->_pointer], range(1, 6))) {
+            $name = 'h' . $this->_value[$this->_pointer++];
+
+            $info = $this->_parseTagInfo($name, '.', true);
+
+            // now, the next char should be a dot
+            if ($this->_value[$this->_pointer] == '.') {
+                $info['tag'] .= '.';
+
+                // add the tag
+                $this->_processBuffer();
+
+                $token = new Zend_Markup_Token(
+                    $info['tag'],
+                    Zend_Markup_Token::TYPE_TAG,
+                    $name,
+                    $info['attributes'],
+                    $this->_current
+                );
+                $this->_current->addChild($token);
+                $this->_current = $token;
+
+                if ($this->_value[++$this->_pointer] != ' ') {
+                    $this->_buffer .= $this->_value[$this->_pointer];
+                }
+
+                // find the end
+                $len = strcspn($this->_value, "\n", ++$this->_pointer);
+
+                $this->_buffer.= substr($this->_value, $this->_pointer, $len);
+
+                $this->_pointer += $len;
+
+                // end the tag and return
+                $this->_processBuffer();
+                $this->_current = $this->_current->getParent();
+
+                return;
+            }
+            $this->_buffer .= "\n" . $name;
+            return;
+        }
+
+        // not a valid heading
+        $this->_buffer .= "\nh";
+    }
+
+    /**
+     * Parse a list
+     *
+     * @todo allow a deeper list level
+     * @todo add support for markup inside the list items
+     * @return void
+     */
+    protected function _parseList()
+    {
+        // for this operation, we need the entire line
+        $len  = strcspn($this->_value, "\n", $this->_pointer);
+        $line = substr($this->_value, $this->_pointer, $len);
+
+        // add the list tag
+        $this->_processBuffer();
+
+        // maybe we have to rewind
+        $oldPointer = $this->_pointer;
+
+        // attributes array
+        $attrs = array();
+        if ($line[0] == '#') {
+            $attrs['list'] = 'decimal';
+        }
+
+        if ((strlen($line) <= 1) || ($line[1] != ' ')) {
+            // rewind and return
+            unset($list);
+            $this->_pointer = $oldPointer;
+
+            return;
+        }
+
+        // add the token
+        $list = new Zend_Markup_Token('', Zend_Markup_Token::TYPE_TAG, 'list', $attrs, $this->_current);
+
+        // loop through every next line, until there are no list items any more
+        while ($this->_valueLen > $this->_pointer) {
+            // add the li-tag with contents
+            $item = new Zend_Markup_Token(
+                $line[0],
+                Zend_Markup_Token::TYPE_TAG,
+                'li',
+                array(),
+                $list
+            );
+
+            // parse and add the content
+            $this->_parseInline(substr($line, 2), $item);
+
+            $list->addChild($item);
+
+            $this->_pointer += $len;
+
+            // check if the next line is a list item too
+            if (($this->_pointer + 1 >= $this->_valueLen) || $this->_value[++$this->_pointer] != $line[0]) {
+                // there is no new list item coming
+                break;
+            }
+
+            // get the next line
+            $len  = strcspn($this->_value, "\n", $this->_pointer);
+            $line = substr($this->_value, $this->_pointer, $len);
+        }
+
+        // end the list tag
+        $this->_current->addChild($list);
+        $this->_current = $this->_current->getParent();
+    }
+
+    /**
+     * Parse an acronym
+     *
+     * @return void
+     */
+    protected function _parseAcronym()
+    {
+        $this->_pointer++;
+
+        // first find the acronym itself
+        $acronym = '';
+        $pointer = 0;
+
+        if (empty($this->_buffer)) {
+            $this->_buffer .= '(';
+            return;
+        }
+
+        $bufferLen = strlen($this->_buffer);
+
+        while (($bufferLen > $pointer) && ctype_upper($this->_buffer[$bufferLen - ++$pointer])) {
+            $acronym = $this->_buffer[strlen($this->_buffer) - $pointer] . $acronym;
+        }
+
+        if (strlen($acronym) < 3) {
+            // just add the '(' to the buffer, this isn't an acronym
+            $this->_buffer .= '(';
+            return;
+        }
+
+        // now, find the closing ')'
+        $title = '';
+        while ($this->_pointer < $this->_valueLen) {
+            if ($this->_value[$this->_pointer] == ')') {
+                break;
+            } else {
+                $title .= $this->_value[$this->_pointer];
+            }
+            $this->_pointer++;
+        }
+
+        if ($this->_pointer >= $this->_valueLen) {
+            $this->_buffer .= '(';
+            return;
+        }
+
+        $this->_pointer++;
+
+        if (empty($title)) {
+            $this->_buffer .= '()';
+            return;
+        }
+
+        $this->_buffer = substr($this->_buffer, 0, -$this->_pointer);
+        $this->_processBuffer();
+
+        // now add the tag
+        $token = new Zend_Markup_Token(
+            '',
+            Zend_Markup_Token::TYPE_TAG,
+            'acronym',
+            array('title' => $title),
+            $this->_current
+        );
+        $token->setStopper('(' . $title . ')');
+        $token->addChild(new Zend_Markup_Token(
+            $acronym,
+            Zend_Markup_Token::TYPE_NONE,
+            '',
+            array(),
+            $token
+        ));
+        $this->_current->addChild($token);
+    }
+
+    /**
+     * Check if the tag is a simple stopper
+     *
+     * @param  string $tag
+     * @return bool
+     */
+    protected function _isSimpleStopper($tag)
+    {
+        if ($tag == substr($this->_current->getTag(), 0, strlen($tag))) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Process the current buffer
+     *
+     * @return void
+     */
+    protected function _processBuffer()
+    {
+        if (!empty($this->_buffer)) {
+            // no tag start found, add the buffer to the current tag and stop parsing
+            $token = new Zend_Markup_Token(
+                $this->_buffer,
+                Zend_Markup_Token::TYPE_NONE,
+                '',
+                array(),
+                $this->_current
+            );
+            $this->_current->addChild($token);
+            $this->_buffer  = '';
+        }
+    }
+}

+ 40 - 0
library/Zend/Markup/Renderer/Exception.php

@@ -0,0 +1,40 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Markup_Exception
+ */
+require_once 'Zend/Markup/Exception.php';
+
+/**
+ * Exception class for Zend_Markup_Renderer
+ *
+ * @category   Zend
+ * @uses       Zend_Markup_Exception
+ * @package    Zend_Markup
+ * @subpackage Renderer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Renderer_Exception extends Zend_Markup_Exception
+{
+}

+ 575 - 0
library/Zend/Markup/Renderer/Html.php

@@ -0,0 +1,575 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Uri
+ */
+require_once 'Zend/Uri.php';
+
+/**
+ * @see Zend_Markup_Renderer_RendererAbstract
+ */
+require_once 'Zend/Markup/Renderer/RendererAbstract.php';
+
+/**
+ * HTML renderer
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Renderer_Html extends Zend_Markup_Renderer_RendererAbstract
+{
+    /**
+     * Tag info
+     *
+     * @var array
+     */
+    protected $_tags = array(
+        'b' => array(
+            'type'   => 10, // self::TYPE_REPLACE | self::TAG_NORMAL
+            'tag'    => 'strong',
+            'group'  => 'inline',
+            'filter' => true,
+        ),
+        'u' => array(
+            'type'        => 10,
+            'tag'         => 'span',
+            'attributes'  => array(
+                'style' => 'text-decoration: underline;',
+            ),
+            'group'       => 'inline',
+            'filter'      => true,
+        ),
+        'i' => array(
+            'type'   => 10,
+            'tag'    => 'em',
+            'group'  => 'inline',
+            'filter' => true,
+        ),
+        'cite' => array(
+            'type'   => 10,
+            'tag'    => 'cite',
+            'group'  => 'inline',
+            'filter' => true,
+        ),
+        'del' => array(
+            'type'   => 10,
+            'tag'    => 'del',
+            'group'  => 'inline',
+            'filter' => true,
+        ),
+        'ins' => array(
+            'type'   => 10,
+            'tag'    => 'ins',
+            'group'  => 'inline',
+            'filter' => true,
+        ),
+        'sub' => array(
+            'type'   => 10,
+            'tag'    => 'sub',
+            'group'  => 'inline',
+            'filter' => true,
+        ),
+        'sup' => array(
+            'type'   => 10,
+            'tag'    => 'sup',
+            'group'  => 'inline',
+            'filter' => true,
+        ),
+        'span' => array(
+            'type'   => 10,
+            'tag'    => 'span',
+            'group'  => 'inline',
+            'filter' => true,
+        ),
+        'acronym'  => array(
+            'type'   => 10,
+            'tag'    => 'acronym',
+            'group'  => 'inline',
+            'filter' => true,
+        ),
+        // headings
+        'h1' => array(
+            'type'   => 10,
+            'tag'    => 'h1',
+            'group'  => 'inline',
+            'filter' => false,
+        ),
+        'h2' => array(
+            'type'   => 10,
+            'tag'    => 'h2',
+            'group'  => 'inline',
+            'filter' => false,
+        ),
+        'h3' => array(
+            'type'   => 10,
+            'tag'    => 'h3',
+            'group'  => 'inline',
+            'filter' => false,
+        ),
+        'h4' => array(
+            'type'   => 10,
+            'tag'    => 'h4',
+            'group'  => 'inline',
+            'filter' => false,
+        ),
+        'h5' => array(
+            'type'   => 10,
+            'tag'    => 'h5',
+            'group'  => 'inline',
+            'filter' => false,
+        ),
+        'h6' => array(
+            'type'   => 10,
+            'tag'    => 'h6',
+            'group'  => 'inline',
+            'filter' => false,
+        ),
+        // callback tags
+        'url' => array(
+            'type'     => 6, // self::TYPE_CALLBACK | self::TAG_NORMAL
+            'callback' => array('Zend_Markup_Renderer_Html', '_htmlUrl'),
+            'group'    => 'inline',
+            'filter'   => true,
+        ),
+        'img' => array(
+            'type'     => 6,
+            'callback' => array('Zend_Markup_Renderer_Html', '_htmlImg'),
+            'group'    => 'inline_empty',
+            'filter'   => true,
+        ),
+        'code' => array(
+            'type'     => 6,
+            'callback' => array('Zend_Markup_Renderer_Html', '_htmlCode'),
+            'group'    => 'block_empty',
+            'filter'   => false,
+        ),
+        'p' => array(
+            'type'   => 10,
+            'tag'    => 'p',
+            'group'  => 'block',
+            'filter' => true,
+        ),
+        'ignore' => array(
+            'type'   => 10,
+            'start'  => '',
+            'end'    => '',
+            'group'  => 'block_empty',
+            'filter' => true,
+        ),
+        'quote' => array(
+            'type'   => 10,
+            'tag'    => 'blockquote',
+            'group'  => 'block',
+            'filter' => true,
+        ),
+        'list' => array(
+            'type'     => 6,
+            'callback' => array('Zend_Markup_Renderer_Html', '_htmlList'),
+            'group'    => 'list',
+            'filter'   => false,
+        ),
+        '*' => array(
+            'type'   => 10,
+            'tag'    => 'li',
+            'group'  => 'list-item',
+            'filter' => false,
+        ),
+        'hr' => array(
+            'type'    => 9, // self::TYPE_REPLACE | self::TAG_SINGLE
+            'tag'     => 'hr',
+            'group'   => 'block',
+        ),
+        // aliases
+        'bold' => array(
+            'type' => 16,
+            'name' => 'b',
+        ),
+        'strong' => array(
+            'type' => 16,
+            'name' => 'b',
+        ),
+        'italic' => array(
+            'type' => 16,
+            'name' => 'i',
+        ),
+        'em' => array(
+            'type' => 16,
+            'name' => 'i',
+        ),
+        'emphasized' => array(
+            'type' => 16,
+            'name' => 'i',
+        ),
+        'underline' => array(
+            'type' => 16,
+            'name' => 'u',
+        ),
+        'citation' => array(
+            'type' => 16,
+            'name' => 'cite',
+        ),
+        'deleted' => array(
+            'type' => 16,
+            'name' => 'del',
+        ),
+        'insert' => array(
+            'type' => 16,
+            'name' => 'ins',
+        ),
+        'strike' => array(
+            'type' => 16,
+            'name' => 's',
+        ),
+        's' => array(
+            'type' => 16,
+            'name' => 'del',
+        ),
+        'subscript' => array(
+            'type' => 16,
+            'name' => 'sub',
+        ),
+        'superscript' => array(
+            'type' => 16,
+            'name' => 'sup',
+        ),
+        'a' => array(
+            'type' => 16,
+            'name' => 'url',
+        ),
+        'image' => array(
+            'type' => 16,
+            'name' => 'img',
+        ),
+        'li' => array(
+            'type' => 16,
+            'name' => '*',
+        ),
+        'color' => array(
+            'type' => 16,
+            'name' => 'span'
+        )
+    );
+
+    /**
+     * Element groups
+     *
+     * @var array
+     */
+    protected $_groups = array(
+        'block'        => array('block', 'inline', 'block_empty', 'inline_empty', 'list'),
+        'inline'       => array('inline', 'inline_empty'),
+        'list'         => array('list-item'),
+        'list-item'    => array('inline', 'inline_empty', 'list'),
+        'block_empty'  => array(),
+        'inline_empty' => array(),
+    );
+
+    /**
+     * The current group
+     *
+     * @var string
+     */
+    protected $_group = 'block';
+
+    /**
+     * Default attributes
+     *
+     * @var array
+     */
+    protected static $_defaultAttributes = array(
+        'id'    => '',
+        'class' => '',
+        'style' => '',
+        'lang'  => '',
+        'title' => ''
+    );
+
+
+    /**
+     * Execute a replace token
+     *
+     * @param  Zend_Markup_Token $token
+     * @param  array $tag
+     * @return string
+     */
+    protected function _executeReplace(Zend_Markup_Token $token, $tag)
+    {
+        if (isset($tag['tag'])) {
+            if (!isset($tag['attributes'])) {
+                $tag['attributes'] = array();
+            }
+            $attrs = self::_renderAttributes($token, $tag['attributes']);
+            return "<{$tag['tag']}{$attrs}>{$this->_render($token)}</{$tag['tag']}>";
+        }
+
+        return parent::_executeReplace($token, $tag);
+    }
+
+    /**
+     * Execute a single replace token
+     *
+     * @param  Zend_Markup_Token $token
+     * @param  array $tag
+     * @return string
+     */
+    protected function _executeSingleReplace(Zend_Markup_Token $token, $tag)
+    {
+        if (isset($tag['tag'])) {
+            if (!isset($tag['attributes'])) {
+                $tag['attributes'] = array();
+            }
+            $attrs = self::_renderAttributes($token, $tag['attributes']);
+            return "<{$tag['tag']}{$attrs} />";
+        }
+        return parent::_executeSingleReplace($token, $tag);
+    }
+
+    /**
+     * Render some attributes
+     *
+     * @param  Zend_Markup_Token $token
+     * @param  array $tag
+     * @return string
+     */
+    protected static function _renderAttributes(Zend_Markup_Token $token, array $attributes = array())
+    {
+        $attributes = array_merge(self::$_defaultAttributes, $attributes);
+
+        $return = '';
+
+        $tokenAttributes = $token->getAttributes();
+
+        // correct style attribute
+        if (isset($tokenAttributes['style'])) {
+            $tokenAttributes['style'] = trim($tokenAttributes['style']);
+
+            if ($tokenAttributes['style'][strlen($tokenAttributes['style']) - 1] != ';') {
+                $tokenAttributes['style'] .= ';';
+            }
+        } else {
+            $tokenAttributes['style'] = '';
+        }
+
+        // special treathment for 'align' and 'color' attribute
+        if (isset($tokenAttributes['align'])) {
+            $tokenAttributes['style'] .= 'text-align: ' . $tokenAttributes['align'] . ';';
+            unset($tokenAttributes['align']);
+        }
+        if (isset($tokenAttributes['color']) && self::_checkColor($tokenAttributes['color'])) {
+            $tokenAttributes['style'] .= 'color: ' . $tokenAttributes['color'] . ';';
+            unset($tokenAttributes['color']);
+        }
+
+        /*
+         * loop through all the available attributes, and check if there is
+         * a value defined by the token
+         * if there is no value defined by the token, use the default value or
+         * don't set the attribute
+         */
+        foreach ($attributes as $attribute => $value) {
+            if (isset($tokenAttributes[$attribute]) && !empty($tokenAttributes[$attribute])) {
+                $return .= ' ' . $attribute . '="' . htmlentities($tokenAttributes[$attribute], ENT_QUOTES) . '"';
+            } elseif (!empty($value)) {
+                $return .= ' ' . $attribute . '="' . htmlentities($value, ENT_QUOTES) . '"';
+            }
+        }
+
+        return $return;
+    }
+
+    /**
+     * Method for the URL tag
+     *
+     * @param  Zend_Markup_Token $token
+     * @param  string $text
+     * @return string
+     */
+    protected static function _htmlUrl(Zend_Markup_Token $token, $text)
+    {
+        if ($token->hasAttribute('url')) {
+            $url = $token->getAttribute('url');
+        } else {
+            $url = $text;
+        }
+
+        // check if the URL is valid
+        if (!Zend_Uri::check($url)) {
+            return $text;
+        }
+
+        $attributes = self::_renderAttributes($token);
+
+        return "<a href=\"{$url}\"{$attributes}>{$text}</a>";
+    }
+
+    /**
+     * Method for the img tag
+     *
+     * @param  Zend_Markup_Token $token
+     * @param  string $text
+     * @return string
+     */
+    protected static function _htmlImg(Zend_Markup_Token $token, $text)
+    {
+        $url = $text;
+
+        // check if the URL is valid
+        if (!Zend_Uri::check($url)) {
+            return $text;
+        }
+
+        if ($token->hasAttribute('alt')) {
+            $alt = $token->getAttribute('alt');
+        } else {
+            // try to get the alternative from the URL
+            $alt = rtrim($text, '/');
+            $alt = strrchr($alt, '/');
+            if (false !== strpos($alt, '.')) {
+                $alt = substr($alt, 1, strpos($alt, '.') - 1);
+            }
+        }
+
+        return "<img src=\"{$url}\" alt=\"{$alt}\"" . self::_renderAttributes($token) . " />";
+    }
+
+    /**
+     * Method for the list tag
+     *
+     * @param  Zend_Markup_Token $token
+     * @param  string $text
+     * @return void
+     */
+    protected static function _htmlList(Zend_Markup_Token $token, $text)
+    {
+        $type = null;
+        if ($token->hasAttribute('list')) {
+            // because '01' == '1'
+            if ($token->getAttribute('list') === '01') {
+                $type = 'decimal-leading-zero';
+            } else {
+                switch ($token->getAttribute('list')) {
+                    case '1':
+                        $type = 'decimal';
+                        break;
+                    case 'i':
+                        $type = 'lower-roman';
+                        break;
+                    case 'I':
+                        $type = 'upper-roman';
+                        break;
+                    case 'a':
+                        $type = 'lower-alpha';
+                        break;
+                    case 'A':
+                        $type = 'upper-alpha';
+                        break;
+
+                    // the following type is unsupported by IE (including IE8)
+                    case 'alpha':
+                        $type = 'lower-greek';
+                        break;
+
+                    // the CSS names itself
+                    case 'armenian': // unsupported by IE (including IE8)
+                    case 'decimal':
+                    case 'decimal-leading-zero': // unsupported by IE (including IE8)
+                    case 'georgian': // unsupported by IE (including IE8)
+                    case 'lower-alpha':
+                    case 'lower-greek': // unsupported by IE (including IE8)
+                    case 'lower-latin': // unsupported by IE (including IE8)
+                    case 'lower-roman':
+                    case 'upper-alpha':
+                    case 'upper-latin': // unsupported by IE (including IE8)
+                    case 'upper-roman':
+                        $type = $token->getAttribute('list');
+                        break;
+                }
+            }
+        }
+
+        if (null !== $type) {
+            return "<ol style=\"list-style-type: {$type}\">{$text}</ol>";
+        } else {
+            return "<ul>{$text}</ul>";
+        }
+    }
+
+    /**
+     * Method for the code tag
+     *
+     * @param  Zend_Markup_Token $token
+     * @param  string $text
+     * @return string
+     */
+    protected static function _htmlCode(Zend_Markup_Token $token, $text)
+    {
+        return highlight_string($text, true);
+    }
+
+    /**
+     * Check if a color is a valid HTML color
+     *
+     * @param string $color
+     *
+     * @return bool
+     */
+    protected static function _checkColor($color)
+    {
+        /*
+         * aqua, black, blue, fuchsia, gray, green, lime, maroon, navy, olive,
+         * purple, red, silver, teal, white, and yellow.
+         */
+        $colors = array(
+            'aqua', 'black', 'blue', 'fuchsia', 'gray', 'green', 'lime',
+            'maroon', 'navy', 'olive', 'purple', 'red', 'silver', 'teal',
+            'white', 'yellow'
+        );
+
+        if (in_array($color, $colors)) {
+            return true;
+        }
+
+        if (preg_match('/\#[0-9a-f]{6}/i', $color)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Filter method, used for converting newlines to <br /> tags
+     *
+     * @param  string $value
+     * @return string
+     */
+    protected function _filter($value)
+    {
+        if ($this->_filter) {
+            return nl2br(htmlentities($value, ENT_QUOTES));
+        }
+        return $value;
+    }
+}

+ 338 - 0
library/Zend/Markup/Renderer/RendererAbstract.php

@@ -0,0 +1,338 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Defines the basic rendering functionality
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Markup_Renderer_RendererAbstract
+{
+    const TAG_SINGLE    = 1;
+    const TAG_NORMAL    = 2;
+
+    const TYPE_CALLBACK = 4;
+    const TYPE_REPLACE  = 8;
+    const TYPE_ALIAS    = 16;
+
+    /**
+     * Tag info
+     *
+     * @var array
+     */
+    protected $_tags = array();
+
+    /**
+     * Parser
+     *
+     * @var Zend_Markup_Parser_ParserInterface
+     */
+    protected $_parser;
+
+    /**
+     * Use the filter or not
+     *
+     * @var bool
+     */
+    protected $_filter = true;
+
+    /**
+     * The current group
+     *
+     * @var string
+     */
+    protected $_group;
+
+    /**
+     * Groups definition
+     *
+     * @var array
+     */
+    protected $_groups = array();
+
+
+    /**
+     * Constructor
+     *
+     * @param  array $options
+     * @return void
+     */
+    public function __construct(array $options = array())
+    {
+        if (isset($options['parser'])) {
+            $this->setParser($options['parser']);
+        }
+    }
+
+    /**
+     * Set the parser
+     *
+     * @param  Zend_Markup_Parser_ParserInterface $parser
+     * @return Zend_Markup_Renderer_RendererAbstract
+     */
+    public function setParser(Zend_Markup_Parser_ParserInterface $parser)
+    {
+        $this->_parser = $parser;
+        return $this;
+    }
+
+    /**
+     * Get the parser
+     *
+     * @return Zend_Markup_Parser_ParserInterface
+     */
+    public function getParser()
+    {
+        return $this->_parser;
+    }
+
+    /**
+     * Add a new tag
+     *
+     * @param  string $name
+     * @param  string $type
+     * @param  array $info
+     * @return Zend_Markup_Renderer_RendererAbstract
+     */
+    public function addTag($name, $type, array $info)
+    {
+        if (!isset($info['group']) && ($type ^ self::TYPE_ALIAS)) {
+            require_once 'Zend/Markup/Renderer/Exception.php';
+            throw new Zend_Markup_Renderer_Exception("There is no render group defined.");
+        }
+
+        if (isset($info['filter'])) {
+            $filter = (boolean) $info['filter'];
+        } else {
+            $filter = true;
+        }
+
+        // check the type
+        if ($type & self::TYPE_CALLBACK) {
+            // add a callback tag
+            if (!isset($info['callback']) || !is_callable($info['callback'])) {
+                require_once 'Zend/Markup/Renderer/Exception.php';
+                throw new Zend_Markup_Renderer_Exception("No valid callback defined for tag '$name'.");
+            }
+
+            $info['type'] = $type;
+            $info['filter'] = $filter;
+
+            $this->_tags[$name] = $info;
+        } elseif ($type & self::TYPE_ALIAS) {
+            // add an alias
+            if (empty($info['name'])) {
+                require_once 'Zend/Markup/Renderer/Exception.php';
+                throw new Zend_Markup_Renderer_Exception(
+                        'No alias was provided but tag was defined as such');
+            }
+
+            $this->_tags[$name] = array(
+                'type' => self::TYPE_ALIAS,
+                'name' => $info['name']
+            );
+        } else {
+            if ($type & self::TAG_SINGLE) {
+                // add a single replace tag
+                $info['type']   = $type;
+                $info['filter'] = $filter;
+
+                $this->_tags[$name] = $info;
+            } else {
+                // add a replace tag
+                $info['type']   = $type;
+                $info['filter'] = $filter;
+
+                $this->_tags[$name] = $info;
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * Render function
+     *
+     * @param  Zend_Markup_TokenList|string $tokenList
+     * @return string
+     */
+    public function render($value)
+    {
+        if ($value instanceof Zend_Markup_TokenList) {
+            $tokenList = $value;
+        } else {
+            $tokenList = $this->getParser()->parse($value);
+        }
+
+        $root = $tokenList->current();
+
+        return $this->_render($root);
+    }
+
+    /**
+     * Render a single token
+     *
+     * @param  Zend_Markup_Token $token
+     * @return string
+     */
+    protected function _render(Zend_Markup_Token $token)
+    {
+        $return    = '';
+        $oldFilter = $this->_filter;
+        $oldGroup  = $this->_group;
+
+        // check filter and group usage in this tag
+        if (isset($this->_tags[$token->getName()])) {
+            if (isset($this->_tags[$token->getName()]['filter'])
+                && $this->_tags[$token->getName()]['filter']
+            ) {
+                $this->_filter = true;
+            } else {
+                $this->_filter = false;
+            }
+
+            if ($group = $this->_getGroup($token)) {
+                $this->_group = $group;
+            }
+        }
+
+        // if this tag has children, execute them
+        if ($token->hasChildren()) {
+            foreach ($token->getChildren() as $child) {
+                $return .= $this->_execute($child);
+            }
+        }
+
+        $this->_filter = $oldFilter;
+        $this->_group  = $oldGroup;
+
+        return $return;
+    }
+
+    /**
+     * Get the group of a token
+     *
+     * @param  Zend_Markup_Token $token
+     * @return string|bool
+     */
+    protected function _getGroup(Zend_Markup_Token $token)
+    {
+        if (!isset($this->_tags[$token->getName()])) {
+            return false;
+        }
+
+        $tag = $this->_tags[$token->getName()];
+
+        // alias processing
+        while ($tag['type'] & self::TYPE_ALIAS) {
+            $tag = $this->_tags[$tag['name']];
+        }
+
+        return isset($tag['group']) ? $tag['group'] : false;
+    }
+
+    /**
+     * Execute the token
+     *
+     * @param  Zend_Markup_Token $token
+     * @return string
+     */
+    protected function _execute(Zend_Markup_Token $token)
+    {
+        // first return the normal text tags
+        if ($token->getType() == Zend_Markup_Token::TYPE_NONE) {
+            return $this->_filter($token->getTag());
+        }
+
+        // if the token doesn't have a notation, return the plain text
+        if (!isset($this->_tags[$token->getName()])) {
+            return $this->_filter($token->getTag()) . $this->_render($token) . $token->getStopper();
+        }
+
+        $tag = $this->_tags[$token->getName()];
+
+        // alias processing
+        while ($tag['type'] & self::TYPE_ALIAS) {
+            $tag = $this->_tags[$tag['name']];
+        }
+
+        // check if the tag has content
+        if (($tag['type'] & self::TAG_NORMAL) && !$token->hasChildren()) {
+            return '';
+        }
+
+        // check for the context
+        if (!in_array($tag['group'], $this->_groups[$this->_group])) {
+            return $this->_filter($token->getTag()) . $this->_render($token) . $token->getStopper();
+        }
+
+        // callback
+        if ($tag['type'] & self::TYPE_CALLBACK) {
+            if ($tag['type'] & self::TAG_NORMAL) {
+                return call_user_func_array($tag['callback'], array($token, $this->_render($token)));
+            }
+            return call_user_func_array($tag['callback'], array($token));
+        }
+        // replace
+        if ($tag['type'] & self::TAG_NORMAL) {
+            return $this->_executeReplace($token, $tag);
+        }
+        return $this->_executeSingleReplace($token, $tag);
+    }
+
+    // methods that will probably be interhited by subclasses
+
+    /**
+     * Execute a replace token
+     *
+     * @param  Zend_Markup_Token $token
+     * @param  array $tag
+     * @return string
+     */
+    protected function _executeReplace(Zend_Markup_Token $token, $tag)
+    {
+        return $tag['start'] . $this->_render($token) . $tag['end'];
+    }
+
+    /**
+     * Execute a single replace token
+     *
+     * @param  Zend_Markup_Token $token
+     * @param  array $tag
+     * @return string
+     */
+    protected function _executeSingleReplace(Zend_Markup_Token $token, $tag)
+    {
+        return $tag['replace'];
+    }
+
+    /**
+     * Abstract filter method
+     *
+     * @param  string $value
+     * @return string
+     */
+    abstract protected function _filter($value);
+}

+ 294 - 0
library/Zend/Markup/Token.php

@@ -0,0 +1,294 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Parser
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Markup_TokenList
+ */
+require_once 'Zend/Markup/TokenList.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Markup
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Token
+{
+    const TYPE_NONE    = 'none';
+    const TYPE_TAG     = 'tag';
+
+    /**
+     * Children of this token
+     *
+     * @var Zend_Markup_TokenList
+     */
+    protected $_children;
+
+    /**
+     * The complete tag
+     *
+     * @var string
+     */
+    protected $_tag;
+
+    /**
+     * The tag's type
+     *
+     * @var string
+     */
+    protected $_type;
+
+    /**
+     * Tag name
+     *
+     * @var string
+     */
+    protected $_name = '';
+
+    /**
+     * Tag attributes
+     *
+     * @var array
+     */
+    protected $_attributes = array();
+
+    /**
+     * The used tag stopper (empty when none is found)
+     *
+     * @var string
+     */
+    protected $_stopper = '';
+
+    /**
+     * The parent token
+     *
+     * @var Zend_Markup_Token
+     */
+    protected $_parent;
+
+
+    /**
+     * Construct the token
+     *
+     * @param  string $tag
+     * @param  string $type
+     * @param  string $name
+     * @param  array $attributes
+     * @param  Zend_Markup_Token $parent
+     * @return void
+     */
+    public function __construct(
+        $tag, 
+        $type, 
+        $name = '', 
+        array $attributes = array(), 
+        Zend_Markup_Token $parent = null
+    ) {
+        $this->_tag        = $tag;
+        $this->_type       = $type;
+        $this->_name       = $name;
+        $this->_attributes = $attributes;
+        $this->_parent     = $parent;
+    }
+
+    // accessors
+
+    /**
+     * Set the stopper
+     *
+     * @param string $stopper
+     * @return Zend_Markup_Token
+     */
+    public function setStopper($stopper)
+    {
+        $this->_stopper = $stopper;
+
+        return $this;
+    }
+
+    /**
+     * Get the stopper
+     *
+     * @return string
+     */
+    public function getStopper()
+    {
+        return $this->_stopper;
+    }
+
+    /**
+     * Get the token's name
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->_name;
+    }
+
+    /**
+     * Get the token's type
+     *
+     * @return string
+     */
+    public function getType()
+    {
+        return $this->_type;
+    }
+
+    /**
+     * Get the complete tag
+     *
+     * @return string
+     */
+    public function getTag()
+    {
+        return $this->_tag;
+    }
+
+    /**
+     * Get an attribute
+     *
+     * @param string $name
+     *
+     * @return string
+     */
+    public function getAttribute($name)
+    {
+        return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
+    }
+
+    /**
+     * Check if the token has an attribute
+     *
+     * @param string $name
+     *
+     * @return bool
+     */
+    public function hasAttribute($name)
+    {
+        return isset($this->_attributes[$name]);
+    }
+
+    /**
+     * Get all the attributes
+     *
+     * @return array
+     */
+    public function getAttributes()
+    {
+        return $this->_attributes;
+    }
+
+    /**
+     * Check if an attribute is empty
+     *
+     * @param string $name
+     *
+     * @return bool
+     */
+    public function attributeIsEmpty($name)
+    {
+        return empty($this->_attributes[$name]);
+    }
+
+    // functions for child/parent tokens
+
+    /**
+     * Add a child token
+     *
+     * @return void
+     */
+    public function addChild(Zend_Markup_Token $child)
+    {
+        $this->getChildren()->addChild($child);
+    }
+
+    /**
+     * Set the children token list
+     *
+     * @param  Zend_Markup_TokenList $children
+     * @return Zend_Markup_Token
+     */
+    public function setChildren(Zend_Markup_TokenList $children)
+    {
+        $this->_children = $children;
+        return $this;
+    }
+
+    /**
+     * Get the children for this token
+     *
+     * @return Zend_Markup_TokenList
+     */
+    public function getChildren()
+    {
+        if (null === $this->_children) {
+            $this->setChildren(new Zend_Markup_TokenList());
+        }
+        return $this->_children;
+    }
+
+	/**
+     * Does this token have any children
+     *
+     * @return bool
+     */
+    public function hasChildren()
+    {
+        return !empty($this->_children);
+    }
+
+    /**
+     * Get the parent token (if any)
+     *
+     * @return Zend_Markup_Token
+     */
+    public function getParent()
+    {
+        return $this->_parent;
+    }
+
+    /**
+     * Set a parent token
+     *
+     * @param  Zend_Markup_Token $parent
+     * @return Zend_Markup_Token
+     */
+    public function setParent(Zend_Markup_Token $parent)
+    {
+        $this->_parent = $parent;
+        return $this;
+    }
+
+    /**
+     * Magic clone function
+     *
+     * @return void
+     */
+    public function __clone()
+    {
+        $this->_parent   = null;
+        $this->_children = null;
+        $this->_tag      = '';
+    }
+}

+ 124 - 0
library/Zend/Markup/TokenList.php

@@ -0,0 +1,124 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Markup_Token
+ */
+require_once 'Zend/Markup/Token.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Markup
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_TokenList implements RecursiveIterator
+{
+
+    /**
+     * Array of tokens
+     *
+     * @var array
+     */
+    protected $_tokens = array();
+
+    /**
+     * Get the current token
+     *
+     * @return Zend_Markup_Token
+     */
+    public function current()
+    {
+        return current($this->_tokens);
+    }
+
+    /**
+     * Get the children of the current token
+     *
+     * @return Zend_Markup_TokenList
+     */
+    public function getChildren()
+    {
+        return current($this->_tokens)->getChildren();
+    }
+
+    /**
+     * Add a new child token
+     *
+     * @param Zend_Markup_Token $child
+     *
+     * @return void
+     */
+    public function addChild(Zend_Markup_Token $child)
+    {
+        $this->_tokens[] = $child;
+    }
+
+    /**
+     * Check if the current token has children
+     *
+     * @return bool
+     */
+    public function hasChildren()
+    {
+        return current($this->_tokens)->hasChildren();
+    }
+
+    /**
+     * Get the key of the current token
+     *
+     * @return int
+     */
+    public function key()
+    {
+        return key($this->_tokens);
+    }
+
+    /**
+     * Go to the next token
+     *
+     * @return Zend_Markup_Token
+     */
+    public function next()
+    {
+        return next($this->_tokens);
+    }
+
+    /**
+     * Rewind the iterator
+     *
+     * @return void
+     */
+    public function rewind()
+    {
+        reset($this->_tokens);
+    }
+
+    /**
+     * Check if the element is valid
+     *
+     * @return void
+     */
+    public function valid()
+    {
+        return $this->current() !== false;
+    }
+}

+ 45 - 0
tests/Zend/Markup/AllTests.php

@@ -0,0 +1,45 @@
+<?php
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Markup_AllTests::main');
+}
+
+require_once dirname(__FILE__) . '/../../TestHelper.php';
+
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+require_once 'Zend/Markup/BbcodeAndHtmlTest.php';
+require_once 'Zend/Markup/TextileAndHtmlTest.php';
+require_once 'Zend/Markup/ParserIntegrityTest.php';
+require_once 'Zend/Markup/FactoryTest.php';
+
+
+class Zend_Markup_AllTests
+{
+    public static function main()
+    {
+        $parameters = array();
+
+        if (TESTS_GENERATE_REPORT && extension_loaded('xdebug')) {
+            $parameters['reportDirectory'] = TESTS_GENERATE_REPORT_TARGET;
+        }
+
+        PHPUnit_TextUI_TestRunner::run(self::suite(), $parameters);
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Zend Framework - Zend_Markup');
+
+        $suite->addTestSuite('Zend_Markup_BbcodeAndHtmlTest');
+        $suite->addTestSuite('Zend_Markup_TextileAndHtmlTest');
+        $suite->addTestSuite('Zend_Markup_ParserIntegrityTest');
+        $suite->addTestSuite('Zend_Markup_FactoryTest');
+
+        return $suite;
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_Markup_AllTests::main') {
+    Zend_Markup_AllTests::main();
+}

+ 339 - 0
tests/Zend/Markup/BbcodeAndHtmlTest.php

@@ -0,0 +1,339 @@
+<?php
+// Call Zend_Markup_BbcodeAndHtmlTest::main()
+// if this source file is executed directly.
+if (!defined("PHPUnit_MAIN_METHOD")) {
+    define("PHPUnit_MAIN_METHOD", "Zend_Markup_BbcodeAndHtmlTest::main");
+}
+
+require_once dirname(__FILE__) . '/../../TestHelper.php';
+
+require_once 'Zend/Markup.php';
+
+/**
+ * Test class for Zend_Json_Server
+ */
+class Zend_Markup_BbcodeAndHtmlTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Zend_Markup_Renderer_RendererAbstract instance
+     *
+     * @var Zend_Markup_Renderer_RendererAbstract
+     */
+    protected $_markup;
+
+
+    /**
+     * Runs the test methods of this class.
+     *
+     * @return void
+     */
+    public static function main()
+    {
+        require_once "PHPUnit/TextUI/TestRunner.php";
+
+        $suite  = new PHPUnit_Framework_TestSuite("Zend_Markup_MarkupTest");
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    /**
+     * Sets up the fixture
+     * This method is called before a test is executed.
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        $this->_markup = Zend_Markup::factory('bbcode', 'html');
+    }
+
+    /**
+     * Tears down the fixture
+     * This method is called after a test is executed.
+     *
+     * @return void
+     */
+    public function tearDown()
+    {
+        unset($this->_markup);
+    }
+
+    /**
+     * Test for basic tags
+     *
+     * @return void
+     */
+    public function testBasicTags()
+    {
+        $this->assertEquals('<strong>foo</strong>bar', $this->_markup->render('[b]foo[/b]bar'));
+        $this->assertEquals('<strong>foo<em>bar</em>foo</strong>ba[r',
+            $this->_markup->render('[b=test file="test"]foo[i hell=nice]bar[/i]foo[/b]ba[r'));
+    }
+
+    /**
+     * Test the behaviour of complicated tags
+     *
+     * @return void
+     */
+    public function testComplicatedTags()
+    {
+        $this->assertEquals('<a href="http://framework.zend.com/">http://framework.zend.com/</a>',
+            $this->_markup->render('[url]http://framework.zend.com/[/url]'));
+        $this->assertEquals('<a href="http://framework.zend.com/">foo</a>',
+            $this->_markup->render('[url=http://framework.zend.com/]foo[/url]'));
+        $this->assertEquals('bar', $this->_markup->render('[url="invalid"]bar[/url]'));
+
+        $this->assertEquals('<img src="http://framework.zend.com/images/logo.png" alt="logo" />',
+            $this->_markup->render('[img]http://framework.zend.com/images/logo.png[/img]'));
+        $this->assertEquals('<img src="http://framework.zend.com/images/logo.png" alt="Zend Framework" />',
+            $this->_markup->render('[img alt="Zend Framework"]http://framework.zend.com/images/logo.png[/img]'));
+        $this->assertEquals('invalid', $this->_markup->render('[img]invalid[/img]'));
+
+    }
+
+    /**
+     * Test input exceptions
+     *
+     * @return void
+     */
+    public function testExceptionParserWrongInputType()
+    {
+        $this->setExpectedException('Zend_Markup_Parser_Exception');
+
+        $this->_markup->getParser()->parse(array());
+    }
+
+    /**
+     * Test exception
+     *
+     * @return void
+     */
+    public function testExceptionParserEmptyInput()
+    {
+        $this->setExpectedException('Zend_Markup_Parser_Exception');
+
+        $this->_markup->getParser()->parse('');
+    }
+
+    /**
+     * Test adding tags
+     *
+     * @return void
+     */
+    public function testAddTags()
+    {
+        $this->_markup->addTag('bar',
+            Zend_Markup_Renderer_RendererAbstract::TYPE_CALLBACK | Zend_Markup_Renderer_RendererAbstract::TAG_NORMAL,
+            array('callback' => 'markupTestCallback', 'group' => 'inline'));
+        $this->_markup->addTag('suppp',
+            Zend_Markup_Renderer_RendererAbstract::TYPE_REPLACE | Zend_Markup_Renderer_RendererAbstract::TAG_NORMAL,
+            array('start' => '<sup>', 'end' => '</sup>', 'group' => 'inline'));
+        $this->_markup->addTag('zend',
+            Zend_Markup_Renderer_RendererAbstract::TYPE_REPLACE | Zend_Markup_Renderer_RendererAbstract::TAG_SINGLE,
+            array('replace' => 'Zend Framework', 'group' => 'inline'));
+        $this->_markup->addTag('line', Zend_Markup_Renderer_RendererAbstract::TYPE_ALIAS,
+            array('name' => 'hr'));
+
+        $this->assertEquals('[foo=blaat]hell<sup>test</sup>blaat[/foo]',
+            $this->_markup->render('[bar="blaat"]hell[suppp]test[/suppp]blaat[/]'));
+
+        $this->assertEquals('Zend Framework', $this->_markup->render('[zend]'));
+        $this->assertEquals('<hr />', $this->_markup->render('[line]'));
+
+        $this->assertEquals('<sup>test aap</sup>test',
+            $this->_markup->render('[suppp]test aap[/suppp]test'));
+    }
+
+    public function testHtmlUrlTitleIsRenderedCorrectly() {
+        $this->assertEquals('<a href="http://exampl.com" title="foo">test</a>',
+            $this->_markup->render('[url=http://exampl.com title=foo]test[/url]'));
+    }
+
+    public function testValueLessAttributeDoesNotThrowNotice() {
+        // Notice: Uninitialized string offset: 42
+        // in Zend/Markup/Parser/Bbcode.php on line 316
+        $expected = '<a href="http://example.com">Example</a>';
+        $value    = '[url=http://example.com foo]Example[/url]';
+        $this->assertEquals($expected, $this->_markup->render($value));
+    }
+
+    public function testAttributeNotEndingValueDoesNotThrowNotice()
+    {
+        // Notice: Uninitialized string offset: 13
+        // in Zend/Markup/Parser/Bbcode.php on line 337
+
+        $this->_markup->render('[url=http://framework.zend.com/ title="');
+    }
+
+    public function testAttributeFollowingValueDoesNotThrowNotice()
+    {
+        // Notice: Uninitialized string offset: 38
+        // in Zend/Markup/Parser/Bbcode.php on line 337
+
+        $this->_markup->render('[url="http://framework.zend.com/"title');
+    }
+
+    public function testHrTagWorks() {
+        $this->assertEquals('foo<hr />bar', $this->_markup->render('foo[hr]bar'));
+    }
+
+    public function testFunkyCombos() {
+        $expected = '<span style="text-decoration: underline;">a[/b][hr]b'
+                  . '<strong>c</strong></span><strong>d</strong>[/u]e';
+        $outcome = $this->_markup->render('[u]a[/b][hr]b[b]c[/u]d[/b][/u]e');
+        $this->assertEquals($expected, $outcome);
+    }
+
+    public function testImgSrcsConstraints() {
+        $this->assertEquals('F/\!ZLrFz',$this->_markup->render('F[img]/\!ZLrFz[/img]'));
+    }
+
+    public function testColorConstraintsAndJs() {
+        $input = "<kokx> i think you mean? [color=\"onclick='foobar();'\"]your text[/color] DASPRiD";
+        $expected = "&lt;kokx&gt; i think you mean? <span>your text</span> DASPRiD";
+        $this->assertEquals($expected, $this->_markup->render($input));
+    }
+
+    public function testNeverEndingAttribute() {
+        $input = "[color=\"green]your text[/color]";
+        $expected = '[color=&quot;green]your text[/color]';
+        $this->assertEquals($expected, $this->_markup->render($input));
+    }
+
+    public function testTreatmentNonTags() {
+        $input = '[span][acronym][h1][h2][h3][h4][h5][h6][nothing]'
+               . '[/h6][/h5][/h4][/h3][/h2][/h1][/acronym][/span]';
+        $expected = '<span><acronym><h1><h2><h3><h4><h5><h6>[nothing]'
+                  . '</h6></h5></h4></h3></h2></h1></acronym></span>';
+        $this->assertEquals($expected, $this->_markup->render($input));
+    }
+
+    public function testListItems() {
+        $input = "[list][*]Foo*bar (item 1)\n[*]Item 2\n[*]Trimmed (Item 3)\n[/list]";
+        $expected = "<ul><li>Foo*bar (item 1)</li><li>Item 2</li><li>Trimmed (Item 3)</li></ul>";
+        $this->assertEquals($expected, $this->_markup->render($input));
+    }
+
+    public function testListTypes()
+    {
+        $types = array(
+            '01'    => 'decimal-leading-zero',
+            '1'     => 'decimal',
+            'i'     => 'lower-roman',
+            'I'     => 'upper-roman',
+            'a'     => 'lower-alpha',
+            'A'     => 'upper-alpha',
+            'alpha' => 'lower-greek'
+        );
+
+        foreach ($types as $type => $style) {
+            $input    = "[list={$type}][*]Foobar\n[*]Zend\n[/list]";
+            $expected = "<ol style=\"list-style-type: {$style}\"><li>Foobar</li><li>Zend</li></ol>";
+            $this->assertEquals($expected, $this->_markup->render($input));
+        }
+    }
+
+    public function testHtmlTags() {
+        $m = $this->_markup;
+
+        $this->assertEquals('<strong>foo</strong>', $m->render('[b]foo[/b]'));
+        $this->assertEquals('<span style="text-decoration: underline;">foo</span>',
+                            $m->render('[u]foo[/u]'));
+        $this->assertEquals('<em>foo</em>', $m->render('[i]foo[/i]'));
+        $this->assertEquals('<cite>foo</cite>', $m->render('[cite]foo[/cite]'));
+        $this->assertEquals('<del>foo</del>', $m->render('[del]foo[/del]'));
+        $this->assertEquals('<ins>foo</ins>', $m->render('[ins]foo[/ins]'));
+        $this->assertEquals('<sub>foo</sub>', $m->render('[sub]foo[/sub]'));
+        $this->assertEquals('<span>foo</span>', $m->render('[span]foo[/span]'));
+        $this->assertEquals('<acronym>foo</acronym>', $m->render('[acronym]foo[/acronym]'));
+        $this->assertEquals('<h1>F</h1>', $m->render('[h1]F[/h1]'));
+        $this->assertEquals('<h2>R</h2>', $m->render('[h2]R[/h2]'));
+        $this->assertEquals('<h3>E</h3>', $m->render('[h3]E[/h3]'));
+        $this->assertEquals('<h4>E</h4>', $m->render('[h4]E[/h4]'));
+        $this->assertEquals('<h5>A</h5>', $m->render('[h5]A[/h5]'));
+        $this->assertEquals('<h6>Q</h6>', $m->render('[h6]Q[/h6]'));
+        $this->assertEquals('<span style="color: red;">foo</span>', $m->render('[color=red]foo[/color]'));
+        $this->assertEquals('<span style="color: #00FF00;">foo</span>', $m->render('[color=#00FF00]foo[/color]'));
+
+        $expected = '<code><span style="color: #000000">' . "\n"
+                  . '<span style="color: #0000BB">&lt;?php<br /></span>'
+                  . "<span style=\"color: #007700\">exit;</span>\n</span>\n</code>";
+
+        $this->assertEquals($expected, $m->render("[code]<?php\nexit;[/code]"));
+        $this->assertEquals('<p>I</p>', $m->render('[p]I[/p]'));
+        $this->assertEquals('N',
+                $m->render('[ignore]N[/ignore]'));
+        $this->assertEquals('<blockquote>M</blockquote>', $m->render('[quote]M[/quote]'));
+
+        $this->assertEquals('<hr />foo<hr />bar[/hr]', $m->render('[hr]foo[hr]bar[/hr]'));
+    }
+
+    public function testWrongNesting()
+    {
+        $this->assertEquals('<strong>foo<em>bar</em></strong>',
+                                $this->_markup->render('[b]foo[i]bar[/b][/i]'));
+        $this->assertEquals('<strong>foo<em>bar</em></strong><em>kokx</em>',
+                                $this->_markup->render('[b]foo[i]bar[/b]kokx[/i]'));
+    }
+
+    public function testHtmlAliases() {
+        $m = $this->_markup;
+
+        $this->assertEquals($m->render('[b]F[/b]'), $m->render('[bold]F[/bold]'));
+        $this->assertEquals($m->render('[bold]R[/bold]'), $m->render('[strong]R[/strong]'));
+        $this->assertEquals($m->render('[i]E[/i]'), $m->render('[i]E[/i]'));
+        $this->assertEquals($m->render('[i]E[/i]'), $m->render('[italic]E[/italic]'));
+        $this->assertEquals($m->render('[i]A[/i]'), $m->render('[emphasized]A[/emphasized]'));
+        $this->assertEquals($m->render('[i]Q[/i]'), $m->render('[em]Q[/em]'));
+        $this->assertEquals($m->render('[u]I[/u]'), $m->render('[underline]I[/underline]'));
+        $this->assertEquals($m->render('[cite]N[/cite]'), $m->render('[citation]N[/citation]'));
+        $this->assertEquals($m->render('[del]G[/del]'), $m->render('[deleted]G[/deleted]'));
+        $this->assertEquals($m->render('[ins]M[/ins]'), $m->render('[insert]M[/insert]'));
+        $this->assertEquals($m->render('[s]E[/s]'),$m->render('[strike]E[/strike]'));
+        $this->assertEquals($m->render('[sub]-[/sub]'), $m->render('[subscript]-[/subscript]'));
+        $this->assertEquals($m->render('[sup]D[/sup]'), $m->render('[superscript]D[/superscript]'));
+        $this->assertEquals($m->render('[url]google.com[/url]'), $m->render('[a]google.com[/a]'));
+        $this->assertEquals($m->render('[img]http://google.com/favicon.ico[/img]'),
+                            $m->render('[image]http://google.com/favicon.ico[/image]'));
+    }
+
+    public function testEmptyTagName()
+    {
+        $this->assertEquals('[]', $this->_markup->render('[]'));
+    }
+
+    public function testStyleAlignCombination()
+    {
+        $m = $this->_markup;
+        $this->assertEquals('<h1 style="color: green;text-align: left;">Foobar</h1>',
+                            $m->render('[h1 style="color: green" align=left]Foobar[/h1]'));
+        $this->assertEquals('<h1 style="color: green;text-align: center;">Foobar</h1>',
+                            $m->render('[h1 style="color: green;" align=center]Foobar[/h1]'));
+    }
+
+    public function testXssInAttributeValues()
+    {
+        $m = $this->_markup;
+        $this->assertEquals('<strong class="&quot;&gt;xss">foobar</strong>',
+                            $m->render('[b class=\'">xss\']foobar[/b]'));
+    }
+
+}
+
+
+function markupTestCallback($token, $text)
+{
+    $bar = $token->getAttribute('bar');
+
+    if (!empty($bar)) {
+        $bar = '=' . $bar;
+    }
+
+    return "[foo{$bar}]" . $text . '[/foo]';
+}
+
+// Call Zend_Markup_BbcodeAndHtmlTest::main()
+// if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Markup_BbcodeAndHtmlTest::main") {
+    Zend_Markup_BbcodeAndHtmlTest::main();
+}

+ 30 - 0
tests/Zend/Markup/FactoryTest.php

@@ -0,0 +1,30 @@
+<?php
+// Call Zend_Json_ServerTest::main() if this source file is executed directly.
+if (!defined("PHPUnit_MAIN_METHOD")) {
+    define("PHPUnit_MAIN_METHOD", "Zend_Markup_FactoryTest::main");
+}
+
+require_once dirname(__FILE__) . '/../../TestHelper.php';
+
+require_once 'Zend/Markup.php';
+
+/**
+ * Test class for Zend_Markup
+ */
+class Zend_Markup_FactoryTest extends PHPUnit_Framework_TestCase
+{
+
+    public function testFactory()
+    {
+        Zend_Markup::addParserPath('Zend_Markup_Test_Parser', 'Zend/Markup/Test/Parser');
+        Zend_Markup::addRendererPath('Zend_Markup_Test_Renderer', 'Zend/Markup/Test/Renderer');
+
+        Zend_Markup::factory('MockParser', 'MockRenderer');
+    }
+
+}
+
+// Call Zend_Markup_BbcodeTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Markup_FactoryTest::main") {
+    Zend_Markup_BbcodeTest::main();
+}

+ 62 - 0
tests/Zend/Markup/ParserIntegrityTest.php

@@ -0,0 +1,62 @@
+<?php
+// Call Zend_Json_ServerTest::main() if this source file is executed directly.
+if (!defined("PHPUnit_MAIN_METHOD")) {
+    define("PHPUnit_MAIN_METHOD", "Zend_Markup_ParserIntegrityTest::main");
+}
+
+require_once dirname(__FILE__) . '/../../TestHelper.php';
+
+require_once 'Zend/Markup.php';
+
+/**
+ * Test class for Zend_Markup
+ */
+class Zend_Markup_ParserIntegrityTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Runs the test methods of this class.
+     *
+     * @return void
+     */
+    public static function main()
+    {
+        require_once "PHPUnit/TextUI/TestRunner.php";
+
+        $suite  = new PHPUnit_Framework_TestSuite("Zend_Markup_MarkupTest");
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    public function testBbcodeParser()
+    {
+        $parser = Zend_Markup::factory('bbcode')->getParser();
+
+        $value  = '[b][s][i]foobar[/i][/s][/b]';
+        $output = '';
+
+        $tree = $parser->parse($value);
+
+        // iterate trough the tree and check if we can generate the original value
+        $iterator = new RecursiveIteratorIterator($tree, RecursiveIteratorIterator::SELF_FIRST);
+
+        foreach ($iterator as $token) {
+            $output .= $token->getTag();
+
+            if ($token->getStopper() != '') {
+                $token->addChild(new Zend_Markup_Token(
+                    $token->getStopper(),
+                    Zend_Markup_Token::TYPE_NONE,
+                    '', array(), $token)
+                );
+            }
+        }
+
+        $this->assertEquals($value, $output);
+    }
+
+}
+
+// Call Zend_Markup_BbcodeTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Markup_ParserIntegrityTest::main") {
+    Zend_Markup_BbcodeTest::main();
+}

+ 88 - 0
tests/Zend/Markup/Test/Parser/MockParser.php

@@ -0,0 +1,88 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Parser
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Markup_TokenList
+ */
+require_once 'Zend/Markup/TokenList.php';
+
+/**
+ * @see Zend_Markup_Parser_ParserInterface
+ */
+require_once 'Zend/Markup/Parser/ParserInterface.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Parser
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Test_Parser_MockParser implements Zend_Markup_Parser_ParserInterface
+{
+
+    /**
+     * Parse a string
+     *
+     * @param  string $value
+     * @return Zend_Markup_TokenList
+     */
+    public function parse($value)
+    {
+        if (!is_string($value)) {
+            /**
+             * @see Zend_Markup_Parser_Exception
+             */
+            require_once 'Zend/Markup/Parser/Exception.php';
+            throw new Zend_Markup_Parser_Exception('Value to parse should be a string.');
+        }
+
+        if (empty($value)) {
+            /**
+             * @see Zend_Markup_Parser_Exception
+             */
+            require_once 'Zend/Markup/Parser/Exception.php';
+            throw new Zend_Markup_Parser_Exception('Value to parse cannot be left empty.');
+        }
+
+        // initialize variables
+        $tree    = new Zend_Markup_TokenList();
+        $current = new Zend_Markup_Token(
+            '',
+            Zend_Markup_Token::TYPE_NONE,
+            'Zend_Markup_Root'
+        );
+
+        $tree->addChild($current);
+
+        $token = new Zend_Markup_Token(
+            $value,
+            Zend_Markup_Token::TYPE_NONE,
+            '',
+            array(),
+            $current
+        );
+        $current->addChild($token);
+
+        return $tree;
+    }
+}

+ 49 - 0
tests/Zend/Markup/Test/Renderer/MockRenderer.php

@@ -0,0 +1,49 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Uri
+ */
+require_once 'Zend/Uri.php';
+
+/**
+ * @see Zend_Markup_Renderer_RendererAbstract
+ */
+require_once 'Zend/Markup/Renderer/RendererAbstract.php';
+
+/**
+ * HTML renderer
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Test_Renderer_MockRenderer extends Zend_Markup_Renderer_RendererAbstract
+{
+
+    public function _filter($text)
+    {
+        return $text;
+    }
+}

+ 160 - 0
tests/Zend/Markup/TextileAndHtmlTest.php

@@ -0,0 +1,160 @@
+<?php
+// Call Zend_Markup_TextileAndHtmlTest::main()
+// if this source file is executed directly.
+if (!defined("PHPUnit_MAIN_METHOD")) {
+    define("PHPUnit_MAIN_METHOD", "Zend_Markup_TextileAndHtmlTest::main");
+}
+
+require_once dirname(__FILE__) . '/../../TestHelper.php';
+
+require_once 'Zend/Markup.php';
+
+/**
+ * Test class for Zend_Markup_Renderer_Html and Zend_Markup_Parser_Textile
+ */
+class Zend_Markup_TextileAndHtmlTest extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Zend_Markup_Renderer_RendererAbstract instance
+     *
+     * @var Zend_Markup_Renderer_RendererAbstract
+     */
+    protected $_markup;
+
+
+    /**
+     * Runs the test methods of this class.
+     *
+     * @return void
+     */
+    public static function main()
+    {
+        require_once "PHPUnit/TextUI/TestRunner.php";
+
+        $suite  = new PHPUnit_Framework_TestSuite("Zend_Markup_MarkupTest");
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    /**
+     * Sets up the fixture
+     * This method is called before a test is executed.
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        $this->_markup = Zend_Markup::factory('Textile', 'html');
+    }
+
+    /**
+     * Tears down the fixture
+     * This method is called after a test is executed.
+     *
+     * @return void
+     */
+    public function tearDown()
+    {
+        unset($this->_markup);
+    }
+
+
+    public function testHtmlTags()
+    {
+    	$m = $this->_markup;
+
+        $this->assertEquals('<p><strong>foo</strong></p>', $m->render('*foo*'));
+        $this->assertEquals('<p><strong>foo</strong></p>', $m->render('**foo**'));
+        $this->assertEquals('<p><em>foo</em></p>', $m->render('_foo_'));
+        $this->assertEquals('<p><em>foo</em></p>', $m->render('__foo__'));
+        $this->assertEquals('<p><cite>foo</cite></p>', $m->render('??foo??'));
+        $this->assertEquals('<p><del>foo</del></p>', $m->render('-foo-'));
+        $this->assertEquals('<p><ins>foo</ins></p>', $m->render('+foo+'));
+        $this->assertEquals('<p><sup>foo</sup></p>', $m->render('^foo^'));
+        $this->assertEquals('<p><sub>foo</sub></p>', $m->render('~foo~'));
+        $this->assertEquals('<p><span>foo</span></p>', $m->render('%foo%'));
+        $this->assertEquals('<p><acronym title="Teh Zend Framework">TZF</acronym></p>',
+                            $m->render('TZF(Teh Zend Framework)'));
+        $this->assertEquals('<p><a href="http://framework.zend.com/">Zend Framework</a></p>',
+                            $m->render('"Zend Framework":http://framework.zend.com/'));
+        $this->assertEquals('<p><h1>foobar</h1></p>',
+                            $m->render('h1. foobar'));
+        $this->assertEquals('<p><img src="http://framework.zend.com/images/logo.gif" alt="logo" /></p>',
+                            $m->render('!http://framework.zend.com/images/logo.gif!'));
+
+        $value    = "# Zend Framework\n# Unit Tests";
+        $expected = '<p><ol style="list-style-type: decimal"><li>Zend Framework</li><li>Unit Tests</li></ol></p>';
+        $this->assertEquals($expected, $m->render($value));
+
+        $value    = "* Zend Framework\n* Foo Bar";
+        $expected = '<p><ul><li>Zend Framework</li><li>Foo Bar</li></ul></p>';
+        $this->assertEquals($expected, $m->render($value));
+    }
+
+    public function testSimpleAttributes()
+    {
+        $m = $this->_markup;
+
+        $this->assertEquals('<p><strong class="zend">foo</strong></p>', $m->render('*(zend)foo*'));
+        $this->assertEquals('<p><strong id="zend">foo</strong></p>', $m->render('*(#zend)foo*'));
+        $this->assertEquals('<p><strong id="framework" class="zend">foo</strong></p>',
+                            $m->render('*(zend#framework)foo*'));
+
+        $this->assertEquals('<p><strong style="color:green;">foo</strong></p>', $m->render('*{color:green;}foo*'));
+        $this->assertEquals('<p><strong lang="en">foo</strong></p>', $m->render('*[en]foo*'));
+    }
+
+    public function testBlockAttributes()
+    {
+        $m = $this->_markup;
+
+        $this->assertEquals('<p class="zend">foo</p>', $m->render('p(zend). foo'));
+        $this->assertEquals('<p id="zend">foo</p>', $m->render('p(#zend). foo'));
+        $this->assertEquals('<p id="framework" class="zend">foo</p>', $m->render('p(zend#framework). foo'));
+
+        $this->assertEquals('<p style="color:green;">foo</p>', $m->render('p{color:green;}. foo'));
+        $this->assertEquals('<p lang="en">foo</p>', $m->render('p[en]. foo'));
+
+        $this->assertEquals('<p style="text-align: right;">foo</p>', $m->render('p>. foo'));
+        $this->assertEquals('<p style="text-align: left;">foo</p>', $m->render('p<. foo'));
+        $this->assertEquals('<p style="text-align: justify;">foo</p>', $m->render('p<>. foo'));
+        $this->assertEquals('<p style="text-align: center;">foo</p>', $m->render('p=. foo'));
+    }
+
+    public function testNewlines()
+    {
+        $this->assertEquals("<p>foo</p><p>bar<br />\nbaz</p>", $this->_markup->render("foo\n\nbar\nbaz"));
+        $this->assertEquals("<p>foo</p><p style=\"color:green;\">bar<br />\nbaz</p>",
+                            $this->_markup->render("foo\n\np{color:green}. bar\nbaz"));
+        $this->assertEquals("<p>foo</p><p>pahbarbaz</p>",
+                            $this->_markup->render("foo\n\npahbarbaz"));
+    }
+
+    public function testAttributeNotEndingDoesNotThrowNotice()
+    {
+        $m = $this->_markup;
+
+        $this->assertEquals("<p><strong>[</strong></p>", $m->render('*['));
+        $this->assertEquals("<p><strong>{</strong></p>", $m->render('*{'));
+        $this->assertEquals("<p><strong>(</strong></p>", $m->render('*('));
+    }
+
+    public function testTagOnEofDoesNotThrowNotice()
+    {
+        $m = $this->_markup;
+        $this->assertEquals("<p></p>", $m->render('!'));
+        $this->assertEquals("<p>*</p>", $m->render('*'));
+    }
+
+    public function testAcronymOnEofDoesNotThrowNotice()
+    {
+        $this->assertEquals('<p>ZFC(</p>', $this->_markup->render('ZFC('));
+    }
+
+
+}
+
+// Call Zend_Markup_BbcodeTest::main() if this source file is executed directly.
+if (PHPUnit_MAIN_METHOD == "Zend_Markup_TextileAndHtmlTest::main") {
+    Zend_Markup_TextileAndHtmlTest::main();
+}