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', '{option}'); + $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', '{alt}'); + $parser->addCodeDefinition($builder->setUseOption(true)->setParseContent(false)->build()); + + $expected = 'Multiple alt text 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('">colorful text', + $parser->getAsHtml()); + $parser->parse('[color=#00ff00]green[/color]'); + $this->assertEquals('green', $parser->getAsHtml()); + } + + /** + * Tests invalid css colors in a color bbcode. + * + * @depends testInvalidCssColor + */ + public function testInvalidColorBBCode() + { + $parser = new JBBCode\Parser(); + $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); + $parser->parse('[color=" onclick="alert(\'hey ya!\');]click me[/color]'); + $this->assertEquals('[color=" onclick="alert(\'hey ya!\');]click me[/color]', + $parser->getAsHtml()); + } + +} diff --git a/app/jbbcode/validators/CssColorValidator.php b/app/jbbcode/validators/CssColorValidator.php new file mode 100644 index 0000000..ce51fa4 --- /dev/null +++ b/app/jbbcode/validators/CssColorValidator.php @@ -0,0 +1,30 @@ +getChildren() as $child) { + $child->accept($this); + } + } + + public function visitTextNode(\JBBCode\TextNode $textNode) + { + $textNode->setValue($this->htmlSafe($textNode->getValue())); + } + + public function visitElementNode(\JBBCode\ElementNode $elementNode) + { + $attrs = $elementNode->getAttribute(); + if (is_array($attrs)) + { + foreach ($attrs as &$el) + $el = $this->htmlSafe($el); + + $elementNode->setAttribute($attrs); + } + + foreach ($elementNode->getChildren() as $child) { + $child->accept($this); + } + } + + protected function htmlSafe($str, $options = null) + { + if (is_null($options)) + { + if (defined('ENT_DISALLOWED')) + $options = ENT_QUOTES | ENT_DISALLOWED | ENT_HTML401; // PHP 5.4+ + else + $options = ENT_QUOTES; // PHP 5.3 + } + + return htmlspecialchars($str, $options, 'UTF-8'); + } +} diff --git a/app/jbbcode/visitors/NestLimitVisitor.php b/app/jbbcode/visitors/NestLimitVisitor.php new file mode 100644 index 0000000..f550dd0 --- /dev/null +++ b/app/jbbcode/visitors/NestLimitVisitor.php @@ -0,0 +1,65 @@ +getChildren() as $child) { + $child->accept($this); + } + } + + public function visitTextNode(\JBBCode\TextNode $textNode) + { + /* Nothing to do. Text nodes don't have tag names or children. */ + } + + public function visitElementNode(\JBBCode\ElementNode $elementNode) + { + $tagName = strtolower($elementNode->getTagName()); + + /* Update the current depth for this tag name. */ + if (isset($this->depth[$tagName])) { + $this->depth[$tagName]++; + } else { + $this->depth[$tagName] = 1; + } + + /* Check if $elementNode is nested too deeply. */ + if ($elementNode->getCodeDefinition()->getNestLimit() != -1 && + $elementNode->getCodeDefinition()->getNestLimit() < $this->depth[$tagName]) { + /* This element is nested too deeply. We need to remove it and not visit any + * of its children. */ + $elementNode->getParent()->removeChild($elementNode); + } else { + /* This element is not nested too deeply. Visit all of its children. */ + foreach ($elementNode->getChildren() as $child) { + $child->accept($this); + } + } + + /* Now that we're done visiting this node, decrement the depth. */ + $this->depth[$tagName]--; + } + +} diff --git a/app/jbbcode/visitors/SmileyVisitor.php b/app/jbbcode/visitors/SmileyVisitor.php new file mode 100644 index 0000000..16fb22a --- /dev/null +++ b/app/jbbcode/visitors/SmileyVisitor.php @@ -0,0 +1,42 @@ +getChildren() as $child) { + $child->accept($this); + } + } + + function visitTextNode(\JBBCode\TextNode $textNode) + { + /* Convert :) into an image tag. */ + $textNode->setValue(str_replace(':)', + ':)', + $textNode->getValue())); + } + + function visitElementNode(\JBBCode\ElementNode $elementNode) + { + /* We only want to visit text nodes within elements if the element's + * code definition allows for its content to be parsed. + */ + if ($elementNode->getCodeDefinition()->parseContent()) { + foreach ($elementNode->getChildren() as $child) { + $child->accept($this); + } + } + } + +} diff --git a/app/jbbcode/visitors/TagCountingVisitor.php b/app/jbbcode/visitors/TagCountingVisitor.php new file mode 100644 index 0000000..3e52b43 --- /dev/null +++ b/app/jbbcode/visitors/TagCountingVisitor.php @@ -0,0 +1,60 @@ +getChildren() as $child) { + $child->accept($this); + } + } + + public function visitTextNode(\JBBCode\TextNode $textNode) + { + // Nothing to do here, text nodes do not have tag names or children + } + + public function visitElementNode(\JBBCode\ElementNode $elementNode) + { + $tagName = strtolower($elementNode->getTagName()); + + // Update this tag name's frequency + if (isset($this->frequencies[$tagName])) { + $this->frequencies[$tagName]++; + } else { + $this->frequencies[$tagName] = 1; + } + + // Visit all the node's childrens + foreach ($elementNode->getChildren() as $child) { + $child->accept($this); + } + + } + + /** + * Retrieves the frequency of the given tag name. + * + * @param $tagName the tag name to look up + */ + public function getFrequency($tagName) + { + if (!isset($this->frequencies[$tagName])) { + return 0; + } else { + return $this->frequencies[$tagName]; + } + } + +} diff --git a/app/post.class.php b/app/post.class.php index d768e76..cbd02af 100644 --- a/app/post.class.php +++ b/app/post.class.php @@ -7,35 +7,74 @@ class Post throw new Exception(__("You need to be logged in to perform this action.")); } } - + private static function parse_content($c){ - //$c = htmlentities($c); + require_once APP_PATH."jbbcode/Parser.php"; + + $parser = new JBBCode\Parser(); + $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); - // Highlight if(Config::get("highlight")){ - $c = preg_replace_callback('/\[code(?:=([^\[]+))?\]((.|\s)+?)(?:(?=\[\/code\]))\[\/code\]/m', function($m){ - return ''.htmlentities(trim($m[2])).''; - }, $c); - } else { - // Links - $c = preg_replace('/\"([^\"]+)\"/i', "„$1\"", $c); + $c = str_replace("\t", " ", $c); + $c = preg_replace("/\[(\/?)code(=(?:[^\[]+))?\]\s*?(?:\n|\r)?/i", '[$1code$2]', $c); + + // Add code definiton + $parser->addCodeDefinition(new class extends \JBBCode\CodeDefinition { + public function __construct($useOption){ + parent::__construct($useOption); + $this->setTagName("code"); + $this->setParseContent(false); + $this->setUseOption(true); + } + + public function asHtml(\JBBCode\ElementNode $el){ + $content = $this->getContent($el); + return ''.htmlentities($content).''; + } + }); } + + if(($tags = Config::get_safe("bbtags", [])) && !empty($tags)){ + foreach($tags as $tag => $content){ + $builder = new JBBCode\CodeDefinitionBuilder($tag, $content); + $parser->addCodeDefinition($builder->build()); + } + } + + $parser->parse($c); + + // Visit every text node + $parser->accept(new class implements \JBBCode\NodeVisitor{ + function visitDocumentElement(\JBBCode\DocumentElement $documentElement){ + foreach($documentElement->getChildren() as $child) { + $child->accept($this); + } + } - $c = preg_replace('/(https?\:\/\/[^\" \n]+)/i', "\\0", $c); - //$c = preg_replace('/(\#([A-Za-z0-9-_]+))/i', "\\0", $c); - $c = preg_replace('/(\#[A-Za-z0-9-_]+)/i', "\\0", $c); + function visitTextNode(\JBBCode\TextNode $textNode){ + $c = $textNode->getValue(); + $c = preg_replace('/\"([^\"]+)\"/i', "„$1\"", $c); + $c = htmlentities($c); + $c = preg_replace('/\*([^\*]+)\*/i', "$1", $c); + $c = preg_replace('/(https?\:\/\/[^\" \n]+)/i', "\\0", $c); + $c = preg_replace('/(\#[A-Za-z0-9-_]+)/i', "\\0", $c); + $c = nl2br($c); + $textNode->setValue($c); + } - ////Headlines - //$c = preg_replace('/^\# (.*)$/m', "

