miki-template 2.3.1 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +1 -1
  2. package/package.json +24 -2
  3. package/src/codegen.js +140 -9
  4. package/src/parser.js +69 -1
  5. package/src/tags/control.js +112 -51
  6. package/src/tags/extra.js +4 -17
  7. package/src/tags/i18n.js +6 -17
  8. package/src/tags/inheritance.js +6 -20
  9. package/src/tags/util.js +5 -20
  10. package/.eslintrc.json +0 -16
  11. package/.github/release-notes/v1.3.1.md +0 -55
  12. package/.github/release-notes/v1.3.3.md +0 -77
  13. package/.github/release-notes/v1.3.7.md +0 -20
  14. package/.github/workflows/ci.yml +0 -42
  15. package/.github/workflows/docs.yml +0 -107
  16. package/.github/workflows/release.yml +0 -107
  17. package/AGENT.md +0 -71
  18. package/API_REFERENCE.md +0 -341
  19. package/CHANGELOG.md +0 -173
  20. package/CODE_OF_CONDUCT.md +0 -14
  21. package/CONTRIBUTING.md +0 -27
  22. package/ROADMAP.md +0 -40
  23. package/assets/banner.png +0 -0
  24. package/assets/logo.png +0 -0
  25. package/benchmarks/ejs-results.json +0 -17
  26. package/benchmarks/ejs.js +0 -38
  27. package/benchmarks/handlebars-results.json +0 -17
  28. package/benchmarks/handlebars.js +0 -45
  29. package/benchmarks/miki-results.json +0 -17
  30. package/benchmarks/miki.js +0 -39
  31. package/benchmarks/pug-results.json +0 -17
  32. package/benchmarks/pug.js +0 -38
  33. package/benchmarks/report.json +0 -17
  34. package/benchmarks/run.js +0 -81
  35. package/benchmarks/stress.mjs +0 -647
  36. package/benchmarks/templates/large.dtpl +0 -7
  37. package/benchmarks/templates/medium.dtpl +0 -3
  38. package/benchmarks/templates/small.dtpl +0 -7
  39. package/context/component.md +0 -109
  40. package/context/prd.md +0 -131
  41. package/context/project-structure.md +0 -33
  42. package/dir/base.html +0 -23
  43. package/dir/cmpnt.html +0 -11
  44. package/dir/footer.html +0 -3
  45. package/dir/home.html +0 -80
  46. package/dir/index.html +0 -80
  47. package/dir/navbar.html +0 -9
  48. package/docs/api/async-render.md +0 -170
  49. package/docs/api/cache.md +0 -174
  50. package/docs/api/compile.md +0 -256
  51. package/docs/api/context-processors.md +0 -154
  52. package/docs/api/filters.md +0 -437
  53. package/docs/api/finder.md +0 -188
  54. package/docs/api/helpers.md +0 -106
  55. package/docs/api/i18n.md +0 -314
  56. package/docs/api/index.md +0 -108
  57. package/docs/api/libraries.md +0 -414
  58. package/docs/api/render-partial.md +0 -162
  59. package/docs/api/render.md +0 -184
  60. package/docs/api/security.md +0 -290
  61. package/docs/api/setup-express.md +0 -152
  62. package/docs/api/tags.md +0 -268
  63. package/docs/assets/banner.png +0 -0
  64. package/docs/assets/logo.png +0 -0
  65. package/docs/filter.md +0 -0
  66. package/docs/guide/advanced-usage.md +0 -794
  67. package/docs/guide/async-rendering.md +0 -616
  68. package/docs/guide/context-processors.md +0 -514
  69. package/docs/guide/custom-filters.md +0 -622
  70. package/docs/guide/custom-tags.md +0 -542
  71. package/docs/guide/filters.md +0 -1314
  72. package/docs/guide/getting-started.md +0 -204
  73. package/docs/guide/installation.md +0 -190
  74. package/docs/guide/partial-templates.md +0 -734
  75. package/docs/guide/quick-start.md +0 -444
  76. package/docs/guide/security.md +0 -690
  77. package/docs/guide/tags.md +0 -1566
  78. package/docs/guide/template-discovery.md +0 -340
  79. package/docs/guide/template-inheritance.md +0 -546
  80. package/docs/guide/what-is-miki-template.md +0 -28
  81. package/docs/guide/why-miki-template.md +0 -75
  82. package/docs/index.md +0 -86
  83. package/docs/integrations/elysia.md +0 -80
  84. package/docs/integrations/express.md +0 -219
  85. package/docs/integrations/fastify.md +0 -79
  86. package/docs/integrations/hono.md +0 -80
  87. package/docs/integrations/index.md +0 -68
  88. package/docs/integrations/koa.md +0 -90
  89. package/docs/integrations/nestjs.md +0 -80
  90. package/docs/integrations/tsed.md +0 -83
  91. package/docs/performance.md +0 -74
  92. package/eslint.config.mjs +0 -42
  93. package/ex.mjs +0 -36
  94. package/live-test/integrations/elysia-example.js +0 -16
  95. package/live-test/integrations/express-example.js +0 -24
  96. package/live-test/integrations/fastify-example.js +0 -20
  97. package/live-test/integrations/hono-example.js +0 -16
  98. package/live-test/integrations/koa-example.js +0 -30
  99. package/live-test/integrations/nestjs-example.js +0 -25
  100. package/live-test/integrations/smoke-test.js +0 -166
  101. package/live-test/integrations/tsed-example.js +0 -23
  102. package/live-test/package-lock.json +0 -1150
  103. package/live-test/package.json +0 -13
  104. package/live-test/packages/product/templates/product/detail.html +0 -7
  105. package/live-test/server.js +0 -38
  106. package/live-test/templates/app_templates/detail.html +0 -6
  107. package/live-test/views/base.html +0 -8
  108. package/live-test/views/child.html +0 -7
  109. package/live-test/views/home.html +0 -17
  110. package/live-test/views/index.html +0 -1
  111. package/miki-template-extension/.github/workflows/ci.yml +0 -116
  112. package/miki-template-extension/.vscodeignore +0 -7
  113. package/miki-template-extension/CHANGELOG.md +0 -99
  114. package/miki-template-extension/LICENSE +0 -21
  115. package/miki-template-extension/README.md +0 -273
  116. package/miki-template-extension/extension.js +0 -1013
  117. package/miki-template-extension/icon.png +0 -0
  118. package/miki-template-extension/icon.svg +0 -10
  119. package/miki-template-extension/miki-template-1.7.1.vsix +0 -0
  120. package/miki-template-extension/package.json +0 -280
  121. package/miki-template-extension/snippets/miki-template.json +0 -717
  122. package/miki-template-extension/syntaxes/language-configuration.json +0 -114
  123. package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +0 -355
  124. package/miki-template-extension/tests/grammar-tests.json +0 -162
  125. package/miki-template-extension/tests/run-grammar-tests.js +0 -82
  126. package/mkdocs.yml +0 -195
  127. package/overrides/main.html +0 -26
  128. package/requirements-docs.txt +0 -2
  129. package/sample-app/package-lock.json +0 -901
  130. package/sample-app/package.json +0 -9
  131. package/sample-app/server.js +0 -14
  132. package/sample-app/views/index.html +0 -1
  133. package/scripts/build-vsix.js +0 -129
  134. package/scripts/build-vsix.ps1 +0 -15
  135. package/snippets/miki-template.json +0 -177
  136. package/src/asyncRender.js +0 -21
  137. package/src/types.d.ts +0 -107
  138. package/syntaxes/language-configuration.json +0 -26
  139. package/syntaxes/miki-template.tmLanguage.json +0 -146
  140. package/tests/asyncRender.test.js +0 -17
  141. package/tests/base.html +0 -6
  142. package/tests/child.html +0 -3
  143. package/tests/context_processors.test.js +0 -13
  144. package/tests/esm.test.mjs +0 -61
  145. package/tests/filters.test.js +0 -263
  146. package/tests/finder-appdirs.test.js +0 -19
  147. package/tests/finder.test.js +0 -17
  148. package/tests/fixtures/views/nested/index.html +0 -1
  149. package/tests/fixtures/views/partial.html +0 -1
  150. package/tests/fixtures/views/sub/deepfile.html +0 -1
  151. package/tests/fixtures/views-appdirs/product/site/detail.html +0 -1
  152. package/tests/include_security.test.js +0 -9
  153. package/tests/integration/README.md +0 -32
  154. package/tests/integration/features.test.cjs +0 -1681
  155. package/tests/integration/features.test.mjs +0 -1697
  156. package/tests/integration/finder.esm.test.mjs +0 -13
  157. package/tests/integration/partial-render.test.cjs +0 -13
  158. package/tests/integration/templates/base.miki +0 -6
  159. package/tests/integration/templates/child.miki +0 -6
  160. package/tests/integration/templates/index.html +0 -17
  161. package/tests/lexer.test.js +0 -45
  162. package/tests/parser.test.js +0 -57
  163. package/tests/partial.html +0 -1
  164. package/tests/partialdef.test.js +0 -79
  165. package/tests/production_checks.js +0 -57
  166. package/tests/security.test.js +0 -28
  167. package/tests/tags.test.js +0 -233
