This commit is contained in:
lukeod 2025-12-20 08:31:13 +01:00 committed by GitHub
commit d60ab7c791
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 752 additions and 0 deletions

View file

@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerLanguage } from '../_.contribution';
declare var AMD: any;
declare var require: any;
registerLanguage({
id: 'jinja',
extensions: ['.jinja', '.j2'],
aliases: ['Jinja', 'Jinja2', 'jinja'],
mimetypes: ['text/jinja', 'text/x-jinja-template'],
loader: () => {
if (AMD) {
return new Promise((resolve, reject) => {
require(['vs/basic-languages/jinja/jinja'], resolve, reject);
});
} else {
return import('./jinja');
}
}
});

View file

@ -0,0 +1,434 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { testTokenization } from '../test/testRunner';
testTokenization('jinja', [
// Comments
[
{
line: '{# This is a comment #}',
tokens: [{ startIndex: 0, type: 'comment.block.jinja' }]
},
{
line: 'Some text {#- comment -#} More text',
tokens: [
{ startIndex: 0, type: '' }, // Some text
{ startIndex: 10, type: 'comment.block.jinja' }, // {#- comment -#}
{ startIndex: 25, type: '' } // More text (Adjusted expectation)
]
}
],
// Variables
[
{
line: '{{ variable_name }}',
tokens: [
{ startIndex: 0, type: 'delimiter.variable.jinja' }, // {{
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'variable.other.jinja' }, // variable_name
{ startIndex: 16, type: 'white.jinja' }, // Corrected index
{ startIndex: 17, type: 'delimiter.variable.jinja' } // Corrected index
]
},
{
line: '{{- variable | filter -}}',
tokens: [
{ startIndex: 0, type: 'delimiter.variable.jinja' }, // {{-
{ startIndex: 3, type: 'white.jinja' },
{ startIndex: 4, type: 'variable.other.jinja' }, // variable
{ startIndex: 12, type: 'white.jinja' },
{ startIndex: 13, type: 'operators.filter.jinja' }, // |
{ startIndex: 14, type: 'white.jinja' },
{ startIndex: 15, type: 'variable.other.filter.jinja' }, // filter
{ startIndex: 21, type: 'white.jinja' },
{ startIndex: 22, type: 'delimiter.variable.jinja' } // -}}
]
}
],
// Blocks
[
{
line: '{% if condition %}',
tokens: [
{ startIndex: 0, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'keyword.control.jinja' }, // if
{ startIndex: 5, type: 'white.jinja' },
{ startIndex: 6, type: 'variable.other.jinja' }, // condition
{ startIndex: 15, type: 'white.jinja' },
{ startIndex: 16, type: 'delimiter.tag.jinja' } // %}
]
},
{
line: '{% set my_var = "value" %}',
tokens: [
{ startIndex: 0, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'keyword.control.jinja' }, // set
{ startIndex: 6, type: 'white.jinja' },
{ startIndex: 7, type: 'variable.other.jinja' }, // my_var
{ startIndex: 13, type: 'white.jinja' },
{ startIndex: 14, type: 'keyword.operator.jinja' }, // =
{ startIndex: 15, type: 'white.jinja' },
{ startIndex: 16, type: 'string.quote.double.jinja' }, // "
{ startIndex: 17, type: 'string.jinja' }, // value
{ startIndex: 22, type: 'string.quote.double.jinja' }, // "
{ startIndex: 23, type: 'white.jinja' },
{ startIndex: 24, type: 'delimiter.tag.jinja' } // %}
]
}
],
// Raw Block
[
{
line: '{% raw %}This {{ is not processed }} {% endraw %}',
tokens: [
// Actual tokens produced by the simpler tokenizer rules:
// Adjusted to match actual output reported by test runner
{ startIndex: 0, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 3, type: 'keyword.control.jinja' }, // raw
{ startIndex: 6, type: 'delimiter.tag.jinja' }, // %}
{ startIndex: 9, type: 'comment.block.raw.jinja' }, // This {{ is not processed }}
{ startIndex: 37, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 40, type: 'keyword.control.jinja' }, // endraw
{ startIndex: 46, type: 'delimiter.tag.jinja' } // %}
]
}
],
// Strings and Numbers within expressions
[
{
line: "{{ 'string' + 123 }}",
tokens: [
{ startIndex: 0, type: 'delimiter.variable.jinja' }, // {{
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'string.quote.single.jinja' }, // '
{ startIndex: 4, type: 'string.jinja' }, // string
{ startIndex: 10, type: 'string.quote.single.jinja' }, // '
{ startIndex: 11, type: 'white.jinja' },
{ startIndex: 12, type: 'keyword.operator.jinja' }, // +
{ startIndex: 13, type: 'white.jinja' },
{ startIndex: 14, type: 'number.jinja' }, // 123
{ startIndex: 17, type: 'white.jinja' },
{ startIndex: 18, type: 'delimiter.variable.jinja' } // }}
]
}
],
// For loop with loop variable and else
[
{
line: '{% for item in items %}{{ loop.index }}: {{ item }}{% else %}No items.{% endfor %}',
tokens: [
{ startIndex: 0, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'keyword.control.jinja' }, // for
{ startIndex: 6, type: 'white.jinja' },
{ startIndex: 7, type: 'variable.other.jinja' }, // item
{ startIndex: 11, type: 'white.jinja' },
{ startIndex: 12, type: 'keyword.control.jinja' }, // in
{ startIndex: 14, type: 'white.jinja' },
{ startIndex: 15, type: 'variable.other.jinja' }, // items
{ startIndex: 20, type: 'white.jinja' },
{ startIndex: 21, type: 'delimiter.tag.jinja' }, // %}
{ startIndex: 23, type: 'delimiter.variable.jinja' }, // {{
{ startIndex: 25, type: 'white.jinja' },
{ startIndex: 26, type: 'variable.language.jinja' }, // loop
{ startIndex: 30, type: 'delimiter.accessor.jinja' }, // .
{ startIndex: 31, type: 'variable.other.jinja' }, // index
{ startIndex: 36, type: 'white.jinja' },
{ startIndex: 37, type: 'delimiter.variable.jinja' }, // }}
{ startIndex: 39, type: '' }, // ': ' (colon and space are plain text)
{ startIndex: 41, type: 'delimiter.variable.jinja' }, // {{ (starts at index 41 now)
{ startIndex: 43, type: 'white.jinja' },
{ startIndex: 44, type: 'variable.other.jinja' }, // item
{ startIndex: 48, type: 'white.jinja' },
{ startIndex: 49, type: 'delimiter.variable.jinja' }, // }}
{ startIndex: 51, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 53, type: 'white.jinja' },
{ startIndex: 54, type: 'keyword.control.jinja' }, // else
{ startIndex: 58, type: 'white.jinja' },
{ startIndex: 59, type: 'delimiter.tag.jinja' }, // %}
{ startIndex: 61, type: '' }, // No items.
{ startIndex: 70, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 72, type: 'white.jinja' },
{ startIndex: 73, type: 'keyword.control.jinja' }, // endfor
{ startIndex: 79, type: 'white.jinja' },
{ startIndex: 80, type: 'delimiter.tag.jinja' } // %}
]
}
],
// Complex Expressions: attr access, subscript, func call, comparison, logic, test
[
{
line: "{{ obj.attr + my_dict['key'] | func(1 > 0 and not False) is defined }}",
tokens: [
{ startIndex: 0, type: 'delimiter.variable.jinja' }, // {{
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'variable.other.jinja' }, // obj
{ startIndex: 6, type: 'delimiter.accessor.jinja' }, // .
{ startIndex: 7, type: 'variable.other.jinja' }, // attr
{ startIndex: 11, type: 'white.jinja' },
{ startIndex: 12, type: 'keyword.operator.jinja' }, // +
{ startIndex: 13, type: 'white.jinja' },
{ startIndex: 14, type: 'variable.other.jinja' }, // my_dict
{ startIndex: 21, type: 'delimiter.jinja' }, // [
{ startIndex: 22, type: 'string.quote.single.jinja' }, // '
{ startIndex: 23, type: 'string.jinja' }, // key
{ startIndex: 26, type: 'string.quote.single.jinja' }, // '
{ startIndex: 27, type: 'delimiter.jinja' }, // ]
{ startIndex: 28, type: 'white.jinja' },
{ startIndex: 29, type: 'operators.filter.jinja' }, // |
{ startIndex: 30, type: 'white.jinja' },
{ startIndex: 31, type: 'variable.other.filter.jinja' }, // func
{ startIndex: 35, type: 'delimiter.jinja' }, // (
{ startIndex: 36, type: 'number.jinja' }, // 1
{ startIndex: 37, type: 'white.jinja' },
{ startIndex: 38, type: 'keyword.operator.jinja' }, // >
{ startIndex: 39, type: 'white.jinja' },
{ startIndex: 40, type: 'number.jinja' }, // 0
{ startIndex: 41, type: 'white.jinja' },
{ startIndex: 42, type: 'keyword.control.jinja' }, // and
{ startIndex: 45, type: 'white.jinja' },
{ startIndex: 46, type: 'keyword.control.jinja' }, // not
{ startIndex: 49, type: 'white.jinja' },
{ startIndex: 50, type: 'constant.language.jinja' }, // False
{ startIndex: 55, type: 'delimiter.jinja' }, // )
{ startIndex: 56, type: 'white.jinja' },
{ startIndex: 57, type: 'keyword.control.jinja' }, // is
{ startIndex: 59, type: 'white.jinja' },
{ startIndex: 60, type: 'variable.other.jinja' }, // defined (common test treated as variable here, which is acceptable)
{ startIndex: 67, type: 'white.jinja' },
{ startIndex: 68, type: 'delimiter.variable.jinja' } // }}
]
}
],
// Block and Extends
[
{
line: '{% extends "base.html" %}',
tokens: [
{ startIndex: 0, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'keyword.control.jinja' }, // extends
{ startIndex: 10, type: 'white.jinja' },
{ startIndex: 11, type: 'string.quote.double.jinja' }, // "
{ startIndex: 12, type: 'string.jinja' }, // base.html
{ startIndex: 21, type: 'string.quote.double.jinja' }, // "
{ startIndex: 22, type: 'white.jinja' },
{ startIndex: 23, type: 'delimiter.tag.jinja' } // %}
]
},
{
line: '{% block content %} Content {% endblock %}',
tokens: [
{ startIndex: 0, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'keyword.control.jinja' }, // block
{ startIndex: 8, type: 'white.jinja' },
{ startIndex: 9, type: 'variable.other.jinja' }, // content
{ startIndex: 16, type: 'white.jinja' },
{ startIndex: 17, type: 'delimiter.tag.jinja' }, // %}
{ startIndex: 19, type: '' }, // Content
{ startIndex: 28, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 30, type: 'white.jinja' },
{ startIndex: 31, type: 'keyword.control.jinja' }, // endblock
{ startIndex: 39, type: 'white.jinja' },
{ startIndex: 40, type: 'delimiter.tag.jinja' } // %}
]
}
],
// Macro definition and call
[
{
line: '{% macro input(name, value) %}<input name="{{ name }}">{% endmacro %}',
tokens: [
{ startIndex: 0, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'keyword.control.jinja' }, // macro
{ startIndex: 8, type: 'white.jinja' },
{ startIndex: 9, type: 'variable.other.jinja' }, // input
{ startIndex: 14, type: 'delimiter.jinja' }, // (
{ startIndex: 15, type: 'variable.other.jinja' }, // name
{ startIndex: 19, type: 'delimiter.jinja' }, // ,
{ startIndex: 20, type: 'white.jinja' },
{ startIndex: 21, type: 'variable.other.jinja' }, // value
{ startIndex: 26, type: 'delimiter.jinja' }, // )
{ startIndex: 27, type: 'white.jinja' },
{ startIndex: 28, type: 'delimiter.tag.jinja' }, // %}
{ startIndex: 30, type: '' }, // <input name="
{ startIndex: 43, type: 'delimiter.variable.jinja' }, // {{
{ startIndex: 45, type: 'white.jinja' },
{ startIndex: 46, type: 'variable.other.jinja' }, // name
{ startIndex: 50, type: 'white.jinja' },
{ startIndex: 51, type: 'delimiter.variable.jinja' }, // }}
{ startIndex: 53, type: '' }, // ">
{ startIndex: 55, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 57, type: 'white.jinja' },
{ startIndex: 58, type: 'keyword.control.jinja' }, // endmacro
{ startIndex: 66, type: 'white.jinja' },
{ startIndex: 67, type: 'delimiter.tag.jinja' } // %}
]
},
{
line: '{{ mymacros.input("user") }}',
tokens: [
{ startIndex: 0, type: 'delimiter.variable.jinja' }, // {{
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'variable.other.jinja' }, // mymacros
{ startIndex: 11, type: 'delimiter.accessor.jinja' }, // .
{ startIndex: 12, type: 'variable.other.jinja' }, // input
{ startIndex: 17, type: 'delimiter.jinja' }, // (
{ startIndex: 18, type: 'string.quote.double.jinja' }, // "
{ startIndex: 19, type: 'string.jinja' }, // user
{ startIndex: 23, type: 'string.quote.double.jinja' }, // "
{ startIndex: 24, type: 'delimiter.jinja' }, // )
{ startIndex: 25, type: 'white.jinja' },
{ startIndex: 26, type: 'delimiter.variable.jinja' } // }}
]
}
],
// String Escapes
[
{
// Test escapes on a single line
line: '{{ "World \\"Quote\\" \\\\ Backslash" }}',
tokens: [
{ startIndex: 0, type: 'delimiter.variable.jinja' }, // {{
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'string.quote.double.jinja' }, // "
{ startIndex: 4, type: 'string.jinja' }, // World
{ startIndex: 10, type: 'constant.character.escape.jinja' }, // \"
{ startIndex: 12, type: 'string.jinja' }, // Quote
{ startIndex: 17, type: 'constant.character.escape.jinja' }, // \"
{ startIndex: 19, type: 'string.jinja' }, //
{ startIndex: 20, type: 'constant.character.escape.jinja' }, // \\
{ startIndex: 22, type: 'string.jinja' }, // Backslash
{ startIndex: 32, type: 'string.quote.double.jinja' }, // "
{ startIndex: 33, type: 'white.jinja' },
{ startIndex: 34, type: 'delimiter.variable.jinja' } // }}
]
}
],
// Constants
[
{
line: '{{ True and false or None }}',
tokens: [
{ startIndex: 0, type: 'delimiter.variable.jinja' },
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'constant.language.jinja' },
{ startIndex: 7, type: 'white.jinja' },
{ startIndex: 8, type: 'keyword.control.jinja' },
{ startIndex: 11, type: 'white.jinja' },
{ startIndex: 12, type: 'constant.language.jinja' },
{ startIndex: 17, type: 'white.jinja' },
{ startIndex: 18, type: 'keyword.control.jinja' },
{ startIndex: 20, type: 'white.jinja' },
{ startIndex: 21, type: 'constant.language.jinja' },
{ startIndex: 25, type: 'white.jinja' },
{ startIndex: 26, type: 'delimiter.variable.jinja' }
]
}
],
// Filter block
[
{
line: '{% filter upper %}Text{% endfilter %}',
tokens: [
{ startIndex: 0, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'keyword.control.jinja' }, // filter
{ startIndex: 9, type: 'white.jinja' },
{ startIndex: 10, type: 'variable.other.jinja' }, // upper (filter name treated as variable here)
{ startIndex: 15, type: 'white.jinja' },
{ startIndex: 16, type: 'delimiter.tag.jinja' }, // %}
{ startIndex: 18, type: '' }, // Text
{ startIndex: 22, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 24, type: 'white.jinja' },
{ startIndex: 25, type: 'keyword.control.jinja' }, // endfilter
{ startIndex: 34, type: 'white.jinja' },
{ startIndex: 35, type: 'delimiter.tag.jinja' } // %}
]
}
],
// Include and Import
[
{
line: '{% include "partial.html" %}',
tokens: [
{ startIndex: 0, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'keyword.control.jinja' }, // include
{ startIndex: 10, type: 'white.jinja' },
{ startIndex: 11, type: 'string.quote.double.jinja' }, // "
{ startIndex: 12, type: 'string.jinja' }, // partial.html
{ startIndex: 24, type: 'string.quote.double.jinja' }, // "
{ startIndex: 25, type: 'white.jinja' },
{ startIndex: 26, type: 'delimiter.tag.jinja' } // %}
]
},
{
line: '{% import "macros.jinja" as forms %}',
tokens: [
{ startIndex: 0, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'keyword.control.jinja' }, // import
{ startIndex: 9, type: 'white.jinja' },
{ startIndex: 10, type: 'string.quote.double.jinja' }, // "
{ startIndex: 11, type: 'string.jinja' }, // macros.jinja
{ startIndex: 23, type: 'string.quote.double.jinja' }, // "
{ startIndex: 24, type: 'white.jinja' },
{ startIndex: 25, type: 'keyword.control.jinja' }, // as
{ startIndex: 27, type: 'white.jinja' },
{ startIndex: 28, type: 'variable.other.jinja' }, // forms
{ startIndex: 33, type: 'white.jinja' },
{ startIndex: 34, type: 'delimiter.tag.jinja' } // %}
]
}
],
// With block
[
{
line: '{% with var = 42 %}{{ var }}{% endwith %}',
tokens: [
{ startIndex: 0, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 2, type: 'white.jinja' },
{ startIndex: 3, type: 'keyword.control.jinja' }, // with
{ startIndex: 7, type: 'white.jinja' },
{ startIndex: 8, type: 'variable.other.jinja' }, // var
{ startIndex: 11, type: 'white.jinja' },
{ startIndex: 12, type: 'keyword.operator.jinja' }, // =
{ startIndex: 13, type: 'white.jinja' },
{ startIndex: 14, type: 'number.jinja' }, // 42
{ startIndex: 16, type: 'white.jinja' },
{ startIndex: 17, type: 'delimiter.tag.jinja' }, // %}
{ startIndex: 19, type: 'delimiter.variable.jinja' }, // {{
{ startIndex: 21, type: 'white.jinja' },
{ startIndex: 22, type: 'variable.other.jinja' }, // var
{ startIndex: 25, type: 'white.jinja' },
{ startIndex: 26, type: 'delimiter.variable.jinja' }, // }}
{ startIndex: 28, type: 'delimiter.tag.jinja' }, // {%
{ startIndex: 30, type: 'white.jinja' },
{ startIndex: 31, type: 'keyword.control.jinja' }, // endwith
{ startIndex: 38, type: 'white.jinja' },
{ startIndex: 39, type: 'delimiter.tag.jinja' } // %}
]
}
]
]);

