SMF Modifications and Hooks

Started by snow, Mar 03, 2025, 08:07 AM

Previous topic - Next topic

snow

This month's abyss I've been staring into has apparently been SMF, and what better to talk about here than what I've been working on for the past month? Be warned that this is very stream-of-consciousness! Hopefully it'll be useful to others, or even my future self.

Instead of "plugins" or "addons," SMF uses the term "modifications" to refer to bits of code you tack onto it to change or add functionality. That's because mods often look like this:

Code (xml) Select
<?xml version="1.0"?>
<!DOCTYPE modification SYSTEM "http://www.simplemachines.org/xml/modification">
<modification xmlns="http://www.simplemachines.org/xml/modification">
  <id>flurry:example<id>
  <version>1.0</version>
  <file name="$languagedir/index.english.php">
    <operation>
      <search position="end" />
      <add><![CDATA[
        $txt['example_string'] = "This is an example of a string in the English locale!";
      ]]></add>
    </operation>
  </file>
</modification>

Haha yes, that is basically a patch file written in XML!

Thankfully, SMF 2.0 and above provide integration hooks, which let you do less painful things, like this:

Code (php) Select
function my_cool_hook(&$buttons)
{
  global $context;
  $buttons[] = array(
    'cool' => array(
      'title' => 'Cool Video',
      'href' => 'https://www.youtube.com/watch?v=klqi_h9FElc',
      // only logged in users get to see this
      'show' => !empty($context['user']['is_logged']),
      'sub_buttons' => array(),
    ),
  );
}

Of course, you still have to tell SMF how to wire up the hook. Since we're looking to add a button, we'll need to use the integrate_menu_buttons hook. There are functions to do this, but we want to ensure no "core" SMF code is touched. We can do this in the package-info.xml file required at the root of a package:

Code (xml) Select
<?xml version="1.0"?>
<!DOCTYPE package-info SYSTEM "http://www.simplemachines.org/xml/package-info">
<package-info xmlns="http://www.simplemachines.org/xml/package-info">
  <id>@flurry:cool-button</id>
  <name>Add cool button</name>
  <version>1.0</version>
  <type>modification</type>

  <!-- We're assuming this will work for every version of SMF 2.1.x -->
  <install for="2.1.* - 2.1.99">
    <require-file name="Subs-CoolButton.php" destination="$sourcedir" />
    <hook hook="integrate_menu_buttons" function="my_cool_hook" file="$sourcedir/Subs-CoolButton.php" />
  </install>

  <!-- ... and then we have to tell SMF how to uninstall it: -->
  <uninstall for="2.1.* - 2.1.99">
    <remove-file name="$sourcedir/Subs-CoolButton.php" />
    <hook hook="integrate_menu_buttons" function="my_cool_hook" file="$sourcedir/Subs-CoolButton.php" reverse="true" />
  </uninstall>
</package-info>

A lot more information is available on the SMF wiki, so if you're interested, I highly recommend checking there!



At this point, you might be wondering how to find these hooks.


heh. heheh.


The SMF wiki does list a good chunk of hooks, but not all of them. It also doesn't do a great job explaining what they do or how they work (looking at you, integrate_egg_nog). One of those examples is the rich text editor.

SMF 2.1 introduced SCEditor as the rich text editor for posts and PMs. It's very extensible and configurable, but that wouldn't be immediately obvious to someone looking to build a mod.

I wish I noticed the wiki page listing all integration hooks in SMF 2.1, but hindsight etc etc.

There is a hook, integrate_sceditor_options, that lets us make modifications to the configuration. This, in tandem with integrate_bbc_codes (to modify the server-side BBCode renderer), integrate_bbc_buttons (to modify the buttons in the SCEditor toolbar), and good ol' JavaScript (to handle the SCEditor functionality), is how I added the [video] and [audio] tags to the forum.

The one problem with integrate_sceditor_options is that it only accepts types that are usable JSON, since it's just dumping the options as JSON into the JS code. This means any actual JavaScript code is out, which makes implementing the dragdrop plugin for SCEditor (which requires a function as part of the config object) a bit difficult.

Despite that, I'm really appreciative of the effort the SMF devs have put into hooks! Only time will tell, but I at least feel like the changes I'm making are less likely to break across versions.

viviridian

that's really cool. I was basically stuck on 1.5 because I deviated *recklessly* from stock, including but not limited to rewriting the user info panel beside posts lol

snow

Types of BBCode

The spoiler plugin I've built, which adds spoilers and "restricted" blocks of text in the form of BBCode, involved working with the BBCode renderer in SMF. Surprising, I know!

