Sfoglia il codice sorgente

Refactored the renderers to use plugins instead of callbacks (unit tests not completely done).

git-svn-id: http://framework.zend.com/svn/framework/standard/trunk@20270 44c647ce-9c0f-0410-b52a-842ac1e357ba
kokx 16 anni fa
parent
commit
bc902289b0

+ 362 - 703
library/Zend/Markup/Parser/Textile.php

@@ -39,6 +39,18 @@ require_once 'Zend/Markup/Parser/ParserInterface.php';
  */
 class Zend_Markup_Parser_Textile implements Zend_Markup_Parser_ParserInterface
 {
+
+    const STATE_SCAN          = 0;
+    const STATE_NEW_PARAGRAPH = 1;
+    const STATE_NEWLINE       = 2;
+
+    const MATCH_ATTR_CLASSID = '\((?<attr_class>[a-zA-Z0-9_]+)?(?:\#(?<attr_id>[a-zA-Z0-9_]+))?\)';
+    const MATCH_ATTR_STYLE   = "\{(?<attr_style>[^\}\n]+)\}";
+    const MATCH_ATTR_LANG    = '\[(?<attr_lang>[a-zA-Z_]+)\]';
+    const MATCH_ATTR_ALIGN   = '(?<attr_align>\<\>?|\>|=)';
+
+
+
     /**
      * Token tree
      *
@@ -103,17 +115,18 @@ class Zend_Markup_Parser_Textile implements Zend_Markup_Parser_ParserInterface
     );
 
     /**
-     * The list's level
+     * Token array
      *
-     * @var int
+     * @var array
      */
-    protected $_listLevel = 0;
+    protected $_tokens = array();
 
 
     /**
      * Prepare the parsing of a Textile string, the real parsing is done in {@link _parse()}
      *
-     * @param  string $value
+     * @param string $value
+     *
      * @return array
      */
     public function parse($value)
@@ -133,787 +146,433 @@ class Zend_Markup_Parser_Textile implements Zend_Markup_Parser_ParserInterface
             throw new Zend_Markup_Parser_Exception('Value to parse cannot be left empty.');
         }
 
-        // first make we only have LF newlines
+        // first make we only have LF newlines, also trim the value
         $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);
+        // initialize variables and tokenize
+        $this->_valueLen = iconv_strlen($this->_value, 'UTF-8');
         $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);
-
+        $this->_tokens   = array();
 
-        $info = array(
-            'tag'        => '',
-            'attributes' => array()
-        );
+        $this->_tokenize();
 
-        // 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;
-            }
-        }
+        // create the tree
+        $this->_tree     = new Zend_Markup_TokenList();
 
-        // 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;
+        $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(true);
+        $this->_createTree();
 
         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
+     * Tokenize a textile 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)
+    protected function _tokenize()
     {
-        $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;
-                }
+        $state    = self::STATE_NEW_PARAGRAPH;
 
-                $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);
+        $attrsMatch = implode('|', array(
+            self::MATCH_ATTR_CLASSID,
+            self::MATCH_ATTR_STYLE,
+            self::MATCH_ATTR_LANG,
+            self::MATCH_ATTR_ALIGN
+        ));
 
-                if (!$attribute) {
-                    break;
-                }
+        $paragraph = '';
 