View file

@ -0,0 +1,269 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { languages } from '../../fillers/monaco-editor-core';
// Language Configuration for Jinja
export const conf: languages.LanguageConfiguration = {
comments: {
blockComment: ['{#', '#}']
},
brackets: [
['{%', '%}'],
['{{', '}}'],
['{#', '#}'],
['(', ')'],
['[', ']'],
['{', '}']
// Note: Whitespace control variants like {%-, -%} are part of the token, not separate brackets
],
autoClosingPairs: [
{ open: '{#', close: ' #}' },
{ open: '{%', close: ' %}' },
{ open: '{{', close: ' }}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '{', close: '}' },
{ open: '"', close: '"', notIn: ['string', 'comment'] },
{ open: "'", close: "'", notIn: ['string', 'comment'] }
// Whitespace control pairs might be tricky here, stick to standard for reliable auto-closing
],
surroundingPairs: [
{ open: '"', close: '"' },
{ open: "'", close: "'" },
{ open: '(', close: ')' },
{ open: '[', close: ']' },
{ open: '{', close: '}' },
{ open: '{%', close: '%}' },
{ open: '{{', close: '}}' },
{ open: '{#', close: '#}' }
],
// Add folding markers based on TextMate grammar
folding: {
markers: {
start: new RegExp('^\\s*({%\\s*(block|filter|for|if|macro|raw))'), // Matches start tags
end: new RegExp('^\\s*({%\\s*(endblock|endfilter|endfor|endif|endmacro|endraw)\\s*%})') // Matches end tags
}
},
indentationRules: {
increaseIndentPattern: new RegExp(
'^\\s*({%\\s*(block|filter|for|if|macro|raw|with|autoescape)\\b(?!.*\\b(endblock|endfilter|endfor|endif|endmacro|endraw|endwith|endautoescape))[^%]*%})'
),
decreaseIndentPattern: new RegExp(
'^\\s*({%\\s*(elif|else|endblock|endfilter|endfor|endif|endmacro|endraw|endwith|endautoescape)\\b.*?%})'
)
}
};
// Monarch Tokenizer Definition for Jinja
export const language = <languages.IMonarchLanguage>{
defaultToken: '', // Default to no specific token, avoid 'invalid' spam
tokenPostfix: '.jinja',
keywords: [
// Control Structures
'if',
'endif',
'for',
'endfor',
'block',
'endblock',
'extends',
'include',
'import',
'from',
'as',
'recursive',
'macro',
'endmacro',
'call',
'endcall',
'filter',
'endfilter',
'set',
'endset',
'raw',
'endraw',
'with',
'endwith',
'autoescape',
'endautoescape',
// Jinja specific keywords often used within tags
'scoped',
'required',
'ignore',
'missing',
'context', // Modifiers for include/import/block
'trimmed',
'notrimmed',
'pluralize', // i18n extension
'continue',
'break', // loop controls extension
'do', // do extension
// Expressions/Logic
'and',
'or',
'not',
'in',
'is',
'else',
'elif'
// Note: true, false, none, loop, super, self, varargs, kwargs are handled in tokenizer
],
operators: [
'+',
'-',
'*',
'**',
'/',
'//',
'%', // Arithmetic
'==',
'<=',
'>=',
'<',
'>',
'!=', // Comparison
'=', // Assignment
'|', // Filter pipe
'~' // Concatenation
// 'and', 'or', 'not', 'in', 'is' are keywords but act as operators
],
// Symbols used for operators, delimiters etc. - simplified as specific tokens are better
symbols: /[=><!~?&|+\-*/^%]+/,
// Common Jinja constants and special variables
constants: ['true', 'false', 'none', 'True', 'False', 'None'], // Allow title case for compatibility
specialVars: ['loop', 'super', 'self', 'varargs', 'kwargs', 'caller'], // Added caller
// Modified escapes regex (removed \\, \", \')
escapes: /\\(?:[abfnrtv]|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8}|N\{[a-zA-Z ]+\})/,
tokenizer: {
root: [
// Match Jinja delimiters first
// Comments: {# ... #}
[/\{#-+/, { token: 'comment.block', bracket: '@open', next: '@comment' }], // With whitespace control
[/\{#/, { token: 'comment.block', bracket: '@open', next: '@comment' }], // Standard
// Variables: {{ ... }}
[/\{\{-+/, { token: 'delimiter.variable', bracket: '@open', next: '@variable' }], // With whitespace control
[/\{\{/, { token: 'delimiter.variable', bracket: '@open', next: '@variable' }], // Standard
// Blocks: {% ... %}
// Raw block needs to be matched first to prevent inner content parsing
[
/(\{%\s*)(raw)(\s*%\})/,
['delimiter.tag', 'keyword.control', { token: 'delimiter.tag', next: '@rawblock' }]
],
[
/(\{%-?\s*)(raw)(\s*-?%\})/,
['delimiter.tag', 'keyword.control', { token: 'delimiter.tag', next: '@rawblock' }]
], // With whitespace control
[/\{%-+/, { token: 'delimiter.tag', bracket: '@open', next: '@block' }], // With whitespace control
[/\{%/, { token: 'delimiter.tag', bracket: '@open', next: '@block' }], // Standard
// Anything else is treated as plain text/HTML until a delimiter is found
[/[^\{]+/, ''],
[/\{/, ''] // Match stray opening braces that aren't part of a delimiter
],
comment: [
[/-?#\}/, { token: 'comment.block', bracket: '@close', next: '@pop' }], // Match closing delimiter with optional whitespace control
[/[^#\}]+/, 'comment.block'], // Content inside comment
[/#|\}/, 'comment.block'] // Consume '#' or '}' within comment (e.g., nested comments - though Jinja doesn't support true nesting)
],
variable: [
[/-?\}\}/, { token: 'delimiter.variable', bracket: '@close', next: '@pop' }], // Match closing delimiter with optional whitespace control
{ include: '@expressionInside' }
],
block: [
[/-?%\}/, { token: 'delimiter.tag', bracket: '@close', next: '@pop' }], // Match closing delimiter with optional whitespace control
{ include: '@expressionInside' }
],
// Simplified raw block handling: consumes everything until endraw
rawblock: [
[
/(\{%-?\s*)(endraw)(\s*-?%\})/,
['delimiter.tag', 'keyword.control', { token: 'delimiter.tag', next: '@pop' }]
],
[/[^{%]+/, 'comment.block.raw'], // Any character not part of start delimiter
[/\{%?/, 'comment.block.raw'] // Consume parts of delimiters if not endraw
],
expressionInside: [
// Match keywords, constants, and special variables
[
/\b[a-zA-Z_]\w*\b/,
{
cases: {
'@keywords': 'keyword.control',
'@constants': 'constant.language',
'@specialVars': 'variable.language',
'@default': 'variable.other'
}
}
],
// Numbers (allow underscore separators like in TextMate, though less common in Jinja)
[/\d+(_\d+)*(\.\d+)?([eE][+\-]?\d+)?/, 'number'],
// Strings
[/"/, { token: 'string.quote.double', bracket: '@open', next: '@string_double' }], // Start double-quoted string
[/'/, { token: 'string.quote.single', bracket: '@open', next: '@string_single' }], // Start single-quoted string
// Operators and Symbols
// Specific rule for filter pipe - pushes to a state to identify the filter name
[/\|(?=\s*[a-zA-Z_])/, { token: 'operators.filter', next: '@filterName' }],
[
/@symbols/,
{
cases: {
'@operators': 'keyword.operator',
'@default': 'delimiter' // Treat other symbols as delimiters (e.g., ~)
}
}
],
// Delimiters: . : , ( ) [ ] { } (pipe handled separately)
[/\./, 'delimiter.accessor'], // Dot for attribute access
[/[?:,()\[\]{}]/, 'delimiter'], // Other delimiters
// Whitespace
[/\s+/, 'white']
],
string_double: [
[/\\\\/, 'constant.character.escape'], // 1. Explicit \\
[/\\"/, 'constant.character.escape'], // 2. Explicit \"
[/@escapes/, 'constant.character.escape'], // 3. Other known escapes (modified regex)
[/\\./, 'string.escape.invalid'], // 4. Invalid escapes
[/[^\\"]+/, 'string'], // 5. Regular string content
[/"/, { token: 'string.quote.double', bracket: '@close', next: '@pop' }] // 6. Closing quote
],
string_single: [
[/\\\\/, 'constant.character.escape'], // 1. Explicit \\
[/\\'/, 'constant.character.escape'], // 2. Explicit \'
[/@escapes/, 'constant.character.escape'], // 3. Other known escapes (modified regex)
[/\\./, 'string.escape.invalid'], // 4. Invalid escapes
[/[^\\']+/, 'string'], // 5. Regular string content
[/'/, { token: 'string.quote.single', bracket: '@close', next: '@pop' }] // 6. Closing quote
],
// State to capture the filter name after a pipe
filterName: [
[/\s+/, 'white'], // Eat whitespace before the name
[/[a-zA-Z_]\w*/, { token: 'variable.other.filter', next: '@pop' }], // Filter name itself
['', { token: '', next: '@pop' }] // If anything else follows pipe (like another pipe or delimiter), just pop
]
}
};

View file

@ -31,6 +31,7 @@ import './html/html.contribution';
import './ini/ini.contribution'; import './ini/ini.contribution';
import './java/java.contribution'; import './java/java.contribution';
import './javascript/javascript.contribution'; import './javascript/javascript.contribution';
import './jinja/jinja.contribution';
import './julia/julia.contribution'; import './julia/julia.contribution';
import './kotlin/kotlin.contribution'; import './kotlin/kotlin.contribution';
import './less/less.contribution'; import './less/less.contribution';

View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ page_title|default("Default Title") }}</title>
</head>
<body>
<h1>Hello, {{ user_name }}!</h1>
{% if items %}
<h2>Items:</h2>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% else %}
<li>No items found.</li>
{% endfor %}
</ul>
{% endif %}
{# This is a comment #}
<p>Current year: {{ current_year }}</p>
</body>
</html>