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.
A Declarative Language Module may be either:
.bbdlm; or.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.
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.
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.
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.
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).
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 live in the BBLMSyntaxRules dictionary.
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.
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:]_\\.\\-"
}
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.
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 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 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.
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.
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.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.
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.
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.
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.
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.
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 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.
Template spans are recognized in element content, inside tags, and within attribute values. They are deliberately not recognized in two places:
<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.<!-- ... --> 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.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 -->
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.)
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.
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.
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.