jBBCode used
This commit is contained in:
parent
31786706d1
commit
a5926a5179
38 changed files with 3268 additions and 24 deletions
328
app/jbbcode/CodeDefinition.php
Normal file
328
app/jbbcode/CodeDefinition.php
Normal 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;
|
||||
}
|
||||
}
|
160
app/jbbcode/CodeDefinitionBuilder.php
Normal file
160
app/jbbcode/CodeDefinitionBuilder.php
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
22
app/jbbcode/CodeDefinitionSet.php
Normal file
22
app/jbbcode/CodeDefinitionSet.php
Normal 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();
|
||||
|
||||
}
|
75
app/jbbcode/DefaultCodeDefinitionSet.php
Normal file
75
app/jbbcode/DefaultCodeDefinitionSet.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
67
app/jbbcode/DocumentElement.php
Normal file
67
app/jbbcode/DocumentElement.php
Normal 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
241
app/jbbcode/ElementNode.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
20
app/jbbcode/InputValidator.php
Normal file
20
app/jbbcode/InputValidator.php
Normal 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
109
app/jbbcode/Node.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
20
app/jbbcode/NodeVisitor.php
Normal file
20
app/jbbcode/NodeVisitor.php
Normal 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
662
app/jbbcode/Parser.php
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
7
app/jbbcode/ParserException.php
Normal file
7
app/jbbcode/ParserException.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace JBBCode;
|
||||
use Exception;
|
||||
|
||||
class ParserException extends Exception{
|
||||
}
|
102
app/jbbcode/TextNode.php
Normal file
102
app/jbbcode/TextNode.php
Normal 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
105
app/jbbcode/Tokenizer.php
Normal 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));
|
||||
}
|
||||
|
||||
}
|
12
app/jbbcode/examples/1-GettingStarted.php
Normal file
12
app/jbbcode/examples/1-GettingStarted.php
Normal 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();
|
10
app/jbbcode/examples/2-ClosingUnclosedTags.php
Normal file
10
app/jbbcode/examples/2-ClosingUnclosedTags.php
Normal 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();
|
11
app/jbbcode/examples/3-MarkuplessText.php
Normal file
11
app/jbbcode/examples/3-MarkuplessText.php
Normal 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();
|
7
app/jbbcode/examples/4-CreatingNewCodes.php
Normal file
7
app/jbbcode/examples/4-CreatingNewCodes.php
Normal 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);
|
22
app/jbbcode/examples/SmileyVisitorTest.php
Normal file
22
app/jbbcode/examples/SmileyVisitorTest.php
Normal 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";
|
23
app/jbbcode/examples/TagCountingVisitorTest.php
Normal file
23
app/jbbcode/examples/TagCountingVisitorTest.php
Normal 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";
|
85
app/jbbcode/tests/BBCodeToBBCodeTest.php
Normal file
85
app/jbbcode/tests/BBCodeToBBCodeTest.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
78
app/jbbcode/tests/BBCodeToTextTest.php
Normal file
78
app/jbbcode/tests/BBCodeToTextTest.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
54
app/jbbcode/tests/DefaultCodesTest.php
Normal file
54
app/jbbcode/tests/DefaultCodesTest.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
77
app/jbbcode/tests/HTMLSafeTest.php
Normal file
77
app/jbbcode/tests/HTMLSafeTest.php
Normal 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"xt te&xt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests escaping quotes and ampersands inside a BBCode tag
|
||||
*/
|
||||
public function testQuoteAndAmpInTag()
|
||||
{
|
||||
$this->assertProduces('[b]te"xt te&xt[/b]', '<strong>te"xt te&xt</strong>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests escaping HTML tags
|
||||
*/
|
||||
public function testHtmlTag()
|
||||
{
|
||||
$this->assertProduces('<b>not bold</b>', '<b>not bold</b>');
|
||||
$this->assertProduces('[b]<b>bold</b>[/b] <hr>', '<strong><b>bold</b></strong> <hr>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests escaping ampersands in URL using [url]...[/url]
|
||||
*/
|
||||
public function testUrlParam()
|
||||
{
|
||||
$this->assertProduces('text [url]http://example.com/?a=b&c=d[/url] more text', 'text <a href="http://example.com/?a=b&c=d">http://example.com/?a=b&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&c=d">this is a "link"</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&c=d">this is a "link"</a>');
|
||||
}
|
||||
|
||||
}
|
46
app/jbbcode/tests/NestLimitTest.php
Normal file
46
app/jbbcode/tests/NestLimitTest.php
Normal 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());
|
||||
}
|
||||
|
||||
}
|
97
app/jbbcode/tests/ParseContentTest.php
Normal file
97
app/jbbcode/tests/ParseContentTest.php
Normal 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());
|
||||
}
|
||||
|
||||
}
|
130
app/jbbcode/tests/ParsingEdgeCaseTest.php
Normal file
130
app/jbbcode/tests/ParsingEdgeCaseTest.php
Normal 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 ] ');
|
||||
}
|
||||
|
||||
}
|
131
app/jbbcode/tests/SimpleEvaluationTest.php
Normal file
131
app/jbbcode/tests/SimpleEvaluationTest.php
Normal 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>');
|
||||
}
|
||||
|
||||
}
|
74
app/jbbcode/tests/TokenizerTest.php
Normal file
74
app/jbbcode/tests/TokenizerTest.php
Normal 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());
|
||||
}
|
||||
|
||||
}
|
151
app/jbbcode/tests/ValidatorTest.php
Normal file
151
app/jbbcode/tests/ValidatorTest.php
Normal 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());
|
||||
}
|
||||
|
||||
}
|
30
app/jbbcode/validators/CssColorValidator.php
Normal file
30
app/jbbcode/validators/CssColorValidator.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
27
app/jbbcode/validators/UrlValidator.php
Normal file
27
app/jbbcode/validators/UrlValidator.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
52
app/jbbcode/visitors/HTMLSafeVisitor.php
Normal file
52
app/jbbcode/visitors/HTMLSafeVisitor.php
Normal 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');
|
||||
}
|
||||
}
|
65
app/jbbcode/visitors/NestLimitVisitor.php
Normal file
65
app/jbbcode/visitors/NestLimitVisitor.php
Normal 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]--;
|
||||
}
|
||||
|
||||
}
|
42
app/jbbcode/visitors/SmileyVisitor.php
Normal file
42
app/jbbcode/visitors/SmileyVisitor.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
60
app/jbbcode/visitors/TagCountingVisitor.php
Normal file
60
app/jbbcode/visitors/TagCountingVisitor.php
Normal 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];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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', "„ <i>$1</i> “", $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){
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -934,5 +934,6 @@ body > .error {
|
|||
|
||||
code {
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
white-space: pre;
|
||||
margin: 5px 0;
|
||||
}
|
Loading…
Add table
Reference in a new issue