Some checks failed
Validate / Hassfest (push) Has been cancelled
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>
99 lines
2.6 KiB
Svelte
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>
|