jBBCode used

This commit is contained in:
Miroslav Šedivý 2017-09-24 20:01:23 +02:00
parent 31786706d1
commit a5926a5179
38 changed files with 3268 additions and 24 deletions

View file

@ -0,0 +1,328 @@
<?php
namespace JBBCode;
/**
* This class represents a BBCode Definition. You may construct instances of this class directly,
* usually through the CodeDefinitionBuilder class, to create text replacement bbcodes, or you
* may subclass it to create more complex bbcode definitions.
*
* @author jbowens
*/
class CodeDefinition
{
/* NOTE: THIS PROPERTY SHOULD ALWAYS BE LOWERCASE; USE setTagName() TO ENSURE THIS */
protected $tagName;
/* Whether or not this CodeDefinition uses an option parameter. */
protected $useOption;
/* The replacement text to be used for simple CodeDefinitions */
protected $replacementText;
/* Whether or not to parse elements of this definition's contents */
protected $parseContent;
/* How many of this element type may be nested within each other */
protected $nestLimit;
/* How many of this element type have been seen */
protected $elCounter;
/* The input validator to run options through */
protected $optionValidator;
/* The input validator to run the body ({param}) through */
protected $bodyValidator;
/**
* Constructs a new CodeDefinition.
*/
public static function construct($tagName, $replacementText, $useOption = false,
$parseContent = true, $nestLimit = -1, $optionValidator = array(),
$bodyValidator = null)
{
$def = new CodeDefinition();
$def->elCounter = 0;
$def->setTagName($tagName);
$def->setReplacementText($replacementText);
$def->useOption = $useOption;
$def->parseContent = $parseContent;
$def->nestLimit = $nestLimit;
$def->optionValidator = $optionValidator;
$def->bodyValidator = $bodyValidator;
return $def;
}
/**
* Constructs a new CodeDefinition.
*
* This constructor is deprecated. You should use the static construct() method or the
* CodeDefinitionBuilder class to construct a new CodeDefiniton.
*
* @deprecated
*/
public function __construct()
{
/* WARNING: This function is deprecated and will be made protected in a future
* version of jBBCode. */
$this->parseContent = true;
$this->useOption = false;
$this->nestLimit = -1;
$this->elCounter = 0;
$this->optionValidator = array();
$this->bodyValidator = null;
}
/**
* Determines if the arguments to the given element are valid based on
* any validators attached to this CodeDefinition.
*
* @param $el the ElementNode to validate
* @return true if the ElementNode's {option} and {param} are OK, false if they're not
*/
public function hasValidInputs(ElementNode $el)
{
if ($this->usesOption() && $this->optionValidator) {
$att = $el->getAttribute();
foreach($att as $name => $value){
if(isset($this->optionValidator[$name]) && !$this->optionValidator[$name]->validate($value)){
return false;
}
}
}
if (!$this->parseContent() && $this->bodyValidator) {
/* We only evaluate the content if we're not parsing the content. */
$content = "";
foreach ($el->getChildren() as $child) {
$content .= $child->getAsBBCode();
}
if (!$this->bodyValidator->validate($content)) {
/* The content of the element is not valid. */
return false;
}
}
return true;
}
/**
* Accepts an ElementNode that is defined by this CodeDefinition and returns the HTML
* markup of the element. This is a commonly overridden class for custom CodeDefinitions
* so that the content can be directly manipulated.
*
* @param $el the element to return an html representation of
*
* @return the parsed html of this element (INCLUDING ITS CHILDREN)
*/
public function asHtml(ElementNode $el)
{
if (!$this->hasValidInputs($el)) {
return $el->getAsBBCode();
}
$html = $this->getReplacementText();
if ($this->usesOption()) {
$options = $el->getAttribute();
if(count($options)==1){
$vals = array_values($options);
$html = str_ireplace('{option}', reset($vals), $html);
}
else{
foreach($options as $key => $val){
$html = str_ireplace('{' . $key . '}', $val, $html);
}
}
}
$content = $this->getContent($el);
$html = str_ireplace('{param}', $content, $html);
return $html;
}
protected function getContent(ElementNode $el){
if ($this->parseContent()) {
$content = "";
foreach ($el->getChildren() as $child)
$content .= $child->getAsHTML();
} else {
$content = "";
foreach ($el->getChildren() as $child)
$content .= $child->getAsBBCode();
}
return $content;
}
/**
* Accepts an ElementNode that is defined by this CodeDefinition and returns the text
* representation of the element. This may be overridden by a custom CodeDefinition.
*
* @param $el the element to return a text representation of
*
* @return the text representation of $el
*/
public function asText(ElementNode $el)
{
if (!$this->hasValidInputs($el)) {
return $el->getAsBBCode();
}
$s = "";
foreach ($el->getChildren() as $child)
$s .= $child->getAsText();
return $s;
}
/**
* Returns the tag name of this code definition
*
* @return this definition's associated tag name
*/
public function getTagName()
{
return $this->tagName;
}
/**
* Returns the replacement text of this code definition. This usually has little, if any meaning if the
* CodeDefinition class was extended. For default, html replacement CodeDefinitions this returns the html
* markup for the definition.
*
* @return the replacement text of this CodeDefinition
*/
public function getReplacementText()
{
return $this->replacementText;
}
/**
* Returns whether or not this CodeDefinition uses the optional {option}
*
* @return true if this CodeDefinition uses the option, false otherwise
*/
public function usesOption()
{
return $this->useOption;
}
/**
* Returns whether or not this CodeDefnition parses elements contained within it,
* or just treats its children as text.
*
* @return true if this CodeDefinition parses elements contained within itself
*/
public function parseContent()
{
return $this->parseContent;
}
/**
* Returns the limit of how many elements defined by this CodeDefinition may be
* nested together. If after parsing elements are nested beyond this limit, the
* subtrees formed by those nodes will be removed from the parse tree. A nest
* limit of -1 signifies no limit.
*/
public function getNestLimit()
{
return $this->nestLimit;
}
/**
* Sets the tag name of this CodeDefinition
*
* @deprecated
*
* @param the new tag name of this definition
*/
public function setTagName($tagName)
{
$this->tagName = strtolower($tagName);
}
/**
* Sets the html replacement text of this CodeDefinition
*
* @deprecated
*
* @param the new replacement text
*/
public function setReplacementText($txt)
{
$this->replacementText = $txt;
}
/**
* Sets whether or not this CodeDefinition uses the {option}
*
* @deprecated
*
* @param boolean $bool
*/
public function setUseOption($bool)
{
$this->useOption = $bool;
}
/**
* Sets whether or not this CodeDefinition allows its children to be parsed as html
*
* @deprecated
*
* @param boolean $bool
*/
public function setParseContent($bool)
{
$this->parseContent = $bool;
}
/**
* Increments the element counter. This is used for tracking depth of elements of the same type for next limits.
*
* @deprecated
*
* @return void
*/
public function incrementCounter()
{
$this->elCounter++;
}
/**
* Decrements the element counter.
*
* @deprecated
*
* @return void
*/
public function decrementCounter()
{
$this->elCounter--;
}
/**
* Resets the element counter.
*
* @deprecated
*/
public function resetCounter()
{
$this->elCounter = 0;
}
/**
* Returns the current value of the element counter.
*
* @deprecated
*
* @return int
*/
public function getCounter()
{
return $this->elCounter;
}
}

View file

