Files
haos-hacs-immich-album-watcher/frontend/src/lib/components/JinjaEditor.svelte
alexei.dolgolyov ce21733ae6
Some checks failed
Validate / Hassfest (push) Has been cancelled
Jinja2 syntax highlighting + description field + preview toggle
JinjaEditor:
- Custom StreamLanguage parser for Jinja2 syntax highlighting:
  {{ variables }} in blue, {% statements %} in purple, {# comments #} in gray
- Replaced HTML mode (didn't understand Jinja2 syntax)
- Proper monospace font (Consolas/Monaco)

TemplateConfig:
- Added `description` field to model + seed defaults with descriptions
- Description shown on template cards instead of raw template text
- Description input in create/edit form

Preview:
- Toggle behavior: clicking Preview again hides the preview
- Per-slot preview uses preview-raw API (renders current editor content)

i18n: added common.description, templateConfig.descriptionPlaceholder

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 19:28:00 +03:00

99 lines
2.6 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { EditorView, placeholder as cmPlaceholder } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { StreamLanguage } from '@codemirror/language';
import { oneDark } from '@codemirror/theme-one-dark';
import { getTheme } from '$lib/theme.svelte';
let { value = '', onchange, rows = 6, placeholder = '' } = $props<{
value: string;
onchange: (val: string) => void;
rows?: number;
placeholder?: string;
}>();
let container: HTMLDivElement;
let view: EditorView;
const theme = getTheme();
// Simple Jinja2 stream parser for syntax highlighting
const jinjaLang = StreamLanguage.define({
token(stream) {
// Jinja2 comment {# ... #}
if (stream.match('{#')) {
stream.skipTo('#}') && stream.match('#}');
return 'comment';
}
// Jinja2 expression {{ ... }}
if (stream.match('{{')) {
while (!stream.eol()) {
if (stream.match('}}')) return 'variableName';
stream.next();
}
return 'variableName';
}
// Jinja2 statement {% ... %}
if (stream.match('{%')) {
while (!stream.eol()) {
if (stream.match('%}')) return 'keyword';
stream.next();
}
return 'keyword';
}
// Regular text
while (stream.next()) {
if (stream.peek() === '{') break;
}
return null;
},
});
onMount(() => {
const extensions = [
jinjaLang,
EditorView.updateListener.of((update) => {
if (update.docChanged) {
onchange(update.state.doc.toString());
}
}),
EditorView.lineWrapping,
EditorView.theme({
'&': { fontSize: '13px', fontFamily: "'Consolas', 'Monaco', 'Courier New', monospace" },
'.cm-content': { minHeight: `${rows * 1.5}em`, padding: '8px' },
'.cm-editor': { borderRadius: '0.375rem', border: '1px solid var(--color-border)' },
'.cm-focused': { outline: '2px solid var(--color-primary)', outlineOffset: '0px' },
// Jinja2 syntax colors
'.ͼc': { color: '#e879f9' }, // keyword ({% %}) - purple
'.ͼd': { color: '#38bdf8' }, // variableName ({{ }}) - blue
'.ͼ5': { color: '#6b7280' }, // comment ({# #}) - gray
}),
];
if (theme.isDark) {
extensions.push(oneDark);
}
if (placeholder) {
extensions.push(cmPlaceholder(placeholder));
}
view = new EditorView({
state: EditorState.create({ doc: value, extensions }),
parent: container,
});
return () => view.destroy();
});
$effect(() => {
if (view && view.state.doc.toString() !== value) {
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: value },
});
}
});
</script>
<div bind:this={container}></div>