Kody for Craft CMS

Usage

The field in a template

A Code field gives templates a value object. The common case is one call:

{{ entry.snippet.render() }}

render() returns a <figure> with the highlighted code, a caption bar and a copy button, depending on the field's and the plugin's defaults. Pass overrides for anything they decided:

{{ entry.snippet.render({
    lineNumbers: true,
    highlightLines: '4, 7-9',
    filename: 'templates/news/index.twig',
    copyButton: true,
    maxHeight: '30rem',
}) }}

The rest of the value:

{{ entry.snippet }}The source, escaped by Twig
.codeThe source as a string
.languageA Language: .id, .label, .extension()
.lines, .lineCount, .characterCount
.filenamecode. plus the language's extension, e.g. code.twig
.highlight()Just the coloured source, with no wrapper
.inline()A <code> element, for code in a sentence
.json, .isValidJsonFor fields holding JSON. .json is null when it doesn't parse
.isEmpty, .isNotEmptyWhitespace-only counts as empty

To skip an empty field, test .isNotEmpty:

{% if entry.snippet.isNotEmpty %}
    {{ entry.snippet.render() }}
{% endif %}

A bare {% if entry.snippet %} won't work for this. The value is an object, so it is always truthy.

To find entries by the field's language, pass language as the query param. It takes one language ID or a list:

{% set twigEntries = craft.entries().snippet({ language: 'twig' }).all() %}
{% set styles = craft.entries().snippet({ language: ['css', 'scss'] }).all() %}

Any other param is matched against the code itself, e.g. .snippet('*@media*').

Render options

Option
languageOverride the stored language
lineNumbersLine numbers are CSS counters, so copying never picks them up
startLineThe number the first line is labelled with
highlightLines'3', '1,4', '3-5', '3..5' or [3, 4]. Counted from the first line of the code, whatever startLine says
wrapSoft-wrap instead of scrolling sideways
maxHeightOne CSS length. Taller blocks scroll. Anything else is ignored
copyButtonThe only JavaScript Kody puts on a page
filenameShown in the caption
captionFree text for the caption
showLanguageThe language name in the caption
themeauto (follows the visitor's colour scheme), light or dark
tabSize
dedentStrip the indentation every line shares
trimDrop blank leading and trailing lines
class, idApplied to the wrapping <figure>

A misspelt option is ignored rather than fatal, so a typo falls back to the default instead of breaking the page.

Snippets

Some code belongs to the site rather than to an entry: the install command, the deploy script, the .htaccess everybody asks for. Kody → Snippets is a library of it. Each snippet has a title, a handle, a language and the code.

From a template:

{{ kodySnippet('deploy') }}
{{ kodySnippet('deploy', { lineNumbers: true }) }}
{{ craft.kody.snippet('deploy').code.lineCount }}

{% for snippet in craft.kody.snippets({ language: 'twig' }).all() %}
    <h3>{{ snippet.title }}</h3>
    {{ snippet.render() }}
{% endfor %}

A snippet uses its title as the caption unless you pass a filename or caption.

In a rich-text field

Paste a reference tag into any rich-text field. No template change is needed:

{kody:deploy:render}

The :render part is required. A reference tag that names no property falls back to the element's URL, and snippets don't have URLs. The snippet's edit screen shows the exact tag to copy.

The Snippets field

A Snippets relation field gives authors the same library as a picker:

{% for snippet in entry.relatedSnippets.all() %}
    {{ snippet.render() }}
{% endfor %}

From the command line

php craft kody/snippets/import ./deploy.sh --handle=deploy
php craft kody/snippets/import ./deploy.sh --update=deploy
php craft kody/snippets/list --language=shell

import guesses the language from the file extension unless you pass --language. Without --handle, the handle is the filename. --update overwrites the snippet that has that handle rather than creating a new one.

Anywhere else

craft.kody works on any string, not only on Kody fields:

{{ craft.kody.render(source, 'php', { filename: 'Plugin.php' }) }}
{{ craft.kody.highlight(source, 'php') }}
{{ craft.kody.inline('npm run build', 'shell') }}

{{ source|kody('php') }}
{{ source|kodyHighlight('php') }}
{{ 'npm run build'|kodyInline('shell') }}

craft.kody.languages(), craft.kody.highlightable() and craft.kody.canHighlight('rust') describe the registry.

Languages

Kody knows 49 languages. All of them edit with Monaco's colours, and 46 are also highlighted on the front end. The ones that carry most of the weight have hand-written grammars: Twig, HTML, CSS, SCSS, Less, JavaScript, TypeScript, JSON, PHP, YAML, SQL, Markdown, GraphQL, shell, Python, INI, Dockerfile and diff. A keyword-driven generic grammar handles the rest. CSV, log output and plain text are deliberately left unhighlighted, because guessing would be worse.

php craft kody/languages/list
php craft kody/languages/list --highlightable
php craft kody/languages/check ./src/Plugin.php

check reports what the highlighter found in a file: the token count, a breakdown by type, and how long it took.

Speed

A 9.5 KB, 288-line PHP file tokenises in about 6 ms. The result is cached by content hash, so the same snippet on a thousand pages is tokenised once. Anything over 256 KB is rendered as plain text rather than tokenised.

GraphQL

A Code field resolves to KodyCode:

{
  entries(section: "docs") {
    ... on docs_Entry {
      snippet {
        code
        language
        languageLabel
        lineCount
        characterCount
        highlighted
        html
      }
    }
  }
}

A headless front end can take code and highlight it itself, use highlighted (spans ready to go inside a <code>), or take html, the finished block. A Snippets field resolves to KodySnippet, with id, title, handle, description, language, code, highlighted and html.

Running code as Twig

A Code field can render its own contents as a Twig template:

{{ entry.snippet.eval({ products: products }) }}

The variables you pass are available in the code, along with entry and element.

This is off, and it stays off until two switches are on: Allow Twig rendering in the plugin settings, and the same setting on the field. Calling .eval() when either is off throws an exception. {{ entry.snippet.canEval() }} tells a template whether both switches are on.

Even with both switches on, full Twig can reach everything, craft.app.config.db.password included. So Kody only gives full Twig to code from people Craft already trusts with templates:

  • With Craft's Twig sandbox on (enableTwigSandbox, Craft 5.9+), the code runs sandboxed, whoever wrote it. Arithmetic, loops, filters and your variables work; reaching into craft.app doesn't.
  • Without the sandbox, the code only runs if the content was last saved by an admin. Kody asks the entry's current revision, or the draft being previewed. A Matrix block is asked about through the entry it belongs to. Content last saved by anyone else renders nothing, and Kody logs a warning that says why.
  • Elements Craft keeps no revisions for (categories, globals, users) can't prove who saved them, so they only run when the sandbox is on.

The safest way to turn the plugin-level switch on is config/kody.php, so that only a deploy can change it.

More in Development

Pairs well with Kody

For the developer on the project. Migrations in and between sites, a code editor field, schema docs, content inventories, reports and email testing.