Created
October 9, 2025 21:13
-
-
Save rgon/1bd6bf7800aa8c4eedc5b1674a4f1f15 to your computer and use it in GitHub Desktop.
Monaco Editor + Svelte 5, with YAML Support, schema validation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export type SupportedLanguage = 'yaml' | 'json' | 'javascript' | 'typescript' | 'html' | 'css' | |
| export interface JsonYamlSchema { | |
| uri: string; | |
| fileMatch: string[]; | |
| schema?: any; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'] | |
| } | |
| } | |
| } | |
| } | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'monaco-yaml/yaml.worker' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment