code:
<?php

/**
 * Script generating CAPTCHAs
 *
 * @author Jose Rodriguez <jose.rodriguez@exec.cl>

 * @author Desuneko (added Imagick support)

 * @license GPLv3

 * @link http://code.google.com/p/cool-php-captcha

 * @package captcha

 * @version 0.3
 */

/**
 * SimpleCaptcha class
 */

class SimpleCaptcha
{

    /**
     * Width of the image
     */

    public $width  = 300;

    /**
     * Height of the image
     */

    public $height = 70;

    /**
     * Dictionary word file (empty for random text)
     */

    public $wordsFile = 'words/en.php';

    /**
     * Path for resource files (fonts, words, etc.)

     * "resources" by default. For security reasons, is better move this

     * directory to another location outise the web server
     */

    public $resourcesPath = 'libs/cool-php-captcha/resources';

    /**
     * Min word length (for non-dictionary random text generation)
     */

    public $minWordLength = 5;

    /**
     * Max word length (for non-dictionary random text generation)

     * Used for dictionary words indicating the word-length

     * for font-size modification purposes
     */

    public $maxWordLength = 8;

    /**
     * Sessionname to store the original text
     */

    public $session_var = 'captcha';

    /**
     * Background color in RGB-array
     */

    public $backgroundColor = array(255, 255, 255);

    /**
     * Foreground colors in RGB-array
     */

    public $colors = array(

    array(27,78,181), // blue

    array(22,163,35), // green

    array(214,36,7),  // red

    );

    /**
     * Shadow color in RGB-array or null
     */

    public $shadowColor = null; //array(0, 0, 0);

    /**
     * Horizontal line through the text
     */

    public $lineWidth = 0;

    /**
     * Font configuration

     * - font: TTF file

     * - spacing: relative pixel space between character

     * - minSize: min font size

     * - maxSize: max font size
     */

    public $fonts = array(

    'Antykwa'  => array('spacing' => -3, 'minSize' => 27, 'maxSize' => 30, 'font' => 'AntykwaBold.ttf'),

    'Candice'  => array('spacing' =>-1.5,'minSize' => 28, 'maxSize' => 31, 'font' => 'Candice.ttf'),

    'DingDong' => array('spacing' => -2, 'minSize' => 24, 'maxSize' => 30, 'font' => 'Ding-DongDaddyO.ttf'),

    'Duality'  => array('spacing' => -2, 'minSize' => 30, 'maxSize' => 38, 'font' => 'Duality.ttf'),

    'Heineken' => array('spacing' => -2, 'minSize' => 24, 'maxSize' => 34, 'font' => 'Heineken.ttf'),

    'Jura'     => array('spacing' => -2, 'minSize' => 28, 'maxSize' => 32, 'font' => 'Jura.ttf'),

    'StayPuft' => array('spacing' =>-1.5,'minSize' => 28, 'maxSize' => 32, 'font' => 'StayPuft.ttf'),

    'Times'    => array('spacing' => -2, 'minSize' => 28, 'maxSize' => 34, 'font' => 'TimesNewRomanBold.ttf'),

    'VeraSans' => array('spacing' => -1, 'minSize' => 20, 'maxSize' => 28, 'font' => 'VeraSansBold.ttf'),

    );

    /**
     * Wave configuracion in X and Y axes
     */

    public $Yperiod    = 12;

    public $Yamplitude = 14;

    public $Xperiod    = 11;

    public $Xamplitude = 5;

    /**
     * letter rotation clockwise
     */

    public $maxRotation = 8;

    /**
     * Internal image size factor (for better image quality)

     * 1: low, 2: medium, 3: high
     */

    public $scale = 2;

    /**
     * Blur effect for better image quality (but slower image processing).

     * Better image results with scale=3
     */

    public $blur = false;

    /**
     * Debug?
     */

    public $debug = false;

    /**
     * Image format: jpeg or png
     */

    public $imageFormat = 'jpeg';

    /**
     * GD image
     */

    public $im;

    public $useImageMagick = false;

    /**
     * Get image as <img> element with base64 string [imagick]
     **/

