Creating a Custom BB Code in XenForo: A Comprehensive Guide

Jeremy

in memoriam 1991-2020
As requested, here is a comprehensive Step By Step guide to creating BB Codes for XenForo, or at least, until Kier and Mike create a manager in the backend.

In XenForo, BB Codes are handled via PHP classes (as in most other bulletin boards), and to add more, you must extend these classes, adding more tags to the list. Below, I will take you step by step in adding three tags: a spoiler tag (one that may or may not have options), a simple header tag (h2), and another simple float tag (with an option).

Note: This article does not cover how to utilize a template for your display, and everything is currently handled via PHP. I may revisit this in the future to add a "template based" PHP BBCode.

The Basic Setup and Classes​
In this section, I will describe the basic setup required to setup the BB Code system to utilize your newly created tags / classes.


Step 1: Place a test/development board into debug mode.

I do not suggest you place a live, functioning XenForo forum into debug mode. Always, always utilize a test / development board. To accomplish this, add the following PHP code to the beginning of your config.php file after creating a back up of the file:
PHP:
$config['debug'] = 1;

Reloading your Administrator Control Panel (ACP) should now show a 'Development' tab next to the 'Tools' tab. If it does not, please make sure you uploaded and overwrote your existing configuration file.

Step 2: Create your add on.

Since XenForo wants everything related (and rightly so), our next step is to setup a new "add on" for use on your site (you will export this at the end of this article to import to your live board). For simplicity, I shall name mine "XenBBCode". To add a new add on you want to go into the Development tab within the ACP and click on "Create Add-On." In here, you want to place your ID in the ID slot. This ID will be used later in the article in naming of classes, and should not include any spaces.

Step 3: Create your event listener class.

The next required step, is to create an event listener class to allow extension of the BB Code system. To accomplish this, create a file located in /library/XenBBCode/EventListener/ and called BbCode.php. You will need to create the XenBBCode directory and its sub-directories yourself. These are unique to your add-on. (Note: XenBBCode should be the ID of your add-on for the rest of this article.) Now, add the following contents to your new PHP file...
PHP:
<?php

class XenBBCode_EventListener_BbCode
{
    public static function listen($class, array &$extend)
    {
        if ($class == 'XenForo_BbCode_Formatter_Base')
        {
            $extend[] = 'XenBBCode_BbCode_Formatter_Base';
        }
    }
}

Within this file, as you may have noticed is a class. The title of the class should always follow your directory structure. Every time you have a slash ( / ), replace it with an underscore ( _ ), and just chop off the .php extension. So, let's look at that transformation:
XenBBCode/EventListener/BbCode.php -> XenBBCode_EventListener_BbCode

This allows XenForo's autoloading of classes to recognize and know the location of the file your new class resides in.

You may have also noticed the contents of the class is a single function, with two arguments. These arguments are gathered from XenForo, and will be explained in the next step. The code within the function is something we only want run when it attempts to load the BbCode formatting class that comes with XenForo, so, we add the condition. And lastly, the code within the condition tells XenForo which class should be used to extend the default class (and it is $extend[] so that XenForo can automatically resolve hierarchy and such for you). Within the function definition, it is also important to have the word "static" in there as XenForo calls this function statically, and never creates a new object.

Step 4: Add the Event Listener

Now that we have created our class, we want to tell XenForo to utilize the class when it attempts to load the specified class. We do this through the extremely powerful Event Listener system. This system basically opens up the entire forum to extension through just a few extremely well placed "events." So, let's add ours to the mix.

Within the development tab, lets click on "Code Event Listeners" and then again on "Create New Code Event Lisener." Within this screen, we want to set a few options. Firstly, we want to choose "load_class_bb_code" as the event we want to listen in on. Once you choose this option, you will notice a a description pop up below the select. Within this description, you will see a list of arguments, that correspond to the arguments we added into our listen() function in the last step. These must be absolutely exactly the same for the system to work correctly.

Now, let's set the callback method. The first option is the class name you created (XenBBCode_EventListener_BbCode, for my example) and the second is the method. The method will be listen as that's the way I told you to name it in the last one, but it should be the name of the function you created to extend the BbCode parsing class.

