feat(code-editor): add support for HTML templates in JavaScript

This commit is contained in:
Zeke Zhang
2025-09-27 02:43:49 +08:00
parent fa7aa08ee4
commit 7fba933094
4 changed files with 110 additions and 2 deletions
+1
View File
@@ -30,6 +30,7 @@
"@nocobase/evaluators": "1.9.0-alpha.16",
"@nocobase/sdk": "1.9.0-alpha.16",
"@nocobase/utils": "1.9.0-alpha.16",
"@codemirror/lang-html": "^6.4.10",
"ahooks": "^3.7.2",
"antd": "5.24.2",
"antd-style": "3.7.1",
@@ -0,0 +1,41 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { syntaxTree } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
import { describe, expect, it } from 'vitest';
import { javascriptWithHtmlTemplates } from '../javascriptHtmlTemplate';
describe('javascriptWithHtmlTemplates', () => {
it('mounts html parser for template literal segments', () => {
const support = javascriptWithHtmlTemplates();
const state = EditorState.create({
doc: 'const template = `<div class="box">${value}</div>`;',
extensions: [support],
});
const htmlPos = state.doc.toString().indexOf('div class');
const node = syntaxTree(state).resolveInner(htmlPos, 1);
expect(node.name).toBe('TagName');
});
it('preserves interpolation nodes from javascript parser', () => {
const support = javascriptWithHtmlTemplates();
const state = EditorState.create({
doc: 'const template = `<div>${value}</div>`;',
extensions: [support],
});
const variablePos = state.doc.toString().indexOf('value');
const node = syntaxTree(state).resolveInner(variablePos, 1);
expect(node.name).toBe('VariableName');
});
});
@@ -11,12 +11,12 @@ import React, { useEffect, useRef } from 'react';
// CodeMirror imports
import { autocompletion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { lintGutter } from '@codemirror/lint';
import { EditorState } from '@codemirror/state';
import { oneDark } from '@codemirror/theme-one-dark';
import { basicSetup, EditorView } from 'codemirror';
import completions from './completions';
import { javascriptWithHtmlTemplates } from './javascriptHtmlTemplate';
import { createJavaScriptLinter } from './linter';
import { InjectableRendingEventTrigger, InjectableRendingEventTriggerProps } from '../decorator';
import { Flex } from 'antd';
@@ -83,7 +83,7 @@ const InnerCodeEditor: React.FC<CodeEditorProps> = ({
const extensions = [
basicSetup,
javascript(),
javascriptWithHtmlTemplates(),
autocompletion({
override: [createCustomCompletion()],
closeOnBlur: false,
@@ -0,0 +1,66 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { html, htmlLanguage } from '@codemirror/lang-html';
import { LanguageSupport } from '@codemirror/language';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { parseMixed } from '@lezer/common';
import type { SyntaxNode } from '@lezer/common';
interface OverlayRange {
from: number;
to: number;
}
function buildTemplateHtmlRanges(node: SyntaxNode): OverlayRange[] {
const ranges: OverlayRange[] = [];
const open = node.from + 1;
const close = node.to - 1;
let cursor = open;
for (let child = node.firstChild; child; child = child.nextSibling) {
if (child.type.name === 'Interpolation') {
if (child.from > cursor) {
ranges.push({ from: cursor, to: child.from });
}
cursor = Math.max(cursor, child.to);
}
}
if (cursor < close) {
ranges.push({ from: cursor, to: close });
}
return ranges.filter((range) => range.to > range.from);
}
export function javascriptWithHtmlTemplates(): LanguageSupport {
const baseJavascript = javascript();
const htmlSupport = html();
const mixedLanguage = javascriptLanguage.configure({
wrap: parseMixed((node) => {
if (node.type.name !== 'TemplateString') {
return null;
}
const ranges = buildTemplateHtmlRanges(node.node);
if (!ranges.length) {
return null;
}
return {
parser: htmlLanguage.parser,
overlay: ranges,
};
}),
});
return new LanguageSupport(mixedLanguage, [baseJavascript.support, htmlSupport.support]);
}