    public $getImageAsBase64String = false;

    /**
     * Reduce image colours to 18 [imagick]
     **/

    public $reduceImageColors = false;

    public function __construct($config = array())
    {

    }

    public function CreateImage()
    {

        $ini = microtime(true);

        /**
 * Initialization
*/

        $this->ImageAllocate();

        /**
 * Text insertion
*/

        $text = $this->GetCaptchaText();

        $fontcfg  = $this->fonts[array_rand($this->fonts)];

        $this->WriteText($text, $fontcfg);

        $_SESSION[$this->session_var] = $text;

        /**
 * Transformations
*/

        if (!empty($this->lineWidth)) {

            $this->WriteLine();

        }

        $this->WaveImage();

        if (($this->useImageMagick) && ($this->blur)) {

            $this->im->gaussianBlurImage(4, 1);

        } elseif ($this->blur && function_exists('imagefilter')) {

            imagefilter($this->im, IMG_FILTER_GAUSSIAN_BLUR);

        }

        $this->ReduceImage();

        if ($this->debug) {

            if ($this->useImageMagick) {

                $draw = new ImagickDraw();

                $draw->setFontSize(9);

                $draw->setFillColor(new ImagickPixel("black"));

                $this->im->annotateImage($draw, 10, 10, 0, "$text {$fontcfg['font']} ".round((microtime(true)-$ini)*1000)."ms");

            } else {

                imagestring(
                    $this->im, 1, 1, $this->height-8,
                    "$text {$fontcfg['font']} ".round((microtime(true)-$ini)*1000)."ms",
                    $this->GdFgColor
                );

            }

        }

        /**
 * Output
*/

        $this->WriteImage();

        $this->Cleanup();

    }

    /**
     * Creates the image resources
     */

    protected function ImageAllocate()
    {

        if ($this->useImageMagick) {

            if (!empty($this->im)) {

                $this->im->destroy();

            }

            $this->im = new Imagick();

            $this->GdBgColor = new ImagickPixel(
                "rgb(".

                $this->backgroundColor[0].", ".

                $this->backgroundColor[1].", ".

                $this->backgroundColor[2].")"
            );

            $this->im->newImage($this->width*$this->scale, $this->height*$this->scale, $this->GdBgColor);

            $this->im->setImageBackgroundColor($this->GdBgColor);

            $this->im->setImageMatteColor($this->GdBgColor);

            // Foreground color

            $color           = $this->colors[mt_rand(0, sizeof($this->colors)-1)];

            $this->GdFgColor = new ImagickPixel(
                "rgb(".

                $color[0].", ".

                $color[1].", ".

                $color[2].")"
            );

            // Shadow color

            if (!empty($this->shadowColor) && is_array($this->shadowColor) && sizeof($this->shadowColor) >= 3) {

                $this->GdShadowColor = new ImagickPixel(
                    "rgb(".

                    $this->shadowColor[0].", ".

                    $this->shadowColor[1].", ".

                    $this->shadowColor[2].")"
                );

            }

        } else {

            if (!empty($this->im)) {

                imagedestroy($this->im);

            }

            $this->im = imagecreatetruecolor($this->width*$this->scale, $this->height*$this->scale);

            // Background color

            $this->GdBgColor = imagecolorallocate(
                $this->im,
                $this->backgroundColor[0],
                $this->backgroundColor[1],
                $this->backgroundColor[2]
            );

            imagefilledrectangle($this->im, 0, 0, $this->width*$this->scale, $this->height*$this->scale, $this->GdBgColor);

            // Foreground color

            $color           = $this->colors[mt_rand(0, sizeof($this->colors)-1)];

            $this->GdFgColor = imagecolorallocate($this->im, $color[0], $color[1], $color[2]);

            // Shadow color

            if (!empty($this->shadowColor) && is_array($this->shadowColor) && sizeof($this->shadowColor) >= 3) {

                $this->GdShadowColor = imagecolorallocate(
                    $this->im,
                    $this->shadowColor[0],
                    $this->shadowColor[1],
                    $this->shadowColor[2]
                );

            }

        }

    }