The last two options you want to set are the description and add-on. The add-on should be the one you created in the second step. The description can be anything you want, but should be used to describe why the event listener is added. For this example, I placed "Adds three custom BBCodes to XenForo."

Adding the Custom BB Code Parser Class​
Now that we have the basics of our add-on / custom BB Code system in place, we can start actually adding BB Codes to our system. To do this, we will create a class that will be used to house all three BB Codes that will be added. By creating a simple class, you ensure all are properly added and created for use on your board. I'm sure you can already figure out the class name we will be using, but in case it didn't immediately jump into your head (don't worry, there's nothing wrong with that) the class name will be: XenBBCode_BbCode_Formatter_Base.


Step 5: Create the Extending Class

Next, we want to create the class that we defined as the extension to the default BB Code formatting class. Create a new file in /library/XenBBCode/BbCode/Formatter/ and name it Base.php. Again, refer to Step 3 as to why the file is to be located in this directory and named Base.php. Add the following code to your class (Note: If this class was left as is, your forum's BBCode would be unchanged, as it is identical to the included BB Code formatter):
PHP:
class XenBBCode_BbCode_Formatter_Base extends XFCP_XenBBCode_BbCode_Formatter_Base
{
	protected $_tags;
	public function getTags()
	{
		$this->_tags = parent::getTags();
		return $this->_tags;
	}
}

You will (or should) notice that our class is extending a class that isn't defined. This will be resolved by XenForo automatically, but you always want to place an extends statement in your definition. Our classes extend a class with the same name as ours, just with the prefix of XFCP_. Make sure this line is there, or else, our add-on will not work as desired, if at all.

Within the class, we define a protected variable _tags, which will hold an array off all the tags and their information. This variable name was taken from its parent class, but isn't strictly required to be _tags, but I recommend you do this.

Now, we overwrite a function called getTags() inside of our class so that we can add tags. When the parser starts to parse BB Code, it'll call our getTags(), rather than the included one. Which means, if we just define our tags, everything as simple as [b][/b] would not be displayed as bolded text. So, the first line in our getTags() class should always be:
PHP:
$this->_tags = parent::getTags();

This statement will return an array of all default XenForo BB Codes and add them to our array, which we will build upon in later sections of this guide. And the last line should always be returning our array, so the parser can build and display posts correctly (with custom tags!).

Creating a simple, no option required BB Code (<h2></h2>)​

Next, we are going to look at how to add our BB Codes to the system. To do this (and for the rest of this article), we will be working in the class we defined in Step 5, and not moving outside of that class. So, when I ask you to add code, modify a function, etc., it should be done within this class.

Step 6: Add the definition for an [h2][/h2] tag.

Within the getTags() function, we want to add a new entry into our _tags[] array, so that it'll recognize [h2][/h2] as a valid tag. To do this, we want to modify our getTags() function to look like such:
PHP:
	public function getTags()
	{
		$this->_tags = parent::getTags();
		$this->_tags['h2'] = array(
			'hasOption' => false,
			'replace' => array('<h2>', '</h2>')
		);
		return $this->_tags;
	}

What we do, is we add a new index to the array ('h2'), and define an array of options as its value. For this simple tag, we want to say that it does not have an option ('hasOption' => false) and that it should be replaced with <h2> for [h2] and </h2> for [/h2]. This is defined in the option 'replace', which itself houses an array of two things: the replacement for the opening tag (first index) and the replacement for the second tag (second index).

If you save and upload this file, you should now be able to utilize [h2] within your posts. If you comment out these lines, [h2] will not be replaced when BB Code formatting occurs. Test this, just to make sure there weren't any errors (there shouldn't be, tho).
 
Building a simple, option required BB Code (float)​

Now that you've seen how to add a simple, no option required BB Code for your forum via PHP and Event listeners, let's now add one that adds a small bit of complexity, but is still relatively simple. We want to create a [float][/float] tag that tags the direction (left, right) as the option. in theory, if a user knows his way around this system and CSS, you can style you're float any way you want (as it's just a <div style="float: {option};">) in different posts. So, let's dive right in.

Step 7: Add the definition for a [float][/float] tag.

