Skip to main content

Overview

ForkBB includes a powerful BBCode parser for formatting posts and signatures. The system supports standard BBCode tags, custom tags, and extensible handlers for advanced formatting.

Parser Architecture

The BBCode parser is implemented in app/Core/Parser.php:16 and extends the Parserus library:
app/Core/Parser.php
class Parser extends Parserus
{
    public function __construct(int $flag, protected Container $c)
    {
        parent::__construct($flag);
        $this->init();
    }
    
    protected function init(): void
    {
        if (
            1 === $this->c->config->b_message_bbcode
            || 1 === $this->c->config->b_sig_bbcode
        ) {
            $this->setBBCodes($this->c->bbcode->list);
        }
        
        if (
            1 === $this->c->user->show_smilies
            && (
                1 === $this->c->config->b_smilies_sig
                || 1 === $this->c->config->b_smilies
            )
        ) {
            $smilies = [];
            
            foreach ($this->c->smilies->list as $cur) {
                $smilies[$cur['sm_code']] = $this->c->PUBLIC_URL . '/img/sm/' . $cur['sm_image'];
            }
            
            $info = $this->c->BBCODE_INFO;
            
            $this->setSmilies($smilies)->setSmTpl($info['smTpl'], $info['smTplTag'], $info['smTplBl']);
        }
        
        $this->setAttr('baseUrl', $this->c->BASE_URL);
        $this->setAttr('showImg', 1 === $this->c->user->show_img);
        $this->setAttr('showImgSign', 1 === $this->c->user->show_img_sig);
        $this->setAttr(
            'hashtagLink',
            1 === $this->c->user->g_search ? $this->c->Router->link('Search', ['keywords' => 'HASHTAG']) : null
        );
        $this->setAttr('user', $this->c->user);
    }
}
The parser configuration is initialized per-request based on user preferences and forum settings.

BBCode Management

BBCodes are managed through the BBCodeList model:
app/Models/BBCodeList/BBCodeList.php
class BBCodeList extends Model
{
    protected string $cKey = 'BBCodeList';
    
    public function __construct(string $file, Container $container)
    {
        parent::__construct($container);
        
        $this->fileDefault = "{$container->DIR_CONFIG}/{$file}";
        $this->fileCache   = "{$container->DIR_CACHE}/generated_bbcode.php";
    }
    
    public function init(): BBCodeList
    {
        if (! \is_file($this->fileCache)) {
            $this->generate();
        }
        
        $oldBadFile = $this->c->ErrorHandler->addBadFile($this->fileCache);
        
        $this->list = include $this->fileCache;
        
        $this->c->ErrorHandler->addBadFile($oldBadFile);
        
        return $this;
    }
    
    public function reset(): BBCodeList
    {
        if (\is_file($this->fileCache)) {
            if (\unlink($this->fileCache)) {
                return $this->invalidate();
            } else {
                throw new RuntimeException('The generated bbcode file cannot be deleted');
            }
        } else {
            return $this;
        }
    }
    
    public function invalidate(): BBCodeList
    {
        if (\function_exists('\\opcache_invalidate')) {
            \opcache_invalidate($this->fileCache, true);
        }
        
        return $this;
    }
}
BBCode configurations are compiled and cached for performance. Clear the cache after modifying BBCode definitions.

Standard BBCode Tags

Default BBCode tags are defined in app/config/defaultBBCode.php:

Text Formatting

