@coding-script/script-engine 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,20 +1,21 @@
1
1
  [![npm](https://img.shields.io/npm/v/@coding-script/script-engine.svg)](https://www.npmjs.com/package/@coding-script/script-engine)
2
2
  # Script Engine
3
3
 
4
- 基于 React + CodeMirror 6 Groovy 脚本编辑器组件库,提供语法高亮、动态类型自动补全、属性面板等功能。
4
+ 基于 React + CodeMirror 6 的多语言脚本编辑器组件库,内置支持 Groovy 和 JavaScript,提供语法高亮、动态类型自动补全、属性面板等功能。
5
5
 
6
6
  ## 功能特性
7
7
 
8
- - **Groovy 语法高亮**:基于 CodeMirror 6,支持完整的 Groovy/Java 语法着色
8
+ - **多语言支持**:内置 Groovy JavaScript 语言支持,支持通过 `LanguageConfig` 扩展任意语言
9
+ - **语法高亮**:基于 CodeMirror 6,通过 `@codemirror/lang-*` 系列包提供各语言的语法着色
9
10
  - **动态类型自动补全**:基于 `ScriptMetadata` 提供变量名补全和点号链式访问补全(如 `request.test.name`)
10
- - **Groovy 语法提示**:内置 `if`/`for`/`while`/`println`/`return` 等常用 Groovy 语法片段
11
- - **代码格式化**:内置 Groovy 格式化器,也可通过 `onFormat` 自定义格式化逻辑
11
+ - **语言语法提示**:内置各语言的常用语法片段(`if`/`for`/`while`/`return` 等),支持 tab-stop 占位符
12
+ - **代码格式化**:Groovy 内置格式化器,也可通过 `onFormat` 自定义格式化逻辑
12
13
  - **属性面板**:右侧侧边栏展示主函数签名、函数入参、绑定参数、数据类型(字段和方法),支持折叠/展开和拖拽调节宽度
13
14
  - **脚本说明**:工具栏"脚本说明"按钮,点击展开/收起脚本描述弹框,支持多行文本和 `key: value` 格式高亮
14
15
  - **主题切换**:暗色/亮色两套主题,编辑器、补全弹窗、属性面板同步切换,编辑器内部管理主题状态
15
16
  - **全屏模式**:支持 CSS 全屏(`position: fixed` 覆盖视口),不影响编辑器内容
16
17
  - **自定义工具栏**:通过 `toolbarExtra` 传入任意 React 节点
17
- - **热更新**:主题和 metadata 变化时通过 Compartment 热更新,不重建编辑器,不丢失用户输入
18
+ - **热更新**:主题、语言和 metadata 变化时通过 Compartment 热更新,不重建编辑器,不丢失用户输入
18
19
 
19
20
  ## 安装
20
21
 
@@ -77,6 +78,7 @@ function App() {
77
78
  <ScriptCodeEditor
78
79
  value="def run(request){\n return request.count;\n}\n"
79
80
  title="Groovy 脚本编辑器"
81
+ language="groovy"
80
82
  defaultTheme="dark"
81
83
  metadata={metadata}
82
84
  enableThemeToggle
@@ -92,6 +94,32 @@ function App() {
92
94
  }
93
95
  ```
94
96
 
97
+ ### JavaScript 用法
98
+
99
+ ```tsx
100
+ import { ScriptCodeEditor } from '@coding-script/script-engine';
101
+
102
+ function App() {
103
+ return (
104
+ <ScriptCodeEditor
105
+ value="function run(request) {\n return request.count;\n}\n"
106
+ title="JavaScript 脚本编辑器"
107
+ language="javascript"
108
+ defaultTheme="dark"
109
+ metadata={metadata}
110
+ enableThemeToggle
111
+ enableFormat
112
+ enableCompile
113
+ enableFullscreen
114
+ onFormat={() => {
115
+ // JavaScript 无内置格式化器,需自行提供
116
+ // 例如使用 prettier
117
+ }}
118
+ />
119
+ );
120
+ }
121
+ ```
122
+
95
123
  ## Props
96
124
 
97
125
  | 属性 | 类型 | 默认值 | 说明 |
@@ -99,7 +127,8 @@ function App() {
99
127
  | `value` | `string` | `undefined` | 代码内容 |
100
128
  | `readonly` | `boolean` | `false` | 是否只读 |
101
129
  | `onChange` | `(value: string) => void` | `undefined` | 代码变化回调 |
102
- | `placeholder` | `string` | `'请输入 Groovy 脚本...'` | 空内容占位符 |
130
+ | `language` | `string \| LanguageConfig` | `'groovy'` | 编程语言配置,支持内置语言名或自定义配置 |
131
+ | `placeholder` | `string` | 由语言配置决定 | 空内容占位符(覆盖语言默认值) |
103
132
  | `defaultTheme` | `'dark' \| 'light'` | `'dark'` | 初始主题,编辑器内部管理状态 |
104
133
  | `onThemeChange` | `(theme: 'dark' \| 'light') => void` | `undefined` | 主题切换通知回调 |
105
134
  | `title` | `string` | `undefined` | 工具栏标题 |
@@ -113,7 +142,7 @@ function App() {
113
142
  | 属性 | 类型 | 默认值 | 说明 |
114
143
  |---|---|---|---|
115
144
  | `enableThemeToggle` | `boolean` | `false` | 是否显示主题切换按钮 |
116
- | `enableFormat` | `boolean` | `false` | 是否显示格式化按钮(需配合 `onFormat`) |
145
+ | `enableFormat` | `boolean` | `false` | 是否显示格式化按钮(需配合 `onFormat` 或 `language.formatter`) |
117
146
  | `enableCompile` | `boolean` | `false` | 是否显示编译验证按钮(需配合 `onCompile`) |
118
147
  | `enableFullscreen` | `boolean` | `false` | 是否显示全屏按钮 |
119
148
 
@@ -121,7 +150,7 @@ function App() {
121
150
 
122
151
  | 属性 | 类型 | 默认值 | 说明 |
123
152
  |---|---|---|---|
124
- | `onFormat` | `() => void` | `undefined` | 格式化代码回调(不提供时使用内置 Groovy 格式化器) |
153
+ | `onFormat` | `() => void` | `undefined` | 格式化代码回调(优先级高于 `language.formatter`) |
125
154
  | `onCompile` | `(code: string) => void` | `undefined` | 编译/测试脚本回调 |
126
155
 
127
156
  ### 自定义工具栏
@@ -245,6 +274,87 @@ interface ScriptRequestInfo {
245
274
 
246
275
  > **注意**:`metadata` 必须是解析后的 JavaScript 对象,不能是 JSON 字符串。如果从 API 获取的是 JSON 字符串,需要先 `JSON.parse()` 再传入。
247
276
 
277
+ ## 多语言支持
278
+
279
+ ### 内置语言
280
+
281
+ 组件内置支持以下语言:
282
+
283
+ | 语言名 | 语法高亮 | 语法片段 | 内置格式化器 |
284
+ |---|---|---|---|
285
+ | `'groovy'` | `@codemirror/lang-java` | 21 个(`if`/`for`/`def`/`each`/`collect` 等) | ✅ `GroovyFormatter` |
286
+ | `'javascript'` | `@codemirror/lang-javascript` | 22 个(`if`/`for`/`function`/`=>`/`class` 等) | ❌(可通过 `onFormat` 提供) |
287
+
288
+ 使用内置语言只需传入语言名:
289
+
290
+ ```tsx
291
+ <ScriptCodeEditor language="groovy" />
292
+ <ScriptCodeEditor language="javascript" />
293
+ ```
294
+
295
+ ### 自定义语言扩展
296
+
297
+ 通过传入 `LanguageConfig` 对象可以支持任意语言:
298
+
299
+ ```tsx
300
+ import { python } from '@codemirror/lang-python';
301
+
302
+ const PYTHON_SNIPPETS = [
303
+ { label: 'def', type: 'keyword', apply: snippet('def ${name}(${params}):\n\t${}\n') },
304
+ { label: 'class', type: 'keyword', apply: snippet('class ${ClassName}:\n\tdef __init__(self):\n\t\t${}\n') },
305
+ { label: 'if', type: 'keyword', apply: snippet('if ${condition}:\n\t${}\n') },
306
+ { label: 'for', type: 'keyword', apply: snippet('for ${item} in ${iterable}:\n\t${}\n') },
307
+ { label: 'print', type: 'keyword', apply: snippet('print(${})') },
308
+ { label: 'return', type: 'keyword', apply: snippet('return ${}') },
309
+ // ... 更多语法片段
310
+ ];
311
+
312
+ <ScriptCodeEditor
313
+ language={{
314
+ name: 'python',
315
+ displayName: 'Python',
316
+ extension: () => python(),
317
+ keywordSnippets: PYTHON_SNIPPETS,
318
+ syntaxNodeNames: {
319
+ stringNodes: ['String'],
320
+ commentNodes: ['LineComment', 'BlockComment'],
321
+ },
322
+ placeholder: '请输入 Python 脚本...',
323
+ formatter: (code) => customPythonFormatter(code),
324
+ }}
325
+ />
326
+ ```
327
+
328
+ ### LanguageConfig 类型定义
329
+
330
+ ```typescript
331
+ interface LanguageConfig {
332
+ /** 语言标识名(小写) */
333
+ name: string;
334
+ /** 显示名称 */
335
+ displayName: string;
336
+ /** CodeMirror 语言扩展工厂 */
337
+ extension: () => Extension;
338
+ /** 关键字和语法片段列表 */
339
+ keywordSnippets: readonly Completion[];
340
+ /** 语法树节点名(用于判断字符串/注释位置,抑制自动补全) */
341
+ syntaxNodeNames: {
342
+ /** 字符串类节点名 */
343
+ stringNodes: string[];
344
+ /** 注释类节点名 */
345
+ commentNodes: string[];
346
+ };
347
+ /** 默认占位符文本(可选) */
348
+ placeholder?: string;
349
+ /** 内置格式化函数(可选,优先级低于 onFormat prop) */
350
+ formatter?: (code: string) => string;
351
+ }
352
+ ```
353
+
354
+ ### 语言切换热更新
355
+
356
+ 语言切换时通过 Compartment 热更新,不重建编辑器,不丢失用户已输入的内容。
357
+
248
358
  ## 自动补全
249
359
 
250
360
  提供 metadata 后,编辑器支持以下补全能力:
@@ -254,9 +364,11 @@ interface ScriptRequestInfo {
254
364
  | `re` | 弹出 `request`、`$request` 等变量 |
255
365
  | `request.` | 弹出 `count`、`test`、`isSupport` 等字段和方法 |
256
366
  | `request.test.` | 弹出 `id`、`name` 等链式访问成员 |
257
- | `if` / `for` / `while` | 弹出 Groovy 语法片段(含 tab-stop 占位符) |
367
+ | `if` / `for` / `while` | 弹出当前语言的语法片段(含 tab-stop 占位符) |
368
+
369
+ 不提供 metadata 时,仅启用当前语言的关键字和语法片段补全。
258
370
 
259
- 不提供 metadata 时,仅启用 Groovy 关键字和语法片段补全。
371
+ > 补全不会在字符串和注释内触发(通过各语言的语法树节点名判断)。
260
372
 
261
373
  ## 本地开发
262
374
 
@@ -275,7 +387,7 @@ pnpm run dev:app-pc
275
387
 
276
388
  ## 技术栈
277
389
 
278
- - **编辑器**:[CodeMirror 6](https://codemirror.net/)(`@codemirror/view`、`state`、`autocomplete`、`lang-java`、`theme-one-dark`)
390
+ - **编辑器**:[CodeMirror 6](https://codemirror.net/)(`@codemirror/view`、`state`、`autocomplete`、`lang-java`、`lang-javascript`、`theme-one-dark`)
279
391
  - **构建工具**:[Rslib](https://rslib.rs/)(库)+ [Rsbuild](https://rsbuild.dev/)(演示应用)
280
392
  - **包管理**:pnpm monorepo(workspaces)
281
393
  - **UI**:纯 CSS-in-JS(React `style` 对象),库本身不依赖 Ant Design
@@ -1,11 +1,25 @@
1
1
  import { type CompletionSource } from '@codemirror/autocomplete';
2
- import type { ScriptMetadata } from '../types';
2
+ import type { LanguageConfig, ScriptMetadata } from '../types';
3
+ /**
4
+ * 创建语言无关的自动补全源
5
+ *
6
+ * 当 metadata 存在时,同时提供元数据驱动的类型补全和语言关键字片段补全。
7
+ * 当 metadata 为空时,仅提供关键字片段补全。
8
+ *
9
+ * @param languageConfig 语言配置(提供 keywordSnippets 和 syntaxNodeNames)
10
+ * @param metadata 脚本元数据(可选,提供变量/字段/方法补全)
11
+ */
12
+ export declare function createCompletionSource(languageConfig: LanguageConfig, metadata?: ScriptMetadata): CompletionSource;
3
13
  /**
4
14
  * 创建绑定到指定元数据的 Groovy 自动补全源
5
15
  * 同时提供元数据驱动的类型补全和 Groovy 常用语法补全
16
+ *
17
+ * @deprecated 请使用 `createCompletionSource(languageConfig, metadata)` 代替
6
18
  */
7
19
  export declare function createGroovyCompletionSource(metadata: ScriptMetadata): CompletionSource;
8
20
  /**
9
21
  * 创建仅包含 Groovy 语法提示的补全源(无元数据时使用)
22
+ *
23
+ * @deprecated 请使用 `createCompletionSource(languageConfig)` 代替
10
24
  */
11
25
  export declare function createGroovyKeywordSource(): CompletionSource;
@@ -1,6 +1,6 @@
1
- import { snippet } from "@codemirror/autocomplete";
2
1
  import { syntaxTree } from "@codemirror/language";
3
2
  import { resolveChainType } from "./resolve.js";
3
+ import { groovyConfig } from "../languages/groovy";
4
4
  function formatFunctionSignature(fn) {
5
5
  const params = fn.parameters.map((p)=>`${p.name}: ${p.dataType}`).join(', ');
6
6
  return `(${params})`;
@@ -28,170 +28,21 @@ function buildMemberCompletions(typeInfo) {
28
28
  }
29
29
  return options;
30
30
  }
31
- function isInStringOrComment(context) {
31
+ function isInStringOrComment(context, syntaxNodeNames) {
32
32
  const node = syntaxTree(context.state).resolveInner(context.pos, -1);
33
33
  const name = node.name;
34
- return 'String' === name || 'StringLiteral' === name || 'LineComment' === name || 'BlockComment' === name || 'CharLiteral' === name;
34
+ return syntaxNodeNames.stringNodes.includes(name) || syntaxNodeNames.commentNodes.includes(name);
35
35
  }
36
- const GROOVY_SNIPPETS = [
37
- {
38
- label: 'if',
39
- detail: '条件语句',
40
- info: '如果条件为真则执行代码块',
41
- snippet: 'if (${condition}) {\n\t${}\n}',
42
- boost: -1
43
- },
44
- {
45
- label: 'else',
46
- detail: '否则分支',
47
- info: '条件为假时执行的代码块',
48
- snippet: 'else {\n\t${}\n}',
49
- boost: -2
50
- },
51
- {
52
- label: 'for',
53
- detail: '循环语句',
54
- info: '遍历集合或范围',
55
- snippet: 'for (${item} in ${collection}) {\n\t${}\n}',
56
- boost: -1
57
- },
58
- {
59
- label: 'while',
60
- detail: '循环语句',
61
- info: '当条件为真时循环执行',
62
- snippet: 'while (${condition}) {\n\t${}\n}',
63
- boost: -1
64
- },
65
- {
66
- label: 'switch',
67
- detail: '分支语句',
68
- info: '多条件分支选择',
69
- snippet: 'switch (${expression}) {\n\tcase ${value}:\n\t\t${}\n\t\tbreak\n\tdefault:\n\t\tbreak\n}',
70
- boost: -1
71
- },
72
- {
73
- label: 'try',
74
- detail: '异常捕获',
75
- info: '尝试执行代码并捕获异常',
76
- snippet: 'try {\n\t${}\n} catch (${Exception} e) {\n\t\n}',
77
- boost: -1
78
- },
79
- {
80
- label: 'def',
81
- detail: '变量/函数定义',
82
- info: '定义变量或函数',
83
- snippet: 'def ${name}',
84
- boost: -1
85
- },
86
- {
87
- label: 'println',
88
- detail: '打印输出',
89
- info: '输出到控制台并换行',
90
- snippet: 'println(${})',
91
- boost: 0
92
- },
93
- {
94
- label: 'print',
95
- detail: '打印输出',
96
- info: '输出到控制台不换行',
97
- snippet: 'print(${})',
98
- boost: -1
99
- },
100
- {
101
- label: 'return',
102
- detail: '返回值',
103
- info: '从函数中返回结果',
104
- snippet: 'return ${}',
105
- boost: 0
106
- },
107
- {
108
- label: 'each',
109
- detail: '遍历闭包',
110
- info: '遍历集合中的每个元素',
111
- snippet: 'each { ${item} ->\n\t${}\n}',
112
- boost: -1
113
- },
114
- {
115
- label: 'collect',
116
- detail: '转换闭包',
117
- info: '将集合元素转换为新集合',
118
- snippet: 'collect { ${item} ->\n\t${}\n}',
119
- boost: -1
120
- },
121
- {
122
- label: 'find',
123
- detail: '查找闭包',
124
- info: '查找集合中第一个匹配的元素',
125
- snippet: 'find { ${item} ->\n\t${}\n}',
126
- boost: -1
127
- },
128
- {
129
- label: 'findAll',
130
- detail: '查找全部',
131
- info: '查找集合中所有匹配的元素',
132
- snippet: 'findAll { ${item} ->\n\t${}\n}',
133
- boost: -1
134
- },
135
- {
136
- label: 'inject',
137
- detail: '累加闭包',
138
- info: '对集合进行累加操作',
139
- snippet: 'inject(${initial}) { ${acc}, ${item} ->\n\t${}\n}',
140
- boost: -1
141
- },
142
- {
143
- label: 'assert',
144
- detail: '断言',
145
- info: '验证条件是否为真',
146
- snippet: 'assert ${condition}',
147
- boost: -1
148
- },
149
- {
150
- label: 'new',
151
- detail: '创建实例',
152
- info: '创建新的对象实例',
153
- snippet: 'new ${ClassName}(${})',
154
- boost: -1
155
- },
156
- {
157
- label: 'class',
158
- detail: '类定义',
159
- info: '定义一个新的类',
160
- snippet: 'class ${ClassName} {\n\t${}\n}',
161
- boost: -1
162
- },
163
- {
164
- label: 'interface',
165
- detail: '接口定义',
166
- info: '定义一个新的接口',
167
- snippet: 'interface ${InterfaceName} {\n\t${}\n}',
168
- boost: -2
169
- },
170
- {
171
- label: 'throw',
172
- detail: '抛出异常',
173
- info: '抛出一个异常对象',
174
- snippet: 'throw new ${Exception}("${}")',
175
- boost: -1
176
- },
177
- {
178
- label: 'import',
179
- detail: '导入',
180
- info: '导入外部类或包',
181
- snippet: 'import ${}',
182
- boost: -2
183
- }
184
- ];
185
- function createGroovyCompletionSource(metadata) {
36
+ function createCompletionSource(languageConfig, metadata) {
186
37
  return function(context) {
187
38
  try {
188
- if (isInStringOrComment(context)) return null;
39
+ if (isInStringOrComment(context, languageConfig.syntaxNodeNames)) return null;
189
40
  const chainMatch = context.matchBefore(/[\w$]+(?:\.[\w$]+)*\.|[\w$]+/);
190
41
  if (!chainMatch) return null;
191
42
  const hasDot = chainMatch.text.includes('.');
192
43
  const parts = chainMatch.text.split('.');
193
44
  const isDotAccess = hasDot && parts.length >= 2;
194
- if (isDotAccess) {
45
+ if (isDotAccess && metadata) {
195
46
  const baseParts = parts.slice(0, -1);
196
47
  const currentPrefix = parts[parts.length - 1];
197
48
  const resolvedType = resolveChainType(baseParts, metadata);
@@ -205,28 +56,24 @@ function createGroovyCompletionSource(metadata) {
205
56
  };
206
57
  }
207
58
  const options = [];
208
- for (const bind of metadata.binds)options.push({
209
- label: bind.name,
210
- type: 'variable',
211
- detail: bind.description ? `${bind.dataType} — ${bind.description}` : bind.dataType,
212
- info: bind.description || '(绑定变量)',
213
- boost: 3
214
- });
215
- for (const req of metadata.requests)options.push({
216
- label: req.name,
217
- type: 'variable',
218
- detail: req.description ? `${req.dataType} — ${req.description}` : req.dataType,
219
- info: req.description || '(请求参数)',
220
- boost: 3
221
- });
222
- const prefix = chainMatch.text.toLowerCase();
223
- for (const s of GROOVY_SNIPPETS)if (s.label.toLowerCase().startsWith(prefix) || '' === prefix) options.push({
224
- label: s.label,
225
- type: 'keyword',
226
- detail: s.detail,
227
- info: s.info,
228
- apply: snippet(s.snippet),
229
- boost: s.boost
59
+ if (metadata) {
60
+ for (const bind of metadata.binds)options.push({
61
+ label: bind.name,
62
+ type: 'variable',
63
+ detail: bind.description ? `${bind.dataType} — ${bind.description}` : bind.dataType,
64
+ info: bind.description || '(绑定变量)',
65
+ boost: 3
66
+ });
67
+ for (const req of metadata.requests)options.push({
68
+ label: req.name,
69
+ type: 'variable',
70
+ detail: req.description ? `${req.dataType} — ${req.description}` : req.dataType,
71
+ info: req.description || '(请求参数)',
72
+ boost: 3
73
+ });
74
+ }
75
+ for (const s of languageConfig.keywordSnippets)options.push({
76
+ ...s
230
77
  });
231
78
  if (0 === options.length) return null;
232
79
  return {
@@ -239,31 +86,10 @@ function createGroovyCompletionSource(metadata) {
239
86
  }
240
87
  };
241
88
  }
89
+ function createGroovyCompletionSource(metadata) {
90
+ return createCompletionSource(groovyConfig, metadata);
91
+ }
242
92
  function createGroovyKeywordSource() {
243
- return function(context) {
244
- try {
245
- if (isInStringOrComment(context)) return null;
246
- const match = context.matchBefore(/[\w$]+/);
247
- if (!match) return null;
248
- const prefix = match.text.toLowerCase();
249
- const options = [];
250
- for (const s of GROOVY_SNIPPETS)if (s.label.toLowerCase().startsWith(prefix) || '' === prefix) options.push({
251
- label: s.label,
252
- type: 'keyword',
253
- detail: s.detail,
254
- info: s.info,
255
- apply: snippet(s.snippet),
256
- boost: s.boost
257
- });
258
- if (0 === options.length) return null;
259
- return {
260
- from: match.from,
261
- options,
262
- validFor: /^[\w$]*$/
263
- };
264
- } catch {
265
- return null;
266
- }
267
- };
93
+ return createCompletionSource(groovyConfig);
268
94
  }
269
- export { createGroovyCompletionSource, createGroovyKeywordSource };
95
+ export { createCompletionSource, createGroovyCompletionSource, createGroovyKeywordSource };
@@ -1,2 +1,2 @@
1
- export { createGroovyCompletionSource, createGroovyKeywordSource } from './completion-source';
1
+ export { createCompletionSource, createGroovyCompletionSource, createGroovyKeywordSource } from './completion-source';
2
2
  export { resolveChainType, resolveVariableType, resolveMemberType } from './resolve';
@@ -1,2 +1,2 @@
1
- export { createGroovyCompletionSource, createGroovyKeywordSource } from "./completion-source.js";
1
+ export { createCompletionSource, createGroovyCompletionSource, createGroovyKeywordSource } from "./completion-source.js";
2
2
  export { resolveChainType, resolveMemberType, resolveVariableType } from "./resolve.js";
@@ -41,6 +41,9 @@ function ensureScrollbarStyle() {
41
41
  .${SCROLL_CLASS}::-webkit-scrollbar-track { background: transparent; }
42
42
  body.se-panel-resizing { cursor: col-resize !important; user-select: none !important; }
43
43
  body.se-panel-resizing * { cursor: col-resize !important; user-select: none !important; }
44
+ .se-desc-tip p { margin: 0 0 4px 0; }
45
+ .se-desc-tip p:last-child { margin-bottom: 0; }
46
+ .se-desc-tip code { font-family: Menlo, Consolas, monospace; font-size: 11px; }
44
47
  `;
45
48
  document.head.appendChild(style);
46
49
  }
@@ -2,7 +2,7 @@ import react, { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { ToolbarButton } from "./toolbar-button.js";
3
3
  import { FormatIcon } from "./format-icon.js";
4
4
  import { MaximizeIcon, MinimizeIcon } from "./fullscreen-icon.js";
5
- import { themes } from "./theme-colors.js";
5
+ import { ensureScrollbarStyle, themes } from "./theme-colors.js";
6
6
  const HammerIcon = ({ color })=>/*#__PURE__*/ react.createElement("svg", {
7
7
  width: "14",
8
8
  height: "14",
@@ -93,6 +93,7 @@ const Toolbar = ({ title, theme, onThemeChange, enableThemeToggle, enableFormat,
93
93
  }, []);
94
94
  useEffect(()=>{
95
95
  if (!showDesc) return;
96
+ ensureScrollbarStyle();
96
97
  updateTipPos();
97
98
  window.addEventListener('scroll', updateTipPos, true);
98
99
  window.addEventListener('resize', updateTipPos);
@@ -207,6 +208,7 @@ const Toolbar = ({ title, theme, onThemeChange, enableThemeToggle, enableFormat,
207
208
  onClick: item.onClick
208
209
  })), toolbarExtra, showDesc && tipPos && descHtml && /*#__PURE__*/ react.createElement("div", {
209
210
  ref: tipRef,
211
+ className: "se-desc-tip",
210
212
  style: {
211
213
  position: 'fixed',
212
214
  top: tipPos.top,
@@ -220,7 +222,7 @@ const Toolbar = ({ title, theme, onThemeChange, enableThemeToggle, enableFormat,
220
222
  borderRadius: 6,
221
223
  color: colors.text,
222
224
  fontSize: 12,
223
- lineHeight: 1.65,
225
+ lineHeight: 1.5,
224
226
  wordBreak: 'break-word',
225
227
  zIndex: 9999,
226
228
  boxShadow: '0 4px 16px rgba(0,0,0,0.3)',
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from "./script-code";
2
2
  export * from "./types";
3
3
  export { GroovyFormatter, GroovyScriptConvertorUtil } from "./utils/groovy-formatter";
4
4
  export type { FormatOptions } from "./utils/groovy-formatter";
5
+ export { BUILTIN_LANGUAGES, resolveLanguageConfig, groovyConfig, javascriptConfig, } from "./languages";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./script-code.js";
2
2
  export * from "./types/index.js";
3
3
  export { GroovyFormatter, GroovyScriptConvertorUtil } from "./utils/groovy-formatter.js";
4
+ export { BUILTIN_LANGUAGES, groovyConfig, javascriptConfig, resolveLanguageConfig } from "./languages";
@@ -0,0 +1,3 @@
1
+ import type { LanguageConfig } from '../types';
2
+ /** Groovy 语言配置 */
3
+ export declare const groovyConfig: LanguageConfig;
@@ -0,0 +1,150 @@
1
+ import { snippet } from "@codemirror/autocomplete";
2
+ import { java } from "@codemirror/lang-java";
3
+ import { GroovyFormatter } from "../utils/groovy-formatter.js";
4
+ const GROOVY_SNIPPETS = [
5
+ {
6
+ label: 'if',
7
+ type: 'keyword',
8
+ boost: -1,
9
+ apply: snippet('if (${condition}) {\n\t${}\n}')
10
+ },
11
+ {
12
+ label: 'else',
13
+ type: 'keyword',
14
+ boost: -2,
15
+ apply: snippet('else {\n\t${}\n}')
16
+ },
17
+ {
18
+ label: 'for',
19
+ type: 'keyword',
20
+ boost: -1,
21
+ apply: snippet('for (${item} in ${collection}) {\n\t${}\n}')
22
+ },
23
+ {
24
+ label: 'while',
25
+ type: 'keyword',
26
+ boost: -1,
27
+ apply: snippet('while (${condition}) {\n\t${}\n}')
28
+ },
29
+ {
30
+ label: 'switch',
31
+ type: 'keyword',
32
+ boost: -1,
33
+ apply: snippet('switch (${expression}) {\n\tcase ${value}:\n\t\t${}\n\t\tbreak\n\tdefault:\n\t\tbreak\n}')
34
+ },
35
+ {
36
+ label: 'try',
37
+ type: 'keyword',
38
+ boost: -1,
39
+ apply: snippet('try {\n\t${}\n} catch (${Exception} e) {\n\t\n}')
40
+ },
41
+ {
42
+ label: 'def',
43
+ type: 'keyword',
44
+ boost: -1,
45
+ apply: snippet('def ${name}')
46
+ },
47
+ {
48
+ label: 'println',
49
+ type: 'keyword',
50
+ boost: 0,
51
+ apply: snippet('println(${})')
52
+ },
53
+ {
54
+ label: 'print',
55
+ type: 'keyword',
56
+ boost: -1,
57
+ apply: snippet('print(${})')
58
+ },
59
+ {
60
+ label: 'return',
61
+ type: 'keyword',
62
+ boost: 0,
63
+ apply: snippet('return ${}')
64
+ },
65
+ {
66
+ label: 'each',
67
+ type: 'keyword',
68
+ boost: -1,
69
+ apply: snippet('each { ${item} ->\n\t${}\n}')
70
+ },
71
+ {
72
+ label: 'collect',
73
+ type: 'keyword',
74
+ boost: -1,
75
+ apply: snippet('collect { ${item} ->\n\t${}\n}')
76
+ },
77
+ {
78
+ label: 'find',
79
+ type: 'keyword',
80
+ boost: -1,
81
+ apply: snippet('find { ${item} ->\n\t${}\n}')
82
+ },
83
+ {
84
+ label: 'findAll',
85
+ type: 'keyword',
86
+ boost: -1,
87
+ apply: snippet('findAll { ${item} ->\n\t${}\n}')
88
+ },
89
+ {
90
+ label: 'inject',
91
+ type: 'keyword',
92
+ boost: -1,
93
+ apply: snippet('inject(${initial}) { ${acc}, ${item} ->\n\t${}\n}')
94
+ },
95
+ {
96
+ label: 'assert',
97
+ type: 'keyword',
98
+ boost: -1,
99
+ apply: snippet('assert ${condition}')
100
+ },
101
+ {
102
+ label: 'new',
103
+ type: 'keyword',
104
+ boost: -1,
105
+ apply: snippet('new ${ClassName}(${})')
106
+ },
107
+ {
108
+ label: 'class',
109
+ type: 'keyword',
110
+ boost: -1,
111
+ apply: snippet('class ${ClassName} {\n\t${}\n}')
112
+ },
113
+ {
114
+ label: 'interface',
115
+ type: 'keyword',
116
+ boost: -2,
117
+ apply: snippet('interface ${InterfaceName} {\n\t${}\n}')
118
+ },
119
+ {
120
+ label: 'throw',
121
+ type: 'keyword',
122
+ boost: -1,
123
+ apply: snippet('throw new ${Exception}("${}")')
124
+ },
125
+ {
126
+ label: 'import',
127
+ type: 'keyword',
128
+ boost: -2,
129
+ apply: snippet('import ${}')
130
+ }
131
+ ];
132
+ const groovyConfig = {
133
+ name: 'groovy',
134
+ displayName: 'Groovy',
135
+ extension: ()=>java(),
136
+ keywordSnippets: GROOVY_SNIPPETS,
137
+ syntaxNodeNames: {
138
+ stringNodes: [
139
+ 'StringLiteral',
140
+ 'CharLiteral'
141
+ ],
142
+ commentNodes: [
143
+ 'LineComment',
144
+ 'BlockComment'
145
+ ]
146
+ },
147
+ placeholder: '请输入 Groovy 脚本...',
148
+ formatter: (code)=>GroovyFormatter.formatScript(code)
149
+ };
150
+ export { groovyConfig };
@@ -0,0 +1,17 @@
1
+ import type { LanguageConfig } from '../types';
2
+ /** 内置语言配置注册表 */
3
+ export declare const BUILTIN_LANGUAGES: Record<string, LanguageConfig>;
4
+ /**
5
+ * 解析语言配置
6
+ *
7
+ * 接受语言名(字符串)或自定义 LanguageConfig 对象,返回完整的 LanguageConfig。
8
+ * 当传入字符串时,从内置注册表中查找;未找到则抛出错误。
9
+ * 不传参时默认使用 Groovy 配置。
10
+ *
11
+ * @param language 语言名或自定义配置
12
+ * @returns 解析后的完整语言配置
13
+ * @throws 当传入的语言名不在内置注册表中时
14
+ */
15
+ export declare function resolveLanguageConfig(language?: string | LanguageConfig): LanguageConfig;
16
+ export { groovyConfig } from './groovy';
17
+ export { javascriptConfig } from './javascript';
@@ -0,0 +1,16 @@
1
+ import { groovyConfig } from "./groovy.js";
2
+ import { javascriptConfig } from "./javascript.js";
3
+ const BUILTIN_LANGUAGES = {
4
+ groovy: groovyConfig,
5
+ javascript: javascriptConfig
6
+ };
7
+ function resolveLanguageConfig(language) {
8
+ if (null == language) return BUILTIN_LANGUAGES.groovy;
9
+ if ('string' == typeof language) {
10
+ const config = BUILTIN_LANGUAGES[language];
11
+ if (!config) throw new Error(`Unknown language "${language}". Available built-in languages: ${Object.keys(BUILTIN_LANGUAGES).join(', ')}. You can also pass a custom LanguageConfig object.`);
12
+ return config;
13
+ }
14
+ return language;
15
+ }
16
+ export { BUILTIN_LANGUAGES, groovyConfig, javascriptConfig, resolveLanguageConfig };
@@ -0,0 +1,3 @@
1
+ import type { LanguageConfig } from '../types';
2
+ /** JavaScript 语言配置 */
3
+ export declare const javascriptConfig: LanguageConfig;
@@ -0,0 +1,155 @@
1
+ import { snippet } from "@codemirror/autocomplete";
2
+ import { javascript } from "@codemirror/lang-javascript";
3
+ const JAVASCRIPT_SNIPPETS = [
4
+ {
5
+ label: 'if',
6
+ type: 'keyword',
7
+ boost: -1,
8
+ apply: snippet('if (${condition}) {\n\t${}\n}')
9
+ },
10
+ {
11
+ label: 'else',
12
+ type: 'keyword',
13
+ boost: -2,
14
+ apply: snippet('else {\n\t${}\n}')
15
+ },
16
+ {
17
+ label: 'for',
18
+ type: 'keyword',
19
+ boost: -1,
20
+ apply: snippet('for (let ${i} = 0; ${i} < ${length}; ${i}++) {\n\t${}\n}')
21
+ },
22
+ {
23
+ label: 'for...of',
24
+ type: 'keyword',
25
+ boost: -1,
26
+ apply: snippet('for (const ${item} of ${array}) {\n\t${}\n}')
27
+ },
28
+ {
29
+ label: 'for...in',
30
+ type: 'keyword',
31
+ boost: -1,
32
+ apply: snippet('for (const ${key} in ${object}) {\n\t${}\n}')
33
+ },
34
+ {
35
+ label: 'while',
36
+ type: 'keyword',
37
+ boost: -1,
38
+ apply: snippet('while (${condition}) {\n\t${}\n}')
39
+ },
40
+ {
41
+ label: 'function',
42
+ type: 'keyword',
43
+ boost: -1,
44
+ apply: snippet('function ${name}(${params}) {\n\t${}\n}')
45
+ },
46
+ {
47
+ label: 'const',
48
+ type: 'keyword',
49
+ boost: -1,
50
+ apply: snippet('const ${name} = ${}')
51
+ },
52
+ {
53
+ label: 'let',
54
+ type: 'keyword',
55
+ boost: -1,
56
+ apply: snippet('let ${name} = ${}')
57
+ },
58
+ {
59
+ label: '=>',
60
+ type: 'keyword',
61
+ boost: -1,
62
+ apply: snippet('(${params}) => {\n\t${}\n}')
63
+ },
64
+ {
65
+ label: 'class',
66
+ type: 'keyword',
67
+ boost: -1,
68
+ apply: snippet('class ${ClassName} {\n\tconstructor(${params}) {\n\t\t${}\n\t}\n}')
69
+ },
70
+ {
71
+ label: 'try',
72
+ type: 'keyword',
73
+ boost: -1,
74
+ apply: snippet('try {\n\t${}\n} catch (${error}) {\n\t\n}')
75
+ },
76
+ {
77
+ label: 'switch',
78
+ type: 'keyword',
79
+ boost: -1,
80
+ apply: snippet('switch (${expression}) {\n\tcase ${value}:\n\t\t${}\n\t\tbreak\n\tdefault:\n\t\tbreak\n}')
81
+ },
82
+ {
83
+ label: 'return',
84
+ type: 'keyword',
85
+ boost: 0,
86
+ apply: snippet('return ${}')
87
+ },
88
+ {
89
+ label: 'import',
90
+ type: 'keyword',
91
+ boost: -2,
92
+ apply: snippet("import { ${} } from '${module}'")
93
+ },
94
+ {
95
+ label: 'export',
96
+ type: 'keyword',
97
+ boost: -2,
98
+ apply: snippet('export ${}')
99
+ },
100
+ {
101
+ label: 'async',
102
+ type: 'keyword',
103
+ boost: -1,
104
+ apply: snippet('async function ${name}(${params}) {\n\t${}\n}')
105
+ },
106
+ {
107
+ label: 'await',
108
+ type: 'keyword',
109
+ boost: -1,
110
+ apply: snippet('await ${}')
111
+ },
112
+ {
113
+ label: 'new',
114
+ type: 'keyword',
115
+ boost: -1,
116
+ apply: snippet('new ${ClassName}(${})')
117
+ },
118
+ {
119
+ label: 'throw',
120
+ type: 'keyword',
121
+ boost: -1,
122
+ apply: snippet('throw new ${Error}("${}")')
123
+ },
124
+ {
125
+ label: 'console.log',
126
+ type: 'keyword',
127
+ boost: 0,
128
+ apply: snippet('console.log(${})')
129
+ },
130
+ {
131
+ label: 'Promise',
132
+ type: 'keyword',
133
+ boost: -1,
134
+ apply: snippet('new Promise((${resolve}, ${reject}) => {\n\t${}\n})')
135
+ }
136
+ ];
137
+ const javascriptConfig = {
138
+ name: "javascript",
139
+ displayName: 'JavaScript',
140
+ extension: ()=>javascript(),
141
+ keywordSnippets: JAVASCRIPT_SNIPPETS,
142
+ syntaxNodeNames: {
143
+ stringNodes: [
144
+ 'String',
145
+ 'TemplateString',
146
+ 'RegExp'
147
+ ],
148
+ commentNodes: [
149
+ 'LineComment',
150
+ 'BlockComment'
151
+ ]
152
+ },
153
+ placeholder: '请输入 JavaScript 脚本...'
154
+ };
155
+ export { javascriptConfig };
@@ -1,3 +1,3 @@
1
1
  import React from 'react';
2
- import { ScriptCodeEditorProps } from './types';
2
+ import type { ScriptCodeEditorProps } from './types';
3
3
  export declare const ScriptCodeEditor: React.FC<ScriptCodeEditorProps>;
@@ -3,15 +3,14 @@ import { Compartment, EditorState } from "@codemirror/state";
3
3
  import { EditorView, crosshairCursor, drawSelection, dropCursor, highlightActiveLine, highlightActiveLineGutter, highlightSpecialChars, keymap, lineNumbers, placeholder as view_placeholder, rectangularSelection } from "@codemirror/view";
4
4
  import { defaultKeymap, history as commands_history, historyKeymap, indentWithTab } from "@codemirror/commands";
5
5
  import { HighlightStyle, bracketMatching, defaultHighlightStyle, foldGutter, foldKeymap, indentOnInput, syntaxHighlighting } from "@codemirror/language";
6
- import { java } from "@codemirror/lang-java";
7
6
  import { oneDark } from "@codemirror/theme-one-dark";
8
7
  import { tags } from "@lezer/highlight";
9
8
  import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
10
- import { createGroovyCompletionSource, createGroovyKeywordSource } from "./autocomplete/index.js";
9
+ import { createCompletionSource } from "./autocomplete/index.js";
10
+ import { resolveLanguageConfig } from "./languages";
11
11
  import { TypePanel } from "./type-panel/index.js";
12
12
  import { Toolbar } from "./components/toolbar.js";
13
13
  import { ExpandSidebarButton } from "./components/expand-sidebar-button.js";
14
- import { GroovyFormatter } from "./utils/groovy-formatter.js";
15
14
  const darkHighlightStyle = HighlightStyle.define([
16
15
  {
17
16
  tag: tags.keyword,
@@ -132,16 +131,10 @@ function buildThemeExtensions(theme) {
132
131
  }));
133
132
  return exts;
134
133
  }
135
- function buildAutocompleteExt(metadata) {
136
- return metadata ? autocompletion({
134
+ function buildAutocompleteExt(languageConfig, metadata) {
135
+ return autocompletion({
137
136
  override: [
138
- createGroovyCompletionSource(metadata)
139
- ],
140
- activateOnTyping: true,
141
- icons: true
142
- }) : autocompletion({
143
- override: [
144
- createGroovyKeywordSource()
137
+ createCompletionSource(languageConfig, metadata)
145
138
  ],
146
139
  activateOnTyping: true,
147
140
  icons: true
@@ -181,8 +174,11 @@ function buildLayoutExtensions(fontSize, minHeight, maxHeight, isFullscreen) {
181
174
  });
182
175
  }
183
176
  const ScriptCodeEditor = (props)=>{
184
- const { value, readonly = false, onChange, onCompile, onFormat, onThemeChange, placeholder = '请输入 Groovy 脚本...', defaultTheme, title, metadata, defaultSidebarOpen, enableThemeToggle, enableFormat, enableCompile, enableFullscreen, toolbar, toolbarExtra, options = {} } = props;
177
+ const { value, readonly = false, onChange, onCompile, onFormat, onThemeChange, language = 'groovy', placeholder, defaultTheme, title, metadata, defaultSidebarOpen, enableThemeToggle, enableFormat, enableCompile, enableFullscreen, toolbar, toolbarExtra, options = {} } = props;
185
178
  const { fontSize = 14, minHeight = 300, maxHeight = 300 } = options;
179
+ const languageConfig = resolveLanguageConfig(language);
180
+ const effectivePlaceholder = placeholder ?? languageConfig.placeholder ?? '';
181
+ const effectiveFormatter = onFormat ?? (languageConfig.formatter ? (code)=>languageConfig.formatter(code) : void 0);
186
182
  const [internalTheme, setInternalTheme] = useState(defaultTheme ?? 'dark');
187
183
  useEffect(()=>{
188
184
  if (void 0 !== defaultTheme) setInternalTheme(defaultTheme);
@@ -194,6 +190,7 @@ const ScriptCodeEditor = (props)=>{
194
190
  const themeCompartmentRef = useRef(new Compartment());
195
191
  const autocompleteCompartmentRef = useRef(new Compartment());
196
192
  const layoutCompartmentRef = useRef(new Compartment());
193
+ const languageCompartmentRef = useRef(new Compartment());
197
194
  const onChangeRef = useRef(onChange);
198
195
  onChangeRef.current = onChange;
199
196
  const metadataRef = useRef(metadata);
@@ -227,6 +224,7 @@ const ScriptCodeEditor = (props)=>{
227
224
  const themeCompartment = themeCompartmentRef.current;
228
225
  const acCompartment = autocompleteCompartmentRef.current;
229
226
  const layoutCompartment = layoutCompartmentRef.current;
227
+ const langCompartment = languageCompartmentRef.current;
230
228
  const extensions = [
231
229
  lineNumbers(),
232
230
  highlightActiveLineGutter(),
@@ -248,14 +246,14 @@ const ScriptCodeEditor = (props)=>{
248
246
  ...completionKeymap,
249
247
  indentWithTab
250
248
  ]),
251
- java(),
252
- view_placeholder(placeholder),
249
+ langCompartment.of(languageConfig.extension()),
250
+ view_placeholder(effectivePlaceholder),
253
251
  EditorView.updateListener.of((update)=>{
254
252
  if (update.docChanged && onChangeRef.current) onChangeRef.current(update.state.doc.toString());
255
253
  }),
256
254
  layoutCompartment.of(buildLayoutExtensions(fontSize, minHeight, maxHeight, false)),
257
255
  themeCompartment.of(buildThemeExtensions(internalTheme)),
258
- acCompartment.of(buildAutocompleteExt(metadataRef.current))
256
+ acCompartment.of(buildAutocompleteExt(languageConfig, metadataRef.current))
259
257
  ];
260
258
  if (readonly) extensions.push(EditorState.readOnly.of(true));
261
259
  const state = EditorState.create({
@@ -274,7 +272,7 @@ const ScriptCodeEditor = (props)=>{
274
272
  fontSize,
275
273
  minHeight,
276
274
  maxHeight,
277
- placeholder,
275
+ effectivePlaceholder,
278
276
  readonly
279
277
  ]);
280
278
  useEffect(()=>{
@@ -299,10 +297,22 @@ const ScriptCodeEditor = (props)=>{
299
297
  useEffect(()=>{
300
298
  if (!viewRef.current) return;
301
299
  viewRef.current.dispatch({
302
- effects: autocompleteCompartmentRef.current.reconfigure(buildAutocompleteExt(metadata))
300
+ effects: autocompleteCompartmentRef.current.reconfigure(buildAutocompleteExt(languageConfig, metadata))
303
301
  });
304
302
  }, [
305
- metadata
303
+ metadata,
304
+ languageConfig
305
+ ]);
306
+ useEffect(()=>{
307
+ if (!viewRef.current) return;
308
+ viewRef.current.dispatch({
309
+ effects: [
310
+ languageCompartmentRef.current.reconfigure(languageConfig.extension()),
311
+ autocompleteCompartmentRef.current.reconfigure(buildAutocompleteExt(languageConfig, metadata))
312
+ ]
313
+ });
314
+ }, [
315
+ languageConfig
306
316
  ]);
307
317
  useEffect(()=>{
308
318
  if (!viewRef.current) return;
@@ -326,17 +336,18 @@ const ScriptCodeEditor = (props)=>{
326
336
  if (onCompile && viewRef.current) onCompile(viewRef.current.state.doc.toString());
327
337
  };
328
338
  const handleFormat = ()=>{
329
- if (viewRef.current) {
330
- const code = viewRef.current.state.doc.toString();
331
- const formatted = GroovyFormatter.formatScript(code);
332
- if (formatted !== code) viewRef.current.dispatch({
333
- changes: {
334
- from: 0,
335
- to: viewRef.current.state.doc.length,
336
- insert: formatted
337
- }
338
- });
339
- }
339
+ if (!viewRef.current) return;
340
+ const code = viewRef.current.state.doc.toString();
341
+ const formatter = effectiveFormatter;
342
+ if (!formatter) return;
343
+ const formatted = formatter(code);
344
+ if ('string' == typeof formatted && formatted !== code) viewRef.current.dispatch({
345
+ changes: {
346
+ from: 0,
347
+ to: viewRef.current.state.doc.length,
348
+ insert: formatted
349
+ }
350
+ });
340
351
  };
341
352
  return /*#__PURE__*/ react.createElement("div", {
342
353
  style: isFullscreen ? {
@@ -362,7 +373,7 @@ const ScriptCodeEditor = (props)=>{
362
373
  },
363
374
  enableThemeToggle: enableThemeToggle,
364
375
  enableFormat: enableFormat,
365
- onFormat: readonly ? void 0 : onFormat ?? handleFormat,
376
+ onFormat: readonly || !effectiveFormatter ? void 0 : onFormat ?? handleFormat,
366
377
  enableCompile: enableCompile,
367
378
  onCompile: onCompile ? handleCompile : void 0,
368
379
  enableFullscreen: enableFullscreen,
@@ -1,4 +1,6 @@
1
1
  import type React from 'react';
2
+ import type { Completion } from '@codemirror/autocomplete';
3
+ import type { Extension } from '@codemirror/state';
2
4
  /** 工具栏按钮属性 */
3
5
  export interface ToolbarButtonProps {
4
6
  /** 按钮内容(支持 ReactNode,可包含图标 + 文字) */
@@ -79,6 +81,26 @@ export interface ScriptCodeEditorProps {
79
81
  onChange?: (value: string) => void;
80
82
  /** 占位符 */
81
83
  placeholder?: string;
84
+ /**
85
+ * 编程语言配置(默认 'groovy')。
86
+ *
87
+ * - 传入内置语言名:`'groovy'` | `'javascript'`
88
+ * - 传入自定义 `LanguageConfig` 对象以支持其他语言
89
+ *
90
+ * @example
91
+ * // 内置语言
92
+ * <ScriptCodeEditor language="javascript" />
93
+ *
94
+ * // 自定义语言
95
+ * <ScriptCodeEditor language={{
96
+ * name: 'python',
97
+ * displayName: 'Python',
98
+ * extension: () => python(),
99
+ * keywordSnippets: PYTHON_SNIPPETS,
100
+ * syntaxNodeNames: { stringNodes: ['String'], commentNodes: ['LineComment', 'BlockComment'] },
101
+ * }} />
102
+ */
103
+ language?: string | LanguageConfig;
82
104
  /** 初始主题(默认 'dark'),编辑器内部管理状态 */
83
105
  defaultTheme?: 'dark' | 'light';
84
106
  /** 主题切换通知回调(可选) */
@@ -91,13 +113,13 @@ export interface ScriptCodeEditorProps {
91
113
  defaultSidebarOpen?: boolean;
92
114
  /** 是否显示主题切换按钮(默认 false) */
93
115
  enableThemeToggle?: boolean;
94
- /** 是否显示格式化按钮(默认 false,需配合 onFormat 使用) */
116
+ /** 是否显示格式化按钮(默认 false,需配合 onFormat 或 language.formatter 使用) */
95
117
  enableFormat?: boolean;
96
118
  /** 是否显示编译验证按钮(默认 false,需配合 onCompile 使用) */
97
119
  enableCompile?: boolean;
98
120
  /** 是否显示全屏按钮(默认 false) */
99
121
  enableFullscreen?: boolean;
100
- /** 格式化代码回调(enableFormat 时需提供) */
122
+ /** 格式化代码回调(enableFormat 时提供,优先级高于 language.formatter) */
101
123
  onFormat?: () => void;
102
124
  /** 编译/测试脚本回调(enableCompile 时需提供) */
103
125
  onCompile?: (code: string) => void;
@@ -115,3 +137,49 @@ export interface ScriptCodeEditorProps {
115
137
  maxHeight?: number;
116
138
  };
117
139
  }
140
+ /** 格式化选项 */
141
+ export interface FormatOptions {
142
+ /** 缩进大小(空格数) */
143
+ indentSize?: number;
144
+ /** 操作符周围添加空格 */
145
+ addSpacesAroundOperators?: boolean;
146
+ /** 格式化注释 */
147
+ formatComments?: boolean;
148
+ /** 最大行长度 */
149
+ maxLineLength?: number;
150
+ /** 保留空行 */
151
+ preserveEmptyLines?: boolean;
152
+ }
153
+ /** 格式化函数签名 */
154
+ export type FormatFn = (code: string, options?: FormatOptions) => string;
155
+ /**
156
+ * 语言配置接口
157
+ *
158
+ * 封装一个编程语言在编辑器中所需的全部差异点:
159
+ * - CodeMirror 语法高亮扩展
160
+ * - 关键字/语法片段
161
+ * - AST 节点名(用于判断字符串/注释位置,抑制自动补全)
162
+ * - 默认占位符文本
163
+ * - 可选的内置格式化器
164
+ */
165
+ export interface LanguageConfig {
166
+ /** 语言标识名(小写),如 'groovy'、'javascript' */
167
+ name: string;
168
+ /** 显示名称,如 'Groovy'、'JavaScript' */
169
+ displayName: string;
170
+ /** CodeMirror 语言扩展工厂 */
171
+ extension: () => Extension;
172
+ /** 关键字和语法片段列表 */
173
+ keywordSnippets: readonly Completion[];
174
+ /** 各语言的语法树节点名(用于 isInStringOrComment 判断) */
175
+ syntaxNodeNames: {
176
+ /** 字符串类节点名,如 Java: ['StringLiteral'],JS: ['String', 'TemplateString'] */
177
+ stringNodes: string[];
178
+ /** 注释类节点名,如 ['LineComment', 'BlockComment'] */
179
+ commentNodes: string[];
180
+ };
181
+ /** 默认占位符文本 */
182
+ placeholder?: string;
183
+ /** 可选的内置格式化函数(优先级低于 onFormat prop) */
184
+ formatter?: FormatFn;
185
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coding-script/script-engine",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "script-engine components",
5
5
  "keywords": [
6
6
  "coding-script",
@@ -31,6 +31,7 @@
31
31
  "@codemirror/autocomplete": "^6.18.0",
32
32
  "@codemirror/commands": "^6.10.2",
33
33
  "@codemirror/lang-java": "^6.0.2",
34
+ "@codemirror/lang-javascript": "^6.2.0",
34
35
  "@codemirror/language": "^6.12.2",
35
36
  "@codemirror/state": "^6.5.4",
36
37
  "@codemirror/theme-one-dark": "^6.1.3",