2125 lines
53 KiB
PHP
Executable File
2125 lines
53 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* BB code string parsing class
|
|
*
|
|
* Version: 0.3.3
|
|
*
|
|
* @author Christian Seiler <spam@christian-seiler.de>
|
|
* @copyright Christian Seiler 2004-2008
|
|
* @package stringparser
|
|
*
|
|
* The MIT License
|
|
*
|
|
* Copyright (c) 2004-2008 Christian Seiler
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
require_once dirname(__FILE__) . '/stringparser.class.php';
|
|
|
|
define('BBCODE_CLOSETAG_FORBIDDEN', -1);
|
|
define('BBCODE_CLOSETAG_OPTIONAL', 0);
|
|
define('BBCODE_CLOSETAG_IMPLICIT', 1);
|
|
define('BBCODE_CLOSETAG_IMPLICIT_ON_CLOSE_ONLY', 2);
|
|
define('BBCODE_CLOSETAG_MUSTEXIST', 3);
|
|
|
|
define('BBCODE_NEWLINE_PARSE', 0);
|
|
define('BBCODE_NEWLINE_IGNORE', 1);
|
|
define('BBCODE_NEWLINE_DROP', 2);
|
|
|
|
define('BBCODE_PARAGRAPH_ALLOW_BREAKUP', 0);
|
|
define('BBCODE_PARAGRAPH_ALLOW_INSIDE', 1);
|
|
define('BBCODE_PARAGRAPH_BLOCK_ELEMENT', 2);
|
|
|
|
/**
|
|
* BB code string parser class
|
|
*
|
|
* @package stringparser
|
|
*/
|
|
class StringParser_BBCode extends StringParser {
|
|
|
|
/**
|
|
* String parser mode
|
|
*
|
|
* The BBCode string parser works in search mode
|
|
*
|
|
* @access protected
|
|
* @var int
|
|
* @see STRINGPARSER_MODE_SEARCH, STRINGPARSER_MODE_LOOP
|
|
*/
|
|
var $_parserMode = STRINGPARSER_MODE_SEARCH;
|
|
|
|
/**
|
|
* Defined BB Codes
|
|
*
|
|
* The registered BB codes
|
|
*
|
|
* @access protected
|
|
* @var array
|
|
*/
|
|
var $_codes = array();
|
|
|
|
/**
|
|
* Registered parsers
|
|
*
|
|
* @access protected
|
|
* @var array
|
|
*/
|
|
var $_parsers = array();
|
|
|
|
/**
|
|
* Defined maximum occurrences
|
|
*
|
|
* @access protected
|
|
* @var array
|
|
*/
|
|
var $_maxOccurrences = array();
|
|
|
|
/**
|
|
* Root content type
|
|
*
|
|
* @access protected
|
|
* @var string
|
|
*/
|
|
var $_rootContentType = 'block';
|
|
|
|
/**
|
|
* Do not output but return the tree
|
|
*
|
|
* @access protected
|
|
* @var bool
|
|
*/
|
|
var $_noOutput = false;
|
|
|
|
/**
|
|
* Global setting: case sensitive
|
|
*
|
|
* @access protected
|
|
* @var bool
|
|
*/
|
|
var $_caseSensitive = true;
|
|
|
|
/**
|
|
* Root paragraph handling enabled
|
|
*
|
|
* @access protected
|
|
* @var bool
|
|
*/
|
|
var $_rootParagraphHandling = false;
|
|
|
|
/**
|
|
* Paragraph handling parameters
|
|
*
|
|
* @access protected
|
|
* @var array
|
|
*/
|
|
var $_paragraphHandling = array(
|
|
'detect_string' => "\n\n",
|
|
'start_tag' => '<p>',
|
|
'end_tag' => "</p>\n"
|
|
);
|
|
|
|
/**
|
|
* Allow mixed attribute types (e.g.
|
|
* [code=bla attr=blub])
|
|
*
|
|
* @access private
|
|
* @var bool
|
|
*/
|
|
var $_mixedAttributeTypes = false;
|
|
|
|
/**
|
|
* Whether to call validation function again (with $action == 'validate_auto') when closetag comes
|
|
*
|
|
* @access protected
|
|
* @var bool
|
|
*/
|
|
var $_validateAgain = false;
|
|
|
|
/**
|
|
* Add a code
|
|
*
|
|
* @access public
|
|
* @param string $name
|
|
* The name of the code
|
|
* @param string $callback_type
|
|
* See documentation
|
|
* @param string $callback_func
|
|
* The callback function to call
|
|
* @param array $callback_params
|
|
* The callback parameters
|
|
* @param string $content_type
|
|
* See documentation
|
|
* @param array $allowed_within
|
|
* See documentation
|
|
* @param array $not_allowed_within
|
|
* See documentation
|
|
* @return bool
|
|
*/
|
|
function addCode($name, $callback_type, $callback_func, $callback_params, $content_type, $allowed_within, $not_allowed_within) {
|
|
if (isset($this->_codes [$name])) {
|
|
return false; // already exists
|
|
}
|
|
if (!preg_match('/^[a-zA-Z0-9*_!+-]+$/', $name)) {
|
|
return false; // invalid
|
|
}
|
|
$this->_codes [$name] = array(
|
|
'name' => $name,
|
|
'callback_type' => $callback_type,
|
|
'callback_func' => $callback_func,
|
|
'callback_params' => $callback_params,
|
|
'content_type' => $content_type,
|
|
'allowed_within' => $allowed_within,
|
|
'not_allowed_within' => $not_allowed_within,
|
|
'flags' => array()
|
|
);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Remove a code
|
|
*
|
|
* @access public
|
|
* @param string $name
|
|
* The code to remove
|
|
* @return bool
|
|
*/
|
|
function removeCode($name) {
|
|
if (isset($this->_codes [$name])) {
|
|
unset($this->_codes [$name]);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Remove all codes
|
|
*
|
|
* @access public
|
|
*/
|
|
function removeAllCodes() {
|
|
$this->_codes = array();
|
|
}
|
|
|
|
/**
|
|
* Set a code flag
|
|
*
|
|
* @access public
|
|
* @param string $name
|
|
* The name of the code
|
|
* @param string $flag
|
|
* The name of the flag to set
|
|
* @param mixed $value
|
|
* The value of the flag to set
|
|
* @return bool
|
|
*/
|
|
function setCodeFlag($name, $flag, $value) {
|
|
if (!isset($this->_codes [$name])) {
|
|
return false;
|
|
}
|
|
$this->_codes [$name] ['flags'] [$flag] = $value;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set occurrence type
|
|
*
|
|
* Example:
|
|
* $bbcode->setOccurrenceType ('url', 'link');
|
|
* $bbcode->setMaxOccurrences ('link', 4);
|
|
* Would create the situation where a link may only occur four
|
|
* times in the hole text.
|
|
*
|
|
* @access public
|
|
* @param string $code
|
|
* The name of the code
|
|
* @param string $type
|
|
* The name of the occurrence type to set
|
|
* @return bool
|
|
*/
|
|
function setOccurrenceType($code, $type) {
|
|
return $this->setCodeFlag($code, 'occurrence_type', $type);
|
|
}
|
|
|
|
/**
|
|
* Set maximum number of occurrences
|
|
*
|
|
* @access public
|
|
* @param string $type
|
|
* The name of the occurrence type
|
|
* @param int $count
|
|
* The maximum number of occurrences
|
|
* @return bool
|
|
*/
|
|
function setMaxOccurrences($type, $count) {
|
|
settype($count, 'integer');
|
|
if ($count < 0) { // sorry, does not make any sense
|
|
return false;
|
|
}
|
|
$this->_maxOccurrences [$type] = $count;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Add a parser
|
|
*
|
|
* @access public
|
|
* @param string $type
|
|
* The content type for which the parser is to add
|
|
* @param mixed $parser
|
|
* The function to call
|
|
* @return bool
|
|
*/
|
|
function addParser($type, $parser) {
|
|
if (is_array($type)) {
|
|
foreach ($type as $t) {
|
|
$this->addParser($t, $parser);
|
|
}
|
|
return true;
|
|
}
|
|
if (!isset($this->_parsers [$type])) {
|
|
$this->_parsers [$type] = array();
|
|
}
|
|
$this->_parsers [$type] [] = $parser;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set root content type
|
|
*
|
|
* @access public
|
|
* @param string $content_type
|
|
* The new root content type
|
|
*/
|
|
function setRootContentType($content_type) {
|
|
$this->_rootContentType = $content_type;
|
|
}
|
|
|
|
/**
|
|
* Set paragraph handling on root element
|
|
*
|
|
* @access public
|
|
* @param bool $enabled
|
|
* The new status of paragraph handling on root element
|
|
*/
|
|
function setRootParagraphHandling($enabled) {
|
|
$this->_rootParagraphHandling = (bool) $enabled;
|
|
}
|
|
|
|
/**
|
|
* Set paragraph handling parameters
|
|
*
|
|
* @access public
|
|
* @param string $detect_string
|
|
* The string to detect
|
|
* @param string $start_tag
|
|
* The replacement for the start tag (e.g. <p>)
|
|
* @param string $end_tag
|
|
* The replacement for the start tag (e.g. </p>)
|
|
*/
|
|
function setParagraphHandlingParameters($detect_string, $start_tag, $end_tag) {
|
|
$this->_paragraphHandling = array(
|
|
'detect_string' => $detect_string,
|
|
'start_tag' => $start_tag,
|
|
'end_tag' => $end_tag
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Set global case sensitive flag
|
|
*
|
|
* If this is set to true, the class normally is case sensitive, but
|
|
* the case_sensitive code flag may override this for a single code.
|
|
*
|
|
* If this is set to false, all codes are case insensitive.
|
|
*
|
|
* @access public
|
|
* @param bool $caseSensitive
|
|
*/
|
|
function setGlobalCaseSensitive($caseSensitive) {
|
|
$this->_caseSensitive = (bool) $caseSensitive;
|
|
}
|
|
|
|
/**
|
|
* Get global case sensitive flag
|
|
*
|
|
* @access public
|
|
* @return bool
|
|
*/
|
|
function globalCaseSensitive() {
|
|
return $this->_caseSensitive;
|
|
}
|
|
|
|
/**
|
|
* Set mixed attribute types flag
|
|
*
|
|
* If set, [code=val1 attr=val2] will cause 2 attributes to be parsed:
|
|
* 'default' will have value 'val1', 'attr' will have value 'val2'.
|
|
* If not set, only one attribute 'default' will have the value
|
|
* 'val1 attr=val2' (the default and original behaviour)
|
|
*
|
|
* @access public
|
|
* @param bool $mixedAttributeTypes
|
|
*/
|
|
function setMixedAttributeTypes($mixedAttributeTypes) {
|
|
$this->_mixedAttributeTypes = (bool) $mixedAttributeTypes;
|
|
}
|
|
|
|
/**
|
|
* Get mixed attribute types flag
|
|
*
|
|
* @access public
|
|
* @return bool
|
|
*/
|
|
function mixedAttributeTypes() {
|
|
return $this->_mixedAttributeTypes;
|
|
}
|
|
|
|
/**
|
|
* Set validate again flag
|
|
*
|
|
* If this is set to true, the class calls the validation function
|
|
* again with $action == 'validate_again' when closetag comes.
|
|
*
|
|
* @access public
|
|
* @param bool $validateAgain
|
|
*/
|
|
function setValidateAgain($validateAgain) {
|
|
$this->_validateAgain = (bool) $validateAgain;
|
|
}
|
|
|
|
/**
|
|
* Get validate again flag
|
|
*
|
|
* @access public
|
|
* @return bool
|
|
*/
|
|
function validateAgain() {
|
|
return $this->_validateAgain;
|
|
}
|
|
|
|
/**
|
|
* Get a code flag
|
|
*
|
|
* @access public
|
|
* @param string $name
|
|
* The name of the code
|
|
* @param string $flag
|
|
* The name of the flag to get
|
|
* @param string $type
|
|
* The type of the return value
|
|
* @param mixed $default
|
|
* The default return value
|
|
* @return bool
|
|
*/
|
|
function getCodeFlag($name, $flag, $type = 'mixed', $default = null) {
|
|
if (!isset($this->_codes [$name])) {
|
|
return $default;
|
|
}
|
|
if (!array_key_exists($flag, $this->_codes [$name] ['flags'])) {
|
|
return $default;
|
|
}
|
|
$return = $this->_codes [$name] ['flags'] [$flag];
|
|
if ($type != 'mixed') {
|
|
settype($return, $type);
|
|
}
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Set a specific status
|
|
*
|
|
* @access protected
|
|
*/
|
|
function _setStatus($status) {
|
|
switch ($status) {
|
|
case 0:
|
|
$this->_charactersSearch = array(
|
|
'[/',
|
|
'['
|
|
);
|
|
$this->_status = $status;
|
|
break;
|
|
case 1:
|
|
$this->_charactersSearch = array(
|
|
']',
|
|
' = "',
|
|
'="',
|
|
' = \'',
|
|
'=\'',
|
|
' = ',
|
|
'=',
|
|
': ',
|
|
':',
|
|
' '
|
|
);
|
|
$this->_status = $status;
|
|
break;
|
|
case 2:
|
|
$this->_charactersSearch = array(
|
|
']'
|
|
);
|
|
$this->_status = $status;
|
|
$this->_savedName = '';
|
|
break;
|
|
case 3:
|
|
if ($this->_quoting !== null) {
|
|
if ($this->_mixedAttributeTypes) {
|
|
$this->_charactersSearch = array(
|
|
'\\\\',
|
|
'\\' . $this->_quoting,
|
|
$this->_quoting . ' ',
|
|
$this->_quoting . ']',
|
|
$this->_quoting
|
|
);
|
|
} else {
|
|
$this->_charactersSearch = array(
|
|
'\\\\',
|
|
'\\' . $this->_quoting,
|
|
$this->_quoting . ']',
|
|
$this->_quoting
|
|
);
|
|
}
|
|
$this->_status = $status;
|
|
break;
|
|
}
|
|
if ($this->_mixedAttributeTypes) {
|
|
$this->_charactersSearch = array(
|
|
' ',
|
|
']'
|
|
);
|
|
} else {
|
|
$this->_charactersSearch = array(
|
|
']'
|
|
);
|
|
}
|
|
$this->_status = $status;
|
|
break;
|
|
case 4:
|
|
$this->_charactersSearch = array(
|
|
' ',
|
|
']',
|
|
'="',
|
|
'=\'',
|
|
'='
|
|
);
|
|
$this->_status = $status;
|
|
$this->_savedName = '';
|
|
$this->_savedValue = '';
|
|
break;
|
|
case 5:
|
|
if ($this->_quoting !== null) {
|
|
$this->_charactersSearch = array(
|
|
'\\\\',
|
|
'\\' . $this->_quoting,
|
|
$this->_quoting . ' ',
|
|
$this->_quoting . ']',
|
|
$this->_quoting
|
|
);
|
|
} else {
|
|
$this->_charactersSearch = array(
|
|
' ',
|
|
']'
|
|
);
|
|
}
|
|
$this->_status = $status;
|
|
$this->_savedValue = '';
|
|
break;
|
|
case 7:
|
|
$this->_charactersSearch = array(
|
|
'[/' . $this->_topNode('name') . ']'
|
|
);
|
|
if (!$this->_topNode('getFlag', 'case_sensitive', 'boolean', true) || !$this->_caseSensitive) {
|
|
$this->_charactersSearch [] = '[/';
|
|
}
|
|
$this->_status = $status;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Abstract method Append text depending on current status
|
|
*
|
|
* @access protected
|
|
* @param string $text
|
|
* The text to append
|
|
* @return bool On success, the function returns true, else false
|
|
*/
|
|
function _appendText($text) {
|
|
if (!strlen($text)) {
|
|
return true;
|
|
}
|
|
switch ($this->_status) {
|
|
case 0:
|
|
case 7:
|
|
return $this->_appendToLastTextChild($text);
|
|
case 1:
|
|
return $this->_topNode('appendToName', $text);
|
|
case 2:
|
|
case 4:
|
|
$this->_savedName .= $text;
|
|
return true;
|
|
case 3:
|
|
return $this->_topNode('appendToAttribute', 'default', $text);
|
|
case 5:
|
|
$this->_savedValue .= $text;
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restart parsing after current block
|
|
*
|
|
* To achieve this the current top stack object is removed from the
|
|
* tree. Then the current item
|
|
*
|
|
* @access protected
|
|
* @return bool
|
|
*/
|
|
function _reparseAfterCurrentBlock() {
|
|
if ($this->_status == 2) {
|
|
// this status will *never* call _reparseAfterCurrentBlock itself
|
|
// so this is called if the loop ends
|
|
// therefore, just add the [/ to the text
|
|
|
|
// _savedName should be empty but just in case
|
|
$this->_cpos -= strlen($this->_savedName);
|
|
$this->_savedName = '';
|
|
$this->_status = 0;
|
|
$this->_appendText('[/');
|
|
return true;
|
|
} else {
|
|
return parent::_reparseAfterCurrentBlock();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply parsers
|
|
*/
|
|
function _applyParsers($type, $text) {
|
|
if (!isset($this->_parsers [$type])) {
|
|
return $text;
|
|
}
|
|
foreach ($this->_parsers [$type] as $parser) {
|
|
if (is_callable($parser)) {
|
|
$ntext = call_user_func($parser, $text);
|
|
if (is_string($ntext)) {
|
|
$text = $ntext;
|
|
}
|
|
}
|
|
}
|
|
return $text;
|
|
}
|
|
|
|
/**
|
|
* Handle status
|
|
*
|
|
* @access protected
|
|
* @param int $status
|
|
* The current status
|
|
* @param string $needle
|
|
* The needle that was found
|
|
* @return bool
|
|
*/
|
|
function _handleStatus($status, $needle) {
|
|
switch ($status) {
|
|
case 0: // NORMAL TEXT
|
|
if ($needle != '[' && $needle != '[/') {
|
|
$this->_appendText($needle);
|
|
return true;
|
|
}
|
|
if ($needle == '[') {
|
|
$node = new StringParser_BBCode_Node_Element($this->_cpos);
|
|
$res = $this->_pushNode($node);
|
|
if (!$res) {
|
|
return false;
|
|
}
|
|
$this->_setStatus(1);
|
|
} else if ($needle == '[/') {
|
|
if (count($this->_stack) <= 1) {
|
|
$this->_appendText($needle);
|
|
return true;
|
|
}
|
|
$this->_setStatus(2);
|
|
}
|
|
break;
|
|
case 1: // OPEN TAG
|
|
if ($needle == ']') {
|
|
return $this->_openElement(0);
|
|
} else if (trim($needle) == ':' || trim($needle) == '=') {
|
|
$this->_quoting = null;
|
|
$this->_setStatus(3); // default value parser
|
|
break;
|
|
} else if (trim($needle) == '="' || trim($needle) == '= "' || trim($needle) == '=\'' || trim($needle) == '= \'') {
|
|
$this->_quoting = substr(trim($needle), -1);
|
|
$this->_setStatus(3); // default value parser with quotation
|
|
break;
|
|
} else if ($needle == ' ') {
|
|
$this->_setStatus(4); // attribute parser
|
|
break;
|
|
} else {
|
|
$this->_appendText($needle);
|
|
return true;
|
|
}
|
|
// break not necessary because every if clause contains return
|
|
case 2: // CLOSE TAG
|
|
if ($needle != ']') {
|
|
$this->_appendText($needle);
|
|
return true;
|
|
}
|
|
$closecount = 0;
|
|
if (!$this->_isCloseable($this->_savedName, $closecount)) {
|
|
$this->_setStatus(0);
|
|
$this->_appendText('[/' . $this->_savedName . $needle);
|
|
return true;
|
|
}
|
|
// this validates the code(s) to be closed after the content tree of
|
|
// that code(s) are built - if the second validation fails, we will have
|
|
// to reparse. note that as _reparseAfterCurrentBlock will not work correctly
|
|
// if we're in $status == 2, we will have to set our status to 0 manually
|
|
if (!$this->_validateCloseTags($closecount)) {
|
|
$this->_setStatus(0);
|
|
return $this->_reparseAfterCurrentBlock();
|
|
}
|
|
$this->_setStatus(0);
|
|
for($i = 0; $i < $closecount; $i++) {
|
|
if ($i == $closecount - 1) {
|
|
$this->_topNode('setHadCloseTag');
|
|
}
|
|
if (!$this->_popNode()) {
|
|
return false;
|
|
}
|
|
}
|
|
break;
|
|
case 3: // DEFAULT ATTRIBUTE
|
|
if ($this->_quoting !== null) {
|
|
if ($needle == '\\\\') {
|
|
$this->_appendText('\\');
|
|
return true;
|
|
} else if ($needle == '\\' . $this->_quoting) {
|
|
$this->_appendText($this->_quoting);
|
|
return true;
|
|
} else if ($needle == $this->_quoting . ' ') {
|
|
$this->_setStatus(4);
|
|
return true;
|
|
} else if ($needle == $this->_quoting . ']') {
|
|
return $this->_openElement(2);
|
|
} else if ($needle == $this->_quoting) {
|
|
// can't be, only ']' and ' ' allowed after quoting char
|
|
return $this->_reparseAfterCurrentBlock();
|
|
} else {
|
|
$this->_appendText($needle);
|
|
return true;
|
|
}
|
|
} else {
|
|
if ($needle == ' ') {
|
|
$this->_setStatus(4);
|
|
return true;
|
|
} else if ($needle == ']') {
|
|
return $this->_openElement(2);
|
|
} else {
|
|
$this->_appendText($needle);
|
|
return true;
|
|
}
|
|
}
|
|
// break not needed because every if clause contains return!
|
|
case 4: // ATTRIBUTE NAME
|
|
if ($needle == ' ') {
|
|
if (strlen($this->_savedName)) {
|
|
$this->_topNode('setAttribute', $this->_savedName, true);
|
|
}
|
|
// just ignore and continue in same mode
|
|
$this->_setStatus(4); // reset parameters
|
|
return true;
|
|
} else if ($needle == ']') {
|
|
if (strlen($this->_savedName)) {
|
|
$this->_topNode('setAttribute', $this->_savedName, true);
|
|
}
|
|
return $this->_openElement(2);
|
|
} else if ($needle == '=') {
|
|
$this->_quoting = null;
|
|
$this->_setStatus(5);
|
|
return true;
|
|
} else if ($needle == '="') {
|
|
$this->_quoting = '"';
|
|
$this->_setStatus(5);
|
|
return true;
|
|
} else if ($needle == '=\'') {
|
|
$this->_quoting = '\'';
|
|
$this->_setStatus(5);
|
|
return true;
|
|
} else {
|
|
$this->_appendText($needle);
|
|
return true;
|
|
}
|
|
// break not needed because every if clause contains return!
|
|
case 5: // ATTRIBUTE VALUE
|
|
if ($this->_quoting !== null) {
|
|
if ($needle == '\\\\') {
|
|
$this->_appendText('\\');
|
|
return true;
|
|
} else if ($needle == '\\' . $this->_quoting) {
|
|
$this->_appendText($this->_quoting);
|
|
return true;
|
|
} else if ($needle == $this->_quoting . ' ') {
|
|
$this->_topNode('setAttribute', $this->_savedName, $this->_savedValue);
|
|
$this->_setStatus(4);
|
|
return true;
|
|
} else if ($needle == $this->_quoting . ']') {
|
|
$this->_topNode('setAttribute', $this->_savedName, $this->_savedValue);
|
|
return $this->_openElement(2);
|
|
} else if ($needle == $this->_quoting) {
|
|
// can't be, only ']' and ' ' allowed after quoting char
|
|
return $this->_reparseAfterCurrentBlock();
|
|
} else {
|
|
$this->_appendText($needle);
|
|
return true;
|
|
}
|
|
} else {
|
|
if ($needle == ' ') {
|
|
$this->_topNode('setAttribute', $this->_savedName, $this->_savedValue);
|
|
$this->_setStatus(4);
|
|
return true;
|
|
} else if ($needle == ']') {
|
|
$this->_topNode('setAttribute', $this->_savedName, $this->_savedValue);
|
|
return $this->_openElement(2);
|
|
} else {
|
|
$this->_appendText($needle);
|
|
return true;
|
|
}
|
|
}
|
|
// break not needed because every if clause contains return!
|
|
case 7:
|
|
if ($needle == '[/') {
|
|
// this was case insensitive match
|
|
if (strtolower(substr($this->_text, $this->_cpos + strlen($needle), strlen($this->_topNode('name')) + 1)) == strtolower($this->_topNode('name') . ']')) {
|
|
// this matched
|
|
$this->_cpos += strlen($this->_topNode('name')) + 1;
|
|
} else {
|
|
// it didn't match
|
|
$this->_appendText($needle);
|
|
return true;
|
|
}
|
|
}
|
|
$closecount = $this->_savedCloseCount;
|
|
if (!$this->_topNode('validate')) {
|
|
return $this->_reparseAfterCurrentBlock();
|
|
}
|
|
// do we have to close subnodes?
|
|
if ($closecount) {
|
|
// get top node
|
|
$mynode = $this->_stack [count($this->_stack) - 1];
|
|
// close necessary nodes
|
|
for($i = 0; $i <= $closecount; $i++) {
|
|
if (!$this->_popNode()) {
|
|
return false;
|
|
}
|
|
}
|
|
if (!$this->_pushNode($mynode)) {
|
|
return false;
|
|
}
|
|
}
|
|
$this->_setStatus(0);
|
|
$this->_popNode();
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Open the next element
|
|
*
|
|
* @access protected
|
|
* @return bool
|
|
*/
|
|
function _openElement($type = 0) {
|
|
$name = $this->_getCanonicalName($this->_topNode('name'));
|
|
if ($name === false) {
|
|
return $this->_reparseAfterCurrentBlock();
|
|
}
|
|
$occ_type = $this->getCodeFlag($name, 'occurrence_type', 'string');
|
|
if ($occ_type !== null && isset($this->_maxOccurrences [$occ_type])) {
|
|
$max_occs = $this->_maxOccurrences [$occ_type];
|
|
$occs = $this->_root->getNodeCountByCriterium('flag:occurrence_type', $occ_type);
|
|
if ($occs >= $max_occs) {
|
|
return $this->_reparseAfterCurrentBlock();
|
|
}
|
|
}
|
|
$closecount = 0;
|
|
$this->_topNode('setCodeInfo', $this->_codes [$name]);
|
|
if (!$this->_isOpenable($name, $closecount)) {
|
|
return $this->_reparseAfterCurrentBlock();
|
|
}
|
|
$this->_setStatus(0);
|
|
switch ($type) {
|
|
case 0:
|
|
$cond = $this->_isUseContent($this->_stack [count($this->_stack) - 1], false);
|
|
break;
|
|
case 1:
|
|
$cond = $this->_isUseContent($this->_stack [count($this->_stack) - 1], true);
|
|
break;
|
|
case 2:
|
|
$cond = $this->_isUseContent($this->_stack [count($this->_stack) - 1], true);
|
|
break;
|
|
default:
|
|
$cond = false;
|
|
break;
|
|
}
|
|
if ($cond) {
|
|
$this->_savedCloseCount = $closecount;
|
|
$this->_setStatus(7);
|
|
return true;
|
|
}
|
|
if (!$this->_topNode('validate')) {
|
|
return $this->_reparseAfterCurrentBlock();
|
|
}
|
|
// do we have to close subnodes?
|
|
if ($closecount) {
|
|
// get top node
|
|
$mynode = $this->_stack [count($this->_stack) - 1];
|
|
// close necessary nodes
|
|
for($i = 0; $i <= $closecount; $i++) {
|
|
if (!$this->_popNode()) {
|
|
return false;
|
|
}
|
|
}
|
|
if (!$this->_pushNode($mynode)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ($this->_codes [$name] ['callback_type'] == 'simple_replace_single' || $this->_codes [$name] ['callback_type'] == 'callback_replace_single') {
|
|
if (!$this->_popNode()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Is a node closeable?
|
|
*
|
|
* @access protected
|
|
* @return bool
|
|
*/
|
|
function _isCloseable($name, &$closecount) {
|
|
$node = $this->_findNamedNode($name, false);
|
|
if ($node === false) {
|
|
return false;
|
|
}
|
|
$scount = count($this->_stack);
|
|
for($i = $scount - 1; $i > 0; $i--) {
|
|
$closecount++;
|
|
if ($this->_stack [$i]->equals($node)) {
|
|
return true;
|
|
}
|
|
if ($this->_stack [$i]->getFlag('closetag', 'integer', BBCODE_CLOSETAG_IMPLICIT) == BBCODE_CLOSETAG_MUSTEXIST) {
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Revalidate codes when close tags appear
|
|
*
|
|
* @access protected
|
|
* @return bool
|
|
*/
|
|
function _validateCloseTags($closecount) {
|
|
$scount = count($this->_stack);
|
|
for($i = $scount - 1; $i >= $scount - $closecount; $i--) {
|
|
if ($this->_validateAgain) {
|
|
if (!$this->_stack [$i]->validate('validate_again')) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Is a node openable?
|
|
*
|
|
* @access protected
|
|
* @return bool
|
|
*/
|
|
function _isOpenable($name, &$closecount) {
|
|
if (!isset($this->_codes [$name])) {
|
|
return false;
|
|
}
|
|
|
|
$closecount = 0;
|
|
|
|
$allowed_within = $this->_codes [$name] ['allowed_within'];
|
|
$not_allowed_within = $this->_codes [$name] ['not_allowed_within'];
|
|
|
|
$scount = count($this->_stack);
|
|
if ($scount == 2) { // top level element
|
|
if (!in_array($this->_rootContentType, $allowed_within)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
if (!in_array($this->_stack [$scount - 2]->_codeInfo ['content_type'], $allowed_within)) {
|
|
return $this->_isOpenableWithClose($name, $closecount);
|
|
}
|
|
}
|
|
|
|
for($i = 1; $i < $scount - 1; $i++) {
|
|
if (in_array($this->_stack [$i]->_codeInfo ['content_type'], $not_allowed_within)) {
|
|
return $this->_isOpenableWithClose($name, $closecount);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Is a node openable by closing other nodes?
|
|
*
|
|
* @access protected
|
|
* @return bool
|
|
*/
|
|
function _isOpenableWithClose($name, &$closecount) {
|
|
$tnname = $this->_getCanonicalName($this->_topNode('name'));
|
|
if (!in_array($this->getCodeFlag($tnname, 'closetag', 'integer', BBCODE_CLOSETAG_IMPLICIT), array(
|
|
BBCODE_CLOSETAG_FORBIDDEN,
|
|
BBCODE_CLOSETAG_OPTIONAL
|
|
))) {
|
|
return false;
|
|
}
|
|
$node = $this->_findNamedNode($name, true);
|
|
if ($node === false) {
|
|
return false;
|
|
}
|
|
$scount = count($this->_stack);
|
|
if ($scount < 3) {
|
|
return false;
|
|
}
|
|
for($i = $scount - 2; $i > 0; $i--) {
|
|
$closecount++;
|
|
if ($this->_stack [$i]->equals($node)) {
|
|
return true;
|
|
}
|
|
if (in_array($this->_stack [$i]->getFlag('closetag', 'integer', BBCODE_CLOSETAG_IMPLICIT), array(
|
|
BBCODE_CLOSETAG_IMPLICIT_ON_CLOSE_ONLY,
|
|
BBCODE_CLOSETAG_MUSTEXIST
|
|
))) {
|
|
return false;
|
|
}
|
|
if ($this->_validateAgain) {
|
|
if (!$this->_stack [$i]->validate('validate_again')) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Abstract method: Close remaining blocks
|
|
*
|
|
* @access protected
|
|
*/
|
|
function _closeRemainingBlocks() {
|
|
// everything closed
|
|
if (count($this->_stack) == 1) {
|
|
return true;
|
|
}
|
|
// not everything close
|
|
if ($this->strict) {
|
|
return false;
|
|
}
|
|
while (count($this->_stack) > 1) {
|
|
if ($this->_topNode('getFlag', 'closetag', 'integer', BBCODE_CLOSETAG_IMPLICIT) == BBCODE_CLOSETAG_MUSTEXIST) {
|
|
return false; // sorry
|
|
}
|
|
$res = $this->_popNode();
|
|
if (!$res) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Find a node with a specific name in stack
|
|
*
|
|
* @access protected
|
|
* @return mixed
|
|
*/
|
|
function &_findNamedNode($name, $searchdeeper = false) {
|
|
$lname = $this->_getCanonicalName($name);
|
|
$case_sensitive = $this->_caseSensitive && $this->getCodeFlag($lname, 'case_sensitive', 'boolean', true);
|
|
if ($case_sensitive) {
|
|
$name = strtolower($name);
|
|
}
|
|
$scount = count($this->_stack);
|
|
if ($searchdeeper) {
|
|
$scount--;
|
|
}
|
|
for($i = $scount - 1; $i > 0; $i--) {
|
|
if (!$case_sensitive) {
|
|
$cmp_name = strtolower($this->_stack [$i]->name());
|
|
} else {
|
|
$cmp_name = $this->_stack [$i]->name();
|
|
}
|
|
if ($cmp_name == $lname) {
|
|
return $this->_stack [$i];
|
|
}
|
|
}
|
|
$result = false;
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Abstract method: Output tree
|
|
*
|
|
* @access protected
|
|
* @return bool
|
|
*/
|
|
function _outputTree() {
|
|
if ($this->_noOutput) {
|
|
return true;
|
|
}
|
|
$output = $this->_outputNode($this->_root);
|
|
if (is_string($output)) {
|
|
$this->_output = $this->_applyPostfilters($output);
|
|
unset($output);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Output a node
|
|
*
|
|
* @access protected
|
|
* @return bool
|
|
*/
|
|
function _outputNode(&$node) {
|
|
$output = '';
|
|
if ($node->_type == STRINGPARSER_BBCODE_NODE_PARAGRAPH || $node->_type == STRINGPARSER_BBCODE_NODE_ELEMENT || $node->_type == STRINGPARSER_NODE_ROOT) {
|
|
$ccount = count($node->_children);
|
|
for($i = 0; $i < $ccount; $i++) {
|
|
$suboutput = $this->_outputNode($node->_children [$i]);
|
|
if (!is_string($suboutput)) {
|
|
return false;
|
|
}
|
|
$output .= $suboutput;
|
|
}
|
|
if ($node->_type == STRINGPARSER_BBCODE_NODE_PARAGRAPH) {
|
|
return $this->_paragraphHandling ['start_tag'] . $output . $this->_paragraphHandling ['end_tag'];
|
|
}
|
|
if ($node->_type == STRINGPARSER_BBCODE_NODE_ELEMENT) {
|
|
return $node->getReplacement($output);
|
|
}
|
|
return $output;
|
|
} else if ($node->_type == STRINGPARSER_NODE_TEXT) {
|
|
$output = $node->content;
|
|
$before = '';
|
|
$after = '';
|
|
$ol = strlen($output);
|
|
switch ($node->getFlag('newlinemode.begin', 'integer', BBCODE_NEWLINE_PARSE)) {
|
|
case BBCODE_NEWLINE_IGNORE:
|
|
if ($ol && $output [0] == "\n") {
|
|
$before = "\n";
|
|
}
|
|
// don't break!
|
|
case BBCODE_NEWLINE_DROP:
|
|
if ($ol && $output [0] == "\n") {
|
|
$output = substr($output, 1);
|
|
$ol--;
|
|
}
|
|
break;
|
|
}
|
|
switch ($node->getFlag('newlinemode.end', 'integer', BBCODE_NEWLINE_PARSE)) {
|
|
case BBCODE_NEWLINE_IGNORE:
|
|
if ($ol && $output [$ol - 1] == "\n") {
|
|
$after = "\n";
|
|
}
|
|
// don't break!
|
|
case BBCODE_NEWLINE_DROP:
|
|
if ($ol && $output [$ol - 1] == "\n") {
|
|
$output = substr($output, 0, -1);
|
|
$ol--;
|
|
}
|
|
break;
|
|
}
|
|
// can't do anything
|
|
if ($node->_parent === null) {
|
|
return $before . $output . $after;
|
|
}
|
|
if ($node->_parent->_type == STRINGPARSER_BBCODE_NODE_PARAGRAPH) {
|
|
$parent = $node->_parent;
|
|
unset($node);
|
|
$node = $parent;
|
|
unset($parent);
|
|
// if no parent for this paragraph
|
|
if ($node->_parent === null) {
|
|
return $before . $output . $after;
|
|
}
|
|
}
|
|
if ($node->_parent->_type == STRINGPARSER_NODE_ROOT) {
|
|
return $before . $this->_applyParsers($this->_rootContentType, $output) . $after;
|
|
}
|
|
if ($node->_parent->_type == STRINGPARSER_BBCODE_NODE_ELEMENT) {
|
|
return $before . $this->_applyParsers($node->_parent->_codeInfo ['content_type'], $output) . $after;
|
|
}
|
|
return $before . $output . $after;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abstract method: Manipulate the tree
|
|
*
|
|
* @access protected
|
|
* @return bool
|
|
*/
|
|
function _modifyTree() {
|
|
// first pass: try to do newline handling
|
|
$nodes = $this->_root->getNodesByCriterium('needsTextNodeModification', true);
|
|
$nodes_count = count($nodes);
|
|
for($i = 0; $i < $nodes_count; $i++) {
|
|
$v = $nodes [$i]->getFlag('opentag.before.newline', 'integer', BBCODE_NEWLINE_PARSE);
|
|
if ($v != BBCODE_NEWLINE_PARSE) {
|
|
$n = $nodes [$i]->findPrevAdjentTextNode();
|
|
if (!is_null($n)) {
|
|
$n->setFlag('newlinemode.end', $v);
|
|
}
|
|
unset($n);
|
|
}
|
|
$v = $nodes [$i]->getFlag('opentag.after.newline', 'integer', BBCODE_NEWLINE_PARSE);
|
|
if ($v != BBCODE_NEWLINE_PARSE) {
|
|
$n = $nodes [$i]->firstChildIfText();
|
|
if (!is_null($n)) {
|
|
$n->setFlag('newlinemode.begin', $v);
|
|
}
|
|
unset($n);
|
|
}
|
|
$v = $nodes [$i]->getFlag('closetag.before.newline', 'integer', BBCODE_NEWLINE_PARSE);
|
|
if ($v != BBCODE_NEWLINE_PARSE) {
|
|
$n = $nodes [$i]->lastChildIfText();
|
|
if (!is_null($n)) {
|
|
$n->setFlag('newlinemode.end', $v);
|
|
}
|
|
unset($n);
|
|
}
|
|
$v = $nodes [$i]->getFlag('closetag.after.newline', 'integer', BBCODE_NEWLINE_PARSE);
|
|
if ($v != BBCODE_NEWLINE_PARSE) {
|
|
$n = $nodes [$i]->findNextAdjentTextNode();
|
|
if (!is_null($n)) {
|
|
$n->setFlag('newlinemode.begin', $v);
|
|
}
|
|
unset($n);
|
|
}
|
|
}
|
|
|
|
// second pass a: do paragraph handling on root element
|
|
if ($this->_rootParagraphHandling) {
|
|
$res = $this->_handleParagraphs($this->_root);
|
|
if (!$res) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// second pass b: do paragraph handling on other elements
|
|
unset($nodes);
|
|
$nodes = $this->_root->getNodesByCriterium('flag:paragraphs', true);
|
|
$nodes_count = count($nodes);
|
|
for($i = 0; $i < $nodes_count; $i++) {
|
|
$res = $this->_handleParagraphs($nodes [$i]);
|
|
if (!$res) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// second pass c: search for empty paragraph nodes and remove them
|
|
unset($nodes);
|
|
$nodes = $this->_root->getNodesByCriterium('empty', true);
|
|
$nodes_count = count($nodes);
|
|
if (isset($parent)) {
|
|
unset($parent);
|
|
$parent = null;
|
|
}
|
|
for($i = 0; $i < $nodes_count; $i++) {
|
|
if ($nodes [$i]->_type != STRINGPARSER_BBCODE_NODE_PARAGRAPH) {
|
|
continue;
|
|
}
|
|
unset($parent);
|
|
$parent = $nodes [$i]->_parent;
|
|
$parent->removeChild($nodes [$i], true);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Handle paragraphs
|
|
*
|
|
* @access protected
|
|
* @param object $node
|
|
* The node to handle
|
|
* @return bool
|
|
*/
|
|
function _handleParagraphs(&$node) {
|
|
// if this node is already a subnode of a paragraph node, do NOT
|
|
// do paragraph handling on this node!
|
|
if ($this->_hasParagraphAncestor($node)) {
|
|
return true;
|
|
}
|
|
$dest_nodes = array();
|
|
$last_node_was_paragraph = false;
|
|
$prevtype = STRINGPARSER_NODE_TEXT;
|
|
$paragraph = null;
|
|
while (count($node->_children)) {
|
|
$mynode = $node->_children [0];
|
|
$node->removeChild($mynode);
|
|
$subprevtype = $prevtype;
|
|
$sub_nodes = $this->_breakupNodeByParagraphs($mynode);
|
|
for($i = 0; $i < count($sub_nodes); $i++) {
|
|
if (!$last_node_was_paragraph || ($prevtype == $sub_nodes [$i]->_type && ($i != 0 || $prevtype != STRINGPARSER_BBCODE_NODE_ELEMENT))) {
|
|
unset($paragraph);
|
|
$paragraph = new StringParser_BBCode_Node_Paragraph();
|
|
}
|
|
$prevtype = $sub_nodes [$i]->_type;
|
|
if ($sub_nodes [$i]->_type != STRINGPARSER_BBCODE_NODE_ELEMENT || $sub_nodes [$i]->getFlag('paragraph_type', 'integer', BBCODE_PARAGRAPH_ALLOW_BREAKUP) != BBCODE_PARAGRAPH_BLOCK_ELEMENT) {
|
|
$paragraph->appendChild($sub_nodes [$i]);
|
|
$dest_nodes [] = $paragraph;
|
|
$last_node_was_paragraph = true;
|
|
} else {
|
|
$dest_nodes [] = $sub_nodes [$i];
|
|
$last_onde_was_paragraph = false;
|
|
unset($paragraph);
|
|
$paragraph = new StringParser_BBCode_Node_Paragraph();
|
|
}
|
|
}
|
|
}
|
|
$count = count($dest_nodes);
|
|
for($i = 0; $i < $count; $i++) {
|
|
$node->appendChild($dest_nodes [$i]);
|
|
}
|
|
unset($dest_nodes);
|
|
unset($paragraph);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Search for a paragraph node in tree in upward direction
|
|
*
|
|
* @access protected
|
|
* @param object $node
|
|
* The node to analyze
|
|
* @return bool
|
|
*/
|
|
function _hasParagraphAncestor(&$node) {
|
|
if ($node->_parent === null) {
|
|
return false;
|
|
}
|
|
$parent = $node->_parent;
|
|
if ($parent->_type == STRINGPARSER_BBCODE_NODE_PARAGRAPH) {
|
|
return true;
|
|
}
|
|
return $this->_hasParagraphAncestor($parent);
|
|
}
|
|
|
|
/**
|
|
* Break up nodes
|
|
*
|
|
* @access protected
|
|
* @param object $node
|
|
* The node to break up
|
|
* @return array
|
|
*/
|
|
function &_breakupNodeByParagraphs(&$node) {
|
|
$detect_string = $this->_paragraphHandling ['detect_string'];
|
|
$dest_nodes = array();
|
|
// text node => no problem
|
|
if ($node->_type == STRINGPARSER_NODE_TEXT) {
|
|
$cpos = 0;
|
|
while (($npos = strpos($node->content, $detect_string, $cpos)) !== false) {
|
|
$subnode = new StringParser_Node_Text(substr($node->content, $cpos, $npos - $cpos), $node->occurredAt + $cpos);
|
|
// copy flags
|
|
foreach ($node->_flags as $flag => $value) {
|
|
if ($flag == 'newlinemode.begin') {
|
|
if ($cpos == 0) {
|
|
$subnode->setFlag($flag, $value);
|
|
}
|
|
} else if ($flag == 'newlinemode.end') {
|
|
// do nothing
|
|
} else {
|
|
$subnode->setFlag($flag, $value);
|
|
}
|
|
}
|
|
$dest_nodes [] = $subnode;
|
|
unset($subnode);
|
|
$cpos = $npos + strlen($detect_string);
|
|
}
|
|
$subnode = new StringParser_Node_Text(substr($node->content, $cpos), $node->occurredAt + $cpos);
|
|
if ($cpos == 0) {
|
|
$value = $node->getFlag('newlinemode.begin', 'integer', null);
|
|
if ($value !== null) {
|
|
$subnode->setFlag('newlinemode.begin', $value);
|
|
}
|
|
}
|
|
$value = $node->getFlag('newlinemode.end', 'integer', null);
|
|
if ($value !== null) {
|
|
$subnode->setFlag('newlinemode.end', $value);
|
|
}
|
|
$dest_nodes [] = $subnode;
|
|
unset($subnode);
|
|
return $dest_nodes;
|
|
}
|
|
// not a text node or an element node => no way
|
|
if ($node->_type != STRINGPARSER_BBCODE_NODE_ELEMENT) {
|
|
$dest_nodes [] = $node;
|
|
return $dest_nodes;
|
|
}
|
|
if ($node->getFlag('paragraph_type', 'integer', BBCODE_PARAGRAPH_ALLOW_BREAKUP) != BBCODE_PARAGRAPH_ALLOW_BREAKUP || !count($node->_children)) {
|
|
$dest_nodes [] = $node;
|
|
return $dest_nodes;
|
|
}
|
|
$dest_node = $node->duplicate();
|
|
$nodecount = count($node->_children);
|
|
// now this node allows breakup - do it
|
|
for($i = 0; $i < $nodecount; $i++) {
|
|
$firstnode = $node->_children [0];
|
|
$node->removeChild($firstnode);
|
|
$sub_nodes = $this->_breakupNodeByParagraphs($firstnode);
|
|
for($j = 0; $j < count($sub_nodes); $j++) {
|
|
if ($j != 0) {
|
|
$dest_nodes [] = $dest_node;
|
|
unset($dest_node);
|
|
$dest_node = $node->duplicate();
|
|
}
|
|
$dest_node->appendChild($sub_nodes [$j]);
|
|
}
|
|
unset($sub_nodes);
|
|
}
|
|
$dest_nodes [] = $dest_node;
|
|
return $dest_nodes;
|
|
}
|
|
|
|
/**
|
|
* Is this node a usecontent node
|
|
*
|
|
* @access protected
|
|
* @param object $node
|
|
* The node to check
|
|
* @param bool $check_attrs
|
|
* Also check whether 'usecontent?'-attributes exist
|
|
* @return bool
|
|
*/
|
|
function _isUseContent(&$node, $check_attrs = false) {
|
|
$name = $this->_getCanonicalName($node->name());
|
|
// this should NOT happen
|
|
if ($name === false) {
|
|
return false;
|
|
}
|
|
if ($this->_codes [$name] ['callback_type'] == 'usecontent') {
|
|
return true;
|
|
}
|
|
$result = false;
|
|
if ($this->_codes [$name] ['callback_type'] == 'callback_replace?') {
|
|
$result = true;
|
|
} else if ($this->_codes [$name] ['callback_type'] != 'usecontent?') {
|
|
return false;
|
|
}
|
|
if ($check_attrs === false) {
|
|
return !$result;
|
|
}
|
|
$attributes = array_keys($this->_topNodeVar('_attributes'));
|
|
$p = @$this->_codes [$name] ['callback_params'] ['usecontent_param'];
|
|
if (is_array($p)) {
|
|
foreach ($p as $param) {
|
|
if (in_array($param, $attributes)) {
|
|
return $result;
|
|
}
|
|
}
|
|
} else {
|
|
if (in_array($p, $attributes)) {
|
|
return $result;
|
|
}
|
|
}
|
|
return !$result;
|
|
}
|
|
|
|
/**
|
|
* Get canonical name of a code
|
|
*
|
|
* @access protected
|
|
* @param string $name
|
|
* @return string
|
|
*/
|
|
function _getCanonicalName($name) {
|
|
if (isset($this->_codes [$name])) {
|
|
return $name;
|
|
}
|
|
$found = false;
|
|
// try to find the code in the code list
|
|
foreach (array_keys($this->_codes) as $rname) {
|
|
// match
|
|
if (strtolower($rname) == strtolower($name)) {
|
|
$found = $rname;
|
|
break;
|
|
}
|
|
}
|
|
if ($found === false || ($this->_caseSensitive && $this->getCodeFlag($found, 'case_sensitive', 'boolean', true))) {
|
|
return false;
|
|
}
|
|
return $rname;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Node type: BBCode Element node
|
|
*
|
|
* @see StringParser_BBCode_Node_Element::_type
|
|
*/
|
|
define('STRINGPARSER_BBCODE_NODE_ELEMENT', 32);
|
|
|
|
/**
|
|
* Node type: BBCode Paragraph node
|
|
*
|
|
* @see StringParser_BBCode_Node_Paragraph::_type
|
|
*/
|
|
define('STRINGPARSER_BBCODE_NODE_PARAGRAPH', 33);
|
|
|
|
/**
|
|
* BBCode String parser paragraph node class
|
|
*
|
|
* @package stringparser
|
|
*/
|
|
class StringParser_BBCode_Node_Paragraph extends StringParser_Node {
|
|
|
|
/**
|
|
* The type of this node.
|
|
*
|
|
* This node is a bbcode paragraph node.
|
|
*
|
|
* @access protected
|
|
* @var int
|
|
* @see STRINGPARSER_BBCODE_NODE_PARAGRAPH
|
|
*/
|
|
var $_type = STRINGPARSER_BBCODE_NODE_PARAGRAPH;
|
|
|
|
/**
|
|
* Determines whether a criterium matches this node
|
|
*
|
|
* @access public
|
|
* @param string $criterium
|
|
* The criterium that is to be checked
|
|
* @param mixed $value
|
|
* The value that is to be compared
|
|
* @return bool True if this node matches that criterium
|
|
*/
|
|
function matchesCriterium($criterium, $value) {
|
|
if ($criterium == 'empty') {
|
|
if (!count($this->_children)) {
|
|
return true;
|
|
}
|
|
if (count($this->_children) > 1) {
|
|
return false;
|
|
}
|
|
if ($this->_children [0]->_type != STRINGPARSER_NODE_TEXT) {
|
|
return false;
|
|
}
|
|
if (!strlen($this->_children [0]->content)) {
|
|
return true;
|
|
}
|
|
if (strlen($this->_children [0]->content) > 2) {
|
|
return false;
|
|
}
|
|
$f_begin = $this->_children [0]->getFlag('newlinemode.begin', 'integer', BBCODE_NEWLINE_PARSE);
|
|
$f_end = $this->_children [0]->getFlag('newlinemode.end', 'integer', BBCODE_NEWLINE_PARSE);
|
|
$content = $this->_children [0]->content;
|
|
if ($f_begin != BBCODE_NEWLINE_PARSE && $content [0] == "\n") {
|
|
$content = substr($content, 1);
|
|
}
|
|
if ($f_end != BBCODE_NEWLINE_PARSE && $content [strlen($content) - 1] == "\n") {
|
|
$content = substr($content, 0, -1);
|
|
}
|
|
if (!strlen($content)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* BBCode String parser element node class
|
|
*
|
|
* @package stringparser
|
|
*/
|
|
class StringParser_BBCode_Node_Element extends StringParser_Node {
|
|
|
|
/**
|
|
* The type of this node.
|
|
*
|
|
* This node is a bbcode element node.
|
|
*
|
|
* @access protected
|
|
* @var int
|
|
* @see STRINGPARSER_BBCODE_NODE_ELEMENT
|
|
*/
|
|
var $_type = STRINGPARSER_BBCODE_NODE_ELEMENT;
|
|
|
|
/**
|
|
* Element name
|
|
*
|
|
* @access protected
|
|
* @var string
|
|
* @see StringParser_BBCode_Node_Element::name
|
|
* @see StringParser_BBCode_Node_Element::setName
|
|
* @see StringParser_BBCode_Node_Element::appendToName
|
|
*/
|
|
var $_name = '';
|
|
|
|
/**
|
|
* Element flags
|
|
*
|
|
* @access protected
|
|
* @var array
|
|
*/
|
|
var $_flags = array();
|
|
|
|
/**
|
|
* Element attributes
|
|
*
|
|
* @access protected
|
|
* @var array
|
|
*/
|
|
var $_attributes = array();
|
|
|
|
/**
|
|
* Had a close tag
|
|
*
|
|
* @access protected
|
|
* @var bool
|
|
*/
|
|
var $_hadCloseTag = false;
|
|
|
|
/**
|
|
* Was processed by paragraph handling
|
|
*
|
|
* @access protected
|
|
* @var bool
|
|
*/
|
|
var $_paragraphHandled = false;
|
|
|
|
// ////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Duplicate this node (but without children / parents)
|
|
*
|
|
* @access public
|
|
* @return object
|
|
*/
|
|
function &duplicate() {
|
|
$newnode = new StringParser_BBCode_Node_Element($this->occurredAt);
|
|
$newnode->_name = $this->_name;
|
|
$newnode->_flags = $this->_flags;
|
|
$newnode->_attributes = $this->_attributes;
|
|
$newnode->_hadCloseTag = $this->_hadCloseTag;
|
|
$newnode->_paragraphHandled = $this->_paragraphHandled;
|
|
$newnode->_codeInfo = $this->_codeInfo;
|
|
return $newnode;
|
|
}
|
|
|
|
/**
|
|
* Retreive name of this element
|
|
*
|
|
* @access public
|
|
* @return string
|
|
*/
|
|
function name() {
|
|
return $this->_name;
|
|
}
|
|
|
|
/**
|
|
* Set name of this element
|
|
*
|
|
* @access public
|
|
* @param string $name
|
|
* The new name of the element
|
|
*/
|
|
function setName($name) {
|
|
$this->_name = $name;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Append to name of this element
|
|
*
|
|
* @access public
|
|
* @param string $chars
|
|
* The chars to append to the name of the element
|
|
*/
|
|
function appendToName($chars) {
|
|
$this->_name .= $chars;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Append to attribute of this element
|
|
*
|
|
* @access public
|
|
* @param string $name
|
|
* The name of the attribute
|
|
* @param string $chars
|
|
* The chars to append to the attribute of the element
|
|
*/
|
|
function appendToAttribute($name, $chars) {
|
|
if (!isset($this->_attributes [$name])) {
|
|
$this->_attributes [$name] = $chars;
|
|
return true;
|
|
}
|
|
$this->_attributes [$name] .= $chars;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set attribute
|
|
*
|
|
* @access public
|
|
* @param string $name
|
|
* The name of the attribute
|
|
* @param string $value
|
|
* The new value of the attribute
|
|
*/
|
|
function setAttribute($name, $value) {
|
|
$this->_attributes [$name] = $value;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set code info
|
|
*
|
|
* @access public
|
|
* @param array $info
|
|
* The code info array
|
|
*/
|
|
function setCodeInfo($info) {
|
|
$this->_codeInfo = $info;
|
|
$this->_flags = $info ['flags'];
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get attribute value
|
|
*
|
|
* @access public
|
|
* @param string $name
|
|
* The name of the attribute
|
|
*/
|
|
function attribute($name) {
|
|
if (!isset($this->_attributes [$name])) {
|
|
return null;
|
|
}
|
|
return $this->_attributes [$name];
|
|
}
|
|
|
|
/**
|
|
* Set flag that this element had a close tag
|
|
*
|
|
* @access public
|
|
*/
|
|
function setHadCloseTag() {
|
|
$this->_hadCloseTag = true;
|
|
}
|
|
|
|
/**
|
|
* Set flag that this element was already processed by paragraph handling
|
|
*
|
|
* @access public
|
|
*/
|
|
function setParagraphHandled() {
|
|
$this->_paragraphHandled = true;
|
|
}
|
|
|
|
/**
|
|
* Get flag if this element was already processed by paragraph handling
|
|
*
|
|
* @access public
|
|
* @return bool
|
|
*/
|
|
function paragraphHandled() {
|
|
return $this->_paragraphHandled;
|
|
}
|
|
|
|
/**
|
|
* Get flag if this element had a close tag
|
|
*
|
|
* @access public
|
|
* @return bool
|
|
*/
|
|
function hadCloseTag() {
|
|
return $this->_hadCloseTag;
|
|
}
|
|
|
|
/**
|
|
* Determines whether a criterium matches this node
|
|
*
|
|
* @access public
|
|
* @param string $criterium
|
|
* The criterium that is to be checked
|
|
* @param mixed $value
|
|
* The value that is to be compared
|
|
* @return bool True if this node matches that criterium
|
|
*/
|
|
function matchesCriterium($criterium, $value) {
|
|
if ($criterium == 'tagName') {
|
|
return ($value == $this->_name);
|
|
}
|
|
if ($criterium == 'needsTextNodeModification') {
|
|
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);
|
|
}
|
|
if (substr($criterium, 0, 5) == 'flag:') {
|
|
$criterium = substr($criterium, 5);
|
|
return ($this->getFlag($criterium) == $value);
|
|
}
|
|
if (substr($criterium, 0, 6) == '!flag:') {
|
|
$criterium = substr($criterium, 6);
|
|
return ($this->getFlag($criterium) != $value);
|
|
}
|
|
if (substr($criterium, 0, 6) == 'flag=:') {
|
|
$criterium = substr($criterium, 6);
|
|
return ($this->getFlag($criterium) === $value);
|
|
}
|
|
if (substr($criterium, 0, 7) == '!flag=:') {
|
|
$criterium = substr($criterium, 7);
|
|
return ($this->getFlag($criterium) !== $value);
|
|
}
|
|
return parent::matchesCriterium($criterium, $value);
|
|
}
|
|
|
|
/**
|
|
* Get first child if it is a text node
|
|
*
|
|
* @access public
|
|
* @return mixed
|
|
*/
|
|
function &firstChildIfText() {
|
|
$ret = $this->firstChild();
|
|
if (is_null($ret)) {
|
|
return $ret;
|
|
}
|
|
if ($ret->_type != STRINGPARSER_NODE_TEXT) {
|
|
// DON'T DO $ret = null WITHOUT unset BEFORE!
|
|
// ELSE WE WILL ERASE THE NODE ITSELF! EVIL!
|
|
unset($ret);
|
|
$ret = null;
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Get last child if it is a text node AND if this element had a close tag
|
|
*
|
|
* @access public
|
|
* @return mixed
|
|
*/
|
|
function &lastChildIfText() {
|
|
$ret = $this->lastChild();
|
|
if (is_null($ret)) {
|
|
return $ret;
|
|
}
|
|
if ($ret->_type != STRINGPARSER_NODE_TEXT || !$this->_hadCloseTag) {
|
|
// DON'T DO $ret = null WITHOUT unset BEFORE!
|
|
// ELSE WE WILL ERASE THE NODE ITSELF! EVIL!
|
|
if ($ret->_type != STRINGPARSER_NODE_TEXT && !$ret->hadCloseTag()) {
|
|
$ret2 = $ret->_findPrevAdjentTextNodeHelper();
|
|
unset($ret);
|
|
$ret = $ret2;
|
|
unset($ret2);
|
|
} else {
|
|
unset($ret);
|
|
$ret = null;
|
|
}
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Find next adjent text node after close tag
|
|
*
|
|
* returns the node or null if none exists
|
|
*
|
|
* @access public
|
|
* @return mixed
|
|
*/
|
|
function &findNextAdjentTextNode() {
|
|
$ret = null;
|
|
if (is_null($this->_parent)) {
|
|
return $ret;
|
|
}
|
|
if (!$this->_hadCloseTag) {
|
|
return $ret;
|
|
}
|
|
$ccount = count($this->_parent->_children);
|
|
$found = false;
|
|
for($i = 0; $i < $ccount; $i++) {
|
|
if ($this->_parent->_children [$i]->equals($this)) {
|
|
$found = $i;
|
|
break;
|
|
}
|
|
}
|
|
if ($found === false) {
|
|
return $ret;
|
|
}
|
|
if ($found < $ccount - 1) {
|
|
if ($this->_parent->_children [$found + 1]->_type == STRINGPARSER_NODE_TEXT) {
|
|
return $this->_parent->_children [$found + 1];
|
|
}
|
|
return $ret;
|
|
}
|
|
if ($this->_parent->_type == STRINGPARSER_BBCODE_NODE_ELEMENT && !$this->_parent->hadCloseTag()) {
|
|
$ret = $this->_parent->findNextAdjentTextNode();
|
|
return $ret;
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Find previous adjent text node before open tag
|
|
*
|
|
* returns the node or null if none exists
|
|
*
|
|
* @access public
|
|
* @return mixed
|
|
*/
|
|
function &findPrevAdjentTextNode() {
|
|
$ret = null;
|
|
if (is_null($this->_parent)) {
|
|
return $ret;
|
|
}
|
|
$ccount = count($this->_parent->_children);
|
|
$found = false;
|
|
for($i = 0; $i < $ccount; $i++) {
|
|
if ($this->_parent->_children [$i]->equals($this)) {
|
|
$found = $i;
|
|
break;
|
|
}
|
|
}
|
|
if ($found === false) {
|
|
return $ret;
|
|
}
|
|
if ($found > 0) {
|
|
if ($this->_parent->_children [$found - 1]->_type == STRINGPARSER_NODE_TEXT) {
|
|
return $this->_parent->_children [$found - 1];
|
|
}
|
|
if (!$this->_parent->_children [$found - 1]->hadCloseTag()) {
|
|
$ret = $this->_parent->_children [$found - 1]->_findPrevAdjentTextNodeHelper();
|
|
}
|
|
return $ret;
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Helper function for findPrevAdjentTextNode
|
|
*
|
|
* Looks at the last child node; if it's a text node, it returns it,
|
|
* if the element node did not have an open tag, it calls itself
|
|
* recursively.
|
|
*/
|
|
function &_findPrevAdjentTextNodeHelper() {
|
|
$lastnode = $this->lastChild();
|
|
if ($lastnode === null || $lastnode->_type == STRINGPARSER_NODE_TEXT) {
|
|
return $lastnode;
|
|
}
|
|
if (!$lastnode->hadCloseTag()) {
|
|
$ret = $lastnode->_findPrevAdjentTextNodeHelper();
|
|
} else {
|
|
$ret = null;
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Get Flag
|
|
*
|
|
* @access public
|
|
* @param string $flag
|
|
* The requested flag
|
|
* @param string $type
|
|
* The requested type of the return value
|
|
* @param mixed $default
|
|
* The default return value
|
|
* @return mixed
|
|
*/
|
|
function getFlag($flag, $type = 'mixed', $default = null) {
|
|
if (!isset($this->_flags [$flag])) {
|
|
return $default;
|
|
}
|
|
$return = $this->_flags [$flag];
|
|
if ($type != 'mixed') {
|
|
settype($return, $type);
|
|
}
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Set a flag
|
|
*
|
|
* @access public
|
|
* @param string $name
|
|
* The name of the flag
|
|
* @param mixed $value
|
|
* The value of the flag
|
|
*/
|
|
function setFlag($name, $value) {
|
|
$this->_flags [$name] = $value;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validate code
|
|
*
|
|
* @access public
|
|
* @param string $action
|
|
* The action which is to be called ('validate'
|
|
* for first validation, 'validate_again' for
|
|
* second validation (optional))
|
|
* @return bool
|
|
*/
|
|
function validate($action = 'validate') {
|
|
if ($action != 'validate' && $action != 'validate_again') {
|
|
return false;
|
|
}
|
|
if ($this->_codeInfo ['callback_type'] != 'simple_replace' && $this->_codeInfo ['callback_type'] != 'simple_replace_single') {
|
|
if (!is_callable($this->_codeInfo ['callback_func'])) {
|
|
return false;
|
|
}
|
|
|
|
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) {
|
|
// we have to make sure the object gets passed on as a reference
|
|
// if we do call_user_func(..., &$this) this will clash with PHP5
|
|
$callArray = array(
|
|
$action,
|
|
$this->_attributes,
|
|
$this->_children [0]->content,
|
|
$this->_codeInfo ['callback_params']
|
|
);
|
|
$callArray [] = $this;
|
|
$res = call_user_func_array($this->_codeInfo ['callback_func'], $callArray);
|
|
if ($res) {
|
|
// ok, now, if we've got a usecontent type, set a flag that
|
|
// this may not be broken up by paragraph handling!
|
|
// but PLEASE do NOT change if already set to any other setting
|
|
// than BBCODE_PARAGRAPH_ALLOW_BREAKUP because we could
|
|
// override e.g. BBCODE_PARAGRAPH_BLOCK_ELEMENT!
|
|
$val = $this->getFlag('paragraph_type', 'integer', BBCODE_PARAGRAPH_ALLOW_BREAKUP);
|
|
if ($val == BBCODE_PARAGRAPH_ALLOW_BREAKUP) {
|
|
$this->_flags ['paragraph_type'] = BBCODE_PARAGRAPH_ALLOW_INSIDE;
|
|
}
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
// we have to make sure the object gets passed on as a reference
|
|
// if we do call_user_func(..., &$this) this will clash with PHP5
|
|
$callArray = array(
|
|
$action,
|
|
$this->_attributes,
|
|
null,
|
|
$this->_codeInfo ['callback_params']
|
|
);
|
|
$callArray [] = $this;
|
|
return call_user_func_array($this->_codeInfo ['callback_func'], $callArray);
|
|
}
|
|
return (bool) (!count($this->_attributes));
|
|
}
|
|
|
|
/**
|
|
* Get replacement for this code
|
|
*
|
|
* @access public
|
|
* @param string $subcontent
|
|
* The content of all sub-nodes
|
|
* @return string
|
|
*/
|
|
function getReplacement($subcontent) {
|
|
if ($this->_codeInfo ['callback_type'] == 'simple_replace' || $this->_codeInfo ['callback_type'] == 'simple_replace_single') {
|
|
if ($this->_codeInfo ['callback_type'] == 'simple_replace_single') {
|
|
if (strlen($subcontent)) { // can't be!
|
|
return false;
|
|
}
|
|
return $this->_codeInfo ['callback_params'] ['start_tag'];
|
|
}
|
|
return $this->_codeInfo ['callback_params'] ['start_tag'] . $subcontent . $this->_codeInfo ['callback_params'] ['end_tag'];
|
|
}
|
|
// else usecontent, usecontent? or callback_replace or callback_replace_single
|
|
// => call function (the function is callable, determined in validate()!)
|
|
|
|
// we have to make sure the object gets passed on as a reference
|
|
// if we do call_user_func(..., &$this) this will clash with PHP5
|
|
$callArray = array(
|
|
'output',
|
|
$this->_attributes,
|
|
$subcontent,
|
|
$this->_codeInfo ['callback_params']
|
|
);
|
|
$callArray [] = $this;
|
|
return call_user_func_array($this->_codeInfo ['callback_func'], $callArray);
|
|
}
|
|
|
|
/**
|
|
* Dump this node to a string
|
|
*
|
|
* @access protected
|
|
* @return string
|
|
*/
|
|
function _dumpToString() {
|
|
$str = "bbcode \"" . substr(preg_replace('/\s+/', ' ', $this->_name), 0, 40) . "\"";
|
|
if (count($this->_attributes)) {
|
|
$attribs = array_keys($this->_attributes);
|
|
sort($attribs);
|
|
$str .= ' (';
|
|
$i = 0;
|
|
foreach ($attribs as $attrib) {
|
|
if ($i != 0) {
|
|
$str .= ', ';
|
|
}
|
|
$str .= $attrib . '="';
|
|
$str .= substr(preg_replace('/\s+/', ' ', $this->_attributes [$attrib]), 0, 10);
|
|
$str .= '"';
|
|
$i++;
|
|
}
|
|
$str .= ')';
|
|
}
|
|
return $str;
|
|
}
|
|
|
|
}
|
|
|
|
?>
|