/** * ARE-HTML Monaco language registration. * * Usage: * * import * as monaco from 'monaco-editor'; * import cfg from './arehtml.monaco.json'; * import { registerAreHtml } from './arehtml.monaco'; * * registerAreHtml(monaco, cfg); * monaco.editor.create(el, { value, language: 'arehtml', theme: 'arehtml-dark' }); * * The JSON config stores regex patterns as strings; this loader converts them * to RegExp objects where Monaco requires it. No other transformations are * performed, so the JSON remains the single source of truth. */ export interface AreHtmlMonacoConfig { languageId: string; displayName: string; fileExtensions: string[]; aliases: string[]; mimeTypes: string[]; configuration: any; monarch: any; themeRules: { rules: any[] }; completions?: { directives?: { label: string; insertText: string; documentation?: string }[]; events?: { label: string; insertText: string; documentation?: string }[]; bindings?: { label: string; insertText: string; documentation?: string }[]; interpolation?: { label: string; insertText: string; documentation?: string }[]; }; } /** * Convert any string regex literals in the Monarch tokenizer rules to actual * RegExp instances. Monaco's Monarch accepts both, but RegExp gives slightly * better error messages and parity with the in-source documentation. */ function normalizeMonarch(monarch: any): any { const out: any = { ...monarch, tokenizer: {} }; for (const stateName of Object.keys(monarch.tokenizer)) { const state = monarch.tokenizer[stateName]; out.tokenizer[stateName] = state.map((rule: any) => { if (Array.isArray(rule) && typeof rule[0] === 'string') { try { return [new RegExp(rule[0]), rule[1]]; } catch { return rule; } } return rule; }); } return out; } export function registerAreHtml(monaco: any, cfg: AreHtmlMonacoConfig) { monaco.languages.register({ id: cfg.languageId, extensions: cfg.fileExtensions, aliases: cfg.aliases, mimetypes: cfg.mimeTypes, }); monaco.languages.setLanguageConfiguration(cfg.languageId, cfg.configuration); monaco.languages.setMonarchTokensProvider(cfg.languageId, normalizeMonarch(cfg.monarch)); monaco.editor.defineTheme('arehtml-dark', { base: 'vs-dark', inherit: true, rules: cfg.themeRules.rules, colors: {}, }); monaco.editor.defineTheme('arehtml-light', { base: 'vs', inherit: true, rules: cfg.themeRules.rules, colors: {}, }); if (cfg.completions) { monaco.languages.registerCompletionItemProvider(cfg.languageId, { triggerCharacters: ['$', '@', ':', '{'], provideCompletionItems(model: any, position: any) { const word = model.getWordUntilPosition(position); const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, endColumn: word.endColumn, }; const kind = monaco.languages.CompletionItemKind; const insertRule = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; const c = cfg.completions!; const suggestions = [ ...(c.directives || []).map(s => ({ ...s, kind: kind.Keyword, insertTextRules: insertRule, range, })), ...(c.events || []).map(s => ({ ...s, kind: kind.Event, insertTextRules: insertRule, range, })), ...(c.bindings || []).map(s => ({ ...s, kind: kind.Property, insertTextRules: insertRule, range, })), ...(c.interpolation || []).map(s => ({ ...s, kind: kind.Snippet, insertTextRules: insertRule, range, })), ]; return { suggestions }; } }); } }