useMonaco.js
import { useState, useEffect } from 'react';
// Singleton promise for one-time Monaco load
let monacoLoadPromise = null;
let monacoInstance = null;
function loadMonaco() {
if (monacoInstance) {
return Promise.resolve(monacoInstance);
}
if (monacoLoadPromise) {
return monacoLoadPromise;
}
monacoLoadPromise = new Promise((resolve, reject) => {
// Add AMD loader if not present
if (!window.require) {
const loaderScript = document.createElement('script');
loaderScript.src = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs/loader.js';
loaderScript.onload = () => initMonaco(resolve, reject);
loaderScript.onerror = () => reject(new Error('Failed to load Monaco loader'));
document.head.appendChild(loaderScript);
} else {
initMonaco(resolve, reject);
}
});
return monacoLoadPromise;
}
function initMonaco(resolve, reject) {
window.require.config({
paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs' }
});
window.require(['vs/editor/editor.main'], function() {
monacoInstance = window.monaco;
registerGoHtmlLanguage(monacoInstance);
resolve(monacoInstance);
}, reject);
}
// Register Go HTML template language with syntax highlighting
function registerGoHtmlLanguage(monaco) {
// Check if already registered
if (monaco.languages.getLanguages().some(lang => lang.id === 'gohtml')) {
return;
}
monaco.languages.register({ id: 'gohtml' });
monaco.languages.setMonarchTokensProvider('gohtml', {
defaultToken: '',
tokenPostfix: '.html',
// Go template keywords
templateKeywords: [
'if', 'else', 'end', 'range', 'with', 'define', 'template', 'block',
],
// Go template built-in functions
builtinFunctions: [
'and', 'or', 'not', 'eq', 'ne', 'lt', 'le', 'gt', 'ge',
'len', 'index', 'slice', 'print', 'printf', 'println', 'js', 'urlquery',
'call', 'html',
],
// ReadySite template functions
templateFunctions: [
// Data functions
'documents', 'document', 'collection', 'page', 'pages', 'published_pages',
'partial', 'partials',
// Site functions
'site_name', 'site_description',
// Request functions
'user', 'req', 'query', 'path',
// Time functions
'now', 'date',
// String functions
'substr', 'truncate', 'upper', 'lower', 'title', 'join', 'split',
'contains', 'replace', 'trim', 'hasPrefix', 'hasSuffix',
// Utility functions
'default',
// Math functions
'add', 'sub', 'mul', 'div', 'mod', 'seq',
// Number formatting
'number', 'currency',
],
// HTML tag names
tags: [
'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo',
'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup',
'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed',
'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd',
'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'meta', 'meter', 'nav',
'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress',
'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'slot', 'small', 'source',
'span', 'strong', 'style', 'sub', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'template',
'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr',
],
tokenizer: {
root: [
// Go template expressions {{ ... }}
[/\{\{-?/, { token: 'delimiter.template', next: '@gotemplate' }],
// HTML comments
[/<!--/, 'comment', '@htmlComment'],
// DOCTYPE
[/<!DOCTYPE/i, 'metatag', '@doctype'],
// HTML tags
[/<\/?/, { token: 'delimiter.html', next: '@tag' }],
// Whitespace and text
[/[^<{]+/, ''],
],
// Go template content
gotemplate: [
// Closing delimiter
[/-?\}\}/, { token: 'delimiter.template', next: '@pop' }],
// Template keywords (if, else, end, range, etc.)
[/\b(if|else|end|range|with|define|template|block)\b/, 'keyword.template'],
// Built-in Go template functions
[/\b(and|or|not|eq|ne|lt|le|gt|ge|len|index|slice|print|printf|println|js|urlquery|call|html)\b/, 'builtin.template'],
// ReadySite template functions (data, site, request, etc.)
[/\b(documents|document|collection|page|pages|published_pages|partial|partials|site_name|site_description|user|req|query|path|now|date|substr|truncate|upper|lower|title|join|split|contains|replace|trim|hasPrefix|hasSuffix|default|add|sub|mul|div|mod|seq|number|currency)\b/, 'function.template'],
// Boolean and nil
[/\b(true|false|nil)\b/, 'constant.template'],
// Variables starting with $ or .
[/\$[a-zA-Z_][a-zA-Z0-9_]*/, 'variable.template'],
[/\.[a-zA-Z_][a-zA-Z0-9_]*/, 'variable.template'],
// Pipe operator
[/\|/, 'operator.template'],
// Assignment
[/:=/, 'operator.template'],
// Strings
[/"([^"\\]|\\.)*"/, 'string.template'],
[/`[^`]*`/, 'string.template'],
// Numbers
[/\d+(\.\d+)?/, 'number.template'],
// Identifiers (other function calls, etc.)
[/[a-zA-Z_][a-zA-Z0-9_]*/, 'identifier.template'],
// Whitespace
[/\s+/, ''],
],
// HTML tag
tag: [
// Go template in attributes
[/\{\{-?/, { token: 'delimiter.template', next: '@gotemplate' }],
// Tag name
[/[a-zA-Z][\w-]*/, {
cases: {
'@tags': 'tag.html',
'@default': 'tag.html',
}
}],
// Attribute name
[/[a-zA-Z][\w-]*(?=\s*=)/, 'attribute.name.html'],
[/[a-zA-Z][\w-]*/, 'attribute.name.html'],
// Attribute value
[/=/, 'delimiter.html'],
[/"[^"]*"/, 'attribute.value.html'],
[/'[^']*'/, 'attribute.value.html'],
// Self-closing and closing
[/\/?>/, { token: 'delimiter.html', next: '@pop' }],
// Whitespace
[/\s+/, ''],
],
// HTML comment
htmlComment: [
[/-->/, 'comment', '@pop'],
[/./, 'comment'],
],
// DOCTYPE
doctype: [
[/>/, 'metatag', '@pop'],
[/./, 'metatag'],
],
},
});
// Define theme rules for Go template tokens
monaco.editor.defineTheme('gohtml-dark', {
base: 'vs-dark',
inherit: true,
rules: [
// Template delimiters {{ }}
{ token: 'delimiter.template', foreground: 'C586C0', fontStyle: 'bold' },
// Template keywords (if, else, end, range, etc.)
{ token: 'keyword.template', foreground: 'C586C0' },
// Go built-in functions (eq, ne, len, etc.)
{ token: 'builtin.template', foreground: '569CD6' },
// ReadySite functions (site_name, documents, partial, etc.)
{ token: 'function.template', foreground: '4EC9B0' },
// Variables ($var, .Field)
{ token: 'variable.template', foreground: '9CDCFE' },
// Strings
{ token: 'string.template', foreground: 'CE9178' },
// Numbers
{ token: 'number.template', foreground: 'B5CEA8' },
// Constants (true, false, nil)
{ token: 'constant.template', foreground: '569CD6' },
// Operators (|, :=)
{ token: 'operator.template', foreground: 'D4D4D4' },
// Other identifiers
{ token: 'identifier.template', foreground: 'DCDCAA' },
// HTML elements
{ token: 'tag.html', foreground: '569CD6' },
{ token: 'attribute.name.html', foreground: '9CDCFE' },
{ token: 'attribute.value.html', foreground: 'CE9178' },
{ token: 'delimiter.html', foreground: '808080' },
{ token: 'comment', foreground: '6A9955' },
{ token: 'metatag', foreground: '569CD6' },
],
colors: {},
});
}
export function useMonaco() {
const [monaco, setMonaco] = useState(monacoInstance);
const [loading, setLoading] = useState(!monacoInstance);
const [error, setError] = useState(null);
useEffect(() => {
if (monacoInstance) {
setMonaco(monacoInstance);
setLoading(false);
return;
}
loadMonaco()
.then((instance) => {
setMonaco(instance);
setLoading(false);
})
.catch((err) => {
setError(err);
setLoading(false);
});
}, []);
return { monaco, loading, error };
}