TOC BB code add-on implementation

Slion

Active member
I'm trying to implement some BB code add-on that would build a Table Of Content for a post.
I'm new to PHP and XenForo and have never implemented add-ons before.
However I'm familiar with Perl and other web applications.

I would like to publish the following BB codes: H1 to H6 and TOC.
I got my Hx BB code working.
Now I would like to be able to store information between callbacks call so that this information is available to each of my callbacks.
The idea would be for Hx BB code to record themselves in some kind of data structure that the TOC callback could use to render itself.
If I can get the TOC to render last that should just work, right?

So far I tried using load class bb code data member and rendererStates but neither worked.
They are not preserved between callback calls.

PHP:
<?php

/**

*/
class Slions_Toc_BbCode_Formatter_Base extends XFCP_Slions_Toc_BbCode_Formatter_Base
{
    //protected $_tags;
 
    protected $rendersCount;
 
    /**
    * Override getTags to publish our ouwn BB codes.
    */
    public function getTags()
    {
        $tags = parent::getTags();
        $tags['h2'] = array(
            'hasOption' => false,
            //'replace' => array('<h2>', '</h2>')
            'callback' => array($this, 'renderTagHeader')
        );
        return $tags;
    }
 
    /**
     * @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 renderTagHeader(array $tag, array $rendererStates)
    {        
        if (!isset($rendererStates['slionsHeaderCount']))
        {
            $rendererStates['slionsHeaderCount'] = 0;
        }
        $output = '<h2>' . $rendererStates['slionsHeaderCount'] . $tag['children'][0] . '</h2>';
        $rendererStates['slionsHeaderCount']++;
        return $output;         
    } 
}

@Chris D Since you are online now, maybe you could help me out there?
 
Last edited:
I now tried through parent::getView()->getParams() and static data member but still could not get it to work.
 
I got some level of success through $GLOBALS but the lifetime of those are for the whole request.
Ideally I would need a variable which lifetime is the rendering of a single post.
Alternatively I could reset my $GLOBALS at the start or end of the rendering of a post if I knew which event to listen to.
Can a XenForo_BbCode_Formatter_Base find out the ID of the post currently being rendered?
 
What you're trying to do is difficult, because you're basically trying to have one tag render based on other parts of the tree. You can try it, but you'll need some somewhat hacky approaches.

The function you may want to start with is XenForo_BbCode_Formatter_Base::renderTree() as this will essentially be the root call, so you can do setup and tear down code here. The parsing will happen between those parts, so you can push data into a variable that can be accessed during tear down. Note that the $rendererStates variables is specifically to push modifiers down; it won't fit your needs.

The hack is that if you want a ToC somewhere (probably the beginning of the post), you need to output a unique placeholder and then replace it at the end once you understand the full structure of the post (which you wouldn't have when the ToC tag was encountered).

Alternatively, if you can pre-parse the template and store the ToC, that would solve the issue with a slightly less hacky feel, though that would likely be a much more significant challenge.
 
What you're trying to do is difficult, because you're basically trying to have one tag render based on other parts of the tree. You can try it, but you'll need some somewhat hacky approaches.
That was expected.

The function you may want to start with is XenForo_BbCode_Formatter_Base::renderTree() as this will essentially be the root call
Thanks, it looks like there is one renderTree call per post so that should allow me to build a data structure for my headers tree.

The hack is that if you want a ToC somewhere (probably the beginning of the post), you need to output a unique placeholder and then replace it at the end once you understand the full structure of the post (which you wouldn't have when the ToC tag was encountered).
I'm still not quite clear on how to handle that. I was thinking to use a different listener with a higher Callback Execution Order for the TOC tag, hoping that all Hx tags would get processed before the TOC tag no matter where it lies in the post. Do you think that could work?

Thanks @Mike you made my day.
 
Do you think that could work?
In short, no. The post isn't parsed through regular expressions; it's parsed in a beginning-to-end, top-to-bottom style order, essentially like you'd see in the DOM. If you want a tag that exists early in the message to be affected by something later, you need to take an approach where you can defer your "real" processing until the full message has been parsed.
 
FWIW I once wrote a table of contents add on and doing it purely with the BB code system seemed both hacky and problematic to the point that I gave up and settled on doing string replacements directly on the message content... which was just hacky but not so problematic :)

Not saying it's impossible but it's certainly not straightforward.
 
I'm no CSS expert but I believe that even if the TOC tag has to be last for it to work you could have some CSS that makes it float at the top right corner of the post right?
That would possibly prevent the user from having an inline ToC at the top of the post but I guess I can live with that.

Another solution for inline ToC could be to use the TOC tag has a wrapper.
Just place all you post content between the TOC start and end markers.
I reckon that should work, right?
 
Last edited:
This is slowly going somewhere. I can reliably group headers per post.

Though I'm still having a hard time determining the current post id.
I have the 'posts' on the page from the view parameters and the keys in that array are the 'post ids' so I was hoping that by getting the post index I could fetch the post id from there.
However I can't even count the post index as it seems my globals are lost between two renderTree() calls.

PHP:
    public function renderTree(array $tree, array $extraStates = array())
    {
        if (!isset($GLOBALS['slionsCurrentPostIndex']))
        {
            $GLOBALS['slionsCurrentPostIndex'] = 0;        
        }
     
        $this->resetToc();
        $output = parent::renderTree($tree, $extraStates);
        $GLOBALS['slionsCurrentPostIndex']++;
        return $output;
    }

Is there any kind of storage you could recommend for that purpose?


I did manage to work out the post id after all.
For the longer term it would be nice if you could somehow make the current post id more directly available to bb code formatters in future XenForo 1 releases.
 
Last edited:
Another issue I have is that I was hoping to use relative in-page links for my TOC hrefs like "#post-id_header-index".
However that's not working since our XenForo <header> specifies <base> as the domain root.
I'll need to either remove that <base> or provide the page name in my href like "my-page.123#post-id_header-index".
Any idea how I can fetch the URL page name from my BB code formatter?
 
Last edited:
Is there any kind of storage you could recommend for that purpose?
Oups, looks like that's just a misplaced return statement, how embarrassing.

I'm left with two issues:
It looks like that even when floating it with CSS the TOC should ideally come first.
I guess I'll have to do some pre-parsing/look-ahead of the message. The post object contains the raw text in its message property so while more involved it looks like it's totally doable.

Then I'll have to tackle wysiwyg and BB code help to make it all pretty.
 
Last edited:
This isn't really ideal but you can do something like this:
PHP:
$requestPaths = XenForo_Application::getRequestPaths(new Zend_Controller_Request_Http());
It might not be exactly what you need but should help you get what you want.
 
This isn't really ideal but you can do something like this:
PHP:
$requestPaths = XenForo_Application::getRequestPaths(new Zend_Controller_Request_Http());
It might not be exactly what you need but should help you get what you want.

Brilliant, that's just what I was looking for. It provides an array with various sub-parts of the full HTTP address including one called 'requestURI' that I could use to resolve my TOC links.
So I already have a fully functioning TOC, I just need to implement the look ahead parsing and make the whole thing pretty.
 
Last edited:
After implementing the message parsing to build the TOC I can put the TOC where ever in the post, all that from BB formatter without too many ugly hacks.
There are still some rough edges however. When I do a quick edit of a post it's not rendering the TOC, I need to trigger a full page reload for it to work. I'll need to investigate.
I believe the view parameters are quite different for single post rendering.

Do BB code always need start and end markers? Right now I need to [TOC][/TOC] and I wish I could just do [TOC].

It will be a while until this is production ready but I'm quite satisfied with this prototype so far and I don't see any show stopper ahead.
 
When I do a quick edit of a post it's not rendering the TOC, I need to trigger a full page reload for it to work. I'll need to investigate.
I believe the view parameters are quite different for single post rendering.

That's now sorted out. It was most likely hitting a wall when trying to render single topic after quick edit.
I've consolidated that use case and it's now working just fine too.

From now on, all I need is wysiwyg, help, prettier HTML output, CSS support and code clean-up.
If you have tips on how to expedite wysiwyg and help page support I'll gladly take them.
 
Last edited:
As explained above I got the core feature wrapped up. I've also dealt with CSS styling and got something like that:
upload_2017-3-31_17-50-53.webp

Now I'm trying to tackle wysiwyg. I've implemented a wysiwyg class which outputs HTML headers too and that works nice when first editing your post. However going through an edit cycle my HTML headers are converted the BB codes like:
Code:
[SIZE=6][B]That's my header[/B][/SIZE]

I'm obviously missing something there that would prevent the wysiwyg editor to convert my HTML headers into those BB codes and preserve the original [hx] BB codes.
Any help would be appreciated.

@Chris D are you still online?
 
So I guess what I'm looking for is something like those redactor format.
I'll need to somehow add that format button to redactor and hook the whole thing with the BB code translator somehow.
I'm just not sure where to get started and if there is even a clean way to implement this as an add-on that would survive upgrades.
 
Looks like I found the code that's doing the magic I'm look for.
It's in:
PHP:
class XenForo_Html_Renderer_BbCode

I guess I would need to replace the headers entries in:
PHP:
    protected $_handlers = array(
        'b'          => array('wrap' => '[B]%s[/B]'),
        'strong'     => array('wrap' => '[B]%s[/B]'),

        'i'          => array('wrap' => '[I]%s[/I]'),
        'em'         => array('wrap' => '[I]%s[/I]'),

        'u'          => array('wrap' => '[U]%s[/U]'),
        's'          => array('wrap' => '[S]%s[/S]'),
        'strike'     => array('wrap' => '[S]%s[/S]'),

        'font'       => array('filterCallback' => array('$this', 'handleTagFont')),
        'a'          => array('filterCallback' => array('$this', 'handleTagA')),
        'img'        => array('filterCallback' => array('$this', 'handleTagImg')),

        'ul'         => array('filterCallback' => array('$this', 'handleTagUl'), 'skipCss' => true),
        'ol'         => array('filterCallback' => array('$this', 'handleTagOl'), 'skipCss' => true),
        'li'         => array('filterCallback' => array('$this', 'handleTagLi')),

        'blockquote' => array('wrap' => '[INDENT]%s[/INDENT]'),

        'h1'         => array('filterCallback' => array('$this', 'handleTagH')),
        'h2'         => array('filterCallback' => array('$this', 'handleTagH')),
        'h3'         => array('filterCallback' => array('$this', 'handleTagH')),
        'h4'         => array('filterCallback' => array('$this', 'handleTagH')),
        'h5'         => array('filterCallback' => array('$this', 'handleTagH')),
        'h6'         => array('filterCallback' => array('$this', 'handleTagH')),
    );
with something like:
PHP:
        'h1'         => array('wrap' => '[h1]%s[/h1]'),

or just override handleTagH.
Now the question is can I override that class through XenForo add-on mechanisms.
 
Top Bottom