$1

", $c); - //$c = preg_replace('/^\#\# (.*)$/m', "

$1

", $c); - //$c = preg_replace('/^\#\#\# (.*)$/m', "

$1

", $c); - - //$c = preg_replace('/\"([^\"]+)\"/i', "„ $1 “", $c); - $c = preg_replace('/\*([^\*]+)\*/i', "$1", $c); - - $c = nl2br($c); - - return $c; + function visitElementNode(\JBBCode\ElementNode $elementNode){ + /* We only want to visit text nodes within elements if the element's + * code definition allows for its content to be parsed. + */ + if ($elementNode->getCodeDefinition()->parseContent()) { + foreach ($elementNode->getChildren() as $child) { + $child->accept($this); + } + } + } + }); + + return $parser->getAsHtml(); } private static function raw_data($raw_input){ diff --git a/config.ini b/config.ini index 09d507f..bc14c10 100644 --- a/config.ini +++ b/config.ini @@ -23,6 +23,8 @@ highlight = true ;styles[] = static/styles/custom2.css ;scripts = static/styles/scripts.css +bbtags[quote] = "{param}" + [login] force_login = true nick = demo diff --git a/static/styles/design.css b/static/styles/design.css index 76cc80f..7c63233 100644 --- a/static/styles/design.css +++ b/static/styles/design.css @@ -934,5 +934,6 @@ body > .error { code { overflow: auto; - white-space: nowrap; + white-space: pre; + margin: 5px 0; } \ No newline at end of file