Editor Support

Syntax highlighting for .stpl files. Covers ScribeEngine decorators, {$ $} Python blocks, and Jinja2 expressions.

Without highlighting
@route('/tasks')
@require_auth
{$
# load open tasks
tasks = db['default'].query(
    "SELECT title, due FROM tasks "
    "WHERE user_id = ?",
    [session['user_id']]
)
$}

<h1>My Tasks</h1>
{% for task in tasks %}
<li>{{ task.title }}</li>
{% endfor %}
With highlighting
@route('/tasks')
@require_auth
{$
# load open tasks
tasks = db['default'].query(
    "SELECT title, due FROM tasks "
    "WHERE user_id = ?",
    [session['user_id']]
)
$}

<h1>My Tasks</h1>
{% for task in tasks %}
<li>{{ task.title }}</li>
{% endfor %}

Neovim / Vim

Copy stpl.vim into your Vim syntax directory, then tell Vim to use it for .stpl files.

1. Copy the syntax file

# Neovim
mkdir -p ~/.config/nvim/syntax
cp stpl.vim ~/.config/nvim/syntax/stpl.vim

# Vim
mkdir -p ~/.vim/syntax
cp stpl.vim ~/.vim/syntax/stpl.vim

2. Register the filetype

Add this line to your init.vim or .vimrc:

au BufRead,BufNewFile *.stpl set filetype=stpl

Highlights: @route / @require_auth / @no_layout / @sse decorators, {$ $} Python blocks, HTML, and Jinja2.

stpl.vim

" Quit when a syntax file was already loaded
if exists("b:current_syntax")
  finish
endif

" 1. Load HTML + Jinja2 (htmldjango is built-in and covers both)
runtime! syntax/htmldjango.vim
unlet! b:current_syntax

" 2. Safely include standard Python syntax
syntax include @Python syntax/python.vim

" 3. Define your custom Python block {$ ... $}
" matchgroup colors the {$ and $} delimiters differently from the code inside
syntax region stplPythonBlock matchgroup=stplDelimiter start="{\$" end="\$}" keepend contains=@Python

" 4. Define the custom @route decorator
syntax match stplRoute "^\s*@route\s*(.*)"

" 5. Link our custom elements to standard highlight groups
hi def link stplDelimiter   Delimiter
hi def link stplRoute       Macro

let b:current_syntax = "stpl"

VS Code (and forks)

Install as a local extension. Works in VS Code, VS Codium, Cursor, and any VS Code fork.

Install

  1. Create the folder ~/.vscode/extensions/ScribeFramework.vscode-stpl/ (use ~/.vscodium/extensions/ for VS Codium)
  2. Copy all three files below into that folder, then move stpl.tmLanguage.json to a syntaxes/ subfolder
  3. Restart VS Code — any .stpl file will now have syntax highlighting
Extension layout
~/.vscode/extensions/vscode-stpl/
  ├── package.json
  ├── language-configuration.json
  └── syntaxes/
      └── stpl.tmLanguage.json