    /**
     * Text generation
     *
     * @return string Text
     */

    protected function GetCaptchaText()
    {

        $text = $this->GetDictionaryCaptchaText();

        if (!$text) {

            $text = $this->GetRandomCaptchaText();

        }

        return $text;

    }

    /**
     * Random text generation
     *
     * @return string Text
     */

    protected function GetRandomCaptchaText($length = null)
    {

        if (empty($length)) {

            $length = rand($this->minWordLength, $this->maxWordLength);

        }

        $words  = "abcdefghijlmnopqrstvwyz";

        $vocals = "aeiou";

        $text  = "";

        $vocal = rand(0, 1);

        for ($i=0; $i<$length; $i++) {

            if ($vocal) {

                $text .= substr($vocals, mt_rand(0, 4), 1);

            } else {

                $text .= substr($words, mt_rand(0, 22), 1);

            }

            $vocal = !$vocal;

        }

        return $text;

    }

    /**
     * Random dictionary word generation
     *
     * @param boolean $extended Add extended "fake" words

     * @return string Word
     */

    function GetDictionaryCaptchaText($extended = false)
    {

        if (empty($this->wordsFile)) {

            return false;

        }

        // Full path of words file

        if (substr($this->wordsFile, 0, 1) == '/') {

            $wordsfile = $this->wordsFile;

        } else {

            $wordsfile = $this->resourcesPath.'/'.$this->wordsFile;

        }

        if (!file_exists($wordsfile)) {

            return false;

        }

        $fp     = fopen($wordsfile, "r");

        $length = strlen(fgets($fp));

        if (!$length) {

            return false;

        }

        $line   = rand(1, (filesize($wordsfile)/$length)-2);

        if (fseek($fp, $length*$line) == -1) {

            return false;

        }

        $text = trim(fgets($fp));

        fclose($fp);

        /**
 * Change ramdom volcals
*/

        if ($extended) {

            $text   = preg_split('//', $text, -1, PREG_SPLIT_NO_EMPTY);

            $vocals = array('a', 'e', 'i', 'o', 'u');

            foreach ($text as $i => $char) {

                if (mt_rand(0, 1) && in_array($char, $vocals)) {

                    $text[$i] = $vocals[mt_rand(0, 4)];

                }

            }

            $text = implode('', $text);

        }

        return $text;

    }

    /**
     * Horizontal line insertion
     */

    protected function WriteLine()
    {

        if ($this->useImageMagick) {

            $x1 = $this->width*$this->scale*.15;

            $x2 = $this->textFinalX;

            $y1 = rand($this->height*$this->scale*.40, $this->height*$this->scale*.65);

            $y2 = rand($this->height*$this->scale*.40, $this->height*$this->scale*.65);

            $width = $this->lineWidth/2*$this->scale;

            $draw = new ImagickDraw();

            $draw->setFillColor($this->GdFgColor);

            for ($i = $width*-1; $i <= $width; $i++) {

                $draw->line($x1, $y1+$i, $x2, $y2+$i);

            }

            $this->im->drawImage($draw);

        } else {

            $x1 = $this->width*$this->scale*.15;

            $x2 = $this->textFinalX;

            $y1 = rand($this->height*$this->scale*.40, $this->height*$this->scale*.65);

            $y2 = rand($this->height*$this->scale*.40, $this->height*$this->scale*.65);

            $width = $this->lineWidth/2*$this->scale;

            for ($i = $width*-1; $i <= $width; $i++) {

                imageline($this->im, $x1, $y1+$i, $x2, $y2+$i, $this->GdFgColor);

            }

        }

    }

    /**
     * Text insertion
     */