app/config/defaultBBCode.php
[
    'tag' => 'b',
    'parents' => ['inline', 'block', 'url', 'h'],
    'handler' => <<<'HANDLER'
return "<b>{$body}</b>";
HANDLER,
],
[
    'tag' => 'i',
    'parents' => ['inline', 'block', 'url', 'h'],
    'handler' => <<<'HANDLER'
return "<i>{$body}</i>";
HANDLER,
],
[
    'tag' => 'u',
    'parents' => ['inline', 'block', 'url', 'h'],
    'handler' => <<<'HANDLER'
return "<u>{$body}</u>";
HANDLER,
],
[
    'tag' => 's',
    'parents' => ['inline', 'block', 'url', 'h'],
    'handler' => <<<'HANDLER'
return "<s>{$body}</s>";
HANDLER,
]
Usage:
  • [b]bold text[/b] → bold text
  • [i]italic text[/i] → italic text
  • [u]underline[/u] → underline
  • [s]strikethrough[/s] → strikethrough

Headers

Headers create sections with automatic anchors:
app/config/defaultBBCode.php
[
    'tag' => 'h1',
    'type' => 'h',
    'handler' => <<<'HANDLER'
$text  = $parser->getText($id);
$ident = $parser->createIdentifier($text);
$ident = $parser->e($ident);

return "</p><div id=\"{$ident}\" class=\"f-bb-h1\"><p>{$body}</p></div><p>";
HANDLER,
],
[
    'tag' => 'h2',
    'type' => 'h',
    'handler' => <<<'HANDLER'
$text  = $parser->getText($id);
$ident = $parser->createIdentifier($text);
$ident = $parser->e($ident);

return "</p><div id=\"{$ident}\" class=\"f-bb-h2\"><p>{$body}</p></div><p>";
HANDLER,
]
// h3, h4, h5, h6 follow same pattern
Usage:
[h1]Main Title[/h1]
[h2]Section Title[/h2]
[h3]Subsection[/h3]
Headers automatically generate IDs for linking and table of contents generation.

Colors

