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

Documentation generated on Sun, 14 Aug 2005 14:26:47 +0200 by phpDocumentor 1.3.0RC3