File Source for stringparser_bbcode.class.php

# Source for file stringparser_bbcode.class.php

Documentation is available at stringparser_bbcode.class.php

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

Documentation generated on Mon, 10 Dec 2007 13:30:04 +0100 by phpDocumentor 1.4.0