-                $info['tag'] .= '{' . $attribute . '}';
+        while ($this->_pointer < $this->_valueLen) {
+            switch ($state) {
+                case self::STATE_SCAN:
+                    $matches = array(); //[^\n*_?+~%@!-]
+                    $acronym = '(?<acronym>[A-Z]{2,})\((?<title>[^\)]+)\)';
+                    $regex   = '#\G(?<text>.*?)(?:'
+                             . "(?:(?<nl_paragraph>\n{2,})|(?<nl_break>\n))|"
+                             . '(?<tag>'
+                             . "(?<name>\*{1,2}|_{1,2}|\?{2}|\-|\+|\~|\^|%|@|!|$|{$acronym}"
+                             . '|":(?<url>[^\s]+)|")'
+                             . "(?:{$attrsMatch})*)"
+                             . ')#si';
+                    preg_match($regex, $this->_value, $matches, null, $this->_pointer);
+
+                    $this->_pointer += strlen($matches[0]);
+
+                    if (!empty($matches['text'])) {
+                        $this->_buffer .= $matches['text'];
+                    }
 
-                $info['attributes']['style'] = $attribute;
+                    // first add the buffer
+                    if (!empty($this->_buffer)) {
+                        $this->_tokens[] = array(
+                            'tag'  => $this->_buffer,
+                            'type' => Zend_Markup_Token::TYPE_NONE
+                        );
+                        $this->_buffer = '';
+                    }
 
-                $this->_pointer++;
-                break;
-            case '[':
-                // style
-                $attribute = $this->_parseAttributeEnd(']', $tagEnd);
+                    if (!empty($matches['nl_paragraph'])) {
+                        $this->_temp = array(
+                            'tag'        => $matches['nl_paragraph'],
+                            'name'       => 'p',
+                            'type'       => Zend_Markup_Token::TYPE_TAG,
+                            'attributes' => array()
+                        );
+
+                        $state = self::STATE_NEW_PARAGRAPH;
+                    } elseif (!empty($matches['nl_break'])) {
+                        $this->_tokens[] = array(
+                            'tag'        => $matches['nl_break'],
+                            'name'       => 'break',
+                            'type'       => Zend_Markup_Token::TYPE_TAG,
+                            'attributes' => array()
+                        );
+
+                        $state   = self::STATE_NEWLINE;
+                    } elseif (!empty($matches['tag'])) {
+                        if (isset($this->_simpleTags[$matches['name']])) {
+                            // now add the new token
+                            $this->_tokens[] = array(
+                                'tag'        => $matches['tag'],
+                                'type'       => Zend_Markup_Token::TYPE_TAG,
+                                'name'       => $this->_simpleTags[$matches['name']],
+                                'attributes' => $this->_extractAttributes($matches)
+                            );
+                        } else {
+                            $attributes = $this->_extractAttributes($matches);
+                            if ($matches['tag'][0] == '"') {
+                                $name = 'url';
+                                if (isset($matches['url'])) {
+                                    $attributes['url'] = $matches['url'];
+                                }
+                                $this->_tokens[] = array(
+                                    'tag'        => $matches['tag'],
+                                    'type'       => Zend_Markup_Token::TYPE_TAG,
+                                    'name'       => $name,
+                                    'attributes' => $attributes
+                                );
+                            } else {
+                                $name = 'acronym';
+                                $this->_tokens[] = array(
+                                    'tag'        => '',
+                                    'type'       => Zend_Markup_Token::TYPE_TAG,
+                                    'name'       => 'acronym',
+                                    'attributes' => array(
+                                        'title' => $matches['title']
+                                    )
+                                );
+                                $this->_tokens[] = array(
+                                    'tag'  => $matches['acronym'],
+                                    'type' => Zend_Markup_Token::TYPE_NONE
+                                );
+                                $this->_tokens[] = array(
+                                    'tag'        => '(' . $matches['title'] . ')',
+                                    'type'       => Zend_Markup_Token::TYPE_TAG,
+                                    'name'       => 'acronym',
+                                    'attributes' => array()
+                                );
+                            }
+                        }
+                        $state = self::STATE_SCAN;
+                    }
 
-                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++;
+                case self::STATE_NEW_PARAGRAPH:
+                    if (empty($this->_temp)) {
+                        $this->_temp = array(
+                            'tag'        => '',
+                            'name'       => 'p',
+                            'type'       => Zend_Markup_token::TYPE_TAG,
+                            'attributes' => array()
+                        );
                     } else {
-                        $info['attributes']['align'] = 'left';
+                        $this->_tokens[] = array(
+                            'tag'        => "\n",
+                            'name'       => 'p',
+                            'type'       => Zend_Markup_Token::TYPE_TAG,
+                            'attributes' => array()
+                        );
+                        $this->_temp['tag'] = substr($this->_temp['tag'], 1);
                     }
-                }
-                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];
+                    $matches = array(); //[^\n*_?+~%@!-] (\()? [^()]+ (?(1)\))
+                    $regex   = "#\G(?<name>(h[1-6]|p)|(?:\#|\*))(?:{$attrsMatch})*(?(2)\.\s|\s)#i";
+                    if (!preg_match($regex, $this->_value, $matches, null, $this->_pointer)) {
+                        $this->_tokens[] = $this->_temp;
+                        $state    = self::STATE_SCAN;
+                        break;
+                    }
 
-        $token = new Zend_Markup_Token(
-            $info['tag'],
-            Zend_Markup_Token::TYPE_TAG,
-            $name,
-            $info['attributes'],
-            $this->_current
-        );
-        $this->_current->addChild($token);
-        $this->_current = $token;
+                    $this->_pointer += strlen($matches[0]);
 
