Bbcode.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. <?php
  2. /**
  3. * Zend Framework
  4. *
  5. * LICENSE
  6. *
  7. * This source file is subject to the new BSD license that is bundled
  8. * with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://framework.zend.com/license/new-bsd
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@zend.com so we can send you a copy immediately.
  14. *
  15. * @category Zend
  16. * @package Zend_Markup
  17. * @subpackage Parser
  18. * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
  19. * @license http://framework.zend.com/license/new-bsd New BSD License
  20. * @version $Id$
  21. */
  22. /**
  23. * @see Zend_Markup_TokenList
  24. */
  25. require_once 'Zend/Markup/TokenList.php';
  26. /**
  27. * @see Zend_Markup_Parser_ParserInterface
  28. */
  29. require_once 'Zend/Markup/Parser/ParserInterface.php';
  30. /**
  31. * @category Zend
  32. * @package Zend_Markup
  33. * @subpackage Parser
  34. * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
  35. * @license http://framework.zend.com/license/new-bsd New BSD License
  36. */
  37. class Zend_Markup_Parser_Bbcode implements Zend_Markup_Parser_ParserInterface
  38. {
  39. const NEWLINE = "[newline\0]";
  40. // there is a parsing difference between the default tags and single tags
  41. const TYPE_DEFAULT = 'default';
  42. const TYPE_SINGLE = 'single';
  43. const NAME_CHARSET = '^\[\]=\s';
  44. const STATE_SCAN = 0;
  45. const STATE_SCANATTRS = 1;
  46. const STATE_PARSEVALUE = 2;
  47. /**
  48. * Token tree
  49. *
  50. * @var Zend_Markup_TokenList
  51. */
  52. protected $_tree;
  53. /**
  54. * Current token
  55. *
  56. * @var Zend_Markup_Token
  57. */
  58. protected $_current;
  59. /**
  60. * Source to tokenize
  61. *
  62. * @var string
  63. */
  64. protected $_value = '';
  65. /**
  66. * Length of the value
  67. *
  68. * @var int
  69. */
  70. protected $_valueLen = 0;
  71. /**
  72. * Current pointer
  73. *
  74. * @var int
  75. */
  76. protected $_pointer = 0;
  77. /**
  78. * The buffer
  79. *
  80. * @var string
  81. */
  82. protected $_buffer = '';
  83. /**
  84. * Temporary tag storage
  85. *
  86. * @var array
  87. */
  88. protected $_temp;
  89. /**
  90. * Stoppers that we are searching for
  91. *
  92. * @var array
  93. */
  94. protected $_searchedStoppers = array();
  95. /**
  96. * Tag information
  97. *
  98. * @var array
  99. */
  100. protected $_tags = array(
  101. 'Zend_Markup_Root' => array(
  102. 'type' => self::TYPE_DEFAULT,
  103. 'stoppers' => array(),
  104. ),
  105. '*' => array(
  106. 'type' => self::TYPE_DEFAULT,
  107. 'stoppers' => array(self::NEWLINE),
  108. ),
  109. 'hr' => array(
  110. 'type' => self::TYPE_SINGLE,
  111. 'stoppers' => array(),
  112. ),
  113. );
  114. /**
  115. * Token array
  116. *
  117. * @var array
  118. */
  119. protected $_tokens = array();
  120. /**
  121. * State
  122. *
  123. * @var int
  124. */
  125. protected $_state = self::STATE_SCAN;
  126. /**
  127. * Prepare the parsing of a bbcode string, the real parsing is done in {@link _parse()}
  128. *
  129. * @param string $value
  130. * @return Zend_Markup_TokenList
  131. */
  132. public function parse($value)
  133. {
  134. if (!is_string($value)) {
  135. /**
  136. * @see Zend_Markup_Parser_Exception
  137. */
  138. require_once 'Zend/Markup/Parser/Exception.php';
  139. throw new Zend_Markup_Parser_Exception('Value to parse should be a string.');
  140. }
  141. if (empty($value)) {
  142. /**
  143. * @see Zend_Markup_Parser_Exception
  144. */
  145. require_once 'Zend/Markup/Parser/Exception.php';
  146. throw new Zend_Markup_Parser_Exception('Value to parse cannot be left empty.');
  147. }
  148. $this->_value = str_replace(array("\r\n", "\r", "\n"), self::NEWLINE, $value);
  149. // variable initialization for tokenizer
  150. $this->_valueLen = strlen($this->_value);
  151. $this->_pointer = 0;
  152. $this->_buffer = '';
  153. $this->_temp = array();
  154. $this->_state = self::STATE_SCAN;
  155. $this->_tokens = array();
  156. $this->_tokens = array();
  157. $this->_tokenize();
  158. // variable initialization for treebuilder
  159. $this->_searchedStoppers = array();
  160. $this->_tree = new Zend_Markup_TokenList();
  161. $this->_current = new Zend_Markup_Token(
  162. '',
  163. Zend_Markup_Token::TYPE_NONE,
  164. 'Zend_Markup_Root'
  165. );
  166. $this->_tree->addChild($this->_current);
  167. $this->_createTree();
  168. return $this->_tree;
  169. }
  170. /**
  171. * Tokenize
  172. *
  173. * @param string $input
  174. *
  175. * @return void
  176. */
  177. protected function _tokenize()
  178. {
  179. $attribute = '';
  180. while ($this->_pointer < $this->_valueLen) {
  181. switch ($this->_state) {
  182. case self::STATE_SCAN:
  183. $matches = array();
  184. $regex = '#\G(?<text>[^\[]*)(?<open>\[(?<name>[' . self::NAME_CHARSET . ']+)?)?#';
  185. preg_match($regex, $this->_value, $matches, null, $this->_pointer);
  186. $this->_pointer += strlen($matches[0]);
  187. if (!empty($matches['text'])) {
  188. $this->_buffer .= $matches['text'];
  189. }
  190. if (!isset($matches['open'])) {
  191. // great, no tag, we are ending the string
  192. break;
  193. }
  194. if (!isset($matches['name'])) {
  195. $this->_buffer .= $matches['open'];
  196. break;
  197. }
  198. $this->_temp = array(
  199. 'tag' => '[' . $matches['name'],
  200. 'name' => $matches['name'],
  201. 'attributes' => array()
  202. );
  203. if ($this->_pointer >= $this->_valueLen) {
  204. // damn, no tag
  205. $this->_buffer .= $this->_temp['tag'];
  206. break 2;
  207. }
  208. if ($this->_value[$this->_pointer] == '=') {
  209. $this->_pointer++;
  210. $this->_temp['tag'] .= '=';
  211. $this->_state = self::STATE_PARSEVALUE;
  212. $attribute = $this->_temp['name'];
  213. } else {
  214. $this->_state = self::STATE_SCANATTRS;
  215. }
  216. break;
  217. case self::STATE_SCANATTRS:
  218. $matches = array();
  219. $regex = '#\G((?<end>\s*\])|\s+(?<attribute>[' . self::NAME_CHARSET . ']+)(?<eq>=?))#';
  220. if (!preg_match($regex, $this->_value, $matches, null, $this->_pointer)) {
  221. break 2;
  222. }
  223. $this->_pointer += strlen($matches[0]);
  224. if (!empty($matches['end'])) {
  225. if (!empty($this->_buffer)) {
  226. $this->_tokens[] = array(
  227. 'tag' => $this->_buffer,
  228. 'type' => Zend_Markup_Token::TYPE_NONE
  229. );
  230. $this->_buffer = '';
  231. }
  232. $this->_temp['tag'] .= $matches['end'];
  233. $this->_temp['type'] = Zend_Markup_Token::TYPE_TAG;
  234. $this->_tokens[] = $this->_temp;
  235. $this->_temp = array();
  236. $this->_state = self::STATE_SCAN;
  237. } else {
  238. // attribute name
  239. $attribute = $matches['attribute'];
  240. $this->_temp['tag'] .= $matches[0];
  241. $this->_temp['attributes'][$attribute] = '';
  242. if (empty($matches['eq'])) {
  243. $this->_state = self::STATE_SCANATTRS;
  244. } else {
  245. $this->_state = self::STATE_PARSEVALUE;
  246. }
  247. }
  248. break;
  249. case self::STATE_PARSEVALUE:
  250. $matches = array();
  251. $regex = '#\G((?<quote>"|\')(?<valuequote>[^\\2]*)\\2|(?<value>[^\]\s]+))#';
  252. if (!preg_match($regex, $this->_value, $matches, null, $this->_pointer)) {
  253. $this->_state = self::STATE_SCANATTRS;
  254. break;
  255. }
  256. $this->_pointer += strlen($matches[0]);
  257. if (!empty($matches['quote'])) {
  258. $this->_temp['attributes'][$attribute] = $matches['valuequote'];
  259. } else {
  260. $this->_temp['attributes'][$attribute] = $matches['value'];
  261. }
  262. $this->_temp['tag'] .= $matches[0];
  263. $this->_state = self::STATE_SCANATTRS;
  264. break;
  265. }
  266. }
  267. if (!empty($this->_buffer)) {
  268. $this->_tokens[] = array(
  269. 'tag' => $this->_buffer,
  270. 'type' => Zend_Markup_Token::TYPE_NONE
  271. );
  272. }
  273. }
  274. /**
  275. * Parse the token array into a tree
  276. *
  277. * @param array $tokens
  278. *
  279. * @return void
  280. */
  281. public function _createTree()
  282. {
  283. foreach ($this->_tokens as $token) {
  284. // first we want to know if this tag is a stopper, or at least a searched one
  285. if ($this->_isStopper($token['tag'])) {
  286. // find the stopper
  287. $oldItems = array();
  288. while (!in_array($token['tag'], $this->_tags[$this->_current->getName()]['stoppers'])) {
  289. $oldItems[] = clone $this->_current;
  290. $this->_current = $this->_current->getParent();
  291. }
  292. // we found the stopper, so stop the tag
  293. $this->_current->setStopper($token['tag']);
  294. $this->_removeFromSearchedStoppers($this->_current);
  295. $this->_current = $this->_current->getParent();
  296. // add the old items again if there are any
  297. if (!empty($oldItems)) {
  298. foreach (array_reverse($oldItems) as $item) {
  299. /* @var $token Zend_Markup_Token */
  300. $this->_current->addChild($item);
  301. $item->setParent($this->_current);
  302. $this->_current = $item;
  303. }
  304. }
  305. } else {
  306. if ($token['type'] == Zend_Markup_Token::TYPE_TAG) {
  307. if ($token['tag'] == self::NEWLINE) {
  308. // this is a newline tag, add it as a token
  309. $this->_current->addChild(new Zend_Markup_Token(
  310. "\n",
  311. Zend_Markup_Token::TYPE_NONE,
  312. '',
  313. array(),
  314. $this->_current
  315. ));
  316. } elseif (isset($token['name']) && ($token['name'][0] == '/')) {
  317. // this is a stopper, add it as a empty token
  318. $this->_current->addChild(new Zend_Markup_Token(
  319. $token['tag'],
  320. Zend_Markup_Token::TYPE_NONE,
  321. '',
  322. array(),
  323. $this->_current
  324. ));
  325. } else {
  326. // add the tag
  327. $child = new Zend_Markup_Token(
  328. $token['tag'],
  329. $token['type'],
  330. $token['name'],
  331. $token['attributes'],
  332. $this->_current
  333. );
  334. $this->_current->addChild($child);
  335. // add stoppers for this tag, if its has stoppers
  336. if ($this->_getType($token['name']) == self::TYPE_DEFAULT) {
  337. $this->_current = $child;
  338. $this->_addToSearchedStoppers($this->_current);
  339. }
  340. }
  341. } else {
  342. // no tag, just add it as a simple token
  343. $this->_current->addChild(new Zend_Markup_Token(
  344. $token['tag'],
  345. Zend_Markup_Token::TYPE_NONE,
  346. '',
  347. array(),
  348. $this->_current
  349. ));
  350. }
  351. }
  352. }
  353. }
  354. /**
  355. * Check if there is a tag declaration, and if it isnt there, add it
  356. *
  357. * @param string $name
  358. *
  359. * @return void
  360. */
  361. protected function _checkTagDeclaration($name)
  362. {
  363. if (!isset($this->_tags[$name])) {
  364. $this->_tags[$name] = array(
  365. 'type' => self::TYPE_DEFAULT,
  366. 'stoppers' => array(
  367. '[/' . $name . ']',
  368. '[/]'
  369. )
  370. );
  371. }
  372. }
  373. /**
  374. * Check the tag's type
  375. *
  376. * @param string $name
  377. * @return string
  378. */
  379. protected function _getType($name)
  380. {
  381. $this->_checkTagDeclaration($name);
  382. return $this->_tags[$name]['type'];
  383. }
  384. /**
  385. * Check if the tag is a stopper
  386. *
  387. * @param string $tag
  388. * @return bool
  389. */
  390. protected function _isStopper($tag)
  391. {
  392. $this->_checkTagDeclaration($this->_current->getName());
  393. if (!empty($this->_searchedStoppers[$tag])) {
  394. return true;
  395. }
  396. return false;
  397. }
  398. /**
  399. * Add to searched stoppers
  400. *
  401. * @param Zend_Markup_Token $token
  402. * @return void
  403. */
  404. protected function _addToSearchedStoppers(Zend_Markup_Token $token)
  405. {
  406. $this->_checkTagDeclaration($token->getName());
  407. foreach ($this->_tags[$token->getName()]['stoppers'] as $stopper) {
  408. if (!isset($this->_searchedStoppers[$stopper])) {
  409. $this->_searchedStoppers[$stopper] = 0;
  410. }
  411. ++$this->_searchedStoppers[$stopper];
  412. }
  413. }
  414. /**
  415. * Remove from searched stoppers
  416. *
  417. * @param Zend_Markup_Token $token
  418. * @return void
  419. */
  420. protected function _removeFromSearchedStoppers(Zend_Markup_Token $token)
  421. {
  422. $this->_checkTagDeclaration($token->getName());
  423. foreach ($this->_tags[$token->getName()]['stoppers'] as $stopper) {
  424. --$this->_searchedStoppers[$stopper];
  425. }
  426. }
  427. }