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[$namearray (
  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[$typearray ();
  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[$typeas $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->_savedNametrue);
  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->_savedNametrue);
  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 ($namefalse);
  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_FORBIDDENBBCODE_CLOSETAG_OPTIONAL))) {
  924.             return false;
  925.         }
  926.         $node =$this->_findNamedNode ($nametrue);
  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_ONLYBBCODE_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->_stack1{
  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 ($output1);
  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 ($output0-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 != || $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->_codesas $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->_children1{
  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]->content2{
  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 ($content1);
  1453.             }
  1454.             if ($f_end != BBCODE_NEWLINE_PARSE && $content{strlen($content)-1== "\n"{
  1455.                 $content substr ($content0-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 ($criterium05== 'flag:'{
  1682.             $criterium substr ($criterium5);
  1683.             return ($this->getFlag ($criterium== $value);
  1684.         }
  1685.         if (substr ($criterium06== '!flag:'{
  1686.             $criterium substr ($criterium6);
  1687.             return ($this->getFlag ($criterium!= $value);
  1688.         }
  1689.         if (substr ($criterium06== 'flag=:'{
  1690.             $criterium substr ($criterium6);
  1691.             return ($this->getFlag ($criterium=== $value);
  1692.         }
  1693.         if (substr ($criterium07== '!flag=:'{
  1694.             $criterium substr ($criterium7);
  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== && $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->_attributesnull$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)040)."\"";
  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])010);
  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