BBEdit Declarative Language Modules

Declarative Language Modules describe lightweight language support for BBEdit using JSON. They are intended for language authors who want syntax coloring, filename mapping, function navigation, fold ranges, and optional language server settings without writing and signing compiled code.

For most authors, the recommended way to create a Declarative Language Module is to use BBEdit’s Language Playground. The playground provides an editable authoring document, sample text, live validation, and an export path for the runtime module.

File Formats

A Declarative Language Module may be either:

  • a single JSON file whose filename extension is .bbdlm; or
  • a package directory whose filename extension is .bbdlmd and which contains a top-level Language.json file.

Use the single-file .bbdlm form when the entire language description fits in one JSON document. Use the .bbdlmd package form when the language module needs supporting files, such as keyword lists or language server initialization options. Package resources live in the package’s Resources directory.

Canonical examples are in:

FrameworksAndLibraries/BBEdit Plug-ins/Declarative Examples/

ToyDeclarativeLanguage.bbdlm demonstrates the single-file form. PackagedDeclarativeLanguage.bbdlmd demonstrates the package form with resource files.

Language Playground Documents

A Language Playground document is an authoring document, not the runtime language module itself. Playground documents use the .bblpg filename extension and store the editable language-design state, including sample text and other UI state that does not belong in an installed language module.

Opening a .bblpg file opens the saved playground. Opening a .bbdlm, .bbdlmd, or legacy codeless language module imports that module into a new playground document. Saving the playground saves the authoring document. Exporting or installing from the playground writes the generated .bbdlm or .bbdlmd runtime module.

Required Top-Level Keys

Every DLM must include the document type key:

{
	"com.barebones.DocumentType": "DeclarativeLanguageModule"
}

Use com.barebones.DocumentType; do not use the older BBEditDocumentType key for new documents.

A minimal useful DLM looks like this:

{
	"com.barebones.DocumentType": "DeclarativeLanguageModule",
	"BBLMLanguageDescriptionVersion": 1,
	"BBLMLanguageDisplayName": "Example",
	"BBLMLanguageCode": "EXMP",
	"BBLMVersionString": "1.0",
	"BBLMBuildNumber": 1,
	"BBLMFilenameExtensions": ["ex"],
	"BBLMSyntaxRules": {
		"KeywordGroups": [
			{
				"RunKind": "com.barebones.bblm.keyword",
				"Keywords": ["if", "then", "else"]
			}
		]
	}
}

BBLMLanguageDescriptionVersion is the DLM schema version. It is not the version number of the language module. Version 1 describes every capability in this guide except template language flavors, which require version 2; see “Template Language Flavors” below.

Language Identity

BBLMLanguageDisplayName is the name shown in BBEdit’s language menus and preferences.

BBLMLanguageCode is the language’s four-character code. The Language Playground accepts shorter codes while editing and pads them with spaces when generating and validating the DLM. The placeholder code ???? is reserved for new Language Playground documents and is not valid in a loadable DLM.

BBLMFilenameExtensions is an array of filename extensions, without leading periods:

"BBLMFilenameExtensions": ["ex", "example"]

DLMs may also specify exact and wildcard filename matches:

"BBLMFilenameRules": {
	"ExactFileNames": ["Makefile"],
	"WildcardFileNames": ["Makefile.*"]
}

Exact filename matches are considered before extension matching. Wildcard filename matches are considered after extension matching.

Language Module Versioning

DLMs may include a marketing version and a build number:

"BBLMVersionString": "1.0",
"BBLMBuildNumber": 5

BBLMVersionString is the version shown to users. It may be x, x.y, or x.y.z; the Language Playground writes single-component versions as x.0.

BBLMBuildNumber is a non-negative integer. The Language Playground increments the existing build number each time it exports a generated DLM. BBEdit displays the two values together as MarketingVersion (BuildNumber), for example 1.0 (5).

Comment Defaults

DLMs may specify the preferred delimiters for BBEdit’s Un/Comment commands:

"BBLMCommentLineDefault": "//",
"BBLMCommentPrefixDefault": "/*",
"BBLMCommentSuffixDefault": "*/"

These keys are optional. If they are omitted, BBEdit may infer usable defaults from comment-like delimited runs.

Syntax Rules

Syntax rules live in the BBLMSyntaxRules dictionary.