@ -0,0 +1,160 @@
<?php
namespace JBBCode;
require_once "CodeDefinition.php";
/**
* Implements the builder pattern for the CodeDefinition class. A builder
* is the recommended way of constructing CodeDefinition objects.
*
* @author jbowens
*/
class CodeDefinitionBuilder
{
protected $tagName;
protected $useOption = false;
protected $replacementText;
protected $parseContent = true;
protected $nestLimit = -1;
protected $optionValidator = array();
protected $bodyValidator = null;
/**
* Construct a CodeDefinitionBuilder.
*
* @param $tagName the tag name of the definition to build
* @param $replacementText the replacement text of the definition to build
*/
public function __construct($tagName, $replacementText)
{
$this->tagName = $tagName;
$this->replacementText = $replacementText;
}
/**
* Sets the tag name the CodeDefinition should be built with.
*
* @param $tagName the tag name for the new CodeDefinition
*/
public function setTagName($tagName)
{
$this->tagName = $tagName;
return $this;
}
/**
* Sets the replacement text that the new CodeDefinition should be
* built with.
*
* @param $replacementText the replacement text for the new CodeDefinition
*/
public function setReplacementText($replacementText)
{
$this->replacementText = $replacementText;
return $this;
}
/**
* Set whether or not the built CodeDefinition should use the {option} bbcode
* argument.
*
* @param $option ture iff the definition includes an option
*/
public function setUseOption($option)
{
$this->useOption = $option;
return $this;
}
/**
* Set whether or not the built CodeDefinition should allow its content
* to be parsed and evaluated as bbcode.
*
* @param $parseContent true iff the content should be parsed
*/
public function setParseContent($parseContent)
{
$this->parseContent = $parseContent;
return $this;
}
/**
* Sets the nest limit for this code definition.
*
* @param $nestLimit a positive integer, or -1 if there is no limit.
* @throws \InvalidArgumentException if the nest limit is invalid
*/
public function setNestLimit($limit)
{
if(!is_int($limit) || ($limit <= 0 && -1 != $limit)) {
throw new \InvalidArgumentException("A nest limit must be a positive integer " .
"or -1.");
}
$this->nestLimit = $limit;
return $this;
}
/**
* Sets the InputValidator that option arguments should be validated with.
*
* @param $validator the InputValidator instance to use
*/
public function setOptionValidator(\JBBCode\InputValidator $validator, $option=null)
{
if(empty($option)){
$option = $this->tagName;
}
$this->optionValidator[$option] = $validator;
return $this;
}
/**
* Sets the InputValidator that body ({param}) text should be validated with.
*
* @param $validator the InputValidator instance to use
*/
public function setBodyValidator(\JBBCode\InputValidator $validator)
{
$this->bodyValidator = $validator;
return $this;
}
/**
* Removes the attached option validator if one is attached.
*/
public function removeOptionValidator()
{
$this->optionValidator = array();
return $this;
}
/**
* Removes the attached body validator if one is attached.
*/
public function removeBodyValidator()
{
$this->bodyValidator = null;
return $this;
}
/**
* Builds a CodeDefinition with the current state of the builder.
*
* @return a new CodeDefinition instance
*/
public function build()
{
$definition = CodeDefinition::construct($this->tagName,
$this->replacementText,
$this->useOption,
$this->parseContent,
$this->nestLimit,
$this->optionValidator,
$this->bodyValidator);
return $definition;
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace JBBCode;
require_once 'CodeDefinition.php';
use JBBCode\CodeDefinition;
/**
* An interface for sets of code definitons.
*
* @author jbowens
*/
interface CodeDefinitionSet
{
/**
* Retrieves the CodeDefinitions within this set as an array.
*/
public function getCodeDefinitions();
}

View file

@ -0,0 +1,75 @@
<?php
namespace JBBCode;
require_once 'CodeDefinition.php';
require_once 'CodeDefinitionBuilder.php';
require_once 'CodeDefinitionSet.php';
require_once 'validators/CssColorValidator.php';
require_once 'validators/UrlValidator.php';
/**
* Provides a default set of common bbcode definitions.
*
* @author jbowens
*/
class DefaultCodeDefinitionSet implements CodeDefinitionSet
{
/* The default code definitions in this set. */
protected $definitions = array();
/**
* Constructs the default code definitions.
*/
public function __construct()
{
/* [b] bold tag */
$builder = new CodeDefinitionBuilder('b', '<strong>{param}</strong>');
array_push($this->definitions, $builder->build());
/* [i] italics tag */
$builder = new CodeDefinitionBuilder('i', '<em>{param}</em>');
array_push($this->definitions, $builder->build());
/* [u] underline tag */
$builder = new CodeDefinitionBuilder('u', '<u>{param}</u>');
array_push($this->definitions, $builder->build());
$urlValidator = new \JBBCode\validators\UrlValidator();
/* [url] link tag */
$builder = new CodeDefinitionBuilder('url', '<a href="{param}">{param}</a>');
$builder->setParseContent(false)->setBodyValidator($urlValidator);
array_push($this->definitions, $builder->build());
/* [url=http://example.com] link tag */
$builder = new CodeDefinitionBuilder('url', '<a href="{option}">{param}</a>');
$builder->setUseOption(true)->setParseContent(true)->setOptionValidator($urlValidator);
array_push($this->definitions, $builder->build());
/* [img] image tag */
$builder = new CodeDefinitionBuilder('img', '<img src="{param}" />');
$builder->setUseOption(false)->setParseContent(false)->setBodyValidator($urlValidator);
array_push($this->definitions, $builder->build());
/* [img=alt text] image tag */
$builder = new CodeDefinitionBuilder('img', '<img src="{param}" alt="{option}" />');
$builder->setUseOption(true)->setParseContent(false)->setBodyValidator($urlValidator);
array_push($this->definitions, $builder->build());
/* [color] color tag */
$builder = new CodeDefinitionBuilder('color', '<span style="color: {option}">{param}</span>');
$builder->setUseOption(true)->setOptionValidator(new \JBBCode\validators\CssColorValidator());
array_push($this->definitions, $builder->build());
}
/**
* Returns an array of the default code definitions.
*/
public function getCodeDefinitions()
{
return $this->definitions;
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace JBBCode;
require_once 'ElementNode.php';
/**
* A DocumentElement object represents the root of a document tree. All
* documents represented by this document model should have one as its root.
*
* @author jbowens
*/
class DocumentElement extends ElementNode
{
/**
* Constructs the document element node
*/
public function __construct()
{
parent::__construct();
$this->setTagName("Document");
$this->setNodeId(0);
}
/**
* (non-PHPdoc)
* @see JBBCode.ElementNode::getAsBBCode()
*
* Returns the BBCode representation of this document
*
* @return this document's bbcode representation
*/
public function getAsBBCode()
{
$s = "";
foreach($this->getChildren() as $child){
$s .= $child->getAsBBCode();
}
return $s;
}
/**
* (non-PHPdoc)
* @see JBBCode.ElementNode::getAsHTML()
*
* Documents don't add any html. They only exist as a container for their
* children, so getAsHTML() simply iterates through the document's children,
* returning their html.
*
* @return the HTML representation of this document
*/
public function getAsHTML()
{
$s = "";
foreach($this->getChildren() as $child)
$s .= $child->getAsHTML();
return $s;
}
public function accept(NodeVisitor $visitor)
{
$visitor->visitDocumentElement($this);
}
}

241
app/jbbcode/ElementNode.php Normal file
View file

@ -0,0 +1,241 @@
<?php
namespace JBBCode;
require_once 'Node.php';
/**
* An element within the tree. Consists of a tag name which defines the type of the
* element and any number of Node children. It also contains a CodeDefinition matching
* the tag name of the element.
*
* @author jbowens
*/
class ElementNode extends Node
{
/* The tagname of this element, for i.e. "b" in [b]bold[/b] */
protected $tagName;
/* The attribute, if any, of this element node */
protected $attribute;
/* The child nodes contained within this element */
protected $children;
/* The code definition that defines this element's behavior */
protected $codeDefinition;
/* How deeply this node is nested */
protected $nestDepth;
/**
* Constructs the element node
*/
public function __construct()
{
$this->children = array();
$this->nestDepth = 0;
}
/**
* Accepts the given NodeVisitor. This is part of an implementation
* of the Visitor pattern.
*
* @param $nodeVisitor the visitor attempting to visit this node
*/
public function accept(NodeVisitor $nodeVisitor)
{
$nodeVisitor->visitElementNode($this);
}
/**
* Gets the CodeDefinition that defines this element.
*
* @return this element's code definition
*/
public function getCodeDefinition()
{
return $this->codeDefinition;
}
/**
* Sets the CodeDefinition that defines this element.
*
* @param codeDef the code definition that defines this element node
*/
public function setCodeDefinition(CodeDefinition $codeDef)
{
$this->codeDefinition = $codeDef;
$this->setTagName($codeDef->getTagName());
}
/**
* Returns the tag name of this element.
*
* @return the element's tag name
*/
public function getTagName()
{
return $this->tagName;
}
/**
* Returns the attribute (used as the option in bbcode definitions) of this element.
*
* @return the attribute of this element
*/
public function getAttribute()
{
return $this->attribute;
}
/**
* Returns all the children of this element.
*
* @return an array of this node's child nodes
*/
public function getChildren()
{
return $this->children;
}
/**
* (non-PHPdoc)
* @see JBBCode.Node::getAsText()
*
* Returns the element as text (not including any bbcode markup)
*
* @return the plain text representation of this node
*/
public function getAsText()
{
if ($this->codeDefinition) {
return $this->codeDefinition->asText($this);
} else {
$s = "";
foreach ($this->getChildren() as $child)
$s .= $child->getAsText();
return $s;
}
}
/**
* (non-PHPdoc)
* @see JBBCode.Node::getAsBBCode()
*
* Returns the element as bbcode (with all unclosed tags closed)
*
* @return the bbcode representation of this element
*/
public function getAsBBCode()
{
$str = "[".$this->tagName;
if (!empty($this->attribute)) {
foreach($this->attribute as $key => $value){
if($key == $this->tagName){
$str .= "=".$value;
}
else{
$str .= " ".$key."=" . $value;
}
}
}
$str .= "]";
foreach ($this->getChildren() as $child) {
$str .= $child->getAsBBCode();
}
$str .= "[/".$this->tagName."]";
return $str;
}
/**
* (non-PHPdoc)
* @see JBBCode.Node::getAsHTML()
*
* Returns the element as html with all replacements made
*
* @return the html representation of this node
*/
public function getAsHTML()
{
if($this->codeDefinition) {
return $this->codeDefinition->asHtml($this);
} else {
return "";
}
}
/**
* Adds a child to this node's content. A child may be a TextNode, or
* another ElementNode... or anything else that may extend the
* abstract Node class.
*
* @param child the node to add as a child
*/
public function addChild(Node $child)
{
array_push($this->children, $child);
$child->setParent($this);
}
/**
* Removes a child from this node's contnet.
*
* @param child the child node to remove
*/
public function removeChild(Node $child)
{
foreach ($this->children as $key => $value) {
if ($value == $child)
unset($this->children[$key]);
}
}
/**
* Sets the tag name of this element node.
*
* @param tagName the element's new tag name
*/
public function setTagName($tagName)
{
$this->tagName = $tagName;
}
/**
* Sets the attribute (option) of this element node.
*
* @param attribute the attribute of this element node
*/
public function setAttribute($attribute)
{
$this->attribute = $attribute;
}
/**
* Traverses the parse tree upwards, going from parent to parent, until it finds a
* parent who has the given tag name. Returns the parent with the matching tag name
* if it exists, otherwise returns null.
*
* @param str the tag name to search for
*
* @return the closest parent with the given tag name
*/
public function closestParentOfType($str)
{
$str = strtolower($str);
$currentEl = $this;
while (strtolower($currentEl->getTagName()) != $str && $currentEl->hasParent()) {
$currentEl = $currentEl->getParent();
}
if (strtolower($currentEl->getTagName()) != $str) {
return null;
} else {
return $currentEl;
}
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace JBBCode;
/**
* Defines an interface for validation filters for bbcode options and
* parameters.
*
* @author jbowens
* @since May 2013
*/
interface InputValidator
{
/**
* Returns true iff the given input is valid, false otherwise.
*/
public function validate($input);
}

109
app/jbbcode/Node.php Normal file
View file

@ -0,0 +1,109 @@
<?php
namespace JBBCode;
/**
* A node within the document tree.
*
* Known subclasses: TextNode, ElementNode
*
* @author jbowens
*/
abstract class Node
{
/* Pointer to the parent node of this node */
protected $parent;
/* The node id of this node */
protected $nodeid;
/**
* Returns the node id of this node. (Not really ever used. Dependent upon the parse tree the node exists within.)
*
* @return this node's id
*/
public function getNodeId()
{
return $this->nodeid;
}
/**
* Returns this node's immediate parent.
*
* @return the node's parent
*/
public function getParent()
{
return $this->parent;
}
/**
* Determines if this node has a parent.
*
* @return true if this node has a parent, false otherwise
*/
public function hasParent()
{
return $this->parent != null;
}
/**
* Returns true if this is a text node. Returns false otherwise.
* (Overridden by TextNode to return true)
*
* @return true if this node is a text node
*/
public function isTextNode()
{
return false;
}
/**
* Accepts a NodeVisitor
*
* @param nodeVisitor the NodeVisitor traversing the graph
*/
abstract public function accept(NodeVisitor $nodeVisitor);
/**
* Returns this node as text (without any bbcode markup)
*
* @return the plain text representation of this node
*/
abstract public function getAsText();
/**
* Returns this node as bbcode
*
* @return the bbcode representation of this node
*/
abstract public function getAsBBCode();
/**
* Returns this node as HTML
*
* @return the html representation of this node
*/
abstract public function getAsHTML();
/**
* Sets this node's parent to be the given node.
*
* @param parent the node to set as this node's parent
*/
public function setParent(Node $parent)
{
$this->parent = $parent;
}
/**
* Sets this node's nodeid
*
* @param nodeid this node's node id
*/
public function setNodeId($nodeid)
{
$this->nodeid = $nodeid;
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace JBBCode;
/**
* Defines an interface for a visitor to traverse the node graph.
*
* @author jbowens
* @since January 2013
*/
interface NodeVisitor
{
public function visitDocumentElement(DocumentElement $documentElement);
public function visitTextNode(TextNode $textNode);
public function visitElementNode(ElementNode $elementNode);
}

662
app/jbbcode/Parser.php Normal file
View file

@ -0,0 +1,662 @@
<?php
namespace JBBCode;
require_once 'ElementNode.php';
require_once 'TextNode.php';
require_once 'DefaultCodeDefinitionSet.php';
require_once 'DocumentElement.php';
require_once 'CodeDefinition.php';
require_once 'CodeDefinitionBuilder.php';
require_once 'CodeDefinitionSet.php';
require_once 'NodeVisitor.php';
require_once 'ParserException.php';
require_once 'Tokenizer.php';
require_once 'visitors/NestLimitVisitor.php';
require_once 'InputValidator.php';
use JBBCode\CodeDefinition;
/**
* BBCodeParser is the main parser class that constructs and stores the parse tree. Through this class
* new bbcode definitions can be added, and documents may be parsed and converted to html/bbcode/plaintext, etc.
*
* @author jbowens
*/
class Parser
{
const OPTION_STATE_DEFAULT = 0;
const OPTION_STATE_TAGNAME = 1;
const OPTION_STATE_KEY = 2;
const OPTION_STATE_VALUE = 3;
const OPTION_STATE_QUOTED_VALUE = 4;
const OPTION_STATE_JAVASCRIPT = 5;
/* The root element of the parse tree */
protected $treeRoot;
/* The list of bbcodes to be used by the parser. */
protected $bbcodes;
/* The next node id to use. This is used while parsing. */
protected $nextNodeid;
/**
* Constructs an instance of the BBCode parser
*/
public function __construct()
{
$this->reset();
$this->bbcodes = array();
}
/**
* Adds a simple (text-replacement only) bbcode definition
*
* @param string $tagName the tag name of the code (for example the b in [b])
* @param string $replace the html to use, with {param} and optionally {option} for replacements
* @param boolean $useOption whether or not this bbcode uses the secondary {option} replacement
* @param boolean $parseContent whether or not to parse the content within these elements
* @param integer $nestLimit an optional limit of the number of elements of this kind that can be nested within
* each other before the parser stops parsing them.
* @param InputValidator $optionValidator the validator to run {option} through
* @param BodyValidator $bodyValidator the validator to run {param} through (only used if $parseContent == false)
*
* @return Parser
*/
public function addBBCode($tagName, $replace, $useOption = false, $parseContent = true, $nestLimit = -1,
InputValidator $optionValidator = null, InputValidator $bodyValidator = null)
{
$builder = new CodeDefinitionBuilder($tagName, $replace);
$builder->setUseOption($useOption);
$builder->setParseContent($parseContent);
$builder->setNestLimit($nestLimit);
if ($optionValidator) {
$builder->setOptionValidator($optionValidator);
}
if ($bodyValidator) {
$builder->setBodyValidator($bodyValidator);
}
$this->addCodeDefinition($builder->build());
return $this;
}
/**
* Adds a complex bbcode definition. You may subclass the CodeDefinition class, instantiate a definition of your new
* class and add it to the parser through this method.
*
* @param CodeDefinition $definition the bbcode definition to add
*
* @return Parser
*/
public function addCodeDefinition(CodeDefinition $definition)
{
array_push($this->bbcodes, $definition);
return $this;
}
/**
* Adds a set of CodeDefinitions.
*
* @param CodeDefinitionSet $set the set of definitions to add
*
* @return Parser
*/
public function addCodeDefinitionSet(CodeDefinitionSet $set) {
foreach ($set->getCodeDefinitions() as $def) {
$this->addCodeDefinition($def);
}
return $this;
}
/**
* Returns the entire parse tree as text. Only {param} content is returned. BBCode markup will be ignored.
*
* @return string a text representation of the parse tree
*/
public function getAsText()
{
return $this->treeRoot->getAsText();
}
/**
* Returns the entire parse tree as bbcode. This will be identical to the inputted string, except unclosed tags
* will be closed.
*
* @return string a bbcode representation of the parse tree
*/
public function getAsBBCode()
{
return $this->treeRoot->getAsBBCode();
}
/**
* Returns the entire parse tree as HTML. All BBCode replacements will be made. This is generally the method
* you will want to use to retrieve the parsed bbcode.
*
* @return string a parsed html string
*/
public function getAsHTML()
{
return $this->treeRoot->getAsHTML();
}
/**
* Accepts the given NodeVisitor at the root.
*
* @param NodeVisitor a NodeVisitor
*
* @return Parser
*/
public function accept(NodeVisitor $nodeVisitor)
{
$this->treeRoot->accept($nodeVisitor);
return $this;
}
/**
* Constructs the parse tree from a string of bbcode markup.
*
* @param string $str the bbcode markup to parse
*
* @return Parser
*/
public function parse($str)
{
/* Set the tree root back to a fresh DocumentElement. */
$this->reset();
$parent = $this->treeRoot;
$tokenizer = new Tokenizer($str);
while ($tokenizer->hasNext()) {
$parent = $this->parseStartState($parent, $tokenizer);
if ($parent->getCodeDefinition() && false ===
$parent->getCodeDefinition()->parseContent()) {
/* We're inside an element that does not allow its contents to be parseable. */
$this->parseAsTextUntilClose($parent, $tokenizer);
$parent = $parent->getParent();
}
}
/* We parsed ignoring nest limits. Do an O(n) traversal to remove any elements that
* are nested beyond their CodeDefinition's nest limit. */
$this->removeOverNestedElements();
return $this;
}
/**
* Removes any elements that are nested beyond their nest limit from the parse tree. This
* method is now deprecated. In a future release its access privileges will be made
* protected.
*
* @deprecated
*/
public function removeOverNestedElements()
{
$nestLimitVisitor = new \JBBCode\visitors\NestLimitVisitor();
$this->accept($nestLimitVisitor);
}
/**
* Removes the old parse tree if one exists.
*/
protected function reset()
{
// remove any old tree information
$this->treeRoot = new DocumentElement();
/* The document element is created with nodeid 0. */
$this->nextNodeid = 1;
}
/**
* Determines whether a bbcode exists based on its tag name and whether or not it uses an option
*
* @param string $tagName the bbcode tag name to check
* @param boolean $usesOption whether or not the bbcode accepts an option
*
* @return bool true if the code exists, false otherwise
*/
public function codeExists($tagName, $usesOption = false)
{
foreach ($this->bbcodes as $code) {
if (strtolower($tagName) == $code->getTagName() && $usesOption == $code->usesOption()) {
return true;
}
}
return false;
}
/**
* Returns the CodeDefinition of a bbcode with the matching tag name and usesOption parameter
*
* @param string $tagName the tag name of the bbcode being searched for
* @param boolean $usesOption whether or not the bbcode accepts an option
*
* @return CodeDefinition if the bbcode exists, null otherwise
*/
public function getCode($tagName, $usesOption = false)
{
foreach ($this->bbcodes as $code) {
if (strtolower($tagName) == $code->getTagName() && $code->usesOption() == $usesOption) {
return $code;
}
}
return null;
}
/**
* Adds a set of default, standard bbcode definitions commonly used across the web.
*
* This method is now deprecated. Please use DefaultCodeDefinitionSet and
* addCodeDefinitionSet() instead.
*
* @deprecated
*/
public function loadDefaultCodes()
{
$defaultSet = new DefaultCodeDefinitionSet();
$this->addCodeDefinitionSet($defaultSet);
}
/**
* Creates a new text node with the given parent and text string.
*
* @param $parent the parent of the text node
* @param $string the text of the text node
*
* @return TextNode the newly created TextNode
*/
protected function createTextNode(ElementNode $parent, $string)
{
if (count($parent->getChildren())) {
$children = $parent->getChildren();
$lastElement = end($children);
reset($children);
if ($lastElement->isTextNode()) {
$lastElement->setValue($lastElement->getValue() . $string);
return $lastElement;
}
}
$textNode = new TextNode($string);
$textNode->setNodeId(++$this->nextNodeid);
$parent->addChild($textNode);
return $textNode;
}
/**
* jBBCode parsing logic is loosely modelled after a FSM. While not every function maps
* to a unique DFSM state, each function handles the logic of one or more FSM states.
* This function handles the beginning parse state when we're not currently in a tag
* name.
*
* @param ElementNode $parent the current parent node we're under
* @param Tokenizer $tokenizer the tokenizer we're using
*
* @return ElementNode the new parent we should use for the next iteration.
*/
protected function parseStartState(ElementNode $parent, Tokenizer $tokenizer)
{
$next = $tokenizer->next();
if ('[' == $next) {
return $this->parseTagOpen($parent, $tokenizer);
}
else {
$this->createTextNode($parent, $next);
/* Drop back into the main parse loop which will call this
* same method again. */
return $parent;
}
}
/**
* This function handles parsing the beginnings of an open tag. When we see a [
* at an appropriate time, this function is entered.
*
* @param ElementNode $parent the current parent node
* @param Tokenizer $tokenizer the tokenizer we're using
*
* @return ElementNode the new parent node
*/
protected function parseTagOpen(ElementNode $parent, Tokenizer $tokenizer)
{
if (!$tokenizer->hasNext()) {
/* The [ that sent us to this state was just a trailing [, not the
* opening for a new tag. Treat it as such. */
$this->createTextNode($parent, '[');
return $parent;
}
$next = $tokenizer->next();
/* This while loop could be replaced by a recursive call to this same method,
* which would likely be a lot clearer but I decided to use a while loop to
* prevent stack overflow with a string like [[[[[[[[[...[[[.
*/
while ('[' == $next) {
/* The previous [ was just a random bracket that should be treated as text.
* Continue until we get a non open bracket. */
$this->createTextNode($parent, '[');
if (!$tokenizer->hasNext()) {
$this->createTextNode($parent, '[');
return $parent;
}
$next = $tokenizer->next();
}
if (!$tokenizer->hasNext()) {
$this->createTextNode($parent, '['.$next);
return $parent;
}
$after_next = $tokenizer->next();
$tokenizer->stepBack();
if ($after_next != ']')
{
$this->createTextNode($parent, '['.$next);
return $parent;
}
/* At this point $next is either ']' or plain text. */
if (']' == $next) {
$this->createTextNode($parent, '[');
$this->createTextNode($parent, ']');
return $parent;
} else {
/* $next is plain text... likely a tag name. */
return $this->parseTag($parent, $tokenizer, $next);
}
}
protected function parseOptions($tagContent)
{
$buffer = "";
$tagName = "";
$state = static::OPTION_STATE_TAGNAME;
$keys = array();
$values = array();
$options = array();
$len = strlen($tagContent);
$done = false;
$idx = 0;
try{
while(!$done){
$char = $idx < $len ? $tagContent[$idx]:null;
switch($state){
case static::OPTION_STATE_TAGNAME:
switch($char){
case '=':
$state = static::OPTION_STATE_VALUE;
$tagName = $buffer;
$keys[] = $tagName;
$buffer = "";
break;
case ' ':
$state = static::OPTION_STATE_DEFAULT;
$tagName = $buffer;
$buffer = '';
$keys[] = $tagName;
break;
case null:
$tagName = $buffer;
$buffer = '';
$keys[] = $tagName;
break;
default:
$buffer .= $char;
}
break;
case static::OPTION_STATE_DEFAULT:
switch($char){
case ' ':
// do nothing
default:
$state = static::OPTION_STATE_KEY;
$buffer .= $char;
}
break;
case static::OPTION_STATE_VALUE:
switch($char){
case '"':
$state = static::OPTION_STATE_QUOTED_VALUE;
break;
case null: // intentional fall-through
case ' ': // key=value<space> delimits to next key
$values[] = $buffer;
$buffer = "";
$state = static::OPTION_STATE_KEY;
break;
case ":":
if($buffer=="javascript"){
$state = static::OPTION_STATE_JAVASCRIPT;
}
$buffer .= $char;
break;
default:
$buffer .= $char;
}
break;
case static::OPTION_STATE_JAVASCRIPT:
switch($char){
case ";":
$buffer .= $char;
$values[] = $buffer;
$buffer = "";
$state = static::OPTION_STATE_KEY;
break;
default:
$buffer .= $char;
}
break;
case static::OPTION_STATE_KEY:
switch($char){
case '=':
$state = static::OPTION_STATE_VALUE;
$keys[] = $buffer;
$buffer = '';
break;
case ' ': // ignore <space>key=value
break;
default:
$buffer .= $char;
break;
}
break;
case static::OPTION_STATE_QUOTED_VALUE:
switch($char){
case null:
case '"':
$state = static::OPTION_STATE_KEY;
$values[] = $buffer;
$buffer = '';
// peek ahead. If the next character is not a space or a closing brace, we have a bad tag and need to abort
if(isset($tagContent[$idx+1]) && $tagContent[$idx+1]!=" " && $tagContent[$idx+1]!="]" ){
throw new ParserException("Badly formed attribute: $tagContent");
}
break;
default:
$buffer .= $char;
break;
}
break;
default:
if(!empty($char)){
$state = static::OPTION_STATE_KEY;
}
}
if($idx >= $len){
$done = true;
}
$idx++;
}
if(count($keys) && count($values)){
if(count($keys)==(count($values)+1)){
array_unshift($values, "");
}
$options = array_combine($keys, $values);
}
}
catch(ParserException $e){
// if we're in this state, then something evidently went wrong. We'll consider everything that came after the tagname to be the attribute for that keyname
$options[$tagName]= substr($tagContent, strpos($tagContent, "=")+1);
}
return array($tagName, $options);
}
/**
* This is the next step in parsing a tag. It's possible for it to still be invalid at this
* point but many of the basic invalid tag name conditions have already been handled.
*
* @param ElementNode $parent the current parent element
* @param Tokenizer $tokenizer the tokenizer we're using
* @param string $tagContent the text between the [ and the ], assuming there is actually a ]
*
* @return ElementNode the new parent element
*/
protected function parseTag(ElementNode $parent, Tokenizer $tokenizer, $tagContent)
{
$next;
if (!$tokenizer->hasNext() || ($next = $tokenizer->next()) != ']') {
/* This is a malformed tag. Both the previous [ and the tagContent
* is really just plain text. */
$this->createTextNode($parent, '[');
$this->createTextNode($parent, $tagContent);
return $parent;
}
/* This is a well-formed tag consisting of [something] or [/something], but
* we still need to ensure that 'something' is a valid tag name. Additionally,
* if it's a closing tag, we need to ensure that there was a previous matching
* opening tag.
*/
/* There could be attributes. */
list($tmpTagName, $options) = $this->parseOptions($tagContent);
// $tagPieces = explode('=', $tagContent);
// $tmpTagName = $tagPieces[0];
$actualTagName;
if ('' != $tmpTagName && '/' == $tmpTagName[0]) {
/* This is a closing tag name. */
$actualTagName = substr($tmpTagName, 1);
} else {
$actualTagName = $tmpTagName;
}
if ('' != $tmpTagName && '/' == $tmpTagName[0]) {
/* This is attempting to close an open tag. We must verify that there exists an
* open tag of the same type and that there is no option (options on closing
* tags don't make any sense). */
$elToClose = $parent->closestParentOfType($actualTagName);
if (null == $elToClose || count($options) > 1) {
/* Closing an unopened tag or has an option. Treat everything as plain text. */
$this->createTextNode($parent, '[');
$this->createTextNode($parent, $tagContent);
$this->createTextNode($parent, ']');
return $parent;
} else {
/* We're closing $elToClose. In order to do that, we just need to return
* $elToClose's parent, since that will change our effective parent to be
* elToClose's parent. */
return $elToClose->getParent();
}
}
/* Verify that this is a known bbcode tag name. */
if ('' == $actualTagName || !$this->codeExists($actualTagName, !empty($options))) {
/* This is an invalid tag name! Treat everything we've seen as plain text. */
$this->createTextNode($parent, '[');
$this->createTextNode($parent, $tagContent);
$this->createTextNode($parent, ']');
return $parent;
}
/* If we're here, this is a valid opening tag. Let's make a new node for it. */
$el = new ElementNode();
$el->setNodeId(++$this->nextNodeid);
$code = $this->getCode($actualTagName, !empty($options));
$el->setCodeDefinition($code);
if (!empty($options)) {
/* We have an attribute we should save. */
$el->setAttribute($options);
}
$parent->addChild($el);
return $el;
}
/**
* Handles parsing elements whose CodeDefinitions disable parsing of element
* contents. This function uses a rolling window of 3 tokens until it finds the
* appropriate closing tag or reaches the end of the token stream.
*
* @param ElementNode $parent the current parent element
* @param Tokenizer $tokenizer the tokenizer we're using
*
* @return ElementNode the new parent element
*/
protected function parseAsTextUntilClose(ElementNode $parent, Tokenizer $tokenizer)
{
/* $parent's code definition doesn't allow its contents to be parsed. Here we use
* a sliding window of three tokens until we find [ /tagname ], signifying the
* end of the parent. */
if (!$tokenizer->hasNext()) {
return $parent;
}
$prevPrev = $tokenizer->next();
if (!$tokenizer->hasNext()) {
$this->createTextNode($parent, $prevPrev);
return $parent;
}
$prev = $tokenizer->next();
if (!$tokenizer->hasNext()) {
$this->createTextNode($parent, $prevPrev);
$this->createTextNode($parent, $prev);
return $parent;
}
$curr = $tokenizer->next();
while ('[' != $prevPrev || '/'.$parent->getTagName() != strtolower($prev) ||
']' != $curr) {
$this->createTextNode($parent, $prevPrev);
$prevPrev = $prev;
$prev = $curr;
if (!$tokenizer->hasNext()) {
$this->createTextNode($parent, $prevPrev);
$this->createTextNode($parent, $prev);
return $parent;
}
$curr = $tokenizer->next();
}
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace JBBCode;
use Exception;
class ParserException extends Exception{
}

102
app/jbbcode/TextNode.php Normal file
View file

@ -0,0 +1,102 @@
<?php
namespace JBBCode;
require_once 'Node.php';
/**
* Represents a piece of text data. TextNodes never have children.
*
* @author jbowens
*/
class TextNode extends Node
{
/* The value of this text node */
protected $value;
/**
* Constructs a text node from its text string
*
* @param string $val
*/
public function __construct($val)
{
$this->value = $val;
}
public function accept(NodeVisitor $visitor)
{
$visitor->visitTextNode($this);
}
/**
* (non-PHPdoc)
* @see JBBCode.Node::isTextNode()
*
* returns true
*/
public function isTextNode()
{
return true;
}
/**
* Returns the text string value of this text node.
*
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* (non-PHPdoc)
* @see JBBCode.Node::getAsText()
*
* Returns the text representation of this node.
*
* @return this node represented as text
*/
public function getAsText()
{
return $this->getValue();
}
/**
* (non-PHPdoc)
* @see JBBCode.Node::getAsBBCode()
*
* Returns the bbcode representation of this node. (Just its value)
*
* @return this node represented as bbcode
*/
public function getAsBBCode()
{
return $this->getValue();
}
/**
* (non-PHPdoc)
* @see JBBCode.Node::getAsHTML()
*
* Returns the html representation of this node. (Just its value)
*
* @return this node represented as HTML
*/
public function getAsHTML()
{
return $this->getValue();
}
/**
* Edits the text value contained within this text node.
*
* @param newValue the new text value of the text node
*/
public function setValue($newValue)
{
$this->value = $newValue;
}
}

105
app/jbbcode/Tokenizer.php Normal file
View file

@ -0,0 +1,105 @@
<?php
namespace JBBCode;
/**
* This Tokenizer is used while constructing the parse tree. The tokenizer
* handles splitting the input into brackets and miscellaneous text. The
* parser is then built as a FSM ontop of these possible inputs.
*
* @author jbowens
*/
class Tokenizer
{
protected $tokens = array();
protected $i = -1;
/**
* Constructs a tokenizer from the given string. The string will be tokenized
* upon construction.
*
* @param $str the string to tokenize
*/
public function __construct($str)
{
$strStart = 0;
for ($index = 0; $index < strlen($str); ++$index) {
if (']' == $str[$index] || '[' == $str[$index]) {
/* Are there characters in the buffer from a previous string? */
if ($strStart < $index) {
array_push($this->tokens, substr($str, $strStart, $index - $strStart));
$strStart = $index;
}
/* Add the [ or ] to the tokens array. */
array_push($this->tokens, $str[$index]);
$strStart = $index+1;
}
}
if ($strStart < strlen($str)) {
/* There are still characters in the buffer. Add them to the tokens. */
array_push($this->tokens, substr($str, $strStart, strlen($str) - $strStart));
}
}
/**
* Returns true if there is another token in the token stream.
*/
public function hasNext()
{
return count($this->tokens) > 1 + $this->i;
}
/**
* Advances the token stream to the next token and returns the new token.
*/
public function next()
{
if (!$this->hasNext()) {
return null;
} else {
return $this->tokens[++$this->i];
}
}
/**
* Retrieves the current token.
*/
public function current()
{
if ($this->i < 0) {
return null;
} else {
return $this->tokens[$this->i];
}
}
/**
* Moves the token stream back a token.
*/
public function stepBack()
{
if ($this->i > -1) {
$this->i--;
}
}
/**
* Restarts the tokenizer, returning to the beginning of the token stream.
*/
public function restart()
{
$this->i = -1;
}
/**
* toString method that returns the entire string from the current index on.
*/
public function toString()
{
return implode('', array_slice($this->tokens, $this->i + 1));
}
}

View file

@ -0,0 +1,12 @@
<?php
require_once "/path/to/jbbcode/Parser.php";
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$text = "The default codes include: [b]bold[/b], [i]italics[/i], [u]underlining[/u], ";
$text .= "[url=http://jbbcode.com]links[/url], [color=red]color![/color] and more.";
$parser->parse($text);
print $parser->getAsHtml();

View file

@ -0,0 +1,10 @@
<?php
require_once "/path/to/jbbcode/Parser.php";
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$text = "The bbcode in here [b]is never closed!";
$parser->parse($text);
print $parser->getAsBBCode();

View file

@ -0,0 +1,11 @@
<?php
require_once "/path/to/jbbcode/Parser.php";
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$text = "[b][u]There is [i]a lot[/i] of [url=http://en.wikipedia.org/wiki/Markup_language]markup[/url] in this";
$text .= "[color=#333333]text[/color]![/u][/b]";
$parser->parse($text);
print $parser->getAsText();

View file

@ -0,0 +1,7 @@
<?php
require_once "/path/to/jbbcode/Parser.php";
$parser = new JBBCode\Parser();
$parser->addBBCode("quote", '<div class="quote">{param}</div>');
$parser->addBBCode("code", '<pre class="code">{param}</pre>', false, false, 1);

View file

@ -0,0 +1,22 @@
<?php
require_once("../Parser.php");
require_once("../visitors/SmileyVisitor.php");
error_reporting(E_ALL);
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
if (count($argv) < 2) {
die("Usage: " . $argv[0] . " \"bbcode string\"\n");
}
$inputText = $argv[1];
$parser->parse($inputText);
$smileyVisitor = new \JBBCode\visitors\SmileyVisitor();
$parser->accept($smileyVisitor);
echo $parser->getAsHTML() . "\n";

View file

@ -0,0 +1,23 @@
<?php
require_once("../Parser.php");
require_once("../visitors/TagCountingVisitor.php");
error_reporting(E_ALL);
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
if (count($argv) < 3) {
die("Usage: " . $argv[0] . " \"bbcode string\" <tag name to check>\n");
}
$inputText = $argv[1];
$tagName = $argv[2];
$parser->parse($inputText);
$tagCountingVisitor = new \JBBCode\visitors\TagCountingVisitor();
$parser->accept($tagCountingVisitor);
echo $tagCountingVisitor->getFrequency($tagName) . "\n";

View file

@ -0,0 +1,85 @@
<?php
require_once(dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'Parser.php');
/**
* Test cases testing the functionality of parsing bbcode and
* retrieving a bbcode well-formed bbcode representation.
*
* @author jbowens
*/
class BBCodeToBBCodeTest extends PHPUnit_Framework_TestCase
{
/**
* A utility method for these tests that will evaluate its arguments as bbcode with
* a fresh parser loaded with only the default bbcodes. It returns the
* bbcode output, which in most cases should be in the input itself.
*/
private function defaultBBCodeParse($bbcode)
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->parse($bbcode);
return $parser->getAsBBCode();
}
/**
* Asserts that the given bbcode matches the given text when
* the bbcode is run through defaultBBCodeParse
*/
private function assertBBCodeOutput($bbcode, $text)
{
$this->assertEquals($this->defaultBBCodeParse($bbcode), $text);
}
public function testEmptyString()
{
$this->assertBBCodeOutput('', '');
}
public function testOneTag()
{
$this->assertBBCodeOutput('[b]this is bold[/b]', '[b]this is bold[/b]');
}
public function testOneTagWithSurroundingText()
{
$this->assertBBCodeOutput('buffer text [b]this is bold[/b] buffer text',
'buffer text [b]this is bold[/b] buffer text');
}
public function testMultipleTags()
{
$bbcode = 'this is some text with [b]bold tags[/b] and [i]italics[/i] and ' .
'things like [u]that[/u].';
$bbcodeOutput = 'this is some text with [b]bold tags[/b] and [i]italics[/i] and ' .
'things like [u]that[/u].';
$this->assertBBCodeOutput($bbcode, $bbcodeOutput);
}
public function testCodeOptions()
{
$code = 'This contains a [url=http://jbbcode.com]url[/url] which uses an option.';
$codeOutput = 'This contains a [url=http://jbbcode.com]url[/url] which uses an option.';
$this->assertBBCodeOutput($code, $codeOutput);
}
/**
* @depends testCodeOptions
*/
public function testOmittedOption()
{
$code = 'This doesn\'t use the url option [url]http://jbbcode.com[/url].';
$codeOutput = 'This doesn\'t use the url option [url]http://jbbcode.com[/url].';
$this->assertBBCodeOutput($code, $codeOutput);
}
public function testUnclosedTags()
{
$code = '[b]bold';
$codeOutput = '[b]bold[/b]';
$this->assertBBCodeOutput($code, $codeOutput);
}
}

View file

@ -0,0 +1,78 @@
<?php
require_once(dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'Parser.php');
/**
* Test cases testing the ability to parse bbcode and retrieve a
* plain text representation without any markup.
*
* @author jbowens
*/
class BBCodeToTextTest extends PHPUnit_Framework_TestCase
{
/**
* A utility method for these tests that will evaluate
* its arguments as bbcode with a fresh parser loaded
* with only the default bbcodes. It returns the
* text output.
*/
private function defaultTextParse($bbcode)
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->parse($bbcode);
return $parser->getAsText();
}
/**
* Asserts that the given bbcode matches the given text when
* the bbcode is run through defaultTextParse
*/
private function assertTextOutput($bbcode, $text)
{
$this->assertEquals($text, $this->defaultTextParse($bbcode));
}
public function testEmptyString()
{
$this->assertTextOutput('', '');
}
public function testOneTag()
{
$this->assertTextOutput('[b]this is bold[/b]', 'this is bold');
}
public function testOneTagWithSurroundingText()
{
$this->assertTextOutput('buffer text [b]this is bold[/b] buffer text',
'buffer text this is bold buffer text');
}
public function testMultipleTags()
{
$bbcode = 'this is some text with [b]bold tags[/b] and [i]italics[/i] and ' .
'things like [u]that[/u].';
$text = 'this is some text with bold tags and italics and things like that.';
$this->assertTextOutput($bbcode, $text);
}
public function testCodeOptions()
{
$code = 'This contains a [url=http://jbbcode.com]url[/url] which uses an option.';
$text = 'This contains a url which uses an option.';
$this->assertTextOutput($code, $text);
}
/**
* @depends testCodeOptions
*/
public function testOmittedOption()
{
$code = 'This doesn\'t use the url option [url]http://jbbcode.com[/url].';
$text = 'This doesn\'t use the url option http://jbbcode.com.';
$this->assertTextOutput($code, $text);
}
}

View file

@ -0,0 +1,54 @@
<?php
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'Parser.php';
/**
* Test cases for the default bbcode set.
*
* @author jbowens
* @since May 2013
*/
class DefaultCodesTest extends PHPUnit_Framework_TestCase
{
/**
* Asserts that the given bbcode string produces the given html string
* when parsed with the default bbcodes.
*/
public function assertProduces($bbcode, $html)
{
$parser = new \JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->parse($bbcode);
$this->assertEquals($html, $parser->getAsHtml());
}
/**
* Tests the [b] bbcode.
*/
public function testBold()
{
$this->assertProduces('[b]this should be bold[/b]', '<strong>this should be bold</strong>');
}
/**
* Tests the [color] bbcode.
*/
public function testColor()
{
$this->assertProduces('[color=red]red[/color]', '<span style="color: red">red</span>');
}
/**
* Tests the example from the documentation.
*/
public function testExample()
{
$text = "The default codes include: [b]bold[/b], [i]italics[/i], [u]underlining[/u], ";
$text .= "[url=http://jbbcode.com]links[/url], [color=red]color![/color] and more.";
$html = 'The default codes include: <strong>bold</strong>, <em>italics</em>, <u>underlining</u>, ';
$html .= '<a href="http://jbbcode.com">links</a>, <span style="color: red">color!</span> and more.';
$this->assertProduces($text, $html);
}
}

View file

@ -0,0 +1,77 @@
<?php
require_once(dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'Parser.php');
/**
* Test cases testing the HTMLSafe visitor, which escapes all html characters in the source text
*
* @author astax-t
*/
class HTMLSafeTest extends PHPUnit_Framework_TestCase
{
/**
* Asserts that the given bbcode string produces the given html string
* when parsed with the default bbcodes.
*/
public function assertProduces($bbcode, $html)
{
$parser = new \JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->parse($bbcode);
$htmlsafer = new JBBCode\visitors\HTMLSafeVisitor();
$parser->accept($htmlsafer);
$this->assertEquals($html, $parser->getAsHtml());
}
/**
* Tests escaping quotes and ampersands in simple text
*/
public function testQuoteAndAmp()
{
$this->assertProduces('te"xt te&xt', 'te&quot;xt te&amp;xt');
}
/**
* Tests escaping quotes and ampersands inside a BBCode tag
*/
public function testQuoteAndAmpInTag()
{
$this->assertProduces('[b]te"xt te&xt[/b]', '<strong>te&quot;xt te&amp;xt</strong>');
}
/**
* Tests escaping HTML tags
*/
public function testHtmlTag()
{
$this->assertProduces('<b>not bold</b>', '&lt;b&gt;not bold&lt;/b&gt;');
$this->assertProduces('[b]<b>bold</b>[/b] <hr>', '<strong>&lt;b&gt;bold&lt;/b&gt;</strong> &lt;hr&gt;');
}
/**
* Tests escaping ampersands in URL using [url]...[/url]
*/
public function testUrlParam()
{
$this->assertProduces('text [url]http://example.com/?a=b&c=d[/url] more text', 'text <a href="http://example.com/?a=b&amp;c=d">http://example.com/?a=b&amp;c=d</a> more text');
}
/**
* Tests escaping ampersands in URL using [url=...] tag
*/
public function testUrlOption()
{
$this->assertProduces('text [url=http://example.com/?a=b&c=d]this is a "link"[/url]', 'text <a href="http://example.com/?a=b&amp;c=d">this is a &quot;link&quot;</a>');
}
/**
* Tests escaping ampersands in URL using [url=...] tag when URL is in quotes
*/
public function testUrlOptionQuotes()
{
$this->assertProduces('text [url="http://example.com/?a=b&c=d"]this is a "link"[/url]', 'text <a href="http://example.com/?a=b&amp;c=d">this is a &quot;link&quot;</a>');
}
}

View file

@ -0,0 +1,46 @@
<?php
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'Parser.php';
/**
* Test cases for CodeDefinition nest limits. If an element is nested beyond
* its CodeDefinition's nest limit, it should be removed from the parse tree.
*
* @author jbowens
* @since May 2013
*/
class NestLimitTest extends PHPUnit_Framework_TestCase
{
/**
* Tests that when elements have no nest limits they may be
* nested indefinitely.
*/
public function testIndefiniteNesting()
{
$parser = new JBBCode\Parser();
$parser->addBBCode('b', '<strong>{param}</strong>', false, true, -1);
$parser->parse('[b][b][b][b][b][b][b][b]bold text[/b][/b][/b][/b][/b][/b][/b][/b]');
$this->assertEquals('<strong><strong><strong><strong><strong><strong><strong><strong>' .
'bold text' .
'</strong></strong></strong></strong></strong></strong></strong></strong>',
$parser->getAsHtml());
}
/**
* Test over nesting.
*/
public function testOverNesting()
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->addBBCode('quote', '<blockquote>{param}</blockquote>', false, true, 2);
$bbcode = '[quote][quote][quote]wut[/quote] huh?[/quote] i don\'t know[/quote]';
$parser->parse($bbcode);
$expectedBbcode = '[quote][quote] huh?[/quote] i don\'t know[/quote]';
$expectedHtml = '<blockquote><blockquote> huh?</blockquote> i don\'t know</blockquote>';
$this->assertEquals($expectedBbcode, $parser->getAsBBCode());
$this->assertEquals($expectedHtml, $parser->getAsHtml());
}
}

View file

@ -0,0 +1,97 @@
<?php
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'Parser.php';
/**
* Test cases for the code definition parameter that disallows parsing
* of an element's content.
*
* @author jbowens
*/
class ParseContentTest extends PHPUnit_Framework_TestCase
{
/**
* Tests that when a bbcode is created with parseContent = false,
* its contents actually are not parsed.
*/
public function testSimpleNoParsing()
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->addBBCode('verbatim', '{param}', false, false);
$parser->parse('[verbatim]plain text[/verbatim]');
$this->assertEquals('plain text', $parser->getAsHtml());
$parser->parse('[verbatim][b]bold[/b][/verbatim]');
$this->assertEquals('[b]bold[/b]', $parser->getAsHtml());
}
public function testNoParsingWithBufferText()
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->addBBCode('verbatim', '{param}', false, false);
$parser->parse('buffer text[verbatim]buffer text[b]bold[/b]buffer text[/verbatim]buffer text');
$this->assertEquals('buffer textbuffer text[b]bold[/b]buffer textbuffer text', $parser->getAsHtml());
}
/**
* Tests that when a tag is not closed within an unparseable tag,
* the BBCode output does not automatically close that tag (because
* the contents were not parsed).
*/
public function testUnclosedTag()
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->addBBCode('verbatim', '{param}', false, false);
$parser->parse('[verbatim]i wonder [b]what will happen[/verbatim]');
$this->assertEquals('i wonder [b]what will happen', $parser->getAsHtml());
$this->assertEquals('[verbatim]i wonder [b]what will happen[/verbatim]', $parser->getAsBBCode());
}
/**
* Tests that an unclosed tag with parseContent = false ends cleanly.
*/
public function testUnclosedVerbatimTag()
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->addBBCode('verbatim', '{param}', false, false);
$parser->parse('[verbatim]yo this [b]text should not be bold[/b]');
$this->assertEquals('yo this [b]text should not be bold[/b]', $parser->getAsHtml());
}
/**
* Tests a malformed closing tag for a verbatim block.
*/
public function testMalformedVerbatimClosingTag()
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->addBBCode('verbatim', '{param}', false, false);
$parser->parse('[verbatim]yo this [b]text should not be bold[/b][/verbatim');
$this->assertEquals('yo this [b]text should not be bold[/b][/verbatim', $parser->getAsHtml());
}
/**
* Tests an immediate end after a verbatim.
*/
public function testVerbatimThenEof()
{
$parser = new JBBCode\Parser();
$parser->addBBCode('verbatim', '{param}', false, false);
$parser->parse('[verbatim]');
$this->assertEquals('', $parser->getAsHtml());
}
}

View file

@ -0,0 +1,130 @@
<?php
require_once(dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'Parser.php');
/**
* A series of test cases for various potential parsing edge cases. This
* includes a lot of tests using brackets for things besides genuine tag
* names.
*
* @author jbowens
*
*/
class ParsingEdgeCaseTest extends PHPUnit_Framework_TestCase
{
/**
* A utility method for these tests that will evaluate
* its arguments as bbcode with a fresh parser loaded
* with only the default bbcodes. It returns the
* html output.
*/
private function defaultParse($bbcode)
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->parse($bbcode);
return $parser->getAsHtml();
}
/**
* Asserts that the given bbcode matches the given html when
* the bbcode is run through defaultParse.
*/
private function assertProduces($bbcode, $html)
{
$this->assertEquals($html, $this->defaultParse($bbcode));
}
/**
* Tests attempting to use a code that doesn't exist.
*/
public function testNonexistentCodeMalformed()
{
$this->assertProduces('[wat]', '[wat]');
}
/**
* Tests attempting to use a code that doesn't exist, but this
* time in a well-formed fashion.
*
* @depends testNonexistentCodeMalformed
*/
public function testNonexistentCodeWellformed()
{
$this->assertProduces('[wat]something[/wat]', '[wat]something[/wat]');
}
/**
* Tests a whole bunch of meaningless left brackets.
*/
public function testAllLeftBrackets()
{
$this->assertProduces('[[[[[[[[', '[[[[[[[[');
}
/**
* Tests a whole bunch of meaningless right brackets.
*/
public function testAllRightBrackets()
{
$this->assertProduces(']]]]]', ']]]]]');
}
/**
* Intermixes well-formed, meaningful tags with meaningless brackets.
*/
public function testRandomBracketsInWellformedCode()
{
$this->assertProduces('[b][[][[i]heh[/i][/b]',
'<strong>[[][<em>heh</em></strong>');
}
/**
* Tests an unclosed tag within a closed tag.
*/
public function testUnclosedWithinClosed()
{
$this->assertProduces('[url=http://jbbcode.com][b]oh yeah[/url]',
'<a href="http://jbbcode.com"><strong>oh yeah</strong></a>');
}
/**
* Tests half completed opening tag.
*/
public function testHalfOpenTag()
{
$this->assertProduces('[b', '[b');
$this->assertProduces('wut [url=http://jbbcode.com',
'wut [url=http://jbbcode.com');
}
/**
* Tests half completed closing tag.
*/
public function testHalfClosingTag()
{
$this->assertProduces('[b]this should be bold[/b',
'<strong>this should be bold[/b</strong>');
}
/**
* Tests lots of left brackets before the actual tag. For example:
* [[[[[[[[b]bold![/b]
*/
public function testLeftBracketsThenTag()
{
$this->assertProduces('[[[[[b]bold![/b]',
'[[[[<strong>bold!</strong>');
}
/**
* Tests a whitespace after left bracket.
*/
public function testWhitespaceAfterLeftBracketWhithoutTag()
{
$this->assertProduces('[ ABC ] ',
'[ ABC ] ');
}
}

View file

@ -0,0 +1,131 @@
<?php
require_once(dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'Parser.php');
class SimpleEvaluationTest extends PHPUnit_Framework_TestCase
{
/**
* A utility method for these tests that will evaluate
* its arguments as bbcode with a fresh parser loaded
* with only the default bbcodes. It returns the
* html output.
*/
private function defaultParse($bbcode)
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->parse($bbcode);
return $parser->getAsHtml();
}
/**
* Asserts that the given bbcode matches the given html when
* the bbcode is run through defaultParse.
*/
private function assertProduces($bbcode, $html)
{
$this->assertEquals($html, $this->defaultParse($bbcode));
}
public function testEmptyString()
{
$this->assertProduces('', '');
}
public function testOneTag()
{
$this->assertProduces('[b]this is bold[/b]', '<strong>this is bold</strong>');
}
public function testOneTagWithSurroundingText()
{
$this->assertProduces('buffer text [b]this is bold[/b] buffer text',
'buffer text <strong>this is bold</strong> buffer text');
}
public function testMultipleTags()
{
$bbcode = <<<EOD
this is some text with [b]bold tags[/b] and [i]italics[/i] and
things like [u]that[/u].
EOD;
$html = <<<EOD
this is some text with <strong>bold tags</strong> and <em>italics</em> and
things like <u>that</u>.
EOD;
$this->assertProduces($bbcode, $html);
}
public function testCodeOptions()
{
$code = 'This contains a [url=http://jbbcode.com/?b=2]url[/url] which uses an option.';
$html = 'This contains a <a href="http://jbbcode.com/?b=2">url</a> which uses an option.';
$this->assertProduces($code, $html);
}
public function testAttributes()
{
$parser = new JBBCode\Parser();
$builder = new JBBCode\CodeDefinitionBuilder('img', '<img src="{param}" height="{height}" alt="{alt}" />');
$parser->addCodeDefinition($builder->setUseOption(true)->setParseContent(false)->build());
$expected = 'Multiple <img src="http://jbbcode.com/img.png" height="50" alt="alt text" /> options.';
$code = 'Multiple [img height="50" alt="alt text"]http://jbbcode.com/img.png[/img] options.';
$parser->parse($code);
$result = $parser->getAsHTML();
$this->assertEquals($expected, $result);
$code = 'Multiple [img height=50 alt="alt text"]http://jbbcode.com/img.png[/img] options.';
$parser->parse($code);
$result = $parser->getAsHTML();
$this->assertEquals($expected, $result);
}
/**
* @depends testCodeOptions
*/
public function testOmittedOption()
{
$code = 'This doesn\'t use the url option [url]http://jbbcode.com[/url].';
$html = 'This doesn\'t use the url option <a href="http://jbbcode.com">http://jbbcode.com</a>.';
$this->assertProduces($code, $html);
}
public function testUnclosedTag()
{
$code = 'hello [b]world';
$html = 'hello <strong>world</strong>';
$this->assertProduces($code, $html);
}
public function testNestingTags()
{
$code = '[url=http://jbbcode.com][b]hello [u]world[/u][/b][/url]';
$html = '<a href="http://jbbcode.com"><strong>hello <u>world</u></strong></a>';
$this->assertProduces($code, $html);
}
public function testBracketInTag()
{
$this->assertProduces('[b]:-[[/b]', '<strong>:-[</strong>');
}
public function testBracketWithSpaceInTag()
{
$this->assertProduces('[b]:-[ [/b]', '<strong>:-[ </strong>');
}
public function testBracketWithTextInTag()
{
$this->assertProduces('[b]:-[ foobar[/b]', '<strong>:-[ foobar</strong>');
}
public function testMultibleBracketsWithTextInTag()
{
$this->assertProduces('[b]:-[ [fo[o[bar[/b]', '<strong>:-[ [fo[o[bar</strong>');
}
}

