Cách thêm thẻ H1,H2,H3 vào khung soạn thảo cho Xenforo

  • Thread starter Thread starter PVS
  • Ngày gửi Ngày gửi

PVS

Super Moderator
Thành viên BQT
Hiện nay mình thấy Xenforo không cho phép viết thẻ BBcode H1, BBcode H2, BBcode H3 …v…v… vì vậy bài viết này mình sẽ hướng dẫn các bạn cách thêm những BBcode trên.

Cách thêm thẻ H1, H2, H3 cho Xenforo

bbcode-h1-h2-xenforo.jpg

Đầu tiên chúng ta mở file Base.php trong thư mục library\XenForo\BbCode\Formatter
Tìm đoạn:
Mã:
'img' => array(

Khoảng dòng thứ 342, cả đoạn của nó sẽ là:
Mã:
'img' => array(
'hasOption' => false,
'plainChildren' => true,
'callback' => array($this, 'renderTagImage')
),

Thêm vào bên dưới đoạn trên code sau:
Mã:
'h1' => array(
'hasOption' => false,
'plainChildren' => true,
'callback' => array($this, 'renderTagH2')
),
'h2' => array(
'hasOption' => false,
'plainChildren' => true,
'callback' => array($this, 'renderTagH2')
),
'h3' => array(
'hasOption' => false,
'plainChildren' => true,
'callback' => array($this, 'renderTagH3')
),

Tiếp theo kéo xuống dưới cùng của trang, ngay bên trên dấu
Mã:
}

Dán đoạn code sau vào:
Mã:
public function renderTagH1(array $tag, array $rendererStates)
{
$text = $this->renderSubTree($tag['children'], $rendererStates);
return '<h1>' . $text . '</h1>';
}
public function renderTagH2(array $tag, array $rendererStates)
{
$text = $this->renderSubTree($tag['children'], $rendererStates);
return '<h2>' . $text . '</h2>';
}
public function renderTagH3(array $tag, array $rendererStates)
{
$text = $this->renderSubTree($tag['children'], $rendererStates);
return '<h3>' . $text . '</h3>';
}

Xong. Như vậy là bạn đã thêm được thẻ h1, h2, h3 vào trang Xenforo của mình. Bây giờ vào viết bài để thưởng thức thành quả nhé.

Bạn có thể viết như sau:
Mã:
[h1]Nội dung thẻ h1[/h1]
[h2]Nội dung thẻ h1[/h2]

Chúc các bạn thành công.


Nguồn: root.vn​
 
bác xem giúp code mình đặt đúng mà ko ra h2 nhỉ? demo bác xem 1 bài viết giúp mình nha
demo: http://duancanho.org/
Mã:
<?php

/**
* Base class for defining the formatting used by the BB code parser.
* This class implements HTML formatting.
*
* @package XenForo_BbCode
*/
class XenForo_BbCode_Formatter_Base
{
    /**
     * Lookup array that translates a smilie replacement text to an untypeable
     * sentinel value (\0-id-\0).
     *
     * @var array Format: [smilie replacement text] => sentinel value
     */
    protected $_smilieTranslate = array();

    /**
     * Essentially the reverse of the above lookup, this one translates a smilie ID
     * to the actual "rich" replacement (for HTML, an image tag).
     *
     * @var array Format: [smilie id] => final replacement
     */
    protected $_smilieReverse = array();

    /**
     * Array to store smilie paths for [IMG] lookup
     *
     * @var array Format [path] => smilie ID
     */
    protected $_smiliePaths = array();

    /**
     * List of media sites that are known.
     *
     * @var array Format: [media site id] => info
     */
    protected $_mediaSites = array();

    /**
     * List of tags this formatter knows about.
     *
     * @var array|****
     */
    protected $_tags = ****;

    /**
     * View for rendering tags that require templates.
     *
     * @var XenForo_View|****
     */
    protected $_view = ****;

    /**
     * String used for outputting [IMG] tags. Will be passed the following params:
     * 1    URL
     * 2    Additional CSS classes
     *
     * @var string
     */
    protected $_imageTemplate = '<img src="%1$s" class="bbCodeImage%2$s" alt="[&#x200B;IMG]" data-url="%3$s" />';

    /**
     * String used for outputting smilies. Will be passed the following params:
     * 1    Image URL
     * 2    Smilie text
     * 3    Smilie title
     *
     * @var string
     */
    protected $_smilieTemplate = '<img src="%1$s" class="mceSmilie" alt="%2$s" title="%3$s    %2$s" />';

    /**
     * String used for outputting smilies as sprites. Will be passed the following params:
     * 1    Smilie ID
     * 2    Smilie text
     * 3    Smilie title
     *
     * @var string
     */
    protected $_smilieSpriteTemplate = '<img src="styles/default/xenforo/clear.png" class="mceSmilieSprite mceSmilie%1$d" alt="%2$s" title="%3$s    %2$s" />';

    /**
     * Cache to store processed smilie URLs, to avoid having to process them for every single smilie.
     *
     * @var array
     */
    protected $_smilieUrlCache = array();

    /**
     * List of ignored users, for quoted content mostly.
     *
     * @var array Key: user ID, value: user name
     */
    protected $_ignoredUsers = array();

    /**
     * Direction of the text on the page by default.
     *
     * @var string    LTR or RTL
     */
    protected $_textDirection = 'LTR';

    /**
     * Maximum depth of tags that will be parsed. 0 to disable.
     *
     * @var int
     */
    protected $_tagDepthLimit = 20;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->_tags = $this->getTags();
        $this->preLoadData();

