PageRenderTime 56ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/branches/coding-style/fp-plugins/bbcode/inc/stringparser_bbcode.class.php

https://bitbucket.org/alexandrul/flatpress
PHP | 1890 lines | 1182 code | 100 blank | 608 comment | 401 complexity | d274c433a4325d9336c2aeb918bfcd52 MD5 | raw file
Possible License(s): AGPL-1.0, GPL-2.0, MIT

Large files files are truncated, but you can click here to view the full file

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

Large files files are truncated, but you can click here to view the full file