-        // find the stopper
-        while ($this->_valueLen > $this->_pointer) {
-            if ($this->_value[$this->_pointer] == $tag) {
-                // found the stopper, set it and return
-                $this->_pointer++;
+                    if ($matches['name'] == 'p') {
+                        $this->_temp['tag']       .= $matches[0];
+                        $this->_temp['attributes'] = $this->_extractAttributes($matches);
 
-                $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);
-    }
+                        $this->_tokens[]    = $this->_temp;
+                        $this->_temp = array();
+                    } else {
+                        $this->_tokens[] = $this->_temp;
+                        $this->_temp = array();
+
+                        $name       = $matches['name'];
+                        $attributes = $this->_extractAttributes($matches);
+
+                        if ($name == '#') {
+                            $name               = 'list';
+                            $attributes['list'] = 'decimal';
+                        } elseif ($name == '*') {
+                            $name = 'list';
+                        }
+
+                        $this->_tokens[] = array(
+                            'tag'        => $matches[0],
+                            'name'       => $name,
+                            'type'       => Zend_Markup_Token::TYPE_TAG,
+                            'attributes' => $attributes
+                        );
+                    }
 
-    /**
-     * 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();
+                    $state = self::STATE_SCAN;
                     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();
+                case self::STATE_NEWLINE:
+                    $matches = array(); //[^\n*_?+~%@!-]
+                    $regex   = "#\G(?<name>(h[1-6])|(?:\#|\*))(?:{$attrsMatch})*(?(2)\.\s|\s)#si";
+                    if (!preg_match($regex, $this->_value, $matches, null, $this->_pointer)) {
+                        $state = self::STATE_SCAN;
+                        break;
+                    }
 
-            $info = array(
-                'tag'        => "\n",
-                'attributes' => array()
-            );
+                    $this->_pointer += strlen($matches[0]);
 
-            $oldPointer = $this->_pointer;
+                    $name       = $matches['name'];
+                    $attributes = $this->_extractAttributes($matches);
 
-            if ($this->_value[$this->_pointer] == 'p') {
-                $this->_pointer++;
-                $info = $this->_parseTagInfo("\np", '.', true);
+                    if ($name == '#') {
+                        $name               = 'list';
+                        $attributes['list'] = 'decimal';
+                    } elseif ($name == '*') {
+                        $name = 'list';
+                    }
 
-                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()
+                    $this->_tokens[] = array(
+                        'tag'        => $matches[0],
+                        'name'       => $name,
+                        'type'       => Zend_Markup_Token::TYPE_TAG,
+                        'attributes' => $attributes
                     );
-                }
+                    break;
             }
+        }
 
-            // 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
+        if (!empty($buffer)) {
+            $this->_tokens[] = array(
+                'tag'  => $buffer,
+                'type' => Zend_Markup_Token::TYPE_NONE
             );
-            $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
-             */
+            $buffer = '';
         }
     }
 
     /**
-     * Parse a heading
+     * Create a tree from the tokenized text
      *
-     * @todo implement support for attributes
      * @return void
      */