Maybe a bit more of a surprise is that SMF 2.x has not one, but two BBCode renderers. The one SMF 1.x hackers might remember is the server-side rendering code that's an undocumented mess. Don't worry, it's still a mess. We'll have to worry about that soon, but first, I want to talk about the JavaScript-based SCEditor used in SMF 2.x.

There are two modes to SCEditor: plain text and WYSIWYG. You can switch between the two by clicking the page icon in the toolbar:

image.png

The text-based editor is as basic as it gets, and the WYSIWYG editor is... an iframe that you edit because it's content-editable. Which, okay fair, I can't think of a better way to do it. But that does mean we have to convert between HTML and BBCode. So how to we do that?

By hand, of course :rember:

I won't go into the details since the SCEditor docs handle it pretty well, but basically: you either receive the BBCode form pre-parsed and have to turn the values into an HTML fragment (text -> WYSIWYG), or you receive the HTML element in the DOM and have to return the BBCode as a string. Somewhat annoying when you have something as complex as a details/summary pair of elements, but I couldn't think of a better way to do it generically.

This is all client-side, though. SCEditor always sends the BBCode to SMF when you hit Post or Save Draft, since SMF doesn't know what to do with the HTML. Unfortunately, this aspect of SMF is poorly documented. I have come to expect this at least.

So, to start off, we have to hook integrate_bbc_codes with a function that we'll use to include our BBCode. That hook function will look a little something like this:

Code (php) Select
function hook_my_bbcode(&$codes, &$no_autolink_tags) {
  // $no_autolink_tags is an array of tags that shouldn't have their
  // content automatically turn into links. we'll be ignoring that
  // here.
  $codes[] = array(
    // the tag will be [box][/box]
    'tag' => 'box',
    // more on this in a moment!
    'type' => 'unparsed_content',
    // $1 is the content between the tags
    'content' => '<div style="border: 1px solid white;">$1</div>',
    // Provide a function to transform the content the user entered.
    // For example, [noguest] uses this to hide text from guests.
    'validate' => function(&$tag, &$data, $disabled, $params)
    {
      // If the user didn't provide content, provide it for them :)
      if (empty($data)) {
        $data = "im in the box!";
      }
    },
    // yes this is a block element
    'block_level' => true,
  );
}

There are a bunch of other array keys I'm skipping over here, which are only really documented in the SMF parse_bbc function in Subs.php. The main thing that I want to share that tripped me up is the "type" key. Here it's "unparsed content," but what does that mean? What is "parsed content"?

Fear not, curious reader, for I shall share the code comment in the middle of a function documentation with you:

  type: one of...
      - (missing): [tag]parsed content[/tag]
      - unparsed_equals: [tag=xyz]parsed content[/tag]
      - parsed_equals: [tag=parsed data]parsed content[/tag]
      - unparsed_content: [tag]unparsed content[/tag]
      - closed: [tag], [tag/], [tag /]
      - unparsed_commas: [tag=1,2,3]parsed content[/tag]
      - unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
      - unparsed_equals_content: [tag=...]unparsed content[/tag]

Well. This is useful, but only vaguely elucidates what "parsed" content is. It's referring to the parsing of BBCode: for example, the content of [mytag][b]hello[/b][/mytag] would be "hello" if [mytag] is parsed content, but "[b]hello[/b]" if it's unparsed. In retrospect, this was probably obvious...

There's an important caveat to the first one, internally "parsed_content" but actually defined by the lack of a "type" parameter: you cannot use the "content" field, and "validate" can't be used to modify the content. To actually convert the tag to HTML, you have to use the "before" and "after" fields, like so:

Code (php) Select
$codes[] = array(
  // the tag will be [box][/box]
  'tag' => 'box',
  // what to add before the content
  'before' => '<div style="border: 1px solid white;">',
  // what to add after the content
  'after' => '</div>',
  'block_level' => true,
);

This is because the parser is effectively replacing the beginning tag with the "before" parameter, noting there's an open tag currently, and continuing on with parsing. Once it finds that closing tag (or the BBCode ends abruptly), it inserts the "after" content.

As far as I can tell, this doesn't happen for any other form of parsed content, as it recursively calls parse_bbc in those cases. I suppose it's far more likely for the actual content to have lots of recursive calls; e.g. a pile of nested quote blocks. But it's an annoying pitfall if you don't know about it!

This especially caused issues for me with the [noguest] tag, because the "validate" function is the core of it. So, at least at the moment, you can't actually use BBCode within that tag. I might just fix it by doing a recursive parse_bbc call, but I want to see what (if any) impact that has first.

lifning

...okay i had no idea the WYSIWYG mode even existed. very impressive that you got this working with that!!
You cannot view this attachment.
Please consider the environment before printing this post.