'),
-
- new SBBCodeParser_BBCode('notag', '%content%', SBBCodeParser_BBCode::INLINE_TAG, false, array(), array('text_node'),
- SBBCodeParser_BBCode::AUTO_DETECT_EXCLUDE_ALL),
- new SBBCodeParser_BBCode('nobbc', '%content%', SBBCodeParser_BBCode::INLINE_TAG, false, array(), array('text_node'),
- SBBCodeParser_BBCode::AUTO_DETECT_EXCLUDE_ALL),
- new SBBCodeParser_BBCode('noparse', '%content%', SBBCodeParser_BBCode::INLINE_TAG, false, array(), array('text_node'),
- SBBCodeParser_BBCode::AUTO_DETECT_EXCLUDE_ALL),
-
- new SBBCodeParser_BBCode('h1', '
%content%
'),
- new SBBCodeParser_BBCode('h2', '
%content%
'),
- new SBBCodeParser_BBCode('h3', '
%content%
'),
- new SBBCodeParser_BBCode('h4', '
%content%
'),
- new SBBCodeParser_BBCode('h5', '
%content%
'),
- new SBBCodeParser_BBCode('h6', '
%content%
'),
- new SBBCodeParser_BBCode('h7', '%content%'),
-
- new SBBCodeParser_BBCode('big', '%content%'),
- new SBBCodeParser_BBCode('small', '%content%'),
- // tables use this tag so can't be a header.
- //new SBBCodeParser_BBCode('h', '
%content%
'),
-
- new SBBCodeParser_BBCode('br', ' ', SBBCodeParser_BBCode::INLINE_TAG, true),
- new SBBCodeParser_BBCode('sp', ' ', SBBCodeParser_BBCode::INLINE_TAG, true),
- new SBBCodeParser_BBCode('hr', '', SBBCodeParser_BBCode::INLINE_TAG, true),
-
- new SBBCodeParser_BBCode('anchor', function($content, $attribs, $node)
- {
- if(empty($attribs['default']))
- {
- $attribs['default'] = $content;
- // remove the content for [anchor]test[/anchor]
- // usage as test is the anchor
- $content = '';
- }
-
- $attribs['default'] = preg_replace('/[^a-zA-Z0-9_\-]+/', '', $attribs['default']);
-
- return "{$content}";
- }),
- new SBBCodeParser_BBCode('goto', function($content, $attribs, $node)
- {
- if(empty($attribs['default']))
- $attribs['default'] = $content;
-
- $attribs['default'] = preg_replace('/[^a-zA-Z0-9_\-#]+/', '', $attribs['default']);
-
- return "{$content}";
- }),
- new SBBCodeParser_BBCode('jumpto', function($content, $attribs, $node)
- {
- if(empty($attribs['default']))
- $attribs['default'] = $content;
-
- $attribs['default'] = preg_replace('/[^a-zA-Z0-9_\-#]+/', '', $attribs['default']);
-
- return "{$content}";
- }),
-
- new SBBCodeParser_BBCode('img', function($content, $attribs, $node)
- {
- $attrs = '';
-
- // for when default attrib is used for width x height
- if(isset($attribs['default']) && preg_match("/[0-9]+[Xx\*][0-9]+/", $attribs['default']))
- {
- list($attribs['width'],$attribs['height']) = explode('x', $attribs['default']);
- $attribs['default'] = '';
- }
- // for when width & height are specified as the default attrib
- else if(isset($attribs['default']) && is_numeric($attribs['default']))
- {
- $attribs['width'] = $attribs['height'] = $attribs['default'];
- $attribs['default'] = '';
- }
-
- // add alt tag if is one
- if(isset($attribs['default']) && !empty($attribs['default']))
- $attrs .= " alt=\"{$attribs['default']}\"";
- else if(isset($attribs['alt']))
- $attrs .= " alt=\"{$attribs['alt']}\"";
- else
- $attrs .= " alt=\"{$content}\"";
-
- // width and height can only be numeric, anything else should be ignored to prevent XSS
- if(isset($attribs['width']) && is_numeric($attribs['width']))
- $attrs .= " width=\"{$attribs['width']}\"";
- if(isset($attribs['height']) && is_numeric($attribs['height']))
- $attrs .= " height=\"{$attribs['height']}\"";
-
- // add http:// to www starting urls
- if(strpos($content, 'www') === 0)
- $content = 'http://' . $content;
- // add the base url to any urls not starting with http or ftp as they must be relative
- else if(substr($content, 0, 4) !== 'http'
- && substr($content, 0, 3) !== 'ftp')
- $content = $node->root()->get_base_uri() . $content;
-
- return "";
- }, SBBCodeParser_BBCode::INLINE_TAG, false, array(), array('text_node'), SBBCodeParser_BBCode::AUTO_DETECT_EXCLUDE_ALL),
- new SBBCodeParser_BBCode('email', function($content, $attribs, $node)
- {
- if(empty($attribs['default']))
- $attribs['default'] = $content;
-
-
- return "{$content}";
- }, SBBCodeParser_BBCode::INLINE_TAG, false, array(), array('text_node'), SBBCodeParser_BBCode::AUTO_DETECT_EXCLUDE_ALL),
- new SBBCodeParser_BBCode('url', function($content, $attribs, $node)
- {
- if(empty($attribs['default']))
- $attribs['default'] = $content;
-
- // add http:// to www starting urls
- if(strpos($attribs['default'], 'www') === 0)
- $attribs['default'] = 'http://' . $attribs['default'];
- // add the base url to any urls not starting with http or ftp as they must be relative
- else if(substr($attribs['default'], 0, 4) !== 'http'
- && substr($attribs['default'], 0, 3) !== 'ftp')
- $attribs['default'] = $node->root()->get_base_uri() . $attribs['default'];
-
- return "{$content}";
- }, SBBCodeParser_BBCode::INLINE_TAG, false, array(), array('text_node'), SBBCodeParser_BBCode::AUTO_DETECT_EXCLUDE_ALL)
- );
- }
-
- /**
- * Adds an emoticon to the parser.
- * @param string $key
- * @param string $url
- * @param bool $replace If to replace another emoticon with the same key
- * @return bool
- */
- public function add_emoticon($key, $url, $replace=true)
- {
- if(isset($this->emoticons[$key]) && !$replace)
- return false;
-
- $this->emoticons[$key] = $url;
- return true;
- }
-
- /**
- * Adds multipule emoticons to the parser. Should be in
- * emoticon_key => image_url format.
- * @param Array $emoticons
- * @param bool $replace If to replace another emoticon with the same key
- */
- public function add_emoticons(Array $emoticons, $replace=true)
- {
- foreach($emoticons as $key => $url)
- $this->add_emoticon($key, $url, $replace);
- }
-
- /**
- * Removes an emoticon from the parser.
- * @param string $key
- * @return bool
- */
- public function remove_emoticon($key, $url)
- {
- if(!isset($this->emoticons[$key]))
- return false;
-
- unset($this->emoticons[$key]);
- return true;
- }
-
- /**
- * Adds a bbcode to the parser
- * @param SBBCodeParser_BBCode $bbcode
- * @param bool $replace If to replace another bbcode which is for the same tag
- * @return bool
- */
- public function add_bbcode(SBBCodeParser_BBCode $bbcode, $replace=true)
- {
- if(!$replace && isset($this->bbcodes[$bbcode->tag()]))
- return false;
-
- $this->bbcodes[$bbcode->tag()] = $bbcode;
- return true;
- }
-
- /**
- * Adds an array of SBBCodeParser_BBCode's to the document
- * @param array $bbcodes
- * @param bool $replace
- * @see add_bbcode
- */
- public function add_bbcodes(Array $bbcodes, $replace=true)
- {
- foreach($bbcodes as $bbcode)
- $this->add_bbcode($bbcode, $replace);
- }
-
- /**
- * Removes a BBCode from the document
- * @param mixed $bbcode String tag name or SBBCodeParser_BBCode
- */
- public function remove_bbcode($bbcode)
- {
- if($bbcode instanceof SBBCodeParser_BBCode)
- $bbcode = $bbcode->tag();
-
- unset($this->bbcodes[$bbcode]);
- }
-
- /**
- * Gets an array of bbcode tags that will currently be processed
- * @return array
- */
- public function list_bbcodes()
- {
- return array_keys($this->bbcodes);
- }
-
- /**
- * Returns the SBBCodeParser_BBCode object for the passed
- * tag.
- * @param string $tag
- * @return SBBCodeParser_BBCode
- */
- public function get_bbcode($tag)
- {
- if(!isset($this->bbcodes[$tag]))
- return null;
-
- return $this->bbcodes[$tag];
- }
-
- /**
- * Gets the base URI to be used in links, images, ect.
- * @return string
- */
- public function get_base_uri()
- {
- if($this->base_uri != null)
- return $this->base_uri;
-
- return htmlentities(dirname($_SERVER['PHP_SELF']), ENT_QUOTES | ENT_IGNORE, "UTF-8") . '/';
- }
-
- /**
- * Sets the base URI to be used in links, images, ect.
- * @param string $uri
- */
- public function set_base_uri($uri)
- {
- $this->base_uri = $uri;
- }
-
- /**
- * Parses a BBCode string into the current document
- * @param string $str
- * @return SBBCodeParser_Document
- */
- public function parse($str)
- {
- $str = preg_replace('/[\r\n|\r]/', "\n", $str);
- $len = strlen($str);
- $tag_open = false;
- $tag_text = '';
- $tag = '';
-
- // set the document as the current tag.
- $this->current_tag = $this;
-
- for($i=0; $i<$len; ++$i)
- {
- if($str[$i] === '[')
- {
- if($tag_open)
- $tag_text .= '[' . $tag;
-
- $tag_open = true;
- $tag = '';
- }
- else if($str[$i] === ']' && $tag_open)
- {
- if($tag !== '')
- {
- $bits = preg_split('/([ =])/', trim($tag), 2, PREG_SPLIT_DELIM_CAPTURE);
- $tag_attrs = (isset($bits[2]) ? $bits[1] . $bits[2] : '');
- $tag_closing = ($bits[0][0] === '/');
- $tag_name = ($bits[0][0] === '/' ? substr($bits[0], 1) : $bits[0]);
-
- if(isset($this->bbcodes[$tag_name]))
- {
- $this->tag_text($tag_text);
- $tag_text = '';
-
- if($tag_closing)
- {
- if(!$this->tag_close($tag_name))
- $tag_text = "[{$tag}]";
- }
- else
- {
- if(!$this->tag_open($tag_name, $this->parse_attribs($tag_attrs)))
- $tag_text = "[{$tag}]";
- }
- }
- else
- $tag_text .= "[{$tag}]";
- }
- else
- $tag_text .= '[]';
-
- $tag_open = false;
- $tag = '';
- }
- else if($tag_open)
- $tag .= $str[$i];
- else
- $tag_text .= $str[$i];
- }
-
- $this->tag_text($tag_text);
-
- if($this->throw_errors && !$this->current_tag instanceof SBBCodeParser_Document)
- throw new SBBCodeParser_MissingEndTagException("Missing closing tag for tag [{$this->current_tag->tag()}]");
-
- return $this;
- }
-
- /**
- * Handles a BBCode opening tag
- * @param string $tag
- * @param array $attrs
- * @return bool
- */
- private function tag_open($tag, $attrs)
- {
- if($this->current_tag instanceof SBBCodeParser_TagNode)
- {
- $closing_tags = $this->bbcodes[$this->current_tag->tag()]->closing_tags();
-
- if(in_array($tag, $closing_tags))
- $this->tag_close($this->current_tag->tag());
- }
-
- if($this->current_tag instanceof SBBCodeParser_TagNode)
- {
- $accepted_children = $this->bbcodes[$this->current_tag->tag()]->accepted_children();
-
- if(!empty($accepted_children) && !in_array($tag, $accepted_children))
- return false;
-
- if($this->throw_errors && !$this->bbcodes[$tag]->is_inline()
- && $this->bbcodes[$this->current_tag->tag()]->is_inline())
- throw new SBBCodeParser_InvalidNestingException("Block level tag [{$tag}] was opened within an inline tag [{$this->current_tag->tag()}]");
- }
-
- $node = new SBBCodeParser_TagNode($tag, $attrs);
- $this->current_tag->add_child($node);
-
- if(!$this->bbcodes[$tag]->is_self_closing())
- $this->current_tag = $node;
-
- return true;
- }
-
- /**
- * Handles tag text
- * @param string $text
- * @return void
- */
- private function tag_text($text)
- {
- if($this->current_tag instanceof SBBCodeParser_TagNode)
- {
- $accepted_children = $this->bbcodes[$this->current_tag->tag()]->accepted_children();
-
- if(!empty($accepted_children) && !in_array('text_node', $accepted_children))
- return;
- }
-
- $this->current_tag->add_child(new SBBCodeParser_TextNode($text));
- }
-
- /**
- * Handles BBCode closing tag
- * @param string $tag
- * @return bool
- */
- private function tag_close($tag)
- {
- if(!$this->current_tag instanceof SBBCodeParser_Document
- && $tag !== $this->current_tag->tag())
- {
- $closing_tags = $this->bbcodes[$this->current_tag->tag()]->closing_tags();
-
- if(in_array($tag, $closing_tags) || in_array('/' . $tag, $closing_tags))
- $this->current_tag = $this->current_tag->parent();
- }
-
- if($this->current_tag instanceof SBBCodeParser_Document)
- return false;
- else if($tag !== $this->current_tag->tag())
- {
- // check if this is a tag inside another tag like
- // [tag1] [tag2] [/tag1] [/tag2]
- $node = $this->current_tag->find_parent_by_tag($tag);
-
- if($node !== null)
- {
- $this->current_tag = $node->parent();
-
- while(($node = $node->last_tag_node()) !== null)
- {
- $new_node = new SBBCodeParser_TagNode($node->tag(), $node->attributes());
- $this->current_tag->add_child($new_node);
- $this->current_tag = $new_node;
- }
- }
- else
- return false;
- }
- else
- $this->current_tag = $this->current_tag->parent();
-
- return true;
- }
-
- /**
- * Parses a bbcode attribute string into an array
- * @param string $attribs
- * @return array
- */
- private function parse_attribs($attribs)
- {
- $ret = array('default' => null);
- $attribs = trim($attribs);
-
- if($attribs == '')
- return $ret;
-
- // if this tag only has one = then there is only one attribute
- // so add it all to default
- if($attribs[0] == '=' && strrpos($attribs, '=') === 0)
- $ret['default'] = htmlentities(substr($attribs, 1), ENT_QUOTES | ENT_IGNORE, "UTF-8");
- else
- {
- preg_match_all('/(\S+)=((?:(?:(["\'])(?:\\\3|[^\3])*?\3))|(?:[^\'"\s]+))/',
- $attribs,
- $matches,
- PREG_SET_ORDER);
-
- foreach($matches as $match)
- $ret[$match[1]] = htmlentities($match[2], ENT_QUOTES | ENT_IGNORE, "UTF-8");
- }
-
- return $ret;
- }
-
- private function loop_text_nodes($func, array $exclude=array(), SBBCodeParser_ContainerNode $node=null)
- {
- if($node === null)
- $node = $this;
-
- foreach($node->children() as $child)
- {
- if($child instanceof SBBCodeParser_TagNode)
- {
- if(!in_array($child->tag(), $exclude))
- $this->loop_text_nodes($func, $exclude, $child);
- }
- else if($child instanceof SBBCodeParser_TextNode)
- {
- $func($child);
- }
- }
- }
-
- /**
- * Detects any none clickable links and makes them clickable
- * @return SBBCodeParserDdocument
- */
- public function detect_links()
- {
- $this->loop_text_nodes(function($child) {
- preg_match_all("/(?:(?:https?|ftp):\/\/|(?:www|ftp)\.)(?:[a-zA-Z0-9\-\.]{1,255}\.[a-zA-Z]{1,20})(?::[0-9]{1,5})?(?:\/[^\s'\"]*)?(?:(?get_text(),
- $matches,
- PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE);
-
- if(count($matches[0]) == 0)
- return;
-
- $replacment = array();
- $last_pos = 0;
-
- foreach($matches[0] as $match)
- {
- if(substr($match[0], 0, 3) === 'ftp' && $match[0][3] !== ':')
- $url = 'ftp://' . $match[0];
- else if($match[0][0] === 'w')
- $url = 'http://' . $match[0];
- else
- $url = $match[0];
-
- $url = new SBBCodeParser_TagNode('url', array('default' => htmlentities($url, ENT_QUOTES | ENT_IGNORE, "UTF-8")));
- $url_text = new SBBCodeParser_TextNode($match[0]);
- $url->add_child($url_text);
-
- $replacment[] = new SBBCodeParser_TextNode(substr($child->get_text(), $last_pos, $match[1] - $last_pos));
- $replacment[] = $url;
- $last_pos = $match[1] + strlen($match[0]);
- }
-
- $replacment[] = new SBBCodeParser_TextNode(substr($child->get_text(), $last_pos));
- $child->parent()->replace_child($child, $replacment);
- }, $this->get_excluded_tags(SBBCodeParser_BBCode::AUTO_DETECT_EXCLUDE_URL));
-
- return $this;
- }
-
- /**
- * Detects any none clickable emails and makes them clickable
- * @return SBBCodeParserDdocument
- */
- public function detect_emails()
- {
- $this->loop_text_nodes(function($child) {
- preg_match_all("/(?:[a-zA-Z0-9\-\._]){1,}@(?:[a-zA-Z0-9\-\.]{1,255}\.[a-zA-Z]{1,20})/",
- $child->get_text(),
- $matches,
- PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE);
-
- if(count($matches[0]) == 0)
- return;
-
- $replacment = array();
- $last_pos = 0;
-
- foreach($matches[0] as $match)
- {
- $url = new SBBCodeParser_TagNode('email', array());
- $url_text = new SBBCodeParser_TextNode($match[0]);
- $url->add_child($url_text);
-
- $replacment[] = new SBBCodeParser_TextNode(substr($child->get_text(), $last_pos, $match[1] - $last_pos));
- $replacment[] = $url;
- $last_pos = $match[1] + strlen($match[0]);
- }
-
- $replacment[] = new SBBCodeParser_TextNode(substr($child->get_text(), $last_pos));
- $child->parent()->replace_child($child, $replacment);
- }, $this->get_excluded_tags(SBBCodeParser_BBCode::AUTO_DETECT_EXCLUDE_EMAIL));
-
- return $this;
- }
-
- /**
- * Detects any emoticons and replaces them with their images
- * @return SBBCodeParserDdocument
- */
- public function detect_emoticons()
- {
- $pattern = '';
- foreach($this->emoticons as $key => $url)
- $pattern .= ($pattern === ''? '/(?:':'|') . preg_quote($key, '/');
- $pattern .= ')/';
-
- $emoticons = $this->emoticons;
-
- $this->loop_text_nodes(function($child) use ($pattern, $emoticons) {
- preg_match_all($pattern,
- $child->get_text(),
- $matches,
- PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE);
-
- if(count($matches[0]) == 0)
- return;
-
- $replacment = array();
- $last_pos = 0;
-
- foreach($matches[0] as $match)
- {
- $url = new SBBCodeParser_TagNode('img', array('alt'=>$match[0]));
- $url_text = new SBBCodeParser_TextNode($emoticons[$match[0]]);
- $url->add_child($url_text);
-
- $replacment[] = new SBBCodeParser_TextNode(substr($child->get_text(), $last_pos, $match[1] - $last_pos));
- $replacment[] = $url;
- $last_pos = $match[1] + strlen($match[0]);
- }
-
- $replacment[] = new SBBCodeParser_TextNode(substr($child->get_text(), $last_pos));
- $child->parent()->replace_child($child, $replacment);
- }, $this->get_excluded_tags(SBBCodeParser_BBCode::AUTO_DETECT_EXCLUDE_EMOTICON));
-
- return $this;
- }
-
- /**
- * Gets an array of tages to be excluded from
- * the elcude param
- * @param int $exclude What to gets excluded from i.e. SBBCodeParser_BBCode::AUTO_DETECT_EXCLUDE_EMOTICON
- * @return array
- */
- private function get_excluded_tags($exclude)
- {
- $ret = array();
-
- foreach($this->bbcodes as $bbcode)
- if($bbcode->auto_detect_exclude() & $exclude)
- $ret[] = $bbcode->tag();
-
- return $ret;
- }
-}
+
+require('classes/Exception.php');
+require('classes/Exception/MissingEndTag.php');
+require('classes/Exception/InvalidNesting.php');
+
+require('classes/Node.php');
+require('classes/Node/Text.php');
+require('classes/Node/Container.php');
+require('classes/Node/Container/Tag.php');
+require('classes/Node/Container/Document.php');
+
+require('classes/BBCode.php');
diff --git a/classes/BBCode.php b/classes/BBCode.php
new file mode 100644
index 0000000..acedcd5
--- /dev/null
+++ b/classes/BBCode.php
@@ -0,0 +1,146 @@
+tag = $tag;
+ $this->is_inline = $is_inline;
+ $this->handler = $handler;
+ $this->is_self_closing = $is_self_closing;
+ $this->closing_tags = $closing_tags;
+ $this->accepted_children = $accepted_children;
+ $this->auto_detect_exclude = $auto_detect_exclude;
+ }
+
+ /**
+ * Gets the tag name this BBCode is for
+ * @return string
+ */
+ public function tag()
+ {
+ return $this->tag;
+ }
+
+ /**
+ * Gets if this BBCode is inline or if it's block
+ * @return bool
+ */
+ public function is_inline()
+ {
+ return $this->is_inline;
+ }
+
+ /**
+ * Gets if this BBCode is self closing
+ * @return bool
+ */
+ public function is_self_closing()
+ {
+ return $this->is_self_closing;
+ }
+
+ /**
+ * Gets the format string/handler for this BBCode
+ * @return mixed String or function
+ */
+ public function handler()
+ {
+ return $this->handler;
+ }
+
+ /**
+ * Gets an array of tags which will cause this tag to be closed
+ * @return array
+ */
+ public function closing_tags()
+ {
+ return $this->closing_tags;
+ }
+
+ /**
+ * Gets an array of tags which are allowed as children of this tag
+ * @return array
+ */
+ public function accepted_children()
+ {
+ return $this->accepted_children;
+ }
+
+ /**
+ * Which auto detections this BBCode should be excluded from
+ * @return int
+ */
+ public function auto_detect_exclude()
+ {
+ return $this->auto_detect_exclude;
+ }
+}
\ No newline at end of file
diff --git a/classes/Exception.php b/classes/Exception.php
new file mode 100644
index 0000000..74d2ade
--- /dev/null
+++ b/classes/Exception.php
@@ -0,0 +1,7 @@
+parent = $parent;
+
+ if($parent instanceof Node_Container_Document)
+ $this->root = $parent;
+ else
+ $this->root = $parent->root();
+ }
+
+ /**
+ * Gets the nodes parent. Returns null if there
+ * is no parent
+ * @return Node
+ */
+ public function parent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * @return string
+ */
+ public function get_html()
+ {
+ return null;
+ }
+
+ /**
+ * Gets the nodes root node
+ * @return Node
+ */
+ public function root()
+ {
+ return $this->root;
+ }
+
+ /**
+ * Finds a parent node of the passed type.
+ * Returns null if none found.
+ * @param string $tag
+ * @return Node_Container_Tag
+ */
+ public function find_parent_by_tag($tag)
+ {
+ $node = $this->parent();
+
+ while($this->parent() != null
+ && !$node instanceof Node_Container_Document)
+ {
+ if($node->tag() === $tag)
+ return $node;
+
+ $node = $node->parent();
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/classes/Node/Container.php b/classes/Node/Container.php
new file mode 100644
index 0000000..e2ffa11
--- /dev/null
+++ b/classes/Node/Container.php
@@ -0,0 +1,129 @@
+children[] = $child;
+ $child->set_parent($this);
+ }
+
+ /**
+ * Replaces a child node
+ * @param Node $what
+ * @param mixed $with Node or an array of Node
+ * @return bool
+ */
+ public function replace_child(Node $what, $with)
+ {
+ $replace_key = array_search($what, $this->children);
+
+ if($replace_key === false)
+ return false;
+
+ if(is_array($with))
+ foreach($with as $child)
+ $child->set_parent($this);
+
+ array_splice($this->children, $replace_key, 1, $with);
+
+ return true;
+ }
+
+ /**
+ * Removes a child fromthe node
+ * @param Node $child
+ * @return bool
+ */
+ public function remove_child(Node $child)
+ {
+ $key = array_search($what, $this->children);
+
+ if($key === false)
+ return false;
+
+ $this->children[$key]->set_parent();
+ unset($this->children[$key]);
+ return true;
+ }
+
+ /**
+ * Gets the nodes children
+ * @return array
+ */
+ public function children()
+ {
+ return $this->children;
+ }
+
+ /**
+ * Gets the last child of type Node_Container_Tag.
+ * @return Node_Container_Tag
+ */
+ public function last_tag_node()
+ {
+ $children_len = count($this->children);
+
+ for($i=$children_len-1; $i >= 0; $i--)
+ if($this->children[$i] instanceof Node_Container_Tag)
+ return $this->children[$i];
+
+ return null;
+ }
+
+ /**
+ * Gets a HTML representation of this node
+ * @return string
+ */
+ public function get_html($nl2br=true)
+ {
+ $html = '';
+
+ foreach($this->children as $child)
+ $html .= $child->get_html($nl2br);
+
+ if($this instanceof Node_Container_Document)
+ return $html;
+
+ $bbcode = $this->root()->get_bbcode($this->tag);
+
+ if(is_callable($bbcode->handler()) && ($func = $bbcode->handler()) !== false)
+ return $func($html, $this->attribs, $this);
+ //return call_user_func($bbcode->handler(), $html, $this->attribs, $this);
+
+ return str_replace('%content%', $html, $bbcode->handler());
+ }
+
+ /**
+ * Gets the raw text content of this node
+ * and it's children.
+ *
+ * The returned text is UNSAFE and should not
+ * be used without filtering!
+ * @return string
+ */
+ public function get_text()
+ {
+ $text = '';
+
+ foreach($this->children as $child)
+ $text .= $child->get_text();
+
+ return $text;
+ }
+}
\ No newline at end of file
diff --git a/classes/Node/Container/Document.php b/classes/Node/Container/Document.php
new file mode 100644
index 0000000..7e51349
--- /dev/null
+++ b/classes/Node/Container/Document.php
@@ -0,0 +1,1051 @@
+ smiley_url format
+ * @var array
+ */
+ protected $emoticons = array();
+
+ /*
+ * If to throw errors when encountering bad BBCode or
+ * to silently try and fix
+ * @var bool
+ */
+ protected $throw_errors = false;
+
+
+ public function __construct($load_defaults=true, $throw_errors=true)
+ {
+ $this->throw_errors = $throw_errors;
+
+ // Load default BBCodes
+ if($load_defaults)
+ $this->add_bbcodes($this->default_bbcodes());
+ }
+
+ /**
+ * Gets an array of the default BBCodes
+ * @return array
+ */
+ public static function default_bbcodes()
+ {
+ return array(
+ new BBCode('b', '%content%'),
+ new BBCode('i', '%content%'),
+ new BBCode('strong', '%content%'),
+ new BBCode('em', '%content%'),
+ new BBCode('u', '%content%'),
+ new BBCode('s', '%content%'),
+ new BBCode('blink', '%content%'),
+ new BBCode('sub', '%content%'),
+ new BBCode('sup', '%content%'),
+ new BBCode('ins', '%content%'),
+ new BBCode('del', '%content%'),
+
+ new BBCode('right', '
'),
+
+ new BBCode('notag', '%content%', BBCode::INLINE_TAG, false, array(), array('text_node'),
+ BBCode::AUTO_DETECT_EXCLUDE_ALL),
+ new BBCode('nobbc', '%content%', BBCode::INLINE_TAG, false, array(), array('text_node'),
+ BBCode::AUTO_DETECT_EXCLUDE_ALL),
+ new BBCode('noparse', '%content%', BBCode::INLINE_TAG, false, array(), array('text_node'),
+ BBCode::AUTO_DETECT_EXCLUDE_ALL),
+
+ new BBCode('h1', '
%content%
'),
+ new BBCode('h2', '
%content%
'),
+ new BBCode('h3', '
%content%
'),
+ new BBCode('h4', '
%content%
'),
+ new BBCode('h5', '
%content%
'),
+ new BBCode('h6', '
%content%
'),
+ new BBCode('h7', '%content%'),
+
+ new BBCode('big', '%content%'),
+ new BBCode('small', '%content%'),
+ // tables use this tag so can't be a header.
+ //new BBCode('h', '
%content%
'),
+
+ new BBCode('br', ' ', BBCode::INLINE_TAG, true),
+ new BBCode('sp', ' ', BBCode::INLINE_TAG, true),
+ new BBCode('hr', '', BBCode::INLINE_TAG, true),
+
+ new BBCode('anchor', function($content, $attribs, $node)
+ {
+ if(empty($attribs['default']))
+ {
+ $attribs['default'] = $content;
+ // remove the content for [anchor]test[/anchor]
+ // usage as test is the anchor
+ $content = '';
+ }
+
+ $attribs['default'] = preg_replace('/[^a-zA-Z0-9_\-]+/', '', $attribs['default']);
+
+ return "{$content}";
+ }),
+ new BBCode('goto', function($content, $attribs, $node)
+ {
+ if(empty($attribs['default']))
+ $attribs['default'] = $content;
+
+ $attribs['default'] = preg_replace('/[^a-zA-Z0-9_\-#]+/', '', $attribs['default']);
+
+ return "{$content}";
+ }),
+ new BBCode('jumpto', function($content, $attribs, $node)
+ {
+ if(empty($attribs['default']))
+ $attribs['default'] = $content;
+
+ $attribs['default'] = preg_replace('/[^a-zA-Z0-9_\-#]+/', '', $attribs['default']);
+
+ return "{$content}";
+ }),
+
+ new BBCode('img', function($content, $attribs, $node)
+ {
+ $attrs = '';
+
+ // for when default attrib is used for width x height
+ if(isset($attribs['default']) && preg_match("/[0-9]+[Xx\*][0-9]+/", $attribs['default']))
+ {
+ list($attribs['width'],$attribs['height']) = explode('x', $attribs['default']);
+ $attribs['default'] = '';
+ }
+ // for when width & height are specified as the default attrib
+ else if(isset($attribs['default']) && is_numeric($attribs['default']))
+ {
+ $attribs['width'] = $attribs['height'] = $attribs['default'];
+ $attribs['default'] = '';
+ }
+
+ // add alt tag if is one
+ if(isset($attribs['default']) && !empty($attribs['default']))
+ $attrs .= " alt=\"{$attribs['default']}\"";
+ else if(isset($attribs['alt']))
+ $attrs .= " alt=\"{$attribs['alt']}\"";
+ else
+ $attrs .= " alt=\"{$content}\"";
+
+ // width and height can only be numeric, anything else should be ignored to prevent XSS
+ if(isset($attribs['width']) && is_numeric($attribs['width']))
+ $attrs .= " width=\"{$attribs['width']}\"";
+ if(isset($attribs['height']) && is_numeric($attribs['height']))
+ $attrs .= " height=\"{$attribs['height']}\"";
+
+ // add http:// to www starting urls
+ if(strpos($content, 'www') === 0)
+ $content = 'http://' . $content;
+ // add the base url to any urls not starting with http or ftp as they must be relative
+ else if(substr($content, 0, 4) !== 'http'
+ && substr($content, 0, 3) !== 'ftp')
+ $content = $node->root()->get_base_uri() . $content;
+
+ return "";
+ }, BBCode::INLINE_TAG, false, array(), array('text_node'), BBCode::AUTO_DETECT_EXCLUDE_ALL),
+ new BBCode('email', function($content, $attribs, $node)
+ {
+ if(empty($attribs['default']))
+ $attribs['default'] = $content;
+
+
+ return "{$content}";
+ }, BBCode::INLINE_TAG, false, array(), array('text_node'), BBCode::AUTO_DETECT_EXCLUDE_ALL),
+ new BBCode('url', function($content, $attribs, $node)
+ {
+ if(empty($attribs['default']))
+ $attribs['default'] = $content;
+
+ // add http:// to www starting urls
+ if(strpos($attribs['default'], 'www') === 0)
+ $attribs['default'] = 'http://' . $attribs['default'];
+ // add the base url to any urls not starting with http or ftp as they must be relative
+ else if(substr($attribs['default'], 0, 4) !== 'http'
+ && substr($attribs['default'], 0, 3) !== 'ftp')
+ $attribs['default'] = $node->root()->get_base_uri() . $attribs['default'];
+
+ return "{$content}";
+ }, BBCode::INLINE_TAG, false, array(), array('text_node'), BBCode::AUTO_DETECT_EXCLUDE_ALL)
+ );
+ }
+
+ /**
+ * Adds an emoticon to the parser.
+ * @param string $key
+ * @param string $url
+ * @param bool $replace If to replace another emoticon with the same key
+ * @return bool
+ */
+ public function add_emoticon($key, $url, $replace=true)
+ {
+ if(isset($this->emoticons[$key]) && !$replace)
+ return false;
+
+ $this->emoticons[$key] = $url;
+ return true;
+ }
+
+ /**
+ * Adds multipule emoticons to the parser. Should be in
+ * emoticon_key => image_url format.
+ * @param Array $emoticons
+ * @param bool $replace If to replace another emoticon with the same key
+ */
+ public function add_emoticons(Array $emoticons, $replace=true)
+ {
+ foreach($emoticons as $key => $url)
+ $this->add_emoticon($key, $url, $replace);
+ }
+
+ /**
+ * Removes an emoticon from the parser.
+ * @param string $key
+ * @return bool
+ */
+ public function remove_emoticon($key, $url)
+ {
+ if(!isset($this->emoticons[$key]))
+ return false;
+
+ unset($this->emoticons[$key]);
+ return true;
+ }
+
+ /**
+ * Adds a bbcode to the parser
+ * @param BBCode $bbcode
+ * @param bool $replace If to replace another bbcode which is for the same tag
+ * @return bool
+ */
+ public function add_bbcode(BBCode $bbcode, $replace=true)
+ {
+ if(!$replace && isset($this->bbcodes[$bbcode->tag()]))
+ return false;
+
+ $this->bbcodes[$bbcode->tag()] = $bbcode;
+ return true;
+ }
+
+ /**
+ * Adds an array of BBCode's to the document
+ * @param array $bbcodes
+ * @param bool $replace
+ * @see add_bbcode
+ */
+ public function add_bbcodes(Array $bbcodes, $replace=true)
+ {
+ foreach($bbcodes as $bbcode)
+ $this->add_bbcode($bbcode, $replace);
+ }
+
+ /**
+ * Removes a BBCode from the document
+ * @param mixed $bbcode String tag name or BBCode
+ */
+ public function remove_bbcode($bbcode)
+ {
+ if($bbcode instanceof BBCode)
+ $bbcode = $bbcode->tag();
+
+ unset($this->bbcodes[$bbcode]);
+ }
+
+ /**
+ * Gets an array of bbcode tags that will currently be processed
+ * @return array
+ */
+ public function list_bbcodes()
+ {
+ return array_keys($this->bbcodes);
+ }
+
+ /**
+ * Returns the BBCode object for the passed
+ * tag.
+ * @param string $tag
+ * @return BBCode
+ */
+ public function get_bbcode($tag)
+ {
+ if(!isset($this->bbcodes[$tag]))
+ return null;
+
+ return $this->bbcodes[$tag];
+ }
+
+ /**
+ * Gets the base URI to be used in links, images, ect.
+ * @return string
+ */
+ public function get_base_uri()
+ {
+ if($this->base_uri != null)
+ return $this->base_uri;
+
+ return htmlentities(dirname($_SERVER['PHP_SELF']), ENT_QUOTES | ENT_IGNORE, "UTF-8") . '/';
+ }
+
+ /**
+ * Sets the base URI to be used in links, images, ect.
+ * @param string $uri
+ */
+ public function set_base_uri($uri)
+ {
+ $this->base_uri = $uri;
+ }
+
+ /**
+ * Parses a BBCode string into the current document
+ * @param string $str
+ * @return Node_Container_Document
+ */
+ public function parse($str)
+ {
+ $str = preg_replace('/[\r\n|\r]/', "\n", $str);
+ $len = strlen($str);
+ $tag_open = false;
+ $tag_text = '';
+ $tag = '';
+
+ // set the document as the current tag.
+ $this->current_tag = $this;
+
+ for($i=0; $i<$len; ++$i)
+ {
+ if($str[$i] === '[')
+ {
+ if($tag_open)
+ $tag_text .= '[' . $tag;
+
+ $tag_open = true;
+ $tag = '';
+ }
+ else if($str[$i] === ']' && $tag_open)
+ {
+ if($tag !== '')
+ {
+ $bits = preg_split('/([ =])/', trim($tag), 2, PREG_SPLIT_DELIM_CAPTURE);
+ $tag_attrs = (isset($bits[2]) ? $bits[1] . $bits[2] : '');
+ $tag_closing = ($bits[0][0] === '/');
+ $tag_name = ($bits[0][0] === '/' ? substr($bits[0], 1) : $bits[0]);
+
+ if(isset($this->bbcodes[$tag_name]))
+ {
+ $this->tag_text($tag_text);
+ $tag_text = '';
+
+ if($tag_closing)
+ {
+ if(!$this->tag_close($tag_name))
+ $tag_text = "[{$tag}]";
+ }
+ else
+ {
+ if(!$this->tag_open($tag_name, $this->parse_attribs($tag_attrs)))
+ $tag_text = "[{$tag}]";
+ }
+ }
+ else
+ $tag_text .= "[{$tag}]";
+ }
+ else
+ $tag_text .= '[]';
+
+ $tag_open = false;
+ $tag = '';
+ }
+ else if($tag_open)
+ $tag .= $str[$i];
+ else
+ $tag_text .= $str[$i];
+ }
+
+ $this->tag_text($tag_text);
+
+ if($this->throw_errors && !$this->current_tag instanceof Node_Container_Document)
+ throw new Exception_MissingEndTag("Missing closing tag for tag [{$this->current_tag->tag()}]");
+
+ return $this;
+ }
+
+ /**
+ * Handles a BBCode opening tag
+ * @param string $tag
+ * @param array $attrs
+ * @return bool
+ */
+ private function tag_open($tag, $attrs)
+ {
+ if($this->current_tag instanceof Node_Container_Tag)
+ {
+ $closing_tags = $this->bbcodes[$this->current_tag->tag()]->closing_tags();
+
+ if(in_array($tag, $closing_tags))
+ $this->tag_close($this->current_tag->tag());
+ }
+
+ if($this->current_tag instanceof Node_Container_Tag)
+ {
+ $accepted_children = $this->bbcodes[$this->current_tag->tag()]->accepted_children();
+
+ if(!empty($accepted_children) && !in_array($tag, $accepted_children))
+ return false;
+
+ if($this->throw_errors && !$this->bbcodes[$tag]->is_inline()
+ && $this->bbcodes[$this->current_tag->tag()]->is_inline())
+ throw new Exception_InvalidNesting("Block level tag [{$tag}] was opened within an inline tag [{$this->current_tag->tag()}]");
+ }
+
+ $node = new Node_Container_Tag($tag, $attrs);
+ $this->current_tag->add_child($node);
+
+ if(!$this->bbcodes[$tag]->is_self_closing())
+ $this->current_tag = $node;
+
+ return true;
+ }
+
+ /**
+ * Handles tag text
+ * @param string $text
+ * @return void
+ */
+ private function tag_text($text)
+ {
+ if($this->current_tag instanceof Node_Container_Tag)
+ {
+ $accepted_children = $this->bbcodes[$this->current_tag->tag()]->accepted_children();
+
+ if(!empty($accepted_children) && !in_array('text_node', $accepted_children))
+ return;
+ }
+
+ $this->current_tag->add_child(new Node_Text($text));
+ }
+
+ /**
+ * Handles BBCode closing tag
+ * @param string $tag
+ * @return bool
+ */
+ private function tag_close($tag)
+ {
+ if(!$this->current_tag instanceof Node_Container_Document
+ && $tag !== $this->current_tag->tag())
+ {
+ $closing_tags = $this->bbcodes[$this->current_tag->tag()]->closing_tags();
+
+ if(in_array($tag, $closing_tags) || in_array('/' . $tag, $closing_tags))
+ $this->current_tag = $this->current_tag->parent();
+ }
+
+ if($this->current_tag instanceof Node_Container_Document)
+ return false;
+ else if($tag !== $this->current_tag->tag())
+ {
+ // check if this is a tag inside another tag like
+ // [tag1] [tag2] [/tag1] [/tag2]
+ $node = $this->current_tag->find_parent_by_tag($tag);
+
+ if($node !== null)
+ {
+ $this->current_tag = $node->parent();
+
+ while(($node = $node->last_tag_node()) !== null)
+ {
+ $new_node = new Node_Container_Tag($node->tag(), $node->attributes());
+ $this->current_tag->add_child($new_node);
+ $this->current_tag = $new_node;
+ }
+ }
+ else
+ return false;
+ }
+ else
+ $this->current_tag = $this->current_tag->parent();
+
+ return true;
+ }
+
+ /**
+ * Parses a bbcode attribute string into an array
+ * @param string $attribs
+ * @return array
+ */
+ private function parse_attribs($attribs)
+ {
+ $ret = array('default' => null);
+ $attribs = trim($attribs);
+
+ if($attribs == '')
+ return $ret;
+
+ // if this tag only has one = then there is only one attribute
+ // so add it all to default
+ if($attribs[0] == '=' && strrpos($attribs, '=') === 0)
+ $ret['default'] = htmlentities(substr($attribs, 1), ENT_QUOTES | ENT_IGNORE, "UTF-8");
+ else
+ {
+ preg_match_all('/(\S+)=((?:(?:(["\'])(?:\\\3|[^\3])*?\3))|(?:[^\'"\s]+))/',
+ $attribs,
+ $matches,
+ PREG_SET_ORDER);
+
+ foreach($matches as $match)
+ $ret[$match[1]] = htmlentities($match[2], ENT_QUOTES | ENT_IGNORE, "UTF-8");
+ }
+
+ return $ret;
+ }
+
+ private function loop_text_nodes($func, array $exclude=array(), Node_Container $node=null)
+ {
+ if($node === null)
+ $node = $this;
+
+ foreach($node->children() as $child)
+ {
+ if($child instanceof Node_Container_Tag)
+ {
+ if(!in_array($child->tag(), $exclude))
+ $this->loop_text_nodes($func, $exclude, $child);
+ }
+ else if($child instanceof Node_Text)
+ {
+ $func($child);
+ }
+ }
+ }
+
+ /**
+ * Detects any none clickable links and makes them clickable
+ * @return Node_Container_Document
+ */
+ public function detect_links()
+ {
+ $this->loop_text_nodes(function($child) {
+ preg_match_all("/(?:(?:https?|ftp):\/\/|(?:www|ftp)\.)(?:[a-zA-Z0-9\-\.]{1,255}\.[a-zA-Z]{1,20})(?::[0-9]{1,5})?(?:\/[^\s'\"]*)?(?:(?get_text(),
+ $matches,
+ PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE);
+
+ if(count($matches[0]) == 0)
+ return;
+
+ $replacment = array();
+ $last_pos = 0;
+
+ foreach($matches[0] as $match)
+ {
+ if(substr($match[0], 0, 3) === 'ftp' && $match[0][3] !== ':')
+ $url = 'ftp://' . $match[0];
+ else if($match[0][0] === 'w')
+ $url = 'http://' . $match[0];
+ else
+ $url = $match[0];
+
+ $url = new Node_Container_Tag('url', array('default' => htmlentities($url, ENT_QUOTES | ENT_IGNORE, "UTF-8")));
+ $url_text = new Node_Text($match[0]);
+ $url->add_child($url_text);
+
+ $replacment[] = new Node_Text(substr($child->get_text(), $last_pos, $match[1] - $last_pos));
+ $replacment[] = $url;
+ $last_pos = $match[1] + strlen($match[0]);
+ }
+
+ $replacment[] = new Node_Text(substr($child->get_text(), $last_pos));
+ $child->parent()->replace_child($child, $replacment);
+ }, $this->get_excluded_tags(BBCode::AUTO_DETECT_EXCLUDE_URL));
+
+ return $this;
+ }
+
+ /**
+ * Detects any none clickable emails and makes them clickable
+ * @return Node_Container_Document
+ */
+ public function detect_emails()
+ {
+ $this->loop_text_nodes(function($child) {
+ preg_match_all("/(?:[a-zA-Z0-9\-\._]){1,}@(?:[a-zA-Z0-9\-\.]{1,255}\.[a-zA-Z]{1,20})/",
+ $child->get_text(),
+ $matches,
+ PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE);
+
+ if(count($matches[0]) == 0)
+ return;
+
+ $replacment = array();
+ $last_pos = 0;
+
+ foreach($matches[0] as $match)
+ {
+ $url = new Node_Container_Tag('email', array());
+ $url_text = new Node_Text($match[0]);
+ $url->add_child($url_text);
+
+ $replacment[] = new Node_Text(substr($child->get_text(), $last_pos, $match[1] - $last_pos));
+ $replacment[] = $url;
+ $last_pos = $match[1] + strlen($match[0]);
+ }
+
+ $replacment[] = new Node_Text(substr($child->get_text(), $last_pos));
+ $child->parent()->replace_child($child, $replacment);
+ }, $this->get_excluded_tags(BBCode::AUTO_DETECT_EXCLUDE_EMAIL));
+
+ return $this;
+ }
+
+ /**
+ * Detects any emoticons and replaces them with their images
+ * @return Node_Container_Document
+ */
+ public function detect_emoticons()
+ {
+ $pattern = '';
+ foreach($this->emoticons as $key => $url)
+ $pattern .= ($pattern === ''? '/(?:':'|') . preg_quote($key, '/');
+ $pattern .= ')/';
+
+ $emoticons = $this->emoticons;
+
+ $this->loop_text_nodes(function($child) use ($pattern, $emoticons) {
+ preg_match_all($pattern,
+ $child->get_text(),
+ $matches,
+ PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE);
+
+ if(count($matches[0]) == 0)
+ return;
+
+ $replacment = array();
+ $last_pos = 0;
+
+ foreach($matches[0] as $match)
+ {
+ $url = new Node_Container_Tag('img', array('alt'=>$match[0]));
+ $url_text = new Node_Text($emoticons[$match[0]]);
+ $url->add_child($url_text);
+
+ $replacment[] = new Node_Text(substr($child->get_text(), $last_pos, $match[1] - $last_pos));
+ $replacment[] = $url;
+ $last_pos = $match[1] + strlen($match[0]);
+ }
+
+ $replacment[] = new Node_Text(substr($child->get_text(), $last_pos));
+ $child->parent()->replace_child($child, $replacment);
+ }, $this->get_excluded_tags(BBCode::AUTO_DETECT_EXCLUDE_EMOTICON));
+
+ return $this;
+ }
+
+ /**
+ * Gets an array of tages to be excluded from
+ * the elcude param
+ * @param int $exclude What to gets excluded from i.e. BBCode::AUTO_DETECT_EXCLUDE_EMOTICON
+ * @return array
+ */
+ private function get_excluded_tags($exclude)
+ {
+ $ret = array();
+
+ foreach($this->bbcodes as $bbcode)
+ if($bbcode->auto_detect_exclude() & $exclude)
+ $ret[] = $bbcode->tag();
+
+ return $ret;
+ }
+}
diff --git a/classes/Node/Container/Tag.php b/classes/Node/Container/Tag.php
new file mode 100644
index 0000000..65bcf74
--- /dev/null
+++ b/classes/Node/Container/Tag.php
@@ -0,0 +1,43 @@
+tag = $tag;
+ $this->attribs = $attribs;
+ }
+
+ /**
+ * Gets the tag of this node
+ * @return string
+ */
+ public function tag()
+ {
+ return $this->tag;
+ }
+
+ /**
+ * Gets the tags attributes
+ * @return array
+ */
+ public function attributes()
+ {
+ return $this->attribs;
+ }
+}
diff --git a/classes/Node/Text.php b/classes/Node/Text.php
new file mode 100644
index 0000000..4f8e77d
--- /dev/null
+++ b/classes/Node/Text.php
@@ -0,0 +1,27 @@
+text = $text;
+ }
+
+ public function get_html($nl2br=true)
+ {
+ if(!$nl2br)
+ return str_replace(" ", " ", htmlentities($this->text, ENT_QUOTES | ENT_IGNORE, "UTF-8"));
+
+ return str_replace(" ", " ", nl2br(htmlentities($this->text, ENT_QUOTES | ENT_IGNORE, "UTF-8")));
+ }
+
+ public function get_text()
+ {
+ return $this->text;
+ }
+}
\ No newline at end of file
diff --git a/classes/SBBCodeParser.php b/classes/SBBCodeParser.php
new file mode 100644
index 0000000..729d1fd
--- /dev/null
+++ b/classes/SBBCodeParser.php
@@ -0,0 +1,41 @@
+.
+ */
+
+require('Exception.php');
+require('Exception/MissingEndTag.php');
+require('Exception/InvalidNesting.php');
+
+require('Node.php');
+require('Node/Text.php');
+require('Node/Container.php');
+require('Node/Container/Tag.php');
+require('Node/Container/Document.php');
+
+require('BBCode.php');