-    protected function _parseHeading()
+    protected function _createTree()
     {
-        // check if it is a valid heading
-        if (in_array($this->_value[++$this->_pointer], range(1, 6))) {
-            $name = 'h' . $this->_value[$this->_pointer++];
+        $inside = true;
+
+        foreach ($this->_tokens as $key => $token) {
+            // first check if the token is a stopper
+            if ($this->_isStopper($token, $this->_current)) {
+                if ($this->_current->getName() == 'li') {
+                    // list items are handled differently
+                    if (isset($this->_tokens[$key + 1])
+                        && ($this->_tokens[$key + 1]['type'] == Zend_Markup_Token::TYPE_TAG)
+                        && ($this->_tokens[$key + 1]['name'] == 'list')
+                    ) {
+                        // the next item is a correct tag
+                        $this->_current->setStopper($token['tag']);
+
+                        $this->_current = $this->_current->getParent();
+                    } else {
+                        // close the list
+                        $this->_current->setStopper($token['tag']);
 
-            $info = $this->_parseTagInfo($name, '.', true);
+                        $this->_current = $this->_current->getParent()->getParent();
 
-            // now, the next char should be a dot
-            if ($this->_value[$this->_pointer] == '.') {
-                $info['tag'] .= '.';
+                        // go up in the tree until we found the end
+                        while ($this->_isStopper($token, $this->_current)) {
+                            $this->_current->setStopper($token['tag']);
 
-                // add the tag
-                $this->_processBuffer();
+                            $this->_current = $this->_current->getParent();
+                        }
+                    }
+                } else {
+                    // go up in the tree until we found the end of stoppers
+                    while ($this->_isStopper($token, $this->_current)) {
+                        $this->_current->setStopper($token['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;
+                        if (!empty($token['attributes'])) {
+                            foreach ($token['attributes'] as $name => $value) {
+                                $this->_current->addAttribute($name, $value);
+                            }
+                        }
 
-                if ($this->_value[++$this->_pointer] != ' ') {
-                    $this->_buffer .= $this->_value[$this->_pointer];
+                        $this->_current = $this->_current->getParent();
+                    }
                 }
+                $inside = true;
+            } elseif (($token['type'] == Zend_Markup_Token::TYPE_TAG) && $inside) {
+                if ($token['name'] == 'break') {
+                    // add the newline and continue parsing
+                    $this->_current->addChild(new Zend_Markup_Token(
+                        $token['tag'],
+                        Zend_Markup_Token::TYPE_NONE,
+                        '',
+                        array(),
+                        $this->_current
+                    ));
+                } else {
+                    // handle a list item
+                    if ($token['name'] == 'list') {
+                        $attributes = array();
+                        if (isset($token['attributes']['list'])) {
+                            $attributes['list'] = $token['attributes']['list'];
+                            unset($token['attributes']['list']);
+                        }
+
+                        if ($this->_current->getName() != 'list') {
+                            // the list isn't started yet, create it
+                            $child = new Zend_Markup_Token(
+                                '',
+                                Zend_Markup_Token::TYPE_TAG,
+                                'list',
+                                $attributes,
+                                $this->_current
+                            );
+
+                            $this->_current->addChild($child);
+
+                            $this->_current = $child;
+                        }
+                        $token['name'] = 'li';
+                    } elseif (($token['name'] == 'img') || ($token['name'] == 'url')) {
+                        $inside = false;
+                    }
 
-                // find the end
-                $len = strcspn($this->_value, "\n", ++$this->_pointer);
-
-                $this->_buffer.= substr($this->_value, $this->_pointer, $len);
-
-                $this->_pointer += $len;
+                    // add the token
+                    $child = new Zend_Markup_Token(
+                        $token['tag'],
+                        Zend_Markup_Token::TYPE_TAG,
+                        $token['name'],
+                        $token['attributes'],
+                        $this->_current
+                    );
 
-                // end the tag and return
-                $this->_processBuffer();
-                $this->_current = $this->_current->getParent();
+                    $this->_current->addChild($child);
 
-                return;
+                    $this->_current = $child;
+                }
+            } else {
+                // simply add the token as text
+                $this->_current->addChild(new Zend_Markup_Token(
+                    $token['tag'],
+                    Zend_Markup_Token::TYPE_NONE,
+                    '',
+                    array(),
+                    $this->_current
+                ));
             }
-            $this->_buffer .= "\n" . $name;
-            return;
         }
-
-        // not a valid heading
-        $this->_buffer .= "\nh";
     }
 
     /**
-     * Parse a list
+     * Check if a tag is a stopper
      *
-     * @todo allow a deeper list level
-     * @todo add support for markup inside the list items
-     * @return void
+     * @param array $token
+     * @param Zend_Markup_Token $current
+     *
+     * @return bool
      */
-    protected function _parseList()
+    protected function _isStopper(array $token, Zend_Markup_Token $current)
     {
-        // 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
+        switch ($current->getName()) {
+            case 'h1':
+            case 'h2':
+            case 'h3':
+            case 'h4':
+            case 'h5':
+            case 'h6':
+            case 'list':
+            case 'li':
+                if (($token['type'] == Zend_Markup_Token::TYPE_TAG)
+                    && (($token['name'] == 'break') || ($token['name'] == 'p'))
+                ) {
+                    return true;
+                }
+                break;
+            case 'break':
+                return false;
+                break;
+            default:
+                if (($token['type'] == Zend_Markup_Token::TYPE_TAG) && ($token['name'] == $current->getName())) {
+                    return true;
+                }
                 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();
+        return false;
     }
 
     /**
-     * Parse an acronym
+     * Extract the attributes
      *
-     * @return void
+     * @param array $matches
+     *
+     * @return array
      */
-    protected function _parseAcronym()
+    protected function _extractAttributes(array $matches)
     {
-        $this->_pointer++;
-
-        // first find the acronym itself
-        $acronym = '';
-        $pointer = 0;
-
-        if (empty($this->_buffer)) {
-            $this->_buffer .= '(';
-            return;
-        }
-
-        $bufferLen = strlen($this->_buffer);
+        $attributes = array();
 
-        while (($bufferLen > $pointer) && ctype_upper($this->_buffer[$bufferLen - ++$pointer])) {
-            $acronym = $this->_buffer[strlen($this->_buffer) - $pointer] . $acronym;
+        if (!empty($matches['attr_class'])) {
+            $attributes['class'] = $matches['attr_class'];
         }
-
-        if (strlen($acronym) < 3) {
-            // just add the '(' to the buffer, this isn't an acronym
-            $this->_buffer .= '(';
-            return;
+        if (!empty($matches['attr_id'])) {
+            $attributes['id'] = $matches['attr_id'];
         }
-
-        // 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 (!empty($matches['attr_style'])) {
+            $attributes['style'] = $matches['attr_style'];
         }
-
-        if ($this->_pointer >= $this->_valueLen) {
-            $this->_buffer .= '(';
-            return;
+        if (!empty($matches['attr_lang'])) {
+            $attributes['lang'] = $matches['attr_lang'];
         }
-
-        $this->_pointer++;
-
-        if (empty($title)) {
-            $this->_buffer .= '()';
-            return;
+        if (!empty($matches['attr_align'])) {
+            switch ($matches['attr_align']) {
+                case '=':
+                    $attributes['align'] = 'center';
+                    break;
+                case '>':
+                    $attributes['align'] = 'right';
+                    break;
+                case '<>':
+                    $attributes['align'] = 'justify';
+                    break;
+                default:
+                case '<':
+                    $attributes['align'] = 'left';
+                    break;
+            }
         }
 
-        $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);
+        return $attributes;
     }
 
-    /**
-     * 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  = '';
-        }
-    }
 }

+ 32 - 142
library/Zend/Markup/Renderer/Html.php

@@ -24,7 +24,6 @@
  * @see Zend_Uri
  */
 require_once 'Zend/Uri.php';
-
 /**
  * @see Zend_Markup_Renderer_RendererAbstract
  */
@@ -41,6 +40,7 @@ require_once 'Zend/Markup/Renderer/RendererAbstract.php';
  */
 class Zend_Markup_Renderer_Html extends Zend_Markup_Renderer_RendererAbstract
 {
+
     /**
      * Tag info
      *
@@ -150,20 +150,20 @@ class Zend_Markup_Renderer_Html extends Zend_Markup_Renderer_RendererAbstract
         // callback tags
         'url' => array(
             'type'     => 6, // self::TYPE_CALLBACK | self::TAG_NORMAL
-            'callback' => array('Zend_Markup_Renderer_Html', '_htmlUrl'),
+            'callback' => null,
             'group'    => 'inline',
             'filter'   => true,
         ),
         'img' => array(
             'type'     => 6,
-            'callback' => array('Zend_Markup_Renderer_Html', '_htmlImg'),
-            'group'    => 'inline_empty',
+            'callback' => null,
+            'group'    => 'inline-empty',
             'filter'   => true,
         ),
         'code' => array(
             'type'     => 6,
-            'callback' => array('Zend_Markup_Renderer_Html', '_htmlCode'),
-            'group'    => 'block_empty',
+            'callback' => null,
+            'group'    => 'block-empty',
             'filter'   => false,
         ),
         'p' => array(
@@ -176,7 +176,7 @@ class Zend_Markup_Renderer_Html extends Zend_Markup_Renderer_RendererAbstract
             'type'   => 10,
             'start'  => '',
             'end'    => '',
-            'group'  => 'block_empty',
+            'group'  => 'block-empty',
             'filter' => true,
         ),
         'quote' => array(
@@ -187,7 +187,7 @@ class Zend_Markup_Renderer_Html extends Zend_Markup_Renderer_RendererAbstract
         ),
         'list' => array(
             'type'     => 6,
-            'callback' => array('Zend_Markup_Renderer_Html', '_htmlList'),
+            'callback' => null,
             'group'    => 'list',
             'filter'   => false,
         ),
@@ -309,6 +309,25 @@ class Zend_Markup_Renderer_Html extends Zend_Markup_Renderer_RendererAbstract
 
 
     /**
+     * Constructor
+     *
+     * @param array|Zend_Config $options
+     *
+     * @return void
+     */
+    public function __construct($options = array())
+    {
+        if ($options instanceof Zend_Config) {
+            $options = $options->toArray();
+        }
+
+        $this->_pluginLoader = new Zend_Loader_PluginLoader(array(
+            'Zend_Markup_Renderer_Html' => 'Zend/Markup/Renderer/Html/'
+        ));
+
+        parent::__construct($options);
+    }
+    /**
      * Execute a replace token
      *
      * @param  Zend_Markup_Token $token
@@ -321,7 +340,7 @@ class Zend_Markup_Renderer_Html extends Zend_Markup_Renderer_RendererAbstract
             if (!isset($tag['attributes'])) {
                 $tag['attributes'] = array();
             }
-            $attrs = self::_renderAttributes($token, $tag['attributes']);
+            $attrs = self::renderAttributes($token, $tag['attributes']);
             return "<{$tag['tag']}{$attrs}>{$this->_render($token)}</{$tag['tag']}>";
         }
 
@@ -341,7 +360,7 @@ class Zend_Markup_Renderer_Html extends Zend_Markup_Renderer_RendererAbstract
             if (!isset($tag['attributes'])) {
                 $tag['attributes'] = array();
             }
-            $attrs = self::_renderAttributes($token, $tag['attributes']);
+            $attrs = self::renderAttributes($token, $tag['attributes']);
             return "<{$tag['tag']}{$attrs} />";
         }
         return parent::_executeSingleReplace($token, $tag);
@@ -354,7 +373,7 @@ class Zend_Markup_Renderer_Html extends Zend_Markup_Renderer_RendererAbstract
      * @param  array $tag
      * @return string
      */
-    protected static function _renderAttributes(Zend_Markup_Token $token, array $attributes = array())
+    public static function renderAttributes(Zend_Markup_Token $token, array $attributes = array())
     {
         $attributes = array_merge(self::$_defaultAttributes, $attributes);
 
@@ -378,7 +397,7 @@ class Zend_Markup_Renderer_Html extends Zend_Markup_Renderer_RendererAbstract
             $tokenAttributes['style'] .= 'text-align: ' . $tokenAttributes['align'] . ';';
             unset($tokenAttributes['align']);
         }
-        if (isset($tokenAttributes['color']) && self::_checkColor($tokenAttributes['color'])) {
+        if (isset($tokenAttributes['color']) && self::checkColor($tokenAttributes['color'])) {
             $tokenAttributes['style'] .= 'color: ' . $tokenAttributes['color'] . ';';
             unset($tokenAttributes['color']);
         }
@@ -401,142 +420,13 @@ class Zend_Markup_Renderer_Html extends Zend_Markup_Renderer_RendererAbstract
     }
 
     /**
-     * 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)
+    public static function checkColor($color)
     {
         /*
          * aqua, black, blue, fuchsia, gray, green, lime, maroon, navy, olive,

+ 53 - 0
library/Zend/Markup/Renderer/Html/Code.php

@@ -0,0 +1,53 @@
+<?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_Html
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Markup_Renderer_Html_HtmlAbstract
+ */
+require_once 'Zend/Markup/Renderer/Html/HtmlAbstract.php';
+
+/**
+ * Tag interface
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer_Html
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Renderer_Html_Code extends Zend_Markup_Renderer_Html_HtmlAbstract
+{
+
+    /**
+     * Convert the token
+     *
+     * @param Zend_Markup_Token $token
+     * @param string $text
+     *
+     * @return string
+     */
+    public function convert(Zend_Markup_Token $token, $text)
+    {
+        return highlight_string($text, true);
+    }
+
+}

+ 69 - 0
library/Zend/Markup/Renderer/Html/HtmlAbstract.php

@@ -0,0 +1,69 @@
+<?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_Html
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Markup_Renderer_TagInterface
+ */
+require_once 'Zend/Markup/Renderer/TagInterface.php';
+
+/**
+ * Tag interface
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer_Html
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Markup_Renderer_Html_HtmlAbstract implements Zend_Markup_Renderer_TagInterface
+{
+
+    /**
+     * The HTML renderer
+     *
+     * @var Zend_Markup_Renderer_Html
+     */
+    protected $_renderer;
+
+
+    /**
+     * Set the HTML renderer instance
+     *
+     * @param Zend_Markup_Renderer_Html $renderer
+     *
+     * @return Zend_Markup_Renderer_Html_HtmlAbstract
+     */
+    public function setRenderer(Zend_Markup_Renderer_Html $renderer)
+    {
+        $this->_renderer = $renderer;
+    }
+
+    /**
+     * Get the HTML renderer instance
+     *
+     * @return Zend_Markup_Renderer_Html
+     */
+    public function getRenderer()
+    {
+        return $this->_renderer;
+    }
+}

+ 71 - 0
library/Zend/Markup/Renderer/Html/Img.php

@@ -0,0 +1,71 @@
+<?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_Html
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Markup_Renderer_Html_HtmlAbstract
+ */
+require_once 'Zend/Markup/Renderer/Html/HtmlAbstract.php';
+
+/**
+ * Tag interface
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer_Html
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Renderer_Html_Img extends Zend_Markup_Renderer_Html_HtmlAbstract
+{
+
+    /**
+     * Convert the token
+     *
+     * @param Zend_Markup_Token $token
+     * @param string $text
+     *
+     * @return string
+     */
+    public function convert(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}\"" . Zend_Markup_Renderer_Html::renderAttributes($token) . " />";
+    }
+
+}

+ 103 - 0
library/Zend/Markup/Renderer/Html/List.php

@@ -0,0 +1,103 @@
+<?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_Html
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Markup_Renderer_Html_HtmlAbstract
+ */
+require_once 'Zend/Markup/Renderer/Html/HtmlAbstract.php';
+
+/**
+ * Tag interface
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer_Html
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Renderer_Html_List extends Zend_Markup_Renderer_Html_HtmlAbstract
+{
+
+    /**
+     * Convert the token
+     *
+     * @param Zend_Markup_Token $token
+     * @param string $text
+     *
+     * @return string
+     */
+    public function convert(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>";
+        }
+    }
+
+}

+ 68 - 0
library/Zend/Markup/Renderer/Html/Url.php

@@ -0,0 +1,68 @@
+<?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_Html
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Markup_Renderer_Html_HtmlAbstract
+ */
+require_once 'Zend/Markup/Renderer/Html/HtmlAbstract.php';
+
+/**
+ * Tag interface
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer_Html
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Renderer_Html_Url extends Zend_Markup_Renderer_Html_HtmlAbstract
+{
+
+    /**
+     * Convert the token
+     *
+     * @param Zend_Markup_Token $token
+     * @param string $text
+     *
+     * @return string
+     */
+    public function convert(Zend_Markup_Token $token, $text)
+    {
+        if ($token->hasAttribute('url')) {
+            $url = $token->getAttribute('url');
+        } else {
+            $url = $text;
+        }
+
+        //if (subs)
+
+        // check if the URL is valid
+        if (!Zend_Uri::check($url)) {
+            return $text;
+        }
+
+        $attributes = Zend_Markup_Renderer_Html::renderAttributes($token);
+
+        return "<a href=\"{$url}\"{$attributes}>{$text}</a>";
+    }
+
+}

+ 49 - 8
library/Zend/Markup/Renderer/RendererAbstract.php

@@ -21,6 +21,11 @@
  */
 
 /**
+ * @see Zend_config
+ */
+require_once 'Zend/Config.php';
+
+/**
  * Defines the basic rendering functionality
  *
  * @category   Zend
@@ -73,15 +78,26 @@ abstract class Zend_Markup_Renderer_RendererAbstract
      */
     protected $_groups = array();
 
+    /**
+     * Plugin loader for tags
+     *
+     * @var Zend_Loader_PluginLoader
+     */
+    protected $_pluginLoader;
 
     /**
      * Constructor
      *
-     * @param  array $options
+     * @param array|Zend_Config $options
+     *
      * @return void
      */
-    public function __construct(array $options = array())
+    public function __construct($options = array())
     {
+        if ($options instanceof Zend_Config) {
+            $options = $options->toArray();
+        }
+
         if (isset($options['parser'])) {
             $this->setParser($options['parser']);
         }
@@ -113,6 +129,16 @@ abstract class Zend_Markup_Renderer_RendererAbstract
     }
 
     /**
+     * Get the plugin loader
+     *
+     * @return Zend_Loader_PluginLoader
+     */
+    public function getPluginLoader()
+    {
+        return $this->_pluginLoader;
+    }
+
+    /**
      * Add a new tag
      *
      * @param  string $name
@@ -136,9 +162,11 @@ abstract class Zend_Markup_Renderer_RendererAbstract
         // check the type
         if ($type & self::TYPE_CALLBACK) {
             // add a callback tag
-            if (!isset($info['callback']) || !is_callable($info['callback'])) {
+            if (isset($info['callback']) && !($info['callback'] instanceof Zend_Markup_Renderer_TagInterface)) {
                 require_once 'Zend/Markup/Renderer/Exception.php';
-                throw new Zend_Markup_Renderer_Exception("No valid callback defined for tag '$name'.");
+                throw new Zend_Markup_Renderer_Exception("Not a valid tag callback.");
+            } else {
+                $info['callback'] = null;
             }
 
             $info['type'] = $type;
@@ -296,11 +324,13 @@ abstract class Zend_Markup_Renderer_RendererAbstract
             return $this->_filter($token->getTag()) . $this->_render($token) . $token->getStopper();
         }
 
-        $tag = $this->_tags[$token->getName()];
+        $tag  = $this->_tags[$token->getName()];
+        $name = $token->getName();
 
         // alias processing
         while ($tag['type'] & self::TYPE_ALIAS) {
-            $tag = $this->_tags[$tag['name']];
+            $name = $tag['name'];
+            $tag  = $this->_tags[$name];
         }
 
         // check if the tag has content
@@ -315,10 +345,21 @@ abstract class Zend_Markup_Renderer_RendererAbstract
 
         // callback
         if ($tag['type'] & self::TYPE_CALLBACK) {
+            // load the callback if the tag doesn't exist
+            if (!($tag['callback'] instanceof Zend_Markup_Renderer_TagInterface)) {
+                $class = $this->getPluginLoader()->load($name);
+
+                $tag['callback'] = new $class;
+
+                if (!($tag['callback'] instanceof Zend_Markup_Renderer_TagInterface)) {
+                    require_once 'Zend/Markup/Renderer/Exception.php';
+                    throw new Zend_Markup_Renderer_Exception("Callback for tag '$name' found, but it isn't valid.");
+                }
+            }
             if ($tag['type'] & self::TAG_NORMAL) {
-                return call_user_func_array($tag['callback'], array($token, $this->_render($token)));
+                return $tag['callback']->convert($token, $this->_render($token));
             }
-            return call_user_func_array($tag['callback'], array($token));
+            return $tag['callback']->convert($token, null);
         }
         // replace
         if ($tag['type'] & self::TAG_NORMAL) {

+ 44 - 0
library/Zend/Markup/Renderer/TagInterface.php

@@ -0,0 +1,44 @@
+<?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-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Tag interface
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_Markup_Renderer_TagInterface
+{
+
+    /**
+     * Convert the token
+     *
+     * @param Zend_Markup_Token $token
+     * @param string $text
+     *
+     * @return string
+     */
+    public function convert(Zend_Markup_Token $token, $text);
+}

+ 16 - 4
library/Zend/Markup/Token.php

@@ -97,10 +97,10 @@ class Zend_Markup_Token
      * @return void
      */
     public function __construct(
-        $tag, 
-        $type, 
-        $name = '', 
-        array $attributes = array(), 
+        $tag,
+        $type,
+        $name = '',
+        array $attributes = array(),
         Zend_Markup_Token $parent = null
     ) {
         $this->_tag        = $tag;
@@ -200,6 +200,18 @@ class Zend_Markup_Token
     }
 
     /**
+     * Add an attribute
+     *
+     * @return Zend_Markup_Token
+     */
+    public function addAttribute($name, $value)
+    {
+        $this->_attributes[$name] = $value;
+
+        return $this;
+    }
+
+    /**
      * Check if an attribute is empty
      *
      * @param string $name

+ 6 - 13
tests/Zend/Markup/BbcodeAndHtmlTest.php

@@ -146,9 +146,14 @@ class Zend_Markup_BbcodeAndHtmlTest extends PHPUnit_Framework_TestCase
      */
     public function testAddTags()
     {
+        $this->_markup->getPluginLoader()->addPrefixPath(
+            'Zend_Markup_Test_Renderer_Html',
+            'Zend/Markup/Test/Renderer/Html'
+        );
+
         $this->_markup->addTag('bar',
             Zend_Markup_Renderer_RendererAbstract::TYPE_CALLBACK | Zend_Markup_Renderer_RendererAbstract::TAG_NORMAL,
-            array('callback' => 'markupTestCallback', 'group' => 'inline'));
+            array('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'));
@@ -368,18 +373,6 @@ BBCODE;
 
 }
 
