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.  */
  33.  
  34. require_once dirname(__FILE__).'/stringparser.class.php';
  35.  
  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.  
  42. define ('BBCODE_NEWLINE_PARSE', 0);
  43. define ('BBCODE_NEWLINE_IGNORE', 1);
  44. define ('BBCODE_NEWLINE_DROP', 2);
  45.  
  46. define ('BBCODE_PARAGRAPH_ALLOW_BREAKUP', 0);
  47. define ('BBCODE_PARAGRAPH_ALLOW_INSIDE', 1);
  48. define ('BBCODE_PARAGRAPH_BLOCK_ELEMENT', 2);
  49.  
  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;
  66.     
  67.     /**
  68.      * Defined BB Codes
  69.      *
  70.      * The registered BB codes
  71.      *
  72.      * @access protected
  73.      * @var array 
  74.      */
  75.     var $_codes = array ();
  76.     
  77.     /**
  78.      * Registered parsers
  79.      *
  80.      * @access protected
  81.      * @var array 
  82.      */
  83.     var $_parsers = array ();
  84.     
  85.     /**
  86.      * Defined maximum occurrences
  87.      *
  88.      * @access protected
  89.      * @var array 
  90.      */
  91.     var $_maxOccurrences = array ();
  92.     
  93.     /**
  94.      * Root content type
  95.      *
  96.      * @access protected
  97.      * @var string 
  98.      */
  99.     var $_rootContentType = 'block';
  100.     
  101.     /**
  102.      * Do not output but return the tree
  103.      *
  104.      * @access protected
  105.      * @var bool 
  106.      */
  107.     var $_noOutput = false;
  108.     
  109.     /**
  110.      * Global setting: case sensitive
  111.      *
  112.      * @access protected
  113.      * @var bool 
  114.      */
  115.     var $_caseSensitive = true;
  116.     
  117.     /**
  118.      * Root paragraph handling enabled
  119.      *
  120.      * @access protected
  121.      * @var bool 
  122.      */
  123.     var $_rootParagraphHandling = false;
  124.     
  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.     );
  135.     
  136.     /**
  137.      * Allow mixed attribute types (e.g. [code=bla attr=blub])
  138.      * @access private
  139.      * @var bool 
  140.      */
  141.     var $_mixedAttributeTypes = false;
  142.     
  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;
  149.     
  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.     }
  182.     
  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.     }
  197.     
  198.     /**
  199.      * Remove all codes
  200.      *
  201.      * @access public
  202.      */
  203.     function removeAllCodes () {
  204.         $this->_codes = array ();
  205.     }
  206.     
  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.     }
  223.     
  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.     }
  241.     
  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.     }
  258.     
  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.     }
  280.     
  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.     }
  290.     
  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.     }
  300.     
  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.     }
  316.     
  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.     }
  331.     
  332.     /**
  333.      * Get global case sensitive flag
  334.      *
  335.      * @access public
  336.      * @return bool 
  337.      */
  338.     function globalCaseSensitive () {
  339.         return $this->_caseSensitive;
  340.     }
  341.     
  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.     }
  356.     
  357.     /**
  358.      * Get mixed attribute types flag
  359.      *
  360.      * @access public
  361.      * @return bool 
  362.      */
  363.     function mixedAttributeTypes () {
  364.         return $this->_mixedAttributeTypes;
  365.     }
  366.     
  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.     }
  379.     
  380.     /**
  381.      * Get validate again flag
  382.      *
  383.      * @access public
  384.      * @return bool 
  385.      */
  386.     function validateAgain () {
  387.         return $this->_validateAgain;
  388.     }
  389.     
  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.     }
  413.     
  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.     }
  477.     
  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.     }
  507.     
  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
  522.             
  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.     }
  533.     
  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.     }
  551.     
  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.     }
  765.     
  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.         }
  827.         
  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.         }
  833.         
  834.         return true;
  835.     }
  836.     
  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.     }
  860.     
  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.     }
  878.     
  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.         }
  889.         
  890.         $closecount = 0;
  891.         
  892.         $allowed_within = $this->_codes[$name]['allowed_within'];
  893.         $not_allowed_within = $this->_codes[$name]['not_allowed_within'];
  894.         
  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.         }
  905.         
  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.         }
  911.         
  912.         return true;
  913.     }
  914.     
  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.         }
  948.         
  949.         return false;
  950.     }
  951.     
  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.     }
  976.     
  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.     }
  1006.     
  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.         }
  1022.         
  1023.         return false;
  1024.     }
  1025.     
  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.     }
  1103.     
  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.         }
  1147.         
  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.         }
  1155.         
  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.         }
  1166.         
  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.         }
  1182.         
  1183.         return true;
  1184.     }
  1185.     
  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.     }
  1233.     
  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.     }
  1250.     
  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.     }
  1325.     
  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.     }
  1366.  
  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. }
  1393.  
  1394. /**
  1395.  * Node type: BBCode Element node
  1396.  * @see StringParser_BBCode_Node_Element::_type
  1397.  */
  1398. define ('STRINGPARSER_BBCODE_NODE_ELEMENT', 32);
  1399.  
  1400. /**
  1401.  * Node type: BBCode Paragraph node
  1402.  * @see StringParser_BBCode_Node_Paragraph::_type
  1403.  */
  1404. define ('STRINGPARSER_BBCODE_NODE_PARAGRAPH', 33);
  1405.  
  1406.  
  1407. /**
  1408.  * BBCode String parser paragraph node class
  1409.  *
  1410.  * @package stringparser
  1411.  */
  1412.     /**
  1413.      * The type of this node.
  1414.      * 
  1415.      * This node is a bbcode paragraph node.
  1416.      *
  1417.      * @access protected
  1418.      * @var int 
  1419.      * @see STRINGPARSER_BBCODE_NODE_PARAGRAPH
  1420.      */
  1421.     var $_type = STRINGPARSER_BBCODE_NODE_PARAGRAPH;
  1422.     
  1423.     /**
  1424.      * Determines whether a criterium matches this node
  1425.      *
  1426.      * @access public
  1427.      * @param string $criterium The criterium that is to be checked
  1428.      * @param mixed $value The value that is to be compared
  1429.      * @return bool True if this node matches that criterium
  1430.      */
  1431.     function matchesCriterium ($criterium, $value) {
  1432.         if ($criterium == 'empty') {
  1433.             if (!count ($this->_children)) {
  1434.                 return true;
  1435.             }
  1436.             if (count ($this->_children) > 1) {
  1437.                 return false;
  1438.             }
  1439.             if ($this->_children[0]->_type != STRINGPARSER_NODE_TEXT) {
  1440.                 return false;
  1441.             }
  1442.             if (!strlen ($this->_children[0]->content)) {
  1443.                 return true;
  1444.             }
  1445.             if (strlen ($this->_children[0]->content) > 2) {
  1446.                 return false;
  1447.             }
  1448.             $f_begin = $this->_children[0]->getFlag ('newlinemode.begin', 'integer', BBCODE_NEWLINE_PARSE);
  1449.             $f_end = $this->_children[0]->getFlag ('newlinemode.end', 'integer', BBCODE_NEWLINE_PARSE);
  1450.             $content = $this->_children[0]->content;
  1451.             if ($f_begin != BBCODE_NEWLINE_PARSE && $content{0} == "\n") {
  1452.                 $content = substr ($content, 1);
  1453.             }
  1454.             if ($f_end != BBCODE_NEWLINE_PARSE && $content{strlen($content)-1} == "\n") {
  1455.                 $content = substr ($content, 0, -1);
  1456.             }
  1457.             if (!strlen ($content)) {
  1458.                 return true;
  1459.             }
  1460.             return false;
  1461.         }
  1462.     }
  1463. }
  1464.  
  1465. /**
  1466.  * BBCode String parser element node class
  1467.  *
  1468.  * @package stringparser
  1469.  */
  1470.     /**
  1471.      * The type of this node.
  1472.      * 
  1473.      * This node is a bbcode element node.
  1474.      *
  1475.      * @access protected
  1476.      * @var int 
  1477.      * @see STRINGPARSER_BBCODE_NODE_ELEMENT
  1478.      */
  1479.     var $_type = STRINGPARSER_BBCODE_NODE_ELEMENT;
  1480.     
  1481.     /**
  1482.      * Element name
  1483.      *
  1484.      * @access protected
  1485.      * @var string 
  1486.      * @see StringParser_BBCode_Node_Element::name
  1487.      * @see StringParser_BBCode_Node_Element::setName
  1488.      * @see StringParser_BBCode_Node_Element::appendToName
  1489.      */
  1490.     var $_name = '';
  1491.     
  1492.     /**
  1493.      * Element flags
  1494.      * 
  1495.      * @access protected
  1496.      * @var array 
  1497.      */
  1498.     var $_flags = array ();
  1499.     
  1500.     /**
  1501.      * Element attributes
  1502.      * 
  1503.      * @access protected
  1504.      * @var array 
  1505.      */
  1506.     var $_attributes = array ();
  1507.     
  1508.     /**
  1509.      * Had a close tag
  1510.      *
  1511.      * @access protected
  1512.      * @var bool 
  1513.      */
  1514.     var $_hadCloseTag = false;
  1515.     
  1516.     /**
  1517.      * Was processed by paragraph handling
  1518.      *
  1519.      * @access protected
  1520.      * @var bool 
  1521.      */
  1522.     var $_paragraphHandled = false;
  1523.     
  1524.     //////////////////////////////////////////////////
  1525.     
  1526.     /**
  1527.      * Duplicate this node (but without children / parents)
  1528.      *
  1529.      * @access public
  1530.      * @return object 
  1531.      */
  1532.     function &duplicate () {
  1533.         $newnode =& new StringParser_BBCode_Node_Element ($this->occurredAt);
  1534.         $newnode->_name = $this->_name;
  1535.         $newnode->_flags = $this->_flags;
  1536.         $newnode->_attributes = $this->_attributes;
  1537.         $newnode->_hadCloseTag = $this->_hadCloseTag;
  1538.         $newnode->_paragraphHandled = $this->_paragraphHandled;
  1539.         $newnode->_codeInfo = $this->_codeInfo;
  1540.         return $newnode;
  1541.     }
  1542.     
  1543.     /**
  1544.      * Retreive name of this element
  1545.      *
  1546.      * @access public
  1547.      * @return string 
  1548.      */
  1549.     function name () {
  1550.         return $this->_name;
  1551.     }
  1552.     
  1553.     /**
  1554.      * Set name of this element
  1555.      *
  1556.      * @access public
  1557.      * @param string $name The new name of the element
  1558.      */
  1559.     function setName ($name) {
  1560.         $this->_name = $name;
  1561.         return true;
  1562.     }
  1563.     
  1564.     /**
  1565.      * Append to name of this element
  1566.      *
  1567.      * @access public
  1568.      * @param string $chars The chars to append to the name of the element
  1569.      */
  1570.     function appendToName ($chars) {
  1571.         $this->_name .= $chars;
  1572.         return true;
  1573.     }
  1574.     
  1575.     /**
  1576.      * Append to attribute of this element
  1577.      *
  1578.      * @access public
  1579.      * @param string $name The name of the attribute
  1580.      * @param string $chars The chars to append to the attribute of the element
  1581.      */
  1582.     function appendToAttribute ($name, $chars) {
  1583.         if (!isset ($this->_attributes[$name])) {
  1584.             $this->_attributes[$name] = $chars;
  1585.             return true;
  1586.         }
  1587.         $this->_attributes[$name] .= $chars;
  1588.         return true;
  1589.     }
  1590.     
  1591.     /**
  1592.      * Set attribute
  1593.      *
  1594.      * @access public
  1595.      * @param string $name The name of the attribute
  1596.      * @param string $value The new value of the attribute
  1597.      */
  1598.     function setAttribute ($name, $value) {
  1599.         $this->_attributes[$name] = $value;
  1600.         return true;
  1601.     }
  1602.     
  1603.     /**
  1604.      * Set code info
  1605.      *
  1606.      * @access public
  1607.      * @param array $info The code info array
  1608.      */
  1609.     function setCodeInfo ($info) {
  1610.         $this->_codeInfo = $info;
  1611.         $this->_flags = $info['flags'];
  1612.         return true;
  1613.     }
  1614.     
  1615.     /**
  1616.      * Get attribute value
  1617.      *
  1618.      * @access public
  1619.      * @param string $name The name of the attribute
  1620.      */
  1621.     function attribute ($name) {
  1622.         if (!isset ($this->_attributes[$name])) {
  1623.             return null;
  1624.         }
  1625.         return $this->_attributes[$name];
  1626.     }
  1627.     
  1628.     /**
  1629.      * Set flag that this element had a close tag
  1630.      *
  1631.      * @access public
  1632.      */
  1633.     function setHadCloseTag () {
  1634.         $this->_hadCloseTag = true;
  1635.     }
  1636.     
  1637.     /**
  1638.      * Set flag that this element was already processed by paragraph handling
  1639.      *
  1640.      * @access public
  1641.      */
  1642.     function setParagraphHandled () {
  1643.         $this->_paragraphHandled = true;
  1644.     }
  1645.     
  1646.     /**
  1647.      * Get flag if this element was already processed by paragraph handling
  1648.      *
  1649.      * @access public
  1650.      * @return bool 
  1651.      */
  1652.     function paragraphHandled () {
  1653.         return $this->_paragraphHandled;
  1654.     }
  1655.     
  1656.     /**
  1657.      * Get flag if this element had a close tag
  1658.      *
  1659.      * @access public
  1660.      * @return bool 
  1661.      */
  1662.     function hadCloseTag () {
  1663.         return $this->_hadCloseTag;
  1664.     }
  1665.     
  1666.     /**
  1667.      * Determines whether a criterium matches this node
  1668.      *
  1669.      * @access public
  1670.      * @param string $criterium The criterium that is to be checked
  1671.      * @param mixed $value The value that is to be compared
  1672.      * @return bool True if this node matches that criterium
  1673.      */
  1674.     function matchesCriterium ($criterium, $value) {
  1675.         if ($criterium == 'tagName') {
  1676.             return ($value == $this->_name);
  1677.         }
  1678.         if ($criterium == 'needsTextNodeModification') {
  1679.             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);
  1680.         }
  1681.         if (substr ($criterium, 0, 5) == 'flag:') {
  1682.             $criterium = substr ($criterium, 5);
  1683.             return ($this->getFlag ($criterium) == $value);
  1684.         }
  1685.         if (substr ($criterium, 0, 6) == '!flag:') {
  1686.             $criterium = substr ($criterium, 6);
  1687.             return ($this->getFlag ($criterium) != $value);
  1688.         }
  1689.         if (substr ($criterium, 0, 6) == 'flag=:') {
  1690.             $criterium = substr ($criterium, 6);
  1691.             return ($this->getFlag ($criterium) === $value);
  1692.         }
  1693.         if (substr ($criterium, 0, 7) == '!flag=:') {
  1694.             $criterium = substr ($criterium, 7);
  1695.             return ($this->getFlag ($criterium) !== $value);
  1696.         }
  1697.         return parent::matchesCriterium ($criterium, $value);
  1698.     }
  1699.     
  1700.     /**
  1701.      * Get first child if it is a text node
  1702.      *
  1703.      * @access public
  1704.      * @return mixed 
  1705.      */
  1706.     function &firstChildIfText () {
  1707.         $ret =& $this->firstChild ();
  1708.         if (is_null ($ret)) {
  1709.             return $ret;
  1710.         }
  1711.         if ($ret->_type != STRINGPARSER_NODE_TEXT) {
  1712.             // DON'T DO $ret = null WITHOUT unset BEFORE!
  1713.             // ELSE WE WILL ERASE THE NODE ITSELF! EVIL!
  1714.             unset ($ret);
  1715.             $ret = null;
  1716.         }
  1717.         return $ret;
  1718.     }
  1719.     
  1720.     /**
  1721.      * Get last child if it is a text node AND if this element had a close tag
  1722.      *
  1723.      * @access public
  1724.      * @return mixed 
  1725.      */
  1726.     function &lastChildIfText () {
  1727.         $ret =& $this->lastChild ();
  1728.         if (is_null ($ret)) {
  1729.             return $ret;
  1730.         }
  1731.         if ($ret->_type != STRINGPARSER_NODE_TEXT || !$this->_hadCloseTag) {
  1732.             // DON'T DO $ret = null WITHOUT unset BEFORE!
  1733.             // ELSE WE WILL ERASE THE NODE ITSELF! EVIL!
  1734.             if ($ret->_type != STRINGPARSER_NODE_TEXT && !$ret->hadCloseTag ()) {
  1735.                 $ret2 =& $ret->_findPrevAdjentTextNodeHelper ();
  1736.                 unset ($ret);
  1737.                 $ret =& $ret2;
  1738.                 unset ($ret2);
  1739.             } else {
  1740.                 unset ($ret);
  1741.                 $ret = null;
  1742.             }
  1743.         }
  1744.         return $ret;
  1745.     }
  1746.     
  1747.     /**
  1748.      * Find next adjent text node after close tag
  1749.      *
  1750.      * returns the node or null if none exists
  1751.      *
  1752.      * @access public
  1753.      * @return mixed 
  1754.      */
  1755.     function &findNextAdjentTextNode () {
  1756.         $ret = null;
  1757.         if (is_null ($this->_parent)) {
  1758.             return $ret;
  1759.         }
  1760.         if (!$this->_hadCloseTag) {
  1761.             return $ret;
  1762.         }
  1763.         $ccount = count ($this->_parent->_children);
  1764.         $found = false;
  1765.         for ($i = 0; $i < $ccount; $i++) {
  1766.             if ($this->_parent->_children[$i]->equals ($this)) {
  1767.                 $found = $i;
  1768.                 break;
  1769.             }
  1770.         }
  1771.         if ($found === false) {
  1772.             return $ret;
  1773.         }
  1774.         if ($found < $ccount - 1) {
  1775.             if ($this->_parent->_children[$found+1]->_type == STRINGPARSER_NODE_TEXT) {
  1776.                 return $this->_parent->_children[$found+1];
  1777.             }
  1778.             return $ret;
  1779.         }
  1780.         if ($this->_parent->_type == STRINGPARSER_BBCODE_NODE_ELEMENT && !$this->_parent->hadCloseTag ()) {
  1781.             $ret =& $this->_parent->findNextAdjentTextNode ();
  1782.             return $ret;
  1783.         }
  1784.         return $ret;
  1785.     }
  1786.     
  1787.     /**
  1788.      * Find previous adjent text node before open tag
  1789.      *
  1790.      * returns the node or null if none exists
  1791.      *
  1792.      * @access public
  1793.      * @return mixed 
  1794.      */
  1795.     function &findPrevAdjentTextNode () {
  1796.         $ret = null;
  1797.         if (is_null ($this->_parent)) {
  1798.             return $ret;
  1799.         }
  1800.         $ccount = count ($this->_parent->_children);
  1801.         $found = false;
  1802.         for ($i = 0; $i < $ccount; $i++) {
  1803.             if ($this->_parent->_children[$i]->equals ($this)) {
  1804.                 $found = $i;
  1805.                 break;
  1806.             }
  1807.         }
  1808.         if ($found === false) {
  1809.             return $ret;
  1810.         }
  1811.         if ($found > 0) {
  1812.             if ($this->_parent->_children[$found-1]->_type == STRINGPARSER_NODE_TEXT) {
  1813.                 return $this->_parent->_children[$found-1];
  1814.             }
  1815.             if (!$this->_parent->_children[$found-1]->hadCloseTag ()) {
  1816.                 $ret =& $this->_parent->_children[$found-1]->_findPrevAdjentTextNodeHelper ();
  1817.             }
  1818.             return $ret;
  1819.         }
  1820.         return $ret;
  1821.     }
  1822.     
  1823.     /**
  1824.      * Helper function for findPrevAdjentTextNode
  1825.      *
  1826.      * Looks at the last child node; if it's a text node, it returns it,
  1827.      * if the element node did not have an open tag, it calls itself
  1828.      * recursively.
  1829.      */
  1830.     function &_findPrevAdjentTextNodeHelper () {
  1831.         $lastnode =& $this->lastChild ();
  1832.         if ($lastnode === null || $lastnode->_type == STRINGPARSER_NODE_TEXT) {
  1833.             return $lastnode;
  1834.         }
  1835.         if (!$lastnode->hadCloseTag ()) {
  1836.             $ret =& $lastnode->_findPrevAdjentTextNodeHelper ();
  1837.         } else {
  1838.             $ret = null;
  1839.         }
  1840.         return $ret;
  1841.     }
  1842.     
  1843.     /**
  1844.      * Get Flag
  1845.      *
  1846.      * @access public
  1847.      * @param string $flag The requested flag
  1848.      * @param string $type The requested type of the return value
  1849.      * @param mixed $default The default return value
  1850.      * @return mixed 
  1851.      */
  1852.     function getFlag ($flag, $type = 'mixed', $default = null) {
  1853.         if (!isset ($this->_flags[$flag])) {
  1854.             return $default;
  1855.         }
  1856.         $return = $this->_flags[$flag];
  1857.         if ($type != 'mixed') {
  1858.             settype ($return, $type);
  1859.         }
  1860.         return $return;
  1861.     }
  1862.     
  1863.     /**
  1864.      * Set a flag
  1865.      *
  1866.      * @access public
  1867.      * @param string $name The name of the flag
  1868.      * @param mixed $value The value of the flag
  1869.      */
  1870.     function setFlag ($name, $value) {
  1871.         $this->_flags[$name] = $value;
  1872.         return true;
  1873.     }
  1874.     
  1875.     /**
  1876.      * Validate code
  1877.      *
  1878.      * @access public
  1879.      * @param string $action The action which is to be called ('validate'
  1880.      *                        for first validation, 'validate_again' for
  1881.      *                        second validation (optional))
  1882.      * @return bool 
  1883.      */
  1884.     function validate ($action = 'validate') {
  1885.         if ($action != 'validate' && $action != 'validate_again') {
  1886.             return false;
  1887.         }
  1888.         if ($this->_codeInfo['callback_type'] != 'simple_replace' && $this->_codeInfo['callback_type'] != 'simple_replace_single') {
  1889.             if (!is_callable ($this->_codeInfo['callback_func'])) {
  1890.                 return false;
  1891.             }
  1892.             
  1893.             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) {
  1894.                 // we have to make sure the object gets passed on as a reference
  1895.                 // if we do call_user_func(..., &$this) this will clash with PHP5
  1896.                 $callArray = array ($action, $this->_attributes, $this->_children[0]->content, $this->_codeInfo['callback_params']);
  1897.                 $callArray[] =& $this;
  1898.                 $res = call_user_func_array ($this->_codeInfo['callback_func'], $callArray);
  1899.                 if ($res) {
  1900.                     // ok, now, if we've got a usecontent type, set a flag that
  1901.                     // this may not be broken up by paragraph handling!
  1902.                     // but PLEASE do NOT change if already set to any other setting
  1903.                     // than BBCODE_PARAGRAPH_ALLOW_BREAKUP because we could
  1904.                     // override e.g. BBCODE_PARAGRAPH_BLOCK_ELEMENT!
  1905.                     $val = $this->getFlag ('paragraph_type', 'integer', BBCODE_PARAGRAPH_ALLOW_BREAKUP);
  1906.                     if ($val == BBCODE_PARAGRAPH_ALLOW_BREAKUP) {
  1907.                         $this->_flags['paragraph_type'] = BBCODE_PARAGRAPH_ALLOW_INSIDE;
  1908.                     }
  1909.                 }
  1910.                 return $res;
  1911.             }
  1912.             
  1913.             // we have to make sure the object gets passed on as a reference
  1914.             // if we do call_user_func(..., &$this) this will clash with PHP5
  1915.             $callArray = array ($action, $this->_attributes, null, $this->_codeInfo['callback_params']);
  1916.             $callArray[] =& $this;
  1917.             return call_user_func_array ($this->_codeInfo['callback_func'], $callArray);
  1918.         }
  1919.         return (bool)(!count ($this->_attributes));
  1920.     }
  1921.     
  1922.     /**
  1923.      * Get replacement for this code
  1924.      *
  1925.      * @access public
  1926.      * @param string $subcontent The content of all sub-nodes
  1927.      * @return string 
  1928.      */
  1929.     function getReplacement ($subcontent) {
  1930.         if ($this->_codeInfo['callback_type'] == 'simple_replace' || $this->_codeInfo['callback_type'] == 'simple_replace_single') {
  1931.             if ($this->_codeInfo['callback_type'] == 'simple_replace_single') {
  1932.                 if (strlen ($subcontent)) { // can't be!
  1933.                     return false;
  1934.                 }
  1935.                 return $this->_codeInfo['callback_params']['start_tag'];
  1936.             }
  1937.             return $this->_codeInfo['callback_params']['start_tag'].$subcontent.$this->_codeInfo['callback_params']['end_tag'];
  1938.         }
  1939.         // else usecontent, usecontent? or callback_replace or callback_replace_single
  1940.         // => call function (the function is callable, determined in validate()!)
  1941.         
  1942.         // we have to make sure the object gets passed on as a reference
  1943.         // if we do call_user_func(..., &$this) this will clash with PHP5
  1944.         $callArray = array ('output', $this->_attributes, $subcontent, $this->_codeInfo['callback_params']);
  1945.         $callArray[] =& $this;
  1946.         return call_user_func_array ($this->_codeInfo['callback_func'], $callArray);
  1947.     }
  1948.     
  1949.     /**
  1950.      * Dump this node to a string
  1951.      *
  1952.      * @access protected
  1953.      * @return string 
  1954.      */
  1955.     function _dumpToString () {
  1956.         $str = "bbcode \"".substr (preg_replace ('/\s+/', ' ', $this->_name), 0, 40)."\"";
  1957.         if (count ($this->_attributes)) {
  1958.             $attribs = array_keys ($this->_attributes);
  1959.             sort ($attribs);
  1960.             $str .= ' (';
  1961.             $i = 0;
  1962.             foreach ($attribs as $attrib) {
  1963.                 if ($i != 0) {
  1964.                     $str .= ', ';
  1965.                 }
  1966.                 $str .= $attrib.'="';
  1967.                 $str .= substr (preg_replace ('/\s+/', ' ', $this->_attributes[$attrib]), 0, 10);
  1968.                 $str .= '"';
  1969.                 $i++;
  1970.             }
  1971.             $str .= ')';
  1972.         }
  1973.         return $str;
  1974.     }
  1975. }
  1976.  
  1977. ?>

Documentation generated on Mon, 10 Dec 2007 13:29:48 +0100 by phpDocumentor 1.4.0