    protected function WriteText($text, $fontcfg = array())
    {

        if (empty($fontcfg)) {

            // Select the font configuration

            $fontcfg  = $this->fonts[array_rand($this->fonts)];

        }

        // Full path of font file

        $fontfile = $this->resourcesPath.'/fonts/'.$fontcfg['font'];

        /**
 * Increase font-size for shortest words: 9% for each glyp missing
*/

        $lettersMissing = $this->maxWordLength-strlen($text);

        $fontSizefactor = 1+($lettersMissing*0.09);

        // Text generation (char by char)

        $x      = ($this->width*0.15)*$this->scale;

        $y      = round(($this->height*0.75)*$this->scale);

        $length = strlen($text);

        if ($this->useImageMagick) {

            $draw = new ImagickDraw();

            $draw->setFont($fontfile);

            for ($i=0; $i<$length; $i++) {

                $degree   = rand($this->maxRotation*-1, $this->maxRotation);

                $fontsize = rand($fontcfg['minSize'], $fontcfg['maxSize'])*$this->scale*($this->width/250)*$fontSizefactor*1.5;

                $letter   = substr($text, $i, 1);

                $draw->setFontSize($fontsize);

                if ($this->shadowColor) {

                    $draw->setFillColor($this->GdShadowColor);

                    $this->im->annotateImage($draw, $x+$this->scale, $y+$this->scale, $degree, $letter);

                }

                $draw->setFillColor($this->GdFgColor);

                $fm = $this->im->queryFontMetrics($draw, $letter);

                $this->im->annotateImage($draw, $x, $y, $degree, $letter);

                $x += $fm['boundingBox']['x2'] + ($fontcfg['spacing']*$this->scale);

            }

        } else {

            for ($i=0; $i<$length; $i++) {

                $degree   = rand($this->maxRotation*-1, $this->maxRotation);

                $fontsize = rand($fontcfg['minSize'], $fontcfg['maxSize'])*$this->scale*($this->width/250)*$fontSizefactor;

                $letter   = substr($text, $i, 1);

                if ($this->shadowColor) {

                    $coords = imagettftext(
                        $this->im, $fontsize, $degree,
                        $x+$this->scale, $y+$this->scale,
                        $this->GdShadowColor, $fontfile, $letter
                    );

                }

                $coords = imagettftext(
                    $this->im, $fontsize, $degree,
                    $x, $y,
                    $this->GdFgColor, $fontfile, $letter
                );

                $x += ($coords[2]-$x) + ($fontcfg['spacing']*$this->scale);

            }

        }

        $this->textFinalX = $x;

    }

    /**
     * Wave filter
     */

    protected function WaveImage()
    {

        if ($this->useImageMagick) {

            $this->im->waveImage($this->Xamplitude, $this->scale*$this->Xperiod*rand(1, 3)*4);

            $this->im->rotateImage(new ImagickPixel('none'), 90);

            $this->im->waveImage($this->Yamplitude, $this->scale*$this->Yperiod*rand(1, 3)*4);

            $this->im->rotateImage(new ImagickPixel('none'), -90);

            $this->im->setImageBackgroundColor($this->GdBgColor);

            $this->im = $this->im->flattenImages();

        } else {

            // X-axis wave generation

            $xp = $this->scale*$this->Xperiod*rand(1, 3);

            $k = rand(0, 100);

            for ($i = 0; $i < ($this->width*$this->scale); $i++) {

                imagecopy(
                    $this->im, $this->im,
                    $i-1, sin($k+$i/$xp) * ($this->scale*$this->Xamplitude),
                    $i, 0, 1, $this->height*$this->scale
                );

            }

            // Y-axis wave generation

            $k = rand(0, 100);

            $yp = $this->scale*$this->Yperiod*rand(1, 2);

            for ($i = 0; $i < ($this->height*$this->scale); $i++) {

                imagecopy(
                    $this->im, $this->im,
                    sin($k+$i/$yp) * ($this->scale*$this->Yamplitude), $i-1,
                    0, $i, $this->width*$this->scale, 1
                );

            }

        }

    }

    /**
     * Reduce the image to the final size
     */

    protected function ReduceImage()
    {

        if ($this->useImageMagick) {

            $this->im->scaleImage($this->width, $this->height);

        } else {

            $imResampled = imagecreatetruecolor($this->width, $this->height);

            imagecopyresampled(
                $imResampled, $this->im,
                0, 0, 0, 0,
                $this->width, $this->height,
                $this->width*$this->scale, $this->height*$this->scale
            );

            imagedestroy($this->im);

            $this->im = $imResampled;

        }

    }