View file

@ -0,0 +1,74 @@
<?php
require_once(dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'Tokenizer.php');
/**
* Test cases testing the functionality of the Tokenizer. The tokenizer
* is used by the parser to make parsing simpler.
*
* @author jbowens
*/
class TokenizerTest extends PHPUnit_Framework_TestCase
{
public function testEmptyString()
{
$tokenizer = new JBBCode\Tokenizer('');
$this->assertFalse($tokenizer->hasNext());
}
public function testPlainTextOnly()
{
$tokenizer = new JBBCode\Tokenizer('this is some plain text.');
$this->assertEquals('this is some plain text.', $tokenizer->next());
$this->assertEquals('this is some plain text.', $tokenizer->current());
$this->assertFalse($tokenizer->hasNext());
}
public function testStartingBracket()
{
$tokenizer = new JBBCode\Tokenizer('[this has a starting bracket.');
$this->assertEquals('[', $tokenizer->next());
$this->assertEquals('[', $tokenizer->current());
$this->assertEquals('this has a starting bracket.', $tokenizer->next());
$this->assertEquals('this has a starting bracket.', $tokenizer->current());
$this->assertFalse($tokenizer->hasNext());
$this->assertEquals(null, $tokenizer->next());
}
public function testOneTag()
{
$tokenizer = new JBBCode\Tokenizer('[b]');
$this->assertEquals('[', $tokenizer->next());
$this->assertEquals('b', $tokenizer->next());
$this->assertEquals(']', $tokenizer->next());
$this->assertFalse($tokenizer->hasNext());
}
public function testMatchingTags()
{
$tokenizer = new JBBCode\Tokenizer('[url]http://jbbcode.com[/url]');
$this->assertEquals('[', $tokenizer->next());
$this->assertEquals('url', $tokenizer->next());
$this->assertEquals(']', $tokenizer->next());
$this->assertEquals('http://jbbcode.com', $tokenizer->next());
$this->assertEquals('[', $tokenizer->next());
$this->assertEquals('/url', $tokenizer->next());
$this->assertEquals(']', $tokenizer->next());
$this->assertFalse($tokenizer->hasNext());
}
public function testLotsOfBrackets()
{
$tokenizer = new JBBCode\Tokenizer('[[][]][');
$this->assertEquals('[', $tokenizer->next());
$this->assertEquals('[', $tokenizer->next());
$this->assertEquals(']', $tokenizer->next());
$this->assertEquals('[', $tokenizer->next());
$this->assertEquals(']', $tokenizer->next());
$this->assertEquals(']', $tokenizer->next());
$this->assertEquals('[', $tokenizer->next());
$this->assertFalse($tokenizer->hasNext());
}
}

