PHPackages                             bensomething/craft-wahlberg - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Parsing &amp; Serialization](/categories/parsing)
4. /
5. bensomething/craft-wahlberg

ActiveCraft-plugin[Parsing &amp; Serialization](/categories/parsing)

bensomething/craft-wahlberg
===========================

A Markdown field with a GitHub-style editor.

1.0.0-beta.2(today)00MITPHPPHP ^8.2

Since Jul 27Pushed todayCompare

[ Source](https://github.com/bensomething/craft-wahlberg)[ Packagist](https://packagist.org/packages/bensomething/craft-wahlberg)[ RSS](/packages/bensomething-craft-wahlberg/feed)WikiDiscussions develop Synced today

READMEChangelogDependencies (5)Versions (3)Used By (0)

Wahlberg for Craft CMS
======================

[](#wahlberg-for-craft-cms)

A Markdown field with a GitHub-style editor: a **Write** tab, a **Preview** tab, and a formatting toolbar.

Note

Wahlberg is in beta. The templating surface, `|marky` filter, `wahlberg_Markdown` GraphQL type, `Editor::inputHtml()` options, both events, and the `config/wahlberg.php` snippet format are settled. The field’s own settings may still move.

- Raw Markdown in, raw Markdown out
- Server-side preview, parsed with the same parser as Craft’s `|md` filter, so the preview can’t drift from the front end
- HTML Purifier on the parsed output by default, so inline `` can’t ride in on an author’s Markdown
- Craft reference tags like `[Read more]({entry:123:url})` resolved on output, and left alone inside code
- Markdown syntax highlighting in the Write tab, without giving up the plain textarea
- Formatting toolbar: headings, bold, italic, strikethrough, quote, code, link, entry and asset pickers, bulleted and numbered lists, folding into a menu when the field is too narrow for them
- Entry and asset links written as reference tags, so they survive a slug change or a replaced file
- Snippets: blocks of Markdown you define, dropped in from the toolbar
- The editor grows to fit what’s typed, between a minimum and (optional) maximum height
- `⌘B` / `⌘I` / `⌘K` shortcuts, `⌘⇧K` for snippets, and Enter continues lists and blockquotes
- Buttons toggle: hit **Bold** on already-bold text and the markers come off
- Native browser undo, so formatting buttons don’t blow away the undo stack
- No editor library bundled: it’s a textarea, some vanilla JS, and Craft’s own icons
- Per-field Markdown flavour, toolbar, sizing, placeholder, character or byte limit, and counts
- GraphQL support
- Reusable outside the field type: drop the editor into your own plugin’s settings from Twig or PHP

Requirements
------------

[](#requirements)

- Craft CMS 5
- PHP 8.2+

Installation
------------

[](#installation)

```
composer require bensomething/craft-wahlberg:^1.0.0-beta
```

The `-beta` in the constraint is what lets Composer install it under a project’s default `stable` minimum stability.

Or install from the control panel: **Settings → Plugins**.

The Field
---------

[](#the-field)

Create a field of type **Markdown** and add it to a field layout.

**Field settings**

SettingDefault**Markdown Flavour**GitHub-FlavouredWhich parser the Preview tab and the `html` value use. GFM adds fenced code blocks, tables, strikethrough and autolinking; Traditional Markdown and Markdown Extra are also available.**Preserve Line Breaks**OnGFM only. Turns a single newline into a ``, the way GitHub’s comment boxes do. Turn it off for Markdown that’s hard-wrapped and meant to reflow. This is the parser’s `gfm-comment` flavour, which is what `.flavour` reports.**Inline Only**OffRender without the wrapping ``, for a heading or strapline going into markup of its own. Emphasis, links and code still parse.**Appearance**

SettingDefault**Text Size**14pxThe Markdown source in the editor, 11–20px. Editing comfort only — no bearing on the front end.**Minimum Rows**2How short the editor may get, 1 or more. It grows from there as the author types.**Maximum Rows**noneHow tall it may grow before it scrolls instead. Blank lets it keep growing. Dragging the resize handle overrides auto-growing for that session.**Placeholder Text**noneShown while the field is empty.**Show Preview Tab**OnOff makes the editor source-only, and the toolbar moves to where the tabs were.**Show Formatting Toolbar**OnThe keyboard shortcuts keep working either way.**Toolbar Buttons**all but Heading 1, Heading 3–6 and the guideWhich buttons the toolbar offers — see [The toolbar](#the-toolbar).**Show Syntax Highlighting**OnOff leaves a plain textarea with the same sizing, toolbar and Preview tab. The escape hatch if a font stack won’t hold the highlighted layer and the textarea together.**Show Stats**OffCharacter, word and line counts under the editor, with the field limit alongside when there is one.**Field Limit**noneThe most characters or bytes of Markdown the field accepts, enforced on save. Counts the source an author types, not the HTML it renders to. Bytes matter once the text stops being ASCII: an emoji is one character and four bytes.**Parsing**

SettingDefault**Parse Reference Tags**OnSee [Reference tags](#reference-tags).**Encode HTML**OffEncode HTML before parsing, so a tag an author types shows up as text. See [Raw HTML and purification](#raw-html-and-purification).**Purify HTML**OnSee [Raw HTML and purification](#raw-html-and-purification).**HTML Purifier Config**DefaultWhich JSON config in `config/htmlpurifier/` to sanitise with.**Snippets**

Only shown when `config/wahlberg.php` defines any. See [Snippets](#snippets).

SettingDefault**Available Snippets**allWhich of the defined snippets this field’s **Snippets** button offers.**Assets**

These apply to the toolbar’s **Asset** button.

SettingDefault**Available Volumes**allWhich volumes the button may pick from.**Show unpermitted volumes**OffWhether to offer volumes the author can’t view.**Show unpermitted files**OffWhether to offer files uploaded by other authors, per Craft’s “View files uploaded by other users” permission.The toolbar
-----------

[](#the-toolbar)

Every button is optional, and which ones a field offers is up to *Toolbar Buttons*:

ButtonShortcutWhat it writes**Heading 1**–**Heading 6**that level exactly, so clicking **H3** on an H1 line makes it an H3**Bold**⌘B`**` around the selection, or the word under the caret**Italic**⌘I`_` around the selection, or the word under the caret**Strikethrough**`~~` around the selection, or the word under the caret**Quote**`> `**Code**``` around a selection on one line, a fence around one spanning several**Link**⌘K`[text](url)`, or `[](url)` with the caret in the brackets when a URL was selected**Entry**, **Asset**opens Craft’s element selector — see below**Bulleted list**, **Numbered list**`- ` and `1. `, toggling between each other rather than stacking up**Snippets**⌘⇧Kblocks of Markdown you define — see [Snippets](#snippets)**Markdown guide**a syntax cheatsheet, in a popover off the buttonShortcuts work whether or not the button is shown, so a field with the toolbar switched off still has all of them. On Windows and Linux, Ctrl stands in for ⌘.

The buttons fold into a menu when the field is too narrow to hold them all. **Snippets** and **Markdown guide** are the exceptions: each opens a panel rather than writing anything, so they stay put at the end of the toolbar.

### Headings

[](#headings)

However many heading levels you tick, the toolbar shows **one** control — six near-identical H icons in a row is a lot of toolbar to say one thing. What changes is its shape:

Levels tickedWhat authors getNoneno heading control at allOnea button that applies that level outrightTwo or morea dropdown listing themThe icon is the same plain **H** either way, with the level named in the tooltip, so the toolbar doesn’t shift about between fields. New fields start with **Heading 2** on its own: level 1 is nearly always the element’s own title, so body content starts below it.

### Entry and Asset

[](#entry-and-asset)

Both open Craft’s element selector. **Entry** writes a link; **Asset** writes an image as `![alt](…)` and anything else as a link, taking the alt text from the asset when it has some.

With *Parse Reference Tags* on, both write a reference tag rather than a URL:

```
[The Difference Engine]({entry:19:url})

![Ada Lovelace]({asset:41:url})
```

so the link survives a slug change, or follows the file if it’s replaced or moved. With reference tags off there’s nothing to resolve the tag later, so they write the URL instead — and an entry with no URL of its own writes an empty one.

Snippets
--------

[](#snippets)

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

```
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 that doesn’t name an icon gets a neutral stand-in, so the labels line up either way.

Two markers are understood, both optional:

- **`$0`** is where the caret ends up. Without one it 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.

Mind the quoting: inside a double-quoted PHP string, `$0` and `$SELECTION` read as variables, so escape them as `\$0` and `\$SELECTION`. Single quotes avoid that but cost you `\n`. Heredocs interpolate; nowdocs (` 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 entirely and pass definitions to `Editor::inputHtml()` instead — see [Using the editor in your own plugin](#using-the-editor-in-your-own-plugin).

Templating
----------

[](#templating)

The field value is `null`, or a `MarkdownData` object:

```
{{ entry.body.html }}   {# the parsed HTML #}
{{ entry.body.raw }}    {# the raw Markdown, as typed #}
{{ entry.body.text }}   {# parsed, then stripped to plain text #}
{{ entry.body.flavour }} {# the flavour it was parsed with #}
```

`{{ entry.body }}` on its own outputs the raw Markdown, so Craft’s own filter still works if you’d rather parse it yourself:

```
{{ entry.body|md('gfm') }}
{{ entry.body|md(inlineOnly=true) }}
```

### The `|marky` filter

[](#the-marky-filter)

For Markdown that isn’t in a Markdown field, whether a plain text field, a plugin setting, or a string you built in the template, `|marky` parses it the way the field does: reference tags resolved, HTML purified.

```
{{ entry.summary|marky }}
{{ entry.summary|marky(flavour='original') }}
{{ entry.summary|marky(refs=false, purify=false) }}
```

Arguments are `flavour`, `refs`, `purify`, `purifierConfig` and `siteId`, all optional.

Craft’s `|md` is untouched and still the right choice when plain Markdown parsing is all you want. The difference is what each one does beyond parsing:

`|md``|marky``.html`Parses Markdown✅✅✅Resolves reference tags❌✅✅Purifies HTML❌✅✅Uses the field’s settings❌only when piped a field value✅Piping a field value, `{{ entry.body|marky }}`, is the same as `{{ entry.body.html }}`, since it takes the field’s own settings. It’s worth doing only to override one of them for a single render:

```
{{ entry.body|marky(refs=false) }}
```

Empty fields are `null`, so the usual guard applies:

```
{% if entry.body %}
    {{ entry.body.html }}
{% endif %}
```

Reference tags
--------------

[](#reference-tags)

Craft’s [reference tags](https://craftcms.com/docs/5.x/system/reference-tags.html) work in Markdown fields, and are resolved when the field renders. **Parse Reference Tags** is on by default.

```
[Read the docs]({entry:123:url}), see also {entry:my-section/some-entry:title}.

![Diagram]({asset:456:url})
```

They’re resolved on the way out, not on save, so an entry that changes its slug doesn’t leave a trail of dead links behind it. An unresolvable tag falls back to whatever Craft’s fallback syntax says, or to the tag itself:

```
{entry:999:title || Something else}
```

Two things worth knowing:

- **Code is left alone.** A reference tag in a fenced block or an inline code span renders as the author typed it, which is what you want when the thing you’re documenting *is* reference tags. This works because tags are resolved after parsing, when the parser has already decided what counts as code, so there’s no second guess at Markdown’s fence rules to get wrong.
- **Resolved values are purified.** Whatever a tag resolves to goes through HTML Purifier along with the rest of the content, assuming **Purify HTML** is on.

On a multi-site install, tags resolve against the site the element is being rendered in. Override that per tag with Craft’s own `@` syntax, `{entry:123@german:url}`, or for a whole render with `{{ text|marky(siteId=2) }}`.

Raw HTML and purification
-------------------------

[](#raw-html-and-purification)

Markdown lets authors write HTML inline, so a Markdown field is an HTML field wearing a disguise. **Purify HTML** is on by default: `entry.body.html` is run through [HTML Purifier](http://htmlpurifier.org/) after parsing, using the same defaults as Craft’s own HTML fields, which means YouTube and Vimeo iframes survive and `` doesn’t.

Two things to know about how that works here:

- **It runs at output, not on save.** Craft’s CKEditor field purifies the value as it’s stored, because what’s stored *is* HTML. Here the stored value is Markdown source, and purifying source would mangle it, since autolinks like `` and `