@@ -1,1013 +0,0 @@
1
- const vscode = require('vscode');
2
- const path = require('path');
3
- const fs = require('fs');
4
-
5
- const TAGS = {
6
- if: { doc: 'Conditionally renders content based on an expression.', syntax: '{% if condition %}' },
7
- elif: { doc: 'Else-if condition in an if block.', syntax: '{% elif condition %}' },
8
- else: { doc: 'Else block for if conditions.', syntax: '{% else %}' },
9
- endif: { doc: 'End of an if block.', syntax: '{% endif %}' },
10
- for: { doc: 'Iterates over arrays or objects.', syntax: '{% for item in items %}' },
11
- empty: { doc: 'Content shown when loop has no items.', syntax: '{% empty %}' },
12
- endfor: { doc: 'End of a for loop.', syntax: '{% endfor %}' },
13
- with: { doc: 'Creates scoped aliases for variables.', syntax: '{% with var as alias %}' },
14
- endwith: { doc: 'End of a with block.', syntax: '{% endwith %}' },
15
- cycle: { doc: 'Outputs one of its arguments for each iteration.', syntax: "{% cycle 'val1' 'val2' %}" },
16
- firstof: { doc: 'Outputs the first argument that evaluates to true.', syntax: '{% firstof var1 var2 "fallback" %}' },
17
- comment: { doc: 'Block comment that is stripped from output.', syntax: '{% comment %} ... {% endcomment %}' },
18
- endcomment: { doc: 'End of comment block.', syntax: '{% endcomment %}' },
19
- verbatim: { doc: 'Prevents all tag/variable parsing inside.', syntax: '{% verbatim %} ... {% endverbatim %}' },
20
- endverbatim: { doc: 'End of verbatim block.', syntax: '{% endverbatim %}' },
21
- include: { doc: 'Includes another template file.', syntax: '{% include "template.html" %}' },
22
- extends: { doc: 'Must be first tag - specifies parent template.', syntax: '{% extends "base.html" %}' },
23
- block: { doc: 'Defines a replaceable section.', syntax: '{% block name %} ... {% endblock %}' },
24
- endblock: { doc: 'End of a block.', syntax: '{% endblock %}' },
25
- 'block.super': { doc: 'Renders parent template block content.', syntax: '{{ block.super }}' },
26
- partialdef: { doc: 'Defines a reusable fragment (miki-template).', syntax: '{% partialdef name %} ... {% endpartialdef %}' },
27
- endpartialdef: { doc: 'End of partialdef block.', syntax: '{% endpartialdef %}' },
28
- partial: { doc: 'Renders a previously defined partial.', syntax: '{% partial name %}' },
29
- load: { doc: 'Loads additional filter libraries.', syntax: '{% load i18n %}' },
30
- spaceless: { doc: 'Removes whitespace between HTML tags.', syntax: '{% spaceless %} ... {% endspaceless %}' },
31
- endspaceless: { doc: 'End of spaceless block.', syntax: '{% endspaceless %}' },
32
- autoescape: { doc: 'Controls HTML escaping.', syntax: '{% autoescape on %} ... {% endautoescape %}' },
33
- endautoescape: { doc: 'End of autoescape block.', syntax: '{% endautoescape %}' },
34
- filter: { doc: 'Applies a filter to block content.', syntax: '{% filter lower %} ... {% endfilter %}' },
35
- endfilter: { doc: 'End of filter block.', syntax: '{% endfilter %}' },
36
- templatetag: { doc: 'Outputs a template tag symbol.', syntax: '{% templatetag openblock %}' },
37
- trans: { doc: 'Outputs a translated string.', syntax: '{% trans "Hello" %}' },
38
- blocktrans: { doc: 'Translates a block of text.', syntax: '{% blocktrans %} ... {% endblocktrans %}' },
39
- endblocktrans: { doc: 'End of blocktrans.', syntax: '{% endblocktrans %}' },
40
- plural: { doc: 'Plural form in blocktrans.', syntax: '{% plural %}' },
41
- language: { doc: 'Switches active language.', syntax: '{% language "fr" %} ... {% endlanguage %}' },
42
- endlanguage: { doc: 'End of language block.', syntax: '{% endlanguage %}' },
43
- regroup: { doc: 'Regroups a list by a common attribute.', syntax: '{% regroup items by attr as groups %}' },
44
- widthratio: { doc: 'Calculates proportional width.', syntax: '{% widthratio value max max_width %}' },
45
- debug: { doc: 'Dumps template context.', syntax: '{% debug %}' },
46
- csrf_token: { doc: 'Outputs CSRF token hidden input.', syntax: '{% csrf_token %}' },
47
- csp_nonce_attr: { doc: 'Outputs CSP nonce attribute.', syntax: '{% csp_nonce_attr %}' },
48
- static: { doc: 'Generates URL for static asset.', syntax: '{% static "css/app.css" %}' },
49
- url: { doc: 'Generates URL for named route.', syntax: '{% url "route-name" %}' },
50
- cache: { doc: 'Caches block content (miki-template).', syntax: '{% cache timeout key %} ... {% endcache %}' },
51
- endcache: { doc: 'End of cache block.', syntax: '{% endcache %}' },
52
- addtoblock: { doc: 'Appends content to a block (miki-template).', syntax: '{% addtoblock css %} ... {% endaddtoblock %}' },
53
- endaddtoblock: { doc: 'End of addtoblock.', syntax: '{% endaddtoblock %}' },
54
- };
55
-
56
- const FILTERS = {
57
- upper: { doc: 'Converts to UPPERCASE.', syntax: '{{ value|upper }}', args: [] },
58
- lower: { doc: 'Converts to lowercase.', syntax: '{{ value|lower }}', args: [] },
59
- title: { doc: 'Converts to Title Case.', syntax: '{{ value|title }}', args: [] },
60
- capfirst: { doc: 'Capitalizes first character.', syntax: '{{ value|capfirst }}', args: [] },
61
- slugify: { doc: 'Converts to URL-safe slug.', syntax: '{{ value|slugify }}', args: [] },
62
- wordcount: { doc: 'Returns word count.', syntax: '{{ text|wordcount }}', args: [] },
63
- striptags: { doc: 'Removes HTML tags.', syntax: '{{ html|striptags }}', args: [] },
64
- truncatewords: { doc: 'Truncates to N words.', syntax: '{{ text|truncatewords:10 }}', args: ['n'] },
65
- truncatechars: { doc: 'Truncates to N characters.', syntax: '{{ text|truncatechars:100 }}', args: ['n'] },
66
- linebreaks: { doc: 'Converts newlines to HTML paragraphs.', syntax: '{{ text|linebreaks }}', args: [] },
67
- linebreaksbr: { doc: 'Converts newlines to <br>.', syntax: '{{ text|linebreaksbr }}', args: [] },
68
- cut: { doc: 'Removes occurrences of value.', syntax: '{{ value|cut:" " }}', args: ['value'] },
69
- addslashes: { doc: 'Adds backslashes before quotes.', syntax: "{{ value|addslashes }}", args: [] },
70
- removetags: { doc: 'Removes specific HTML tags.', syntax: '{{ html|removetags:"p,div" }}', args: ['tags'] },
71
- safe: { doc: 'Marks value as HTML-safe.', syntax: '{{ html|safe }}', args: [] },
72
- escape: { doc: 'Escapes HTML entities.', syntax: '{{ value|escape }}', args: [] },
73
- escapejs: { doc: 'Escapes for JavaScript.', syntax: '{{ value|escapejs }}', args: [] },
74
- urlencode: { doc: 'URL encodes the value.', syntax: '{{ value|urlencode }}', args: [] },
75
- escapeurl: { doc: 'Full URL encoding.', syntax: '{{ url|escapeurl }}', args: [] },
76
- stringformat: { doc: 'Python-style format.', syntax: '{{ value|stringformat:"s" }}', args: ['fmt'] },
77
- center: { doc: 'Centers text in field.', syntax: '{{ value|center:10 }}', args: ['width'] },
78
- ljust: { doc: 'Left justifies text.', syntax: '{{ value|ljust:10 }}', args: ['width'] },
79
- rjust: { doc: 'Right justifies text.', syntax: '{{ value|rjust:10 }}', args: ['width'] },
80
- length: { doc: 'Returns length.', syntax: '{{ value|length }}', args: [] },
81
- length_is: { doc: 'Checks if length equals N.', syntax: '{{ value|length_is:5 }}', args: ['n'] },
82
- join: { doc: 'Joins array with separator.', syntax: '{{ list|join:", " }}', args: ['separator'] },
83
- slice: { doc: 'Slices array/string.', syntax: "{{ value|slice:'0:3' }}", args: ['start:end'] },
84
- first: { doc: 'Returns first element.', syntax: '{{ list|first }}', args: [] },
85
- last: { doc: 'Returns last element.', syntax: '{{ list|last }}', args: [] },
86
- dictsort: { doc: 'Sorts by key (ascending).', syntax: '{{ list|dictsort:"name" }}', args: ['key'] },
87
- dictsortreversed: { doc: 'Sorts by key (descending).', syntax: '{{ list|dictsortreversed:"name" }}', args: ['key'] },
88
- default: { doc: 'Fallback if falsy.', syntax: '{{ value|default:"fallback" }}', args: ['fallback'] },
89
- default_if_none: { doc: 'Fallback if None/undefined.', syntax: '{{ value|default_if_none:"fallback" }}', args: ['fallback'] },
90
- firstof: { doc: 'First truthy value.', syntax: '{{ val1|firstof:val2:val3 }}', args: ['val2', 'val3'] },
91
- date: { doc: 'Formats date.', syntax: '{{ date|date:"Y-m-d" }}', args: ['format'] },
92
- time: { doc: 'Formats time.', syntax: '{{ date|time:"H:i" }}', args: ['format'] },
93
- strftime: { doc: 'Format with date-fns.', syntax: '{{ date|strftime:"PPpp" }}', args: ['format'] },
94
- timesince: { doc: 'Human-readable time ago.', syntax: '{{ date|timesince }}', args: ['other_date?'] },
95
- timeuntil: { doc: 'Human-readable time until.', syntax: '{{ date|timeuntil }}', args: ['other_date?'] },
96
- add: { doc: 'Adds N to value.', syntax: '{{ value|add:5 }}', args: ['n'] },
97
- divisibleby: { doc: 'Checks divisibility.', syntax: '{{ value|divisibleby:2 }}', args: ['n'] },
98
- floatformat: { doc: 'Formats decimal places.', syntax: '{{ value|floatformat:2 }}', args: ['decimals?'] },
99
- yesno: { doc: 'Maps boolean to strings.', syntax: '{{ value|yesno:"yes,no,maybe" }}', args: ['yes,no,maybe'] },
100
- pluralize: { doc: 'Returns plural suffix.', syntax: '{{ count|pluralize }}', args: ['suffix?'] },
101
- filesizeformat: { doc: 'Human-readable file size.', syntax: '{{ bytes|filesizeformat }}', args: [] },
102
- trans: { doc: 'Translates string.', syntax: '{{ "Hello"|trans }}', args: ['fallback?'] },
103
- regroup: { doc: 'Groups list by attribute.', syntax: '{{ list|regroup:"category" }}', args: ['key'] },
104
- intcomma: { doc: 'Adds commas to integer.', syntax: '{{ number|intcomma }}', args: [] },
105
- intword: { doc: 'Large number to word.', syntax: '{{ number|intword }}', args: [] },
106
- apnumber: { doc: '1→one, 2→two.', syntax: '{{ number|apnumber }}', args: [] },
107
- ordinal: { doc: '1→1st, 2→2nd.', syntax: '{{ number|ordinal }}', args: [] },
108
- naturalday: { doc: '"yesterday", "today".', syntax: '{{ date|naturalday }}', args: [] },
109
- json_script: { doc: 'JSON script tag.', syntax: "{{ data|json_script:'id' }}", args: ['id'] },
110
- get_digit: { doc: 'Get digit by position.', syntax: '{{ number|get_digit:1 }}', args: ['position'] },
111
- };
112
-
113
- const FORLOOP_VARS = [
114
- { name: 'forloop.counter', doc: '1-indexed loop counter' },
115
- { name: 'forloop.counter0', doc: '0-indexed loop counter' },
116
- { name: 'forloop.revcounter', doc: 'Remaining iterations (1-indexed)' },
117
- { name: 'forloop.revcounter0', doc: 'Remaining iterations (0-indexed)' },
118
- { name: 'forloop.first', doc: 'True if first iteration' },
119
- { name: 'forloop.last', doc: 'True if last iteration' },
120
- { name: 'forloop.parentloop', doc: 'Reference to parent loop' },
121
- ];
122
-
123
- const COLOR_REGEX = /(?:#[0-9A-Fa-f]{3,8}|rgba?\s*\([^)]+\)|hsla?\s*\([^)]+\))/g;
124
-
125
- let customFilters = [];
126
- let customTags = [];
127
- let diagnosticCollection;
128
- let colorDecorationType;
129
- let bracketHighlightDecorations = new Map();
130
-
131
- function debounce(fn, delay) {
132
- let timer;
133
- return (...args) => {
134
- clearTimeout(timer);
135
- timer = setTimeout(() => fn(...args), delay);
136
- };
137
- }
138
-
139
- function createCompletionItem(name, info, kind) {
140
- const item = new vscode.CompletionItem(name, kind);
141
- item.detail = info.syntax;
142
- item.documentation = new vscode.MarkdownString(info.doc);
143
- return item;
144
- }
145
-
146
- const TAG_COMPLETIONS = Object.entries(TAGS).map(([name]) => createCompletionItem(name, TAGS[name], vscode.CompletionItemKind.Keyword));
147
- const FILTER_COMPLETIONS = Object.entries(FILTERS).map(([name]) => createCompletionItem(name, FILTERS[name], vscode.CompletionItemKind.Function));
148
- const FORLOOP_COMPLETIONS = FORLOOP_VARS.map(v => {
149
- const item = new vscode.CompletionItem(v.name, vscode.CompletionItemKind.Variable);
150
- item.detail = v.name;
151
- item.documentation = v.doc;
152
- return item;
153
- });
154
-
155
- function isInsideForLoop(document, position) {
156
- const textBefore = document.getText(new vscode.Range(0, 0, position.line, position.character));
157
- const forMatch = textBefore.match(/\{%-?\s*for\s+\S+\s+in\s+[^\}]*$/);
158
- const endforMatch = textBefore.match(/\{%-?\s*endfor\s/);
159
- if (forMatch && (!endforMatch || forMatch.index > endforMatch.index)) {
160
- return true;
161
- }
162
- return false;
163
- }
164
-
165
- function scanForCustomTagsAndFilters(workspaceFolder) {
166
- if (!workspaceFolder) return;
167
-
168
- const configFiles = [
169
- path.join(workspaceFolder, 'miki-template.config.js'),
170
- path.join(workspaceFolder, 'miki-template.config.json'),
171
- path.join(workspaceFolder, '.mikirc'),
172
- path.join(workspaceFolder, 'package.json'),
173
- ];
174
-
175
- for (const configFile of configFiles) {
176
- try {
177
- if (fs.existsSync(configFile)) {
178
- const content = fs.readFileSync(configFile, 'utf8');
179
- if (configFile.endsWith('.json')) {
180
- const config = JSON.parse(content);
181
- if (config.filters) customFilters = [...customFilters, ...config.filters];
182
- if (config.tags) customTags = [...customTags, ...config.tags];
183
- if (config.mikiTemplate && config.mikiTemplate.filters) {
184
- customFilters = [...customFilters, ...config.mikiTemplate.filters];
185
- }
186
- } else if (configFile.endsWith('.js')) {
187
- const match = content.match(/registerFilter\s*\(\s*['"](\w+)['"]/g);
188
- if (match) {
189
- match.forEach(m => {
190
- const name = m.match(/['"](\w+)['"]/)[1];
191
- if (!customFilters.includes(name)) customFilters.push(name);
192
- });
193
- }
194
- }
195
- }
196
- } catch (e) {}
197
- }
198
- }
199
-
200
- function getAllReferences(word, workspaceFolder) {
201
- const references = [];
202
- if (!workspaceFolder) return references;
203
-
204
- function searchInFolder(folder) {
205
- try {
206
- const entries = fs.readdirSync(folder, { withFileTypes: true });
207
- for (const entry of entries) {
208
- const fullPath = path.join(folder, entry.name);
209
- if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
210
- searchInFolder(fullPath);
211
- } else if (entry.isFile() && /\.(miki|miki-template|django|dj|html|tpl)$/.test(entry.name)) {
212
- try {
213
- const content = fs.readFileSync(fullPath, 'utf8');
214
- const lines = content.split('\n');
215
- lines.forEach((line, idx) => {
216
- if (line.includes(word)) {
217
- references.push(new vscode.Location(
218
- vscode.Uri.file(fullPath),
219
- new vscode.Range(idx, 0, idx, line.length)
220
- ));
221
- }
222
- });
223
- } catch (e) {}
224
- }
225
- }
226
- } catch (e) {}
227
- }
228
-
229
- searchInFolder(workspaceFolder.uri.fsPath);
230
- return references;
231
- }
232
-
233
- function findTemplateFiles(workspaceFolder) {
234
- const results = [];
235
- if (!workspaceFolder) return results;
236
-
237
- function search(folder) {
238
- try {
239
- const entries = fs.readdirSync(folder, { withFileTypes: true });
240
- for (const entry of entries) {
241
- const fullPath = path.join(folder, entry.name);
242
- if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
243
- search(fullPath);
244
- } else if (entry.isFile() && /\.(html|miki|miki-template|django|dj|tpl)$/.test(entry.name)) {
245
- const relativePath = path.relative(workspaceFolder.uri.fsPath, fullPath).replace(/\\/g, '/');
246
- results.push({
247
- label: entry.name,
248
- detail: relativePath,
249
- fsPath: fullPath,
250
- relativePath: relativePath
251
- });
252
- }
253
- }
254
- } catch (e) {}
255
- }
256
-
257
- search(workspaceFolder.uri.fsPath);
258
- return results.sort((a, b) => a.label.localeCompare(b.label));
259
- }
260
-
261
- function activate(context) {
262
- const config = vscode.workspace.getConfiguration('miki-template');
263
-
264
- diagnosticCollection = vscode.languages.createDiagnosticCollection('miki-template');
265
-
266
- const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
267
- scanForCustomTagsAndFilters(workspaceFolder);
268
-
269
- const customFilterCompletions = customFilters.map(name => {
270
- const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Function);
271
- item.detail = `{{ value|${name} }}`;
272
- item.documentation = `Custom filter: ${name}`;
273
- return item;
274
- });
275
-
276
- const customTagCompletions = customTags.map(name => {
277
- const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Keyword);
278
- item.detail = `{% ${name} %}`;
279
- item.documentation = `Custom tag: ${name}`;
280
- return item;
281
- });
282
-
283
- // Completion Provider with Path Completions
284
- const completionProvider = vscode.languages.registerCompletionItemProvider(
285
- ['miki-template', 'django-html'],
286
- {
287
- provideCompletionItems(document, position) {
288
- if (!config.get('enableCompletions', true)) return [];
289
-
290
- const line = document.lineAt(position).text;
291
- const beforeCursor = line.substring(0, position.character);
292
- const allCompletions = [];
293
-
294
- if (beforeCursor.match(/\{%\s*$/)) {
295
- allCompletions.push(...TAG_COMPLETIONS, ...customTagCompletions);
296
- return allCompletions;
297
- }
298
-
299
- if (beforeCursor.match(/\{%\s*load\s+/) && !beforeCursor.match(/\{%\s*load\s+\S+\s+\S+/)) {
300
- return ['i18n', 'humanize', 'cache', 'lorem'].map(
301
- lib => new vscode.CompletionItem(lib, vscode.CompletionItemKind.Module)
302
- );
303
- }
304
-
305
- // Path completions for include "..." and extends "..."
306
- const pathMatch = beforeCursor.match(/\{%-?\s*(include|extends)\s+["']?([\w/.-]*)$/);
307
- if (pathMatch) {
308
- const files = findTemplateFiles(workspaceFolder);
309
- const partial = pathMatch[2] || '';
310
- const filtered = files.filter(f =>
311
- f.label.toLowerCase().includes(partial.toLowerCase()) ||
312
- f.relativePath.toLowerCase().includes(partial.toLowerCase())
313
- );
314
- return filtered.map(f => {
315
- const item = new vscode.CompletionItem(f.label, vscode.CompletionItemKind.File);
316
- item.detail = f.relativePath;
317
- item.insertText = `"${f.relativePath}" `;
318
- return item;
319
- });
320
- }
321
-
322
- if (beforeCursor.match(/\{\{[^|]*$/)) {
323
- allCompletions.push(...FILTER_COMPLETIONS, ...customFilterCompletions);
324
- if (isInsideForLoop(document, position)) {
325
- allCompletions.push(...FORLOOP_COMPLETIONS);
326
- }
327
- return allCompletions;
328
- }
329
-
330
- if (beforeCursor.match(/\|\s*$/)) {
331
- allCompletions.push(...FILTER_COMPLETIONS, ...customFilterCompletions);
332
- return allCompletions;
333
- }
334
-
335
- return [];
336
- }
337
- },
338
- '{', '%', '|', ' ', '"', "'"
339
- );
340
-
341
- // Hover Provider
342
- const hoverProvider = vscode.languages.registerHoverProvider(
343
- ['miki-template', 'django-html'],
344
- {
345
- provideHover(document, position) {
346
- if (!config.get('enableHover', true)) return null;
347
-
348
- const word = document.getText(document.getWordRangeAtPosition(position));
349
-
350
- if (TAGS[word]) {
351
- const info = TAGS[word];
352
- return new vscode.Hover(new vscode.MarkdownString(`**\\${word}**\n\n${info.doc}\n\n\`\`\`django\n${info.syntax}\n\`\`\``));
353
- }
354
-
355
- if (FILTERS[word] || customFilters.includes(word)) {
356
- const info = FILTERS[word] || { doc: 'Custom filter', syntax: `{{ value|${word} }}` };
357
- const argsDoc = info.args && info.args.length > 0 ? `\n\n**Arguments:** \`${info.args.join('`, `')}\`` : '';
358
- return new vscode.Hover(new vscode.MarkdownString(`**${word}**\n\n${info.doc}${argsDoc}\n\n\`\`\`django\n${info.syntax}\n\`\`\``));
359
- }
360
-
361
- // Hover for block.super
362
- const wordRange = document.getWordRangeAtPosition(position);
363
- if (wordRange) {
364
- const wordAtCursor = document.getText(wordRange);
365
- if (wordAtCursor === 'block' || wordAtCursor === 'super') {
366
- const line = document.lineAt(position).text;
367
- const lineUntilCursor = line.substring(0, position.character);
368
- if (lineUntilCursor.includes('block.super') || lineUntilCursor.includes('{{ block.')) {
369
- return new vscode.Hover(new vscode.MarkdownString(`**{{ block.super }}**\n\nRenders the parent template's block content. Use inside a \`{% block %}\` to include parent content.`));
370
- }
371
- }
372
- }
373
-
374
- return null;
375
- }
376
- }
377
- );
378
-
379
- // Semantic Token Provider
380
- const tokenTypesSemantic = ['tag', 'variable', 'filter', 'comment', 'string', 'operator'];
381
- const semanticTokensProvider = vscode.languages.registerDocumentSemanticTokensProvider(
382
- ['miki-template', 'django-html'],
383
- {
384
- provideDocumentSemanticTokens(document) {
385
- const builder = new vscode.SemanticTokensBuilder();
386
- const text = document.getText();
387
-
388
- const tagRegex = /\{%-?\s*\w+/g;
389
- const varRegex = /\{\{[^}]*\}\}/g;
390
- const commentRegex = /\{#[^}]*#\}/g;
391
-
392
- let match;
393
-
394
- while ((match = tagRegex.exec(text)) !== null) {
395
- const startPos = document.positionAt(match.index);
396
- builder.push(startPos.line, startPos.character, match[0].length, 'entity.name.tag', 0);
397
- }
398
-
399
- while ((match = varRegex.exec(text)) !== null) {
400
- const startPos = document.positionAt(match.index);
401
- builder.push(startPos.line, startPos.character, match[0].length, 'variable', 0);
402
- }
403
-
404
- while ((match = commentRegex.exec(text)) !== null) {
405
- const startPos = document.positionAt(match.index);
406
- builder.push(startPos.line, startPos.character, match[0].length, 'comment', 0);
407
- }
408
-
409
- return builder.build();
410
- }
411
- },
412
- new vscode.SemanticTokensLegend(tokenTypesSemantic, [])
413
- );
414
-
415
- // Inlay Hints Provider
416
- const inlayHintsProvider = vscode.languages.registerInlayHintsProvider(
417
- ['miki-template', 'django-html'],
418
- {
419
- provideInlayHints(document, range) {
420
- if (!config.get('enableInlayHints', true)) return [];
421
-
422
- const hints = [];
423
- const text = document.getText(range);
424
- const filterArgRegex = /\|(\w+)(?::(["']?)(\w+)\2)?/g;
425
- let match;
426
-
427
- while ((match = filterArgRegex.exec(text)) !== null) {
428
- const filterName = match[1];
429
- if (FILTERS[filterName] && FILTERS[filterName].args && FILTERS[filterName].args.length > 0) {
430
- const pos = document.positionAt(match.index);
431
- hints.push(new vscode.InlayHint(`${filterName}:`, vscode.InlayHintKind.Parameter));
432
- }
433
- }
434
-
435
- return hints;
436
- }
437
- }
438
- );
439
-
440
- // Definition Provider
441
- const definitionProvider = vscode.languages.registerDefinitionProvider(
442
- ['miki-template', 'django-html'],
443
- {
444
- provideDefinition(document, position) {
445
- const word = document.getText(document.getWordRangeAtPosition(position));
446
- if (!['include', 'extends', 'block', 'partial', 'partialdef'].includes(word)) return null;
447
-
448
- const line = document.lineAt(position).text;
449
- const fileMatch = line.match(/\{%-?\s*(?:include|extends|block|partial|partialdef)\s+["']([^"']+)["']/);
450
- if (fileMatch) {
451
- const currentDir = path.dirname(document.uri.fsPath);
452
- const fileName = fileMatch[1];
453
- const paths = [
454
- path.join(currentDir, fileName),
455
- path.join(currentDir, fileName.replace(/^\//, '')),
456
- path.join(currentDir, '..', fileName),
457
- path.join(currentDir, 'templates', fileName),
458
- ];
459
-
460
- for (const fsPath of paths) {
461
- try {
462
- if (fs.existsSync(fsPath)) {
463
- return new vscode.Location(vscode.Uri.file(fsPath), new vscode.Position(0, 0));
464
- }
465
- } catch (e) {}
466
- }
467
- }
468
- return null;
469
- }
470
- }
471
- );
472
-
473
- // Rename Provider
474
- const renameProvider = vscode.languages.registerRenameProvider(
475
- ['miki-template', 'django-html'],
476
- {
477
- provideRenameEdits(document, position, newName) {
478
- const line = document.lineAt(position).text;
479
-
480
- const blockMatch = line.match(/\{%-?\s*block\s+(\w+)/);
481
- if (!blockMatch) return null;
482
-
483
- const oldBlockName = blockMatch[1];
484
- const workspaceEdit = new vscode.WorkspaceEdit();
485
-
486
- const fullText = document.getText();
487
- const blockDefRegex = new RegExp(`\\{%-?\\s*block\\s+${oldBlockName}\\b`, 'g');
488
- const blockSuperRegex = new RegExp(`\\{\\{[^}]*block\\.${oldBlockName}[^}]*\\}\\}`, 'g');
489
-
490
- let match;
491
- while ((match = blockDefRegex.exec(fullText)) !== null) {
492
- const pos = document.positionAt(match.index);
493
- workspaceEdit.replace(document.uri, new vscode.Range(pos, pos.translate(0, oldBlockName.length)), newName);
494
- }
495
-
496
- while ((match = blockSuperRegex.exec(fullText)) !== null) {
497
- const pos = document.positionAt(match.index);
498
- const text = match[0];
499
- workspaceEdit.replace(document.uri, new vscode.Range(pos, pos.translate(0, text.length)), text.replace(new RegExp(`block\\.${oldBlockName}`, 'g'), `block.${newName}`));
500
- }
501
-
502
- if (workspaceFolder) {
503
- const otherEdits = getBlockRenameEdits(oldBlockName, newName, workspaceFolder, document.uri);
504
- for (const [uri, edits] of otherEdits.entries()) {
505
- for (const edit of edits) {
506
- workspaceEdit.replace(uri, edit.range, edit.newText);
507
- }
508
- }
509
- }
510
-
511
- return workspaceEdit;
512
- }
513
- }
514
- );
515
-
516
- function getBlockRenameEdits(oldBlockName, newBlockName, folder, currentDocUri) {
517
- const fileEditsMap = new Map();
518
-
519
- function search(dir) {
520
- try {
521
- const entries = fs.readdirSync(dir, { withFileTypes: true });
522
- for (const entry of entries) {
523
- const fullPath = path.join(dir, entry.name);
524
- if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
525
- search(fullPath);
526
- } else if (entry.isFile() && /\.(miki|miki-template|django|dj|html|tpl)$/.test(entry.name)) {
527
- const uri = vscode.Uri.file(fullPath);
528
- if (uri.toString() === currentDocUri.toString()) continue;
529
-
530
- try {
531
- const content = fs.readFileSync(fullPath, 'utf8');
532
- if (content.includes(`block.${oldBlockName}`) || content.includes(`{% block ${oldBlockName}`)) {
533
- const edits = [];
534
- const blockSuperRegex = new RegExp(`block\\.${oldBlockName}`, 'g');
535
- const blockDefRegex = new RegExp(`\\{%-?\\s*block\\s+${oldBlockName}\\b`, 'g');
536
-
537
- let match;
538
-
539
- while ((match = blockSuperRegex.exec(content)) !== null) {
540
- const pos = new vscode.Position(0, 0).translate(0, match.index);
541
- edits.push(new vscode.TextEdit(new vscode.Range(pos, pos.translate(0, match[0].length)), `block.${newBlockName}`));
542
- }
543
-
544
- while ((match = blockDefRegex.exec(content)) !== null) {
545
- const pos = new vscode.Position(0, 0).translate(0, match.index);
546
- edits.push(new vscode.TextEdit(new vscode.Range(pos, pos.translate(0, match[0].length)), `{% block ${newBlockName}`));
547
- }
548
-
549
- if (edits.length > 0) {
550
- fileEditsMap.set(uri, edits);
551
- }
552
- }
553
- } catch (e) {}
554
- }
555
- }
556
- } catch (e) {}
557
- }
558
-
559
- search(folder.uri.fsPath);
560
- return fileEditsMap;
561
- }
562
-
563
- // Document Symbol Provider
564
- const symbolProvider = vscode.languages.registerDocumentSymbolProvider(
565
- ['miki-template', 'django-html'],
566
- {
567
- provideDocumentSymbols(document) {
568
- const symbols = [];
569
- const lines = document.getText().split('\n');
570
-
571
- lines.forEach((line, index) => {
572
- const range = new vscode.Range(index, 0, index, line.length);
573
-
574
- const blockMatch = line.match(/\{%-?\s*block\s+(\w+)/);
575
- if (blockMatch) symbols.push(new vscode.SymbolInformation(blockMatch[1], vscode.SymbolKind.Method, range, document.uri));
576
-
577
- const extendsMatch = line.match(/\{%-?\s*extends\s+["']([^"']+)["']/);
578
- if (extendsMatch) symbols.push(new vscode.SymbolInformation(`↳ ${extendsMatch[1]}`, vscode.SymbolKind.Class, range, document.uri));
579
-
580
- const includeMatch = line.match(/\{%-?\s*include\s+["']([^"']+)["']/);
581
- if (includeMatch) symbols.push(new vscode.SymbolInformation(`⊂ ${includeMatch[1]}`, vscode.SymbolKind.Reference, range, document.uri));
582
-
583
- const partialdefMatch = line.match(/\{%-?\s*partialdef\s+(\w+)/);
584
- if (partialdefMatch) symbols.push(new vscode.SymbolInformation(`§ ${partialdefMatch[1]}`, vscode.SymbolKind.Function, range, document.uri));
585
- });
586
-
587
- return symbols;
588
- }
589
- }
590
- );
591
-
592
- // Selection Range Provider
593
- const selectionRangeProvider = vscode.languages.registerSelectionRangeProvider(
594
- ['miki-template', 'django-html'],
595
- {
596
- provideSelectionRanges(document, positions) {
597
- const results = [];
598
- for (const pos of positions) {
599
- const ranges = [];
600
- const line = document.lineAt(pos.line).text;
601
-
602
- const tagMatch = line.match(/\{%-?\s*(\w+)[^}]*%\}[\s\S]*\{%-?\s*end\1\s*%/);
603
- if (tagMatch) {
604
- const startIdx = line.indexOf(tagMatch[0]);
605
- const endIdx = startIdx + tagMatch[0].length;
606
- ranges.push(new vscode.SelectionRange(new vscode.Range(pos.line, startIdx, pos.line, endIdx)));
607
- }
608
-
609
- const varMatch = line.match(/\{\{[\s\S]*?\}\}/);
610
- if (varMatch && pos.character >= line.indexOf(varMatch[0]) && pos.character <= line.indexOf(varMatch[0]) + varMatch[0].length) {
611
- const startIdx = line.indexOf(varMatch[0]);
612
- const endIdx = startIdx + varMatch[0].length;
613
- ranges.push(new vscode.SelectionRange(new vscode.Range(pos.line, startIdx, pos.line, endIdx)));
614
- }
615
-
616
- ranges.push(new vscode.SelectionRange(new vscode.Range(pos, pos)));
617
- results.push(ranges);
618
- }
619
- return results;
620
- }
621
- }
622
- );
623
-
624
- // References Provider
625
- const referencesProvider = vscode.languages.registerReferencesProvider(
626
- ['miki-template', 'django-html'],
627
- {
628
- provideReferences(document, position, context) {
629
- const line = document.lineAt(position).text;
630
- const results = [];
631
-
632
- const blockDefMatch = line.match(/\{%-?\s*block\s+(\w+)/);
633
- if (blockDefMatch) {
634
- const blockName = blockDefMatch[1];
635
- results.push(...getAllReferences(`block.super`, workspaceFolder));
636
- results.push(...getAllReferences(`{{ block.${blockName} }}`, workspaceFolder));
637
- }
638
-
639
- const includeMatch = line.match(/\{%-?\s*include\s+["']([^"']+)["']/);
640
- if (includeMatch) {
641
- const fileName = includeMatch[1];
642
- results.push(...getAllReferences(`include "${fileName}"`, workspaceFolder));
643
- results.push(...getAllReferences(`include '${fileName}'`, workspaceFolder));
644
- }
645
-
646
- return results;
647
- }
648
- }
649
- );
650
-
651
- // Code Actions Provider
652
- const codeActionsProvider = vscode.languages.registerCodeActionsProvider(
653
- ['miki-template', 'django-html'],
654
- {
655
- provideCodeActions(document, range, context) {
656
- if (!config.get('enableCodeActions', true)) return [];
657
-
658
- const actions = [];
659
- const line = document.lineAt(range.start.line).text;
660
-
661
- const openIfs = (line.match(/\{%-?\s*if\b/g) || []).length;
662
- const closeIfs = (line.match(/\{%-?\s*endif\b/g) || []).length;
663
- if (openIfs > closeIfs) {
664
- actions.push(new vscode.CodeAction('Add missing {% endif %}', { command: 'type', arguments: ['{% endif %}'] }, vscode.CodeActionKind.QuickFix));
665
- }
666
-
667
- const openFors = (line.match(/\{%-?\s*for\b/g) || []).length;
668
- const closeFors = (line.match(/\{%-?\s*endfor\b/g) || []).length;
669
- if (openFors > closeFors) {
670
- actions.push(new vscode.CodeAction('Add missing {% endfor %}', { command: 'type', arguments: ['{% endfor %}'] }, vscode.CodeActionKind.QuickFix));
671
- }
672
-
673
- if (line.includes('{{') && !line.includes('{% block')) {
674
- const action = new vscode.CodeAction('Wrap in {% block %}');
675
- action.command = { command: 'miki-template.wrapInBlock', title: 'Wrap in Block' };
676
- actions.push(action);
677
- }
678
-
679
- return actions;
680
- }
681
- }
682
- );
683
-
684
- // Color Decorations
685
- function updateColorDecorations(document) {
686
- if (!config.get('enableColorDecorations', true)) {
687
- if (colorDecorationType) {
688
- colorDecorationType.dispose();
689
- colorDecorationType = null;
690
- }
691
- return;
692
- }
693
-
694
- if (!colorDecorationType) {
695
- colorDecorationType = vscode.window.createTextEditorDecorationType({
696
- backgroundColor: new vscode.ThemeColor('editor.wordHighlightBackground'),
697
- borderRadius: '2px'
698
- });
699
- }
700
-
701
- const editors = vscode.window.visibleTextEditors.filter(e => e.document === document);
702
- const decorations = [];
703
-
704
- for (let i = 0; i < document.lineCount; i++) {
705
- const line = document.lineAt(i);
706
- let match;
707
- const regex = new RegExp(COLOR_REGEX);
708
- while ((match = regex.exec(line.text)) !== null) {
709
- decorations.push({
710
- range: new vscode.Range(i, match.index, i, match.index + match[0].length)
711
- });
712
- }
713
- }
714
-
715
- editors.forEach(editor => editor.setDecorations(colorDecorationType, decorations));
716
- }
717
-
718
- // Bracket Matching Highlights
719
- function updateBracketHighlights(document) {
720
- if (!config.get('enableBracketHighlight', true)) {
721
- bracketHighlightDecorations.forEach(d => d.dispose());
722
- bracketHighlightDecorations.clear();
723
- return;
724
- }
725
-
726
- const decorations = [];
727
- const text = document.getText();
728
-
729
- for (let i = 0; i < text.length; i++) {
730
- if (text.substring(i, i + 2) === '{%') {
731
- const endIdx = text.indexOf('%}', i);
732
- if (endIdx !== -1) {
733
- const tagContent = text.substring(i + 2, endIdx).trim();
734
- const openMatch = tagContent.match(/^(if|for|with|block|comment|verbatim|spaceless|autoescape|filter|cache|addtoblock|partialdef|blocktrans|language)\b/);
735
- const closeMatch = tagContent.match(/^end(if|for|with|block|comment|verbatim|spaceless|autoescape|filter|cache|addtoblock|partialdef|blocktrans|language)\b/);
736
-
737
- if (openMatch) {
738
- decorations.push({
739
- range: new vscode.Range(document.positionAt(i), document.positionAt(i + 2)),
740
- options: { color: 'editorBracketHighlight.foreground1' }
741
- });
742
- } else if (closeMatch) {
743
- decorations.push({
744
- range: new vscode.Range(document.positionAt(i), document.positionAt(i + 2)),
745
- options: { color: 'editorBracketHighlight.foreground2' }
746
- });
747
- }
748
- i = endIdx + 1;
749
- }
750
- }
751
- }
752
-
753
- const editor = vscode.window.activeTextEditor;
754
- if (editor && editor.document === document) {
755
- const decoType = vscode.window.createTextEditorDecorationType({});
756
- editor.setDecorations(decoType, decorations);
757
- bracketHighlightDecorations.set(document.uri.toString(), decoType);
758
- }
759
- }
760
-
761
- // Validation
762
- function validateDocument(document) {
763
- if (!config.get('enableValidation', true)) {
764
- diagnosticCollection.delete(document.uri);
765
- return;
766
- }
767
-
768
- const diagnostics = [];
769
- const lines = document.getText().split('\n');
770
- const openTags = [];
771
-
772
- lines.forEach((line, index) => {
773
- if (line.match(/\{%-?\s*extends\s+["']([^"']+)["']/) && index > 0) {
774
- const prevContent = lines.slice(0, index).join('').replace(/\s/g, '');
775
- if (prevContent.match(/\{%/)) {
776
- diagnostics.push(new vscode.Diagnostic(
777
- new vscode.Range(index, 0, index, line.length),
778
- '{% extends %} must be the first tag',
779
- vscode.DiagnosticSeverity.Warning
780
- ));
781
- }
782
- }
783
-
784
- const openTagMatch = line.match(/\{%-?\s*(if|elif|for|with|block|comment|verbatim|spaceless|autoescape|filter|cache|addtoblock|partialdef|blocktrans|language)\b[^}]*%}/g);
785
- if (openTagMatch) {
786
- openTagMatch.forEach(tag => {
787
- const nameMatch = tag.match(/\{%-?\s*(\w+)/);
788
- if (nameMatch && !['elif', 'empty', 'else', 'plural'].includes(nameMatch[1])) {
789
- openTags.push({ name: nameMatch[1], line: index + 1 });
790
- }
791
- });
792
- }
793
-
794
- const closeTagMatch = line.match(/\{%-?\s*end(if|elif|for|with|block|comment|verbatim|spaceless|autoescape|filter|cache|addtoblock|partialdef|blocktrans|language)\b[^}]*%}/g);
795
- if (closeTagMatch) {
796
- closeTagMatch.forEach(tag => {
797
- const nameMatch = tag.match(/\{%-?\s*end(\w+)/);
798
- if (nameMatch) {
799
- const idx = openTags.findIndex(t => t.name === nameMatch[1]);
800
- if (idx !== -1) openTags.splice(idx, 1);
801
- }
802
- });
803
- }
804
- });
805
-
806
- for (const tag of openTags) {
807
- if (!['empty', 'else', 'elif', 'plural'].includes(tag.name)) {
808
- diagnostics.push(new vscode.Diagnostic(
809
- new vscode.Range(tag.line - 1, 0, tag.line - 1, 100),
810
- `Unclosed tag: {% ${tag.name} %}`,
811
- vscode.DiagnosticSeverity.Warning
812
- ));
813
- }
814
- }
815
-
816
- diagnosticCollection.set(document.uri, diagnostics);
817
- }
818
-
819
- const debouncedValidate = debounce(validateDocument, 300);
820
- const debouncedColorUpdate = debounce(updateColorDecorations, 200);
821
- const debouncedBracketUpdate = debounce(updateBracketHighlights, 200);
822
-
823
- context.subscriptions.push(
824
- vscode.workspace.onDidChangeTextDocument(event => {
825
- if (event.document.languageId === 'miki-template' || event.document.languageId === 'django-html') {
826
- debouncedValidate(event.document);
827
- debouncedColorUpdate(event.document);
828
- debouncedBracketUpdate(event.document);
829
- }
830
- }),
831
- vscode.workspace.onDidOpenTextDocument(document => {
832
- if (document.languageId === 'miki-template' || document.languageId === 'django-html') {
833
- validateDocument(document);
834
- updateColorDecorations(document);
835
- updateBracketHighlights(document);
836
- }
837
- }),
838
- vscode.window.onDidChangeVisibleTextEditors(editors => {
839
- editors.forEach(editor => {
840
- if (editor.document.languageId === 'miki-template' || editor.document.languageId === 'django-html') {
841
- updateColorDecorations(editor.document);
842
- updateBracketHighlights(editor.document);
843
- }
844
- });
845
- })
846
- );
847
-
848
- // Smart Paste
849
- context.subscriptions.push(
850
- vscode.workspace.onWillPaste(async e => {
851
- const editor = vscode.window.activeTextEditor;
852
- if (!editor) return;
853
- if (editor.document.languageId !== 'miki-template' && editor.document.languageId !== 'django-html') return;
854
- if (!config.get('enableSmartPaste', true)) return;
855
-
856
- const pasteText = e.text;
857
- const hasHtml = /<[a-z][\s\S]*>/i.test(pasteText);
858
-
859
- if (hasHtml && !pasteText.includes('|safe') && !pasteText.includes('|escape')) {
860
- e.text = pasteText + '|safe';
861
- }
862
- })
863
- );
864
-
865
- // Commands
866
- context.subscriptions.push(
867
- vscode.commands.registerCommand('miki-template.validateAll', () => {
868
- vscode.workspace.textDocuments.forEach(doc => {
869
- if (doc.languageId === 'miki-template' || doc.languageId === 'django-html') {
870
- validateDocument(doc);
871
- }
872
- });
873
- vscode.window.showInformationMessage('Template validation complete');
874
- }),
875
-
876
- vscode.commands.registerCommand('miki-template.insertFilter', () => {
877
- const editor = vscode.window.activeTextEditor;
878
- if (!editor) return;
879
- const selection = editor.selection;
880
- const selectedText = editor.document.getText(selection);
881
- if (selectedText) {
882
- editor.edit(editBuilder => {
883
- editBuilder.replace(selection, `{{ ${selectedText}| }}`);
884
- }).then(() => {
885
- const newPos = selection.start.translate(0, selectedText.length + 4);
886
- editor.selection = new vscode.Selection(newPos, newPos);
887
- });
888
- }
889
- }),
890
-
891
- vscode.commands.registerCommand('miki-template.wrapInBlock', () => {
892
- const editor = vscode.window.activeTextEditor;
893
- if (!editor) return;
894
- const selection = editor.selection;
895
- const selectedText = editor.document.getText(selection);
896
- editor.edit(editBuilder => {
897
- editBuilder.replace(selection, '{% block ${1:name} %}\n' + selectedText + '\n{% endblock %}');
898
- });
899
- }),
900
-
901
- vscode.commands.registerCommand('miki-template.wrapInFor', () => {
902
- const editor = vscode.window.activeTextEditor;
903
- if (!editor) return;
904
- const selection = editor.selection;
905
- const selectedText = editor.document.getText(selection);
906
- editor.edit(editBuilder => {
907
- editBuilder.replace(selection, '{% for ${1:item} in ${2:items} %}\n' + selectedText + '\n{% endfor %}');
908
- });
909
- }),
910
-
911
- vscode.commands.registerCommand('miki-template.wrapInIf', () => {
912
- const editor = vscode.window.activeTextEditor;
913
- if (!editor) return;
914
- const selection = editor.selection;
915
- const selectedText = editor.document.getText(selection);
916
- editor.edit(editBuilder => {
917
- editBuilder.replace(selection, '{% if ${1:condition} %}\n' + selectedText + '\n{% endif %}');
918
- });
919
- }),
920
-
921
- vscode.commands.registerCommand('miki-template.addPrettierIgnore', () => {
922
- const editor = vscode.window.activeTextEditor;
923
- if (!editor) return;
924
- const line = editor.selection.start.line;
925
- editor.edit(editBuilder => {
926
- editBuilder.insert(new vscode.Position(line, 0), '{# prettier-ignore #}\n');
927
- });
928
- }),
929
-
930
- vscode.commands.registerCommand('miki-template.goToNextBlock', () => {
931
- const editor = vscode.window.activeTextEditor;
932
- if (!editor) return;
933
- const doc = editor.document;
934
- const pos = editor.selection.active;
935
- for (let i = pos.line + 1; i < doc.lineCount; i++) {
936
- if (doc.lineAt(i).text.includes('{% block ')) {
937
- editor.selection = new vscode.Selection(i, 0, i, 0);
938
- editor.revealRange(new vscode.Range(i, 0, i, 0));
939
- return;
940
- }
941
- }
942
- }),
943
-
944
- vscode.commands.registerCommand('miki-template.goToPrevBlock', () => {
945
- const editor = vscode.window.activeTextEditor;
946
- if (!editor) return;
947
- const doc = editor.document;
948
- const pos = editor.selection.active;
949
- for (let i = pos.line - 1; i >= 0; i--) {
950
- if (doc.lineAt(i).text.includes('{% block ')) {
951
- editor.selection = new vscode.Selection(i, 0, i, 0);
952
- editor.revealRange(new vscode.Range(i, 0, i, 0));
953
- return;
954
- }
955
- }
956
- }),
957
-
958
- vscode.commands.registerCommand('miki-template.previewTemplate', async () => {
959
- const editor = vscode.window.activeTextEditor;
960
- if (!editor) return;
961
- const content = editor.document.getText();
962
- const panel = vscode.window.createWebviewPanel('templatePreview', 'Template Preview', vscode.ViewColumn.Two);
963
- const htmlContent = `<!DOCTYPE html><html><head><meta charset="UTF-8"><style>body{font-family:system-ui;padding:20px;background:#1e1e1e;color:#d4d4d4}</style></head><body><pre>${content.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</pre></body></html>`;
964
- panel.webview.html = htmlContent;
965
- }),
966
-
967
- vscode.commands.registerCommand('miki-template.showOutline', async () => {
968
- const symbols = await vscode.commands.executeCommand('vscode.executeDocumentSymbolProvider', vscode.window.activeTextEditor.document.uri);
969
- if (symbols && symbols.length > 0) {
970
- const items = symbols.map(s => ({ label: s.name, detail: vscode.SymbolKind[s.kind] }));
971
- const selected = await vscode.window.showQuickPick(items);
972
- if (selected) {
973
- const symbol = symbols.find(s => s.name === selected.label);
974
- if (symbol) {
975
- const range = symbol.location.range;
976
- vscode.window.activeTextEditor.selection = new vscode.Selection(range.start, range.start);
977
- vscode.window.activeTextEditor.revealRange(range);
978
- }
979
- }
980
- }
981
- }),
982
-
983
- vscode.commands.registerCommand('miki-template.findBlockReferences', async () => {
984
- const editor = vscode.window.activeTextEditor;
985
- if (!editor) return;
986
- const refs = await vscode.commands.executeCommand('vscode.executeReferenceProvider', editor.document.uri, editor.selection.active);
987
- if (refs && refs.length > 0) {
988
- await vscode.commands.executeCommand('editor.action.showReferences', editor.document.uri, editor.selection.active, refs);
989
- }
990
- })
991
- );
992
-
993
- context.subscriptions.push(
994
- completionProvider,
995
- hoverProvider,
996
- semanticTokensProvider,
997
- inlayHintsProvider,
998
- definitionProvider,
999
- renameProvider,
1000
- symbolProvider,
1001
- selectionRangeProvider,
1002
- referencesProvider,
1003
- codeActionsProvider
1004
- );
1005
- }
1006
-
1007
- function deactivate() {
1008
- if (diagnosticCollection) diagnosticCollection.clear();
1009
- if (colorDecorationType) colorDecorationType.dispose();
1010
- bracketHighlightDecorations.forEach(d => d.dispose());
1011
- }
1012
-
1013
- module.exports = { activate, deactivate };