Snippets


Blocks of Markdown authors can drop in from the toolbar, defined in config/wahlberg.php. Copy src/config.php to start from a working example.

PHP
return [
    'snippets' => [
        'callout' => [
            'label' => 'Callout',
            'icon' => 'circle-info',
            'body' => "> **Note**\n> \$0\n",
        ],

        // Shorthand: a body on its own, labelled from its key
        'leadIn' => "**\$SELECTION**\n\n\$0",
    ],
];

icon is optional: any name from Craft’s set, which is Font Awesome’s solid icons. One without gets a neutral stand-in, so the labels line up either way.

Markers, all optional:

  • $1 to $9 are stops. Insert a snippet with more than one and the caret lands on the first. Tab moves to the next, ⇧Tab back.
  • ${1:like this} is a stop with a default. The text goes in, and landing on the stop selects it, so it reads as a prompt and types over as a placeholder. Bare ${1} is the same as $1.
  • $0 is where the caret ends up: the last stop, after the numbered ones. Without any marker the caret lands at the end.
  • $SELECTION is replaced by whatever the author had selected, so a snippet can wrap their text rather than only ever landing beside it. It’s empty when nothing was selected, and every occurrence is replaced.
PHP
'table' => "| \${1:Column} | \${2:Column} |\n| --- | --- |\n| \$3 | \$0 |\n",
'link' => "[\${1:\$SELECTION}](\${0:https://})",

Defaults earn their characters on anything with more than a couple of stops: a bare stop is somewhere to go, a filled one says what goes there. A default is ordinary text, so $SELECTION inside one is still substituted and the stop comes out covering whatever it stood in for. There’s no nesting: a default runs to the first }.

Tab belongs to a run only while one is going, since it’s also how you leave a field. Esc ends a run early, and it ends on its own at the last stop or when the caret leaves the text the snippet put in. A body carrying only $0 is a caret position rather than a run, and behaves as it always did.

Mind the quoting. Inside a double-quoted PHP string, $0 and ${1:…} read as variables, so escape the $ as \$0 and \${1:…}. Single quotes avoid that but cost you \n. Heredocs interpolate. Nowdocs (<<<'MD') don’t.

Why a config file and not a settings screen? A snippet is a contract with the templates and CSS that render it, so the person writing one should be the person who can write those too, and the definition should travel with the code. Which snippets a given field offers is a field setting, under Available Snippets, the same split config/htmlpurifier/ already uses.

A field that has never been saved against a snippet offers all of them, so adding one to the config file reaches every existing field without editing each one. With no config file the Snippets button hides itself rather than opening an empty menu, and the field settings drop the section to match.

Opening the menu #

Typing / opens it at the caret, and what you type after that narrows the list. /cal gets you to a callout without reaching for the arrow keys. ⌘⇧K opens it at the caret too, and clicking the toolbar button opens it under the button, since that's where the eye already is.

Neither needs the button: both work with Snippets unticked in Toolbar Buttons, and with the toolbar switched off altogether. Arrows and Tab move through the list, Enter inserts, Esc closes and puts the caret back.

The / menu also offers Entry and Asset, above the snippets and divided off from them. They're the two commands that put something in at the caret rather than reshaping what's around it, which is the only kind a menu opened by typing can offer. There's nothing selected to make bold.

They don't follow Toolbar Buttons. That setting says what the toolbar shows, not what the field can do, and ⌘⇧E and ⌘⇧U work with every button unticked. So does the snippet half of this menu. On a field with no snippets at all, the menu is there for the two commands alone.

⌘⇧K and the button stay snippets-only. Both have meant snippets since before there was anything else in the list.

What counts as a /. Only one at the start of a line or after a space, and never inside a fenced code block. Markdown source is full of the other kind, in URLs, paths, closing tags and dates. Past that the list narrows as you type and closes the moment nothing matches, so a slash that wasn't meant as a command costs a flicker rather than a dismissal.

Mind what your fields render. With Purify HTML on, the default, raw HTML in a snippet is sanitised on the way out by a purifier that only knows HTML 4. <details> and <summary> are dropped entirely, and iframes survive only for the hosts config/htmlpurifier/ allows. Markdown inside a raw HTML block isn’t parsed either, whatever the purifier does. A snippet that emits Markdown works everywhere. One that emits HTML is worth checking in the Preview tab first.

From a plugin #

Plugins can add snippets to the pool every field picks from:

PHP
use bensomething\wahlberg\events\RegisterSnippetsEvent;
use bensomething\wahlberg\helpers\Snippets;
use yii\base\Event;

Event::on(
    Snippets::class,
    Snippets::EVENT_REGISTER_SNIPPETS,
    function(RegisterSnippetsEvent $event) {
        $event->snippets['productSpec'] = [
            'label' => Craft::t('my-plugin', 'Product spec'),
            'body' => "{spec:\$0}\n",
        ];
    }
);

A handle already defined in config/wahlberg.php wins, so an installation can always overrule a plugin about its own site. Plugins rendering the editor directly can skip the pool and pass definitions to Editor::inputHtml() instead. See Using the editor in your own plugin.