Keyword Groups

KeywordGroups describes words or word patterns that should receive a particular syntax-coloring run kind.

A group may specify literal keywords:

{
	"RunKind": "com.barebones.bblm.keyword",
	"Keywords": ["if", "then", "else"]
}

A group may specify a PCRE keyword pattern:

{
	"RunKind": "com.barebones.bblm.keyword",
	"KeywordPattern": "d\\w+l"
}

Keyword patterns are applied to complete candidate word tokens using BBEdit’s PCRE engine. Keyword matching honors BBLMIsCaseSensitive.

A word listed in more than one group’s Keywords takes the RunKind of the first group that lists it, whether or not the language is case-sensitive. Validation reports a warning when groups with different run kinds list the same word.

For package DLMs, a keyword group may load keywords from a UTF-8 text file in the package’s Resources directory, one keyword per line:

{
	"RunKind": "com.barebones.bblm.keyword",
	"KeywordFileName": "Keywords.txt"
}

KeywordFileName must be a plain file name directly inside the Resources directory; a name with a path separator, a .. component, or an absolute path is rejected and cannot reference a file outside Resources.

If RunKind is omitted, BBEdit uses the standard language-keyword run kind.

Identifier Characters

By default, keyword lookup uses BBEdit’s normal word-character table: alphanumeric characters plus underscore. A DLM can override this with IdentifierAndKeywordCharacterClass. The value is PCRE character-class content, without surrounding square brackets:

"BBLMSyntaxRules": {
	"IdentifierAndKeywordCharacterClass": "[:alnum:]_\\.\\-"
}

Delimited Runs