        if (XenForo_Visitor::hasInstance())
        {
            $visitor = XenForo_Visitor::getInstance();
            if (!empty($visitor['ignoredUsers']))
            {
                $this->_ignoredUsers = $visitor['ignoredUsers'];
            }

            $language = $visitor->getLanguage();
            $this->_textDirection = $language['text_direction'];
        }
    }

    /**
     * Pre-loads any required data, such as templates or phrases that may be be used.
     */
    public function preLoadData()
    {
    }

    /**
     * Add the specified list of smilies to the list that will be processed.
     *
     * @param array $smilies List of smilies with data from the DB (smilie_id, smilieText [array], image_url)
     */
    public function addSmilies(array $smilies)
    {
        foreach ($smilies AS $smilie)
        {
            foreach ($smilie['smilieText'] AS $text)
            {
                $this->_smilieTranslate[$text] = "\0" . $smilie['smilie_id'] . "\0";
            }

            if (empty($smilie['sprite_params']))
            {
                $this->_smilieReverse[$smilie['smilie_id']] = $this->_processSmilieTemplate($smilie);
            }
            else
            {
                $this->_smilieReverse[$smilie['smilie_id']] = $this->_processSmilieSpriteTemplate($smilie);
            }

            $this->_smiliePaths[$smilie['image_url']] = $smilie['smilie_id'];
        }
    }

    /**
     * Populates the image smilie template with data
     *
     * @param array $smilie
     *
     * @return string
     */
    protected function _processSmilieTemplate(array $smilie)
    {
        return sprintf($this->_smilieTemplate,
            $this->_prepareSmilieUrl($smilie['image_url']),
            htmlspecialchars(reset($smilie['smilieText'])),
            htmlspecialchars($smilie['title'])
        );
    }

    /**
     * Populates the sprite smilie template with data
     *
     * @param array $smilie
     *
     * @return string
     */
    protected function _processSmilieSpriteTemplate(array $smilie)
    {
        return sprintf($this->_smilieSpriteTemplate,
            $smilie['smilie_id'],
            htmlspecialchars(reset($smilie['smilieText'])),
            htmlspecialchars($smilie['title']),
            $this->_prepareSmilieUrl($smilie['image_url']),
            $smilie['sprite_params']['w'],
            $smilie['sprite_params']['h'],
            $smilie['sprite_params']['x'],
            $smilie['sprite_params']['y']
        );
    }

    /**
     * Prepares a smilie URL for use in an <img /> tag. Fetches the result from cache if possible.
     *
     * @param string $smilieUrl
     *
     * @return string
     */
    protected function _prepareSmilieUrl($smilieUrl)
    {
        if (!isset($this->_smilieUrlCache[$smilieUrl]))
        {
            $this->_smilieUrlCache[$smilieUrl] = $this->_prepareSmilieUrlInternal($smilieUrl);
        }

        return $this->_smilieUrlCache[$smilieUrl];
    }

    /**
     * Prepares a smilie URL for use in an <img /> tag.
     *
     * @param string $smilieUrl
     *
     * @return string
     */
    protected function _prepareSmilieUrlInternal($smilieUrl)
    {
        return htmlspecialchars($smilieUrl);
    }

    /**
     * Adds to the list of acceptable media sites.
     *
     * @param array $sites
     */
    public function addMediaSites(array $sites)
    {
        $this->_mediaSites = array_merge($this->_mediaSites, $sites);
    }

    /**
     * Sets the view that is used to render tags requiring templates.
     *
     * @param XenForo_View $view
     */
    public function setView(XenForo_View $view = ****)
    {
        $this->_view = $view;
        if ($view)
        {
            $this->preLoadTemplates($view);
        }
    }

    /**
     * @return ****|XenForo_View
     */
    public function getView()
    {
        return $this->_view;
    }

    /**
     * Tells the view to pre-load the templates that are required.
     *
     * @param XenForo_View $view
     */
    public function preLoadTemplates(XenForo_View $view)
    {
        $view->preLoadTemplate('bb_code_tag_code');
        $view->preLoadTemplate('bb_code_tag_php');
        $view->preLoadTemplate('bb_code_tag_html');
        $view->preLoadTemplate('bb_code_tag_quote');
        $view->preLoadTemplate('bb_code_tag_attach');
        $view->preLoadTemplate('bb_code_tag_spoiler');
    }

    /**
     * Get the list of parsable tags and their parsing rules.
     *
     * @return array
     */
    public function getTags()
    {
        if ($this->_tags !== ****)
        {
            return $this->_tags;
        }

        return array(
            'b' => array(
                'hasOption' => false,
                'replace' => array('<b>', '</b>')
            ),
            'i' => array(
                'hasOption' => false,
                'replace' => array('<i>', '</i>')
            ),
            'u' => array(
                'hasOption' => false,
                'replace' => array('<span style="text-decoration: underline">', '</span>')
            ),
            's' => array(
                'hasOption' => false,
                'replace' => array('<span style="text-decoration: line-through">', '</span>')
            ),

            'color' => array(
                'hasOption' => true,
                'optionRegex' => '/^(rgb\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*\)|#[a-f0-9]{6}|#[a-f0-9]{3}|[a-z]+)$/i',
                'replace' => array('<span style="color: %s">', '</span>')
            ),
            'font' => array(
                'hasOption' => true,
                'optionRegex' => '/^[a-z0-9 \-]+$/i', // regex matched to HTML->BB code regex
                'replace' => array('<span style="font-family: \'%s\'">', '</span>')
            ),
            'size' => array(
                'hasOption' => true,
                'optionRegex' => '/^[0-9]+(px)?$/i',
                'callback' => array($this, 'renderTagSize'),
            ),

            'left' => array(
                'hasOption' => false,
                'callback' => array($this, 'renderTagAlign'),
                'trimLeadingLinesAfter' => 1,
            ),
            'center' => array(
                'hasOption' => false,
                'callback' => array($this, 'renderTagAlign'),
                'trimLeadingLinesAfter' => 1,
            ),
            'right' => array(
                'hasOption' => false,
                'callback' => array($this, 'renderTagAlign'),
                'trimLeadingLinesAfter' => 1,
            ),
            'indent' => array(
                'trimLeadingLinesAfter' => 1,
                'optionRegex' => '/^[0-9]+$/',
                'callback' => array($this, 'renderTagIndent')
            ),

            'url' => array(
                'parseCallback' => array($this, 'parseValidatePlainIfNoOption'),
                'callback' => array($this, 'renderTagUrl'),
            ),
            'email' => array(
                'parseCallback' => array($this, 'parseValidatePlainIfNoOption'),
                'callback' => array($this, 'renderTagEmail')
            ),

            'img' => array(
                'hasOption' => false,
                'plainChildren' => true,
                'callback' => array($this, 'renderTagImage')
            ),
           
            'h1' => array(
                'hasOption' => false,
                'plainChildren' => true,
                'callback' => array($this, 'renderTagH2')
                ),
                'h2' => array(
                'hasOption' => false,
                'plainChildren' => true,
                'callback' => array($this, 'renderTagH2')
                ),
                'h3' => array(
                'hasOption' => false,
                'plainChildren' => true,
                'callback' => array($this, 'renderTagH3')
                ),

            'quote' => array(
                'trimLeadingLinesAfter' => 2,
                'callback' => array($this, 'renderTagQuote')
            ),

            'code' => array(
                'parseCallback' => array($this, 'parseValidateTagCode'),
                'stopSmilies' => true,
                'stopLineBreakConversion' => true,
                'trimLeadingLinesAfter' => 2,
                'callback' => array($this, 'renderTagCode')
            ),
            'php' => array(
                'hasOption' => false,
                'plainChildren' => true,
                'stopSmilies' => true,
                'stopLineBreakConversion' => true,
                'trimLeadingLinesAfter' => 2,
                'callback' => array($this, 'renderTagPhp')
            ),
            'html' => array(
                'hasOption' => false,
                'plainChildren' => true,
                'stopSmilies' => true,
                'stopLineBreakConversion' => true,
                'trimLeadingLinesAfter' => 2,
                'callback' => array($this, 'renderTagHtml')
            ),

            'list' => array(
                'trimLeadingLinesAfter' => 1,
                'callback' => array($this, 'renderTagList')
            ),

            'plain' => array(
                'hasOption' => false,
                'plainChildren' => true,
                'stopSmilies' => true,
                'replace' => array('', '')
            ),

            'media' => array(
                'hasOption' => true,
                'plainChildren' => true,
                'callback' => array($this, 'renderTagMedia')
            ),

            'spoiler' => array(
                'trimLeadingLinesAfter' => 1,
                'callback' => array($this, 'renderTagSpoiler')
            ),

            'attach' => array(
                'plainChildren' => true,
                'callback' => array($this, 'renderTagAttach')
            ),

            'user' => array(
                'hasOption' => true,
                'stopSmilies' => true,
                'callback' => array($this, 'renderTagUser')
            )
        );
    }

    public function addCustomTags(array $tags)
    {
        foreach ($tags AS $tagName => $tag)
        {
            $tagInfo = $this->_setupCustomTagInfo($tagName, $tag);
            if ($tagInfo)
            {
                $this->_tags[$tagName] = $tagInfo;
            }
        }
    }

    protected function _setupCustomTagInfo($tagName, array $tag)
    {
        $output = array();

        if ($tag['bb_code_mode'] == 'replace')
        {
            $output['replace'] = $tag['replace_html'];
        }
        else if ($tag['bb_code_mode'] == 'callback')
        {
            $output['callback'] = array($tag['callback_class'], $tag['callback_method']);
        }

        if ($tag['has_option'] == 'yes')
        {
            $output['hasOption'] = true;
        }
        else if ($tag['has_option'] == 'no')
        {
            $output['hasOption'] = false;
        }

        if (strlen($tag['option_regex']))
        {
            $output['optionRegex'] = $tag['option_regex'];
        }
        if ($tag['trim_lines_after'])
        {
            $output['trimLeadingLinesAfter'] = $tag['trim_lines_after'];
        }
        if ($tag['plain_children'])
        {
            $output['plainChildren'] = true;
        }
        if ($tag['disable_smilies'])
        {
            $output['stopSmilies'] = true;
        }
        if ($tag['disable_nl2br'])
        {
            $output['stopLineBreakConversion'] = true;
        }
        if ($tag['allow_empty'])
        {
            $output['keepEmpty'] = true;
        }

        $output['allowSignature'] = $tag['allow_signature'];

        return $output;
    }

    /**
     * Allows the text to be filtered before parsing.
     *
     * @param string $text
     *
     * @return string
     */
    public function preFilterText($text)
    {
        return $text;
    }

    /**
     * Resets rendering state and renders a parsed BB code tree
     * to the required output format. Note that this initializes the default states,
     * so it is likely not the correct function to call for child tags.
     *
     * @param array $tree Tree from {@link parse()}.
     * @param array $extraStates A list of extra states to push into the formatter
     *
     * @return string Output text
     */
    public function renderTree(array $tree, array $extraStates = array())
    {
        $rendererStates = $extraStates + array(
            'stopSmilies' => 0,
            'stopLineBreakConversion' => 0,
            'tagDataStack' => array(),
            'noFollowDefault' => true, // add nofollow attributes
            'shortenUrl' => true, // add ... in middle of long URLs
            'lightBox' => true, // add 'LbImage' class to [IMG] output
            'imgToSmilie' => false, // attempt to convert [IMG] to smilie if URL matches
            'disableProxying' => false, // disable image and link proxying
        );

        $output = $this->renderSubTree($tree, $rendererStates);
        return $this->filterFinalOutput($output);
    }

    /**
     * Renders a parsed BB code tree to the required output format. This does
     * not reset the rendering states, meaning it is ok for recursive calls.
     *
     * @param array $tree Tree from {@link parse()}
     * @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
     *
     * @return string Output text
     */
    public function renderSubTree(array $tree, array $rendererStates)
    {
        $output = '';
        $trimLeadingLines = 0;

        foreach ($tree AS $element)
        {
            $output .= $this->renderTreeElement($element, $rendererStates, $trimLeadingLines);
        }

        return $output;
    }

    /**
     * Renders a tree element, that be a tag (valid or not) or a string.
     *
     * @param array|string $element Tree element
     * @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
     * @param integer $trimLeadingLines By reference. Number of leading lines to strip off next element.
     *
     * @return string Rendered element.
     */
    public function renderTreeElement($element, array $rendererStates, &$trimLeadingLines)
    {
        if (is_array($element))
        {
            return $this->renderTag($element, $rendererStates, $trimLeadingLines);
        }
        else
        {
            return $this->renderString($element, $rendererStates, $trimLeadingLines);
        }
    }

    /**
     * Renders a string tree element.
     *
     * @param string $string
     * @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
     * @param integer $trimLeadingLines By reference. Number of leading lines to strip off next element.
     *
     * @return string Rendered string
     */
    public function renderString($string, array $rendererStates, &$trimLeadingLines)
    {
        if ($trimLeadingLines)
        {
            $string = $this->trimLeadingLines($string, $trimLeadingLines);
            $trimLeadingLines = 0;
        }

        return $this->filterString($string, $rendererStates);
    }

    /**
     * Trims the given number of leading blank lines off of the given string.
     *
     * @param string $string
     * @param integer $amount
     *
     * @return string
     */
    public function trimLeadingLines($string, $amount)
    {
        $amount = intval($amount);
        if ($amount <= 0)
        {
            return $string;
        }

        return preg_replace('#^([ \t]*\r?\n){1,' . $amount . '}#i', '', $string);
    }

    /**
     * Renders a tag. This tag may be valid or invalid.
     *
     * @param array $element Tag element.
     * @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
     * @param integer $trimLeadingLines By reference. Number of leading lines to strip from next element. May be modified by tag.
     *
     * @return string Rendered tag.
     */
    public function renderTag(array $element, array $rendererStates, &$trimLeadingLines)
    {
        $trimLeadingLines = 0;

        if (isset($rendererStates['tagDataStack']))
        {
            $rendererStates['tagDataStack'][] = $element;
        }

        $tagDepthReached = (
            $this->_tagDepthLimit
            && !empty($rendererStates['tagDataStack'])
            && count($rendererStates['tagDataStack']) > $this->_tagDepthLimit
        );

        $tagInfo = $this->_getTagRule($element['tag']);
        if (!$tagInfo || $tagDepthReached)
        {
            $output = $this->renderInvalidTag($element, $rendererStates);
        }
        else
        {
            if (!empty($tagInfo['stopSmilies']))
            {
                $rendererStates['stopSmilies']++;
            }
            if (!empty($tagInfo['stopLineBreakConversion']))
            {
                $rendererStates['stopLineBreakConversion']++;
            }

            $output = $this->renderValidTag($tagInfo, $element, $rendererStates);

            if (!empty($tagInfo['trimLeadingLinesAfter']))
            {
                $trimLeadingLines = $tagInfo['trimLeadingLinesAfter'];
            }
        }

        return $output;
    }

    /**
     * Gets information about the specified tag.
     *
     * @param string $tagName
     *
     * @return array|false
     */
    protected function _getTagRule($tagName)
    {
        $tagName = strtolower($tagName);

        if (!empty($this->_tags[$tagName]) && is_array($this->_tags[$tagName]))
        {
            return $this->_tags[$tagName];
        }
        else
        {
            return false;
        }
    }

    /**
     * Renders an invalid tag. This tag is simply displayed in its original form.
     *
     * @param array $tag Tag data from tree
     * @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
     *
     * @return string Rendered version
     */
    public function renderInvalidTag(array $tag, array $rendererStates)
    {
        return $this->renderTagUnparsed($tag, $rendererStates);
    }

    /**
     * Renders a tag as if it's unparsed (in its original form).
     *
     * @param array $tag Tag data from tree
     * @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
     *
     * @return string Rendered version
     */
    public function renderTagUnparsed(array $tag, array $rendererStates)
    {
        if (!empty($tag['original']) && is_array($tag['original']))
        {
            list($prepend, $append) = $tag['original'];
        }
        else
        {
            $prepend = '';
            $append = '';
        }

        $output = $this->filterString($prepend, $rendererStates)
            . $this->renderSubTree($tag['children'], $rendererStates)
            . $this->filterString($append, $rendererStates);

        return $output;
    }

    /**
     * Renders a tag.
     *
     * @param array $tagInfo Information about how to parse the tag
     * @param array $tag Tag data from tree
     * @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
     *
     * @return string Rendered version
     */
    public function renderValidTag(array $tagInfo, array $tag, array $rendererStates)
    {
        if (!empty($tagInfo['callback']))
        {
            if (is_array($tagInfo['callback']) && $tagInfo['callback'][0] == '$this')
            {
                $tagInfo['callback'][0] = $this;
            }
            if (!is_callable($tagInfo['callback']))
            {
                return $this->renderInvalidTag($tag, $rendererStates);
            }
            return call_user_func($tagInfo['callback'], $tag, $rendererStates, $this);
        }
        else if (!empty($tagInfo['replace']))
        {
            $text = $this->renderSubTree($tag['children'], $rendererStates);
            $option = $this->filterString($tag['option'], array_merge($rendererStates, array(
                'stopSmilies' => true,
                'stopLineBreakConversion' => true
            )));

            if (empty($tagInfo['keepEmpty']) && trim($text) === '')
            {
                return $text;
            }

            if (is_array($tagInfo['replace']))
            {
                list($prepend, $append) = $tagInfo['replace'];
                return $this->_wrapInHtml($prepend, $append, $text, $option);
            }
            else
            {
                return strtr($tagInfo['replace'], array(
                    '{text}' => $text,
                    '{option}' => $option
                ));
            }
        }
        else
        {
            return $this->renderInvalidTag($tag, $rendererStates);
        }
    }

    protected function _wrapInHtml($prepend, $append, $text, $option = ****)
    {
        if ($option === ****)
        {
            return $prepend . $text . $append;
        }
        else
        {
            return sprintf($prepend, $option) . $text . sprintf($append, $option);
        }
    }

    /**
     * Similar to rendering the tree, but this function renders all tags to plain text
     * (as if they weren't special tags). This can be useful for functions that can only
     * take plain text children.
     *
     * Note that this output is not escaped in anyway!
     *
     * @param array $tree Tree or sub-tree to stringify
     *
     * @return string Tree as a string (like the original input)
     */
    public function stringifyTree(array $tree)
    {
        $output = '';

        foreach ($tree AS $element)
        {
            if (is_array($element))
            {
                if (!empty($element['original']) && is_array($element['original']))
                {
                    list($prepend, $append) = $element['original'];
                }
                else
                {
                    $prepend = '';
                    $append = '';
                }

                $output .= $prepend . $this->stringifyTree($element['children']) . $append;
            }
            else
            {
                $output .= strval($element);
            }
        }

        return $output;
    }

    /**
     * Filter a string for the current output format. A string is simply the text
     * between tags. This function is responsible for things like word wrap and smilies
     * and output escaping.
     *
     * @param string $string
     * @param array $rendererStates List of states the renderer may be in
     *
     * @return string Filtered/escaped string
     */
    public function filterString($string, array $rendererStates)
    {
        $string = XenForo_Helper_String::censorString($string);

        if (empty($rendererStates['stopSmilies']))
        {
            $string = $this->replaceSmiliesInText($string, 'htmlspecialchars');
        }
        else
        {
            $string = htmlspecialchars($string);
        }

        if (empty($rendererStates['stopLineBreakConversion']))
        {
            $string = nl2br($string);
        }

        return $string;
    }

    /**
     * Filters the final string output.
     *
     * @param string $output
     *
     * @return string
     */
    public function filterFinalOutput($output)
    {
        return trim($output);
    }

    /**
     * Gets a valid, full URL if possible. False is returned if not possible.
     *
     * @param string $url URL to validate
     *
     * @return string|false
     */
    protected function _getValidUrl($url)
    {
        $url = trim($url);

        if (!$url)
        {
            return false;
        }

        switch ($url[0])
        {
            case '#':
            case '/':
            case ' ':
            case "\r":
            case "\n":
                return false;
        }

        if (preg_match('/\r?\n/', $url))
        {
            return false;
        }

        if (preg_match('#^https?://#i', $url))
        {
            return $url;
        }

        return 'http://' . $url;
    }

    /**
     * Renders an indent tag.
     *
     * @param array $tag Information about the tag reference; keys: tag, option, children
     * @param array $rendererStates Renderer states to push down
     *
     * @return string Rendered tag
     */
    public function renderTagIndent(array $tag, array $rendererStates)
    {
        $text = $this->renderSubTree($tag['children'], $rendererStates);
        if (trim($text) === '')
        {
            $text = '<br />';
        }

        if (isset($tag['option']))
        {
            $amount = intval($tag['option']);
            if ($amount > 10)
            {
                $amount = 10;
            }
        }
        else
        {
            $amount = 1;
        }

        $invisibleSpace = $this->_endsInBlockTag($text) ? '' : '&#8203;';

        if ($amount < 1)
        {
            return $this->_wrapInHtml('<div>', $invisibleSpace . '</div>', $text);
        }
        else
        {
            $paddingSide = ($this->_textDirection == 'RTL' ? 'padding-right' : 'padding-left');
            return $this->_wrapInHtml('<div style="' . $paddingSide . ': ' . (30 * $amount) . 'px">', $invisibleSpace . '</div>', $text);
        }
    }

    /**
     * Renders an alignment (left, center, right) tag.
     *
     * @param array $tag Information about the tag reference; keys: tag, option, children
     * @param array $rendererStates Renderer states to push down
     *
     * @return string Rendered tag
     */
    public function renderTagAlign(array $tag, array $rendererStates)
    {
        $text = $this->renderSubTree($tag['children'], $rendererStates);

        $invisibleSpace = $this->_endsInBlockTag($text) ? '' : '&#8203;';

        switch (strtolower($tag['tag']))
        {
            case 'left':
            case 'center':
            case 'right':
                return $this->_wrapInHtml('<div style="text-align: ' . $tag['tag'] . '">', $invisibleSpace. '</div>', $text);

            default:
                return $this->_wrapInHtml('<div>', $invisibleSpace. '</div>', $text);
        }
    }

    protected function _endsInBlockTag($text)
    {
        return preg_match('#</(p|div)>$#i', substr(rtrim($text), -6));
    }

    /**
     * Renders a size tag.
     *
     * @param array $tag Information about the tag reference; keys: tag, option, children
     * @param array $rendererStates Renderer states to push down
     *
     * @return string Rendered tag
     */
    public function renderTagSize(array $tag, array $rendererStates)
    {
        $text = $this->renderSubTree($tag['children'], $rendererStates);
        if (trim($text) === '')
        {
            return $text;
        }

        $size = $this->getTextSize($tag['option']);
        if ($size)
        {
            return $this->_wrapInHtml('<span style="font-size: ' . htmlspecialchars($size) . '">', '</span>', $text);
        }
        else
        {
            return $text;
        }
    }

    /**
     * Gets the effective text size to use.
     *
     * @param string $inputSize
     *
     * @return string|false
     */
    public function getTextSize($inputSize)
    {
        if (strval(intval($inputSize)) == strval($inputSize))
        {
            // int only, translate size
            if ($inputSize <= 0)
            {
                $size = false;
            }
            else
            {
                switch ($inputSize)
                {
                    case 1: $size = '9px'; break;
                    case 2: $size = '10px'; break;
                    case 3: $size = '12px'; break;
                    case 4: $size = '15px'; break;
                    case 5: $size = '18px'; break;
                    case 6: $size = '22px'; break;

                    case 7:
                    default:
                        $size = '26px';
                }
            }
        }
        else
        {
            // int and unit
            if (preg_match('/^([0-9]+)px$/i', $inputSize, $match))
            {
                if ($match[1] < 8)
                {
                    $size = '8px';
                }
                else if ($match[1] > 36)
                {
                    $size = '36px';
                }
                else
                {
                    $size = $inputSize;
                }
            }
            else
            {
                $size = false;
            }
        }

        return $size;
    }

    /**
     * Disables parsing of child tags if this tag does not have an option.
     * Useful for tags like url/email, where the address may be in the body
     * of the tag.
     *
     * @param array $tagInfo Info about the tag we're parsing.
     * @param string|**** $tagOption Any option passed into the tag
     *
     * @return array|boolean True if tag is ok as is, array to change states, false to reject tag
     */
    public function parseValidatePlainIfNoOption(array $tagInfo, $tagOption)
    {
        if (empty($tagOption))
        {
            return array('plainChildren' => true);
        }
        else
        {
            return true;
        }
    }

    /**
     * Renders a URL tag.
     *
     * @param array $tag Information about the tag reference; keys: tag, option, children
     * @param array $rendererStates Renderer states to push down
     *
     * @return string Rendered tag
     */
    public function renderTagUrl(array $tag, array $rendererStates)
    {
        if (!empty($tag['option']))
        {
            $url = $tag['option'];
            $text = $this->renderSubTree($tag['children'], $rendererStates);
        }
        else
        {
            $url = $this->stringifyTree($tag['children']);
            $text = rawurldecode($url);
            if (!preg_match('/./u', $text))
            {
                $text = $url;
            }
            $text = XenForo_Helper_String::censorString($text);

            if (!empty($rendererStates['shortenUrl']))
            {
                $length = utf8_strlen($text);
                if ($length > 100)
                {
                    $text = utf8_substr_replace($text, '...', 35, $length - 35 - 45);
                }
            }

            $text = htmlspecialchars($text);
        }

        $url = $this->_getValidUrl($url);
        if (!$url)
        {
            return $text;
        }
        else
        {
            list($class, $target, $type) = XenForo_Helper_String::getLinkClassTarget($url);
            if ($type == 'internal')
            {
                $noFollow = '';
            }
            else
            {
                $noFollow = (empty($rendererStates['noFollowDefault']) ? '' : ' rel="nofollow"');
            }

            $href = XenForo_Helper_String::censorString($url);
            if ($rendererStates['disableProxying'])
            {
                $proxyHref = false;
            }
            else
            {
                $proxyHref = $this->_handleLinkProxyOption($href, $type);
            }

            $proxyAttr = '';
            if ($proxyHref)
            {
                $proxyAttr = ' data-proxy-href="' . htmlspecialchars($proxyHref) . '"';
                $class .= ' ProxyLink';
            }

            $class = $class ? " class=\"$class\"" : '';
            $target = $target ? " target=\"$target\"" : '';

            return $this->_wrapInHtml(
                '<a href="' . htmlspecialchars($href) . '"' . $target . $class . $proxyAttr . $noFollow . '>',
                '</a>',
                $text
            );
        }
    }

    /**
     * Renders an email tag.
     *
     * @param array $tag Information about the tag reference; keys: tag, option, children
     * @param array $rendererStates Renderer states to push down
     *
     * @return string Rendered tag
     */
    public function renderTagEmail(array $tag, array $rendererStates)
    {
        if (!empty($tag['option']))
        {
            $email = $tag['option'];
            $text = $this->renderSubTree($tag['children'], $rendererStates);
        }
        else
        {
            $email = $this->stringifyTree($tag['children'], $rendererStates);
            $text = $this->filterString($email, $rendererStates);
        }

        if (strpos($email, '@') === false)
        {
            // invalid URL, ignore
            return $text;
        }

        $email = XenForo_Helper_String::censorString($email);

        return $this->_wrapInHtml('<a href="mailto:' . htmlspecialchars($email) . '">', '</a>', $text);
    }

    /**
     * Renders a img tag.
     *
     * @param array $tag Information about the tag reference; keys: tag, option, children
     * @param array $rendererStates Renderer states to push down
     *
     * @return string Rendered tag
     */
    public function renderTagImage(array $tag, array $rendererStates)
    {
        $url = $this->stringifyTree($tag['children']);

        $validUrl = $this->_getValidUrl($url);
        if (!$validUrl)
        {
            return $this->filterString($url, $rendererStates);
        }

        $censored = XenForo_Helper_String::censorString($validUrl);
        if ($censored != $validUrl)
        {
            return $this->filterString($url, $rendererStates);
        }

        // attempts to convert smilies posted as [IMG] tags back into smilies
        if ($rendererStates['imgToSmilie'])
        {
            foreach ($this->_smiliePaths AS $smiliePath => $smilieId)
            {
                if (strpos($url, $smiliePath) !== false && substr($url, strlen($smiliePath) * -1) == $smiliePath)
                {
                    return $this->_smilieReverse[$smilieId];
                }
            }
        }

        if ($rendererStates['disableProxying'])
        {
            $imageUrl = $validUrl;
        }
        else
        {
            $imageUrl = $this->_handleImageProxyOption($validUrl, $rendererStates);
        }

        return sprintf($this->_imageTemplate,
            htmlspecialchars($imageUrl),
            $rendererStates['lightBox'] ? ' LbImage' : '',
            htmlspecialchars($validUrl)
        );
    }

    /**
     * Pass an image URL to the image proxy system if appropriate
     *
     * @param $url
     *
     * @return string
     */
    protected function _handleImageProxyOption($url)
    {
        list($class, $target, $type, $schemeMatch) = XenForo_Helper_String::getLinkClassTarget($url);

        if (($type == 'external' || !$schemeMatch))
        {
            $options = XenForo_Application::getOptions();
            if (!empty($options->imageLinkProxy['images']))
            {
                $url = $this->_generateProxyLink('image', $url);
            }
        }

        return $url;
    }

    /**
     * Pass a link URL to the proxy / redirect system if appropriate
     *
     * @param string $url
     * @param string $linkType
     *
     * @return string|false
     */
    protected function _handleLinkProxyOption($url, $linkType)
    {
        if ($linkType == 'external')
        {
            $options = XenForo_Application::getOptions();

            if (!empty($options->imageLinkProxy['links']))
            {
                return $this->_generateProxyLink('link', $url);
            }
        }

        return false;
    }

    protected function _generateProxyLink($proxyType, $url)
    {
        $hash = hash_hmac('md5', $url,
            XenForo_Application::getConfig()->globalSalt . XenForo_Application::getOptions()->imageLinkProxyKey
        );
        return 'proxy.php?' . $proxyType . '=' . urlencode($url) . '&hash=' . $hash;
    }

    /**
     * Modifies the parsing options for a code tag. Users must explicitly
     * opt in to allow BB codes to be used within.
     *
     * @param array $tagInfo Info about the tag we're parsing.
     * @param string|**** $tagOption Any option passed into the tag
     *
     * @return array|boolean True if tag is ok as is, array to change states, false to reject tag
     */
    public function parseValidateTagCode(array $tagInfo, $tagOption)
    {
        if (strtolower($tagOption) == 'rich')
        {
            return true;
        }
        else
        {
            return array('plainChildren' => true);
        }
    }

    /**
     * Renders a code tag.
     *
     * @param array $tag Information about the tag reference; keys: tag, option, children
     * @param array $rendererStates Renderer states to push down
     *
     * @return string Rendered tag
     */
    public function renderTagCode(array $tag, array $rendererStates)
    {
        switch (strtolower(strval($tag['option'])))
        {
            case 'php':
                return $this->renderTagPhp($tag, $rendererStates);

            case 'html':
                return $this->renderTagHtml($tag, $rendererStates);
        }

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

        if ($this->_view)
        {
            $template = $this->_view->createTemplateObject('bb_code_tag_code', array(
                'content' => $content
            ));
            return $template->render();
        }
        else
        {
            return $this->_wrapInHtml('<pre style="margin: 1em auto" title="Code">', '</pre>', $content);
        }
    }

    /**
     * Renders an HTML tag.
     *
     * @param array $tag Information about the tag reference; keys: tag, option, children
     * @param array $rendererStates Renderer states to push down
     *
     * @return string Rendered tag
     */
    public function renderTagHtml(array $tag, array $rendererStates)
    {
        $content = $this->stringifyTree($tag['children']);
        $content = $this->filterString($content, $rendererStates);

        if ($this->_view)
        {
            $template = $this->_view->createTemplateObject('bb_code_tag_html', array(
                'content' => $content
            ));
            return $template->render();
        }
        else
        {
            return $this->_wrapInHtml('<pre style="margin: 1em auto" title="HTML">', '</pre>', $content);
        }
    }

    /**
     * Renders a PHP tag.
     *
     * @param array $tag Information about the tag reference; keys: tag, option, children
     * @param array $rendererStates Renderer states to push down
     *
     * @return string Rendered tag
     */
    public function renderTagPhp(array $tag, array $rendererStates)
    {
        $content = $this->stringifyTree($tag['children']);
        $content = XenForo_Helper_String::censorString($content);
        $content = preg_replace('/^[ \t]*\r?\n/', '', $content);

        if (strpos($content, '<?') == false)
        {
            $tagAdded = true;
            $content = "<?php\n$content";
        }
        else
        {
            $tagAdded = false;
        }

        $content = highlight_string($content, true);

        if ($tagAdded)
        {
            $content = preg_replace(
                '#&lt;\?php<br\s*/?>#',
                '',
                $content,
                1
            );
        }

        if ($this->_view)
        {
            $template = $this->_view->createTemplateObject('bb_code_tag_php', array(
                'content' => $content
            ));
            return $template->render();
        }
        else
        {
            return $this->_wrapInHtml('<div style="margin: 1em auto" title="PHP">', '</div>', $content);
        }
    }

    /**
     * Renders a quote tag.
     *
     * @param array $tag Information about the tag reference; keys: tag, option, children
     * @param array $rendererStates Renderer states to push down
     *
     * @return string Rendered tag
     */
    public function renderTagQuote(array $tag, array $rendererStates)
    {
        $keys = array_keys($tag['children']);
        if (!$keys)
        {
            return '';
        }

        $first = reset($keys);
        $last = end($keys);

        if (is_string($tag['children'][$first]))
        {
            $tag['children'][$first] = ltrim($tag['children'][$first]);
        }
        if (is_string($tag['children'][$last]))
        {
            $tag['children'][$last] = rtrim($tag['children'][$last]);
        }

        $content = $this->renderSubTree($tag['children'], $rendererStates);
        if ($content === '')
        {
            return '';
        }

        $source = false;
        $attributes = array();

        /*
         * NOTE: changes to this code must also be reflected in
         * XenForo_Model_Post::alertQuotedMembers()
         */
        if ($tag['option'])
        {
            $parts = explode(',', $tag['option']);
            $name = $this->filterString(array_shift($parts),
                array_merge($rendererStates, array(
                    'stopSmilies' => true,
                    'stopLineBreakConversion' => true
                ))
            );

            foreach ($parts AS $part)
            {
                $partAttributes = explode(':', $part, 2);
                if (isset($partAttributes[1]))
                {
                    $attrName = trim($partAttributes[0]);
                    $attrValue = trim($partAttributes[1]);
                    if ($attrName !== '' && $attrValue !== '')
                    {
                        $attributes[$attrName] = $attrValue;
                    }
                }
            }

            list($firstName, $firstValue) = each($attributes);
            if ($firstName && $firstName != 'member')
            {
                $source = array('type' => $firstName, 'id' => intval($firstValue));
            }
        }
        else
        {
            $name = false;
        }

        if ($this->_view)
        {
            $template = $this->_view->createTemplateObject('bb_code_tag_quote', array(
                'content' => $content,
                'nameHtml' => $name,
                'source' => $source,
                'attributes' => $attributes,
                'ignored' => (isset($attributes['member']) && isset($this->_ignoredUsers[intval($attributes['member'])]))
            ));
            return $template->render();
        }
        else
        {
            return $this->_renderTagQuoteFallback($name, $content);
        }
    }

    /**
     * Returns HTML output for a quote tag when the view is not available
     *
     * @param string $name Name of quoted user
     * @param string $content Quoted text
     *
     * @return string
     */
    protected function _renderTagQuoteFallback($name, $content)
    {
        if ($name)
        {
            $name = '<div>' . $name . '</div>';
        }

        return $this->_wrapInHtml('<blockquote>', '</blockquote>', $name . $content);
    }

    /**
     * Renders a list tag.
     *
     * @param array $tag Information about the tag reference; keys: tag, option, children
     * @param array $rendererStates Renderer states to push down
     *
     * @return string Rendered tag
     */
    public function renderTagList(array $tag, array $rendererStates)
    {
        $listType = ($tag['option'] == '1' ? 'ol' : 'ul');

        $elements = array();
        $lastElement = '';
        $trimLeadingLines = 0;

        foreach ($tag['children'] AS $child)
        {
            if (is_array($child))
            {
                $childText = $this->renderTag($child, $rendererStates, $trimLeadingLines);
                if (preg_match('#^<(ul|ol)#', $childText))
                {
                    $lastElement = rtrim($lastElement);
                    if (substr($lastElement, -6) == '<br />')
                    {
                        $lastElement = substr($lastElement, 0, -6);
                    }
                }
                $lastElement .= $childText;
            }
            else
            {
                if (strpos($child, '[*]') !== false)
                {
                    $parts = explode('[*]', $child);

                    $beforeFirst = array_shift($parts);
                    if ($lastElement !== '' || trim($beforeFirst) !== '')
                    {
                        $lastElement .= $this->renderString($beforeFirst, $rendererStates, $trimLeadingLines);
                    }

                    foreach ($parts AS $part)
                    {
                        $this->_appendListElement($elements, $lastElement);
                        $lastElement = $this->renderString($part, $rendererStates, $trimLeadingLines);
                    }
                }
                else
                {
                    $lastElement .= $this->renderString($child, $rendererStates, $trimLeadingLines);
                }
            }
        }

        $this->_appendListElement($elements, $lastElement);

        if (!$elements)
        {
            return '';
        }

        return $this->_renderListOutput($listType, $elements);
    }

    /**
     * Given already parsed list elements, gets the output for the list.
     *
     * @param string $listType Type of list (ol or ul)
     * @param array $elements List of elements in the list. These are already rendered.
     *
     * @return string
     */
    protected function _renderListOutput($listType, array $elements)
    {
        return "<$listType>\n<li>" . implode("</li>\n<li>", $elements) . "</li>\n</$listType>";
    }

    /**
     * Appends a list element if it is not empty.
     *
     * @param array $elements By reference. List of existing elements.
     * @param string $appendString String to append (if not empty)
     */
    protected function _appendListElement(array &$elements, $appendString)
    {
        if ($appendString !== '')
        {
            $appendString = rtrim($appendString);
            if (substr($appendString, -6) == '<br />')
            {
                $appendString = substr($appendString, 0, -6);
            }

            $elements[] = $appendString;
        }
    }

    public function renderTagAttach(array $tag, array $rendererStates)
    {
        $id = intval($this->stringifyTree($tag['children']));
        if (!$id)
        {
            return '';
        }

        if (!$this->_view)
        {
            $phrase = new XenForo_Phrase('view_attachment_x', array('name' => $id));
            return '<a href="' . XenForo_Link::buildPublicLink('full:attachments', array('attachment_id' => $id)) . '">' . $phrase . '</a>';
        }

        if (empty($rendererStates['attachments'][$id]))
        {
            $attachment = array('attachment_id' => $id);
            $validAttachment = false;
            $canView = false;
        }
        else
        {
            $attachment = $rendererStates['attachments'][$id];
            $validAttachment = true;
            $canView = empty($rendererStates['viewAttachments']) ? false : true;
        }

        $template = $this->_view->createTemplateObject('bb_code_tag_attach', array(
            'attachment' => $attachment,
            'validAttachment' => $validAttachment,
            'canView' => $canView,
            'full' => (strtolower($tag['option']) == 'full')
        ));
        return $template->render();
    }

    public function renderTagUser(array $tag, array $rendererStates)
    {
        $content = $this->renderSubTree($tag['children'], $rendererStates);
        if ($content === '')
        {
            return '';
        }

        $userId = intval($tag['option']);
        if (!$userId)
        {
            return $content;
        }

        $link = XenForo_Link::buildPublicLink('full:members', array('user_id' => $userId));
        $username = $this->stringifyTree($tag['children']);

        return $this->_wrapInHtml('<a href="' . htmlspecialchars($link) . '" class="username" data-user="' . $userId . ', ' . htmlspecialchars($username) . '">', '</a>', $content);
    }

    /**
     * Renders a media tag. Media tags embed rich media (usually videos). To embed a video,
     * the source must be known.
     *
     * @param array $tag Information about the tag reference; keys: tag, option, children
     * @param array $rendererStates Renderer states to push down
     *
     * @return string Rendered tag
     */
    public function renderTagMedia(array $tag, array $rendererStates)
    {
        $mediaKey = trim($this->stringifyTree($tag['children']));
        if (preg_match('#[&?"\'<>\r\n]#', $mediaKey) || strpos($mediaKey, '..') !== false)
        {
            return '';
        }

        $censored = XenForo_Helper_String::censorString($mediaKey);
        if ($censored != $mediaKey)
        {
            return '';
        }

        $mediaSiteId = strtolower($tag['option']);
        if ($mediaSiteId == 'youtube')
        {
            // youtube iframe embed bug workaround
            $mediaKey = str_replace('/', '', $mediaKey);
        }
        if (isset($this->_mediaSites[$mediaSiteId]))
        {
            $embedHtml = $this->_getMediaSiteHtmlFromCallback($mediaKey, $this->_mediaSites[$mediaSiteId], $mediaSiteId);

            if (!$embedHtml)
            {
                $embedHtml = strtr($this->_mediaSites[$mediaSiteId]['embed_html'], array(
                    '{$id}' => rawurlencode($mediaKey),
                    '{$id:digits}' => intval($mediaKey)
                ));
            }

            return $embedHtml;
        }
        else
        {
            return '';
        }
    }

    /**
     * Renders a spoiler tag.
     *
     * @param array $tag Information about the tag reference; keys: tag, option, children
     * @param array $rendererStates Renderer states to push down
     *
     * @return string Rendered tag
     */
    public function renderTagSpoiler(array $tag, array $rendererStates)
    {
        $keys = array_keys($tag['children']);
        if (!$keys)
        {
            return '';
        }

        $first = reset($keys);
        $last = end($keys);

        if (is_string($tag['children'][$first]))
        {
            $tag['children'][$first] = ltrim($tag['children'][$first]);
        }
        if (is_string($tag['children'][$last]))
        {
            $tag['children'][$last] = rtrim($tag['children'][$last]);
        }

        $content = $this->renderSubTree($tag['children'], $rendererStates);
        if ($content === '')
        {
            return '';
        }

        if ($tag['option'])
        {
            $title = $this->filterString($tag['option'],
                array_merge($rendererStates, array(
                    'stopSmilies' => true,
                    'stopLineBreakConversion' => true
                ))
            );
        }
        else
        {
            $title = false;
        }

        if ($this->_view)
        {
            $template = $this->_view->createTemplateObject('bb_code_tag_spoiler', array(
                'content' => $content,
                'titleHtml' => $title
            ));
            return $template->render();
        }
        else
        {
            return $this->_renderTagSpoilerFallback($title, $content, $rendererStates);
        }
    }

    /**
     * Returns HTML output for a spoiler tag when the view is not available
     *
     * @param string $title Title of spoiler
     * @param string $content Spoiler text
     * @param array $rendererStates
     *
     * @return string
     */
    protected function _renderTagSpoilerFallback($title, $content, array $rendererStates)
    {
        if (!empty($rendererStates['spoilerTextWithFallback']))
        {
            return '<div>' . $content . '</div>';
        }
        else
        {
            $spoilerText = new XenForo_Phrase('spoiler');
            return '<div><b>' . ($title ? ($spoilerText . ': ' . $title) : $spoilerText) . '</b></div>';
        }
    }

    /**
     * Attempts to fetch media tag embed HTML using the callback method defined for a media site, if one is specified.
     *
     * @param string $mediaKey
     * @param array $site Information about the site to render this media
     * @param string $siteId
     *
     * @return string|boolean Returns false if callback is invalid
     */
    protected function _getMediaSiteHtmlFromCallback($mediaKey, array $site, $siteId)
    {
        if (!empty($site['callback']) && is_array($site['callback']))
        {
            $class = $site['callback'][0];
            $method = $site['callback'][1];

            if (XenForo_Application::autoload($class) && method_exists($class, $method))
            {
                return call_user_func_array($site['callback'], array($mediaKey, $site, $siteId));
            }
        }

        return false;
    }

    /**
     * Replaces smilie strings in text with the appropriate "rich" markup.
     * This method also escapes the output before the smilies are ultimately replaced.
     * This is necessary to prevent the rich output from being escaped.
     *
     * @param string $text Text to replace in
     * @param mixed $escapeCallback Callback for escaping. If empty, no escaping is done.
     *
     * @return string
     */
    public function replaceSmiliesInText($text, $escapeCallback = '')
    {
        if ($this->_smilieTranslate)
        {
            $text = strtr($text, $this->_smilieTranslate);
        }

        if ($escapeCallback)
        {
            if ($escapeCallback == 'htmlspecialchars')
            {
                $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
            }
            else
            {
                $text = call_user_func($escapeCallback, $text);
            }
        }

        if ($this->_smilieTranslate)
        {
            $split = preg_split("#\\0(\d+)\\0#", $text, -1, PREG_SPLIT_DELIM_CAPTURE);
            $text = '';
            foreach ($split AS $key => $value)
            {
                // odd keys contain the delimiter we want
                if ($key % 2 == 0)
                {
                    $text .= $value;
                }
                else if (isset($this->_smilieReverse[$value]))
                {
                    $text .= $this->_smilieReverse[$value];
                }
            }
        }

        return $text;
    }

    /**
     * Create the specified BB code formatter.
     *
     * @param string $class Name of the class. If empty, uses this class; if doesn't contain an underscore, assumes a partial name
     * @param array|boolean Set of options to configure formatter; defaults to pulling as necessary; if false, doesn't look in registry etc
     *
     * @return XenForo_BbCode_Formatter_Base
     */
    public static function create($class = '', $options = array())
    {
        if (!$class)
        {
            $class = __CLASS__;
        }
        else if (strpos($class, '_') === false)
        {
            $class = 'XenForo_BbCode_Formatter_' . $class;
        }

        $class = XenForo_Application::resolveDynamicClass($class, 'bb_code');

        /** @var XenForo_BbCode_Formatter_Base $formatter */
        $formatter = new $class();

        if (is_array($options))
        {
            $baseOptions = array(
                'smilies' => ****,
                'bbCode' => ****,
                'view' => ****
            );
            $options = array_merge($baseOptions, $options);
        }
        else
        {
            $options = array(
                'smilies' => array(),
                // omit bbCode as we basically always want this for custom tags
                'bbCode' => ****,
                'view' => false
            );
        }

        if (!is_array($options['smilies']))
        {
            if (XenForo_Application::isRegistered('smilies'))
            {
                $options['smilies'] = XenForo_Application::get('smilies');
            }
            else
            {
                $options['smilies'] = XenForo_Model::create('XenForo_Model_Smilie')->getAllSmiliesForCache();
                XenForo_Application::set('smilies', $options['smilies']);
            }
        }

        if ($options['smilies'])
        {
            $formatter->addSmilies($options['smilies']);
        }

        if (!is_array($options['bbCode']))
        {
            if (XenForo_Application::isRegistered('bbCode'))
            {
                $options['bbCode'] = XenForo_Application::get('bbCode');
            }
            else
            {
                $options['bbCode'] = XenForo_Model::create('XenForo_Model_BbCode')->getBbCodeCache();
                XenForo_Application::set('bbCode', $options['bbCode']);
            }
        }

        if (!empty($options['bbCode']['mediaSites']))
        {
            $formatter->addMediaSites($options['bbCode']['mediaSites']);
        }

        if (!empty($options['bbCode']['bbCodes']))
        {
            $formatter->addCustomTags($options['bbCode']['bbCodes']);
        }

        if ($options['view'])
        {
            $formatter->setView($options['view']);
        }

        return $formatter;
    }
   
        public function renderTagH1(array $tag, array $rendererStates)
            {
            $text = $this->renderSubTree($tag['children'], $rendererStates);
            return '<h1>' . $text . '</h1>';
            }
        public function renderTagH2(array $tag, array $rendererStates)
            {
            $text = $this->renderSubTree($tag['children'], $rendererStates);
            return '<h2>' . $text . '</h2>';
            }
        public function renderTagH3(array $tag, array $rendererStates)
            {
            $text = $this->renderSubTree($tag['children'], $rendererStates);
            return '<h3>' . $text . '</h3>';
            }

}
 