    /**
     * File generation
     */

    protected function WriteImage()
    {

        if ($this->useImageMagick) {

            if ($this->debug) {

                $this->im->borderImage(new ImagickPixel("rgb(220,220,220)"), 1, 1);

            }

            $this->im->setImageFormat('png');

            if ($this->getImageAsBase64String) {

                echo "<img src='data:image/jpg;base64,".base64_encode($this->im->getImageBlob())."' alt='Captcha'/>";

            } else {

                if ($this->reduceImageColors) {

                    $this->im->posterizeImage(18, true);

                }

                header("Content-type: image/png");

                echo $this->im->getImageBlob();

            }

        } else {

            if ($this->imageFormat == 'png' && function_exists('imagepng')) {

                header("Content-type: image/png");

                imagepng($this->im);

            } else {

                header("Content-type: image/jpeg");

                imagejpeg($this->im, null, 80);

            }

        }

    }

    /**
     * Cleanup
     */

    protected function Cleanup()
    {

        if ($this->useImageMagick) {

            $this->im->destroy();

        } else {

            imagedestroy($this->im);

        }

    }

}

?>

license: mit


code:
<?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 = null,
        $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 = null;

        $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
            && !$this->optionValidator->validate($el->getAttribute())
        ) {

            /* The option argument to $el does not pass the option validator. */

            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()) {

            $html = str_ireplace('{option}', $el->getAttribute(), $html);

        }

        if ($this->parseContent()) {

            $content = "";

            foreach ($el->getChildren() as $child) {

                $content .= $child->getAsHTML();
            }

        } else {

            $content = "";

            foreach ($el->getChildren() as $child) {

                $content .= $child->getAsBBCode();
            }

        }

        $html = str_ireplace('{param}', $content, $html);

        return $html;

    }

    /**
     * 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;

    }

}


code:
<?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 = null;

    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

     * @return $this
     */

    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

     * @return $this
     */

    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

     * @return $this
     */

    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

     * @return $this
     */

    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.

     * @return $this

     * @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

     * @return $this
     */

    public function setOptionValidator(\JBBCode\InputValidator $validator)
    {

        $this->optionValidator = $validator;

        return $this;

    }

    /**
     * Sets the InputValidator that body ({param}) text should be validated with.
     *
     * @param $validator  the InputValidator instance to use

     * @return $this
     */

    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 = null;

        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;

    }

}


code:
<?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();

}


code:
<?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', '{param}');

        array_push($this->definitions, $builder->build());

        /* [i] italics tag */

        $builder = new CodeDefinitionBuilder('i', '{param}');

        array_push($this->definitions, $builder->build());

        /* [u] italics tag */

        $builder = new CodeDefinitionBuilder('u', '{param}');

        array_push($this->definitions, $builder->build());

        $urlValidator = new \JBBCode\validators\UrlValidator();

        /* [url] link tag */

        $builder = new CodeDefinitionBuilder('url', '{param}');

        $builder->setParseContent(false)->setBodyValidator($urlValidator);

        array_push($this->definitions, $builder->build());

        /* [url=http://example.com] link tag */

        $builder = new CodeDefinitionBuilder('url', '{param}');

        $builder->setUseOption(true)->setParseContent(true)->setOptionValidator($urlValidator);

        array_push($this->definitions, $builder->build());

        /* [img] image tag */

        $builder = new CodeDefinitionBuilder('img', '');

        $builder->setUseOption(false)->setParseContent(false)->setBodyValidator($urlValidator);

        array_push($this->definitions, $builder->build());

        /* [img=alt text] image tag */

        $builder = new CodeDefinitionBuilder('img', '');

        $builder->setUseOption(true);

        array_push($this->definitions, $builder->build());

        /* [color] color tag */

        $builder = new CodeDefinitionBuilder('color', '{param}');

        $builder->setUseOption(true)->setOptionValidator(new \JBBCode\validators\CssColorValidator());

        array_push($this->definitions, $builder->build());

    }

    /**
     * Returns an array of the default code definitions.
     */

    public function getCodeDefinitions()
    {

        return $this->definitions;

    }

}


