<?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-2014 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-2014 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 NEWLINE = "[newline\0]"; // there is a parsing difference between the default tags and single tags const TYPE_DEFAULT = 'default'; const TYPE_SINGLE = 'single'; const NAME_CHARSET = '^\[\]=\s'; const STATE_SCAN = 0; const STATE_SCANATTRS = 1; const STATE_PARSEVALUE = 2; /** * 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 = ''; /** * Temporary tag storage * * @var array */ protected $_temp; /** * 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(), ), 'code' => array( 'type' => self::TYPE_DEFAULT, 'stoppers' => array('[/code]', '[/]'), 'parse_inside' => false ) ); /** * Token array * * @var array */ protected $_tokens = array(); /** * State * * @var int */ protected $_state = self::STATE_SCAN; /** * 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.'); } $this->_value = str_replace(array("\r\n", "\r", "\n"), self::NEWLINE, $value); // variable initialization for tokenizer $this->_valueLen = strlen($this->_value); $this->_pointer = 0; $this->_buffer = ''; $this->_temp = array(); $this->_state = self::STATE_SCAN; $this->_tokens = array(); $this->_tokenize(); // variable initialization for treebuilder $this->_searchedStoppers = array(); $this->_tree = new Zend_Markup_TokenList(); $this->_current = new Zend_Markup_Token( '', Zend_Markup_Token::TYPE_NONE, 'Zend_Markup_Root' ); $this->_tree->addChild($this->_current); $this->_createTree(); return $this->_tree; } /** * Tokenize * * @param string $input * * @return void */ protected function _tokenize() { $attribute = ''; while ($this->_pointer < $this->_valueLen) { switch ($this->_state) { case self::STATE_SCAN: $matches = array(); $regex = '#\G(?<text>[^\[]*)(?<open>\[(?<name>[' . self::NAME_CHARSET . ']+)?)?#'; preg_match($regex, $this->_value, $matches, null, $this->_pointer); $this->_pointer += strlen($matches[0]); if (!empty($matches['text'])) { $this->_buffer .= $matches['text']; } if (!isset($matches['open'])) { // great, no tag, we are ending the string break; } if (!isset($matches['name'])) { $this->_buffer .= $matches['open']; break; } $this->_temp = array( 'tag' => '[' . $matches['name'], 'name' => $matches['name'], 'attributes' => array() ); if ($this->_pointer >= $this->_valueLen) { // damn, no tag $this->_buffer .= $this->_temp['tag']; break 2; } if ($this->_value[$this->_pointer] == '=') { $this->_pointer++; $this->_temp['tag'] .= '='; $this->_state = self::STATE_PARSEVALUE; $attribute = $this->_temp['name']; } else { $this->_state = self::STATE_SCANATTRS; } break; case self::STATE_SCANATTRS: $matches = array(); $regex = '#\G((?<end>\s*\])|\s+(?<attribute>[' . self::NAME_CHARSET . ']+)(?<eq>=?))#'; if (!preg_match($regex, $this->_value, $matches, null, $this->_pointer)) { break 2; } $this->_pointer += strlen($matches[0]); if (!empty($matches['end'])) { if (!empty($this->_buffer)) { $this->_tokens[] = array( 'tag' => $this->_buffer, 'type' => Zend_Markup_Token::TYPE_NONE ); $this->_buffer = ''; } $this->_temp['tag'] .= $matches['end']; $this->_temp['type'] = Zend_Markup_Token::TYPE_TAG; $this->_tokens[] = $this->_temp; $this->_temp = array(); $this->_state = self::STATE_SCAN; } else { // attribute name $attribute = $matches['attribute']; $this->_temp['tag'] .= $matches[0]; $this->_temp['attributes'][$attribute] = ''; if (empty($matches['eq'])) { $this->_state = self::STATE_SCANATTRS; } else { $this->_state = self::STATE_PARSEVALUE; } } break; case self::STATE_PARSEVALUE: $matches = array(); $regex = '#\G((?<quote>"|\')(?<valuequote>.*?)\\2|(?<value>[^\]\s]+))#'; if (!preg_match($regex, $this->_value, $matches, null, $this->_pointer)) { $this->_state = self::STATE_SCANATTRS; break; } $this->_pointer += strlen($matches[0]); if (!empty($matches['quote'])) { $this->_temp['attributes'][$attribute] = $matches['valuequote']; } else { $this->_temp['attributes'][$attribute] = $matches['value']; } $this->_temp['tag'] .= $matches[0]; $this->_state = self::STATE_SCANATTRS; break; } } if (!empty($this->_buffer)) { $this->_tokens[] = array( 'tag' => $this->_buffer, 'type' => Zend_Markup_Token::TYPE_NONE ); } } /** * Parse the token array into a tree * * @param array $tokens * * @return void */ public function _createTree() { foreach ($this->_tokens as $token) { // first we want to know if this tag is a stopper, or at least a searched one if ($this->_isStopper($token['tag'])) { // find the stopper $oldItems = array(); while (!in_array($token['tag'], $this->_tags[$this->_current->getName()]['stoppers'])) { $oldItems[] = clone $this->_current; $this->_current = $this->_current->getParent(); } // we found the stopper, so stop the tag $this->_current->setStopper($token['tag']); $this->_removeFromSearchedStoppers($this->_current); $this->_current = $this->_current->getParent(); // add the old items again if there are any if (!empty($oldItems)) { foreach (array_reverse($oldItems) as $item) { /* @var $token Zend_Markup_Token */ $this->_current->addChild($item); $item->setParent($this->_current); $this->_current = $item; } } } else { if ($token['type'] == Zend_Markup_Token::TYPE_TAG) { if ($token['tag'] == self::NEWLINE) { // this is a newline tag, add it as a token $this->_current->addChild(new Zend_Markup_Token( "\n", Zend_Markup_Token::TYPE_NONE, '', array(), $this->_current )); } elseif (isset($token['name']) && ($token['name'][0] == '/')) { // this is a stopper, add it as a empty token $this->_current->addChild(new Zend_Markup_Token( $token['tag'], Zend_Markup_Token::TYPE_NONE, '', array(), $this->_current )); } elseif (isset($this->_tags[$this->_current->getName()]['parse_inside']) && !$this->_tags[$this->_current->getName()]['parse_inside'] ) { $this->_current->addChild(new Zend_Markup_Token( $token['tag'], Zend_Markup_Token::TYPE_NONE, '', array(), $this->_current )); } else { // add the tag $child = new Zend_Markup_Token( $token['tag'], $token['type'], $token['name'], $token['attributes'], $this->_current ); $this->_current->addChild($child); // add stoppers for this tag, if its has stoppers if ($this->_getType($token['name']) == self::TYPE_DEFAULT) { $this->_current = $child; $this->_addToSearchedStoppers($this->_current); } } } else { // no tag, just add it as a simple token $this->_current->addChild(new Zend_Markup_Token( $token['tag'], Zend_Markup_Token::TYPE_NONE, '', array(), $this->_current )); } } } } /** * Check if there is a tag declaration, and if it isnt there, add it * * @param string $name * * @return void */ protected function _checkTagDeclaration($name) { if (!isset($this->_tags[$name])) { $this->_tags[$name] = array( 'type' => self::TYPE_DEFAULT, 'stoppers' => array( '[/' . $name . ']', '[/]' ) ); } } /** * Check the tag's type * * @param string $name * @return string */ protected function _getType($name) { $this->_checkTagDeclaration($name); return $this->_tags[$name]['type']; } /** * Check if the tag is a stopper * * @param string $tag * @return bool */ protected function _isStopper($tag) { $this->_checkTagDeclaration($this->_current->getName()); if (!empty($this->_searchedStoppers[$tag])) { return true; } return false; } /** * Add to searched stoppers * * @param Zend_Markup_Token $token * @return void */ protected function _addToSearchedStoppers(Zend_Markup_Token $token) { $this->_checkTagDeclaration($token->getName()); 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) { $this->_checkTagDeclaration($token->getName()); foreach ($this->_tags[$token->getName()]['stoppers'] as $stopper) { --$this->_searchedStoppers[$stopper]; } } }