DelimitedRuns describes syntax spans that begin with a literal delimiter and end at another literal delimiter or at the end of the line. Delimited runs are evaluated before pattern runs and keywords, in declaration order. At each position the first run whose start delimiter matches wins, so a run whose start delimiter begins with an earlier run’s (a """ string declared after a " string, say) can never match; validation reports such a run as a warning.

Line comments:

{
	"RunKind": "com.barebones.bblm.line-comment",
	"StartDelimiter": "//",
	"EndsAtLineBreak": true
}

Block comments:

{
	"RunKind": "com.barebones.bblm.block-comment",
	"StartDelimiter": "/*",
	"EndDelimiter": "*/"
}

Strings:

{
	"RunKind": "com.barebones.bblm.double-string",
	"StartDelimiter": "\"",
	"EndDelimiter": "\"",
	"Escape": "\\",
	"EndsAtLineBreak": true
}

Each delimited run must specify RunKind and StartDelimiter, and must either specify EndDelimiter or set EndsAtLineBreak to true.

The optional Escape string protects characters from the EndDelimiter in one of two ways. A conventional escape quotes the single character that follows it, so "Escape": "\\" lets \" appear inside the string above, and a trailing \ continues an EndsAtLineBreak run onto the next line. A doubled-delimiter escape spells the delimiter twice: for a SQL- or Pascal-style string, "EndDelimiter": "'" with "Escape": "''" treats each '' as one literal quote and ends the run only at a single '. Escape must differ from EndDelimiter.

Delimited run delimiters are matched literally and case-sensitively, regardless of BBLMIsCaseSensitive.

Pattern Runs

PatternRuns describes syntax spans found with PCRE patterns:

"PatternRuns": [
	{
		"RunKind": "com.barebones.bblm.number",
		"RunPattern": "\\b\\d+(?:\\.\\d+)?\\b"
	}
]

DelimitedRuns and PatternRuns may both be present. Delimited runs are evaluated first and protect their contents from pattern and keyword matching. If a DLM specifies the same RunKind in both DelimitedRuns and PatternRuns, validation reports a warning.

When matches from different pattern runs overlap, the leftmost match wins in its entirety, and every pattern resumes matching after it; when two matches begin at the same place, the pattern declared first wins. Matches from a single pattern never overlap one another, because each search resumes where that pattern’s previous match ended.

Symbol Rules

Symbol rules live in BBLMSymbolRules. The first supported form is a set of PCRE function patterns:

"BBLMSymbolRules": {
	"FunctionPatterns": [
		{
			"FunctionPattern": "(?m)^(?<function>func\\s+(?<function_name>[A-Za-z_]\\w*)\\s*\{[^}]*\})",
			"FunctionKind": "function",
			"GeneratesFold": true
		}
	]
}

FunctionPattern is required. It must include a named capture for the displayed symbol name. By default BBEdit looks for function_name. NameCapture may specify a different capture name:

{
	"FunctionPattern": "(?m)^def\\s+(?<name>[A-Za-z_]\\w*)",
	"NameCapture": "name"
}

FunctionCapture is optional. When present, it names the capture used for the function’s source range and for generated function folds. When omitted, BBEdit uses a function capture if the pattern provides one, or otherwise the whole match.

FunctionKind is optional and defaults to function. Recognized values include function, typedef, named-mark, include, system-include, site-relative-include, url-include, enum, struct, union, class, protocol, interface, implementation, ivar, method, property, property-synthesis, sgml-id, html-anchor, html-heading1 through html-heading6, and the callout kinds callout-fixme, callout-todo, callout-review, callout-question, callout-warning, and callout-note.

Function patterns use BBEdit’s PCRE engine and honor BBLMIsCaseSensitive. They are matched across the whole document, but a match counts only when it begins outside lexical DelimitedRuns: function-like text that starts inside a string or comment is ignored, while a function whose body contains a string or comment is still recognized.

When function scanning is active, BBEdit also examines comment delimited runs for callout comments. If the first non-whitespace text after the comment delimiter is a recognized callout tag followed by a colon or blank, BBEdit adds the remainder of that line to the function menu. Recognized tags include MARK, FIXME, FIX-ME, TODO, TO-DO, NYI, REVIEW, ???, !!!, NOTE, and XXX.

Fold Rules

Fold rules live in BBLMFoldRules. A DLM may request intrinsic delimiter folds:

"BBLMFoldRules": {
	"IntrinsicFolds": {
		"Parentheses": true,
		"Braces": true,
		"Brackets": true
	}
}

or explicitly declared delimiter pairs:

"BBLMFoldRules": {
	"PairedDelimiterFolds": [
		{
			"StartDelimiter": "begin",
			"EndDelimiter": "end",
			"FoldKind": "generic"
		}
	]
}

FoldKind is optional and defaults to generic. Recognized values are generic, module, class, function, data, block-comment, and here-doc.

Fold ranges are generated only for pairs whose range spans more than one line. A fold hides the content between the delimiters and leaves the delimiters themselves visible. Delimiter pairs are matched literally and case-sensitively, regardless of BBLMIsCaseSensitive. Lexical DelimitedRuns shield their contents, so braces inside strings or comments do not generate folds. Enabled intrinsic folds are tried before any paired delimiter fold, and the pairs in declaration order, so a pair whose start delimiter begins with an enabled intrinsic bracket or with an earlier pair’s start delimiter can never open. Nor can a pair whose start delimiter begins with a delimited run’s start delimiter, unless its delimiters are exactly that run’s own, because delimited runs are recognized first. Validation reports such a pair as a warning.

When the bounding delimiters are not fixed strings – for example CMake’s IF(...) / ENDIF(...), whose parenthesized text varies – a fold can be described by a pair of PCRE patterns instead:

"BBLMFoldRules": {
	"PatternDelimiterFolds": [
		{
			"StartPattern": "\\bIF\\s*\\([^)]*\\)",
			"EndPattern": "\\bENDIF\\s*\\([^)]*\\)",
			"FoldKind": "generic"
		}
	]
}

Each entry needs a non-empty StartPattern and EndPattern (and an optional FoldKind). A StartPattern match opens a fold that the next matching EndPattern match closes; folds nest like paired delimiters, and the fold spans from the end of the opening match to the start of the closing match (so both delimiters stay visible and only the content between them is hidden). These patterns honor BBLMIsCaseSensitive, so a case-insensitive language matches if/endif in any case. A delimiter match that begins inside a comment or string is ignored, and only multi-line ranges fold.

Template Language Flavors

A DLM may describe a template language hosted by HTML – a “flavor” – such as a Django or Twig template dialect. Documents in a flavor language are scanned as HTML, with the flavor’s template spans recognized everywhere they can appear: in element content, inside tags, and within attribute values. Everything outside a template span behaves exactly as it does in an HTML document, including coloring, folding, tag navigation, and the function menu.

Template flavors require description version 2:

"BBLMLanguageDescriptionVersion": 2

BBEdit versions which predate template flavor support decline to load version-2 modules rather than misinterpreting them, so use version 2 only when the module actually declares template rules.

Embedded Language Rules

A flavor declares its template syntax in the top-level BBLMEmbeddedLanguageRules dictionary:

"BBLMEmbeddedLanguageRules": {
	"HostLanguage": "HTML",
	"TemplateSpans": [
		{
			"Role": "expression",
			"StartDelimiter": "{{",
			"EndDelimiter": "}}"
		},
		{
			"Role": "statement",
			"StartDelimiter": "{%",
			"EndDelimiter": "%}"
		},
		{
			"Role": "comment",
			"StartDelimiter": "{#",
			"EndDelimiter": "#}"
		},
		{
			"Role": "verbatim",
			"StartDelimiter": "{% verbatim %}",
			"EndDelimiter": "{% endverbatim %}"
		}
	]
}

HostLanguage is required, and must be HTML.

TemplateSpans is a required, non-empty array of span families. Each family declares:

  • Role (required): one of expression, statement, comment, or verbatim. The role determines how the span colors and behaves; see below.
  • StartDelimiter and EndDelimiter (required): the literal delimiter strings. No two families may share a start delimiter. When one family’s start delimiter is a prefix of another’s, the longer delimiter wins, so a {% verbatim %} family takes precedence over a plain {% family.
  • TrimMarker (optional): a whitespace-control marker that may appear immediately after the start delimiter, such as the - in {%-. When present in the text, the marker colors as part of the delimiter.
  • RunKind (optional): a run-kind string that replaces the run kind the role would normally use – the interior run of an expression or statement span, or the whole single run of a comment span – for a flavor that needs its own vocabulary. A verbatim span’s interior is host content and has no run kind to replace.

How Spans Color

An expression or statement span colors as three runs: the delimiters color as template delimiters, and the interior colors as a template expression or template statement. The delimiter marks a transition into live template code, much as <?php does in a PHP document.

A comment span colors as one comment run, delimiters included – exactly as /* and */ are part of the comment in languages that use them.

A verbatim span’s delimiters color as template delimiters, but its interior remains host content: markup inside a verbatim block continues to color, fold, and navigate as markup, and template spans are not recognized there. Use the verbatim role for constructs like Django’s {% verbatim %}…{% endverbatim %}, whose meaning is “the template processor leaves this region alone.”

The four standard template run kinds – delimiter, expression, statement, and comment – have entries in BBEdit’s Text Colors preferences, so users can adjust their appearance for all flavors at once.

Literal Escapes

BBLMEmbeddedLanguageRules may declare a LiteralEscape string:

"LiteralEscape": "@{{"

Text matching the escape sequence is skipped before span recognition, so delimiters inside it do not open template spans. This serves template dialects with an escape idiom for writing a literal delimiter into the output.

Unterminated Spans

An expression, statement, or comment span with no closing delimiter ends at the first blank line (a line that is empty or contains only spaces and tabs), or after a bounded distance (currently 1024 characters), rather than extending to the end of the document. This is a deliberate difference from <?php-style processing islands: template delimiters are typed far more often, and coloring the rest of the document as template text on every unclosed {% would make editing unpleasant. A verbatim span with no closing delimiter extends to the end of the document, like an unclosed <script> element.

Filename Mapping for Flavors

A flavor may not claim any of the built-in HTML language’s filename extensions (html, htm, shtml, xhtml) – whether as filename extensions or via exact or wildcard filename rules; validation rejects the module with a diagnostic. To open .html files with a flavor, map the extension explicitly in BBEdit’s Languages preferences, which takes precedence over built-in mappings.

Symbols and Folds in Flavors

A flavor’s BBLMSymbolRules and BBLMFoldRules patterns are applied to the template text only; host markup is invisible to them. Patterns therefore match template constructs – {% block %}, {% macro %} – and cannot accidentally match markup. The host contributes its own symbols and folds for the markup, and the two sets merge: the function menu interleaves both in document order, and fold ranges from both layers nest. A pattern delimiter fold whose start and end patterns match template statements folds the entire region between them, including any markup in the interior.

Editing Behavior in Flavor Documents

A flavor’s documents are markup, and BBEdit’s markup-aware editing behavior applies to them as it does to an HTML document: tag-aware word motion, tag balancing, spell checking that understands markup, automatic pairing that knows the difference between element content and an attribute value, and tag and attribute completion. Entity punctuation counts as word characters, so double-clicking &nbsp; selects the whole entity.

Comment and Uncomment Lines use the flavor’s own comment-role span delimiters – {# and #} for a Django-style flavor – rather than the host’s <!-- -->. Both are legal in such a document; the template comment is the one the template processor removes, which is usually what is meant when commenting out template code. A flavor that prefers something else can say so explicitly with BBLMCommentPrefixDefault and BBLMCommentSuffixDefault, which take precedence.

Where Spans Are Not Recognized

Template spans are recognized in element content, inside tags, and within attribute values. They are deliberately not recognized in two places:

  • Inside a <script> or <style> element’s body. That text is handed to the JavaScript or CSS language module, which knows nothing of template spans, so template syntax there is colored as JavaScript or CSS – or not at all. Template code that generates script or style content is best kept outside those elements.
  • Inside an HTML comment. <!-- ... --> colors as one comment, whatever it contains. Note that a real template processor generally does execute template syntax inside an HTML comment, so a span there will still have its effect on the output even though BBEdit shows it as comment text.

A Limitation: “>” Inside a Span, Inside a Tag

Inside a tag – but outside a quoted attribute value – a template span whose content contains a literal > currently ends the tag at that character, exactly as a bare > would there. The span still colors, but the remainder of the tag is treated as content. Spans inside quoted attribute values are not affected, so the practical advice is to keep template code that uses > inside quoted values:

<div class="{% if a > b %}wide{% endif %}">   <!-- fine -->
<div {% if a > b %}data-wide{% endif %}>      <!-- ends the tag early -->

Flavors and the Language Playground

The Language Playground carries BBLMEmbeddedLanguageRules through import, saved playground documents, and export, but does not currently provide an editing interface for them: author the rules in the module’s JSON directly. Until the module is installed, the playground’s sample preview colors the flavor’s template spans over plain sample text; once it is installed, the preview – like document windows – shows full hosted-markup coloring.

The Django template module distributed with this guide is a worked example of the features described here. (Because the Django template language has no whitespace-control marker or literal-escape idiom of its own, that module does not exercise TrimMarker or LiteralEscape.)

Language Server Settings

DLMs may include optional language server settings in BBLMLanguageServerInfo:

"BBLMLanguageServerInfo": {
	"ServerCommand": "example-languageserver",
	"ServerArguments": ["--stdio"],
	"ServerLanguageID": "example",
	"ServerInitParams": {
		"diagnostics": true
	}
}

ServerCommand is the executable name to locate. ServerArguments is an array of command-line arguments. ServerLanguageID is the language ID sent to the server for opened documents. If it is omitted or empty, BBEdit uses the lowercased language display name with spaces replaced by hyphens.

ServerInitParams may be an inline dictionary:

"ServerInitParams": {
	"embeddedLanguages": {
		"javascript": true,
		"css": true
	}
}

For packaged DLMs, ServerInitParams may also be a string naming a JSON file in the package’s Resources directory:

"ServerInitParams": "LSPInitParams.json"

Like KeywordFileName, a string ServerInitParams must be a plain file name directly inside the Resources directory and cannot use a path separator, a .. component, or an absolute path to reach a file outside it.

Single-file DLMs do not have a Resources directory, so they should use the inline dictionary form.

Language module initialization options can be supplemented or overridden by user-specified language server configuration files in the user’s Language Servers/Configuration directory.

Legacy Codeless Language Modules

BBEdit continues to support legacy codeless language modules. Opening a legacy codeless language module imports it into a Language Playground document, where you can inspect, adjust, and export the generated DLM.

When there is a direct equivalent, BBEdit promotes legacy codeless declarations into native DLM rules, including keyword lists and keyword patterns, comment/string/number PCRE patterns, simple delimited comments and strings, function patterns, identifier character classes, filename rules, and language server settings.

Parser Model

BBLMParserModel is normally omitted from hand-authored DLMs. BBEdit infers the appropriate runtime behavior from the rest of the description. If you need to include the key, use:

"BBLMParserModel": "auto"

Other parser model values are implementation details and should not be used in new language modules.

Back to Technical Notes