Highlights: @route(...) and other ScribeEngine decorators, {$ $} Python blocks with full Python token coloring, Jinja2 {% %} / {{ }} / {# #}, and HTML.

Files

vscode-stpl/package.json
{
  "name": "vscode-stpl",
  "displayName": "Scribe Template (.stpl)",
  "description": "Syntax highlighting for ScribeEngine .stpl template files",
  "publisher": "ScribeFramework",
  "version": "0.1.0",
  "engines": {
    "vscode": "^1.80.0"
  },
  "categories": ["Programming Languages"],
  "contributes": {
    "languages": [
      {
        "id": "stpl",
        "aliases": ["Scribe Template", "stpl"],
        "extensions": [".stpl"],
        "configuration": "./language-configuration.json"
      }
    ],
    "grammars": [
      {
        "language": "stpl",
        "scopeName": "text.html.stpl",
        "path": "./syntaxes/stpl.tmLanguage.json",
        "embeddedLanguages": {
          "source.python": "python",
          "text.html.basic": "html"
        }
      }
    ]
  }
}
vscode-stpl/syntaxes/stpl.tmLanguage.json
{
  "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
  "name": "Scribe Template",
  "scopeName": "text.html.stpl",
  "fileTypes": ["stpl"],
  "patterns": [
    { "include": "#scribe-decorator" },
    { "include": "#python-block" },
    { "include": "#jinja-comment" },
    { "include": "#jinja-block" },
    { "include": "#jinja-variable" },
    { "include": "text.html.basic" }
  ],
  "repository": {
    "scribe-decorator": {
      "comment": "ScribeEngine route decorators: @route, @require_auth, @no_layout, @sse",
      "patterns": [
        {
          "name": "meta.function.decorator.stpl",
          "match": "^(@(?:route|require_auth|no_layout|sse))\\b(\\s*\\(.*\\))?",
          "captures": {
            "1": { "name": "entity.name.function.decorator.stpl" },
            "2": { "name": "meta.function.decorator.arguments.stpl" }
          }
        }
      ]
    },
    "python-block": {
      "name": "meta.embedded.block.python.stpl",
      "begin": "\\{\\$",
      "end": "\\$\\}",
      "beginCaptures": {
        "0": { "name": "punctuation.definition.template-expression.begin.stpl" }
      },
      "endCaptures": {
        "0": { "name": "punctuation.definition.template-expression.end.stpl" }
      },
      "contentName": "source.python",
      "patterns": [
        { "include": "source.python" }
      ]
    },
    "jinja-comment": {
      "name": "comment.block.jinja.stpl",
      "begin": "\\{#",
      "end": "#\\}",
      "beginCaptures": {
        "0": { "name": "punctuation.definition.comment.begin.jinja" }
      },
      "endCaptures": {
        "0": { "name": "punctuation.definition.comment.end.jinja" }
      }
    },
    "jinja-block": {
      "name": "meta.embedded.block.jinja.stpl",
      "begin": "\\{%-?",
      "end": "-?%\\}",
      "beginCaptures": {
        "0": { "name": "punctuation.definition.tag.begin.jinja" }
      },
      "endCaptures": {
        "0": { "name": "punctuation.definition.tag.end.jinja" }
      },
      "patterns": [
        { "include": "#jinja-keywords" },
        { "include": "#jinja-string" },
        { "include": "#jinja-number" },
        { "include": "#jinja-constant" }
      ]
    },
    "jinja-variable": {
      "name": "meta.embedded.expression.jinja.stpl",
      "begin": "\\{\\{",
      "end": "\\}\\}",
      "beginCaptures": {
        "0": { "name": "punctuation.definition.expression.begin.jinja" }
      },
      "endCaptures": {
        "0": { "name": "punctuation.definition.expression.end.jinja" }
      },
      "patterns": [
        { "include": "#jinja-string" },
        { "include": "#jinja-number" },
        { "include": "#jinja-constant" },
        { "include": "#jinja-filter" }
      ]
    },
    "jinja-keywords": {
      "name": "keyword.control.jinja",
      "match": "\\b(if|elif|else|endif|for|endfor|block|endblock|extends|include|macro|endmacro|call|endcall|filter|endfilter|set|do|not|and|or|in|is|import|from|with|without|context|ignore|missing|recursive|loop|super|raw|endraw)\\b"
    },
    "jinja-string": {
      "patterns": [
        {
          "name": "string.quoted.double.jinja",
          "begin": "\"",
          "end": "\"",
          "patterns": [{ "name": "constant.character.escape.jinja", "match": "\\\\." }]
        },
        {
          "name": "string.quoted.single.jinja",
          "begin": "'",
          "end": "'",
          "patterns": [{ "name": "constant.character.escape.jinja", "match": "\\\\." }]
        }
      ]
    },
    "jinja-number": {
      "name": "constant.numeric.jinja",
      "match": "\\b[0-9]+(\\.[0-9]+)?\\b"
    },
    "jinja-constant": {
      "name": "constant.language.jinja",
      "match": "\\b(true|false|none|True|False|None)\\b"
    },
    "jinja-filter": {
      "match": "\\|\\s*([a-zA-Z_][a-zA-Z0-9_]*)",
      "captures": {
        "1": { "name": "support.function.jinja" }
      }
    }
  }
}
vscode-stpl/language-configuration.json
{
  "comments": {
    "blockComment": ["{#", "#}"]
  },
  "brackets": [
    ["{", "}"],
    ["[", "]"],
    ["(", ")"]
  ],
  "autoClosingPairs": [
    { "open": "{{", "close": "}}" },
    { "open": "{%", "close": "%}" },
    { "open": "{#", "close": "#}" },
    { "open": "{", "close": "}" },
    { "open": "[", "close": "]" },
    { "open": "(", "close": ")" },
    { "open": "\"", "close": "\"", "notIn": ["string"] },
    { "open": "'", "close": "'", "notIn": ["string"] }
  ],
  "surroundingPairs": [
    ["{", "}"],
    ["[", "]"],
    ["(", ")"],
    ["\"", "\""],
    ["'", "'"],
    ["<", ">"]
  ],
  "wordPattern": "[a-zA-Z_][a-zA-Z0-9_]*"
}