code:
<?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);

    }

}


code:
<?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 elemennt 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 ($this->attribute != null) {

            $str .= "=" . $this->attribute;

        }

        $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;

        }

    }

}


code:
<?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);

}


code:
<?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;

    }

}


code:
<?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);

}


code:
<?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 '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
{

    /* 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)
     */

    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());

    }

    /**
     * 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
     */

    public function addCodeDefinition( CodeDefinition $definition )
    {

        array_push($this->bbcodes, $definition);

    }

    /**
     * Adds a set of CodeDefinitions.
     *
     * @param CodeDefinitionSet $set the set of definitions to add
     */

    public function addCodeDefinitionSet(CodeDefinitionSet $set)
    {

        foreach ($set->getCodeDefinitions() as $def) {

            $this->addCodeDefinition($def);

        }

    }

    /**
     * Returns the entire parse tree as text. Only {param} content is returned. BBCode markup will be ignored.
     *
     * @return 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 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 a parsed html string
     */

    public function getAsHTML()
    {

        return $this->treeRoot->getAsHTML();

    }

    /**
     * Accepts the given NodeVisitor at the root.
     *
     * @param nodeVisitor  a NodeVisitor
     */

    public function accept(NodeVisitor $nodeVisitor)
    {

        $this->treeRoot->accept($nodeVisitor);

    }

    /**
     * Constructs the parse tree from a string of bbcode markup.
     *
     * @param string $str the bbcode markup to parse
     */

    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 a O(n) traversal to remove any elements that

         * are nested beywond their CodeDefinition's nest limit. */

        $this->removeOverNestedElements();

    }

    /**
     * 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 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 the newly created TextNode
     */

    protected function createTextNode(ElementNode $parent, $string)
    {

        $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 $parent  the current parent node we're under

     * @param $tokenizer  the tokenizer we're using
     *
     * @return 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 $parent  the current parent node

     * @param $tokenizer  the tokenizer we're using
     *
     * @return 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();

        }

        /* 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);

        }

    }

    /**
     * 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 $parent  the current parent element

     * @param $tokenizer  the tokenizer we're using

     * @param $tagContent  the text between the [ and the ], assuming there is actually a ]
     *
     * @return 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 an attribute. */

        $tagPieces = explode('=', $tagContent);

        $tmpTagName = $tagPieces[0];

        $actualTagName;

        if ('/' == $tmpTagName[0]) {

            /* This is a closing tag name. */

            $actualTagName = substr($tmpTagName, 1);

        } else {

            $actualTagName = $tmpTagName;

        }

        if ('/' == $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($tagPieces) > 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, count($tagPieces) > 1)) {

            /* 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, count($tagPieces) > 1);

        $el->setCodeDefinition($code);

        if (count($tagPieces) > 1) {

            /* We have an attribute we should save. */

            unset($tagPieces[0]);

            $el->setAttribute(implode('=', $tagPieces));

        }

        $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 $parent  the current parent element

     * @param $tokenizer  the tokenizer we're using

     * @return ElementNode|the
     */

    protected function parseAsTextUntilClose(ElementNode $parent, Tokenizer $tokenizer)
    {

        /* $parent's code definition doesn't allow its contents to be parsed. Here we use

         * a sliding of 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();

        }

    }

}


code:
jBBCode
=======

jBBCode is a bbcode parser written in php 5.3. It's relatively lightweight and parses
bbcodes without resorting to expensive regular expressions.

Documentation
-------------

For complete documentation and examples visit [jbbcode.com](http://jbbcode.com).

###A basic example

jBBCode includes a few optional, default bbcode definitions that may be loaded through the
`DefaultCodeDefinitionSet` class. Below is a simple example of using these codes to convert
a bbcode string to html.

```php
<?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();
```

Contribute
----------

I would love help maintaining jBBCode. Look at [open issues](http://github.com/jbowens/jBBCode/issues) for ideas on
what needs to be done. Before submitting a pull request, verify that all unit tests still pass.

#### Running unit tests
To run the unit tests,
ensure that [phpunit](http://github.com/sebastianbergmann/phpunit) is installed, or install it through the composer
dev dependencies. Then run `phpunit ./tests` from the project directory. If you're adding new functionality, writing
additional unit tests is a great idea.

Author
------

jBBCode was written by Jackson Owens. You may reach him at [jackson_owens@brown.edu](mailto:jackson_owens@brown.edu).

License
-------

    The MIT License

    Copyright (c) 2011 Jackson Owens

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    THE SOFTWARE.


code:
<?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;

    }

}


code:
<?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 iff 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));

    }

}


jbbcode/jbbcode

type: library

description: A lightweight but extensible BBCode parser written in PHP 5.3.

keywords: BBCode BB

license: MIT

Jackson Owens

role: Developer

minimum-stability: stable


code:
<?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();


code:
<?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();


code:
<?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();


code:
<?php

require_once "/path/to/jbbcode/Parser.php";

$parser = new JBBCode\Parser();

$parser->addBBCode("quote", '
{param}
');

$parser->addBBCode("code", '
{param}
', false, false, 1);


code:
<?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";


code:
<?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";


code:
<?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);

    }

}


code:
<?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);

    }

}


code:
<?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]', 'this should be bold');

    }

    /**
     * Tests the [color] bbcode.
     */

    public function testColor()
    {

        $this->assertProduces('[color=red]red[/color]', 'red');

    }

    /**
     * Tests the example from the documentation.
     */

    public function testExample()
    {

        $text = "The default codes include: [b]bold[/b], [i]italics[/i], [u]underlining[/u], ";

        $text .= "[url=http://jbbcode.com]links[/url], [color=red]color![/color] and more.";

        $html = 'The default codes include: bold, italics, underlining, ';

        $html .= 'links, color! and more.';

        $this->assertProduces($text, $html);

    }

}


