Skip to content

Instantly share code, notes, and snippets.

@rgon
Created October 9, 2025 21:13
Show Gist options
  • Select an option

  • Save rgon/1bd6bf7800aa8c4eedc5b1674a4f1f15 to your computer and use it in GitHub Desktop.

Select an option

Save rgon/1bd6bf7800aa8c4eedc5b1674a4f1f15 to your computer and use it in GitHub Desktop.
Monaco Editor + Svelte 5, with YAML Support, schema validation
<script lang="ts">
import YamlEditor from "./MonacoEditor.svelte";
let yamlContent = $state(`# Welcome to YAML Editor
# Start typing...
name: John Doe
age: 30
occupation: Software Engineer
skills:
- JavaScript
- TypeScript
- YAML
`);
let selectedTheme = $state<'vs-dark' | 'vs-light'>('vs-dark');
let selectedLanguage = $state<'yaml' | 'json' | 'javascript' | 'typescript' | 'html' | 'css'>('yaml');
const themes = [
{ value: 'vs-dark', label: 'Dark' },
{ value: 'vs-light', label: 'Light' }
] as const;
const languages = [
{ value: 'yaml', label: 'YAML' },
{ value: 'json', label: 'JSON' },
{ value: 'javascript', label: 'JavaScript' },
{ value: 'typescript', label: 'TypeScript' },
{ value: 'html', label: 'HTML' },
{ value: 'css', label: 'CSS' }
] as const;
// Example YAML/JSON schema for validation
const personSchema = {
uri: 'http://myserver/person-schema.json',
fileMatch: ['*'],
schema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'The person\'s full name'
},
age: {
type: 'integer',
description: 'How old is the person in years?',
minimum: 0,
maximum: 120
},
occupation: {
type: 'string',
enum: ['Software Engineer', 'Designer', 'Manager', 'Other'],
description: 'The person\'s job title'
},
skills: {
type: 'array',
items: {
type: 'string'
},
description: 'List of skills'
}
},
required: ['name', 'age']
}
};
// JSON equivalent content for testing
const jsonContent = $state(`{
"name": "John Doe",
"age": 30,
"occupation": "Software Engineer",
"skills": [
"JavaScript",
"TypeScript",
"JSON"
]
}`);
// Switch content based on language
$effect(() => {
if (selectedLanguage === 'json') {
yamlContent = jsonContent;
} else if (selectedLanguage === 'yaml') {
yamlContent = `# Welcome to YAML Editor
# Start typing...
name: John Doe
age: 30
occupation: Software Engineer
skills:
- JavaScript
- TypeScript
- YAML
`;
}
});
</script>
<div class="my-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold">Monaco Editor Demo</h2>
<div class="flex gap-4">
<!-- Language Selector -->
<div class="flex items-center gap-2">
<label for="language" class="font-medium text-sm">Language:</label>
<select
id="language"
bind:value={selectedLanguage}
class="px-3 py-1.5 border border-gray-300 rounded-md bg-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{#each languages as lang}
<option value={lang.value}>{lang.label}</option>
{/each}
</select>
</div>
<!-- Theme Toggle -->
<div class="flex items-center gap-2">
<label for="theme" class="font-medium text-sm">Theme:</label>
<select
id="theme"
bind:value={selectedTheme}
class="px-3 py-1.5 border border-gray-300 rounded-md bg-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{#each themes as theme}
<option value={theme.value}>{theme.label}</option>
{/each}
</select>
</div>
</div>
</div>
<YamlEditor
bind:value={yamlContent}
language={selectedLanguage}
theme={selectedTheme}
height="h-96"
json_yaml_schema={selectedLanguage === 'yaml' || selectedLanguage === 'json' ? personSchema : undefined}
/>
</div>
<div class="my-4">
<h3 class="text-lg font-semibold">Current Content:</h3>
<pre class="bg-gray-100 p-4 rounded mt-2 overflow-auto">{yamlContent}</pre>
{#if selectedLanguage === 'yaml' || selectedLanguage === 'json'}
<div class="mt-4 p-4 bg-blue-50 border border-blue-200 rounded">
<h4 class="font-semibold text-blue-800 mb-2">
{selectedLanguage.toUpperCase()} Schema Validation Active
</h4>
<p class="text-sm text-blue-700">
This editor validates against a custom schema. Try:
</p>
<ul class="text-sm text-blue-700 list-disc list-inside mt-2">
<li>Set age to a negative number (will show an error)</li>
<li>Use an invalid occupation value (will suggest valid options)</li>
<li>Remove required fields like 'name' or 'age' (will show warnings)</li>
<li>Hover over properties to see descriptions</li>
<li>Use Ctrl+Space for auto-completion suggestions</li>
</ul>
</div>
{/if}
</div>
export type SupportedLanguage = 'yaml' | 'json' | 'javascript' | 'typescript' | 'html' | 'css'
export interface JsonYamlSchema {
uri: string;
fileMatch: string[];
schema?: any;
}
<script lang="ts">
import { browser } from '$app/environment'
import YamlWorker from './yaml.worker?worker'
import type { SupportedLanguage, JsonYamlSchema } from './editor_types';
let editor: HTMLDivElement | null = $state(null)
let monacoEditor: any = null
let monacoInstance: any = null
let yamlConfig: any = null
let isLoaded = $state(false)
let isUpdatingFromEditor = false
interface Props {
value?: string;
language?: SupportedLanguage;
theme?: string;
height?: string;
minimap?: boolean;
readOnly?: boolean;
json_yaml_schema?: JsonYamlSchema | JsonYamlSchema[];
}
let {
value = $bindable(''),
language = 'yaml',
theme = 'vs-dark',
height = 'h-96',
minimap = true,
readOnly = false,
json_yaml_schema = undefined
}: Props = $props()
async function createEditor(container: HTMLDivElement) {
try {
// Dynamic import to prevent SSR issues
const [monaco, { configureMonacoYaml }] = await Promise.all([
import('monaco-editor'),
import('monaco-yaml')
])
// Store monaco instance for later use
monacoInstance = monaco
// Configure Monaco Environment for workers
self.MonacoEnvironment = {
getWorker(_: string, label: string) {
switch (label) {
case 'editorWorkerService':
return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker.js', import.meta.url), { type: 'module' })
case 'css':
case 'less':
case 'scss':
return new Worker(new URL('monaco-editor/esm/vs/language/css/css.worker.js', import.meta.url), { type: 'module' })
case 'html':
case 'handlebars':
case 'razor':
return new Worker(new URL('monaco-editor/esm/vs/language/html/html.worker.js', import.meta.url), { type: 'module' })
case 'json':
return new Worker(new URL('monaco-editor/esm/vs/language/json/json.worker.js', import.meta.url), { type: 'module' })
case 'javascript':
case 'typescript':
return new Worker(new URL('monaco-editor/esm/vs/language/typescript/ts.worker.js', import.meta.url), { type: 'module' })
case 'yaml':
return new YamlWorker()
default:
throw new Error(`Unknown label ${label}`)
}
}
}
// Configure YAML language support (only if using YAML)
if (language === 'yaml') {
// Prepare schemas array
const schemas = json_yaml_schema
? (Array.isArray(json_yaml_schema) ? json_yaml_schema : [json_yaml_schema])
: [];
yamlConfig = configureMonacoYaml(monaco, {
enableSchemaRequest: true,
hover: true,
completion: true,
validate: true,
format: true,
schemas: schemas
})
}
// Configure JSON language support (only if using JSON)
if (language === 'json') {
// Prepare schemas array
const schemas = json_yaml_schema
? (Array.isArray(json_yaml_schema) ? json_yaml_schema : [json_yaml_schema])
: [];
// Configure JSON schemas
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: schemas.map(s => ({
uri: s.uri,
fileMatch: s.fileMatch,
schema: s.schema
}))
})
}
// Create the editor instance
monacoEditor = monaco.editor.create(container, {
value: value,
language: language,
theme: theme,
automaticLayout: true,
readOnly: readOnly,
minimap: {
enabled: minimap
}
})
// Listen to content changes and update the bound value
monacoEditor.onDidChangeModelContent(() => {
isUpdatingFromEditor = true
value = monacoEditor.getValue()
isUpdatingFromEditor = false
})
isLoaded = true
} catch (error) {
console.error('Failed to load Monaco Editor:', error)
}
}
async function destroyEditor() {
if (monacoEditor) {
monacoEditor.dispose()
monacoEditor = null
}
monacoInstance = null
isLoaded = false
}
// Initialize editor
$effect(() => {
if (browser && editor) createEditor(editor)
return () => {
if (browser) destroyEditor()
}
})
// Update editor value when prop changes externally
$effect(() => {
const currentValue = value // Track the value prop
if (monacoEditor && !isUpdatingFromEditor && isLoaded) {
const editorValue = monacoEditor.getValue()
if (currentValue !== editorValue) {
const currentPosition = monacoEditor.getPosition()
monacoEditor.setValue(currentValue)
if (currentPosition) {
monacoEditor.setPosition(currentPosition)
}
}
}
})
// Update editor theme when prop changes
$effect(() => {
const currentTheme = theme // Track the theme prop
if (monacoInstance && monacoEditor && isLoaded) {
try {
monacoInstance.editor.setTheme(currentTheme)
console.log('Theme updated to:', currentTheme)
} catch (error) {
console.error('Failed to update theme:', error)
}
}
})
// Update editor language when prop changes
$effect(() => {
const currentLanguage = language // Track the language prop
if (monacoInstance && monacoEditor && isLoaded) {
try {
const model = monacoEditor.getModel()
if (model) {
monacoInstance.editor.setModelLanguage(model, currentLanguage)
console.log('Language updated to:', currentLanguage)
}
} catch (error) {
console.error('Failed to update language:', error)
}
}
})
// Update read-only state when prop changes
$effect(() => {
const currentReadOnly = readOnly // Track the readOnly prop
if (monacoEditor && isLoaded) {
monacoEditor.updateOptions({ readOnly: currentReadOnly })
}
})
// Update minimap when prop changes
$effect(() => {
const currentMinimap = minimap // Track the minimap prop
if (monacoEditor && isLoaded) {
monacoEditor.updateOptions({ minimap: { enabled: currentMinimap } })
}
})
// Update YAML/JSON schema when prop changes
$effect(() => {
const currentSchema = json_yaml_schema // Track the json_yaml_schema prop
const currentLang = language // Track language changes
if (monacoInstance && isLoaded) {
try {
const schemas = currentSchema
? (Array.isArray(currentSchema) ? currentSchema : [currentSchema])
: [];
// Update YAML schema
if (currentLang === 'yaml' && yamlConfig) {
yamlConfig.update({
enableSchemaRequest: true,
hover: true,
completion: true,
validate: true,
format: true,
schemas: schemas
})
console.log('YAML schema updated:', schemas)
}
// Update JSON schema
if (currentLang === 'json') {
monacoInstance.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: schemas.map(s => ({
uri: s.uri,
fileMatch: s.fileMatch,
schema: s.schema
}))
})
console.log('JSON schema updated:', schemas)
}
} catch (error) {
console.error('Failed to update schema:', error)
}
}
})
</script>
{#if browser}
<div class="w-full {height} border border-gray-300" bind:this={editor}>
{#if !isLoaded}
<div class="w-full h-full flex items-center justify-center bg-gray-50">
<div class="text-gray-500">Loading Monaco Editor...</div>
</div>
{/if}
</div>
{:else}
<div class="w-full {height} border border-gray-300 flex items-center justify-center bg-gray-50">
<div class="text-gray-500">Monaco Editor (Client-side only)</div>
</div>
{/if}
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
optimizeDeps: {
include: ['monaco-editor']
},
ssr: {
noExternal: [],
external: ['monaco-editor', 'monaco-yaml']
},
server: {
fs: {
// Allow serving files from the monaco-editor package
allow: ['..']
}
},
build: {
rollupOptions: {
output: {
manualChunks: {
'monaco-editor': ['monaco-editor']
}
}
}
}
});
import 'monaco-yaml/yaml.worker'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment