├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── JBBCode ├── CodeDefinition.php ├── CodeDefinitionBuilder.php ├── CodeDefinitionSet.php ├── DefaultCodeDefinitionSet.php ├── DocumentElement.php ├── ElementNode.php ├── InputValidator.php ├── Node.php ├── NodeVisitor.php ├── Parser.php ├── TextNode.php ├── Tokenizer.php ├── examples │ ├── 1-GettingStarted.php │ ├── 2-ClosingUnclosedTags.php │ ├── 3-MarkuplessText.php │ ├── 4-CreatingNewCodes.php │ ├── SmileyVisitorTest.php │ └── TagCountingVisitorTest.php ├── tests │ ├── CodeDefinitionBuilderTest.php │ ├── DefaultCodeDefinitionSetTest.php │ ├── DocumentElementTest.php │ ├── ElementNodeTest.php │ ├── ParseContentTest.php │ ├── ParserTest.php │ ├── ParsingEdgeCaseTest.php │ ├── SimpleEvaluationTest.php │ ├── TextNodeTest.php │ ├── TokenizerTest.php │ ├── bootstrap.php │ ├── validators │ │ ├── CssColorValidatorTest.php │ │ ├── FnValidatorTest.php │ │ ├── UrlValidatorTest.php │ │ └── ValidatorTest.php │ └── visitors │ │ ├── HTMLSafeVisitorTest.php │ │ ├── NestLimitVisitorTest.php │ │ ├── SmileyVisitorTest.php │ │ └── TagCountingVisitorTest.php ├── validators │ ├── CssColorValidator.php │ ├── FnValidator.php │ └── UrlValidator.php └── visitors │ ├── HTMLSafeVisitor.php │ ├── NestLimitVisitor.php │ ├── SmileyVisitor.php │ └── TagCountingVisitor.php ├── LICENSE.md ├── README.md ├── composer.json └── phpunit.xml.dist /.coveralls.yml: -------------------------------------------------------------------------------- 1 | src_dir: . 2 | coverage_clover: clover.xml 3 | json_path: clover.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | clover.xml 4 | clover.json 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.6 4 | - hhvm 5 | - nightly 6 | 7 | matrix: 8 | fast_finish: true 9 | allow_failures: 10 | - php: hhvm 11 | - php: nightly 12 | 13 | git: 14 | depth: 10 15 | 16 | cache: 17 | directories: 18 | - vendor 19 | - $HOME/.composer/cache 20 | 21 | sudo: false 22 | 23 | install: 24 | - composer self-update 25 | - composer install --prefer-source --no-interaction 26 | 27 | after_success: 28 | - php vendor/bin/coveralls -v 29 | -------------------------------------------------------------------------------- /JBBCode/CodeDefinition.php: -------------------------------------------------------------------------------- 1 | elCounter = 0; 47 | $def->setTagName($tagName); 48 | $def->setReplacementText($replacementText); 49 | $def->useOption = $useOption; 50 | $def->parseContent = $parseContent; 51 | $def->nestLimit = $nestLimit; 52 | $def->optionValidator = $optionValidator; 53 | $def->bodyValidator = $bodyValidator; 54 | return $def; 55 | } 56 | 57 | /** 58 | * Constructs a new CodeDefinition. 59 | * 60 | * This constructor is deprecated. You should use the static construct() method or the 61 | * CodeDefinitionBuilder class to construct a new CodeDefiniton. 62 | * 63 | * @deprecated 64 | */ 65 | public function __construct() 66 | { 67 | /* WARNING: This function is deprecated and will be made protected in a future 68 | * version of jBBCode. */ 69 | $this->parseContent = true; 70 | $this->useOption = false; 71 | $this->nestLimit = -1; 72 | $this->elCounter = 0; 73 | $this->optionValidator = array(); 74 | $this->bodyValidator = null; 75 | } 76 | 77 | /** 78 | * Determines if the arguments to the given element are valid based on 79 | * any validators attached to this CodeDefinition. 80 | * 81 | * @param ElementNode $el the ElementNode to validate 82 | * @return boolean true if the ElementNode's {option} and {param} are OK, false if they're not 83 | */ 84 | public function hasValidInputs(ElementNode $el) 85 | { 86 | if ($this->usesOption() && $this->optionValidator) { 87 | $att = $el->getAttribute(); 88 | 89 | foreach ($att as $name => $value) { 90 | if (isset($this->optionValidator[$name]) && !$this->optionValidator[$name]->validate($value)) { 91 | return false; 92 | } 93 | } 94 | } 95 | 96 | if (!$this->parseContent() && $this->bodyValidator) { 97 | /* We only evaluate the content if we're not parsing the content. */ 98 | $content = ""; 99 | foreach ($el->getChildren() as $child) { 100 | $content .= $child->getAsBBCode(); 101 | } 102 | if (!$this->bodyValidator->validate($content)) { 103 | /* The content of the element is not valid. */ 104 | return false; 105 | } 106 | } 107 | 108 | return true; 109 | } 110 | 111 | /** 112 | * Accepts an ElementNode that is defined by this CodeDefinition and returns the HTML 113 | * markup of the element. This is a commonly overridden class for custom CodeDefinitions 114 | * so that the content can be directly manipulated. 115 | * 116 | * @param ElementNode $el the element to return an html representation of 117 | * 118 | * @return string the parsed html of this element (INCLUDING ITS CHILDREN) 119 | */ 120 | public function asHtml(ElementNode $el) 121 | { 122 | if (!$this->hasValidInputs($el)) { 123 | return $el->getAsBBCode(); 124 | } 125 | 126 | $html = $this->getReplacementText(); 127 | 128 | if ($this->usesOption()) { 129 | $options = $el->getAttribute(); 130 | if (count($options)==1) { 131 | $vals = array_values($options); 132 | $html = str_ireplace('{option}', reset($vals), $html); 133 | } else { 134 | foreach ($options as $key => $val) { 135 | $html = str_ireplace('{' . $key . '}', $val, $html); 136 | } 137 | } 138 | } 139 | 140 | $content = $this->getContent($el); 141 | 142 | $html = str_ireplace('{param}', $content, $html); 143 | 144 | return $html; 145 | } 146 | 147 | protected function getContent(ElementNode $el) 148 | { 149 | if ($this->parseContent()) { 150 | $content = ""; 151 | foreach ($el->getChildren() as $child) { 152 | $content .= $child->getAsHTML(); 153 | } 154 | } else { 155 | $content = ""; 156 | foreach ($el->getChildren() as $child) { 157 | $content .= $child->getAsBBCode(); 158 | } 159 | } 160 | return $content; 161 | } 162 | 163 | /** 164 | * Accepts an ElementNode that is defined by this CodeDefinition and returns the text 165 | * representation of the element. This may be overridden by a custom CodeDefinition. 166 | * 167 | * @param ElementNode $el the element to return a text representation of 168 | * 169 | * @return string the text representation of $el 170 | */ 171 | public function asText(ElementNode $el) 172 | { 173 | if (!$this->hasValidInputs($el)) { 174 | return $el->getAsBBCode(); 175 | } 176 | 177 | $s = ""; 178 | foreach ($el->getChildren() as $child) { 179 | $s .= $child->getAsText(); 180 | } 181 | return $s; 182 | } 183 | 184 | /** 185 | * Returns the tag name of this code definition 186 | * 187 | * @return string this definition's associated tag name 188 | */ 189 | public function getTagName() 190 | { 191 | return $this->tagName; 192 | } 193 | 194 | /** 195 | * Returns the replacement text of this code definition. This usually has little, if any meaning if the 196 | * CodeDefinition class was extended. For default, html replacement CodeDefinitions this returns the html 197 | * markup for the definition. 198 | * 199 | * @return string the replacement text of this CodeDefinition 200 | */ 201 | public function getReplacementText() 202 | { 203 | return $this->replacementText; 204 | } 205 | 206 | /** 207 | * Returns whether or not this CodeDefinition uses the optional {option} 208 | * 209 | * @return boolean true if this CodeDefinition uses the option, false otherwise 210 | */ 211 | public function usesOption() 212 | { 213 | return $this->useOption; 214 | } 215 | 216 | /** 217 | * Returns whether or not this CodeDefinition parses elements contained within it, 218 | * or just treats its children as text. 219 | * 220 | * @return boolean true if this CodeDefinition parses elements contained within itself 221 | */ 222 | public function parseContent() 223 | { 224 | return $this->parseContent; 225 | } 226 | 227 | /** 228 | * Returns the limit of how many elements defined by this CodeDefinition may be 229 | * nested together. If after parsing elements are nested beyond this limit, the 230 | * subtrees formed by those nodes will be removed from the parse tree. A nest 231 | * limit of -1 signifies no limit. 232 | * 233 | * @return integer 234 | */ 235 | public function getNestLimit() 236 | { 237 | return $this->nestLimit; 238 | } 239 | 240 | /** 241 | * Sets the tag name of this CodeDefinition 242 | * 243 | * @deprecated 244 | * 245 | * @param string $tagName the new tag name of this definition 246 | */ 247 | public function setTagName($tagName) 248 | { 249 | $this->tagName = strtolower($tagName); 250 | } 251 | 252 | /** 253 | * Sets the html replacement text of this CodeDefinition 254 | * 255 | * @deprecated 256 | * 257 | * @param string $txt the new replacement text 258 | */ 259 | public function setReplacementText($txt) 260 | { 261 | $this->replacementText = $txt; 262 | } 263 | 264 | /** 265 | * Sets whether or not this CodeDefinition uses the {option} 266 | * 267 | * @deprecated 268 | * 269 | * @param boolean $bool 270 | */ 271 | public function setUseOption($bool) 272 | { 273 | $this->useOption = $bool; 274 | } 275 | 276 | /** 277 | * Sets whether or not this CodeDefinition allows its children to be parsed as html 278 | * 279 | * @deprecated 280 | * 281 | * @param boolean $bool 282 | */ 283 | public function setParseContent($bool) 284 | { 285 | $this->parseContent = $bool; 286 | } 287 | 288 | /** 289 | * Increments the element counter. This is used for tracking depth of elements of the same type for next limits. 290 | * 291 | * @deprecated 292 | * 293 | * @return void 294 | */ 295 | public function incrementCounter() 296 | { 297 | $this->elCounter++; 298 | } 299 | 300 | /** 301 | * Decrements the element counter. 302 | * 303 | * @deprecated 304 | * 305 | * @return void 306 | */ 307 | public function decrementCounter() 308 | { 309 | $this->elCounter--; 310 | } 311 | 312 | /** 313 | * Resets the element counter. 314 | * 315 | * @deprecated 316 | */ 317 | public function resetCounter() 318 | { 319 | $this->elCounter = 0; 320 | } 321 | 322 | /** 323 | * Returns the current value of the element counter. 324 | * 325 | * @deprecated 326 | * 327 | * @return int 328 | */ 329 | public function getCounter() 330 | { 331 | return $this->elCounter; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /JBBCode/CodeDefinitionBuilder.php: -------------------------------------------------------------------------------- 1 | tagName = $tagName; 40 | $this->replacementText = $replacementText; 41 | } 42 | 43 | /** 44 | * Sets the tag name the CodeDefinition should be built with. 45 | * 46 | * @param string $tagName the tag name for the new CodeDefinition 47 | * @return self 48 | */ 49 | public function setTagName($tagName) 50 | { 51 | $this->tagName = $tagName; 52 | return $this; 53 | } 54 | 55 | /** 56 | * Sets the replacement text that the new CodeDefinition should be 57 | * built with. 58 | * 59 | * @param string $replacementText the replacement text for the new CodeDefinition 60 | * @return self 61 | */ 62 | public function setReplacementText($replacementText) 63 | { 64 | $this->replacementText = $replacementText; 65 | return $this; 66 | } 67 | 68 | /** 69 | * Set whether or not the built CodeDefinition should use the {option} bbcode 70 | * argument. 71 | * 72 | * @param boolean $option true iff the definition includes an option 73 | * @return self 74 | */ 75 | public function setUseOption($option) 76 | { 77 | $this->useOption = $option; 78 | return $this; 79 | } 80 | 81 | /** 82 | * Set whether or not the built CodeDefinition should allow its content 83 | * to be parsed and evaluated as bbcode. 84 | * 85 | * @param boolean $parseContent true iff the content should be parsed 86 | * @return self 87 | */ 88 | public function setParseContent($parseContent) 89 | { 90 | $this->parseContent = $parseContent; 91 | return $this; 92 | } 93 | 94 | /** 95 | * Sets the nest limit for this code definition. 96 | * 97 | * @param integer $limit a positive integer, or -1 if there is no limit. 98 | * @throws \InvalidArgumentException if the nest limit is invalid 99 | * @return self 100 | */ 101 | public function setNestLimit($limit) 102 | { 103 | if (!is_int($limit) || ($limit <= 0 && -1 != $limit)) { 104 | throw new \InvalidArgumentException("A nest limit must be a positive integer " . 105 | "or -1."); 106 | } 107 | $this->nestLimit = $limit; 108 | return $this; 109 | } 110 | 111 | /** 112 | * Sets the InputValidator that option arguments should be validated with. 113 | * 114 | * @param InputValidator $validator the InputValidator instance to use 115 | * @return self 116 | */ 117 | public function setOptionValidator(\JBBCode\InputValidator $validator, $option=null) 118 | { 119 | if (empty($option)) { 120 | $option = $this->tagName; 121 | } 122 | $this->optionValidator[$option] = $validator; 123 | return $this; 124 | } 125 | 126 | /** 127 | * Sets the InputValidator that body ({param}) text should be validated with. 128 | * 129 | * @param InputValidator $validator the InputValidator instance to use 130 | * @return self 131 | */ 132 | public function setBodyValidator(\JBBCode\InputValidator $validator) 133 | { 134 | $this->bodyValidator = $validator; 135 | return $this; 136 | } 137 | 138 | /** 139 | * Removes the attached option validator if one is attached. 140 | * @return self 141 | */ 142 | public function removeOptionValidator() 143 | { 144 | $this->optionValidator = array(); 145 | return $this; 146 | } 147 | 148 | /** 149 | * Removes the attached body validator if one is attached. 150 | * @return self 151 | */ 152 | public function removeBodyValidator() 153 | { 154 | $this->bodyValidator = null; 155 | return $this; 156 | } 157 | 158 | /** 159 | * Builds a CodeDefinition with the current state of the builder. 160 | * 161 | * @return CodeDefinition a new CodeDefinition instance 162 | */ 163 | public function build() 164 | { 165 | $definition = CodeDefinition::construct($this->tagName, 166 | $this->replacementText, 167 | $this->useOption, 168 | $this->parseContent, 169 | $this->nestLimit, 170 | $this->optionValidator, 171 | $this->bodyValidator); 172 | return $definition; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /JBBCode/CodeDefinitionSet.php: -------------------------------------------------------------------------------- 1 | {param}'); 29 | $this->definitions[] = $builder->build(); 30 | 31 | /* [i] italics tag */ 32 | $builder = new CodeDefinitionBuilder('i', '{param}'); 33 | $this->definitions[] = $builder->build(); 34 | 35 | /* [u] underline tag */ 36 | $builder = new CodeDefinitionBuilder('u', '{param}'); 37 | $this->definitions[] = $builder->build(); 38 | 39 | $urlValidator = new \JBBCode\validators\UrlValidator(); 40 | 41 | /* [url] link tag */ 42 | $builder = new CodeDefinitionBuilder('url', '{param}'); 43 | $builder->setParseContent(false)->setBodyValidator($urlValidator); 44 | $this->definitions[] = $builder->build(); 45 | 46 | /* [url=http://example.com] link tag */ 47 | $builder = new CodeDefinitionBuilder('url', '{param}'); 48 | $builder->setUseOption(true)->setParseContent(true)->setOptionValidator($urlValidator); 49 | $this->definitions[] = $builder->build(); 50 | 51 | /* [img] image tag */ 52 | $builder = new CodeDefinitionBuilder('img', ''); 53 | $builder->setUseOption(false)->setParseContent(false)->setBodyValidator($urlValidator); 54 | $this->definitions[] = $builder->build(); 55 | 56 | /* [img=alt text] image tag */ 57 | $builder = new CodeDefinitionBuilder('img', '{option}'); 58 | $builder->setUseOption(true)->setParseContent(false)->setBodyValidator($urlValidator); 59 | $this->definitions[] = $builder->build(); 60 | 61 | /* [color] color tag */ 62 | $builder = new CodeDefinitionBuilder('color', '{param}'); 63 | $builder->setUseOption(true)->setOptionValidator(new \JBBCode\validators\CssColorValidator()); 64 | $this->definitions[] = $builder->build(); 65 | } 66 | 67 | /** 68 | * Returns an array of the default code definitions. 69 | * 70 | * @return CodeDefinition[] 71 | */ 72 | public function getCodeDefinitions() 73 | { 74 | return $this->definitions; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /JBBCode/DocumentElement.php: -------------------------------------------------------------------------------- 1 | setTagName("Document"); 22 | } 23 | 24 | /** 25 | * (non-PHPdoc) 26 | * @see JBBCode.ElementNode::getAsBBCode() 27 | * 28 | * Returns the BBCode representation of this document 29 | * 30 | * @return string this document's bbcode representation 31 | */ 32 | public function getAsBBCode() 33 | { 34 | $s = ""; 35 | foreach ($this->getChildren() as $child) { 36 | $s .= $child->getAsBBCode(); 37 | } 38 | 39 | return $s; 40 | } 41 | 42 | /** 43 | * (non-PHPdoc) 44 | * @see JBBCode.ElementNode::getAsHTML() 45 | * 46 | * Documents don't add any html. They only exist as a container for their 47 | * children, so getAsHTML() simply iterates through the document's children, 48 | * returning their html. 49 | * 50 | * @return string the HTML representation of this document 51 | */ 52 | public function getAsHTML() 53 | { 54 | $s = ""; 55 | foreach ($this->getChildren() as $child) { 56 | $s .= $child->getAsHTML(); 57 | } 58 | 59 | return $s; 60 | } 61 | 62 | public function accept(NodeVisitor $visitor) 63 | { 64 | $visitor->visitDocumentElement($this); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /JBBCode/ElementNode.php: -------------------------------------------------------------------------------- 1 | children = array(); 37 | $this->nestDepth = 0; 38 | } 39 | 40 | public function accept(NodeVisitor $nodeVisitor) 41 | { 42 | $nodeVisitor->visitElementNode($this); 43 | } 44 | 45 | /** 46 | * Gets the CodeDefinition that defines this element. 47 | * 48 | * @return CodeDefinition this element's code definition 49 | */ 50 | public function getCodeDefinition() 51 | { 52 | return $this->codeDefinition; 53 | } 54 | 55 | /** 56 | * Sets the CodeDefinition that defines this element. 57 | * 58 | * @param CodeDefinition $codeDef the code definition that defines this element node 59 | */ 60 | public function setCodeDefinition(CodeDefinition $codeDef) 61 | { 62 | $this->codeDefinition = $codeDef; 63 | $this->setTagName($codeDef->getTagName()); 64 | } 65 | 66 | /** 67 | * Returns the tag name of this element. 68 | * 69 | * @return string the element's tag name 70 | */ 71 | public function getTagName() 72 | { 73 | return $this->tagName; 74 | } 75 | 76 | /** 77 | * Returns the attribute (used as the option in bbcode definitions) of this element. 78 | * 79 | * @return array the attributes of this element 80 | */ 81 | public function getAttribute() 82 | { 83 | return $this->attribute; 84 | } 85 | 86 | /** 87 | * Returns all the children of this element. 88 | * 89 | * @return Node[] an array of this node's child nodes 90 | */ 91 | public function getChildren() 92 | { 93 | return $this->children; 94 | } 95 | 96 | /** 97 | * (non-PHPdoc) 98 | * @see JBBCode.Node::getAsText() 99 | * 100 | * Returns the element as text (not including any bbcode markup) 101 | * 102 | * @return string the plain text representation of this node 103 | */ 104 | public function getAsText() 105 | { 106 | if ($this->codeDefinition) { 107 | return $this->codeDefinition->asText($this); 108 | } else { 109 | $s = ""; 110 | foreach ($this->getChildren() as $child) { 111 | $s .= $child->getAsText(); 112 | } 113 | return $s; 114 | } 115 | } 116 | 117 | /** 118 | * (non-PHPdoc) 119 | * @see JBBCode.Node::getAsBBCode() 120 | * 121 | * Returns the element as bbcode (with all unclosed tags closed) 122 | * 123 | * @return string the bbcode representation of this element 124 | */ 125 | public function getAsBBCode() 126 | { 127 | $str = "[".$this->tagName; 128 | if (!empty($this->attribute)) { 129 | if (isset($this->attribute[$this->tagName])) { 130 | $str .= "=".$this->attribute[$this->tagName]; 131 | } 132 | 133 | foreach ($this->attribute as $key => $value) { 134 | if ($key == $this->tagName) { 135 | continue; 136 | } else { 137 | $str .= " ".$key."=" . $value; 138 | } 139 | } 140 | } 141 | $str .= "]"; 142 | foreach ($this->getChildren() as $child) { 143 | $str .= $child->getAsBBCode(); 144 | } 145 | $str .= "[/".$this->tagName."]"; 146 | 147 | return $str; 148 | } 149 | 150 | /** 151 | * (non-PHPdoc) 152 | * @see JBBCode.Node::getAsHTML() 153 | * 154 | * Returns the element as html with all replacements made 155 | * 156 | * @return string the html representation of this node 157 | */ 158 | public function getAsHTML() 159 | { 160 | if ($this->codeDefinition) { 161 | return $this->codeDefinition->asHtml($this); 162 | } else { 163 | return ""; 164 | } 165 | } 166 | 167 | /** 168 | * Adds a child to this node's content. A child may be a TextNode, or 169 | * another ElementNode... or anything else that may extend the 170 | * abstract Node class. 171 | * 172 | * @param Node $child the node to add as a child 173 | */ 174 | public function addChild(Node $child) 175 | { 176 | $this->children[] = $child; 177 | $child->setParent($this); 178 | } 179 | 180 | /** 181 | * Removes a child from this node's content. 182 | * 183 | * @param Node $child the child node to remove 184 | */ 185 | public function removeChild(Node $child) 186 | { 187 | foreach ($this->children as $key => $value) { 188 | if ($value === $child) { 189 | unset($this->children[$key]); 190 | } 191 | } 192 | } 193 | 194 | /** 195 | * Sets the tag name of this element node. 196 | * 197 | * @param string $tagName the element's new tag name 198 | */ 199 | public function setTagName($tagName) 200 | { 201 | $this->tagName = $tagName; 202 | } 203 | 204 | /** 205 | * Sets the attribute (option) of this element node. 206 | * 207 | * @param string[] $attribute the attribute(s) of this element node 208 | */ 209 | public function setAttribute($attribute) 210 | { 211 | $this->attribute = $attribute; 212 | } 213 | 214 | /** 215 | * Traverses the parse tree upwards, going from parent to parent, until it finds a 216 | * parent who has the given tag name. Returns the parent with the matching tag name 217 | * if it exists, otherwise returns null. 218 | * 219 | * @param string $str the tag name to search for 220 | * 221 | * @return ElementNode|null the closest parent with the given tag name 222 | */ 223 | public function closestParentOfType($str) 224 | { 225 | $str = strtolower($str); 226 | $currentEl = $this; 227 | 228 | while (strtolower($currentEl->getTagName()) != $str && $currentEl->hasParent()) { 229 | $currentEl = $currentEl->getParent(); 230 | } 231 | 232 | if (strtolower($currentEl->getTagName()) != $str) { 233 | return null; 234 | } else { 235 | return $currentEl; 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /JBBCode/InputValidator.php: -------------------------------------------------------------------------------- 1 | parent; 25 | } 26 | 27 | /** 28 | * Determines if this node has a parent. 29 | * 30 | * @return boolean true if this node has a parent, false otherwise 31 | */ 32 | public function hasParent() 33 | { 34 | return $this->parent != null; 35 | } 36 | 37 | /** 38 | * Returns true if this is a text node. Returns false otherwise. 39 | * (Overridden by TextNode to return true) 40 | * 41 | * @return boolean true if this node is a text node 42 | */ 43 | public function isTextNode() 44 | { 45 | return false; 46 | } 47 | 48 | /** 49 | * Accepts the given NodeVisitor. This is part of an implementation 50 | * of the Visitor pattern. 51 | * 52 | * @param NodeVisitor $nodeVisitor the NodeVisitor traversing the graph 53 | */ 54 | abstract public function accept(NodeVisitor $nodeVisitor); 55 | 56 | /** 57 | * Returns this node as text (without any bbcode markup) 58 | * 59 | * @return string the plain text representation of this node 60 | */ 61 | abstract public function getAsText(); 62 | 63 | /** 64 | * Returns this node as bbcode 65 | * 66 | * @return string the bbcode representation of this node 67 | */ 68 | abstract public function getAsBBCode(); 69 | 70 | /** 71 | * Returns this node as HTML 72 | * 73 | * @return string the html representation of this node 74 | */ 75 | abstract public function getAsHTML(); 76 | 77 | /** 78 | * Sets this node's parent to be the given node. 79 | * 80 | * @param Node $parent the node to set as this node's parent 81 | */ 82 | public function setParent(Node $parent) 83 | { 84 | $this->parent = $parent; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /JBBCode/NodeVisitor.php: -------------------------------------------------------------------------------- 1 | treeRoot = new DocumentElement(); 46 | } 47 | 48 | /** 49 | * Adds a simple (text-replacement only) bbcode definition 50 | * 51 | * @param string $tagName the tag name of the code (for example the b in [b]) 52 | * @param string $replace the html to use, with {param} and optionally {option} for replacements 53 | * @param boolean $useOption whether or not this bbcode uses the secondary {option} replacement 54 | * @param boolean $parseContent whether or not to parse the content within these elements 55 | * @param integer $nestLimit an optional limit of the number of elements of this kind that can be nested within 56 | * each other before the parser stops parsing them. 57 | * @param InputValidator $optionValidator the validator to run {option} through 58 | * @param InputValidator $bodyValidator the validator to run {param} through (only used if $parseContent == false) 59 | * 60 | * @return Parser 61 | */ 62 | public function addBBCode($tagName, $replace, $useOption = false, $parseContent = true, $nestLimit = -1, 63 | InputValidator $optionValidator = null, InputValidator $bodyValidator = null) 64 | { 65 | $builder = new CodeDefinitionBuilder($tagName, $replace); 66 | 67 | $builder->setUseOption($useOption); 68 | $builder->setParseContent($parseContent); 69 | $builder->setNestLimit($nestLimit); 70 | 71 | if ($optionValidator) { 72 | $builder->setOptionValidator($optionValidator); 73 | } 74 | 75 | if ($bodyValidator) { 76 | $builder->setBodyValidator($bodyValidator); 77 | } 78 | 79 | $this->addCodeDefinition($builder->build()); 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * Adds a complex bbcode definition. You may subclass the CodeDefinition class, instantiate a definition of your new 86 | * class and add it to the parser through this method. 87 | * 88 | * @param CodeDefinition $definition the bbcode definition to add 89 | * 90 | * @return Parser 91 | */ 92 | public function addCodeDefinition(CodeDefinition $definition) 93 | { 94 | $this->bbcodes[$definition->getTagName()][$definition->usesOption()] = $definition; 95 | return $this; 96 | } 97 | 98 | /** 99 | * Adds a set of CodeDefinitions. 100 | * 101 | * @param CodeDefinitionSet $set the set of definitions to add 102 | * 103 | * @return Parser 104 | */ 105 | public function addCodeDefinitionSet(CodeDefinitionSet $set) 106 | { 107 | foreach ($set->getCodeDefinitions() as $def) { 108 | $this->addCodeDefinition($def); 109 | } 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Returns the entire parse tree as text. Only {param} content is returned. BBCode markup will be ignored. 116 | * 117 | * @return string a text representation of the parse tree 118 | */ 119 | public function getAsText() 120 | { 121 | return $this->treeRoot->getAsText(); 122 | } 123 | 124 | /** 125 | * Returns the entire parse tree as bbcode. This will be identical to the inputted string, except unclosed tags 126 | * will be closed. 127 | * 128 | * @return string a bbcode representation of the parse tree 129 | */ 130 | public function getAsBBCode() 131 | { 132 | return $this->treeRoot->getAsBBCode(); 133 | } 134 | 135 | /** 136 | * Returns the entire parse tree as HTML. All BBCode replacements will be made. This is generally the method 137 | * you will want to use to retrieve the parsed bbcode. 138 | * 139 | * @return string a parsed html string 140 | */ 141 | public function getAsHTML() 142 | { 143 | return $this->treeRoot->getAsHTML(); 144 | } 145 | 146 | /** 147 | * Accepts the given NodeVisitor at the root. 148 | * 149 | * @param NodeVisitor $nodeVisitor a NodeVisitor 150 | * 151 | * @return Parser 152 | */ 153 | public function accept(NodeVisitor $nodeVisitor) 154 | { 155 | $this->treeRoot->accept($nodeVisitor); 156 | 157 | return $this; 158 | } 159 | /** 160 | * Constructs the parse tree from a string of bbcode markup. 161 | * 162 | * @param string $str the bbcode markup to parse 163 | * 164 | * @return Parser 165 | */ 166 | public function parse($str) 167 | { 168 | /* Set the tree root back to a fresh DocumentElement. */ 169 | $this->reset(); 170 | 171 | $parent = $this->treeRoot; 172 | $tokenizer = new Tokenizer($str); 173 | 174 | while ($tokenizer->hasNext()) { 175 | $parent = $this->parseStartState($parent, $tokenizer); 176 | if ($parent->getCodeDefinition() && false === 177 | $parent->getCodeDefinition()->parseContent()) { 178 | /* We're inside an element that does not allow its contents to be parseable. */ 179 | $this->parseAsTextUntilClose($parent, $tokenizer); 180 | $parent = $parent->getParent(); 181 | } 182 | } 183 | 184 | /* We parsed ignoring nest limits. Do an O(n) traversal to remove any elements that 185 | * are nested beyond their CodeDefinition's nest limit. */ 186 | $this->removeOverNestedElements(); 187 | 188 | return $this; 189 | } 190 | 191 | /** 192 | * Removes any elements that are nested beyond their nest limit from the parse tree. This 193 | * method is now deprecated. In a future release its access privileges will be made 194 | * protected. 195 | * 196 | * @deprecated 197 | */ 198 | public function removeOverNestedElements() 199 | { 200 | $nestLimitVisitor = new \JBBCode\visitors\NestLimitVisitor(); 201 | $this->accept($nestLimitVisitor); 202 | } 203 | 204 | /** 205 | * Removes the old parse tree if one exists. 206 | */ 207 | protected function reset() 208 | { 209 | // remove any old tree information 210 | $this->treeRoot = new DocumentElement(); 211 | } 212 | 213 | /** 214 | * Determines whether a bbcode exists based on its tag name and whether or not it uses an option 215 | * 216 | * @param string $tagName the bbcode tag name to check 217 | * @param boolean $usesOption whether or not the bbcode accepts an option 218 | * 219 | * @return bool true if the code exists, false otherwise 220 | */ 221 | public function codeExists($tagName, $usesOption = false) 222 | { 223 | return isset($this->bbcodes[strtolower($tagName)][$usesOption]); 224 | } 225 | 226 | /** 227 | * Returns the CodeDefinition of a bbcode with the matching tag name and usesOption parameter 228 | * 229 | * @param string $tagName the tag name of the bbcode being searched for 230 | * @param boolean $usesOption whether or not the bbcode accepts an option 231 | * 232 | * @return CodeDefinition if the bbcode exists, null otherwise 233 | */ 234 | public function getCode($tagName, $usesOption = false) 235 | { 236 | if ($this->codeExists($tagName, $usesOption)) { 237 | return $this->bbcodes[strtolower($tagName)][$usesOption]; 238 | } 239 | 240 | return null; 241 | } 242 | 243 | /** 244 | * Adds a set of default, standard bbcode definitions commonly used across the web. 245 | * 246 | * This method is now deprecated. Please use DefaultCodeDefinitionSet and 247 | * addCodeDefinitionSet() instead. 248 | * 249 | * @deprecated 250 | */ 251 | public function loadDefaultCodes() 252 | { 253 | $defaultSet = new DefaultCodeDefinitionSet(); 254 | $this->addCodeDefinitionSet($defaultSet); 255 | } 256 | 257 | /** 258 | * Creates a new text node with the given parent and text string. 259 | * 260 | * @param ElementNode $parent the parent of the text node 261 | * @param string $string the text of the text node 262 | * 263 | * @return TextNode the newly created TextNode 264 | */ 265 | protected function createTextNode(ElementNode $parent, $string) 266 | { 267 | $children = $parent->getChildren(); 268 | if (!empty($children)) { 269 | $lastElement = end($children); 270 | reset($children); 271 | 272 | if ($lastElement->isTextNode()) { 273 | $lastElement->setValue($lastElement->getValue() . $string); 274 | return $lastElement; 275 | } 276 | } 277 | 278 | $textNode = new TextNode($string); 279 | $parent->addChild($textNode); 280 | return $textNode; 281 | } 282 | 283 | /** 284 | * jBBCode parsing logic is loosely modelled after a FSM. While not every function maps 285 | * to a unique DFSM state, each function handles the logic of one or more FSM states. 286 | * This function handles the beginning parse state when we're not currently in a tag 287 | * name. 288 | * 289 | * @param ElementNode $parent the current parent node we're under 290 | * @param Tokenizer $tokenizer the tokenizer we're using 291 | * 292 | * @return ElementNode the new parent we should use for the next iteration. 293 | */ 294 | protected function parseStartState(ElementNode $parent, Tokenizer $tokenizer) 295 | { 296 | $next = $tokenizer->next(); 297 | 298 | if ('[' == $next) { 299 | return $this->parseTagOpen($parent, $tokenizer); 300 | } else { 301 | $this->createTextNode($parent, $next); 302 | /* Drop back into the main parse loop which will call this 303 | * same method again. */ 304 | return $parent; 305 | } 306 | } 307 | 308 | /** 309 | * This function handles parsing the beginnings of an open tag. When we see a [ 310 | * at an appropriate time, this function is entered. 311 | * 312 | * @param ElementNode $parent the current parent node 313 | * @param Tokenizer $tokenizer the tokenizer we're using 314 | * 315 | * @return ElementNode the new parent node 316 | */ 317 | protected function parseTagOpen(ElementNode $parent, Tokenizer $tokenizer) 318 | { 319 | if (!$tokenizer->hasNext()) { 320 | /* The [ that sent us to this state was just a trailing [, not the 321 | * opening for a new tag. Treat it as such. */ 322 | $this->createTextNode($parent, '['); 323 | return $parent; 324 | } 325 | 326 | $next = $tokenizer->next(); 327 | 328 | /* This while loop could be replaced by a recursive call to this same method, 329 | * which would likely be a lot clearer but I decided to use a while loop to 330 | * prevent stack overflow with a string like [[[[[[[[[...[[[. 331 | */ 332 | while ('[' == $next) { 333 | /* The previous [ was just a random bracket that should be treated as text. 334 | * Continue until we get a non open bracket. */ 335 | $this->createTextNode($parent, '['); 336 | if (!$tokenizer->hasNext()) { 337 | $this->createTextNode($parent, '['); 338 | return $parent; 339 | } 340 | $next = $tokenizer->next(); 341 | } 342 | 343 | if (!$tokenizer->hasNext()) { 344 | $this->createTextNode($parent, '['.$next); 345 | return $parent; 346 | } 347 | 348 | $after_next = $tokenizer->next(); 349 | $tokenizer->stepBack(); 350 | 351 | if ($after_next != ']') { 352 | $this->createTextNode($parent, '['.$next); 353 | return $parent; 354 | } 355 | 356 | /* At this point $next is either ']' or plain text. */ 357 | if (']' == $next) { 358 | $this->createTextNode($parent, '['); 359 | $this->createTextNode($parent, ']'); 360 | return $parent; 361 | } else { 362 | /* $next is plain text... likely a tag name. */ 363 | return $this->parseTag($parent, $tokenizer, $next); 364 | } 365 | } 366 | 367 | protected function parseOptions($tagContent) 368 | { 369 | $buffer = ""; 370 | $tagName = ""; 371 | $state = static::OPTION_STATE_TAGNAME; 372 | $keys = array(); 373 | $values = array(); 374 | $options = array(); 375 | 376 | $len = strlen($tagContent); 377 | $done = false; 378 | $idx = 0; 379 | 380 | try { 381 | while (!$done) { 382 | $char = $idx < $len ? $tagContent[$idx]:null; 383 | switch ($state) { 384 | case static::OPTION_STATE_TAGNAME: 385 | switch ($char) { 386 | case '=': 387 | $state = static::OPTION_STATE_VALUE; 388 | $tagName = $buffer; 389 | $keys[] = $tagName; 390 | $buffer = ""; 391 | break; 392 | case ' ': 393 | if ($buffer) { 394 | $state = static::OPTION_STATE_DEFAULT; 395 | $tagName = $buffer; 396 | $buffer = ''; 397 | $keys[] = $tagName; 398 | } 399 | break; 400 | case "\n": 401 | case "\r": 402 | break; 403 | 404 | case null: 405 | $tagName = $buffer; 406 | $buffer = ''; 407 | $keys[] = $tagName; 408 | break; 409 | default: 410 | $buffer .= $char; 411 | } 412 | break; 413 | 414 | case static::OPTION_STATE_DEFAULT: 415 | switch ($char) { 416 | case ' ': 417 | // do nothing 418 | default: 419 | $state = static::OPTION_STATE_KEY; 420 | $buffer .= $char; 421 | } 422 | break; 423 | 424 | case static::OPTION_STATE_VALUE: 425 | switch ($char) { 426 | case '"': 427 | $state = static::OPTION_STATE_QUOTED_VALUE; 428 | break; 429 | case null: // intentional fall-through 430 | case ' ': // key=value delimits to next key 431 | $values[] = trim($buffer); 432 | $buffer = ""; 433 | $state = static::OPTION_STATE_KEY; 434 | break; 435 | case ":": 436 | if ($buffer=="javascript") { 437 | $state = static::OPTION_STATE_JAVASCRIPT; 438 | } 439 | $buffer .= $char; 440 | break; 441 | default: 442 | $buffer .= $char; 443 | 444 | } 445 | break; 446 | 447 | case static::OPTION_STATE_JAVASCRIPT: 448 | switch ($char) { 449 | case ";": 450 | $buffer .= $char; 451 | $values[] = $buffer; 452 | $buffer = ""; 453 | $state = static::OPTION_STATE_KEY; 454 | 455 | break; 456 | default: 457 | $buffer .= $char; 458 | } 459 | break; 460 | 461 | case static::OPTION_STATE_KEY: 462 | switch ($char) { 463 | case '=': 464 | $state = static::OPTION_STATE_VALUE; 465 | $keys[] = trim($buffer); 466 | $buffer = ''; 467 | break; 468 | case ' ': // ignore key=value 469 | break; 470 | default: 471 | $buffer .= $char; 472 | break; 473 | } 474 | break; 475 | 476 | case static::OPTION_STATE_QUOTED_VALUE: 477 | switch ($char) { 478 | case null: 479 | case '"': 480 | $state = static::OPTION_STATE_KEY; 481 | $values[] = $buffer; 482 | $buffer = ''; 483 | 484 | // peek ahead. If the next character is not a space or a closing brace, we have a bad tag and need to abort 485 | if (isset($tagContent[$idx+1]) && $tagContent[$idx+1]!=" " && $tagContent[$idx+1]!="]") { 486 | throw new \DomainException("Badly formed attribute: $tagContent"); 487 | } 488 | break; 489 | default: 490 | $buffer .= $char; 491 | break; 492 | } 493 | break; 494 | default: 495 | if (!empty($char)) { 496 | $state = static::OPTION_STATE_KEY; 497 | } 498 | 499 | } 500 | if ($idx >= $len) { 501 | $done = true; 502 | } 503 | $idx++; 504 | } 505 | 506 | if (!empty($keys) && !empty($values)) { 507 | if (count($keys)==(count($values)+1)) { 508 | array_unshift($values, ""); 509 | } 510 | 511 | $options = array_combine($keys, $values); 512 | } 513 | } catch (\DomainException $e) { 514 | // 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 515 | $options[$tagName]= substr($tagContent, strpos($tagContent, "=")+1); 516 | } 517 | return array($tagName, $options); 518 | } 519 | 520 | /** 521 | * This is the next step in parsing a tag. It's possible for it to still be invalid at this 522 | * point but many of the basic invalid tag name conditions have already been handled. 523 | * 524 | * @param ElementNode $parent the current parent element 525 | * @param Tokenizer $tokenizer the tokenizer we're using 526 | * @param string $tagContent the text between the [ and the ], assuming there is actually a ] 527 | * 528 | * @return ElementNode the new parent element 529 | */ 530 | protected function parseTag(ElementNode $parent, Tokenizer $tokenizer, $tagContent) 531 | { 532 | if (!$tokenizer->hasNext() || ($next = $tokenizer->next()) != ']') { 533 | /* This is a malformed tag. Both the previous [ and the tagContent 534 | * is really just plain text. */ 535 | $this->createTextNode($parent, '['); 536 | $this->createTextNode($parent, $tagContent); 537 | return $parent; 538 | } 539 | 540 | /* This is a well-formed tag consisting of [something] or [/something], but 541 | * we still need to ensure that 'something' is a valid tag name. Additionally, 542 | * if it's a closing tag, we need to ensure that there was a previous matching 543 | * opening tag. 544 | */ 545 | /* There could be attributes. */ 546 | list($tmpTagName, $options) = $this->parseOptions($tagContent); 547 | 548 | // $tagPieces = explode('=', $tagContent); 549 | // $tmpTagName = $tagPieces[0]; 550 | 551 | $actualTagName = $tmpTagName; 552 | if ('' != $tmpTagName && '/' == $tmpTagName[0]) { 553 | /* This is a closing tag name. */ 554 | $actualTagName = substr($tmpTagName, 1); 555 | } 556 | 557 | if ('' != $tmpTagName && '/' == $tmpTagName[0]) { 558 | /* This is attempting to close an open tag. We must verify that there exists an 559 | * open tag of the same type and that there is no option (options on closing 560 | * tags don't make any sense). */ 561 | $elToClose = $parent->closestParentOfType($actualTagName); 562 | if (null == $elToClose || count($options) > 1) { 563 | /* Closing an unopened tag or has an option. Treat everything as plain text. */ 564 | $this->createTextNode($parent, '['); 565 | $this->createTextNode($parent, $tagContent); 566 | $this->createTextNode($parent, ']'); 567 | return $parent; 568 | } else { 569 | /* We're closing $elToClose. In order to do that, we just need to return 570 | * $elToClose's parent, since that will change our effective parent to be 571 | * elToClose's parent. */ 572 | return $elToClose->getParent(); 573 | } 574 | } 575 | 576 | /* Verify that this is a known bbcode tag name. */ 577 | if ('' == $actualTagName || !$this->codeExists($actualTagName, !empty($options))) { 578 | /* This is an invalid tag name! Treat everything we've seen as plain text. */ 579 | $this->createTextNode($parent, '['); 580 | $this->createTextNode($parent, $tagContent); 581 | $this->createTextNode($parent, ']'); 582 | return $parent; 583 | } 584 | 585 | /* If we're here, this is a valid opening tag. Let's make a new node for it. */ 586 | $el = new ElementNode(); 587 | $code = $this->getCode($actualTagName, !empty($options)); 588 | $el->setCodeDefinition($code); 589 | if (!empty($options)) { 590 | /* We have an attribute we should save. */ 591 | $el->setAttribute($options); 592 | } 593 | $parent->addChild($el); 594 | return $el; 595 | } 596 | 597 | /** 598 | * Handles parsing elements whose CodeDefinitions disable parsing of element 599 | * contents. This function uses a rolling window of 3 tokens until it finds the 600 | * appropriate closing tag or reaches the end of the token stream. 601 | * 602 | * @param ElementNode $parent the current parent element 603 | * @param Tokenizer $tokenizer the tokenizer we're using 604 | * 605 | * @return ElementNode the new parent element 606 | */ 607 | protected function parseAsTextUntilClose(ElementNode $parent, Tokenizer $tokenizer) 608 | { 609 | /* $parent's code definition doesn't allow its contents to be parsed. Here we use 610 | * a sliding window of three tokens until we find [ /tagname ], signifying the 611 | * end of the parent. */ 612 | if (!$tokenizer->hasNext()) { 613 | return $parent; 614 | } 615 | $prevPrev = $tokenizer->next(); 616 | if (!$tokenizer->hasNext()) { 617 | $this->createTextNode($parent, $prevPrev); 618 | return $parent; 619 | } 620 | $prev = $tokenizer->next(); 621 | if (!$tokenizer->hasNext()) { 622 | $this->createTextNode($parent, $prevPrev); 623 | $this->createTextNode($parent, $prev); 624 | return $parent; 625 | } 626 | $curr = $tokenizer->next(); 627 | while ('[' != $prevPrev || '/'.$parent->getTagName() != strtolower($prev) || 628 | ']' != $curr) { 629 | $this->createTextNode($parent, $prevPrev); 630 | $prevPrev = $prev; 631 | $prev = $curr; 632 | if (!$tokenizer->hasNext()) { 633 | $this->createTextNode($parent, $prevPrev); 634 | $this->createTextNode($parent, $prev); 635 | return $parent; 636 | } 637 | $curr = $tokenizer->next(); 638 | } 639 | } 640 | } 641 | -------------------------------------------------------------------------------- /JBBCode/TextNode.php: -------------------------------------------------------------------------------- 1 | value = $val; 25 | } 26 | 27 | public function accept(NodeVisitor $visitor) 28 | { 29 | $visitor->visitTextNode($this); 30 | } 31 | 32 | /** 33 | * (non-PHPdoc) 34 | * @see JBBCode.Node::isTextNode() 35 | * 36 | * @returns boolean true 37 | */ 38 | public function isTextNode() 39 | { 40 | return true; 41 | } 42 | 43 | /** 44 | * Returns the text string value of this text node. 45 | * 46 | * @return string 47 | */ 48 | public function getValue() 49 | { 50 | return $this->value; 51 | } 52 | 53 | /** 54 | * (non-PHPdoc) 55 | * @see JBBCode.Node::getAsText() 56 | * 57 | * Returns the text representation of this node. 58 | * 59 | * @return string this node represented as text 60 | */ 61 | public function getAsText() 62 | { 63 | return $this->getValue(); 64 | } 65 | 66 | /** 67 | * (non-PHPdoc) 68 | * @see JBBCode.Node::getAsBBCode() 69 | * 70 | * Returns the bbcode representation of this node. (Just its value) 71 | * 72 | * @return string this node represented as bbcode 73 | */ 74 | public function getAsBBCode() 75 | { 76 | return $this->getValue(); 77 | } 78 | 79 | /** 80 | * (non-PHPdoc) 81 | * @see JBBCode.Node::getAsHTML() 82 | * 83 | * Returns the html representation of this node. (Just its value) 84 | * 85 | * @return string this node represented as HTML 86 | */ 87 | public function getAsHTML() 88 | { 89 | return $this->getValue(); 90 | } 91 | 92 | /** 93 | * Edits the text value contained within this text node. 94 | * 95 | * @param string $newValue the new text value of the text node 96 | */ 97 | public function setValue($newValue) 98 | { 99 | $this->value = $newValue; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /JBBCode/Tokenizer.php: -------------------------------------------------------------------------------- 1 | tokens[] = $str[$position]; 37 | $position++; 38 | } else { 39 | $this->tokens[] = substr($str, $position, $offset); 40 | $position += $offset; 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * Returns true if there is another token in the token stream. 47 | * @return boolean 48 | */ 49 | public function hasNext() 50 | { 51 | return isset($this->tokens[$this->i + 1]); 52 | } 53 | 54 | /** 55 | * Advances the token stream to the next token and returns the new token. 56 | * @return null|string 57 | */ 58 | public function next() 59 | { 60 | if (!$this->hasNext()) { 61 | return null; 62 | } else { 63 | return $this->tokens[++$this->i]; 64 | } 65 | } 66 | 67 | /** 68 | * Retrieves the current token. 69 | * @return null|string 70 | */ 71 | public function current() 72 | { 73 | if ($this->i < 0) { 74 | return null; 75 | } else { 76 | return $this->tokens[$this->i]; 77 | } 78 | } 79 | 80 | /** 81 | * Moves the token stream back a token. 82 | */ 83 | public function stepBack() 84 | { 85 | if ($this->i > -1) { 86 | $this->i--; 87 | } 88 | } 89 | 90 | /** 91 | * Restarts the tokenizer, returning to the beginning of the token stream. 92 | */ 93 | public function restart() 94 | { 95 | $this->i = -1; 96 | } 97 | 98 | /** 99 | * toString method that returns the entire string from the current index on. 100 | * @return string 101 | */ 102 | public function toString() 103 | { 104 | return implode('', array_slice($this->tokens, $this->i + 1)); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /JBBCode/examples/1-GettingStarted.php: -------------------------------------------------------------------------------- 1 | addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 6 | 7 | $text = "The default codes include: [b]bold[/b], [i]italics[/i], [u]underlining[/u], "; 8 | $text .= "[url=http://jbbcode.com]links[/url], [color=red]color![/color] and more."; 9 | 10 | $parser->parse($text); 11 | 12 | print $parser->getAsHtml(); 13 | -------------------------------------------------------------------------------- /JBBCode/examples/2-ClosingUnclosedTags.php: -------------------------------------------------------------------------------- 1 | addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 6 | 7 | $text = "The bbcode in here [b]is never closed!"; 8 | $parser->parse($text); 9 | 10 | print $parser->getAsBBCode(); 11 | -------------------------------------------------------------------------------- /JBBCode/examples/3-MarkuplessText.php: -------------------------------------------------------------------------------- 1 | addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 6 | 7 | $text = "[b][u]There is [i]a lot[/i] of [url=http://en.wikipedia.org/wiki/Markup_language]markup[/url] in this"; 8 | $text .= "[color=#333333]text[/color]![/u][/b]"; 9 | $parser->parse($text); 10 | 11 | print $parser->getAsText(); 12 | -------------------------------------------------------------------------------- /JBBCode/examples/4-CreatingNewCodes.php: -------------------------------------------------------------------------------- 1 | addBBCode("quote", '
{param}
'); 7 | $parser->addBBCode("code", '
{param}
', false, false, 1); 8 | -------------------------------------------------------------------------------- /JBBCode/examples/SmileyVisitorTest.php: -------------------------------------------------------------------------------- 1 | addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 10 | 11 | if (count($argv) < 2) { 12 | die("Usage: " . $argv[0] . " \"bbcode string\"\n"); 13 | } 14 | 15 | $inputText = $argv[1]; 16 | 17 | $parser->parse($inputText); 18 | 19 | $smileyVisitor = new \JBBCode\visitors\SmileyVisitor(); 20 | $parser->accept($smileyVisitor); 21 | 22 | echo $parser->getAsHTML() . "\n"; 23 | -------------------------------------------------------------------------------- /JBBCode/examples/TagCountingVisitorTest.php: -------------------------------------------------------------------------------- 1 | addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 10 | 11 | if (count($argv) < 3) { 12 | die("Usage: " . $argv[0] . " \"bbcode string\" \n"); 13 | } 14 | 15 | $inputText = $argv[1]; 16 | $tagName = $argv[2]; 17 | 18 | $parser->parse($inputText); 19 | 20 | $tagCountingVisitor = new \JBBCode\visitors\TagCountingVisitor(); 21 | $parser->accept($tagCountingVisitor); 22 | 23 | echo $tagCountingVisitor->getFrequency($tagName) . "\n"; 24 | -------------------------------------------------------------------------------- /JBBCode/tests/CodeDefinitionBuilderTest.php: -------------------------------------------------------------------------------- 1 | _builder = new CodeDefinitionBuilderStub('foo', 'bar'); 14 | } 15 | 16 | public function testConstructor() 17 | { 18 | $codeDefinition = $this->_builder->build(); 19 | $this->assertInstanceOf('JBBCode\CodeDefinition', $codeDefinition); 20 | $this->assertEquals('foo', $codeDefinition->getTagName()); 21 | $this->assertEquals('bar', $codeDefinition->getReplacementText()); 22 | } 23 | 24 | public function testSetTagName() 25 | { 26 | $this->assertSame($this->_builder, $this->_builder->setTagName('baz')); 27 | $this->assertEquals('baz', $this->_builder->build()->getTagName()); 28 | } 29 | 30 | public function testSetReplacementText() 31 | { 32 | $this->assertSame($this->_builder, $this->_builder->setReplacementText('baz')); 33 | $this->assertEquals('baz', $this->_builder->build()->getReplacementText()); 34 | } 35 | 36 | public function testSetUseOption() 37 | { 38 | $this->assertFalse($this->_builder->build()->usesOption()); 39 | $this->assertSame($this->_builder, $this->_builder->setUseOption(true)); 40 | $this->assertTrue($this->_builder->build()->usesOption()); 41 | } 42 | 43 | public function testSetParseContent() 44 | { 45 | $this->assertTrue($this->_builder->build()->parseContent()); 46 | $this->assertSame($this->_builder, $this->_builder->setParseContent(false)); 47 | $this->assertFalse($this->_builder->build()->parseContent()); 48 | } 49 | 50 | public function testSetNestLimit() 51 | { 52 | $this->assertEquals(-1, $this->_builder->build()->getNestLimit()); 53 | $this->assertSame($this->_builder, $this->_builder->setNestLimit(1)); 54 | $this->assertEquals(1, $this->_builder->build()->getNestLimit()); 55 | } 56 | 57 | /** 58 | * @expectedException InvalidArgumentException 59 | * @dataProvider invalidNestLimitProvider 60 | */ 61 | public function testSetInvalidNestLimit($limit) 62 | { 63 | $this->_builder->setNestLimit($limit); 64 | } 65 | 66 | public function testSetOptionValidator() 67 | { 68 | $this->assertEmpty($this->_builder->getOptionValidators()); 69 | $urlValidator = new JBBCode\validators\UrlValidator(); 70 | $this->assertSame($this->_builder, $this->_builder->setOptionValidator($urlValidator)); 71 | $this->assertArrayHasKey('foo', $this->_builder->getOptionValidators()); 72 | $this->assertContains($urlValidator, $this->_builder->getOptionValidators()); 73 | 74 | $otherUrlValidator = new JBBCode\validators\UrlValidator(); 75 | $this->assertSame($this->_builder, $this->_builder->setOptionValidator($otherUrlValidator, 'url')); 76 | $this->assertArrayHasKey('url', $this->_builder->getOptionValidators()); 77 | $this->assertContains($urlValidator, $this->_builder->getOptionValidators()); 78 | $this->assertContains($otherUrlValidator, $this->_builder->getOptionValidators()); 79 | } 80 | 81 | public function testSetBodyValidator() 82 | { 83 | $this->assertNull($this->_builder->getBodyValidator()); 84 | $validator = new JBBCode\validators\UrlValidator(); 85 | $this->assertSame($this->_builder, $this->_builder->setBodyValidator($validator)); 86 | $this->assertSame($validator, $this->_builder->getBodyValidator()); 87 | } 88 | 89 | /** 90 | * @depends testSetOptionValidator 91 | */ 92 | public function testRemoveOptionValidator() 93 | { 94 | $this->assertSame($this->_builder, $this->_builder->removeOptionValidator()); 95 | $this->assertEmpty($this->_builder->getOptionValidators()); 96 | $this->_builder->setOptionValidator(new JBBCode\validators\UrlValidator()); 97 | $this->assertSame($this->_builder, $this->_builder->removeOptionValidator()); 98 | $this->assertEmpty($this->_builder->getOptionValidators()); 99 | } 100 | 101 | /** 102 | * @depends testSetBodyValidator 103 | */ 104 | public function testRemoveBodyValidator() 105 | { 106 | $this->assertSame($this->_builder, $this->_builder->removeBodyValidator()); 107 | $this->assertNull($this->_builder->getBodyValidator()); 108 | $this->_builder->setOptionValidator(new JBBCode\validators\UrlValidator()); 109 | $this->assertSame($this->_builder, $this->_builder->removeBodyValidator()); 110 | $this->assertNull($this->_builder->getBodyValidator()); 111 | } 112 | 113 | public function invalidNestLimitProvider() 114 | { 115 | return array( 116 | array(-2), 117 | array(null), 118 | array(false), 119 | ); 120 | } 121 | } 122 | 123 | class CodeDefinitionBuilderStub extends \JBBCode\CodeDefinitionBuilder 124 | { 125 | 126 | /** 127 | * @return \JBBCode\InputValidator 128 | */ 129 | public function getBodyValidator() 130 | { 131 | return $this->bodyValidator; 132 | } 133 | 134 | /** 135 | * @return \JBBCode\InputValidator[] 136 | */ 137 | public function getOptionValidators() 138 | { 139 | return $this->optionValidator; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /JBBCode/tests/DefaultCodeDefinitionSetTest.php: -------------------------------------------------------------------------------- 1 | getCodeDefinitions(); 15 | $this->assertInternalType('array', $definitions); 16 | 17 | $parser = new JBBCode\Parser(); 18 | 19 | $this->assertFalse($parser->codeExists('b')); 20 | $this->assertFalse($parser->codeExists('i')); 21 | $this->assertFalse($parser->codeExists('u')); 22 | $this->assertFalse($parser->codeExists('url', true)); 23 | $this->assertFalse($parser->codeExists('img')); 24 | $this->assertFalse($parser->codeExists('img', true)); 25 | $this->assertFalse($parser->codeExists('color', true)); 26 | 27 | $parser->addCodeDefinitionSet($dcds); 28 | 29 | $this->assertTrue($parser->codeExists('b')); 30 | $this->assertTrue($parser->codeExists('i')); 31 | $this->assertTrue($parser->codeExists('u')); 32 | $this->assertTrue($parser->codeExists('url', true)); 33 | $this->assertTrue($parser->codeExists('img')); 34 | $this->assertTrue($parser->codeExists('img', true)); 35 | $this->assertTrue($parser->codeExists('color', true)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /JBBCode/tests/DocumentElementTest.php: -------------------------------------------------------------------------------- 1 | _documentElement = new DocumentElement(); 15 | } 16 | 17 | public function testGetTagName() 18 | { 19 | $this->assertEquals('Document', $this->_documentElement->getTagName()); 20 | } 21 | 22 | public function testGetAsText() 23 | { 24 | $this->assertEmpty($this->_documentElement->getAsText()); 25 | $mock = $this->getMock('JBBCode\ElementNode', array('getAsText')); 26 | $mock->expects($this->once()) 27 | ->method('getAsText') 28 | ->will($this->returnValue('foo')); 29 | $this->_documentElement->addChild($mock); 30 | $this->assertEquals('foo', $this->_documentElement->getAsText()); 31 | } 32 | 33 | public function testGetAsHTML() 34 | { 35 | $this->assertEmpty($this->_documentElement->getAsHTML()); 36 | $mock = $this->getMock('JBBCode\ElementNode', array('getAsHTML')); 37 | $mock->expects($this->once()) 38 | ->method('getAsHTML') 39 | ->will($this->returnValue('foo')); 40 | $this->_documentElement->addChild($mock); 41 | $this->assertEquals('foo', $this->_documentElement->getAsHTML()); 42 | } 43 | 44 | public function testGetAsBBCode() 45 | { 46 | $this->assertEmpty($this->_documentElement->getAsBBCode()); 47 | $mock = $this->getMock('JBBCode\ElementNode', array('getAsBBCOde')); 48 | $mock->expects($this->once()) 49 | ->method('getAsBBCode') 50 | ->will($this->returnValue('[b]foo[/b]')); 51 | $this->_documentElement->addChild($mock); 52 | $this->assertEquals('[b]foo[/b]', $this->_documentElement->getAsBBCode()); 53 | } 54 | 55 | public function testAccept() 56 | { 57 | $mock = $this->getMock('JBBCode\NodeVisitor', 58 | array('visitDocumentElement', 'visitTextNode', 'visitElementNode')); 59 | $mock->expects($this->once()) 60 | ->method('visitDocumentElement') 61 | ->with($this->equalTo($this->_documentElement)); 62 | $mock->expects($this->never()) 63 | ->method('visitTextNode'); 64 | $mock->expects($this->never()) 65 | ->method('visitElementNode'); 66 | $this->_documentElement->accept($mock); 67 | } 68 | 69 | public function testIsTextNode() 70 | { 71 | $this->assertFalse($this->_documentElement->isTextNode()); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /JBBCode/tests/ElementNodeTest.php: -------------------------------------------------------------------------------- 1 | _elementNode = new JBBCode\ElementNode(); 11 | } 12 | 13 | public function testConstructor() 14 | { 15 | $this->assertNull($this->_elementNode->getCodeDefinition()); 16 | $this->assertEmpty($this->_elementNode->getTagName()); 17 | $this->assertEmpty($this->_elementNode->getAttribute()); 18 | $this->assertEmpty($this->_elementNode->getChildren()); 19 | $this->assertEmpty($this->_elementNode->getAsText()); 20 | $this->assertEmpty($this->_elementNode->getAsHTML()); 21 | } 22 | 23 | public function testAccept() 24 | { 25 | $mock = $this->getMock('JBBCode\NodeVisitor', 26 | array('visitDocumentElement', 'visitTextNode', 'visitElementNode')); 27 | $mock->expects($this->never()) 28 | ->method('visitDocumentElement'); 29 | $mock->expects($this->never()) 30 | ->method('visitTextNode'); 31 | $mock->expects($this->once()) 32 | ->method('visitElementNode') 33 | ->with($this->equalTo($this->_elementNode)); 34 | $this->_elementNode->accept($mock); 35 | } 36 | 37 | public function testSetCodeDefinition() 38 | { 39 | $mock = $this->getMock('JBBCode\CodeDefinition', array('getTagName')); 40 | $mock->expects($this->once()) 41 | ->method('getTagName') 42 | ->will($this->returnValue('foo')); 43 | $this->_elementNode->setCodeDefinition($mock); 44 | $this->assertSame($mock, $this->_elementNode->getCodeDefinition()); 45 | $this->assertEquals('foo', $this->_elementNode->getTagName()); 46 | } 47 | 48 | public function testAddChild() 49 | { 50 | $mock = $this->getMock('JBBCode\ElementNode', array('setParent')); 51 | $mock->expects($this->once()) 52 | ->method('setParent') 53 | ->with($this->equalTo($this->_elementNode)); 54 | $this->_elementNode->addChild($mock); 55 | $this->assertContains($mock, $this->_elementNode->getChildren()); 56 | } 57 | 58 | public function testIsTextNode() 59 | { 60 | $this->assertFalse($this->_elementNode->isTextNode()); 61 | } 62 | 63 | public function testGetAsBBCode() 64 | { 65 | $builder = new JBBCode\CodeDefinitionBuilder('foo', 'bar'); 66 | $codeDefinition = $builder->build(); 67 | $this->_elementNode->setCodeDefinition($codeDefinition); 68 | $this->assertEquals('[foo][/foo]', $this->_elementNode->getAsBBCode()); 69 | 70 | $this->_elementNode->setAttribute(array('bar' => 'baz')); 71 | $this->assertEquals('[foo bar=baz][/foo]', $this->_elementNode->getAsBBCode()); 72 | 73 | /** @ticket 55 */ 74 | $this->_elementNode->setAttribute(array( 75 | 'bar' => 'baz', 76 | 'foo' => 'bar' 77 | )); 78 | $this->assertEquals('[foo=bar bar=baz][/foo]', $this->_elementNode->getAsBBCode()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /JBBCode/tests/ParseContentTest.php: -------------------------------------------------------------------------------- 1 | _parser = new JBBCode\Parser(); 20 | $this->_parser->addCodeDefinitionSet(new JBBcode\DefaultCodeDefinitionSet()); 21 | } 22 | 23 | /** 24 | * Tests that when a bbcode is created with parseContent = false, 25 | * its contents actually are not parsed. 26 | */ 27 | public function testSimpleNoParsing() 28 | { 29 | $this->_parser->addBBCode('verbatim', '{param}', false, false); 30 | 31 | $this->_parser->parse('[verbatim]plain text[/verbatim]'); 32 | $this->assertEquals('plain text', $this->_parser->getAsHtml()); 33 | 34 | $this->_parser->parse('[verbatim][b]bold[/b][/verbatim]'); 35 | $this->assertEquals('[b]bold[/b]', $this->_parser->getAsHtml()); 36 | } 37 | 38 | public function testNoParsingWithBufferText() 39 | { 40 | $this->_parser->addBBCode('verbatim', '{param}', false, false); 41 | 42 | $this->_parser->parse('buffer text[verbatim]buffer text[b]bold[/b]buffer text[/verbatim]buffer text'); 43 | $this->assertEquals('buffer textbuffer text[b]bold[/b]buffer textbuffer text', $this->_parser->getAsHtml()); 44 | } 45 | 46 | /** 47 | * Tests that when a tag is not closed within an unparseable tag, 48 | * the BBCode output does not automatically close that tag (because 49 | * the contents were not parsed). 50 | */ 51 | public function testUnclosedTag() 52 | { 53 | $this->_parser->addBBCode('verbatim', '{param}', false, false); 54 | 55 | $this->_parser->parse('[verbatim]i wonder [b]what will happen[/verbatim]'); 56 | $this->assertEquals('i wonder [b]what will happen', $this->_parser->getAsHtml()); 57 | $this->assertEquals('[verbatim]i wonder [b]what will happen[/verbatim]', $this->_parser->getAsBBCode()); 58 | } 59 | 60 | /** 61 | * Tests that an unclosed tag with parseContent = false ends cleanly. 62 | */ 63 | public function testUnclosedVerbatimTag() 64 | { 65 | $this->_parser->addBBCode('verbatim', '{param}', false, false); 66 | 67 | $this->_parser->parse('[verbatim]yo this [b]text should not be bold[/b]'); 68 | $this->assertEquals('yo this [b]text should not be bold[/b]', $this->_parser->getAsHtml()); 69 | } 70 | 71 | /** 72 | * Tests a malformed closing tag for a verbatim block. 73 | */ 74 | public function testMalformedVerbatimClosingTag() 75 | { 76 | $this->_parser->addBBCode('verbatim', '{param}', false, false); 77 | $this->_parser->parse('[verbatim]yo this [b]text should not be bold[/b][/verbatim'); 78 | $this->assertEquals('yo this [b]text should not be bold[/b][/verbatim', $this->_parser->getAsHtml()); 79 | } 80 | 81 | /** 82 | * Tests an immediate end after a verbatim. 83 | */ 84 | public function testVerbatimThenEof() 85 | { 86 | $parser = new JBBCode\Parser(); 87 | $parser->addBBCode('verbatim', '{param}', false, false); 88 | $parser->parse('[verbatim]'); 89 | $this->assertEquals('', $parser->getAsHtml()); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /JBBCode/tests/ParserTest.php: -------------------------------------------------------------------------------- 1 | _parser = new JBBCode\Parser(); 13 | $this->_parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 14 | } 15 | 16 | public function testAddCodeDefinition() 17 | { 18 | $parser = new JBBCode\Parser(); 19 | 20 | $this->assertFalse($parser->codeExists('foo', true)); 21 | $this->assertFalse($parser->codeExists('foo', false)); 22 | } 23 | 24 | public function testAddBBCode() 25 | { 26 | $parser = new JBBCode\Parser(); 27 | 28 | $this->assertFalse($parser->codeExists('foo', true)); 29 | $this->assertFalse($parser->codeExists('foo', false)); 30 | 31 | $this->assertSame($parser, $parser->addBBCode('foo', 'bar', true)); 32 | 33 | $this->assertTrue($parser->codeExists('foo', true)); 34 | $this->assertFalse($parser->codeExists('foo', false)); 35 | 36 | $this->assertSame($parser, $parser->addBBCode('foo', 'bar', true)); 37 | 38 | $this->assertTrue($parser->codeExists('foo', true)); 39 | $this->assertFalse($parser->codeExists('foo', false)); 40 | 41 | $this->assertSame($parser, $parser->addBBCode('foo', 'bar', false)); 42 | 43 | $this->assertTrue($parser->codeExists('foo', true)); 44 | $this->assertTrue($parser->codeExists('foo', false)); 45 | } 46 | 47 | /** 48 | * Check for empty strings being the result of empty input 49 | */ 50 | public function testParseEmptyString() 51 | { 52 | $parser = $this->_parser->parse(''); 53 | $this->assertEmpty($parser->getAsBBCode()); 54 | $this->assertEmpty($parser->getAsText()); 55 | $this->assertEmpty($parser->getAsHTML()); 56 | } 57 | 58 | /** 59 | * Test for artifacts of previous parses 60 | */ 61 | public function testParseContentCleared() 62 | { 63 | $parser = $this->_parser->parse('foo'); 64 | 65 | $this->assertEquals('foo', $parser->getAsText()); 66 | $this->assertEquals('foo', $parser->getAsHTML()); 67 | $this->assertEquals('foo', $parser->getAsBBCode()); 68 | 69 | $parser->parse('bar'); 70 | 71 | $this->assertEquals('bar', $parser->getAsText()); 72 | $this->assertEquals('bar', $parser->getAsHTML()); 73 | $this->assertEquals('bar', $parser->getAsBBCode()); 74 | } 75 | 76 | /** 77 | * @param string $code 78 | * @param string[] $expected 79 | * @dataProvider textCodeProvider 80 | */ 81 | public function testParse($code, $expected) 82 | { 83 | $parser = $this->_parser->parse($code); 84 | $this->assertEquals($expected['text'], $parser->getAsText()); 85 | $this->assertEquals($expected['html'], $parser->getAsHTML()); 86 | $this->assertEquals($expected['bbcode'], $parser->getAsBBCode()); 87 | } 88 | 89 | public function textCodeProvider() 90 | { 91 | return array( 92 | array( 93 | 'foo', 94 | array( 95 | 'text' => 'foo', 96 | 'html' => 'foo', 97 | 'bbcode' => 'foo', 98 | ) 99 | ), 100 | array( 101 | '[b]this is bold[/b]', 102 | array( 103 | 'text' => 'this is bold', 104 | 'html' => 'this is bold', 105 | 'bbcode' => '[b]this is bold[/b]', 106 | ) 107 | ), 108 | array( 109 | '[b]this is bold', 110 | array( 111 | 'text' => 'this is bold', 112 | 'html' => 'this is bold', 113 | 'bbcode' => '[b]this is bold[/b]', 114 | ) 115 | ), 116 | array( 117 | 'buffer text [b]this is bold[/b] buffer text', 118 | array( 119 | 'text' => 'buffer text this is bold buffer text', 120 | 'html' => 'buffer text this is bold buffer text', 121 | 'bbcode' => 'buffer text [b]this is bold[/b] buffer text', 122 | ) 123 | ), 124 | array( 125 | 'this is some text with [b]bold tags[/b] and [i]italics[/i] and things like [u]that[/u].', 126 | array( 127 | 'text' => 'this is some text with bold tags and italics and things like that.', 128 | 'html' => 'this is some text with bold tags and italics and things like that.', 129 | 'bbcode' => 'this is some text with [b]bold tags[/b] and [i]italics[/i] and things like [u]that[/u].', 130 | ) 131 | ), 132 | array( 133 | 'This contains a [url=http://jbbcode.com]url[/url] which uses an option.', 134 | array( 135 | 'text' => 'This contains a url which uses an option.', 136 | 'html' => 'This contains a url which uses an option.', 137 | 'bbcode' => 'This contains a [url=http://jbbcode.com]url[/url] which uses an option.', 138 | ) 139 | ), 140 | array( 141 | 'This doesn\'t use the url option [url]http://jbbcode.com[/url].', 142 | array( 143 | 'text' => 'This doesn\'t use the url option http://jbbcode.com.', 144 | 'html' => 'This doesn\'t use the url option http://jbbcode.com.', 145 | 'bbcode' => 'This doesn\'t use the url option [url]http://jbbcode.com[/url].', 146 | ) 147 | ), 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /JBBCode/tests/ParsingEdgeCaseTest.php: -------------------------------------------------------------------------------- 1 | addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 24 | $parser->parse($bbcode); 25 | return $parser->getAsHtml(); 26 | } 27 | 28 | /** 29 | * Asserts that the given bbcode matches the given html when 30 | * the bbcode is run through defaultParse. 31 | */ 32 | private function assertProduces($bbcode, $html) 33 | { 34 | $this->assertEquals($html, $this->defaultParse($bbcode)); 35 | } 36 | 37 | /** 38 | * Tests attempting to use a code that doesn't exist. 39 | */ 40 | public function testNonexistentCodeMalformed() 41 | { 42 | $this->assertProduces('[wat]', '[wat]'); 43 | } 44 | 45 | /** 46 | * Tests attempting to use a code that doesn't exist, but this 47 | * time in a well-formed fashion. 48 | * 49 | * @depends testNonexistentCodeMalformed 50 | */ 51 | public function testNonexistentCodeWellformed() 52 | { 53 | $this->assertProduces('[wat]something[/wat]', '[wat]something[/wat]'); 54 | } 55 | 56 | /** 57 | * Tests a whole bunch of meaningless left brackets. 58 | */ 59 | public function testAllLeftBrackets() 60 | { 61 | $this->assertProduces('[[[[[[[[', '[[[[[[[['); 62 | } 63 | 64 | /** 65 | * Tests a whole bunch of meaningless right brackets. 66 | */ 67 | public function testAllRightBrackets() 68 | { 69 | $this->assertProduces(']]]]]', ']]]]]'); 70 | } 71 | 72 | /** 73 | * Intermixes well-formed, meaningful tags with meaningless brackets. 74 | */ 75 | public function testRandomBracketsInWellformedCode() 76 | { 77 | $this->assertProduces('[b][[][[i]heh[/i][/b]', 78 | '[[][heh'); 79 | } 80 | 81 | /** 82 | * Tests an unclosed tag within a closed tag. 83 | */ 84 | public function testUnclosedWithinClosed() 85 | { 86 | $this->assertProduces('[url=http://jbbcode.com][b]oh yeah[/url]', 87 | 'oh yeah'); 88 | } 89 | 90 | /** 91 | * Tests half completed opening tag. 92 | */ 93 | public function testHalfOpenTag() 94 | { 95 | $this->assertProduces('[b', '[b'); 96 | $this->assertProduces('wut [url=http://jbbcode.com', 97 | 'wut [url=http://jbbcode.com'); 98 | } 99 | 100 | /** 101 | * Tests half completed closing tag. 102 | */ 103 | public function testHalfClosingTag() 104 | { 105 | $this->assertProduces('[b]this should be bold[/b', 106 | 'this should be bold[/b'); 107 | } 108 | 109 | /** 110 | * Tests lots of left brackets before the actual tag. For example: 111 | * [[[[[[[[b]bold![/b] 112 | */ 113 | public function testLeftBracketsThenTag() 114 | { 115 | $this->assertProduces('[[[[[b]bold![/b]', 116 | '[[[[bold!'); 117 | } 118 | 119 | /** 120 | * Tests a whitespace after left bracket. 121 | */ 122 | public function testWhitespaceAfterLeftBracketWhithoutTag() 123 | { 124 | $this->assertProduces('[ ABC ] ', 125 | '[ ABC ] '); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /JBBCode/tests/SimpleEvaluationTest.php: -------------------------------------------------------------------------------- 1 | addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 16 | $parser->parse($bbcode); 17 | return $parser->getAsHtml(); 18 | } 19 | 20 | /** 21 | * Asserts that the given bbcode matches the given html when 22 | * the bbcode is run through defaultParse. 23 | */ 24 | private function assertProduces($bbcode, $html) 25 | { 26 | $this->assertEquals($html, $this->defaultParse($bbcode)); 27 | } 28 | 29 | public function testCodeOptions() 30 | { 31 | $code = 'This contains a [url=http://jbbcode.com/?b=2]url[/url] which uses an option.'; 32 | $html = 'This contains a url which uses an option.'; 33 | $this->assertProduces($code, $html); 34 | } 35 | 36 | public function testAttributes() 37 | { 38 | $parser = new JBBCode\Parser(); 39 | $builder = new JBBCode\CodeDefinitionBuilder('img', '{alt}'); 40 | $parser->addCodeDefinition($builder->setUseOption(true)->setParseContent(false)->build()); 41 | 42 | $expected = 'Multiple alt text options.'; 43 | 44 | $code = 'Multiple [img height="50" alt="alt text"]http://jbbcode.com/img.png[/img] options.'; 45 | $parser->parse($code); 46 | $result = $parser->getAsHTML(); 47 | $this->assertEquals($expected, $result); 48 | 49 | $code = 'Multiple [img height=50 alt="alt text"]http://jbbcode.com/img.png[/img] options.'; 50 | $parser->parse($code); 51 | $result = $parser->getAsHTML(); 52 | $this->assertEquals($expected, $result); 53 | } 54 | 55 | public function testNestingTags() 56 | { 57 | $code = '[url=http://jbbcode.com][b]hello [u]world[/u][/b][/url]'; 58 | $html = 'hello world'; 59 | $this->assertProduces($code, $html); 60 | } 61 | 62 | public function testBracketInTag() 63 | { 64 | $this->assertProduces('[b]:-[[/b]', ':-['); 65 | } 66 | 67 | public function testBracketWithSpaceInTag() 68 | { 69 | $this->assertProduces('[b]:-[ [/b]', ':-[ '); 70 | } 71 | 72 | public function testBracketWithTextInTag() 73 | { 74 | $this->assertProduces('[b]:-[ foobar[/b]', ':-[ foobar'); 75 | } 76 | 77 | public function testMultibleBracketsWithTextInTag() 78 | { 79 | $this->assertProduces('[b]:-[ [fo[o[bar[/b]', ':-[ [fo[o[bar'); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /JBBCode/tests/TextNodeTest.php: -------------------------------------------------------------------------------- 1 | _textNode = new JBBCode\TextNode(''); 11 | } 12 | 13 | public function accept() 14 | { 15 | $mock = $this->getMock('JBBCode\NodeVisitor', 16 | array('visitDocumentElement', 'visitTextNode', 'visitElementNode')); 17 | $mock->expects($this->never()) 18 | ->method('visitDocumentElement'); 19 | $mock->expects($this->once()) 20 | ->method('visitTextNode') 21 | ->with($this->equalTo($this->_textNode)); 22 | $mock->expects($this->never()) 23 | ->method('visitElementNode'); 24 | $this->_textNode->accept($mock); 25 | } 26 | 27 | public function testIsTextNode() 28 | { 29 | $this->assertTrue($this->_textNode->isTextNode()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /JBBCode/tests/TokenizerTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($tokenizer->hasNext()); 15 | $this->assertNull($tokenizer->current()); 16 | $this->assertNull($tokenizer->next()); 17 | $this->assertEmpty($tokenizer->toString()); 18 | } 19 | 20 | public function testHasNext() 21 | { 22 | $tokenizer = new JBBCode\Tokenizer(''); 23 | $this->assertFalse($tokenizer->hasNext()); 24 | 25 | $tokenizer = new JBBCode\Tokenizer('['); 26 | $this->assertTrue($tokenizer->hasNext()); 27 | $tokenizer->next(); 28 | $this->assertFalse($tokenizer->hasNext()); 29 | } 30 | 31 | public function testNext() 32 | { 33 | $tokenizer = new JBBCode\Tokenizer('['); 34 | $this->assertEquals('[', $tokenizer->next()); 35 | $this->assertNull($tokenizer->next()); 36 | } 37 | 38 | public function testCurrent() 39 | { 40 | $tokenizer = new JBBCode\Tokenizer('['); 41 | $this->assertNull($tokenizer->current()); 42 | $tokenizer->next(); 43 | $this->assertEquals('[', $tokenizer->current()); 44 | } 45 | 46 | public function testStepBack() 47 | { 48 | $tokenizer = new JBBCode\Tokenizer(''); 49 | $tokenizer->stepBack(); 50 | $this->assertFalse($tokenizer->hasNext()); 51 | 52 | $tokenizer = new JBBCode\Tokenizer('['); 53 | $this->assertTrue($tokenizer->hasNext()); 54 | $this->assertEquals('[', $tokenizer->next()); 55 | $this->assertFalse($tokenizer->hasNext()); 56 | $tokenizer->stepBack(); 57 | $this->assertTrue($tokenizer->hasNext()); 58 | $this->assertEquals('[', $tokenizer->next()); 59 | } 60 | 61 | public function testRestart() 62 | { 63 | $tokenizer = new JBBCode\Tokenizer(''); 64 | $tokenizer->restart(); 65 | $this->assertFalse($tokenizer->hasNext()); 66 | 67 | $tokenizer = new JBBCode\Tokenizer('['); 68 | $tokenizer->next(); 69 | $tokenizer->restart(); 70 | $this->assertTrue($tokenizer->hasNext()); 71 | } 72 | 73 | public function testToString() 74 | { 75 | $tokenizer = new JBBCode\Tokenizer('['); 76 | $this->assertEquals('[', $tokenizer->toString()); 77 | $tokenizer->next(); 78 | $this->assertEmpty($tokenizer->toString()); 79 | } 80 | 81 | /** 82 | * @param string[] $tokens 83 | * @dataProvider tokenProvider() 84 | */ 85 | public function testTokenize($tokens) 86 | { 87 | $string = implode('', $tokens); 88 | $tokenizer = new JBBCode\Tokenizer($string); 89 | $this->assertEquals($string, $tokenizer->toString()); 90 | 91 | $this->assertTrue($tokenizer->hasNext()); 92 | $this->assertNull($tokenizer->current()); 93 | 94 | foreach ($tokens as $token) { 95 | $this->assertEquals($token, $tokenizer->next()); 96 | } 97 | 98 | $this->assertNull($tokenizer->next()); 99 | $this->assertFalse($tokenizer->hasNext()); 100 | } 101 | 102 | public function tokenProvider() 103 | { 104 | return array( 105 | array( 106 | array('foo'), 107 | ), 108 | array( 109 | array('foo', '[', 'b', ']', 'bar'), 110 | ), 111 | array( 112 | array('[', 'foo', ']'), 113 | ), 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /JBBCode/tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | _validator = new JBBCode\validators\CssColorValidator(); 14 | } 15 | 16 | /** 17 | * @param string $color 18 | * @dataProvider validColorProvider 19 | */ 20 | public function testValidColors($color) 21 | { 22 | $this->assertTrue($this->_validator->validate($color)); 23 | } 24 | 25 | public function validColorProvider() 26 | { 27 | return array( 28 | array('red'), 29 | array('yellow'), 30 | array('LightGoldenRodYellow'), 31 | array('#000'), 32 | array('#00ff00'), 33 | array('rgba(255, 0, 0, 0.5)'), 34 | array('rgba(50, 50, 50, 0.0)'), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /JBBCode/tests/validators/FnValidatorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($validator->validate('1234567890')); 14 | $this->assertFalse($validator->validate('QWERTZUIOP')); 15 | } 16 | 17 | /** 18 | * Provide custom numeric string validator implementations. 19 | * 20 | */ 21 | public function validatorProvider() 22 | { 23 | return array( 24 | array(new JBBCode\validators\FnValidator('is_numeric')), 25 | array(new JBBCode\validators\FnValidator(function ($input) { 26 | return is_numeric($input); 27 | })), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /JBBCode/tests/validators/UrlValidatorTest.php: -------------------------------------------------------------------------------- 1 | _validator = new JBBCode\validators\UrlValidator(); 14 | } 15 | 16 | /** 17 | * @param string $url 18 | * @dataProvider invalidUrlProvider 19 | */ 20 | public function testInvalidUrl($url) 21 | { 22 | $this->assertFalse($this->_validator->validate($url)); 23 | } 24 | 25 | /** 26 | * @param string $url 27 | * @dataProvider validUrlProvider 28 | */ 29 | public function testValidUrl($url) 30 | { 31 | $this->assertTrue($this->_validator->validate($url)); 32 | } 33 | 34 | public function invalidUrlProvider() 35 | { 36 | return array( 37 | array('#yolo#swag'), 38 | array('giehtiehwtaw352353%3'), 39 | ); 40 | } 41 | 42 | public function validUrlProvider() 43 | { 44 | return array( 45 | array('http://google.com'), 46 | array('http://jbbcode.com/docs'), 47 | array('https://www.maps.google.com'), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /JBBCode/tests/validators/ValidatorTest.php: -------------------------------------------------------------------------------- 1 | addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 19 | $parser->parse('[url=javascript:alert("HACKED!");]click me[/url]'); 20 | $this->assertEquals('[url=javascript:alert("HACKED!");]click me[/url]', 21 | $parser->getAsHtml()); 22 | } 23 | 24 | /** 25 | * Tests an invalid url as the body to a url bbcode. 26 | * 27 | */ 28 | public function testInvalidBodyUrlBBCode() 29 | { 30 | $parser = new JBBCode\Parser(); 31 | $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 32 | $parser->parse('[url]javascript:alert("HACKED!");[/url]'); 33 | $this->assertEquals('[url]javascript:alert("HACKED!");[/url]', $parser->getAsHtml()); 34 | } 35 | 36 | /** 37 | * Tests a valid url as the body to a url bbcode. 38 | * 39 | */ 40 | public function testValidUrlBBCode() 41 | { 42 | $parser = new JBBCode\Parser(); 43 | $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 44 | $parser->parse('[url]http://jbbcode.com[/url]'); 45 | $this->assertEquals('http://jbbcode.com', 46 | $parser->getAsHtml()); 47 | } 48 | 49 | /** 50 | * Tests invalid CSS color values on the CssColorValidator. 51 | */ 52 | public function testInvalidCssColor() 53 | { 54 | $colorValidator = new JBBCode\validators\CssColorValidator(); 55 | $this->assertFalse($colorValidator->validate('" onclick="javascript: alert(\"gotcha!\");')); 56 | $this->assertFalse($colorValidator->validate('">assertProduces('[b]te"xt te&xt[/b]', 'te"xt te&xt'); 42 | } 43 | 44 | /** 45 | * Tests escaping HTML tags 46 | */ 47 | public function testHtmlTag() 48 | { 49 | $this->assertProduces('not bold', '<b>not bold</b>'); 50 | $this->assertProduces('[b]bold[/b]
', '<b>bold</b> <hr>'); 51 | } 52 | 53 | /** 54 | * Tests escaping ampersands in URL using [url]...[/url] 55 | */ 56 | public function testUrlParam() 57 | { 58 | $this->assertProduces('text [url]http://example.com/?a=b&c=d[/url] more text', 'text http://example.com/?a=b&c=d more text'); 59 | } 60 | 61 | /** 62 | * Tests escaping ampersands in URL using [url=...] tag 63 | */ 64 | public function testUrlOption() 65 | { 66 | $this->assertProduces('text [url=http://example.com/?a=b&c=d]this is a "link"[/url]', 'text this is a "link"'); 67 | } 68 | 69 | /** 70 | * Tests escaping ampersands in URL using [url=...] tag when URL is in quotes 71 | */ 72 | public function testUrlOptionQuotes() 73 | { 74 | $this->assertProduces('text [url="http://example.com/?a=b&c=d"]this is a "link"[/url]', 'text this is a "link"'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /JBBCode/tests/visitors/NestLimitVisitorTest.php: -------------------------------------------------------------------------------- 1 | _nestLimitVisitor = new \JBBCode\visitors\NestLimitVisitor(); 19 | } 20 | 21 | public function testVisitDocumentElement() 22 | { 23 | $childMock = $this->getMock('JBBCode\ElementNode', array('accept')); 24 | $childMock->expects($this->once()) 25 | ->method('accept') 26 | ->with($this->equalTo($this->_nestLimitVisitor)); 27 | 28 | $mock = $this->getMock('JBBCode\DocumentElement', array('getChildren')); 29 | $mock->expects($this->once()) 30 | ->method('getChildren') 31 | ->will($this->returnValue(array( 32 | $childMock 33 | ))); 34 | 35 | $this->_nestLimitVisitor->visitDocumentElement($mock); 36 | } 37 | 38 | public function testVisitTextNode() 39 | { 40 | $mock = $this->getMockBuilder('JBBCode\TextNode') 41 | ->setMethods(array('accept')) 42 | ->disableOriginalConstructor() 43 | ->getMock(); 44 | $mock->expects($this->never()) 45 | ->method('accept'); 46 | 47 | $this->_nestLimitVisitor->visitTextNode($mock); 48 | } 49 | 50 | /** 51 | * Tests that when elements have no nest limits they may be 52 | * nested indefinitely. 53 | */ 54 | public function testIndefiniteNesting() 55 | { 56 | $parser = new JBBCode\Parser(); 57 | $parser->addBBCode('b', '{param}', false, true, -1); 58 | $parser->parse('[b][b][b][b][b][b][b][b]bold text[/b][/b][/b][/b][/b][/b][/b][/b]'); 59 | $this->assertEquals('' . 60 | 'bold text' . 61 | '', 62 | $parser->getAsHtml()); 63 | } 64 | 65 | /** 66 | * Test over nesting. 67 | */ 68 | public function testOverNesting() 69 | { 70 | $parser = new JBBCode\Parser(); 71 | $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 72 | $parser->addBBCode('quote', '
{param}
', false, true, 2); 73 | $bbcode = '[quote][quote][quote]wut[/quote] huh?[/quote] i don\'t know[/quote]'; 74 | $parser->parse($bbcode); 75 | $expectedBbcode = '[quote][quote] huh?[/quote] i don\'t know[/quote]'; 76 | $expectedHtml = '
huh?
i don\'t know
'; 77 | $this->assertEquals($expectedBbcode, $parser->getAsBBCode()); 78 | $this->assertEquals($expectedHtml, $parser->getAsHtml()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /JBBCode/tests/visitors/SmileyVisitorTest.php: -------------------------------------------------------------------------------- 1 | _smileyVisitor = new \JBBCode\visitors\SmileyVisitor(); 13 | } 14 | 15 | public function testVisitDocumentElement() 16 | { 17 | $childMock = $this->getMock('JBBCode\ElementNode', array('accept')); 18 | $childMock->expects($this->once()) 19 | ->method('accept') 20 | ->with($this->equalTo($this->_smileyVisitor)); 21 | 22 | $mock = $this->getMock('JBBCode\DocumentElement', array('getChildren')); 23 | $mock->expects($this->once()) 24 | ->method('getChildren') 25 | ->will($this->returnValue(array( 26 | $childMock, 27 | ))); 28 | 29 | $this->_smileyVisitor->visitDocumentElement($mock); 30 | } 31 | 32 | public function testVisitElementNode() 33 | { 34 | $builder = new \JBBCode\CodeDefinitionBuilder('foo', 'bar'); 35 | $builder->setParseContent(false); 36 | 37 | $mock = $this->getMock('JBBCode\DocumentElement', array('getChildren', 'getCodeDefinition')); 38 | $mock->expects($this->never()) 39 | ->method('getChildren'); 40 | $mock->expects($this->once()) 41 | ->method('getCodeDefinition') 42 | ->will($this->returnValue( 43 | $builder->build() 44 | )); 45 | $this->_smileyVisitor->visitElementNode($mock); 46 | 47 | $childMock = $this->getMock('JBBCode\ElementNode', array('accept', 'parseContent')); 48 | $childMock->expects($this->once()) 49 | ->method('accept') 50 | ->with($this->equalTo($this->_smileyVisitor)); 51 | 52 | $mock = $this->getMock('JBBCode\DocumentElement', array('getChildren', 'getCodeDefinition')); 53 | $mock->expects($this->once()) 54 | ->method('getChildren') 55 | ->will($this->returnValue(array( 56 | $childMock, 57 | ))); 58 | $mock->expects($this->once()) 59 | ->method('getCodeDefinition') 60 | ->will($this->returnValue($builder->setParseContent(true)->build())); 61 | $this->_smileyVisitor->visitElementNode($mock); 62 | } 63 | 64 | public function testVisitTextNodeEmpty() 65 | { 66 | $textNode = new JBBCode\TextNode(''); 67 | $textNode->accept($this->_smileyVisitor); 68 | $this->assertEmpty($textNode->getValue()); 69 | } 70 | 71 | /** 72 | * @param $string 73 | * @dataProvider smileyProvider() 74 | */ 75 | public function testVisitTextNode($string) 76 | { 77 | $textNode = new JBBCode\TextNode($string); 78 | $textNode->accept($this->_smileyVisitor); 79 | $this->assertNotFalse(strpos($textNode->getValue(), ':)')); 80 | } 81 | 82 | public function smileyProvider() 83 | { 84 | return array( 85 | array( ':)'), 86 | array( ':) foo'), 87 | array( 'foo :)'), 88 | array( 'foo :) bar'), 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /JBBCode/tests/visitors/TagCountingVisitorTest.php: -------------------------------------------------------------------------------- 1 | _tagCountingVisitor = new JBBCode\visitors\TagCountingVisitor(); 13 | } 14 | 15 | public function testVisitTextNode() 16 | { 17 | $mock = $this->getMock('JBBCode\TextNode', array('accept'), array('')); 18 | $mock->expects($this->never()) 19 | ->method('accept'); 20 | $this->_tagCountingVisitor->visitTextNode($mock); 21 | } 22 | 23 | /** 24 | * @covers JBBCode\visitors\TagCountingVisitor::getFrequency() 25 | * @covers JBBCode\visitors\TagCountingVisitor::visitElementNode() 26 | */ 27 | public function testVisitElementNode() 28 | { 29 | $childMock = $this->getMock('JBBCode\ElementNode', array('accept')); 30 | $childMock->expects($this->once()) 31 | ->method('accept') 32 | ->with($this->equalTo($this->_tagCountingVisitor)); 33 | 34 | $mock = $this->getMock('JBBCode\ElementNode', array('getChildren', 'getTagName')); 35 | $mock->expects($this->once()) 36 | ->method('getChildren') 37 | ->will($this->returnValue(array( 38 | $childMock, 39 | ))); 40 | $mock->expects($this->once()) 41 | ->method('getTagName') 42 | ->will($this->returnValue('foo')); 43 | 44 | $this->assertEquals(0, $this->_tagCountingVisitor->getFrequency('foo')); 45 | 46 | $this->_tagCountingVisitor->visitElementNode($mock); 47 | $this->assertEquals(1, $this->_tagCountingVisitor->getFrequency('foo')); 48 | 49 | $mock = $this->getMock('JBBCode\ElementNode', array('getChildren', 'getTagName')); 50 | $mock->expects($this->once()) 51 | ->method('getChildren') 52 | ->will($this->returnValue(array())); 53 | $mock->expects($this->once()) 54 | ->method('getTagName') 55 | ->will($this->returnValue('foo')); 56 | 57 | $this->_tagCountingVisitor->visitElementNode($mock); 58 | $this->assertEquals(2, $this->_tagCountingVisitor->getFrequency('foo')); 59 | } 60 | 61 | public function testVisitDocumentElement() 62 | { 63 | $childMock = $this->getMock('JBBCode\ElementNode', array('accept')); 64 | $childMock->expects($this->once()) 65 | ->method('accept') 66 | ->with($this->equalTo($this->_tagCountingVisitor)); 67 | 68 | $mock = $this->getMock('JBBCode\DocumentElement', array('getChildren')); 69 | $mock->expects($this->once()) 70 | ->method('getChildren') 71 | ->will($this->returnValue(array( 72 | $childMock, 73 | ))); 74 | 75 | $this->_tagCountingVisitor->visitDocumentElement($mock); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /JBBCode/validators/CssColorValidator.php: -------------------------------------------------------------------------------- 1 | validator = $validator; 28 | } 29 | 30 | /** 31 | * Returns true iff the given input is valid, false otherwise. 32 | * @param string $input 33 | * @return boolean 34 | */ 35 | public function validate($input) 36 | { 37 | $validator = $this->validator; // FIXME: for PHP>=7.0 replace with ($this->validator)($input) 38 | return (bool) $validator($input); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /JBBCode/validators/UrlValidator.php: -------------------------------------------------------------------------------- 1 | getChildren() as $child) { 15 | $child->accept($this); 16 | } 17 | } 18 | 19 | public function visitTextNode(\JBBCode\TextNode $textNode) 20 | { 21 | $textNode->setValue($this->htmlSafe($textNode->getValue())); 22 | } 23 | 24 | public function visitElementNode(\JBBCode\ElementNode $elementNode) 25 | { 26 | $attrs = $elementNode->getAttribute(); 27 | if (is_array($attrs)) { 28 | foreach ($attrs as &$el) { 29 | $el = $this->htmlSafe($el); 30 | } 31 | 32 | $elementNode->setAttribute($attrs); 33 | } 34 | 35 | foreach ($elementNode->getChildren() as $child) { 36 | $child->accept($this); 37 | } 38 | } 39 | 40 | protected function htmlSafe($str, $options = null) 41 | { 42 | if (is_null($options)) { 43 | if (defined('ENT_DISALLOWED')) { 44 | $options = ENT_QUOTES | ENT_DISALLOWED | ENT_HTML401; 45 | } // PHP 5.4+ 46 | else { 47 | $options = ENT_QUOTES; 48 | } // PHP 5.3 49 | } 50 | 51 | return htmlspecialchars($str, $options, 'UTF-8'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /JBBCode/visitors/NestLimitVisitor.php: -------------------------------------------------------------------------------- 1 | getChildren() as $child) { 28 | $child->accept($this); 29 | } 30 | } 31 | 32 | public function visitTextNode(\JBBCode\TextNode $textNode) 33 | { 34 | /* Nothing to do. Text nodes don't have tag names or children. */ 35 | } 36 | 37 | public function visitElementNode(\JBBCode\ElementNode $elementNode) 38 | { 39 | $tagName = strtolower($elementNode->getTagName()); 40 | 41 | /* Update the current depth for this tag name. */ 42 | if (isset($this->depth[$tagName])) { 43 | $this->depth[$tagName]++; 44 | } else { 45 | $this->depth[$tagName] = 1; 46 | } 47 | 48 | /* Check if $elementNode is nested too deeply. */ 49 | if ($elementNode->getCodeDefinition()->getNestLimit() != -1 && 50 | $elementNode->getCodeDefinition()->getNestLimit() < $this->depth[$tagName]) { 51 | /* This element is nested too deeply. We need to remove it and not visit any 52 | * of its children. */ 53 | $elementNode->getParent()->removeChild($elementNode); 54 | } else { 55 | /* This element is not nested too deeply. Visit all of its children. */ 56 | foreach ($elementNode->getChildren() as $child) { 57 | $child->accept($this); 58 | } 59 | } 60 | 61 | /* Now that we're done visiting this node, decrement the depth. */ 62 | $this->depth[$tagName]--; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /JBBCode/visitors/SmileyVisitor.php: -------------------------------------------------------------------------------- 1 | getChildren() as $child) { 17 | $child->accept($this); 18 | } 19 | } 20 | 21 | public function visitTextNode(\JBBCode\TextNode $textNode) 22 | { 23 | /* Convert :) into an image tag. */ 24 | $textNode->setValue(str_replace(':)', 25 | ':)', 26 | $textNode->getValue())); 27 | } 28 | 29 | public function visitElementNode(\JBBCode\ElementNode $elementNode) 30 | { 31 | /* We only want to visit text nodes within elements if the element's 32 | * code definition allows for its content to be parsed. 33 | */ 34 | if ($elementNode->getCodeDefinition()->parseContent()) { 35 | foreach ($elementNode->getChildren() as $child) { 36 | $child->accept($this); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /JBBCode/visitors/TagCountingVisitor.php: -------------------------------------------------------------------------------- 1 | getChildren() as $child) { 19 | $child->accept($this); 20 | } 21 | } 22 | 23 | public function visitTextNode(\JBBCode\TextNode $textNode) 24 | { 25 | // Nothing to do here, text nodes do not have tag names or children 26 | } 27 | 28 | public function visitElementNode(\JBBCode\ElementNode $elementNode) 29 | { 30 | $tagName = strtolower($elementNode->getTagName()); 31 | 32 | // Update this tag name's frequency 33 | if (isset($this->frequencies[$tagName])) { 34 | $this->frequencies[$tagName]++; 35 | } else { 36 | $this->frequencies[$tagName] = 1; 37 | } 38 | 39 | // Visit all the node's childrens 40 | foreach ($elementNode->getChildren() as $child) { 41 | $child->accept($this); 42 | } 43 | } 44 | 45 | /** 46 | * Retrieves the frequency of the given tag name. 47 | * 48 | * @param string $tagName the tag name to look up 49 | * 50 | * @return integer 51 | */ 52 | public function getFrequency($tagName) 53 | { 54 | if (!isset($this->frequencies[$tagName])) { 55 | return 0; 56 | } else { 57 | return $this->frequencies[$tagName]; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2011 Jackson Owens 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jBBCode 2 | ======= 3 | [![GitHub release](https://img.shields.io/github/release/jbowens/jBBCode.svg)](https://github.com/jbowens/jBBCode/releases) 4 | [![Software License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md) 5 | [![Build Status](https://img.shields.io/travis/jbowens/jBBCode.svg)](https://travis-ci.org/jbowens/jBBCode) 6 | 7 | jBBCode is a bbcode parser written in php 5.3. It's relatively lightweight and parses 8 | bbcodes without resorting to expensive regular expressions. 9 | 10 | Documentation 11 | ------------- 12 | 13 | For complete documentation and examples visit [jbbcode.com](http://jbbcode.com). 14 | 15 | ### A basic example 16 | 17 | jBBCode includes a few optional, default bbcode definitions that may be loaded through the 18 | `DefaultCodeDefinitionSet` class. Below is a simple example of using these codes to convert 19 | a bbcode string to html. 20 | 21 | ```php 22 | addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet()); 27 | 28 | $text = "The default codes include: [b]bold[/b], [i]italics[/i], [u]underlining[/u], "; 29 | $text .= "[url=http://jbbcode.com]links[/url], [color=red]color![/color] and more."; 30 | 31 | $parser->parse($text); 32 | 33 | print $parser->getAsHtml(); 34 | ``` 35 | 36 | ### Composer 37 | 38 | You may load jBBCode via composer. In your composer.json file: 39 | 40 | ```json 41 | "require": { 42 | "jbbcode/jbbcode": "1.3.*" 43 | } 44 | ``` 45 | 46 | In your php file: 47 | 48 | ```php 49 | require 'vendor/autoloader.php'; 50 | 51 | $parser = new JBBCode\Parser(); 52 | ``` 53 | 54 | Contribute 55 | ---------- 56 | 57 | I would love help maintaining jBBCode. Look at [open issues](http://github.com/jbowens/jBBCode/issues) for ideas on 58 | what needs to be done. Before submitting a pull request, verify that all unit tests still pass. 59 | 60 | #### Running unit tests 61 | To run the unit tests, 62 | ensure that [phpunit](http://github.com/sebastianbergmann/phpunit) is installed, or install it through the composer 63 | dev dependencies. Then run `phpunit` from the project directory. If you're adding new functionality, writing 64 | additional unit tests is a great idea. 65 | 66 | 67 | License 68 | ------- 69 | 70 | The project is under MIT license. Please see the [license file](LICENSE.md) for details. 71 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jbbcode/jbbcode", 3 | "type": "library", 4 | "description": "A lightweight but extensible BBCode parser written in PHP 5.3.", 5 | "keywords": ["BBCode", "BB"], 6 | "homepage": "http://jbbcode.com/", 7 | "license": "MIT", 8 | "require": { 9 | "php": ">=5.3.0" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "4.5.*", 13 | "satooshi/php-coveralls": "0.6.*", 14 | "friendsofphp/php-cs-fixer": "^2.1" 15 | }, 16 | "authors": [ 17 | { 18 | "name": "Jackson Owens", 19 | "email": "jackson_owens@alumni.brown.edu", 20 | "homepage": "http://jbowens.org/", 21 | "role": "Developer" 22 | } 23 | ], 24 | "autoload": { 25 | "psr-0": { 26 | "JBBCode": "." 27 | } 28 | }, 29 | "minimum-stability": "stable" 30 | } 31 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | JBBCode/tests 13 | 14 | 15 | 16 | 17 | examples/ 18 | vendor 19 | 20 | 21 | 22 | 23 | 24 | 25 | --------------------------------------------------------------------------------