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 |
.code | The source as a string |
.language | A Language: .id, .label, .extension() |
.lines, .lineCount, .characterCount | |
.filename | code. 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, .isValidJson | For fields holding JSON. .json is null when it doesn't parse |
.isEmpty, .isNotEmpty | Whitespace-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 | |
|---|---|
language | Override the stored language |
lineNumbers | Line numbers are CSS counters, so copying never picks them up |
startLine | The 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 |
wrap | Soft-wrap instead of scrolling sideways |
maxHeight | One CSS length. Taller blocks scroll. Anything else is ignored |
copyButton | The only JavaScript Kody puts on a page |
filename | Shown in the caption |
caption | Free text for the caption |
showLanguage | The language name in the caption |
theme | auto (follows the visitor's colour scheme), light or dark |
tabSize | |
dedent | Strip the indentation every line shares |
trim | Drop blank leading and trailing lines |
class, id | Applied 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 intocraft.appdoesn'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.