View file

@ -0,0 +1,151 @@
<?php
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'Parser.php';
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'validators' . DIRECTORY_SEPARATOR . 'UrlValidator.php';
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'validators' . DIRECTORY_SEPARATOR . 'CssColorValidator.php';
/**
* Test cases for InputValidators.
*
* @author jbowens
* @since May 2013
*/
class ValidatorTest extends PHPUnit_Framework_TestCase
{
/**
* Tests an invalid url directly on the UrlValidator.
*/
public function testInvalidUrl()
{
$urlValidator = new \JBBCode\validators\UrlValidator();
$this->assertFalse($urlValidator->validate('#yolo#swag'));
$this->assertFalse($urlValidator->validate('giehtiehwtaw352353%3'));
}
/**
* Tests a valid url directly on the UrlValidator.
*/
public function testValidUrl()
{
$urlValidator = new \JBBCode\validators\UrlValidator();
$this->assertTrue($urlValidator->validate('http://google.com'));
$this->assertTrue($urlValidator->validate('http://jbbcode.com/docs'));
$this->assertTrue($urlValidator->validate('https://www.maps.google.com'));
}
/**
* Tests an invalid url as an option to a url bbcode.
*
* @depends testInvalidUrl
*/
public function testInvalidOptionUrlBBCode()
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->parse('[url=javascript:alert("HACKED!");]click me[/url]');
$this->assertEquals('[url=javascript:alert("HACKED!");]click me[/url]',
$parser->getAsHtml());
}
/**
* Tests an invalid url as the body to a url bbcode.
*
* @depends testInvalidUrl
*/
public function testInvalidBodyUrlBBCode()
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->parse('[url]javascript:alert("HACKED!");[/url]');
$this->assertEquals('[url]javascript:alert("HACKED!");[/url]', $parser->getAsHtml());
}
/**
* Tests a valid url as the body to a url bbcode.
*
* @depends testValidUrl
*/
public function testValidUrlBBCode()
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->parse('[url]http://jbbcode.com[/url]');
$this->assertEquals('<a href="http://jbbcode.com">http://jbbcode.com</a>',
$parser->getAsHtml());
}
/**
* Tests valid english CSS color descriptions on the CssColorValidator.
*/
public function testCssColorEnglish()
{
$colorValidator = new JBBCode\validators\CssColorValidator();
$this->assertTrue($colorValidator->validate('red'));
$this->assertTrue($colorValidator->validate('yellow'));
$this->assertTrue($colorValidator->validate('LightGoldenRodYellow'));
}
/**
* Tests valid hexadecimal CSS color values on the CssColorValidator.
*/
public function testCssColorHex()
{
$colorValidator = new JBBCode\validators\CssColorValidator();
$this->assertTrue($colorValidator->validate('#000'));
$this->assertTrue($colorValidator->validate('#ff0000'));
$this->assertTrue($colorValidator->validate('#aaaaaa'));
}
/**
* Tests valid rgba CSS color values on the CssColorValidator.
*/
public function testCssColorRgba()
{
$colorValidator = new JBBCode\validators\CssColorValidator();
$this->assertTrue($colorValidator->validate('rgba(255, 0, 0, 0.5)'));
$this->assertTrue($colorValidator->validate('rgba(50, 50, 50, 0.0)'));
}
/**
* Tests invalid CSS color values on the CssColorValidator.
*/
public function testInvalidCssColor()
{
$colorValidator = new JBBCode\validators\CssColorValidator();
$this->assertFalse($colorValidator->validate('" onclick="javascript: alert(\"gotcha!\");'));
$this->assertFalse($colorValidator->validate('"><marquee scrollamount="100'));
}
/**
* Tests valid css colors in a color bbcode.
*
* @depends testCssColorEnglish
* @depends testCssColorHex
*/
public function testValidColorBBCode()
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->parse('[color=red]colorful text[/color]');
$this->assertEquals('<span style="color: red">colorful text</span>',
$parser->getAsHtml());
$parser->parse('[color=#00ff00]green[/color]');
$this->assertEquals('<span style="color: #00ff00">green</span>', $parser->getAsHtml());
}
/**
* Tests invalid css colors in a color bbcode.
*
* @depends testInvalidCssColor
*/
public function testInvalidColorBBCode()
{
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
$parser->parse('[color=" onclick="alert(\'hey ya!\');]click me[/color]');
$this->assertEquals('[color=" onclick="alert(\'hey ya!\');]click me[/color]',
$parser->getAsHtml());
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace JBBCode\validators;
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'InputValidator.php';
/**
* An InputValidator for CSS color values. This is a very rudimentary
* validator. It will allow a lot of color values that are invalid. However,
* it shouldn't allow any invalid color values that are also a security
* concern.
*
* @author jbowens
* @since May 2013
*/
class CssColorValidator implements \JBBCode\InputValidator
{
/**
* Returns true if $input uses only valid CSS color value
* characters.
*
* @param $input the string to validate
*/
public function validate($input)
{
return (bool) preg_match('/^[A-z0-9\-#., ()%]+$/', $input);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace JBBCode\validators;
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'InputValidator.php';
/**
* An InputValidator for urls. This can be used to make [url] bbcodes secure.
*
* @author jbowens
* @since May 2013
*/
class UrlValidator implements \JBBCode\InputValidator
{
/**
* Returns true iff $input is a valid url.
*
* @param $input the string to validate
*/
public function validate($input)
{
$valid = filter_var($input, FILTER_VALIDATE_URL);
return !!$valid;
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace JBBCode\visitors;
/**
* This visitor escapes html content of all strings and attributes
*
* @author Alexander Polyanskikh
*/
class HTMLSafeVisitor implements \JBBCode\NodeVisitor
{
public function visitDocumentElement(\JBBCode\DocumentElement $documentElement)
{
foreach ($documentElement->getChildren() as $child) {
$child->accept($this);
}
}
public function visitTextNode(\JBBCode\TextNode $textNode)
{
$textNode->setValue($this->htmlSafe($textNode->getValue()));
}
public function visitElementNode(\JBBCode\ElementNode $elementNode)
{
$attrs = $elementNode->getAttribute();
if (is_array($attrs))
{
foreach ($attrs as &$el)
$el = $this->htmlSafe($el);
$elementNode->setAttribute($attrs);
}
foreach ($elementNode->getChildren() as $child) {
$child->accept($this);
}
}
protected function htmlSafe($str, $options = null)
{
if (is_null($options))
{
if (defined('ENT_DISALLOWED'))
$options = ENT_QUOTES | ENT_DISALLOWED | ENT_HTML401; // PHP 5.4+
else
$options = ENT_QUOTES; // PHP 5.3
}
return htmlspecialchars($str, $options, 'UTF-8');
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace JBBCode\visitors;
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'CodeDefinition.php';
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'DocumentElement.php';
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'ElementNode.php';
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'NodeVisitor.php';
require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'TextNode.php';
/**
* This visitor is used by the jBBCode core to enforce nest limits after
* parsing. It traverses the parse graph depth first, removing any subtrees
* that are nested deeper than an element's code definition allows.
*
* @author jbowens
* @since May 2013
*/
class NestLimitVisitor implements \JBBCode\NodeVisitor
{
/* A map from tag name to current depth. */
protected $depth = array();
public function visitDocumentElement(\JBBCode\DocumentElement $documentElement)
{
foreach($documentElement->getChildren() as $child) {
$child->accept($this);
}
}
public function visitTextNode(\JBBCode\TextNode $textNode)
{
/* Nothing to do. Text nodes don't have tag names or children. */
}
public function visitElementNode(\JBBCode\ElementNode $elementNode)
{
$tagName = strtolower($elementNode->getTagName());
/* Update the current depth for this tag name. */
if (isset($this->depth[$tagName])) {
$this->depth[$tagName]++;
} else {
$this->depth[$tagName] = 1;
}
/* Check if $elementNode is nested too deeply. */
if ($elementNode->getCodeDefinition()->getNestLimit() != -1 &&
$elementNode->getCodeDefinition()->getNestLimit() < $this->depth[$tagName]) {
/* This element is nested too deeply. We need to remove it and not visit any
* of its children. */
$elementNode->getParent()->removeChild($elementNode);
} else {
/* This element is not nested too deeply. Visit all of its children. */
foreach ($elementNode->getChildren() as $child) {
$child->accept($this);
}
}
/* Now that we're done visiting this node, decrement the depth. */
$this->depth[$tagName]--;
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace JBBCode\visitors;
/**
* This visitor is an example of how to implement smiley parsing on the JBBCode
* parse graph. It converts :) into image tags pointing to /smiley.png.
*
* @author jbowens
* @since April 2013
*/
class SmileyVisitor implements \JBBCode\NodeVisitor
{
function visitDocumentElement(\JBBCode\DocumentElement $documentElement)
{
foreach($documentElement->getChildren() as $child) {
$child->accept($this);
}
}
function visitTextNode(\JBBCode\TextNode $textNode)
{
/* Convert :) into an image tag. */
$textNode->setValue(str_replace(':)',
'<img src="/smiley.png" alt=":)" />',
$textNode->getValue()));
}
function visitElementNode(\JBBCode\ElementNode $elementNode)
{
/* We only want to visit text nodes within elements if the element's
* code definition allows for its content to be parsed.
*/
if ($elementNode->getCodeDefinition()->parseContent()) {
foreach ($elementNode->getChildren() as $child) {
$child->accept($this);
}
}
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace JBBCode\visitors;
/**
* This visitor traverses parse graph, counting the number of times each
* tag name occurs.
*
* @author jbowens
* @since January 2013
*/
class TagCountingVisitor implements \JBBcode\NodeVisitor
{
protected $frequencies = array();
public function visitDocumentElement(\JBBCode\DocumentElement $documentElement)
{
foreach ($documentElement->getChildren() as $child) {
$child->accept($this);
}
}
public function visitTextNode(\JBBCode\TextNode $textNode)
{
// Nothing to do here, text nodes do not have tag names or children
}
public function visitElementNode(\JBBCode\ElementNode $elementNode)
{
$tagName = strtolower($elementNode->getTagName());
// Update this tag name's frequency
if (isset($this->frequencies[$tagName])) {
$this->frequencies[$tagName]++;
} else {
$this->frequencies[$tagName] = 1;
}
// Visit all the node's childrens
foreach ($elementNode->getChildren() as $child) {
$child->accept($this);
}
}
/**
* Retrieves the frequency of the given tag name.
*
* @param $tagName the tag name to look up
*/
public function getFrequency($tagName)
{
if (!isset($this->frequencies[$tagName])) {
return 0;
} else {
return $this->frequencies[$tagName];
}
}
}

View file

@ -7,35 +7,74 @@ class Post
throw new Exception(__("You need to be logged in to perform this action."));
}
}
private static function parse_content($c){
//$c = htmlentities($c);
require_once APP_PATH."jbbcode/Parser.php";
$parser = new JBBCode\Parser();
$parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
// Highlight
if(Config::get("highlight")){
$c = preg_replace_callback('/\[code(?:=([^\[]+))?\]((.|\s)+?)(?:(?=\[\/code\]))\[\/code\]/m', function($m){
return '<code'.($m[1] ? ' class="'.$m[1].'"' : '').'>'.htmlentities(trim($m[2])).'</code>';
}, $c);
} else {
// Links
$c = preg_replace('/\"([^\"]+)\"/i', "$1\"", $c);
$c = str_replace("\t", " ", $c);
$c = preg_replace("/\[(\/?)code(=(?:[^\[]+))?\]\s*?(?:\n|\r)?/i", '[$1code$2]', $c);
// Add code definiton
$parser->addCodeDefinition(new class extends \JBBCode\CodeDefinition {
public function __construct($useOption){
parent::__construct($useOption);
$this->setTagName("code");
$this->setParseContent(false);
$this->setUseOption(true);
}
public function asHtml(\JBBCode\ElementNode $el){
$content = $this->getContent($el);
return '<code class="'.$el->getAttribute().'">'.htmlentities($content).'</code>';
}
});
}
if(($tags = Config::get_safe("bbtags", [])) && !empty($tags)){
foreach($tags as $tag => $content){
$builder = new JBBCode\CodeDefinitionBuilder($tag, $content);
$parser->addCodeDefinition($builder->build());
}
}
$parser->parse($c);
// Visit every text node
$parser->accept(new class implements \JBBCode\NodeVisitor{
function visitDocumentElement(\JBBCode\DocumentElement $documentElement){
foreach($documentElement->getChildren() as $child) {
$child->accept($this);
}
}
$c = preg_replace('/(https?\:\/\/[^\" \n]+)/i', "<a href=\"\\0\" target=\"_blank\">\\0</a>", $c);
//$c = preg_replace('/(\#([A-Za-z0-9-_]+))/i', "<a href=\"#tag=\\1\" class=\"tag\">\\0</a>", $c);
$c = preg_replace('/(\#[A-Za-z0-9-_]+)/i', "<span class=\"tag\">\\0</span>", $c);
function visitTextNode(\JBBCode\TextNode $textNode){
$c = $textNode->getValue();
$c = preg_replace('/\"([^\"]+)\"/i', "$1\"", $c);
$c = htmlentities($c);
$c = preg_replace('/\*([^\*]+)\*/i', "<strong>$1</strong>", $c);
$c = preg_replace('/(https?\:\/\/[^\" \n]+)/i', "<a href=\"\\0\" target=\"_blank\">\\0</a>", $c);
$c = preg_replace('/(\#[A-Za-z0-9-_]+)/i', "<span class=\"tag\">\\0</span>", $c);
$c = nl2br($c);
$textNode->setValue($c);
}
////Headlines
//$c = preg_replace('/^\# (.*)$/m', "<h1>$1</h1>", $c);
//$c = preg_replace('/^\#\# (.*)$/m', "<h2>$1</h2>", $c);
//$c = preg_replace('/^\#\#\# (.*)$/m', "<h3>$1</h3>", $c);
//$c = preg_replace('/\"([^\"]+)\"/i', "&#x84;&nbsp;<i>$1</i>&nbsp;&#x93;", $c);
$c = preg_replace('/\*([^\*]+)\*/i', "<strong>$1</strong>", $c);
$c = nl2br($c);
return $c;
function visitElementNode(\JBBCode\ElementNode $elementNode){
/* We only want to visit text nodes within elements if the element's
* code definition allows for its content to be parsed.
*/
if ($elementNode->getCodeDefinition()->parseContent()) {
foreach ($elementNode->getChildren() as $child) {
$child->accept($this);
}
}
}
});
return $parser->getAsHtml();
}
private static function raw_data($raw_input){

View file

@ -23,6 +23,8 @@ highlight = true
;styles[] = static/styles/custom2.css
;scripts = static/styles/scripts.css
bbtags[quote] = "<quote>{param}</quote>"
[login]
force_login = true
nick = demo

View file

@ -934,5 +934,6 @@ body > .error {
code {
overflow: auto;
white-space: nowrap;
white-space: pre;
margin: 5px 0;
}