app/config/defaultBBCode.php
[
    'tag' => 'color',
    'parents' => ['inline', 'block', 'url', 'h'],
    'self_nesting' => 5,
    'attrs' => [
        'Def' => [
            'format' => '%^(?:\#(?:[\dA-Fa-f]{3}){1,2}|(?:aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|yellow|white))$%',
        ],
    ],
    'handler' => <<<'HANDLER'
$parser->inlineStyle(true);

$color = $attrs['Def'];

if ('#' === $color[0]) {
    $color = \strtoupper($color);
} else {
    $repl = [
        'black'   => '#000000',
        'gray'    => '#808080',
        'silver'  => '#C0C0C0',
        'white'   => '#FFFFFF',
        'fuchsia' => '#FF00FF',
        'purple'  => '#800080',
        'red'     => '#FF0000',
        'maroon'  => '#800000',
        'yellow'  => '#FFFF00',
        'olive'   => '#808000',
        'lime'    => '#00FF00',
        'green'   => '#008000',
        'aqua'    => '#00FFFF',
        'teal'    => '#008080',
        'blue'    => '#0000FF',
        'navy'    => '#000080',
    ];
    
    if (isset($repl[$color])) {
        $color = $repl[$color];
    }
}

return "<span class=\"f-bb-color\" style=\"color:{$color};\">{$body}</span>";
HANDLER,
]
Usage:
  • [color=red]Red text[/color]
  • [color=#FF0000]Red text[/color]
  • [color=#F00]Red text[/color]
  • [background=yellow]Highlighted[/background]

Code Blocks

app/config/defaultBBCode.php
[
    'tag' => 'code',
    'type' => 'block',
    'recursive' => true,
    'text_only' => true,
    'pre' => true,
    'attrs' => [
        'Def' => true,
        'No_attr' => true,
    ],
    'handler' => <<<'HANDLER'
return '</p><pre class="f-bb-code f-bbivert">' . \trim($body, "\n\r") . '</pre><p>';
HANDLER,
]
Usage:
[code]
function hello() {
    echo "Hello, World!";
}
[/code]

Content Parsing

Preparing Content

app/Core/Parser.php
public function prepare(string $text, bool $isSignature = false): string
{
    if ($isSignature) {
        $whiteList = 1 === $this->c->config->b_sig_bbcode
            ? (empty($this->c->config->a_bb_white_sig) && empty($this->c->config->a_bb_black_sig)
                ? null
                : $this->c->config->a_bb_white_sig
            )
            : [];
    } else {
        $whiteList = 1 === $this->c->config->b_message_bbcode
            ? (empty($this->c->config->a_bb_white_mes) && empty($this->c->config->a_bb_black_mes)
                ? null
                : $this->c->config->a_bb_white_mes
            )
            : [];
    }
    
    $blackList = 1 === $this->c->user->g_post_links ? null : ['email', 'url', 'img'];
    
    $this->setAttr('isSign', $isSignature)
        ->setWhiteList($whiteList)
        ->setBlackList($blackList)
        ->parse($text, ['strict' => true])
        ->stripEmptyTags(" \n\t\r\v", true);
    
    if (1 === $this->c->config->b_make_links) {
        $this->detectUrls();
    }
    
    // Create hashtags
    $this->detect('hashtag', '%(?<=^|\s|\n|\r)#(?=[\p{L}\p{N}_]{3})[\p{L}\p{N}]+(?:_+[\p{L}\p{N}]+)*(?=$|\s|\n|\r|\.|,)%u', true);
    
    return \preg_replace('%^(\x20*\n)+|(\n\x20*)+$%D', '', $this->getCode());
}

Parsing Messages

app/Core/Parser.php
public function parseMessage(?string $text = null, bool $hideSmilies = false): string
{
    // If null, use data from prepare()
    if (null !== $text) {
        $whiteList = 1 === $this->c->config->b_message_bbcode ? null : [];
        $blackList = $this->c->config->a_bb_black_mes;
        
        $this->setAttr('isSign', false)
            ->setWhiteList($whiteList)
            ->setBlackList($blackList)
            ->parse($text);
    }
    
    if (
        ! $hideSmilies
        && 1 === $this->c->config->b_smilies
    ) {
        $this->detectSmilies();
    }
    
    return $this->getHtml();
}

Parsing Signatures

app/Core/Parser.php
public function parseSignature(?string $text = null): string
{
    // If null, use data from prepare()
    if (null !== $text) {
        $whiteList = 1 === $this->c->config->b_sig_bbcode ? null : [];
        $blackList = $this->c->config->a_bb_black_sig;
        
        $this->setAttr('isSign', true)
            ->setWhiteList($whiteList)
            ->setBlackList($blackList)
            ->parse($text);
    }
    
    if (1 === $this->c->config->b_smilies_sig) {
        $this->detectSmilies();
    }
    
    return $this->getHtml();
}
Messages and signatures can have different allowed BBCode tags and different smilie settings.

Whitelists and Blacklists

Tag Filtering

  • Whitelist: Only listed tags are allowed (null = all tags)
  • Blacklist: Listed tags are forbidden (null = no restrictions)
// Allow only basic formatting
$whiteList = ['b', 'i', 'u', 'color'];

// Block links and images
$blackList = ['url', 'email', 'img'];

$parser->setWhiteList($whiteList)
       ->setBlackList($blackList)
       ->parse($text);
Users without link posting permission have url, email, and img tags automatically blacklisted.

URL Detection

Automatic URL linking:
app/Core/Parser.php
if (1 === $this->c->config->b_make_links) {
    $this->detectUrls();
}
Plain URLs in text are automatically converted to clickable links when enabled.

Hashtag Support

Hashtags are automatically detected and linked:
app/Core/Parser.php
$this->detect('hashtag', '%(?<=^|\s|\n|\r)#(?=[\p{L}\p{N}_]{3})[\p{L}\p{N}]+(?:_+[\p{L}\p{N}]+)*(?=$|\s|\n|\r|\.|,)%u', true);
Hashtags must:
  • Start with #
  • Have at least 3 alphanumeric characters
  • Be preceded by whitespace
  • Be followed by whitespace or punctuation
Example: #programming, #web_development, #ForkBB

Inline Styles

Some BBCodes require inline styles:
app/Core/Parser.php
protected bool $flagInlneStyle = false;

public function inlineStyle(?bool $flag = null): bool
{
    $prev = $this->flagInlneStyle;
    
    if (true === $flag) {
        $this->flagInlneStyle = $flag;
    }
    
    return $prev;
}
Handlers call $parser->inlineStyle(true) when they use inline CSS.

Quoting

Prepare content for quoting by removing certain tags:
app/Core/Parser.php
public function prepareToQuote(string $text, array $remove = []): string
{
    $whiteList = 1 === $this->c->config->b_message_bbcode ? null : [];
    $blackList = $this->c->config->a_bb_black_mes;
    
    $this->setAttr('isSign', false)
        ->setWhiteList($whiteList)
        ->setBlackList($blackList)
        ->parse($text);
    
    if ($remove) {
        $arr = $this->getIds(...(\array_keys($remove)));
        
        if ($arr) {
            foreach ($arr as $id => $name) {
                $this->data[$id]['text'] = $remove[$name];
                $this->data[$id]['tag'] = null;
            }
        }
    }
    
    return \preg_replace('%^(\x20*\n)+|(\n\x20*)+$%D', '', $this->getCode());
}
Usage:
// Remove quote tags when quoting
$quoted = $parser->prepareToQuote($text, ['quote' => '']);

Custom BBCode Tags

Create custom tags by extending the BBCode configuration:
[
    'tag' => 'custom',
    'type' => 'block',
    'parents' => ['block'],
    'attrs' => [
        'Def' => [
            'format' => '%^[a-z]+$%i',
        ],
    ],
    'handler' => <<<'HANDLER'
$attr = $attrs['Def'] ?? 'default';
return "<div class='custom-{$attr}'>{$body}</div>";
HANDLER,
]

Tag Configuration

  • tag: Tag name (lowercase)
  • type: Tag type (block, inline, h, url, etc.)
  • parents: Allowed parent tags
  • recursive: Allow nested tags of same type
  • text_only: Tag contains only text (no other tags)
  • pre: Preserve whitespace
  • single: Self-closing tag
  • attrs: Attribute definitions
  • handler: PHP code to render HTML

Smilies

Emoticons are configured separately:
app/Core/Parser.php
$smilies = [];

foreach ($this->c->smilies->list as $cur) {
    $smilies[$cur['sm_code']] = $this->c->PUBLIC_URL . '/img/sm/' . $cur['sm_image'];
}

$info = $this->c->BBCODE_INFO;

$this->setSmilies($smilies)->setSmTpl($info['smTpl'], $info['smTplTag'], $info['smTplBl']);
Smilies can be:
  • Enabled/disabled globally
  • Enabled/disabled in signatures
  • Controlled per-user preference

Performance Considerations

Caching

Compiled BBCode configurations are cached

Lazy Loading

BBCode list loaded only when needed

Opcache

PHP opcache accelerates handler execution

Minimal Parsing

Parse once, render multiple times

Security

XSS Prevention: The parser escapes all user input. Never use {!! $html !!} raw output in templates.

Safe Output

Always escape dynamic content:
$parser->e($userInput);  // Escape HTML entities

URL Validation

URLs are validated before rendering:
// Only http:// and https:// protocols allowed by default
// Configure additional protocols in bbcode settings

Best Practices

Clear BBCode cache after modifying tag definitions. Use reset() and invalidate() methods.
Define proper parent relationships to prevent invalid nesting. Test thoroughly with complex combinations.
Limit recursive nesting depth with self_nesting property. Consider disabling expensive tags for signatures.
Provide a preview feature. Show available BBCode tags in editor. Consider WYSIWYG editor integration.

Forums & Topics

How BBCode is used in posts and topics

Search

How BBCode affects search indexing