code:
<?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', '{param}', false, true, -1);

        $parser->parse('[b][b][b][b][b][b][b][b]bold text[/b][/b][/b][/b][/b][/b][/b][/b]');

        $this->assertEquals(
            '' .

                'bold text' .

                '',
            $parser->getAsHtml()
        );

    }

    /**
     * Test over nesting.
     */

    public function testOverNesting()
    {

        $parser = new JBBCode\Parser();

        $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());

        $parser->addBBCode('quote', '
{param}
', false, true, 2);

        $bbcode = '[quote][quote][quote]wut[/quote] huh?[/quote] i don\'t know[/quote]';

        $parser->parse($bbcode);

        $expectedBbcode = '[quote][quote] huh?[/quote] i don\'t know[/quote]';

        $expectedHtml = '

 huh?
 i don\'t know
';

        $this->assertEquals($expectedBbcode, $parser->getAsBBCode());

        $this->assertEquals($expectedHtml, $parser->getAsHtml());

    }

}


code:
<?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());

    }

}


code:
<?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]',
            '[[][heh'
        );

    }

    /**
     * Tests an unclosed tag within a closed tag.
     */

    public function testUnclosedWithinClosed()
    {

        $this->assertProduces(
            '[url=http://jbbcode.com][b]oh yeah[/url]',
            'oh yeah'
        );

    }

    /**
     * Tests half completed opening tag.
     */

    public function testHalfOpenTag()
    {

        $this->assertProduces('[b', '[b');

        $this->assertProduces(
            'wut [url=http://jbbcode.com',
            'wut [url=http://jbbcode.com'
        );

    }

    /**
     * Tests half completed closing tag.
     */

    public function testHalfClosingTag()
    {

        $this->assertProduces(
            '[b]this should be bold[/b',
            'this should be bold[/b'
        );

    }

    /**
     * Tests lots of left brackets before the actual tag. For example:

     * [[[[[[[[b]bold![/b]
     */

    public function testLeftBracketsThenTag()
    {

        $this->assertProduces(
            '[[[[[b]bold![/b]',
            '[[[[bold!'
        );

    }

}


