diff --git a/app/jbbcode/CodeDefinition.php b/app/jbbcode/CodeDefinition.php
new file mode 100644
index 0000000..f7a07f7
--- /dev/null
+++ b/app/jbbcode/CodeDefinition.php
@@ -0,0 +1,328 @@
+elCounter = 0;
+ $def->setTagName($tagName);
+ $def->setReplacementText($replacementText);
+ $def->useOption = $useOption;
+ $def->parseContent = $parseContent;
+ $def->nestLimit = $nestLimit;
+ $def->optionValidator = $optionValidator;
+ $def->bodyValidator = $bodyValidator;
+ return $def;
+ }
+
+ /**
+ * Constructs a new CodeDefinition.
+ *
+ * This constructor is deprecated. You should use the static construct() method or the
+ * CodeDefinitionBuilder class to construct a new CodeDefiniton.
+ *
+ * @deprecated
+ */
+ public function __construct()
+ {
+ /* WARNING: This function is deprecated and will be made protected in a future
+ * version of jBBCode. */
+ $this->parseContent = true;
+ $this->useOption = false;
+ $this->nestLimit = -1;
+ $this->elCounter = 0;
+ $this->optionValidator = array();
+ $this->bodyValidator = null;
+ }
+
+ /**
+ * Determines if the arguments to the given element are valid based on
+ * any validators attached to this CodeDefinition.
+ *
+ * @param $el the ElementNode to validate
+ * @return true if the ElementNode's {option} and {param} are OK, false if they're not
+ */
+ public function hasValidInputs(ElementNode $el)
+ {
+ if ($this->usesOption() && $this->optionValidator) {
+ $att = $el->getAttribute();
+
+ foreach($att as $name => $value){
+ if(isset($this->optionValidator[$name]) && !$this->optionValidator[$name]->validate($value)){
+ return false;
+ }
+ }
+ }
+
+ if (!$this->parseContent() && $this->bodyValidator) {
+ /* We only evaluate the content if we're not parsing the content. */
+ $content = "";
+ foreach ($el->getChildren() as $child) {
+ $content .= $child->getAsBBCode();
+ }
+ if (!$this->bodyValidator->validate($content)) {
+ /* The content of the element is not valid. */
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Accepts an ElementNode that is defined by this CodeDefinition and returns the HTML
+ * markup of the element. This is a commonly overridden class for custom CodeDefinitions
+ * so that the content can be directly manipulated.
+ *
+ * @param $el the element to return an html representation of
+ *
+ * @return the parsed html of this element (INCLUDING ITS CHILDREN)
+ */
+ public function asHtml(ElementNode $el)
+ {
+ if (!$this->hasValidInputs($el)) {
+ return $el->getAsBBCode();
+ }
+
+ $html = $this->getReplacementText();
+
+ if ($this->usesOption()) {
+ $options = $el->getAttribute();
+ if(count($options)==1){
+ $vals = array_values($options);
+ $html = str_ireplace('{option}', reset($vals), $html);
+ }
+ else{
+ foreach($options as $key => $val){
+ $html = str_ireplace('{' . $key . '}', $val, $html);
+ }
+ }
+ }
+
+ $content = $this->getContent($el);
+
+ $html = str_ireplace('{param}', $content, $html);
+
+ return $html;
+ }
+
+ protected function getContent(ElementNode $el){
+ if ($this->parseContent()) {
+ $content = "";
+ foreach ($el->getChildren() as $child)
+ $content .= $child->getAsHTML();
+ } else {
+ $content = "";
+ foreach ($el->getChildren() as $child)
+ $content .= $child->getAsBBCode();
+ }
+ return $content;
+ }
+
+ /**
+ * Accepts an ElementNode that is defined by this CodeDefinition and returns the text
+ * representation of the element. This may be overridden by a custom CodeDefinition.
+ *
+ * @param $el the element to return a text representation of
+ *
+ * @return the text representation of $el
+ */
+ public function asText(ElementNode $el)
+ {
+ if (!$this->hasValidInputs($el)) {
+ return $el->getAsBBCode();
+ }
+
+ $s = "";
+ foreach ($el->getChildren() as $child)
+ $s .= $child->getAsText();
+ return $s;
+ }
+
+ /**
+ * Returns the tag name of this code definition
+ *
+ * @return this definition's associated tag name
+ */
+ public function getTagName()
+ {
+ return $this->tagName;
+ }
+
+ /**
+ * Returns the replacement text of this code definition. This usually has little, if any meaning if the
+ * CodeDefinition class was extended. For default, html replacement CodeDefinitions this returns the html
+ * markup for the definition.
+ *
+ * @return the replacement text of this CodeDefinition
+ */
+ public function getReplacementText()
+ {
+ return $this->replacementText;
+ }
+
+ /**
+ * Returns whether or not this CodeDefinition uses the optional {option}
+ *
+ * @return true if this CodeDefinition uses the option, false otherwise
+ */
+ public function usesOption()
+ {
+ return $this->useOption;
+ }
+
+ /**
+ * Returns whether or not this CodeDefnition parses elements contained within it,
+ * or just treats its children as text.
+ *
+ * @return true if this CodeDefinition parses elements contained within itself
+ */
+ public function parseContent()
+ {
+ return $this->parseContent;
+ }
+
+ /**
+ * Returns the limit of how many elements defined by this CodeDefinition may be
+ * nested together. If after parsing elements are nested beyond this limit, the
+ * subtrees formed by those nodes will be removed from the parse tree. A nest
+ * limit of -1 signifies no limit.
+ */
+ public function getNestLimit()
+ {
+ return $this->nestLimit;
+ }
+
+ /**
+ * Sets the tag name of this CodeDefinition
+ *
+ * @deprecated
+ *
+ * @param the new tag name of this definition
+ */
+ public function setTagName($tagName)
+ {
+ $this->tagName = strtolower($tagName);
+ }
+
+ /**
+ * Sets the html replacement text of this CodeDefinition
+ *
+ * @deprecated
+ *
+ * @param the new replacement text
+ */
+ public function setReplacementText($txt)
+ {
+ $this->replacementText = $txt;
+ }
+
+ /**
+ * Sets whether or not this CodeDefinition uses the {option}
+ *
+ * @deprecated
+ *
+ * @param boolean $bool
+ */
+ public function setUseOption($bool)
+ {
+ $this->useOption = $bool;
+ }
+
+ /**
+ * Sets whether or not this CodeDefinition allows its children to be parsed as html
+ *
+ * @deprecated
+ *
+ * @param boolean $bool
+ */
+ public function setParseContent($bool)
+ {
+ $this->parseContent = $bool;
+ }
+
+ /**
+ * Increments the element counter. This is used for tracking depth of elements of the same type for next limits.
+ *
+ * @deprecated
+ *
+ * @return void
+ */
+ public function incrementCounter()
+ {
+ $this->elCounter++;
+ }
+
+ /**
+ * Decrements the element counter.
+ *
+ * @deprecated
+ *
+ * @return void
+ */
+ public function decrementCounter()
+ {
+ $this->elCounter--;
+ }
+
+ /**
+ * Resets the element counter.
+ *
+ * @deprecated
+ */
+ public function resetCounter()
+ {
+ $this->elCounter = 0;
+ }
+
+ /**
+ * Returns the current value of the element counter.
+ *
+ * @deprecated
+ *
+ * @return int
+ */
+ public function getCounter()
+ {
+ return $this->elCounter;
+ }
+}
diff --git a/app/jbbcode/CodeDefinitionBuilder.php b/app/jbbcode/CodeDefinitionBuilder.php
new file mode 100644
index 0000000..6e8bbc1
--- /dev/null
+++ b/app/jbbcode/CodeDefinitionBuilder.php
@@ -0,0 +1,160 @@
+tagName = $tagName;
+ $this->replacementText = $replacementText;
+ }
+
+ /**
+ * Sets the tag name the CodeDefinition should be built with.
+ *
+ * @param $tagName the tag name for the new CodeDefinition
+ */
+ public function setTagName($tagName)
+ {
+ $this->tagName = $tagName;
+ return $this;
+ }
+
+ /**
+ * Sets the replacement text that the new CodeDefinition should be
+ * built with.
+ *
+ * @param $replacementText the replacement text for the new CodeDefinition
+ */
+ public function setReplacementText($replacementText)
+ {
+ $this->replacementText = $replacementText;
+ return $this;
+ }
+
+ /**
+ * Set whether or not the built CodeDefinition should use the {option} bbcode
+ * argument.
+ *
+ * @param $option ture iff the definition includes an option
+ */
+ public function setUseOption($option)
+ {
+ $this->useOption = $option;
+ return $this;
+ }
+
+ /**
+ * Set whether or not the built CodeDefinition should allow its content
+ * to be parsed and evaluated as bbcode.
+ *
+ * @param $parseContent true iff the content should be parsed
+ */
+ public function setParseContent($parseContent)
+ {
+ $this->parseContent = $parseContent;
+ return $this;
+ }
+
+ /**
+ * Sets the nest limit for this code definition.
+ *
+ * @param $nestLimit a positive integer, or -1 if there is no limit.
+ * @throws \InvalidArgumentException if the nest limit is invalid
+ */
+ public function setNestLimit($limit)
+ {
+ if(!is_int($limit) || ($limit <= 0 && -1 != $limit)) {
+ throw new \InvalidArgumentException("A nest limit must be a positive integer " .
+ "or -1.");
+ }
+ $this->nestLimit = $limit;
+ return $this;
+ }
+
+ /**
+ * Sets the InputValidator that option arguments should be validated with.
+ *
+ * @param $validator the InputValidator instance to use
+ */
+ public function setOptionValidator(\JBBCode\InputValidator $validator, $option=null)
+ {
+ if(empty($option)){
+ $option = $this->tagName;
+ }
+ $this->optionValidator[$option] = $validator;
+ return $this;
+ }
+
+ /**
+ * Sets the InputValidator that body ({param}) text should be validated with.
+ *
+ * @param $validator the InputValidator instance to use
+ */
+ public function setBodyValidator(\JBBCode\InputValidator $validator)
+ {
+ $this->bodyValidator = $validator;
+ return $this;
+ }
+
+ /**
+ * Removes the attached option validator if one is attached.
+ */
+ public function removeOptionValidator()
+ {
+ $this->optionValidator = array();
+ return $this;
+ }
+
+ /**
+ * Removes the attached body validator if one is attached.
+ */
+ public function removeBodyValidator()
+ {
+ $this->bodyValidator = null;
+ return $this;
+ }
+
+ /**
+ * Builds a CodeDefinition with the current state of the builder.
+ *
+ * @return a new CodeDefinition instance
+ */
+ public function build()
+ {
+ $definition = CodeDefinition::construct($this->tagName,
+ $this->replacementText,
+ $this->useOption,
+ $this->parseContent,
+ $this->nestLimit,
+ $this->optionValidator,
+ $this->bodyValidator);
+ return $definition;
+ }
+
+
+}
diff --git a/app/jbbcode/CodeDefinitionSet.php b/app/jbbcode/CodeDefinitionSet.php
new file mode 100644
index 0000000..1b16650
--- /dev/null
+++ b/app/jbbcode/CodeDefinitionSet.php
@@ -0,0 +1,22 @@
+{param}');
+ array_push($this->definitions, $builder->build());
+
+ /* [i] italics tag */
+ $builder = new CodeDefinitionBuilder('i', '{param}');
+ array_push($this->definitions, $builder->build());
+
+ /* [u] underline tag */
+ $builder = new CodeDefinitionBuilder('u', '{param}');
+ array_push($this->definitions, $builder->build());
+
+ $urlValidator = new \JBBCode\validators\UrlValidator();
+
+ /* [url] link tag */
+ $builder = new CodeDefinitionBuilder('url', '{param}');
+ $builder->setParseContent(false)->setBodyValidator($urlValidator);
+ array_push($this->definitions, $builder->build());
+
+ /* [url=http://example.com] link tag */
+ $builder = new CodeDefinitionBuilder('url', '{param}');
+ $builder->setUseOption(true)->setParseContent(true)->setOptionValidator($urlValidator);
+ array_push($this->definitions, $builder->build());
+
+ /* [img] image tag */
+ $builder = new CodeDefinitionBuilder('img', '
');
+ $builder->setUseOption(false)->setParseContent(false)->setBodyValidator($urlValidator);
+ array_push($this->definitions, $builder->build());
+
+ /* [img=alt text] image tag */
+ $builder = new CodeDefinitionBuilder('img', '
');
+ $builder->setUseOption(true)->setParseContent(false)->setBodyValidator($urlValidator);
+ array_push($this->definitions, $builder->build());
+
+ /* [color] color tag */
+ $builder = new CodeDefinitionBuilder('color', '{param}');
+ $builder->setUseOption(true)->setOptionValidator(new \JBBCode\validators\CssColorValidator());
+ array_push($this->definitions, $builder->build());
+ }
+
+ /**
+ * Returns an array of the default code definitions.
+ */
+ public function getCodeDefinitions()
+ {
+ return $this->definitions;
+ }
+
+}
diff --git a/app/jbbcode/DocumentElement.php b/app/jbbcode/DocumentElement.php
new file mode 100644
index 0000000..54b40c6
--- /dev/null
+++ b/app/jbbcode/DocumentElement.php
@@ -0,0 +1,67 @@
+setTagName("Document");
+ $this->setNodeId(0);
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see JBBCode.ElementNode::getAsBBCode()
+ *
+ * Returns the BBCode representation of this document
+ *
+ * @return this document's bbcode representation
+ */
+ public function getAsBBCode()
+ {
+ $s = "";
+ foreach($this->getChildren() as $child){
+ $s .= $child->getAsBBCode();
+ }
+
+ return $s;
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see JBBCode.ElementNode::getAsHTML()
+ *
+ * Documents don't add any html. They only exist as a container for their
+ * children, so getAsHTML() simply iterates through the document's children,
+ * returning their html.
+ *
+ * @return the HTML representation of this document
+ */
+ public function getAsHTML()
+ {
+ $s = "";
+ foreach($this->getChildren() as $child)
+ $s .= $child->getAsHTML();
+
+ return $s;
+ }
+
+ public function accept(NodeVisitor $visitor)
+ {
+ $visitor->visitDocumentElement($this);
+ }
+
+}
diff --git a/app/jbbcode/ElementNode.php b/app/jbbcode/ElementNode.php
new file mode 100644
index 0000000..5393bb1
--- /dev/null
+++ b/app/jbbcode/ElementNode.php
@@ -0,0 +1,241 @@
+children = array();
+ $this->nestDepth = 0;
+ }
+
+ /**
+ * Accepts the given NodeVisitor. This is part of an implementation
+ * of the Visitor pattern.
+ *
+ * @param $nodeVisitor the visitor attempting to visit this node
+ */
+ public function accept(NodeVisitor $nodeVisitor)
+ {
+ $nodeVisitor->visitElementNode($this);
+ }
+
+ /**
+ * Gets the CodeDefinition that defines this element.
+ *
+ * @return this element's code definition
+ */
+ public function getCodeDefinition()
+ {
+ return $this->codeDefinition;
+ }
+
+ /**
+ * Sets the CodeDefinition that defines this element.
+ *
+ * @param codeDef the code definition that defines this element node
+ */
+ public function setCodeDefinition(CodeDefinition $codeDef)
+ {
+ $this->codeDefinition = $codeDef;
+ $this->setTagName($codeDef->getTagName());
+ }
+
+ /**
+ * Returns the tag name of this element.
+ *
+ * @return the element's tag name
+ */
+ public function getTagName()
+ {
+ return $this->tagName;
+ }
+
+ /**
+ * Returns the attribute (used as the option in bbcode definitions) of this element.
+ *
+ * @return the attribute of this element
+ */
+ public function getAttribute()
+ {
+ return $this->attribute;
+ }
+
+ /**
+ * Returns all the children of this element.
+ *
+ * @return an array of this node's child nodes
+ */
+ public function getChildren()
+ {
+ return $this->children;
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see JBBCode.Node::getAsText()
+ *
+ * Returns the element as text (not including any bbcode markup)
+ *
+ * @return the plain text representation of this node
+ */
+ public function getAsText()
+ {
+ if ($this->codeDefinition) {
+ return $this->codeDefinition->asText($this);
+ } else {
+ $s = "";
+ foreach ($this->getChildren() as $child)
+ $s .= $child->getAsText();
+ return $s;
+ }
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see JBBCode.Node::getAsBBCode()
+ *
+ * Returns the element as bbcode (with all unclosed tags closed)
+ *
+ * @return the bbcode representation of this element
+ */
+ public function getAsBBCode()
+ {
+ $str = "[".$this->tagName;
+ if (!empty($this->attribute)) {
+
+ foreach($this->attribute as $key => $value){
+ if($key == $this->tagName){
+ $str .= "=".$value;
+ }
+ else{
+ $str .= " ".$key."=" . $value;
+ }
+ }
+ }
+ $str .= "]";
+ foreach ($this->getChildren() as $child) {
+ $str .= $child->getAsBBCode();
+ }
+ $str .= "[/".$this->tagName."]";
+
+ return $str;
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see JBBCode.Node::getAsHTML()
+ *
+ * Returns the element as html with all replacements made
+ *
+ * @return the html representation of this node
+ */
+ public function getAsHTML()
+ {
+ if($this->codeDefinition) {
+ return $this->codeDefinition->asHtml($this);
+ } else {
+ return "";
+ }
+ }
+
+ /**
+ * Adds a child to this node's content. A child may be a TextNode, or
+ * another ElementNode... or anything else that may extend the
+ * abstract Node class.
+ *
+ * @param child the node to add as a child
+ */
+ public function addChild(Node $child)
+ {
+ array_push($this->children, $child);
+ $child->setParent($this);
+ }
+
+ /**
+ * Removes a child from this node's contnet.
+ *
+ * @param child the child node to remove
+ */
+ public function removeChild(Node $child)
+ {
+ foreach ($this->children as $key => $value) {
+ if ($value == $child)
+ unset($this->children[$key]);
+ }
+ }
+
+ /**
+ * Sets the tag name of this element node.
+ *
+ * @param tagName the element's new tag name
+ */
+ public function setTagName($tagName)
+ {
+ $this->tagName = $tagName;
+ }
+
+ /**
+ * Sets the attribute (option) of this element node.
+ *
+ * @param attribute the attribute of this element node
+ */
+ public function setAttribute($attribute)
+ {
+ $this->attribute = $attribute;
+ }
+
+ /**
+ * Traverses the parse tree upwards, going from parent to parent, until it finds a
+ * parent who has the given tag name. Returns the parent with the matching tag name
+ * if it exists, otherwise returns null.
+ *
+ * @param str the tag name to search for
+ *
+ * @return the closest parent with the given tag name
+ */
+ public function closestParentOfType($str)
+ {
+ $str = strtolower($str);
+ $currentEl = $this;
+
+ while (strtolower($currentEl->getTagName()) != $str && $currentEl->hasParent()) {
+ $currentEl = $currentEl->getParent();
+ }
+
+ if (strtolower($currentEl->getTagName()) != $str) {
+ return null;
+ } else {
+ return $currentEl;
+ }
+ }
+
+}
diff --git a/app/jbbcode/InputValidator.php b/app/jbbcode/InputValidator.php
new file mode 100644
index 0000000..6774709
--- /dev/null
+++ b/app/jbbcode/InputValidator.php
@@ -0,0 +1,20 @@
+nodeid;
+ }
+
+ /**
+ * Returns this node's immediate parent.
+ *
+ * @return the node's parent
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Determines if this node has a parent.
+ *
+ * @return true if this node has a parent, false otherwise
+ */
+ public function hasParent()
+ {
+ return $this->parent != null;
+ }
+
+ /**
+ * Returns true if this is a text node. Returns false otherwise.
+ * (Overridden by TextNode to return true)
+ *
+ * @return true if this node is a text node
+ */
+ public function isTextNode()
+ {
+ return false;
+ }
+
+ /**
+ * Accepts a NodeVisitor
+ *
+ * @param nodeVisitor the NodeVisitor traversing the graph
+ */
+ abstract public function accept(NodeVisitor $nodeVisitor);
+
+ /**
+ * Returns this node as text (without any bbcode markup)
+ *
+ * @return the plain text representation of this node
+ */
+ abstract public function getAsText();
+
+ /**
+ * Returns this node as bbcode
+ *
+ * @return the bbcode representation of this node
+ */
+ abstract public function getAsBBCode();
+
+ /**
+ * Returns this node as HTML
+ *
+ * @return the html representation of this node
+ */
+ abstract public function getAsHTML();
+
+ /**
+ * Sets this node's parent to be the given node.
+ *
+ * @param parent the node to set as this node's parent
+ */
+ public function setParent(Node $parent)
+ {
+ $this->parent = $parent;
+ }
+
+ /**
+ * Sets this node's nodeid
+ *
+ * @param nodeid this node's node id
+ */
+ public function setNodeId($nodeid)
+ {
+ $this->nodeid = $nodeid;
+ }
+
+}
diff --git a/app/jbbcode/NodeVisitor.php b/app/jbbcode/NodeVisitor.php
new file mode 100644
index 0000000..1dd228a
--- /dev/null
+++ b/app/jbbcode/NodeVisitor.php
@@ -0,0 +1,20 @@
+reset();
+ $this->bbcodes = array();
+ }
+
+ /**
+ * Adds a simple (text-replacement only) bbcode definition
+ *
+ * @param string $tagName the tag name of the code (for example the b in [b])
+ * @param string $replace the html to use, with {param} and optionally {option} for replacements
+ * @param boolean $useOption whether or not this bbcode uses the secondary {option} replacement
+ * @param boolean $parseContent whether or not to parse the content within these elements
+ * @param integer $nestLimit an optional limit of the number of elements of this kind that can be nested within
+ * each other before the parser stops parsing them.
+ * @param InputValidator $optionValidator the validator to run {option} through
+ * @param BodyValidator $bodyValidator the validator to run {param} through (only used if $parseContent == false)
+ *
+ * @return Parser
+ */
+ public function addBBCode($tagName, $replace, $useOption = false, $parseContent = true, $nestLimit = -1,
+ InputValidator $optionValidator = null, InputValidator $bodyValidator = null)
+ {
+ $builder = new CodeDefinitionBuilder($tagName, $replace);
+
+ $builder->setUseOption($useOption);
+ $builder->setParseContent($parseContent);
+ $builder->setNestLimit($nestLimit);
+
+ if ($optionValidator) {
+ $builder->setOptionValidator($optionValidator);
+ }
+
+ if ($bodyValidator) {
+ $builder->setBodyValidator($bodyValidator);
+ }
+
+ $this->addCodeDefinition($builder->build());
+
+ return $this;
+ }
+
+ /**
+ * Adds a complex bbcode definition. You may subclass the CodeDefinition class, instantiate a definition of your new
+ * class and add it to the parser through this method.
+ *
+ * @param CodeDefinition $definition the bbcode definition to add
+ *
+ * @return Parser
+ */
+ public function addCodeDefinition(CodeDefinition $definition)
+ {
+ array_push($this->bbcodes, $definition);
+
+ return $this;
+ }
+
+ /**
+ * Adds a set of CodeDefinitions.
+ *
+ * @param CodeDefinitionSet $set the set of definitions to add
+ *
+ * @return Parser
+ */
+ public function addCodeDefinitionSet(CodeDefinitionSet $set) {
+ foreach ($set->getCodeDefinitions() as $def) {
+ $this->addCodeDefinition($def);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the entire parse tree as text. Only {param} content is returned. BBCode markup will be ignored.
+ *
+ * @return string a text representation of the parse tree
+ */
+ public function getAsText()
+ {
+ return $this->treeRoot->getAsText();
+ }
+
+ /**
+ * Returns the entire parse tree as bbcode. This will be identical to the inputted string, except unclosed tags
+ * will be closed.
+ *
+ * @return string a bbcode representation of the parse tree
+ */
+ public function getAsBBCode()
+ {
+ return $this->treeRoot->getAsBBCode();
+ }
+
+ /**
+ * Returns the entire parse tree as HTML. All BBCode replacements will be made. This is generally the method
+ * you will want to use to retrieve the parsed bbcode.
+ *
+ * @return string a parsed html string
+ */
+ public function getAsHTML()
+ {
+ return $this->treeRoot->getAsHTML();
+ }
+
+ /**
+ * Accepts the given NodeVisitor at the root.
+ *
+ * @param NodeVisitor a NodeVisitor
+ *
+ * @return Parser
+ */
+ public function accept(NodeVisitor $nodeVisitor)
+ {
+ $this->treeRoot->accept($nodeVisitor);
+
+ return $this;
+ }
+ /**
+ * Constructs the parse tree from a string of bbcode markup.
+ *
+ * @param string $str the bbcode markup to parse
+ *
+ * @return Parser
+ */
+ public function parse($str)
+ {
+ /* Set the tree root back to a fresh DocumentElement. */
+ $this->reset();
+
+ $parent = $this->treeRoot;
+ $tokenizer = new Tokenizer($str);
+
+ while ($tokenizer->hasNext()) {
+ $parent = $this->parseStartState($parent, $tokenizer);
+ if ($parent->getCodeDefinition() && false ===
+ $parent->getCodeDefinition()->parseContent()) {
+ /* We're inside an element that does not allow its contents to be parseable. */
+ $this->parseAsTextUntilClose($parent, $tokenizer);
+ $parent = $parent->getParent();
+ }
+ }
+
+ /* We parsed ignoring nest limits. Do an O(n) traversal to remove any elements that
+ * are nested beyond their CodeDefinition's nest limit. */
+ $this->removeOverNestedElements();
+
+ return $this;
+ }
+
+ /**
+ * Removes any elements that are nested beyond their nest limit from the parse tree. This
+ * method is now deprecated. In a future release its access privileges will be made
+ * protected.
+ *
+ * @deprecated
+ */
+ public function removeOverNestedElements()
+ {
+ $nestLimitVisitor = new \JBBCode\visitors\NestLimitVisitor();
+ $this->accept($nestLimitVisitor);
+ }
+
+ /**
+ * Removes the old parse tree if one exists.
+ */
+ protected function reset()
+ {
+ // remove any old tree information
+ $this->treeRoot = new DocumentElement();
+ /* The document element is created with nodeid 0. */
+ $this->nextNodeid = 1;
+ }
+
+ /**
+ * Determines whether a bbcode exists based on its tag name and whether or not it uses an option
+ *
+ * @param string $tagName the bbcode tag name to check
+ * @param boolean $usesOption whether or not the bbcode accepts an option
+ *
+ * @return bool true if the code exists, false otherwise
+ */
+ public function codeExists($tagName, $usesOption = false)
+ {
+ foreach ($this->bbcodes as $code) {
+ if (strtolower($tagName) == $code->getTagName() && $usesOption == $code->usesOption()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the CodeDefinition of a bbcode with the matching tag name and usesOption parameter
+ *
+ * @param string $tagName the tag name of the bbcode being searched for
+ * @param boolean $usesOption whether or not the bbcode accepts an option
+ *
+ * @return CodeDefinition if the bbcode exists, null otherwise
+ */
+ public function getCode($tagName, $usesOption = false)
+ {
+ foreach ($this->bbcodes as $code) {
+ if (strtolower($tagName) == $code->getTagName() && $code->usesOption() == $usesOption) {
+ return $code;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Adds a set of default, standard bbcode definitions commonly used across the web.
+ *
+ * This method is now deprecated. Please use DefaultCodeDefinitionSet and
+ * addCodeDefinitionSet() instead.
+ *
+ * @deprecated
+ */
+ public function loadDefaultCodes()
+ {
+ $defaultSet = new DefaultCodeDefinitionSet();
+ $this->addCodeDefinitionSet($defaultSet);
+ }
+
+ /**
+ * Creates a new text node with the given parent and text string.
+ *
+ * @param $parent the parent of the text node
+ * @param $string the text of the text node
+ *
+ * @return TextNode the newly created TextNode
+ */
+ protected function createTextNode(ElementNode $parent, $string)
+ {
+ if (count($parent->getChildren())) {
+ $children = $parent->getChildren();
+ $lastElement = end($children);
+ reset($children);
+
+ if ($lastElement->isTextNode()) {
+ $lastElement->setValue($lastElement->getValue() . $string);
+ return $lastElement;
+ }
+ }
+
+ $textNode = new TextNode($string);
+ $textNode->setNodeId(++$this->nextNodeid);
+ $parent->addChild($textNode);
+ return $textNode;
+ }
+
+ /**
+ * jBBCode parsing logic is loosely modelled after a FSM. While not every function maps
+ * to a unique DFSM state, each function handles the logic of one or more FSM states.
+ * This function handles the beginning parse state when we're not currently in a tag
+ * name.
+ *
+ * @param ElementNode $parent the current parent node we're under
+ * @param Tokenizer $tokenizer the tokenizer we're using
+ *
+ * @return ElementNode the new parent we should use for the next iteration.
+ */
+ protected function parseStartState(ElementNode $parent, Tokenizer $tokenizer)
+ {
+ $next = $tokenizer->next();
+
+ if ('[' == $next) {
+ return $this->parseTagOpen($parent, $tokenizer);
+ }
+ else {
+ $this->createTextNode($parent, $next);
+ /* Drop back into the main parse loop which will call this
+ * same method again. */
+ return $parent;
+ }
+ }
+
+ /**
+ * This function handles parsing the beginnings of an open tag. When we see a [
+ * at an appropriate time, this function is entered.
+ *
+ * @param ElementNode $parent the current parent node
+ * @param Tokenizer $tokenizer the tokenizer we're using
+ *
+ * @return ElementNode the new parent node
+ */
+ protected function parseTagOpen(ElementNode $parent, Tokenizer $tokenizer)
+ {
+
+ if (!$tokenizer->hasNext()) {
+ /* The [ that sent us to this state was just a trailing [, not the
+ * opening for a new tag. Treat it as such. */
+ $this->createTextNode($parent, '[');
+ return $parent;
+ }
+
+ $next = $tokenizer->next();
+
+ /* This while loop could be replaced by a recursive call to this same method,
+ * which would likely be a lot clearer but I decided to use a while loop to
+ * prevent stack overflow with a string like [[[[[[[[[...[[[.
+ */
+ while ('[' == $next) {
+ /* The previous [ was just a random bracket that should be treated as text.
+ * Continue until we get a non open bracket. */
+ $this->createTextNode($parent, '[');
+ if (!$tokenizer->hasNext()) {
+ $this->createTextNode($parent, '[');
+ return $parent;
+ }
+ $next = $tokenizer->next();
+ }
+
+ if (!$tokenizer->hasNext()) {
+ $this->createTextNode($parent, '['.$next);
+ return $parent;
+ }
+
+ $after_next = $tokenizer->next();
+ $tokenizer->stepBack();
+
+ if ($after_next != ']')
+ {
+ $this->createTextNode($parent, '['.$next);
+ return $parent;
+ }
+
+ /* At this point $next is either ']' or plain text. */
+ if (']' == $next) {
+ $this->createTextNode($parent, '[');
+ $this->createTextNode($parent, ']');
+ return $parent;
+ } else {
+ /* $next is plain text... likely a tag name. */
+ return $this->parseTag($parent, $tokenizer, $next);
+ }
+ }
+
+ protected function parseOptions($tagContent)
+ {
+ $buffer = "";
+ $tagName = "";
+ $state = static::OPTION_STATE_TAGNAME;
+ $keys = array();
+ $values = array();
+ $options = array();
+
+ $len = strlen($tagContent);
+ $done = false;
+ $idx = 0;
+
+ try{
+ while(!$done){
+ $char = $idx < $len ? $tagContent[$idx]:null;
+ switch($state){
+ case static::OPTION_STATE_TAGNAME:
+ switch($char){
+ case '=':
+ $state = static::OPTION_STATE_VALUE;
+ $tagName = $buffer;
+ $keys[] = $tagName;
+ $buffer = "";
+ break;
+ case ' ':
+ $state = static::OPTION_STATE_DEFAULT;
+ $tagName = $buffer;
+ $buffer = '';
+ $keys[] = $tagName;
+ break;
+
+ case null:
+ $tagName = $buffer;
+ $buffer = '';
+ $keys[] = $tagName;
+ break;
+ default:
+ $buffer .= $char;
+ }
+ break;
+
+ case static::OPTION_STATE_DEFAULT:
+ switch($char){
+ case ' ':
+ // do nothing
+ default:
+ $state = static::OPTION_STATE_KEY;
+ $buffer .= $char;
+ }
+ break;
+
+ case static::OPTION_STATE_VALUE:
+ switch($char){
+ case '"':
+ $state = static::OPTION_STATE_QUOTED_VALUE;
+ break;
+ case null: // intentional fall-through
+ case ' ': // key=value delimits to next key
+ $values[] = $buffer;
+ $buffer = "";
+ $state = static::OPTION_STATE_KEY;
+ break;
+ case ":":
+ if($buffer=="javascript"){
+ $state = static::OPTION_STATE_JAVASCRIPT;
+ }
+ $buffer .= $char;
+ break;
+ default:
+ $buffer .= $char;
+
+ }
+ break;
+
+ case static::OPTION_STATE_JAVASCRIPT:
+ switch($char){
+ case ";":
+ $buffer .= $char;
+ $values[] = $buffer;
+ $buffer = "";
+ $state = static::OPTION_STATE_KEY;
+
+ break;
+ default:
+ $buffer .= $char;
+ }
+ break;
+
+ case static::OPTION_STATE_KEY:
+ switch($char){
+ case '=':
+ $state = static::OPTION_STATE_VALUE;
+ $keys[] = $buffer;
+ $buffer = '';
+ break;
+ case ' ': // ignore key=value
+ break;
+ default:
+ $buffer .= $char;
+ break;
+ }
+ break;
+
+ case static::OPTION_STATE_QUOTED_VALUE:
+ switch($char){
+ case null:
+ case '"':
+ $state = static::OPTION_STATE_KEY;
+ $values[] = $buffer;
+ $buffer = '';
+
+ // peek ahead. If the next character is not a space or a closing brace, we have a bad tag and need to abort
+ if(isset($tagContent[$idx+1]) && $tagContent[$idx+1]!=" " && $tagContent[$idx+1]!="]" ){
+ throw new ParserException("Badly formed attribute: $tagContent");
+ }
+ break;
+ default:
+ $buffer .= $char;
+ break;
+ }
+ break;
+ default:
+ if(!empty($char)){
+ $state = static::OPTION_STATE_KEY;
+ }
+
+ }
+ if($idx >= $len){
+ $done = true;
+ }
+ $idx++;
+ }
+
+ if(count($keys) && count($values)){
+ if(count($keys)==(count($values)+1)){
+ array_unshift($values, "");
+ }
+
+ $options = array_combine($keys, $values);
+ }
+ }
+ catch(ParserException $e){
+ // if we're in this state, then something evidently went wrong. We'll consider everything that came after the tagname to be the attribute for that keyname
+ $options[$tagName]= substr($tagContent, strpos($tagContent, "=")+1);
+ }
+ return array($tagName, $options);
+ }
+
+ /**
+ * This is the next step in parsing a tag. It's possible for it to still be invalid at this
+ * point but many of the basic invalid tag name conditions have already been handled.
+ *
+ * @param ElementNode $parent the current parent element
+ * @param Tokenizer $tokenizer the tokenizer we're using
+ * @param string $tagContent the text between the [ and the ], assuming there is actually a ]
+ *
+ * @return ElementNode the new parent element
+ */
+ protected function parseTag(ElementNode $parent, Tokenizer $tokenizer, $tagContent)
+ {
+
+ $next;
+ if (!$tokenizer->hasNext() || ($next = $tokenizer->next()) != ']') {
+ /* This is a malformed tag. Both the previous [ and the tagContent
+ * is really just plain text. */
+ $this->createTextNode($parent, '[');
+ $this->createTextNode($parent, $tagContent);
+ return $parent;
+ }
+
+ /* This is a well-formed tag consisting of [something] or [/something], but
+ * we still need to ensure that 'something' is a valid tag name. Additionally,
+ * if it's a closing tag, we need to ensure that there was a previous matching
+ * opening tag.
+ */
+ /* There could be attributes. */
+ list($tmpTagName, $options) = $this->parseOptions($tagContent);
+
+ // $tagPieces = explode('=', $tagContent);
+ // $tmpTagName = $tagPieces[0];
+
+ $actualTagName;
+ if ('' != $tmpTagName && '/' == $tmpTagName[0]) {
+ /* This is a closing tag name. */
+ $actualTagName = substr($tmpTagName, 1);
+ } else {
+ $actualTagName = $tmpTagName;
+ }
+
+ if ('' != $tmpTagName && '/' == $tmpTagName[0]) {
+ /* This is attempting to close an open tag. We must verify that there exists an
+ * open tag of the same type and that there is no option (options on closing
+ * tags don't make any sense). */
+ $elToClose = $parent->closestParentOfType($actualTagName);
+ if (null == $elToClose || count($options) > 1) {
+ /* Closing an unopened tag or has an option. Treat everything as plain text. */
+ $this->createTextNode($parent, '[');
+ $this->createTextNode($parent, $tagContent);
+ $this->createTextNode($parent, ']');
+ return $parent;
+ } else {
+ /* We're closing $elToClose. In order to do that, we just need to return
+ * $elToClose's parent, since that will change our effective parent to be
+ * elToClose's parent. */
+ return $elToClose->getParent();
+ }
+ }
+
+ /* Verify that this is a known bbcode tag name. */
+ if ('' == $actualTagName || !$this->codeExists($actualTagName, !empty($options))) {
+ /* This is an invalid tag name! Treat everything we've seen as plain text. */
+ $this->createTextNode($parent, '[');
+ $this->createTextNode($parent, $tagContent);
+ $this->createTextNode($parent, ']');
+ return $parent;
+ }
+
+ /* If we're here, this is a valid opening tag. Let's make a new node for it. */
+ $el = new ElementNode();
+ $el->setNodeId(++$this->nextNodeid);
+ $code = $this->getCode($actualTagName, !empty($options));
+ $el->setCodeDefinition($code);
+ if (!empty($options)) {
+ /* We have an attribute we should save. */
+ $el->setAttribute($options);
+ }
+ $parent->addChild($el);
+ return $el;
+ }
+
+ /**
+ * Handles parsing elements whose CodeDefinitions disable parsing of element
+ * contents. This function uses a rolling window of 3 tokens until it finds the
+ * appropriate closing tag or reaches the end of the token stream.
+ *
+ * @param ElementNode $parent the current parent element
+ * @param Tokenizer $tokenizer the tokenizer we're using
+ *
+ * @return ElementNode the new parent element
+ */
+ protected function parseAsTextUntilClose(ElementNode $parent, Tokenizer $tokenizer)
+ {
+ /* $parent's code definition doesn't allow its contents to be parsed. Here we use
+ * a sliding window of three tokens until we find [ /tagname ], signifying the
+ * end of the parent. */
+ if (!$tokenizer->hasNext()) {
+ return $parent;
+ }
+ $prevPrev = $tokenizer->next();
+ if (!$tokenizer->hasNext()) {
+ $this->createTextNode($parent, $prevPrev);
+ return $parent;
+ }
+ $prev = $tokenizer->next();
+ if (!$tokenizer->hasNext()) {
+ $this->createTextNode($parent, $prevPrev);
+ $this->createTextNode($parent, $prev);
+ return $parent;
+ }
+ $curr = $tokenizer->next();
+ while ('[' != $prevPrev || '/'.$parent->getTagName() != strtolower($prev) ||
+ ']' != $curr) {
+ $this->createTextNode($parent, $prevPrev);
+ $prevPrev = $prev;
+ $prev = $curr;
+ if (!$tokenizer->hasNext()) {
+ $this->createTextNode($parent, $prevPrev);
+ $this->createTextNode($parent, $prev);
+ return $parent;
+ }
+ $curr = $tokenizer->next();
+ }
+ }
+
+}
diff --git a/app/jbbcode/ParserException.php b/app/jbbcode/ParserException.php
new file mode 100644
index 0000000..89501cf
--- /dev/null
+++ b/app/jbbcode/ParserException.php
@@ -0,0 +1,7 @@
+value = $val;
+ }
+
+ public function accept(NodeVisitor $visitor)
+ {
+ $visitor->visitTextNode($this);
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see JBBCode.Node::isTextNode()
+ *
+ * returns true
+ */
+ public function isTextNode()
+ {
+ return true;
+ }
+
+ /**
+ * Returns the text string value of this text node.
+ *
+ * @return string
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see JBBCode.Node::getAsText()
+ *
+ * Returns the text representation of this node.
+ *
+ * @return this node represented as text
+ */
+ public function getAsText()
+ {
+ return $this->getValue();
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see JBBCode.Node::getAsBBCode()
+ *
+ * Returns the bbcode representation of this node. (Just its value)
+ *
+ * @return this node represented as bbcode
+ */
+ public function getAsBBCode()
+ {
+ return $this->getValue();
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see JBBCode.Node::getAsHTML()
+ *
+ * Returns the html representation of this node. (Just its value)
+ *
+ * @return this node represented as HTML
+ */
+ public function getAsHTML()
+ {
+ return $this->getValue();
+ }
+
+ /**
+ * Edits the text value contained within this text node.
+ *
+ * @param newValue the new text value of the text node
+ */
+ public function setValue($newValue)
+ {
+ $this->value = $newValue;
+ }
+
+}
diff --git a/app/jbbcode/Tokenizer.php b/app/jbbcode/Tokenizer.php
new file mode 100644
index 0000000..6d47c44
--- /dev/null
+++ b/app/jbbcode/Tokenizer.php
@@ -0,0 +1,105 @@
+tokens, substr($str, $strStart, $index - $strStart));
+ $strStart = $index;
+ }
+
+ /* Add the [ or ] to the tokens array. */
+ array_push($this->tokens, $str[$index]);
+ $strStart = $index+1;
+ }
+ }
+
+ if ($strStart < strlen($str)) {
+ /* There are still characters in the buffer. Add them to the tokens. */
+ array_push($this->tokens, substr($str, $strStart, strlen($str) - $strStart));
+ }
+ }
+
+ /**
+ * Returns true if there is another token in the token stream.
+ */
+ public function hasNext()
+ {
+ return count($this->tokens) > 1 + $this->i;
+ }
+
+ /**
+ * Advances the token stream to the next token and returns the new token.
+ */
+ public function next()
+ {
+ if (!$this->hasNext()) {
+ return null;
+ } else {
+ return $this->tokens[++$this->i];
+ }
+ }
+
+ /**
+ * Retrieves the current token.
+ */
+ public function current()
+ {
+ if ($this->i < 0) {
+ return null;
+ } else {
+ return $this->tokens[$this->i];
+ }
+ }
+
+ /**
+ * Moves the token stream back a token.
+ */
+ public function stepBack()
+ {
+ if ($this->i > -1) {
+ $this->i--;
+ }
+ }
+
+ /**
+ * Restarts the tokenizer, returning to the beginning of the token stream.
+ */
+ public function restart()
+ {
+ $this->i = -1;
+ }
+
+ /**
+ * toString method that returns the entire string from the current index on.
+ */
+ public function toString()
+ {
+ return implode('', array_slice($this->tokens, $this->i + 1));
+ }
+
+}
diff --git a/app/jbbcode/examples/1-GettingStarted.php b/app/jbbcode/examples/1-GettingStarted.php
new file mode 100644
index 0000000..dd0c174
--- /dev/null
+++ b/app/jbbcode/examples/1-GettingStarted.php
@@ -0,0 +1,12 @@
+addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+
+$text = "The default codes include: [b]bold[/b], [i]italics[/i], [u]underlining[/u], ";
+$text .= "[url=http://jbbcode.com]links[/url], [color=red]color![/color] and more.";
+
+$parser->parse($text);
+
+print $parser->getAsHtml();
diff --git a/app/jbbcode/examples/2-ClosingUnclosedTags.php b/app/jbbcode/examples/2-ClosingUnclosedTags.php
new file mode 100644
index 0000000..35ee7fd
--- /dev/null
+++ b/app/jbbcode/examples/2-ClosingUnclosedTags.php
@@ -0,0 +1,10 @@
+addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+
+$text = "The bbcode in here [b]is never closed!";
+$parser->parse($text);
+
+print $parser->getAsBBCode();
diff --git a/app/jbbcode/examples/3-MarkuplessText.php b/app/jbbcode/examples/3-MarkuplessText.php
new file mode 100644
index 0000000..47f20a3
--- /dev/null
+++ b/app/jbbcode/examples/3-MarkuplessText.php
@@ -0,0 +1,11 @@
+addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+
+$text = "[b][u]There is [i]a lot[/i] of [url=http://en.wikipedia.org/wiki/Markup_language]markup[/url] in this";
+$text .= "[color=#333333]text[/color]![/u][/b]";
+$parser->parse($text);
+
+print $parser->getAsText();
diff --git a/app/jbbcode/examples/4-CreatingNewCodes.php b/app/jbbcode/examples/4-CreatingNewCodes.php
new file mode 100644
index 0000000..e8335b0
--- /dev/null
+++ b/app/jbbcode/examples/4-CreatingNewCodes.php
@@ -0,0 +1,7 @@
+addBBCode("quote", '{param}
');
+$parser->addBBCode("code", '{param}
', false, false, 1);
diff --git a/app/jbbcode/examples/SmileyVisitorTest.php b/app/jbbcode/examples/SmileyVisitorTest.php
new file mode 100644
index 0000000..cfea90f
--- /dev/null
+++ b/app/jbbcode/examples/SmileyVisitorTest.php
@@ -0,0 +1,22 @@
+addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+
+if (count($argv) < 2) {
+ die("Usage: " . $argv[0] . " \"bbcode string\"\n");
+}
+
+$inputText = $argv[1];
+
+$parser->parse($inputText);
+
+$smileyVisitor = new \JBBCode\visitors\SmileyVisitor();
+$parser->accept($smileyVisitor);
+
+echo $parser->getAsHTML() . "\n";
diff --git a/app/jbbcode/examples/TagCountingVisitorTest.php b/app/jbbcode/examples/TagCountingVisitorTest.php
new file mode 100644
index 0000000..8ce5d99
--- /dev/null
+++ b/app/jbbcode/examples/TagCountingVisitorTest.php
@@ -0,0 +1,23 @@
+addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+
+if (count($argv) < 3) {
+ die("Usage: " . $argv[0] . " \"bbcode string\" \n");
+}
+
+$inputText = $argv[1];
+$tagName = $argv[2];
+
+$parser->parse($inputText);
+
+$tagCountingVisitor = new \JBBCode\visitors\TagCountingVisitor();
+$parser->accept($tagCountingVisitor);
+
+echo $tagCountingVisitor->getFrequency($tagName) . "\n";
diff --git a/app/jbbcode/tests/BBCodeToBBCodeTest.php b/app/jbbcode/tests/BBCodeToBBCodeTest.php
new file mode 100644
index 0000000..c832fcc
--- /dev/null
+++ b/app/jbbcode/tests/BBCodeToBBCodeTest.php
@@ -0,0 +1,85 @@
+addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->parse($bbcode);
+ return $parser->getAsBBCode();
+ }
+
+ /**
+ * Asserts that the given bbcode matches the given text when
+ * the bbcode is run through defaultBBCodeParse
+ */
+ private function assertBBCodeOutput($bbcode, $text)
+ {
+ $this->assertEquals($this->defaultBBCodeParse($bbcode), $text);
+ }
+
+ public function testEmptyString()
+ {
+ $this->assertBBCodeOutput('', '');
+ }
+
+ public function testOneTag()
+ {
+ $this->assertBBCodeOutput('[b]this is bold[/b]', '[b]this is bold[/b]');
+ }
+
+ public function testOneTagWithSurroundingText()
+ {
+ $this->assertBBCodeOutput('buffer text [b]this is bold[/b] buffer text',
+ 'buffer text [b]this is bold[/b] buffer text');
+ }
+
+ public function testMultipleTags()
+ {
+ $bbcode = 'this is some text with [b]bold tags[/b] and [i]italics[/i] and ' .
+ 'things like [u]that[/u].';
+ $bbcodeOutput = 'this is some text with [b]bold tags[/b] and [i]italics[/i] and ' .
+ 'things like [u]that[/u].';
+ $this->assertBBCodeOutput($bbcode, $bbcodeOutput);
+ }
+
+ public function testCodeOptions()
+ {
+ $code = 'This contains a [url=http://jbbcode.com]url[/url] which uses an option.';
+ $codeOutput = 'This contains a [url=http://jbbcode.com]url[/url] which uses an option.';
+ $this->assertBBCodeOutput($code, $codeOutput);
+ }
+
+ /**
+ * @depends testCodeOptions
+ */
+ public function testOmittedOption()
+ {
+ $code = 'This doesn\'t use the url option [url]http://jbbcode.com[/url].';
+ $codeOutput = 'This doesn\'t use the url option [url]http://jbbcode.com[/url].';
+ $this->assertBBCodeOutput($code, $codeOutput);
+ }
+
+ public function testUnclosedTags()
+ {
+ $code = '[b]bold';
+ $codeOutput = '[b]bold[/b]';
+ $this->assertBBCodeOutput($code, $codeOutput);
+ }
+
+}
diff --git a/app/jbbcode/tests/BBCodeToTextTest.php b/app/jbbcode/tests/BBCodeToTextTest.php
new file mode 100644
index 0000000..193fc7c
--- /dev/null
+++ b/app/jbbcode/tests/BBCodeToTextTest.php
@@ -0,0 +1,78 @@
+addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->parse($bbcode);
+ return $parser->getAsText();
+ }
+
+ /**
+ * Asserts that the given bbcode matches the given text when
+ * the bbcode is run through defaultTextParse
+ */
+ private function assertTextOutput($bbcode, $text)
+ {
+ $this->assertEquals($text, $this->defaultTextParse($bbcode));
+ }
+
+ public function testEmptyString()
+ {
+ $this->assertTextOutput('', '');
+ }
+
+ public function testOneTag()
+ {
+ $this->assertTextOutput('[b]this is bold[/b]', 'this is bold');
+ }
+
+ public function testOneTagWithSurroundingText()
+ {
+ $this->assertTextOutput('buffer text [b]this is bold[/b] buffer text',
+ 'buffer text this is bold buffer text');
+ }
+
+ public function testMultipleTags()
+ {
+ $bbcode = 'this is some text with [b]bold tags[/b] and [i]italics[/i] and ' .
+ 'things like [u]that[/u].';
+ $text = 'this is some text with bold tags and italics and things like that.';
+ $this->assertTextOutput($bbcode, $text);
+ }
+
+ public function testCodeOptions()
+ {
+ $code = 'This contains a [url=http://jbbcode.com]url[/url] which uses an option.';
+ $text = 'This contains a url which uses an option.';
+ $this->assertTextOutput($code, $text);
+ }
+
+ /**
+ * @depends testCodeOptions
+ */
+ public function testOmittedOption()
+ {
+ $code = 'This doesn\'t use the url option [url]http://jbbcode.com[/url].';
+ $text = 'This doesn\'t use the url option http://jbbcode.com.';
+ $this->assertTextOutput($code, $text);
+ }
+
+}
diff --git a/app/jbbcode/tests/DefaultCodesTest.php b/app/jbbcode/tests/DefaultCodesTest.php
new file mode 100644
index 0000000..e933992
--- /dev/null
+++ b/app/jbbcode/tests/DefaultCodesTest.php
@@ -0,0 +1,54 @@
+addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->parse($bbcode);
+ $this->assertEquals($html, $parser->getAsHtml());
+ }
+
+ /**
+ * Tests the [b] bbcode.
+ */
+ public function testBold()
+ {
+ $this->assertProduces('[b]this should be bold[/b]', 'this should be bold');
+ }
+
+ /**
+ * Tests the [color] bbcode.
+ */
+ public function testColor()
+ {
+ $this->assertProduces('[color=red]red[/color]', 'red');
+ }
+
+ /**
+ * Tests the example from the documentation.
+ */
+ public function testExample()
+ {
+ $text = "The default codes include: [b]bold[/b], [i]italics[/i], [u]underlining[/u], ";
+ $text .= "[url=http://jbbcode.com]links[/url], [color=red]color![/color] and more.";
+ $html = 'The default codes include: bold, italics, underlining, ';
+ $html .= 'links, color! and more.';
+ $this->assertProduces($text, $html);
+ }
+
+}
diff --git a/app/jbbcode/tests/HTMLSafeTest.php b/app/jbbcode/tests/HTMLSafeTest.php
new file mode 100644
index 0000000..bd9391b
--- /dev/null
+++ b/app/jbbcode/tests/HTMLSafeTest.php
@@ -0,0 +1,77 @@
+addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->parse($bbcode);
+
+ $htmlsafer = new JBBCode\visitors\HTMLSafeVisitor();
+ $parser->accept($htmlsafer);
+
+ $this->assertEquals($html, $parser->getAsHtml());
+ }
+
+ /**
+ * Tests escaping quotes and ampersands in simple text
+ */
+ public function testQuoteAndAmp()
+ {
+ $this->assertProduces('te"xt te&xt', 'te"xt te&xt');
+ }
+
+ /**
+ * Tests escaping quotes and ampersands inside a BBCode tag
+ */
+ public function testQuoteAndAmpInTag()
+ {
+ $this->assertProduces('[b]te"xt te&xt[/b]', 'te"xt te&xt');
+ }
+
+ /**
+ * Tests escaping HTML tags
+ */
+ public function testHtmlTag()
+ {
+ $this->assertProduces('not bold', '<b>not bold</b>');
+ $this->assertProduces('[b]bold[/b]
', '<b>bold</b> <hr>');
+ }
+
+ /**
+ * Tests escaping ampersands in URL using [url]...[/url]
+ */
+ public function testUrlParam()
+ {
+ $this->assertProduces('text [url]http://example.com/?a=b&c=d[/url] more text', 'text http://example.com/?a=b&c=d more text');
+ }
+
+ /**
+ * Tests escaping ampersands in URL using [url=...] tag
+ */
+ public function testUrlOption()
+ {
+ $this->assertProduces('text [url=http://example.com/?a=b&c=d]this is a "link"[/url]', 'text this is a "link"');
+ }
+
+ /**
+ * Tests escaping ampersands in URL using [url=...] tag when URL is in quotes
+ */
+ public function testUrlOptionQuotes()
+ {
+ $this->assertProduces('text [url="http://example.com/?a=b&c=d"]this is a "link"[/url]', 'text this is a "link"');
+ }
+
+}
diff --git a/app/jbbcode/tests/NestLimitTest.php b/app/jbbcode/tests/NestLimitTest.php
new file mode 100644
index 0000000..d826fef
--- /dev/null
+++ b/app/jbbcode/tests/NestLimitTest.php
@@ -0,0 +1,46 @@
+addBBCode('b', '{param}', false, true, -1);
+ $parser->parse('[b][b][b][b][b][b][b][b]bold text[/b][/b][/b][/b][/b][/b][/b][/b]');
+ $this->assertEquals('' .
+ 'bold text' .
+ '',
+ $parser->getAsHtml());
+ }
+
+ /**
+ * Test over nesting.
+ */
+ public function testOverNesting()
+ {
+ $parser = new JBBCode\Parser();
+ $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->addBBCode('quote', '{param}
', false, true, 2);
+ $bbcode = '[quote][quote][quote]wut[/quote] huh?[/quote] i don\'t know[/quote]';
+ $parser->parse($bbcode);
+ $expectedBbcode = '[quote][quote] huh?[/quote] i don\'t know[/quote]';
+ $expectedHtml = ' huh?
i don\'t know
';
+ $this->assertEquals($expectedBbcode, $parser->getAsBBCode());
+ $this->assertEquals($expectedHtml, $parser->getAsHtml());
+ }
+
+}
diff --git a/app/jbbcode/tests/ParseContentTest.php b/app/jbbcode/tests/ParseContentTest.php
new file mode 100644
index 0000000..1ea2c78
--- /dev/null
+++ b/app/jbbcode/tests/ParseContentTest.php
@@ -0,0 +1,97 @@
+addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->addBBCode('verbatim', '{param}', false, false);
+
+ $parser->parse('[verbatim]plain text[/verbatim]');
+ $this->assertEquals('plain text', $parser->getAsHtml());
+
+ $parser->parse('[verbatim][b]bold[/b][/verbatim]');
+ $this->assertEquals('[b]bold[/b]', $parser->getAsHtml());
+
+ }
+
+ public function testNoParsingWithBufferText()
+ {
+
+ $parser = new JBBCode\Parser();
+ $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->addBBCode('verbatim', '{param}', false, false);
+
+ $parser->parse('buffer text[verbatim]buffer text[b]bold[/b]buffer text[/verbatim]buffer text');
+ $this->assertEquals('buffer textbuffer text[b]bold[/b]buffer textbuffer text', $parser->getAsHtml());
+ }
+
+ /**
+ * Tests that when a tag is not closed within an unparseable tag,
+ * the BBCode output does not automatically close that tag (because
+ * the contents were not parsed).
+ */
+ public function testUnclosedTag()
+ {
+
+ $parser = new JBBCode\Parser();
+ $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->addBBCode('verbatim', '{param}', false, false);
+
+ $parser->parse('[verbatim]i wonder [b]what will happen[/verbatim]');
+ $this->assertEquals('i wonder [b]what will happen', $parser->getAsHtml());
+ $this->assertEquals('[verbatim]i wonder [b]what will happen[/verbatim]', $parser->getAsBBCode());
+ }
+
+ /**
+ * Tests that an unclosed tag with parseContent = false ends cleanly.
+ */
+ public function testUnclosedVerbatimTag()
+ {
+ $parser = new JBBCode\Parser();
+ $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->addBBCode('verbatim', '{param}', false, false);
+
+ $parser->parse('[verbatim]yo this [b]text should not be bold[/b]');
+ $this->assertEquals('yo this [b]text should not be bold[/b]', $parser->getAsHtml());
+ }
+
+ /**
+ * Tests a malformed closing tag for a verbatim block.
+ */
+ public function testMalformedVerbatimClosingTag()
+ {
+ $parser = new JBBCode\Parser();
+ $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->addBBCode('verbatim', '{param}', false, false);
+ $parser->parse('[verbatim]yo this [b]text should not be bold[/b][/verbatim');
+ $this->assertEquals('yo this [b]text should not be bold[/b][/verbatim', $parser->getAsHtml());
+ }
+
+ /**
+ * Tests an immediate end after a verbatim.
+ */
+ public function testVerbatimThenEof()
+ {
+ $parser = new JBBCode\Parser();
+ $parser->addBBCode('verbatim', '{param}', false, false);
+ $parser->parse('[verbatim]');
+ $this->assertEquals('', $parser->getAsHtml());
+ }
+
+}
diff --git a/app/jbbcode/tests/ParsingEdgeCaseTest.php b/app/jbbcode/tests/ParsingEdgeCaseTest.php
new file mode 100644
index 0000000..a08f713
--- /dev/null
+++ b/app/jbbcode/tests/ParsingEdgeCaseTest.php
@@ -0,0 +1,130 @@
+addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->parse($bbcode);
+ return $parser->getAsHtml();
+ }
+
+ /**
+ * Asserts that the given bbcode matches the given html when
+ * the bbcode is run through defaultParse.
+ */
+ private function assertProduces($bbcode, $html)
+ {
+ $this->assertEquals($html, $this->defaultParse($bbcode));
+ }
+
+ /**
+ * Tests attempting to use a code that doesn't exist.
+ */
+ public function testNonexistentCodeMalformed()
+ {
+ $this->assertProduces('[wat]', '[wat]');
+ }
+
+ /**
+ * Tests attempting to use a code that doesn't exist, but this
+ * time in a well-formed fashion.
+ *
+ * @depends testNonexistentCodeMalformed
+ */
+ public function testNonexistentCodeWellformed()
+ {
+ $this->assertProduces('[wat]something[/wat]', '[wat]something[/wat]');
+ }
+
+ /**
+ * Tests a whole bunch of meaningless left brackets.
+ */
+ public function testAllLeftBrackets()
+ {
+ $this->assertProduces('[[[[[[[[', '[[[[[[[[');
+ }
+
+ /**
+ * Tests a whole bunch of meaningless right brackets.
+ */
+ public function testAllRightBrackets()
+ {
+ $this->assertProduces(']]]]]', ']]]]]');
+ }
+
+ /**
+ * Intermixes well-formed, meaningful tags with meaningless brackets.
+ */
+ public function testRandomBracketsInWellformedCode()
+ {
+ $this->assertProduces('[b][[][[i]heh[/i][/b]',
+ '[[][heh');
+ }
+
+ /**
+ * Tests an unclosed tag within a closed tag.
+ */
+ public function testUnclosedWithinClosed()
+ {
+ $this->assertProduces('[url=http://jbbcode.com][b]oh yeah[/url]',
+ 'oh yeah');
+ }
+
+ /**
+ * Tests half completed opening tag.
+ */
+ public function testHalfOpenTag()
+ {
+ $this->assertProduces('[b', '[b');
+ $this->assertProduces('wut [url=http://jbbcode.com',
+ 'wut [url=http://jbbcode.com');
+ }
+
+ /**
+ * Tests half completed closing tag.
+ */
+ public function testHalfClosingTag()
+ {
+ $this->assertProduces('[b]this should be bold[/b',
+ 'this should be bold[/b');
+ }
+
+ /**
+ * Tests lots of left brackets before the actual tag. For example:
+ * [[[[[[[[b]bold![/b]
+ */
+ public function testLeftBracketsThenTag()
+ {
+ $this->assertProduces('[[[[[b]bold![/b]',
+ '[[[[bold!');
+ }
+
+ /**
+ * Tests a whitespace after left bracket.
+ */
+ public function testWhitespaceAfterLeftBracketWhithoutTag()
+ {
+ $this->assertProduces('[ ABC ] ',
+ '[ ABC ] ');
+ }
+
+}
diff --git a/app/jbbcode/tests/SimpleEvaluationTest.php b/app/jbbcode/tests/SimpleEvaluationTest.php
new file mode 100644
index 0000000..65fb236
--- /dev/null
+++ b/app/jbbcode/tests/SimpleEvaluationTest.php
@@ -0,0 +1,131 @@
+addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->parse($bbcode);
+ return $parser->getAsHtml();
+ }
+
+ /**
+ * Asserts that the given bbcode matches the given html when
+ * the bbcode is run through defaultParse.
+ */
+ private function assertProduces($bbcode, $html)
+ {
+ $this->assertEquals($html, $this->defaultParse($bbcode));
+ }
+
+
+ public function testEmptyString()
+ {
+ $this->assertProduces('', '');
+ }
+
+ public function testOneTag()
+ {
+ $this->assertProduces('[b]this is bold[/b]', 'this is bold');
+ }
+
+ public function testOneTagWithSurroundingText()
+ {
+ $this->assertProduces('buffer text [b]this is bold[/b] buffer text',
+ 'buffer text this is bold buffer text');
+ }
+
+ public function testMultipleTags()
+ {
+ $bbcode = <<bold tags and italics and
+things like that.
+EOD;
+ $this->assertProduces($bbcode, $html);
+ }
+
+ public function testCodeOptions()
+ {
+ $code = 'This contains a [url=http://jbbcode.com/?b=2]url[/url] which uses an option.';
+ $html = 'This contains a url which uses an option.';
+ $this->assertProduces($code, $html);
+ }
+
+ public function testAttributes()
+ {
+ $parser = new JBBCode\Parser();
+ $builder = new JBBCode\CodeDefinitionBuilder('img', '
');
+ $parser->addCodeDefinition($builder->setUseOption(true)->setParseContent(false)->build());
+
+ $expected = 'Multiple
options.';
+
+ $code = 'Multiple [img height="50" alt="alt text"]http://jbbcode.com/img.png[/img] options.';
+ $parser->parse($code);
+ $result = $parser->getAsHTML();
+ $this->assertEquals($expected, $result);
+
+ $code = 'Multiple [img height=50 alt="alt text"]http://jbbcode.com/img.png[/img] options.';
+ $parser->parse($code);
+ $result = $parser->getAsHTML();
+ $this->assertEquals($expected, $result);
+ }
+
+ /**
+ * @depends testCodeOptions
+ */
+ public function testOmittedOption()
+ {
+ $code = 'This doesn\'t use the url option [url]http://jbbcode.com[/url].';
+ $html = 'This doesn\'t use the url option http://jbbcode.com.';
+ $this->assertProduces($code, $html);
+ }
+
+ public function testUnclosedTag()
+ {
+ $code = 'hello [b]world';
+ $html = 'hello world';
+ $this->assertProduces($code, $html);
+ }
+
+ public function testNestingTags()
+ {
+ $code = '[url=http://jbbcode.com][b]hello [u]world[/u][/b][/url]';
+ $html = 'hello world';
+ $this->assertProduces($code, $html);
+ }
+
+ public function testBracketInTag()
+ {
+ $this->assertProduces('[b]:-[[/b]', ':-[');
+ }
+
+ public function testBracketWithSpaceInTag()
+ {
+ $this->assertProduces('[b]:-[ [/b]', ':-[ ');
+ }
+
+ public function testBracketWithTextInTag()
+ {
+ $this->assertProduces('[b]:-[ foobar[/b]', ':-[ foobar');
+ }
+
+ public function testMultibleBracketsWithTextInTag()
+ {
+ $this->assertProduces('[b]:-[ [fo[o[bar[/b]', ':-[ [fo[o[bar');
+ }
+
+}
diff --git a/app/jbbcode/tests/TokenizerTest.php b/app/jbbcode/tests/TokenizerTest.php
new file mode 100644
index 0000000..a5431d3
--- /dev/null
+++ b/app/jbbcode/tests/TokenizerTest.php
@@ -0,0 +1,74 @@
+assertFalse($tokenizer->hasNext());
+ }
+
+ public function testPlainTextOnly()
+ {
+ $tokenizer = new JBBCode\Tokenizer('this is some plain text.');
+ $this->assertEquals('this is some plain text.', $tokenizer->next());
+ $this->assertEquals('this is some plain text.', $tokenizer->current());
+ $this->assertFalse($tokenizer->hasNext());
+ }
+
+ public function testStartingBracket()
+ {
+ $tokenizer = new JBBCode\Tokenizer('[this has a starting bracket.');
+ $this->assertEquals('[', $tokenizer->next());
+ $this->assertEquals('[', $tokenizer->current());
+ $this->assertEquals('this has a starting bracket.', $tokenizer->next());
+ $this->assertEquals('this has a starting bracket.', $tokenizer->current());
+ $this->assertFalse($tokenizer->hasNext());
+ $this->assertEquals(null, $tokenizer->next());
+ }
+
+ public function testOneTag()
+ {
+ $tokenizer = new JBBCode\Tokenizer('[b]');
+ $this->assertEquals('[', $tokenizer->next());
+ $this->assertEquals('b', $tokenizer->next());
+ $this->assertEquals(']', $tokenizer->next());
+ $this->assertFalse($tokenizer->hasNext());
+ }
+
+ public function testMatchingTags()
+ {
+ $tokenizer = new JBBCode\Tokenizer('[url]http://jbbcode.com[/url]');
+ $this->assertEquals('[', $tokenizer->next());
+ $this->assertEquals('url', $tokenizer->next());
+ $this->assertEquals(']', $tokenizer->next());
+ $this->assertEquals('http://jbbcode.com', $tokenizer->next());
+ $this->assertEquals('[', $tokenizer->next());
+ $this->assertEquals('/url', $tokenizer->next());
+ $this->assertEquals(']', $tokenizer->next());
+ $this->assertFalse($tokenizer->hasNext());
+ }
+
+ public function testLotsOfBrackets()
+ {
+ $tokenizer = new JBBCode\Tokenizer('[[][]][');
+ $this->assertEquals('[', $tokenizer->next());
+ $this->assertEquals('[', $tokenizer->next());
+ $this->assertEquals(']', $tokenizer->next());
+ $this->assertEquals('[', $tokenizer->next());
+ $this->assertEquals(']', $tokenizer->next());
+ $this->assertEquals(']', $tokenizer->next());
+ $this->assertEquals('[', $tokenizer->next());
+ $this->assertFalse($tokenizer->hasNext());
+ }
+
+}
diff --git a/app/jbbcode/tests/ValidatorTest.php b/app/jbbcode/tests/ValidatorTest.php
new file mode 100644
index 0000000..e7dacb0
--- /dev/null
+++ b/app/jbbcode/tests/ValidatorTest.php
@@ -0,0 +1,151 @@
+assertFalse($urlValidator->validate('#yolo#swag'));
+ $this->assertFalse($urlValidator->validate('giehtiehwtaw352353%3'));
+ }
+
+ /**
+ * Tests a valid url directly on the UrlValidator.
+ */
+ public function testValidUrl()
+ {
+ $urlValidator = new \JBBCode\validators\UrlValidator();
+ $this->assertTrue($urlValidator->validate('http://google.com'));
+ $this->assertTrue($urlValidator->validate('http://jbbcode.com/docs'));
+ $this->assertTrue($urlValidator->validate('https://www.maps.google.com'));
+ }
+
+ /**
+ * Tests an invalid url as an option to a url bbcode.
+ *
+ * @depends testInvalidUrl
+ */
+ public function testInvalidOptionUrlBBCode()
+ {
+ $parser = new JBBCode\Parser();
+ $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->parse('[url=javascript:alert("HACKED!");]click me[/url]');
+ $this->assertEquals('[url=javascript:alert("HACKED!");]click me[/url]',
+ $parser->getAsHtml());
+ }
+
+ /**
+ * Tests an invalid url as the body to a url bbcode.
+ *
+ * @depends testInvalidUrl
+ */
+ public function testInvalidBodyUrlBBCode()
+ {
+ $parser = new JBBCode\Parser();
+ $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->parse('[url]javascript:alert("HACKED!");[/url]');
+ $this->assertEquals('[url]javascript:alert("HACKED!");[/url]', $parser->getAsHtml());
+ }
+
+ /**
+ * Tests a valid url as the body to a url bbcode.
+ *
+ * @depends testValidUrl
+ */
+ public function testValidUrlBBCode()
+ {
+ $parser = new JBBCode\Parser();
+ $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
+ $parser->parse('[url]http://jbbcode.com[/url]');
+ $this->assertEquals('http://jbbcode.com',
+ $parser->getAsHtml());
+ }
+
+ /**
+ * Tests valid english CSS color descriptions on the CssColorValidator.
+ */
+ public function testCssColorEnglish()
+ {
+ $colorValidator = new JBBCode\validators\CssColorValidator();
+ $this->assertTrue($colorValidator->validate('red'));
+ $this->assertTrue($colorValidator->validate('yellow'));
+ $this->assertTrue($colorValidator->validate('LightGoldenRodYellow'));
+ }
+
+ /**
+ * Tests valid hexadecimal CSS color values on the CssColorValidator.
+ */
+ public function testCssColorHex()
+ {
+ $colorValidator = new JBBCode\validators\CssColorValidator();
+ $this->assertTrue($colorValidator->validate('#000'));
+ $this->assertTrue($colorValidator->validate('#ff0000'));
+ $this->assertTrue($colorValidator->validate('#aaaaaa'));
+ }
+
+ /**
+ * Tests valid rgba CSS color values on the CssColorValidator.
+ */
+ public function testCssColorRgba()
+ {
+ $colorValidator = new JBBCode\validators\CssColorValidator();
+ $this->assertTrue($colorValidator->validate('rgba(255, 0, 0, 0.5)'));
+ $this->assertTrue($colorValidator->validate('rgba(50, 50, 50, 0.0)'));
+ }
+
+ /**
+ * Tests invalid CSS color values on the CssColorValidator.
+ */
+ public function testInvalidCssColor()
+ {
+ $colorValidator = new JBBCode\validators\CssColorValidator();
+ $this->assertFalse($colorValidator->validate('" onclick="javascript: alert(\"gotcha!\");'));
+ $this->assertFalse($colorValidator->validate('">