readysite / website / frontend / hooks / useMonaco.js
8.5 KB
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 };
}
← Back