code:
<?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]', 'this is bold');

    }

    public function testOneTagWithSurroundingText()
    {

        $this->assertProduces(
            'buffer text [b]this is bold[/b] buffer text',
            'buffer text this is bold buffer text'
        );

    }

    public function testMultipleTags()
    {

        $bbcode = <<<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 bold tags and italics and

things like that.

EOD;

        $this->assertProduces($bbcode, $html);

    }

    public function testCodeOptions()
    {

        $code = 'This contains a [url=http://jbbcode.com]url[/url] which uses an option.';

        $html = 'This contains a url which uses an option.';

        $this->assertProduces($code, $html);

    }

    /**

     * @depends testCodeOptions
     */

    public function testOmittedOption()
    {

        $code = 'This doesn\'t use the url option [url]http://jbbcode.com[/url].';

        $html = 'This doesn\'t use the url option http://jbbcode.com.';

        $this->assertProduces($code, $html);

    }

    public function testUnclosedTag()
    {

        $code = 'hello [b]world';

        $html = 'hello world';

        $this->assertProduces($code, $html);

    }

    public function testNestingTags()
    {

        $code = '[url=http://jbbcode.com][b]hello [u]world[/u][/b][/url]';

        $html = 'hello world';

        $this->assertProduces($code, $html);

    }

}


code:
<?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());

    }

}


code:
<?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(
            'http://jbbcode.com',
            $parser->getAsHtml()
        );

    }

    /**
     * Tests valid english CSS color descriptions on the CssColorValidator.
     */

    public function testCssColorEnglish()
    {

        $colorValidator = new JBBCode\validators\CssColorValidator();

        $this->assertTrue($colorValidator->validate('red'));

        $this->assertTrue($colorValidator->validate('yellow'));

        $this->assertTrue($colorValidator->validate('LightGoldenRodYellow'));

    }

    /**
     * Tests valid hexadecimal CSS color values on the CssColorValidator.
     */

    public function testCssColorHex()
    {

        $colorValidator = new JBBCode\validators\CssColorValidator();

        $this->assertTrue($colorValidator->validate('#000'));

        $this->assertTrue($colorValidator->validate('#ff0000'));

        $this->assertTrue($colorValidator->validate('#aaaaaa'));

    }

    /**
     * Tests valid rgba CSS color values on the CssColorValidator.
     */

    public function testCssColorRgba()
    {

        $colorValidator = new JBBCode\validators\CssColorValidator();

        $this->assertTrue($colorValidator->validate('rgba(255, 0, 0, 0.5)'));

        $this->assertTrue($colorValidator->validate('rgba(50, 50, 50, 0.0)'));

    }

    /**
     * Tests invalid CSS color values on the CssColorValidator.
     */

    public function testInvalidCssColor()
    {

        $colorValidator = new JBBCode\validators\CssColorValidator();

        $this->assertFalse($colorValidator->validate('" onclick="javascript: alert(\"gotcha!\");'));

        $this->assertFalse($colorValidator->validate('"><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(
            'colorful text',
            $parser->getAsHtml()
        );

        $parser->parse('[color=#00ff00]green[/color]');

        $this->assertEquals('green', $parser->getAsHtml());

    }

    /**
     * Tests invalid css colors in a color bbcode.
     *
     * @depends testInvalidCssColor
     */

    public function testInvalidColorBBCode()
    {

        $parser = new JBBCode\Parser();

        $parser->addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());

        $parser->parse('[color=" onclick="alert(\'hey ya!\');]click me[/color]');

        $this->assertEquals(
            '[color=" onclick="alert(\'hey ya!\');]click me[/color]',
            $parser->getAsHtml()
        );

    }

}


code:
<?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

     * @return bool
     */

    public function validate($input)
    {

        return (bool) preg_match('/^[A-z0-9\-#., ()%]+$/', $input);

    }

}


code:
<?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

     * @return bool
     */

    public function validate($input)
    {

        $valid = filter_var($input, FILTER_VALIDATE_URL);

        return !!$valid;

    }

}