Hiện nay mình thấy Xenforo không cho phép viết thẻ BBcode H1, BBcode H2, BBcode H3 …v…v… vì vậy bài viết này mình sẽ hướng dẫn các bạn cách thêm những BBcode trên.

Cách thêm thẻ H1, H2, H3 cho Xenforo

Đầu tiên chúng ta mở file Base.php trong thư mục library\XenForo\BbCode\Formatter
Tìm đoạn:
Mã:
'img' => array(

Khoảng dòng thứ 342, cả đoạn của nó sẽ là:
Mã:
'img' => array(
'hasOption' => false,
'plainChildren' => true,
'callback' => array($this, 'renderTagImage')
),

Thêm vào bên dưới đoạn trên code sau:
Mã:
'h1' => array(
'hasOption' => false,
'plainChildren' => true,
'callback' => array($this, 'renderTagH2')
),
'h2' => array(
'hasOption' => false,
'plainChildren' => true,
'callback' => array($this, 'renderTagH2')
),
'h3' => array(
'hasOption' => false,
'plainChildren' => true,
'callback' => array($this, 'renderTagH3')
),

Tiếp theo kéo xuống dưới cùng của trang, ngay bên trên dấu
Mã:
}

Dán đoạn code sau vào:
Mã:
public function renderTagH1(array $tag, array $rendererStates)
{
$text = $this->renderSubTree($tag['children'], $rendererStates);
return '<h1>' . $text . '</h1>';
}
public function renderTagH2(array $tag, array $rendererStates)
{
$text = $this->renderSubTree($tag['children'], $rendererStates);
return '<h2>' . $text . '</h2>';
}
public function renderTagH3(array $tag, array $rendererStates)
{
$text = $this->renderSubTree($tag['children'], $rendererStates);
return '<h3>' . $text . '</h3>';
}

Xong. Như vậy là bạn đã thêm được thẻ h1, h2, h3 vào trang Xenforo của mình. Bây giờ vào viết bài để thưởng thức thành quả nhé.

Bạn có thể viết như sau:
Mã:
[h1]Nội dung thẻ h1[/h1]
[h2]Nội dung thẻ h1[/h2]

Chúc các bạn thành công.


Nguồn: root.vn​
bác cho tớ hỏi
cái hình mà bác demo ở tren thuộc sie nào thế
cho em xin để rip cái style. đẹp quá
 
  • Like
Reactions: THB
Back
Top