Now, we want to add another entry into our index, so let's do that by modifying getTags() once more to give us the following PHP code:
PHP:
	public function getTags()
	{
		$this->_tags = parent::getTags();
		$this->_tags['h2'] = array(
			'hasOption' => false,
			'replace' => array('<h2>', '</h2>')
		);
		$this->_tags['float'] = array(
			'hasOption' => true,
			'replace' => array('<div style="float: %s;">', '</div>')
		);
		return $this->_tags;
	}

Now, you will notice that the form is very similar between float and h2, but the key difference is that 'hasOption' => is now set to true. Meaning, when XenForo starts to parse the thread, if you put [float][/float], it will leave it as such, as you haven't specified an option. You can get around this via a more complicated method that'll be explained with the addition of a spoiler BB Code in the next section.

Now, you must provide XenForo with data on where you want the option to be displayed and you'll want to do this by placing "%s" where you want the option to display. Note: You may only use this method once, meaning you cannot put the option in more than one place. If you'd like to do this, please look at adding the spoiler BBCode, else, it won't work correctly and will spit out PHP errors and warnings.

Now, save you're class, and over write the existing class. Adding [float="right"]Hi![/float] should create a new div that floats to the right. Something such as [float="right; right-margin: 5px"]margin![/float], should still work just fine.

Building a complex BB Code (spoiler)​

You have seen just how easy it is to create a few simple BB Codes via the PHP classes described above. In the next few steps, we will take this a few steps further and create a "complex" BB Code tag. In our example, the tag just needs to use the option in multiple places in the out put, so this'll show you how to accomplish that. However, this type of method can be used to create advanced BB Codes with multiple options, a option/no option set up (as in this case also), and other cool features you may want to incorporate. As long as you can do it via PHP, it can be done via this method.

Step 8: Add the definition for the tags.

Again, we want to update getTags() to add another entry to our list of BB Codes. Your new getTags() function should look like this:
PHP:
	public function getTags()
	{
		$this->_tags = parent::getTags();
		$this->_tags['h2'] = array(
				'hasOption' => false,
				'replace' => array('<h2>', '</h2')
			);
		$this->_tags['float'] = array(
				'hasOption' => true,
				'replace' => array('<div style="float: %s;">', '</div>')
			);
		$this->_tags['spoiler'] = array(
				'parseCallback' => array($this, 'parseValidatePlainIfNoOption'),
				'callback' => array($this, 'renderTagSpoiler')
			);
		return $this->_tags;
	}

If you'll notice in the tag definition for 'spoiler' we only define the option 'callback' rather than 'hasOption' or 'replace', because both of those functions will be handled by the function that'll be added in the next step. Honestly, I'm not sure what 'parseCallback' does, but I modeled my tag definitions for PHP callback functions off of the shipped class and they all had this.

Step 9: create the function renderTagSpoiler()

This new function will have 2 arguments, and the PHP code will utilize an array of data passed to it by the parser. This data is in the form of this (no option):
Code:
array(4) {
  ["tag"]=>
  string(4) "tagDefinition"
  ["option"]=>
  NULL
  ["original"]=>
  array(2) {
    [0]=>
    string(6) "[tagDefinition]"
    [1]=>
    string(7) "[/tagDefintion]"
  }
  ["children"]=>
  array(1) {
    [0]=>
    string(6) "insideTags"
  }
}
or (with option)
Code:
array(4) {
  ["tag"]=>
  string(4) "tagDefinition"
  ["option"]=>
  string(1) "option"
  ["original"]=>
  array(2) {
    [0]=>
    string(10) "[tagDefinition="option"]"
    [1]=>
    string(7) "[/tagDefinition]"
  }
  ["children"]=>
  array(1) {
    [0]=>
    string(6) "inisdeTag"
  }
}

