Source for file stringparser_bbcode.class.php

Documentation is available at stringparser_bbcode.class.php

  1. <?php
  2. /**
  3. * BB code string parsing class
  4. *
  5. * Version: 0.3.0
  6. *
  7. * @author Christian Seiler <spam@christian-seiler.de>
  8. * @copyright Christian Seiler 2006
  9. * @package stringparser
  10. *
  11. * This program is free software; you can redistribute it and/or modify
  12. * it under the terms of either:
  13. *
  14. * a) the GNU General Public License as published by the Free
  15. * Software Foundation; either version 1, or (at your option) any
  16. * later version, or
  17. *
  18. * b) the Artistic License as published by Larry Wall, either version 2.0,
  19. * or (at your option) any later version.
  20. *
  21. * This program is distributed in the hope that it will be useful,
  22. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  23. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either
  24. * the GNU General Public License or the Artistic License for more details.
  25. *
  26. * You should have received a copy of the Artistic License with this Kit,
  27. * in the file named "Artistic.clarified". If not, I'll be glad to provide
  28. * one.
  29. *
  30. * You should also have received a copy of the GNU General Public License
  31. * along with this program in the file named "COPYING"; if not, write to
  32. * the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
  33. * MA 02111-1307, USA.
  34. */
  35. require_once dirname(__FILE__).'/stringparser.class.php';
  36.  
  37. define ('BBCODE_CLOSETAG_FORBIDDEN', -1);
  38. define ('BBCODE_CLOSETAG_OPTIONAL', 0);
  39. define ('BBCODE_CLOSETAG_IMPLICIT', 1);
  40. define ('BBCODE_CLOSETAG_IMPLICIT_ON_CLOSE_ONLY', 2);
  41. define ('BBCODE_CLOSETAG_MUSTEXIST', 3);
  42.  
  43. define ('BBCODE_NEWLINE_PARSE', 0);
  44. define ('BBCODE_NEWLINE_IGNORE', 1);
  45. define ('BBCODE_NEWLINE_DROP', 2);
  46.  
  47. define ('BBCODE_PARAGRAPH_ALLOW_BREAKUP', 0);
  48. define ('BBCODE_PARAGRAPH_ALLOW_INSIDE', 1);
  49. define ('BBCODE_PARAGRAPH_BLOCK_ELEMENT', 2);
  50.  
  51. /**
  52. * BB code string parser class
  53. *
  54. * @package stringparser
  55. */
  56. class StringParser_BBCode extends StringParser {
  57. /**
  58. * String parser mode
  59. *
  60. * The BBCode string parser works in search mode
  61. *
  62. * @access protected
  63. * @var int
  64. * @see STRINGPARSER_MODE_SEARCH, STRINGPARSER_MODE_LOOP
  65. */
  66. var $_parserMode = STRINGPARSER_MODE_SEARCH;
  67. /**
  68. * Defined BB Codes
  69. *
  70. * The registered BB codes
  71. *
  72. * @access protected
  73. * @var array
  74. */
  75. var $_codes = array ();
  76. /**
  77. * Registered parsers
  78. *
  79. * @access protected
  80. * @var array
  81. */
  82. var $_parsers = array ();
  83. /**
  84. * Defined maximum occurrences
  85. *
  86. * @access protected
  87. * @var array
  88. */
  89. var $_maxOccurrences = array ();
  90. /**
  91. * Root content type
  92. *
  93. * @access protected
  94. * @var string
  95. */
  96. var $_rootContentType = 'block';
  97. /**
  98. * Do not output but return the tree
  99. *
  100. * @access protected
  101. * @var bool
  102. */
  103. var $_noOutput = false;
  104. /**
  105. * Global setting: case sensitive
  106. *
  107. * @access protected
  108. * @var bool
  109. */
  110. var $_caseSensitive = true;
  111. /**
  112. * Root paragraph handling enabled
  113. *
  114. * @access protected
  115. * @var bool
  116. */
  117. var $_rootParagraphHandling = false;
  118. /**
  119. * Paragraph handling parameters
  120. * @access protected
  121. * @var array
  122. */
  123. var $_paragraphHandling = array (
  124. 'detect_string' => "\n\n",
  125. 'start_tag' => '<p>',
  126. 'end_tag' => "</p>\n"
  127. );
  128. /**
  129. * Allow mixed attribute types (e.g. [code=bla attr=blub])
  130. * @access private
  131. * @var bool
  132. */
  133. var $_mixedAttributeTypes = false;
  134. /**
  135. * Whether to call validation function again (with $action == 'validate_auto') when closetag comes
  136. * @access protected
  137. * @var bool
  138. */
  139. var $_validateAgain = false;
  140. /**
  141. * Add a code
  142. *
  143. * @access public
  144. * @param string $name The name of the code
  145. * @param string $callback_type See documentation
  146. * @param string $callback_func The callback function to call
  147. * @param array $callback_params The callback parameters
  148. * @param string $content_type See documentation
  149. * @param array $allowed_within See documentation
  150. * @param array $not_allowed_within See documentation
  151. * @return bool
  152. */
  153. function addCode ($name, $callback_type, $callback_func, $callback_params, $content_type, $allowed_within, $not_allowed_within) {
  154. if (isset ($this->_codes[$name])) {
  155. return false; // already exists
  156. }
  157. if (!preg_match ('/^[a-zA-Z0-9*_!+-]+$/', $name)) {
  158. return false; // invalid
  159. }
  160. $this->_codes[$name] = array (
  161. 'name' => $name,
  162. 'callback_type' => $callback_type,
  163. 'callback_func' => $callback_func,
  164. 'callback_params' => $callback_params,
  165. 'content_type' => $content_type,
  166. 'allowed_within' => $allowed_within,
  167. 'not_allowed_within' => $not_allowed_within,
  168. 'flags' => array ()
  169. );
  170. return true;
  171. }
  172. /**
  173. * Remove a code
  174. *
  175. * @access public
  176. * @param $name The code to remove
  177. * @return bool
  178. */
  179. function removeCode ($name) {
  180. if (isset ($this->_codes[$name])) {
  181. unset ($this->_codes[$name]);
  182. return true;
  183. }
  184. return false;
  185. }
  186. /**
  187. * Remove all codes
  188. *
  189. * @access public
  190. */
  191. function removeAllCodes () {
  192. $this->_codes = array ();
  193. }
  194. /**
  195. * Set a code flag
  196. *
  197. * @access public
  198. * @param string $name The name of the code
  199. * @param string $flag The name of the flag to set
  200. * @param mixed $value The value of the flag to set
  201. * @return bool
  202. */
  203. function setCodeFlag ($name, $flag, $value) {
  204. if (!isset ($this->_codes[$name])) {
  205. return false;
  206. }
  207. $this->_codes[$name]['flags'][$flag] = $value;
  208. return true;
  209. }
  210. /**
  211. * Set occurrence type
  212. *
  213. * Example:
  214. * $bbcode->setOccurrenceType ('url', 'link');
  215. * $bbcode->setMaxOccurrences ('link', 4);
  216. * Would create the situation where a link may only occur four
  217. * times in the hole text.
  218. *
  219. * @access public
  220. * @param string $code The name of the code
  221. * @param string $type The name of the occurrence type to set
  222. * @return bool
  223. */
  224. function setOccurrenceType ($code, $type) {
  225. return $this->setCodeFlag ($code, 'occurrence_type', $type);
  226. }
  227. /**
  228. * Set maximum number of occurrences
  229. *
  230. * @access public
  231. * @param string $type The name of the occurrence type
  232. * @param int $count The maximum number of occurrences
  233. * @return bool
  234. */
  235. function setMaxOccurrences ($type, $count) {
  236. settype ($count, 'integer');
  237. if ($count < 0) { // sorry, does not make any sense
  238. return false;
  239. }
  240. $this->_maxOccurrences[$type] = $count;
  241. return true;
  242. }
  243. /**
  244. * Add a parser
  245. *
  246. * @access public
  247. * @param string $type The content type for which the parser is to add
  248. * @param mixed $parser The function to call
  249. * @return bool
  250. */
  251. function addParser ($type, $parser) {
  252. if (is_array ($type)) {
  253. foreach ($type as $t) {
  254. $this->addParser ($t, $parser);
  255. }
  256. return true;
  257. }
  258. if (!isset ($this->_parsers[$type])) {
  259. $this->_parsers[$type] = array ();
  260. }
  261. $this->_parsers[$type][] = $parser;
  262. return true;
  263. }
  264. /**
  265. * Set root content type
  266. *
  267. * @access public
  268. * @param string $content_type The new root content type
  269. */
  270. function setRootContentType ($content_type) {
  271. $this->_rootContentType = $content_type;
  272. }
  273. /**
  274. * Set paragraph handling on root element
  275. *
  276. * @access public
  277. * @param bool $enabled The new status of paragraph handling on root element
  278. */
  279. function setRootParagraphHandling ($enabled) {
  280. $this->_rootParagraphHandling = (bool)$enabled;
  281. }
  282. /**
  283. * Set paragraph handling parameters
  284. *
  285. * @access public
  286. * @param string $detect_string The string to detect
  287. * @param string $start_tag The replacement for the start tag (e.g. <p>)
  288. * @param string $end_tag The replacement for the start tag (e.g. </p>)
  289. */
  290. function setParagraphHandlingParameters ($detect_string, $start_tag, $end_tag) {
  291. $this->_paragraphHandling = array (
  292. 'detect_string' => $detect_string,
  293. 'start_tag' => $start_tag,
  294. 'end_tag' => $end_tag
  295. );
  296. }
  297. /**
  298. * Set global case sensitive flag
  299. *
  300. * If this is set to true, the class normally is case sensitive, but
  301. * the case_sensitive code flag may override this for a single code.
  302. *
  303. * If this is set to false, all codes are case insensitive.
  304. *
  305. * @access public
  306. * @param bool $caseSensitive
  307. */
  308. function setGlobalCaseSensitive ($caseSensitive) {
  309. $this->_caseSensitive = (bool)$caseSensitive;
  310. }
  311. /**
  312. * Get global case sensitive flag
  313. *
  314. * @access public
  315. * @return bool
  316. */
  317. function globalCaseSensitive () {
  318. return $this->_caseSensitive;
  319. }
  320. /**
  321. * Set mixed attribute types flag
  322. *
  323. * If set, [code=val1 attr=val2] will cause 2 attributes to be parsed:
  324. * 'default' will have value 'val1', 'attr' will have value 'val2'.
  325. * If not set, only one attribute 'default' will have the value
  326. * 'val1 attr=val2' (the default and original behaviour)
  327. *
  328. * @access public
  329. * @param bool $mixedAttributeTypes
  330. */
  331. function setMixedAttributeTypes ($mixedAttributeTypes) {
  332. $this->_mixedAttributeTypes = (bool)$mixedAttributeTypes;
  333. }
  334. /**
  335. * Get mixed attribute types flag
  336. *
  337. * @access public
  338. * @return bool
  339. */
  340. function mixedAttributeTypes () {
  341. return $this->_mixedAttributeTypes;
  342. }
  343. /**
  344. * Set validate again flag
  345. *
  346. * If this is set to true, the class calls the validation function
  347. * again with $action == 'validate_again' when closetag comes.
  348. *
  349. * @access public
  350. * @param bool $validateAgain
  351. */
  352. function setValidateAgain ($validateAgain) {
  353. $this->_validateAgain = (bool)$validateAgain;
  354. }
  355. /**
  356. * Get validate again flag
  357. *
  358. * @access public
  359. * @return bool
  360. */
  361. function validateAgain () {
  362. return $this->_validateAgain;
  363. }
  364. /**
  365. * Get a code flag
  366. *
  367. * @access public
  368. * @param string $name The name of the code
  369. * @param string $flag The name of the flag to get
  370. * @param string $type The type of the return value
  371. * @param mixed $default The default return value
  372. * @return bool
  373. */
  374. function getCodeFlag ($name, $flag, $type = 'mixed', $default = null) {
  375. if (!isset ($this->_codes[$name])) {
  376. return $default;
  377. }
  378. if (!array_key_exists ($flag, $this->_codes[$name]['flags'])) {
  379. return $default;
  380. }
  381. $return = $this->_codes[$name]['flags'][$flag];
  382. if ($type != 'mixed') {
  383. settype ($return, $type);
  384. }
  385. return $return;
  386. }
  387. /**
  388. * Set a specific status
  389. * @access protected
  390. */
  391. function _setStatus ($status) {
  392. switch ($status) {
  393. case 0:
  394. $this->_charactersSearch = array ('[/', '[');
  395. $this->_status = $status;
  396. break;
  397. case 1:
  398. $this->_charactersSearch = array (']', ' = "', '="', ' = \'', '=\'', ' = ', '=', ': ', ':', ' ');
  399. $this->_status = $status;
  400. break;
  401. case 2:
  402. $this->_charactersSearch = array (']');
  403. $this->_status = $status;
  404. $this->_savedName = '';
  405. break;
  406. case 3:
  407. if ($this->_quoting !== null) {
  408. if ($this->_mixedAttributeTypes) {
  409. $this->_charactersSearch = array ('\\\\', '\\'.$this->_quoting, $this->_quoting.' ', $this->_quoting.']', $this->_quoting);
  410. } else {
  411. $this->_charactersSearch = array ('\\\\', '\\'.$this->_quoting, $this->_quoting.']', $this->_quoting);
  412. }
  413. $this->_status = $status;
  414. break;
  415. }
  416. if ($this->_mixedAttributeTypes) {
  417. $this->_charactersSearch = array (' ', ']');
  418. } else {
  419. $this->_charactersSearch = array (']');
  420. }
  421. $this->_status = $status;
  422. break;
  423. case 4:
  424. $this->_charactersSearch = array (' ', ']', '="', '=\'', '=');
  425. $this->_status = $status;
  426. $this->_savedName = '';
  427. $this->_savedValue = '';
  428. break;
  429. case 5:
  430. if ($this->_quoting !== null) {
  431. $this->_charactersSearch = array ('\\\\', '\\'.$this->_quoting, $this->_quoting.' ', $this->_quoting.']', $this->_quoting);
  432. } else {
  433. $this->_charactersSearch = array (' ', ']');
  434. }
  435. $this->_status = $status;
  436. $this->_savedValue = '';
  437. break;
  438. case 7:
  439. $this->_charactersSearch = array ('[/'.$this->_topNode ('name').']');
  440. if (!$this->_topNode ('getFlag', 'case_sensitive', 'boolean', true) || !$this->_caseSensitive) {
  441. $this->_charactersSearch[] = '[/';
  442. }
  443. $this->_status = $status;
  444. break;
  445. default:
  446. return false;
  447. }
  448. return true;
  449. }
  450. /**
  451. * Abstract method Append text depending on current status
  452. * @access protected
  453. * @param string $text The text to append
  454. * @return bool On success, the function returns true, else false
  455. */
  456. function _appendText ($text) {
  457. if (!strlen ($text)) {
  458. return true;
  459. }
  460. switch ($this->_status) {
  461. case 0:
  462. case 7:
  463. return $this->_appendToLastTextChild ($text);
  464. case 1:
  465. return $this->_topNode ('appendToName', $text);
  466. case 2:
  467. case 4:
  468. $this->_savedName .= $text;
  469. return true;
  470. case 3:
  471. return $this->_topNode ('appendToAttribute', 'default', $text);
  472. case 5:
  473. $this->_savedValue .= $text;
  474. return true;
  475. default:
  476. return false;
  477. }
  478. }
  479. /**
  480. * Restart parsing after current block
  481. *
  482. * To achieve this the current top stack object is removed from the
  483. * tree. Then the current item
  484. *
  485. * @access protected
  486. * @return bool
  487. */
  488. function _reparseAfterCurrentBlock () {
  489. if ($this->_status == 2) {
  490. // this status will *never* call _reparseAfterCurrentBlock itself
  491. // so this is called if the loop ends
  492. // therefore, just add the [/ to the text
  493. // _savedName should be empty but just in case
  494. $this->_cpos -= strlen ($this->_savedName);
  495. $this->_savedName = '';
  496. $this->_status = 0;
  497. $this->_appendText ('[/');
  498. return true;
  499. } else {
  500. return parent::_reparseAfterCurrentBlock ();
  501. }
  502. }
  503. /**
  504. * Apply parsers
  505. */
  506. function _applyParsers ($type, $text) {
  507. if (!isset ($this->_parsers[$type])) {
  508. return $text;
  509. }
  510. foreach ($this->_parsers[$type] as $parser) {
  511. if (is_callable ($parser)) {
  512. $ntext = call_user_func ($parser, $text);
  513. if (is_string ($ntext)) {
  514. $text = $ntext;
  515. }
  516. }
  517. }
  518. return $text;
  519. }
  520. /**
  521. * Handle status
  522. * @access protected
  523. * @param int $status The current status
  524. * @param string $needle The needle that was found
  525. * @return bool
  526. */
  527. function _handleStatus ($status, $needle) {
  528. switch ($status) {
  529. case 0: // NORMAL TEXT
  530. if ($needle != '[' && $needle != '[/') {
  531. $this->_appendText ($needle);
  532. return true;
  533. }
  534. if ($needle == '[') {
  535. $node =& new StringParser_BBCode_Node_Element ($this->_cpos);
  536. $res = $this->_pushNode ($node);
  537. if (!$res) {
  538. return false;
  539. }
  540. $this->_setStatus (1);
  541. } else if ($needle == '[/') {
  542. if (count ($this->_stack) <= 1) {
  543. $this->_appendText ($needle);
  544. return true;
  545. }
  546. $this->_setStatus (2);
  547. }
  548. break;
  549. case 1: // OPEN TAG
  550. if ($needle == ']') {
  551. return $this->_openElement (0);
  552. } else if (trim ($needle) == ':' || trim ($needle) == '=') {
  553. $this->_quoting = null;
  554. $this->_setStatus (3); // default value parser
  555. break;
  556. } else if (trim ($needle) == '="' || trim ($needle) == '= "' || trim ($needle) == '=\'' || trim ($needle) == '= \'') {
  557. $this->_quoting = substr (trim ($needle), -1);
  558. $this->_setStatus (3); // default value parser with quotation
  559. break;
  560. } else if ($needle == ' ') {
  561. $this->_setStatus (4); // attribute parser
  562. break;
  563. } else {
  564. $this->_appendText ($needle);
  565. return true;
  566. }
  567. break;
  568. case 2: // CLOSE TAG
  569. if ($needle != ']') {
  570. $this->_appendText ($needle);
  571. return true;
  572. }
  573. $closecount = 0;
  574. if (!$this->_isCloseable ($this->_savedName, $closecount)) {
  575. $this->_setStatus (0);
  576. $this->_appendText ('[/'.$this->_savedName.$needle);
  577. return true;
  578. }
  579. // this validates the code(s) to be closed after the content tree of
  580. // that code(s) are built - if the second validation fails, we will have
  581. // to reparse. note that as _reparseAfterCurrentBlock will not work correctly
  582. // if we're in $status == 2, we will have to set our status to 0 manually
  583. if (!$this->_validateCloseTags ($closecount)) {
  584. $this->_setStatus (0);
  585. return $this->_reparseAfterCurrentBlock ();
  586. }
  587. $this->_setStatus (0);
  588. for ($i = 0; $i < $closecount; $i++) {
  589. if ($i == $closecount - 1) {
  590. $this->_topNode ('setHadCloseTag');
  591. }
  592. if (!$this->_popNode ()) {
  593. return false;
  594. }
  595. }
  596. break;
  597. case 3: // DEFAULT ATTRIBUTE
  598. if ($this->_quoting !== null) {
  599. if ($needle == '\\\\') {
  600. $this->_appendText ('\\');
  601. return true;
  602. } else if ($needle == '\\'.$this->_quoting) {
  603. $this->_appendText ($this->_quoting);
  604. return true;
  605. } else if ($needle == $this->_quoting.' ') {
  606. $this->_setStatus (4);
  607. return true;
  608. } else if ($needle == $this->_quoting.']') {
  609. return $this->_openElement (2);
  610. } else if ($needle == $this->_quoting) {
  611. // can't be, only ']' and ' ' allowed after quoting char
  612. return $this->_reparseAfterCurrentBlock ();
  613. } else {
  614. $this->_appendText ($needle);
  615. return true;
  616. }
  617. } else {
  618. if ($needle == ' ') {
  619. $this->_setStatus (4);
  620. return true;
  621. } else if ($needle == ']') {
  622. return $this->_openElement (2);
  623. } else {
  624. $this->_appendText ($needle);
  625. return true;
  626. }
  627. }
  628. // break not needed because every if clause contains return!
  629. case 4: // ATTRIBUTE NAME
  630. if ($needle == ' ') {
  631. if (strlen ($this->_savedName)) {
  632. $this->_topNode ('setAttribute', $this->_savedName, true);
  633. }
  634. // just ignore and continue in same mode
  635. $this->_setStatus (4); // reset parameters
  636. return true;
  637. } else if ($needle == ']') {
  638. if (strlen ($this->_savedName)) {
  639. $this->_topNode ('setAttribute', $this->_savedName, true);
  640. }
  641. return $this->_openElement (2);
  642. } else if ($needle == '=') {
  643. $this->_quoting = null;
  644. $this->_setStatus (5);
  645. return true;
  646. } else if ($needle == '="') {
  647. $this->_quoting = '"';
  648. $this->_setStatus (5);
  649. return true;
  650. } else if ($needle == '=\'') {
  651. $this->_quoting = '\'';
  652. $this->_setStatus (5);
  653. return true;
  654. } else {
  655. $this->_appendText ($needle);
  656. return true;
  657. }
  658. // break not needed because every if clause contains return!
  659. case 5: // ATTRIBUTE VALUE
  660. if ($this->_quoting !== null) {
  661. if ($needle == '\\\\') {
  662. $this->_appendText ('\\');
  663. return true;
  664. } else if ($needle == '\\'.$this->_quoting) {
  665. $this->_appendText ($this->_quoting);
  666. return true;
  667. } else if ($needle == $this->_quoting.' ') {
  668. $this->_topNode ('setAttribute', $this->_savedName, $this->_savedValue);
  669. $this->_setStatus (4);
  670. return true;
  671. } else if ($needle == $this->_quoting.']') {
  672. $this->_topNode ('setAttribute', $this->_savedName, $this->_savedValue);
  673. return $this->_openElement (2);
  674. } else if ($needle == $this->_quoting) {
  675. // can't be, only ']' and ' ' allowed after quoting char
  676. return $this->_reparseAfterCurrentBlock ();
  677. } else {
  678. $this->_appendText ($needle);
  679. return true;
  680. }
  681. } else {
  682. if ($needle == ' ') {
  683. $this->_topNode ('setAttribute', $this->_savedName, $this->_savedValue);
  684. $this->_setStatus (4);
  685. return true;
  686. } else if ($needle == ']') {
  687. $this->_topNode ('setAttribute', $this->_savedName, $this->_savedValue);
  688. return $this->_openElement (2);
  689. } else {
  690. $this->_appendText ($needle);
  691. return true;
  692. }
  693. }
  694. // break not needed because every if clause contains return!
  695. case 7:
  696. if ($needle == '[/') {
  697. // this was case insensitive match
  698. if (strtolower (substr ($this->_text, $this->_cpos + strlen ($needle), strlen ($this->_topNode ('name')) + 1)) == strtolower ($this->_topNode ('name').']')) {
  699. // this matched
  700. $this->_cpos += strlen ($this->_topNode ('name')) + 1;
  701. } else {
  702. // it didn't match
  703. $this->_appendText ($needle);
  704. return true;
  705. }
  706. }
  707. $closecount = $this->_savedCloseCount;
  708. if (!$this->_topNode ('validate')) {
  709. return $this->_reparseAfterCurrentBlock ();
  710. }
  711. // do we have to close subnodes?
  712. if ($closecount) {
  713. // get top node
  714. $mynode =& $this->_stack[count ($this->_stack)-1];
  715. // close necessary nodes
  716. for ($i = 0; $i <= $closecount; $i++) {
  717. if (!$this->_popNode ()) {
  718. return false;
  719. }
  720. }
  721. if (!$this->_pushNode ($mynode)) {
  722. return false;
  723. }
  724. }
  725. $this->_setStatus (0);
  726. $this->_popNode ();
  727. return true;
  728. default:
  729. return false;
  730. }
  731. return true;
  732. }
  733. /**
  734. * Open the next element
  735. *
  736. * @access protected
  737. * @return bool
  738. */
  739. function _openElement ($type = 0) {
  740. $name = $this->_topNode ('name');
  741. if (!isset ($this->_codes[$name])) {
  742. if (isset ($this->_codes[strtolower ($name)]) && (!$this->getCodeFlag (strtolower ($name), 'case_sensitive', 'boolean', true) || !$this->_caseSensitive)) {
  743. $name = strtolower ($name);
  744. } else {
  745. return $this->_reparseAfterCurrentBlock ();
  746. }
  747. }
  748. $occ_type = $this->getCodeFlag ($name, 'occurrence_type', 'string');
  749. if ($occ_type !== null && isset ($this->_maxOccurrences[$occ_type])) {
  750. $max_occs = $this->_maxOccurrences[$occ_type];
  751. $occs = $this->_root->getNodeCountByCriterium ('flag:occurrence_type', $occ_type);
  752. if ($occs >= $max_occs) {
  753. return $this->_reparseAfterCurrentBlock ();
  754. }
  755. }
  756. $closecount = 0;
  757. $this->_topNode ('setCodeInfo', $this->_codes[$name]);
  758. if (!$this->_isOpenable ($name, $closecount)) {
  759. return $this->_reparseAfterCurrentBlock ();
  760. }
  761. $this->_setStatus (0);
  762. switch ($type) {
  763. case 0:
  764. $cond = $this->_isUseContent ($this->_stack[count($this->_stack)-1], false);
  765. break;
  766. case 1:
  767. $cond = $this->_isUseContent ($this->_stack[count($this->_stack)-1], true);
  768. break;
  769. case 2:
  770. $cond = $this->_isUseContent ($this->_stack[count($this->_stack)-1], true);
  771. break;
  772. default:
  773. $cond = false;
  774. break;
  775. }
  776. if ($cond) {
  777. $this->_savedCloseCount = $closecount;
  778. $this->_setStatus (7);
  779. return true;
  780. }
  781. if (!$this->_topNode ('validate')) {
  782. return $this->_reparseAfterCurrentBlock ();
  783. }
  784. // do we have to close subnodes?
  785. if ($closecount) {
  786. // get top node
  787. $mynode =& $this->_stack[count ($this->_stack)-1];
  788. // close necessary nodes
  789. for ($i = 0; $i <= $closecount; $i++) {
  790. if (!$this->_popNode ()) {
  791. return false;
  792. }
  793. }
  794. if (!$this->_pushNode ($mynode)) {
  795. return false;
  796. }
  797. }
  798. if ($this->_codes[$name]['callback_type'] == 'simple_replace_single' || $this->_codes[$name]['callback_type'] == 'callback_replace_single') {
  799. if (!$this->_popNode ()) {
  800. return false;
  801. }
  802. }
  803. return true;
  804. }
  805. /**
  806. * Is a node closeable?
  807. *
  808. * @access protected
  809. * @return bool
  810. */
  811. function _isCloseable ($name, &$closecount) {
  812. $node =& $this->_findNamedNode ($name, false);
  813. if ($node === false) {
  814. return false;
  815. }
  816. $scount = count ($this->_stack);
  817. for ($i = $scount - 1; $i > 0; $i--) {
  818. $closecount++;
  819. if ($this->_stack[$i]->equals ($node)) {
  820. return true;
  821. }
  822. if ($this->_stack[$i]->getFlag ('closetag', 'integer', BBCODE_CLOSETAG_IMPLICIT) == BBCODE_CLOSETAG_MUSTEXIST) {
  823. return false;
  824. }
  825. }
  826. return false;
  827. }
  828. /**
  829. * Revalidate codes when close tags appear
  830. *
  831. * @access protected
  832. * @return bool
  833. */
  834. function _validateCloseTags ($closecount) {
  835. $scount = count ($this->_stack);
  836. for ($i = $scount - 1; $i >= $scount - $closecount; $i--) {
  837. if ($this->_validateAgain) {
  838. if (!$this->_stack[$i]->validate ('validate_again')) {
  839. return false;
  840. }
  841. }
  842. }
  843. return true;
  844. }
  845. /**
  846. * Is a node openable?
  847. *
  848. * @access protected
  849. * @return bool
  850. */
  851. function _isOpenable ($name, &$closecount) {
  852. if (!isset ($this->_codes[$name])) {
  853. return false;
  854. }
  855. $closecount = 0;
  856. $allowed_within = $this->_codes[$name]['allowed_within'];
  857. $not_allowed_within = $this->_codes[$name]['not_allowed_within'];
  858. $scount = count ($this->_stack);
  859. if ($scount == 2) { // top level element
  860. if (!in_array ($this->_rootContentType, $allowed_within)) {
  861. return false;
  862. }
  863. } else {
  864. if (!in_array ($this->_stack[$scount-2]->_codeInfo['content_type'], $allowed_within)) {
  865. return $this->_isOpenableWithClose ($name, $closecount);
  866. }
  867. }
  868. for ($i = 1; $i < $scount - 1; $i++) {
  869. if (in_array ($this->_stack[$i]->_codeInfo['content_type'], $not_allowed_within)) {
  870. return $this->_isOpenableWithClose ($name, $closecount);
  871. }
  872. }
  873. return true;
  874. }
  875. /**
  876. * Is a node openable by closing other nodes?
  877. *
  878. * @access protected
  879. * @return bool
  880. */
  881. function _isOpenableWithClose ($name, &$closecount) {
  882. $tnname = $this->_topNode ('name');
  883. if (isset ($this->_codes[strtolower($tnname)]) && (!$this->getCodeFlag (strtolower($tnname), 'case_sensitive', 'boolean', true) || !$this->_caseSensitive)) {
  884. $tnname = strtolower($tnname);
  885. }
  886. if (!in_array ($this->getCodeFlag ($tnname, 'closetag', 'integer', BBCODE_CLOSETAG_IMPLICIT), array (BBCODE_CLOSETAG_FORBIDDEN, BBCODE_CLOSETAG_OPTIONAL))) {
  887. return false;
  888. }
  889. $node =& $this->_findNamedNode ($name, true);
  890. if ($node === false) {
  891. return false;
  892. }
  893. $scount = count ($this->_stack);
  894. if ($scount < 3) {
  895. return false;
  896. }
  897. for ($i = $scount - 2; $i > 0; $i--) {
  898. $closecount++;
  899. if ($this->_stack[$i]->equals ($node)) {
  900. return true;
  901. }
  902. if (in_array ($this->_stack[$i]->getFlag ('closetag', 'integer', BBCODE_CLOSETAG_IMPLICIT), array (BBCODE_CLOSETAG_IMPLICIT_ON_CLOSE_ONLY, BBCODE_CLOSETAG_MUSTEXIST))) {
  903. return false;
  904. }
  905. if ($this->_validateAgain) {
  906. if (!$this->_stack[$i]->validate ('validate_again')) {
  907. return false;
  908. }
  909. }
  910. }
  911. return false;
  912. }
  913. /**
  914. * Abstract method: Close remaining blocks
  915. * @access protected
  916. */
  917. function _closeRemainingBlocks () {
  918. // everything closed
  919. if (count ($this->_stack) == 1) {
  920. return true;
  921. }
  922. // not everything close
  923. if ($this->strict) {
  924. return false;
  925. }
  926. while (count ($this->_stack) > 1) {
  927. if ($this->_topNode ('getFlag', 'closetag', 'integer', BBCODE_CLOSETAG_IMPLICIT) == BBCODE_CLOSETAG_MUSTEXIST) {
  928. return false; // sorry
  929. }
  930. $res = $this->_popNode ();
  931. if (!$res) {
  932. return false;
  933. }
  934. }
  935. return true;
  936. }
  937. /**
  938. * Find a node with a specific name in stack
  939. *
  940. * @access protected
  941. * @return mixed
  942. */
  943. function &_findNamedNode ($name, $searchdeeper = false) {
  944. $lname = strtolower ($name);
  945. if (isset ($this->_codes[$lname]) && (!$this->getCodeFlag ($lname, 'case_sensitive', 'boolean', true) || !$this->_caseSensitive)) {
  946. $name = $lname;
  947. $case_sensitive = false;
  948. } else {
  949. $case_sensitive = true;
  950. }
  951. $scount = count ($this->_stack);
  952. if ($searchdeeper) {
  953. $scount--;
  954. }
  955. for ($i = $scount - 1; $i > 0; $i--) {
  956. if (!$case_sensitive) {
  957. $cmp_name = strtolower ($this->_stack[$i]->name ());
  958. } else {
  959. $cmp_name = $this->_stack[$i]->name ();
  960. }
  961. if ($cmp_name == $name) {
  962. return $this->_stack[$i];
  963. }
  964. }
  965. return false;
  966. }
  967. /**
  968. * Abstract method: Output tree
  969. * @access protected
  970. * @return bool
  971. */
  972. function _outputTree () {
  973. if ($this->_noOutput) {
  974. return true;
  975. }
  976. $output = $this->_outputNode ($this->_root);
  977. if (is_string ($output)) {
  978. $this->_output = $this->_applyPostfilters ($output);
  979. unset ($output);
  980. return true;
  981. }
  982. return false;
  983. }
  984. /**
  985. * Output a node
  986. * @access protected
  987. * @return bool
  988. */
  989. function _outputNode (&$node) {
  990. $output = '';
  991. if ($node->_type == STRINGPARSER_BBCODE_NODE_PARAGRAPH || $node->_type == STRINGPARSER_BBCODE_NODE_ELEMENT || $node->_type == STRINGPARSER_NODE_ROOT) {
  992. $ccount = count ($node->_children);
  993. for ($i = 0; $i < $ccount; $i++) {
  994. $suboutput = $this->_outputNode ($node->_children[$i]);
  995. if (!is_string ($suboutput)) {
  996. return false;
  997. }
  998. $output .= $suboutput;
  999. }
  1000. if ($node->_type == STRINGPARSER_BBCODE_NODE_PARAGRAPH) {
  1001. return $this->_paragraphHandling['start_tag'].$output.$this->_paragraphHandling['end_tag'];
  1002. }
  1003. if ($node->_type == STRINGPARSER_BBCODE_NODE_ELEMENT) {
  1004. return $node->getReplacement ($output);
  1005. }
  1006. return $output;
  1007. } else if ($node->_type == STRINGPARSER_NODE_TEXT) {
  1008. $output = $node->content;
  1009. $before = '';
  1010. $after = '';
  1011. $ol = strlen ($output);
  1012. switch ($node->getFlag ('newlinemode.begin', 'integer', BBCODE_NEWLINE_PARSE)) {
  1013. case BBCODE_NEWLINE_IGNORE:
  1014. if ($ol && $output{0} == "\n") {
  1015. $before = "\n";
  1016. }
  1017. // don't break!
  1018. case BBCODE_NEWLINE_DROP:
  1019. if ($ol && $output{0} == "\n") {
  1020. $output = substr ($output, 1);
  1021. $ol--;
  1022. }
  1023. break;
  1024. }
  1025. switch ($node->getFlag ('newlinemode.end', 'integer', BBCODE_NEWLINE_PARSE)) {
  1026. case BBCODE_NEWLINE_IGNORE:
  1027. if ($ol && $output{$ol-1} == "\n") {
  1028. $after = "\n";
  1029. }
  1030. // don't break!
  1031. case BBCODE_NEWLINE_DROP:
  1032. if ($ol && $output{$ol-1} == "\n") {
  1033. $output = substr ($output, 0, -1);
  1034. $ol--;
  1035. }
  1036. break;
  1037. }
  1038. // can't do anything
  1039. if ($node->_parent === null) {
  1040. return $before.$output.$after;
  1041. }
  1042. if ($node->_parent->_type == STRINGPARSER_BBCODE_NODE_PARAGRAPH) {
  1043. $parent =& $node->_parent;
  1044. unset ($node);
  1045. $node =& $parent;
  1046. unset ($parent);
  1047. // if no parent for this paragraph
  1048. if ($node->_parent === null) {
  1049. return $before.$output.$after;
  1050. }
  1051. }
  1052. if ($node->_parent->_type == STRINGPARSER_NODE_ROOT) {
  1053. return $before.$this->_applyParsers ($this->_rootContentType, $output).$after;
  1054. }
  1055. if ($node->_parent->_type == STRINGPARSER_BBCODE_NODE_ELEMENT) {
  1056. return $before.$this->_applyParsers ($node->_parent->_codeInfo['content_type'], $output).$after;
  1057. }
  1058. return $before.$output.$after;
  1059. }
  1060. }
  1061. /**
  1062. * Abstract method: Manipulate the tree
  1063. * @access protected
  1064. * @return bool
  1065. */
  1066. function _modifyTree () {
  1067. // first pass: try to do newline handling
  1068. $nodes =& $this->_root->getNodesByCriterium ('needsTextNodeModification', true);
  1069. $nodes_count = count ($nodes);
  1070. for ($i = 0; $i < $nodes_count; $i++) {
  1071. $v = $nodes[$i]->getFlag ('opentag.before.newline', 'integer', BBCODE_NEWLINE_PARSE);
  1072. if ($v != BBCODE_NEWLINE_PARSE) {
  1073. $n =& $nodes[$i]->findPrevAdjentTextNode ();
  1074. if (!is_null ($n)) {
  1075. $n->setFlag ('newlinemode.end', $v);
  1076. }
  1077. unset ($n);
  1078. }
  1079. $v = $nodes[$i]->getFlag ('opentag.after.newline', 'integer', BBCODE_NEWLINE_PARSE);
  1080. if ($v != BBCODE_NEWLINE_PARSE) {
  1081. $n =& $nodes[$i]->firstChildIfText ();
  1082. if (!is_null ($n)) {
  1083. $n->setFlag ('newlinemode.begin', $v);
  1084. }
  1085. unset ($n);
  1086. }
  1087. $v = $nodes[$i]->getFlag ('closetag.before.newline', 'integer', BBCODE_NEWLINE_PARSE);
  1088. if ($v != BBCODE_NEWLINE_PARSE) {
  1089. $n =& $nodes[$i]->lastChildIfText ();
  1090. if (!is_null ($n)) {
  1091. $n->setFlag ('newlinemode.end', $v);
  1092. }
  1093. unset ($n);
  1094. }
  1095. $v = $nodes[$i]->getFlag ('closetag.after.newline', 'integer', BBCODE_NEWLINE_PARSE);
  1096. if ($v != BBCODE_NEWLINE_PARSE) {
  1097. $n =& $nodes[$i]->findNextAdjentTextNode ();
  1098. if (!is_null ($n)) {
  1099. $n->setFlag ('newlinemode.begin', $v);
  1100. }
  1101. unset ($n);
  1102. }
  1103. }
  1104. // second pass a: do paragraph handling on root element
  1105. if ($this->_rootParagraphHandling) {
  1106. $res = $this->_handleParagraphs ($this->_root);
  1107. if (!$res) {
  1108. return false;
  1109. }
  1110. }
  1111. // second pass b: do paragraph handling on other elements
  1112. unset ($nodes);
  1113. $nodes =& $this->_root->getNodesByCriterium ('flag:paragraphs', true);
  1114. $nodes_count = count ($nodes);
  1115. for ($i = 0; $i < $nodes_count; $i++) {
  1116. $res = $this->_handleParagraphs ($nodes[$i]);
  1117. if (!$res) {
  1118. return false;
  1119. }
  1120. }
  1121. // second pass c: search for empty paragraph nodes and remove them
  1122. unset ($nodes);
  1123. $nodes =& $this->_root->getNodesByCriterium ('empty', true);
  1124. $nodes_count = count ($nodes);
  1125. if (isset ($parent)) {
  1126. unset ($parent); $parent = null;
  1127. }
  1128. for ($i = 0; $i < $nodes_count; $i++) {
  1129. if ($nodes[$i]->_type != STRINGPARSER_BBCODE_NODE_PARAGRAPH) {
  1130. continue;
  1131. }
  1132. unset ($parent);
  1133. $parent =& $nodes[$i]->_parent;
  1134. $parent->removeChild ($nodes[$i], true);
  1135. }
  1136. return true;
  1137. }
  1138. /**
  1139. * Handle paragraphs
  1140. * @access protected
  1141. * @param object $node The node to handle
  1142. * @return bool
  1143. */
  1144. function _handleParagraphs (&$node) {
  1145. // if this node is already a subnode of a paragraph node, do NOT
  1146. // do paragraph handling on this node!
  1147. if ($this->_hasParagraphAncestor ($node)) {
  1148. return true;
  1149. }
  1150. $dest_nodes = array ();
  1151. $last_node_was_paragraph = false;
  1152. $prevtype = STRINGPARSER_NODE_TEXT;
  1153. $paragraph = null;
  1154. while (count ($node->_children)) {
  1155. $mynode =& $node->_children[0];
  1156. $node->removeChild ($mynode);
  1157. $subprevtype = $prevtype;
  1158. $sub_nodes =& $this->_breakupNodeByParagraphs ($mynode);
  1159. for ($i = 0; $i < count ($sub_nodes); $i++) {
  1160. if (!$last_node_was_paragraph || ($prevtype == $sub_nodes[$i]->_type && ($i != 0 || $prevtype != STRINGPARSER_BBCODE_NODE_ELEMENT))) {
  1161. unset ($paragraph);
  1162. $paragraph =& new StringParser_BBCode_Node_Paragraph ();
  1163. }
  1164. $prevtype = $sub_nodes[$i]->_type;
  1165. if ($sub_nodes[$i]->_type != STRINGPARSER_BBCODE_NODE_ELEMENT || $sub_nodes[$i]->getFlag ('paragraph_type', 'integer', BBCODE_PARAGRAPH_ALLOW_BREAKUP) != BBCODE_PARAGRAPH_BLOCK_ELEMENT) {
  1166. $paragraph->appendChild ($sub_nodes[$i]);
  1167. $dest_nodes[] =& $paragraph;
  1168. $last_node_was_paragraph = true;
  1169. } else {
  1170. $dest_nodes[] =& $sub_nodes[$i];
  1171. $last_onde_was_paragraph = false;
  1172. unset ($paragraph);
  1173. $paragraph =& new StringParser_BBCode_Node_Paragraph ();
  1174. }
  1175. }
  1176. }
  1177. $count = count ($dest_nodes);
  1178. for ($i = 0; $i < $count; $i++) {
  1179. $node->appendChild ($dest_nodes[$i]);
  1180. }
  1181. unset ($dest_nodes);
  1182. unset ($paragraph);
  1183. return true;
  1184. }
  1185. /**
  1186. * Search for a paragraph node in tree in upward direction
  1187. * @access protected
  1188. * @param object $node The node to analyze
  1189. * @return bool
  1190. */
  1191. function _hasParagraphAncestor (&$node) {
  1192. if ($node->_parent === null) {
  1193. return false;
  1194. }
  1195. $parent =& $node->_parent;
  1196. if ($parent->_type == STRINGPARSER_BBCODE_NODE_PARAGRAPH) {
  1197. return true;
  1198. }
  1199. return $this->_hasParagraphAncestor ($parent);
  1200. }
  1201. /**
  1202. * Break up nodes
  1203. * @access protected
  1204. * @param object $node The node to break up
  1205. * @return array
  1206. */
  1207. function &_breakupNodeByParagraphs (&$node) {
  1208. $detect_string = $this->_paragraphHandling['detect_string'];
  1209. $dest_nodes = array ();
  1210. // text node => no problem
  1211. if ($node->_type == STRINGPARSER_NODE_TEXT) {
  1212. $cpos = 0;
  1213. while (($npos = strpos ($node->content, $detect_string, $cpos)) !== false) {
  1214. $subnode =& new StringParser_Node_Text (substr ($node->content, $cpos, $npos - $cpos), $node->occurredAt + $cpos);
  1215. // copy flags
  1216. foreach ($node->_flags as $flag => $value) {
  1217. if ($flag == 'newlinemode.begin') {
  1218. if ($cpos == 0) {
  1219. $subnode->setFlag ($flag, $value);
  1220. }
  1221. } else if ($flag == 'newlinemode.end') {
  1222. // do nothing
  1223. } else {
  1224. $subnode->setFlag ($flag, $value);
  1225. }
  1226. }
  1227. $dest_nodes[] =& $subnode;
  1228. unset ($subnode);
  1229. $cpos = $npos + strlen ($detect_string);
  1230. }
  1231. $subnode =& new StringParser_Node_Text (substr ($node->content, $cpos), $node->occurredAt + $cpos);
  1232. if ($cpos == 0) {
  1233. $value = $node->getFlag ('newlinemode.begin', 'integer', null);
  1234. if ($value !== null) {
  1235. $subnode->setFlag ('newlinemode.begin', $value);
  1236. }
  1237. }
  1238. $value = $node->getFlag ('newlinemode.end', 'integer', null);
  1239. if ($value !== null) {
  1240. $subnode->setFlag ('newlinemode.end', $value);
  1241. }
  1242. $dest_nodes[] =& $subnode;
  1243. unset ($subnode);
  1244. return $dest_nodes;
  1245. }
  1246. // not a text node or an element node => no way
  1247. if ($node->_type != STRINGPARSER_BBCODE_NODE_ELEMENT) {
  1248. $dest_nodes[] =& $node;
  1249. return $dest_nodes;
  1250. }
  1251. if ($node->getFlag ('paragraph_type', 'integer', BBCODE_PARAGRAPH_ALLOW_BREAKUP) != BBCODE_PARAGRAPH_ALLOW_BREAKUP || !count ($node->_children)) {
  1252. $dest_nodes[] =& $node;
  1253. return $dest_nodes;
  1254. }
  1255. $dest_node =& $node->duplicate ();
  1256. $nodecount = count ($node->_children);
  1257. // now this node allows breakup - do it
  1258. for ($i = 0; $i < $nodecount; $i++) {
  1259. $firstnode =& $node->_children[0];
  1260. $node->removeChild ($firstnode);
  1261. $sub_nodes =& $this->_breakupNodeByParagraphs ($firstnode);
  1262. for ($j = 0; $j < count ($sub_nodes); $j++) {
  1263. if ($j != 0) {
  1264. $dest_nodes[] =& $dest_node;
  1265. unset ($dest_node);
  1266. $dest_node =& $node->duplicate ();
  1267. }
  1268. $dest_node->appendChild ($sub_nodes[$j]);
  1269. }
  1270. unset ($sub_nodes);
  1271. }
  1272. $dest_nodes[] =& $dest_node;
  1273. return $dest_nodes;
  1274. }
  1275. /**
  1276. * Is this node a usecontent node
  1277. * @access protected
  1278. * @param object $node The node to check
  1279. * @param bool $check_attrs Also check whether 'usecontent?'-attributes exist
  1280. * @return bool
  1281. */
  1282. function _isUseContent (&$node, $check_attrs = false) {
  1283. $name = strtolower($node->name ());
  1284. if ($this->_codes[$name]['callback_type'] == 'usecontent') {
  1285. return true;
  1286. }
  1287. $result = true;
  1288. if ($this->_codes[$name]['callback_type'] == 'callback_replace?') {
  1289. $result = false;
  1290. } else if ($this->_codes[$name]['callback_type'] != 'usecontent?') {
  1291. return false;
  1292. }
  1293. if ($check_attrs === false) {
  1294. return !$result;
  1295. }
  1296. $attributes = array_keys ($this->_topNodeVar ('_attributes'));
  1297. $p = @$this->_codes[$name]['callback_params']['usecontent_param'];
  1298. if (is_array ($p)) {
  1299. foreach ($p as $param) {
  1300. if (in_array ($param, $attributes)) {
  1301. return $result;
  1302. }
  1303. }
  1304. } else {
  1305. if (in_array ($p, $attributes)) {
  1306. return $result;
  1307. }
  1308. }
  1309. return !$result;
  1310. }
  1311. }
  1312.  
  1313. /**
  1314. * Node type: BBCode Element node
  1315. * @see StringParser_BBCode_Node_Element::_type
  1316. */
  1317. define ('STRINGPARSER_BBCODE_NODE_ELEMENT', 32);
  1318.  
  1319. /**
  1320. * Node type: BBCode Paragraph node
  1321. * @see StringParser_BBCode_Node_Paragraph::_type
  1322. */
  1323. define ('STRINGPARSER_BBCODE_NODE_PARAGRAPH', 33);
  1324.  
  1325.  
  1326. /**
  1327. * BBCode String parser paragraph node class
  1328. *
  1329. * @package stringparser
  1330. */
  1331. class StringParser_BBCode_Node_Paragraph extends StringParser_Node {
  1332. /**
  1333. * The type of this node.
  1334. *
  1335. * This node is a bbcode paragraph node.
  1336. *
  1337. * @access protected
  1338. * @var int
  1339. * @see STRINGPARSER_BBCODE_NODE_PARAGRAPH
  1340. */
  1341. var $_type = STRINGPARSER_BBCODE_NODE_PARAGRAPH;
  1342. /**
  1343. * Determines whether a criterium matches this node
  1344. *
  1345. * @access public
  1346. * @param string $criterium The criterium that is to be checked
  1347. * @param mixed $value The value that is to be compared
  1348. * @return bool True if this node matches that criterium
  1349. */
  1350. function matchesCriterium ($criterium, $value) {
  1351. if ($criterium == 'empty') {
  1352. if (!count ($this->_children)) {
  1353. return true;
  1354. }
  1355. if (count ($this->_children) > 1) {
  1356. return false;
  1357. }
  1358. if ($this->_children[0]->_type != STRINGPARSER_NODE_TEXT) {
  1359. return false;
  1360. }
  1361. if (!strlen ($this->_children[0]->content)) {
  1362. return true;
  1363. }
  1364. if (strlen ($this->_children[0]->content) > 2) {
  1365. return false;
  1366. }
  1367. $f_begin = $this->_children[0]->getFlag ('newlinemode.begin', 'integer', BBCODE_NEWLINE_PARSE);
  1368. $f_end = $this->_children[0]->getFlag ('newlinemode.end', 'integer', BBCODE_NEWLINE_PARSE);
  1369. $content = $this->_children[0]->content;
  1370. if ($f_begin != BBCODE_NEWLINE_PARSE && $content{0} == "\n") {
  1371. $content = substr ($content, 1);
  1372. }
  1373. if ($f_end != BBCODE_NEWLINE_PARSE && $content{strlen($content)-1} == "\n") {
  1374. $content = substr ($content, 0, -1);
  1375. }
  1376. if (!strlen ($content)) {
  1377. return true;
  1378. }
  1379. return false;
  1380. }
  1381. }
  1382. }
  1383.  
  1384. /**
  1385. * BBCode String parser element node class
  1386. *
  1387. * @package stringparser
  1388. */
  1389. class StringParser_BBCode_Node_Element extends StringParser_Node {
  1390. /**
  1391. * The type of this node.
  1392. *
  1393. * This node is a bbcode element node.
  1394. *
  1395. * @access protected
  1396. * @var int
  1397. * @see STRINGPARSER_BBCODE_NODE_ELEMENT
  1398. */
  1399. var $_type = STRINGPARSER_BBCODE_NODE_ELEMENT;
  1400. /**
  1401. * Element name
  1402. *
  1403. * @access protected
  1404. * @var string
  1405. * @see StringParser_BBCode_Node_Element::name
  1406. * @see StringParser_BBCode_Node_Element::setName
  1407. * @see StringParser_BBCode_Node_Element::appendToName
  1408. */
  1409. var $_name = '';
  1410. /**
  1411. * Element flags
  1412. *
  1413. * @access protected
  1414. * @var array
  1415. */
  1416. var $_flags = array ();
  1417. /**
  1418. * Element attributes
  1419. *
  1420. * @access protected
  1421. * @var array
  1422. */
  1423. var $_attributes = array ();
  1424. /**
  1425. * Had a close tag
  1426. *
  1427. * @access protected
  1428. * @var bool
  1429. */
  1430. var $_hadCloseTag = false;
  1431. /**
  1432. * Was processed by paragraph handling
  1433. *
  1434. * @access protected
  1435. * @var bool
  1436. */
  1437. var $_paragraphHandled = false;
  1438. //////////////////////////////////////////////////
  1439. /**
  1440. * Duplicate this node (but without children / parents)
  1441. *
  1442. * @access public
  1443. * @return object
  1444. */
  1445. function &duplicate () {
  1446. $newnode =& new StringParser_BBCode_Node_Element ($this->occurredAt);
  1447. $newnode->_name = $this->_name;
  1448. $newnode->_flags = $this->_flags;
  1449. $newnode->_attributes = $this->_attributes;
  1450. $newnode->_hadCloseTag = $this->_hadCloseTag;
  1451. $newnode->_paragraphHandled = $this->_paragraphHandled;
  1452. $newnode->_codeInfo = $this->_codeInfo;
  1453. return $newnode;
  1454. }
  1455. /**
  1456. * Retreive name of this element
  1457. *
  1458. * @access public
  1459. * @return string
  1460. */
  1461. function name () {
  1462. return $this->_name;
  1463. }
  1464. /**
  1465. * Set name of this element
  1466. *
  1467. * @access public
  1468. * @param string $name The new name of the element
  1469. */
  1470. function setName ($name) {
  1471. $this->_name = $name;
  1472. return true;
  1473. }
  1474. /**
  1475. * Append to name of this element
  1476. *
  1477. * @access public
  1478. * @param string $chars The chars to append to the name of the element
  1479. */
  1480. function appendToName ($chars) {
  1481. $this->_name .= $chars;
  1482. return true;
  1483. }
  1484. /**
  1485. * Append to attribute of this element
  1486. *
  1487. * @access public
  1488. * @param string $name The name of the attribute
  1489. * @param string $chars The chars to append to the attribute of the element
  1490. */
  1491. function appendToAttribute ($name, $chars) {
  1492. if (!isset ($this->_attributes[$name])) {
  1493. $this->_attributes[$name] = $chars;
  1494. return true;
  1495. }
  1496. $this->_attributes[$name] .= $chars;
  1497. return true;
  1498. }
  1499. /**
  1500. * Set attribute
  1501. *
  1502. * @access public
  1503. * @param string $name The name of the attribute
  1504. * @param string $value The new value of the attribute
  1505. */
  1506. function setAttribute ($name, $value) {
  1507. $this->_attributes[$name] = $value;
  1508. return true;
  1509. }
  1510. /**
  1511. * Set code info
  1512. *
  1513. * @access public
  1514. * @param array $info The code info array
  1515. */
  1516. function setCodeInfo ($info) {
  1517. $this->_codeInfo = $info;
  1518. $this->_flags = $info['flags'];
  1519. return true;
  1520. }
  1521. /**
  1522. * Get attribute value
  1523. *
  1524. * @access public
  1525. * @param string $name The name of the attribute
  1526. */
  1527. function attribute ($name) {
  1528. if (!isset ($this->_attributes[$name])) {
  1529. return null;
  1530. }
  1531. return $this->_attributes[$name];
  1532. }
  1533. /**
  1534. * Set flag that this element had a close tag
  1535. *
  1536. * @access public
  1537. */
  1538. function setHadCloseTag () {
  1539. $this->_hadCloseTag = true;
  1540. }
  1541. /**
  1542. * Set flag that this element was already processed by paragraph handling
  1543. *
  1544. * @access public
  1545. */
  1546. function setParagraphHandled () {
  1547. $this->_paragraphHandled = true;
  1548. }
  1549. /**
  1550. * Get flag if this element was already processed by paragraph handling
  1551. *
  1552. * @access public
  1553. * @return bool
  1554. */
  1555. function paragraphHandled () {
  1556. return $this->_paragraphHandled;
  1557. }
  1558. /**
  1559. * Get flag if this element had a close tag
  1560. *
  1561. * @access public
  1562. * @return bool
  1563. */
  1564. function hadCloseTag () {
  1565. return $this->_hadCloseTag;
  1566. }
  1567. /**
  1568. * Determines whether a criterium matches this node
  1569. *
  1570. * @access public
  1571. * @param string $criterium The criterium that is to be checked
  1572. * @param mixed $value The value that is to be compared
  1573. * @return bool True if this node matches that criterium
  1574. */
  1575. function matchesCriterium ($criterium, $value) {
  1576. if ($criterium == 'tagName') {
  1577. return ($value == $this->_name);
  1578. }
  1579. if ($criterium == 'needsTextNodeModification') {
  1580. return (($this->getFlag ('opentag.before.newline', 'integer', BBCODE_NEWLINE_PARSE) != BBCODE_NEWLINE_PARSE || $this->getFlag ('opentag.after.newline', 'integer', BBCODE_NEWLINE_PARSE) != BBCODE_NEWLINE_PARSE || ($this->_hadCloseTag && ($this->getFlag ('closetag.before.newline', 'integer', BBCODE_NEWLINE_PARSE) != BBCODE_NEWLINE_PARSE || $this->getFlag ('closetag.after.newline', 'integer', BBCODE_NEWLINE_PARSE) != BBCODE_NEWLINE_PARSE))) == (bool)$value);
  1581. }
  1582. if (substr ($criterium, 0, 5) == 'flag:') {
  1583. $criterium = substr ($criterium, 5);
  1584. return ($this->getFlag ($criterium) == $value);
  1585. }
  1586. if (substr ($criterium, 0, 6) == '!flag:') {
  1587. $criterium = substr ($criterium, 6);
  1588. return ($this->getFlag ($criterium) != $value);
  1589. }
  1590. if (substr ($criterium, 0, 6) == 'flag=:') {
  1591. $criterium = substr ($criterium, 6);
  1592. return ($this->getFlag ($criterium) === $value);
  1593. }
  1594. if (substr ($criterium, 0, 7) == '!flag=:') {
  1595. $criterium = substr ($criterium, 7);
  1596. return ($this->getFlag ($criterium) !== $value);
  1597. }
  1598. return parent::matchesCriterium ($criterium, $value);
  1599. }
  1600. /**
  1601. * Get first child if it is a text node
  1602. *
  1603. * @access public
  1604. * @return mixed
  1605. */
  1606. function &firstChildIfText () {
  1607. $ret =& $this->firstChild ();
  1608. if (is_null ($ret)) {
  1609. return $ret;
  1610. }
  1611. if ($ret->_type != STRINGPARSER_NODE_TEXT) {
  1612. // DON'T DO $ret = null WITHOUT unset BEFORE!
  1613. // ELSE WE WILL ERASE THE NODE ITSELF! EVIL!
  1614. unset ($ret);
  1615. $ret = null;
  1616. }
  1617. return $ret;
  1618. }
  1619. /**
  1620. * Get last child if it is a text node AND if this element had a close tag
  1621. *
  1622. * @access public
  1623. * @return mixed
  1624. */
  1625. function &lastChildIfText () {
  1626. $ret =& $this->lastChild ();
  1627. if (is_null ($ret)) {
  1628. return $ret;
  1629. }
  1630. if ($ret->_type != STRINGPARSER_NODE_TEXT || !$this->_hadCloseTag) {
  1631. // DON'T DO $ret = null WITHOUT unset BEFORE!
  1632. // ELSE WE WILL ERASE THE NODE ITSELF! EVIL!
  1633. if ($ret->_type != STRINGPARSER_NODE_TEXT && !$ret->hadCloseTag ()) {
  1634. $ret2 =& $ret->_findPrevAdjentTextNodeHelper ();
  1635. unset ($ret);
  1636. $ret =& $ret2;
  1637. unset ($ret2);
  1638. } else {
  1639. unset ($ret);
  1640. $ret = null;
  1641. }
  1642. }
  1643. return $ret;
  1644. }
  1645. /**
  1646. * Find next adjent text node after close tag
  1647. *
  1648. * returns the node or null if none exists
  1649. *
  1650. * @access public
  1651. * @return mixed
  1652. */
  1653. function &findNextAdjentTextNode () {
  1654. $ret = null;
  1655. if (is_null ($this->_parent)) {
  1656. return $ret;
  1657. }
  1658. if (!$this->_hadCloseTag) {
  1659. return $ret;
  1660. }
  1661. $ccount = count ($this->_parent->_children);
  1662. $found = false;
  1663. for ($i = 0; $i < $ccount; $i++) {
  1664. if ($this->_parent->_children[$i]->equals ($this)) {
  1665. $found = $i;
  1666. break;
  1667. }
  1668. }
  1669. if ($found === false) {
  1670. return $ret;
  1671. }
  1672. if ($found < $ccount - 1) {
  1673. if ($this->_parent->_children[$found+1]->_type == STRINGPARSER_NODE_TEXT) {
  1674. return $this->_parent->_children[$found+1];
  1675. }
  1676. return $ret;
  1677. }
  1678. if ($this->_parent->_type == STRINGPARSER_BBCODE_NODE_ELEMENT && !$this->_parent->hadCloseTag ()) {
  1679. $ret =& $this->_parent->findNextAdjentTextNode ();
  1680. return $ret;
  1681. }
  1682. return $ret;
  1683. }
  1684. /**
  1685. * Find previous adjent text node before open tag
  1686. *
  1687. * returns the node or null if none exists
  1688. *
  1689. * @access public
  1690. * @return mixed
  1691. */
  1692. function &findPrevAdjentTextNode () {
  1693. $ret = null;
  1694. if (is_null ($this->_parent)) {
  1695. return $ret;
  1696. }
  1697. $ccount = count ($this->_parent->_children);
  1698. $found = false;
  1699. for ($i = 0; $i < $ccount; $i++) {
  1700. if ($this->_parent->_children[$i]->equals ($this)) {
  1701. $found = $i;
  1702. break;
  1703. }
  1704. }
  1705. if ($found === false) {
  1706. return $ret;
  1707. }
  1708. if ($found > 0) {
  1709. if ($this->_parent->_children[$found-1]->_type == STRINGPARSER_NODE_TEXT) {
  1710. return $this->_parent->_children[$found-1];
  1711. }
  1712. if (!$this->_parent->_children[$found-1]->hadCloseTag ()) {
  1713. $ret =& $this->_parent->_children[$found-1]->_findPrevAdjentTextNodeHelper ();
  1714. }
  1715. return $ret;
  1716. }
  1717. return $ret;
  1718. }
  1719. /**
  1720. * Helper function for findPrevAdjentTextNode
  1721. *
  1722. * Looks at the last child node; if it's a text node, it returns it,
  1723. * if the element node did not have an open tag, it calls itself
  1724. * recursively.
  1725. */
  1726. function &_findPrevAdjentTextNodeHelper () {
  1727. $lastnode =& $this->lastChild ();
  1728. if ($lastnode->_type == STRINGPARSER_NODE_TEXT) {
  1729. return $lastnode;
  1730. }
  1731. if (!$lastnode->hadCloseTag ()) {
  1732. $ret =& $lastnode->_findPrevAdjentTextNodeHelper ();
  1733. } else {
  1734. $ret = null;
  1735. }
  1736. return $ret;
  1737. }
  1738. /**
  1739. * Get Flag
  1740. *
  1741. * @access public
  1742. * @param string $flag The requested flag
  1743. * @param string $type The requested type of the return value
  1744. * @param mixed $default The default return value
  1745. * @return mixed
  1746. */
  1747. function getFlag ($flag, $type = 'mixed', $default = null) {
  1748. if (!isset ($this->_flags[$flag])) {
  1749. return $default;
  1750. }
  1751. $return = $this->_flags[$flag];
  1752. if ($type != 'mixed') {
  1753. settype ($return, $type);
  1754. }
  1755. return $return;
  1756. }
  1757. /**
  1758. * Set a flag
  1759. *
  1760. * @access public
  1761. * @param string $name The name of the flag
  1762. * @param mixed $value The value of the flag
  1763. */
  1764. function setFlag ($name, $value) {
  1765. $this->_flags[$name] = $value;
  1766. return true;
  1767. }
  1768. /**
  1769. * Validate code
  1770. *
  1771. * @access public
  1772. * @param string $action The action which is to be called ('validate'
  1773. * for first validation, 'validate_again' for
  1774. * second validation (optional))
  1775. * @return bool
  1776. */
  1777. function validate ($action = 'validate') {
  1778. if ($action != 'validate' && $action != 'validate_again') {
  1779. return false;
  1780. }
  1781. if ($this->_codeInfo['callback_type'] != 'simple_replace' && $this->_codeInfo['callback_type'] != 'simple_replace_single') {
  1782. if (!is_callable ($this->_codeInfo['callback_func'])) {
  1783. return false;
  1784. }
  1785. if (($this->_codeInfo['callback_type'] == 'usecontent' || $this->_codeInfo['callback_type'] == 'usecontent?' || $this->_codeInfo['callback_type'] == 'callback_replace?') && count ($this->_children) == 1 && $this->_children[0]->_type == STRINGPARSER_NODE_TEXT) {
  1786. // we have to make sure the object gets passed on as a reference
  1787. // if we do call_user_func(..., &$this) this will clash with PHP5
  1788. $callArray = array ($action, $this->_attributes, $this->_children[0]->content, $this->_codeInfo['callback_params']);
  1789. $callArray[] =& $this;
  1790. $res = call_user_func_array ($this->_codeInfo['callback_func'], $callArray);
  1791. if ($res) {
  1792. // ok, now, if we've got a usecontent type, set a flag that
  1793. // this may not be broken up by paragraph handling!
  1794. // but PLEASE do NOT change if already set to any other setting
  1795. // than BBCODE_PARAGRAPH_ALLOW_BREAKUP because we could
  1796. // override e.g. BBCODE_PARAGRAPH_BLOCK_ELEMENT!
  1797. $val = $this->getFlag ('paragraph_type', 'integer', BBCODE_PARAGRAPH_ALLOW_BREAKUP);
  1798. if ($val == BBCODE_PARAGRAPH_ALLOW_BREAKUP) {
  1799. $this->_flags['paragraph_type'] = BBCODE_PARAGRAPH_ALLOW_INSIDE;
  1800. }
  1801. }
  1802. return $res;
  1803. }
  1804. // we have to make sure the object gets passed on as a reference
  1805. // if we do call_user_func(..., &$this) this will clash with PHP5
  1806. $callArray = array ($action, $this->_attributes, null, $this->_codeInfo['callback_params']);
  1807. $callArray[] =& $this;
  1808. return call_user_func_array ($this->_codeInfo['callback_func'], $callArray);
  1809. }
  1810. return (bool)(!count ($this->_attributes));
  1811. }
  1812. /**
  1813. * Get replacement for this code
  1814. *
  1815. * @access public
  1816. * @param string $subcontent The content of all sub-nodes
  1817. * @return string
  1818. */
  1819. function getReplacement ($subcontent) {
  1820. if ($this->_codeInfo['callback_type'] == 'simple_replace' || $this->_codeInfo['callback_type'] == 'simple_replace_single') {
  1821. if ($this->_codeInfo['callback_type'] == 'simple_replace_single') {
  1822. if (strlen ($subcontent)) { // can't be!
  1823. return false;
  1824. }
  1825. return $this->_codeInfo['callback_params']['start_tag'];
  1826. }
  1827. return $this->_codeInfo['callback_params']['start_tag'].$subcontent.$this->_codeInfo['callback_params']['end_tag'];
  1828. }
  1829. // else usecontent, usecontent? or callback_replace or callback_replace_single
  1830. // => call function (the function is callable, determined in validate()!)
  1831. // we have to make sure the object gets passed on as a reference
  1832. // if we do call_user_func(..., &$this) this will clash with PHP5
  1833. $callArray = array ('output', $this->_attributes, $subcontent, $this->_codeInfo['callback_params']);
  1834. $callArray[] =& $this;
  1835. return call_user_func_array ($this->_codeInfo['callback_func'], $callArray);
  1836. }
  1837. /**
  1838. * Dump this node to a string
  1839. *
  1840. * @access protected
  1841. * @return string
  1842. */
  1843. function _dumpToString () {
  1844. $str = "bbcode \"".substr (preg_replace ('/\s+/', ' ', $this->_name), 0, 40)."\"";
  1845. if (count ($this->_attributes)) {
  1846. $attribs = array_keys ($this->_attributes);
  1847. sort ($attribs);
  1848. $str .= ' (';
  1849. $i = 0;
  1850. foreach ($attribs as $attrib) {
  1851. if ($i != 0) {
  1852. $str .= ', ';
  1853. }
  1854. $str .= $attrib.'="';
  1855. $str .= substr (preg_replace ('/\s+/', ' ', $this->_attributes[$attrib]), 0, 10);
  1856. $str .= '"';
  1857. $i++;
  1858. }
  1859. $str .= ')';
  1860. }
  1861. return $str;
  1862. }
  1863. }
  1864.  
  1865. ?>

Documentation generated on Mon, 24 Apr 2006 10:18:37 +0200 by phpDocumentor 1.3.0RC5