-
-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") {

+ 59 - 0
tests/Zend/Markup/Test/Renderer/Html/Bar.php

@@ -0,0 +1,59 @@
+<?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_Html
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Markup_Renderer_Html_HtmlAbstract
+ */
+require_once 'Zend/Markup/Renderer/Html/HtmlAbstract.php';
+
+/**
+ * Tag interface
+ *
+ * @category   Zend
+ * @package    Zend_Markup
+ * @subpackage Renderer_Html
+ * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Markup_Test_Renderer_Html_Bar implements Zend_Markup_Renderer_TagInterface
+{
+
+    /**
+     * Convert the token
+     *
+     * @param Zend_Markup_Token $token
+     * @param string $text
+     *
+     * @return string
+     */
+    public function convert(Zend_Markup_Token $token, $text)
+    {
+        $bar = $token->getAttribute('bar');
+
+        if (!empty($bar)) {
+            $bar = '=' . $bar;
+        }
+
+        return "[foo{$bar}]" . $text . '[/foo]';
+    }
+
+}

+ 1 - 1
tests/Zend/Markup/TextileAndHtmlTest.php

@@ -169,7 +169,7 @@ class Zend_Markup_TextileAndHtmlTest extends PHPUnit_Framework_TestCase
     {
         $m = $this->_markup;
         $this->assertEquals("<p></p>", $m->render('!'));
-        $this->assertEquals("<p>*</p>", $m->render('*'));
+        $this->assertEquals("<p></p>", $m->render('*'));
     }
 
     public function testAcronymOnEofDoesNotThrowNotice()