Our PHP function (renderTagSpoiler()) must be able to decipher the array and build an output as desired. So, let's create the function with the following PHP code:
PHP:
	public function renderTagSpoiler(array $tag, array $rendererStates)
	{
		if($tag['option'] != NULL)
		{
			$buttonText = $tag['option'];
		}
		else
		{
			$buttonText = 'Show Spoiler';
		}

		$output = '<div class="spoiler">
						<div class="quotetitle">
							<input type="button" value="' . $buttonText . '" style="font-size:11px;margin:0px;padding:0px;" onclick="
								if (this.parentNode.parentNode.getElementsByTagName(\'div\')[1].getElementsByTagName(\'div\')[0].style.display != \'\')
								{
									this.parentNode.parentNode.getElementsByTagName(\'div\')[1].getElementsByTagName(\'div\')[0].style.display = \'\';
									this.innerText = \'\'; this.value = \'Hide\';
								}
								else
								{
									this.parentNode.parentNode.getElementsByTagName(\'div\')[1].getElementsByTagName(\'div\')[0].style.display = \'none\';
									this.innerText = \'\'; this.value = \'' . $buttonText . '\';
								}" />
						</div>
						<div class="quotecontent">
							<div style="display: none;">' . $tag['children'][0] . '</div>
						</div>
					</div>';
		return $output;
	}

Now, let's look at this function in closer detail. First, we are checking to see if the $tag['option'] is NULL, and if it is, set some default text. Now, if you need to see where $tag option is set, please refer to the previous code blocks for the structure. Now, once done, we want to build the output (of which I did in a "pretty" form, but it can be all on one line if need be). And once we build the output as desired, we want to return that output to the parser, who will then display it inside of the post.

Installing​

After finishing building and testing your new BB Codes, you want to export the add on (click on Manage Add-ons in the home screen of the ACP and Controls -> Export on the one you created for this). Download the entire /library/XenBBCode/ folder and upload it to your live sites library directory (keeping the structure under XenBBCode folder there too). Install the add-on in your live site's Add-On manager.

Complete Files​

Now that we have built our classes, let's look at the two files we need for this (note, I have not listed the back end code event listeners / add on set up, as that entirety is listed in steps 1-4).

File: /library/XenBBCode/EventListener/BbCode.php
PHP:
class XenBBCode_EventListener_BbCode
{
    public static function listen($class, array &$extend)
    {
        if ($class == 'XenForo_BbCode_Formatter_Base')
        {
            $extend[] = 'XenBBCode_BbCode_Formatter_Base';
        }
    }
}

File: /library/XenBBCode/BbCode/Formatter/Base.php
PHP:
class XenBBCode_BbCode_Formatter_Base extends XFCP_XenBBCode_BbCode_Formatter_Base
{
	protected $_tags;
	public function getTags()
	{
		$this->_tags = parent::getTags();
		$this->_tags['h2'] = array(
				'hasOption' => false,
				'replace' => array('<h2>', '</h2')
			);
		$this->_tags['float'] = array(
				'hasOption' => true,
				'replace' => array('<div style="float: %s;">', '</div>')
			);
		$this->_tags['spoiler'] = array(
				'callback' => array($this, 'renderTagSpoiler')
			);
		return $this->_tags;
	}
	public function renderTagSpoiler(array $tag, array $rendererStates)
	{
		if($tag['option'] != NULL)
		{
			$buttonText = $tag['option'];
		}
		else
		{
			$buttonText = 'Show Spoiler';
		}
		$output = '<div class="spoiler">
						<div class="quotetitle">
							<input type="button" value="' . $buttonText . '" style="font-size:11px;margin:0px;padding:0px;" onclick="
								if (this.parentNode.parentNode.getElementsByTagName(\'div\')[1].getElementsByTagName(\'div\')[0].style.display != \'\')
								{
									this.parentNode.parentNode.getElementsByTagName(\'div\')[1].getElementsByTagName(\'div\')[0].style.display = \'\';
									this.innerText = \'\'; this.value = \'Hide\';
								}
								else
								{
									this.parentNode.parentNode.getElementsByTagName(\'div\')[1].getElementsByTagName(\'div\')[0].style.display = \'none\';
									this.innerText = \'\'; this.value = \'' . $buttonText . '\';
								}" />
						</div>
						<div class="quotecontent">
							<div style="display: none;">' . $tag['children'][0] . '</div>
						</div>
					</div>';
		return $output;
	}
}

Closing Comments​

Now that we have accomplished building our classes, we can now happily use our new custom BBCodes on our board. However, there is a lot that I may have not covered in this guide, as I was not 100% sure of all the options that can be used. Maybe Kier or Mike can comment, but if you'd like to see more options available to BB Codes, check out this file:
/library/XenForo/BbCode/Formatter/Base.php

Also, the power of this system is that you have access to everything. Options, phrases, visitors, etc. are all accessible.

Also, if you'd like to install these BB Codes, please install my add-on, as it now includes all three of these, plus, a few extra ones.
 
Also, just a small note, I may come out with a third part to this guide, but may not. However, the three codes above were not 'tested' per se, but should work at how simple they are. I have yet to release / include them inside of BB Code Manager.

Edit: Also, this doesn't add your custom BB Codes to the help page, I have yet to look into that, but that'll be in part 3. And all BB Codes are not displayed in the editor (must figure that out too), so they must be typed by hand.
 
Lol... I actually just made custom vbulletin codes for my portal wiki... I wish I had your guide beforehand.
 
Awesome delivery King as you promised,
I am going to go get my mind right really quick and come sit back down right here and have at this in about a half an hour... thrilled to have a guide...pure awesomeness.

Thank you very much. Hopefully I can follow this which looks to be complete guide.
If I make a solid bbcode from this I may just name it after you :) ( yes that was a firstborn joke )

Again
THANK YOU FOR THIS !!!
 
Awesome delivery King as you promised,
I am going to go get my mind right really quick and come sit back down right here and have at this in about a half an hour... thrilled to have a guide...pure awesomeness.

Thank you very much. Hopefully I can follow this which looks to be complete guide.
If I make a solid bbcode from this I may just name it after you :) ( yes that was a firstborn joke )

Again
THANK YOU FOR THIS !!!
I mentioned one or two things that I left out, because I haven't determined how to accomplish them yet, but I will. Trust me... I will. :D Also, if you have any questions, please, please post them. It means my dense head failed to write what I meant to.
 
I mentioned one or two things that I left out, because I haven't determined how to accomplish them yet, but I will. Trust me... I will. :D Also, if you have any questions, please, please post them. It means my dense head failed to write what I meant to.
OP followed slightly absorbed and most importantly functional :)

I am going to try failing at using this to implement my custom bb code right now !awesome!

I should add that I haven't tried the second post yet as I'm tired and anxious to see I can temporarily get some of my codes in with your method here...seeing as how the first part works.:D

Where's that plus one button ? :D


Sleep before I break it and fresh start in the morning
 
We should mention, that such defined BBCodes cannot be dynamic. I mean, that they are parsed only once per post creation/modification and then post content is cached so we don't need to parse BBCodes on every page hit. This prevents us from creating BBCodes, that show different things to different users, for example.
 
Haha. I'm sorry. You figure it out tho? Something like this?
Nope... just added this code...

Code:
$page['pageHTML'] = preg_replace('/\[h(\d+)\](.*?)\[\/h(\d+)\]/i', '<h$1>$2</h$1>', $page['pageHTML']);
$page['pageHTML'] = preg_replace('/<\/h(\d+)><br \/>/i', '</h$1>', $page['pageHTML']);
 
Thank you for this, I am not 100% this is the best way to do it though... For example your spoiler code would allow me to do something along the lines of

[spoiler]<script type="text/javascript">alert('Hacked');</script>[/spoiler]

And it would execute the HTML as it is...

Also the other issue is around nested BBCODES if you place any BBCODES in the spoiler as an example it would just return "Array".

Any idea how to expand to fix this?

-------------------------------
EDIT... Fixed it..

You should do the following...

PHP:
$content = $this->renderSubTree($tag['children'], $rendererStates);

then instead of calling $tag['children'][0] you just use $content.
 
I'll update that soon. :) And I'll check against scripting flaws, etc. I noticed that if I remove the class the posts are immediately back to , so dynamic may or may not work? And this is how XenForo does it, so...
 
I'll update that soon. :) And I'll check against scripting flaws, etc. I noticed that if I remove the class the posts are immediately back to , so dynamic may or may not work? And this is how XenForo does it, so...
I also noticed that there is no database cached post data. But in code XenForo usually extracts models from cache. Do you have any caching enabled (APC for example)? Anyway, having no cached post content is a performance flaw and I'm sure Kier will fix this soon.
 
Step 4:

This is what I did but I get an error message.

2eq5744.png
 
Back
Top Bottom