@coding-script/script-engine 0.0.1 → 0.0.2

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,23 +1,244 @@
1
- # Rslib project
1
+ [![npm](https://img.shields.io/npm/v/@coding-script/script-engine.svg)](https://www.npmjs.com/package/@coding-script/script-engine)
2
+ # Script Engine
2
3
 
3
- ## Setup
4
+ 基于 React + CodeMirror 6 的 Groovy 脚本编辑器组件库,提供语法高亮、动态类型自动补全、属性面板等功能。
4
5
 
5
- Install the dependencies:
6
+ ## 功能特性
7
+
8
+ - **Groovy 语法高亮**:基于 CodeMirror 6,支持完整的 Groovy/Java 语法着色
9
+ - **动态类型自动补全**:基于 `ScriptMetadata` 提供变量名补全和点号链式访问补全(如 `request.test.name`)
10
+ - **Groovy 语法提示**:内置 `if`/`for`/`while`/`println`/`return` 等常用 Groovy 语法片段
11
+ - **代码格式化**:内置 Groovy 格式化器,也可通过 `onFormat` 自定义格式化逻辑
12
+ - **属性面板**:右侧侧边栏展示主函数签名、变量、数据类型(字段和方法),支持折叠/展开和拖拽调节宽度
13
+ - **主题切换**:暗色/亮色两套主题,编辑器、补全弹窗、属性面板同步切换,编辑器内部管理主题状态
14
+ - **全屏模式**:支持 CSS 全屏(`position: fixed` 覆盖视口),不影响编辑器内容
15
+ - **自定义工具栏**:通过 `toolbarExtra` 传入任意 React 节点
16
+ - **热更新**:主题和 metadata 变化时通过 Compartment 热更新,不重建编辑器,不丢失用户输入
17
+
18
+ ## 安装
6
19
 
7
20
  ```bash
8
- pnpm install
21
+ npm install @coding-script/script-engine
22
+ # 或
23
+ pnpm add @coding-script/script-engine
9
24
  ```
10
25
 
11
- ## Get started
26
+ ## 基础用法
12
27
 
13
- Build the library:
28
+ ```tsx
29
+ import { ScriptCodeEditor } from '@coding-script/script-engine';
30
+ import type { ScriptMetadata } from '@coding-script/script-engine';
14
31
 
15
- ```bash
16
- pnpm run build
32
+ const metadata: ScriptMetadata = {
33
+ mainMethod: 'run',
34
+ returnType: 'Integer',
35
+ binds: [
36
+ { dataType: 'GroovyBindObject', name: '$request' },
37
+ ],
38
+ requests: [
39
+ { dataType: 'MyScriptRequest', description: '请求参数', name: 'request' },
40
+ ],
41
+ types: {
42
+ MyScriptRequest: {
43
+ dataType: 'MyScriptRequest',
44
+ description: '请求参数类型',
45
+ fields: [
46
+ { dataType: 'int', description: '总数量', name: 'count' },
47
+ { dataType: 'MyTest', description: '测试对象', name: 'test' },
48
+ ],
49
+ functions: [
50
+ {
51
+ name: 'isSupport',
52
+ description: '是否匹配',
53
+ parameters: [{ dataType: 'int', description: '数量', name: 'count' }],
54
+ },
55
+ ],
56
+ },
57
+ MyTest: {
58
+ dataType: 'MyTest',
59
+ description: '测试对象',
60
+ fields: [
61
+ { dataType: 'Long', description: 'id', name: 'id' },
62
+ { dataType: 'String', description: '名称', name: 'name' },
63
+ ],
64
+ functions: [],
65
+ },
66
+ Integer: { dataType: 'Integer', fields: [], functions: [] },
67
+ String: { dataType: 'String', fields: [], functions: [] },
68
+ Long: { dataType: 'Long', fields: [], functions: [] },
69
+ int: { dataType: 'int', fields: [], functions: [] },
70
+ },
71
+ };
72
+
73
+ function App() {
74
+ return (
75
+ <ScriptCodeEditor
76
+ value="def run(request){\n return request.count;\n}\n"
77
+ title="Groovy 脚本编辑器"
78
+ defaultTheme="dark"
79
+ metadata={metadata}
80
+ enableThemeToggle
81
+ enableFormat
82
+ enableCompile
83
+ enableFullscreen
84
+ onThemeChange={(theme) => console.log('主题切换:', theme)}
85
+ onChange={(code) => console.log('代码变化:', code)}
86
+ onCompile={(code) => console.log('编译验证:', code)}
87
+ options={{ minHeight: 400, maxHeight: 500 }}
88
+ />
89
+ );
90
+ }
17
91
  ```
18
92
 
19
- Build the library in watch mode:
93
+ ## Props
94
+
95
+ | 属性 | 类型 | 默认值 | 说明 |
96
+ |---|---|---|---|
97
+ | `value` | `string` | `undefined` | 代码内容 |
98
+ | `readonly` | `boolean` | `false` | 是否只读 |
99
+ | `onChange` | `(value: string) => void` | `undefined` | 代码变化回调 |
100
+ | `placeholder` | `string` | `'请输入 Groovy 脚本...'` | 空内容占位符 |
101
+ | `defaultTheme` | `'dark' \| 'light'` | `'dark'` | 初始主题,编辑器内部管理状态 |
102
+ | `onThemeChange` | `(theme: 'dark' \| 'light') => void` | `undefined` | 主题切换通知回调 |
103
+ | `title` | `string` | `undefined` | 工具栏标题 |
104
+ | `metadata` | `ScriptMetadata` | `undefined` | 脚本元数据,提供后启用属性面板和自动补全 |
105
+ | `defaultSidebarOpen` | `boolean` | `metadata != null` | 属性面板默认是否展开 |
106
+
107
+ ### 工具栏按钮控制
108
+
109
+ 所有工具栏按钮默认**禁用**,需通过 `enable*` 标志显式开启:
110
+
111
+ | 属性 | 类型 | 默认值 | 说明 |
112
+ |---|---|---|---|
113
+ | `enableThemeToggle` | `boolean` | `false` | 是否显示主题切换按钮 |
114
+ | `enableFormat` | `boolean` | `false` | 是否显示格式化按钮(需配合 `onFormat`) |
115
+ | `enableCompile` | `boolean` | `false` | 是否显示编译验证按钮(需配合 `onCompile`) |
116
+ | `enableFullscreen` | `boolean` | `false` | 是否显示全屏按钮 |
117
+
118
+ ### 按钮回调
119
+
120
+ | 属性 | 类型 | 默认值 | 说明 |
121
+ |---|---|---|---|
122
+ | `onFormat` | `() => void` | `undefined` | 格式化代码回调(不提供时使用内置 Groovy 格式化器) |
123
+ | `onCompile` | `(code: string) => void` | `undefined` | 编译/测试脚本回调 |
124
+
125
+ ### 自定义工具栏
126
+
127
+ | 属性 | 类型 | 默认值 | 说明 |
128
+ |---|---|---|---|
129
+ | `toolbarExtra` | `React.ReactNode` | `undefined` | 工具栏额外内容,渲染在内置按钮之后,可传入任意 JSX |
130
+
131
+ ```tsx
132
+ <ScriptCodeEditor
133
+ toolbarExtra={
134
+ <>
135
+ <button onClick={save}>💾 保存</button>
136
+ <button onClick={help}>❓ 帮助</button>
137
+ </>
138
+ }
139
+ />
140
+ ```
141
+
142
+ ### 布局选项
143
+
144
+ | 属性 | 类型 | 默认值 | 说明 |
145
+ |---|---|---|---|
146
+ | `options.fontSize` | `number` | `14` | 字体大小(px) |
147
+ | `options.minHeight` | `number` | `300` | 编辑器最小高度(px) |
148
+ | `options.maxHeight` | `number` | `300` | 编辑器最大高度(px) |
149
+
150
+ ## ScriptMetadata 数据结构
151
+
152
+ ```typescript
153
+ interface ScriptMetadata {
154
+ /** 主函数名称 */
155
+ mainMethod: string;
156
+ /** 注入变量(如 $request,name 含 $ 前缀) */
157
+ binds: ScriptBindInfo[];
158
+ /** 主函数参数 */
159
+ requests: ScriptRequestInfo[];
160
+ /** 主函数返回类型(可选) */
161
+ returnType?: string;
162
+ /** 所有可用类型定义(含基础类型如 Integer/String) */
163
+ types: Record<string, ScriptTypeInfo>;
164
+ }
165
+
166
+ interface ScriptTypeInfo {
167
+ dataType: string;
168
+ description?: string;
169
+ fields: ScriptFieldInfo[];
170
+ functions: ScriptFunctionInfo[];
171
+ }
172
+
173
+ interface ScriptFieldInfo {
174
+ name: string;
175
+ dataType: string;
176
+ description?: string;
177
+ }
178
+
179
+ interface ScriptFunctionInfo {
180
+ name: string;
181
+ parameters: ScriptParameterInfo[];
182
+ description?: string;
183
+ returnType?: string;
184
+ }
185
+
186
+ interface ScriptParameterInfo {
187
+ name: string;
188
+ dataType: string;
189
+ description?: string;
190
+ }
191
+
192
+ interface ScriptBindInfo {
193
+ name: string;
194
+ dataType: string;
195
+ description?: string;
196
+ }
197
+
198
+ interface ScriptRequestInfo {
199
+ name: string;
200
+ dataType: string;
201
+ description?: string;
202
+ }
203
+ ```
204
+
205
+ > **注意**:`metadata` 必须是解析后的 JavaScript 对象,不能是 JSON 字符串。如果从 API 获取的是 JSON 字符串,需要先 `JSON.parse()` 再传入。
206
+
207
+ ## 自动补全
208
+
209
+ 提供 metadata 后,编辑器支持以下补全能力:
210
+
211
+ | 输入 | 补全内容 |
212
+ |---|---|
213
+ | `re` | 弹出 `request`、`$request` 等变量 |
214
+ | `request.` | 弹出 `count`、`test`、`isSupport` 等字段和方法 |
215
+ | `request.test.` | 弹出 `id`、`name` 等链式访问成员 |
216
+ | `if` / `for` / `while` | 弹出 Groovy 语法片段(含 tab-stop 占位符) |
217
+
218
+ 不提供 metadata 时,仅启用 Groovy 关键字和语法片段补全。
219
+
220
+ ## 本地开发
20
221
 
21
222
  ```bash
22
- pnpm run dev
23
- ```
223
+ # 安装依赖
224
+ pnpm install
225
+
226
+ # 启动库 watch 模式(终端 1)
227
+ pnpm run watch:script-engine
228
+
229
+ # 启动演示应用(终端 2)
230
+ pnpm run dev:app-pc
231
+ ```
232
+
233
+ 演示应用访问 http://localhost:3000
234
+
235
+ ## 技术栈
236
+
237
+ - **编辑器**:[CodeMirror 6](https://codemirror.net/)(`@codemirror/view`、`state`、`autocomplete`、`lang-java`、`theme-one-dark`)
238
+ - **构建工具**:[Rslib](https://rslib.rs/)(库)+ [Rsbuild](https://rsbuild.dev/)(演示应用)
239
+ - **包管理**:pnpm monorepo(workspaces)
240
+ - **UI**:纯 CSS-in-JS(React `style` 对象),库本身不依赖 Ant Design
241
+
242
+ ## License
243
+
244
+ MIT
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ export declare const FormatIcon: React.FC<{
3
+ color: string;
4
+ }>;
@@ -0,0 +1,52 @@
1
+ import react from "react";
2
+ const FormatIcon = ({ color })=>/*#__PURE__*/ react.createElement("svg", {
3
+ width: "14",
4
+ height: "14",
5
+ viewBox: "0 0 24 24",
6
+ fill: "none",
7
+ style: {
8
+ flexShrink: 0
9
+ }
10
+ }, /*#__PURE__*/ react.createElement("rect", {
11
+ x: "4",
12
+ y: "2",
13
+ width: "16",
14
+ height: "20",
15
+ rx: "2",
16
+ stroke: color,
17
+ strokeWidth: "1.5",
18
+ fill: "none"
19
+ }), /*#__PURE__*/ react.createElement("line", {
20
+ x1: "7",
21
+ y1: "7",
22
+ x2: "14",
23
+ y2: "7",
24
+ stroke: color,
25
+ strokeWidth: "1.5",
26
+ strokeLinecap: "round"
27
+ }), /*#__PURE__*/ react.createElement("line", {
28
+ x1: "7",
29
+ y1: "10.5",
30
+ x2: "17",
31
+ y2: "10.5",
32
+ stroke: color,
33
+ strokeWidth: "1.5",
34
+ strokeLinecap: "round"
35
+ }), /*#__PURE__*/ react.createElement("line", {
36
+ x1: "9",
37
+ y1: "14",
38
+ x2: "15",
39
+ y2: "14",
40
+ stroke: color,
41
+ strokeWidth: "1.5",
42
+ strokeLinecap: "round"
43
+ }), /*#__PURE__*/ react.createElement("line", {
44
+ x1: "7",
45
+ y1: "17.5",
46
+ x2: "12",
47
+ y2: "17.5",
48
+ stroke: color,
49
+ strokeWidth: "1.5",
50
+ strokeLinecap: "round"
51
+ }));
52
+ export { FormatIcon };
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ /** 最大化图标(四角向外展开) */
3
+ export declare const MaximizeIcon: React.FC<{
4
+ color: string;
5
+ }>;
6
+ /** 最小化图标(四角向内收缩) */
7
+ export declare const MinimizeIcon: React.FC<{
8
+ color: string;
9
+ }>;
@@ -0,0 +1,68 @@
1
+ import react from "react";
2
+ const MaximizeIcon = ({ color })=>/*#__PURE__*/ react.createElement("svg", {
3
+ width: "14",
4
+ height: "14",
5
+ viewBox: "0 0 24 24",
6
+ fill: "none",
7
+ style: {
8
+ flexShrink: 0
9
+ }
10
+ }, /*#__PURE__*/ react.createElement("path", {
11
+ d: "M4 9V4h5",
12
+ stroke: color,
13
+ strokeWidth: "2",
14
+ strokeLinecap: "round",
15
+ strokeLinejoin: "round"
16
+ }), /*#__PURE__*/ react.createElement("path", {
17
+ d: "M20 9V4h-5",
18
+ stroke: color,
19
+ strokeWidth: "2",
20
+ strokeLinecap: "round",
21
+ strokeLinejoin: "round"
22
+ }), /*#__PURE__*/ react.createElement("path", {
23
+ d: "M4 15v5h5",
24
+ stroke: color,
25
+ strokeWidth: "2",
26
+ strokeLinecap: "round",
27
+ strokeLinejoin: "round"
28
+ }), /*#__PURE__*/ react.createElement("path", {
29
+ d: "M20 15v5h-5",
30
+ stroke: color,
31
+ strokeWidth: "2",
32
+ strokeLinecap: "round",
33
+ strokeLinejoin: "round"
34
+ }));
35
+ const MinimizeIcon = ({ color })=>/*#__PURE__*/ react.createElement("svg", {
36
+ width: "14",
37
+ height: "14",
38
+ viewBox: "0 0 24 24",
39
+ fill: "none",
40
+ style: {
41
+ flexShrink: 0
42
+ }
43
+ }, /*#__PURE__*/ react.createElement("path", {
44
+ d: "M9 4v5H4",
45
+ stroke: color,
46
+ strokeWidth: "2",
47
+ strokeLinecap: "round",
48
+ strokeLinejoin: "round"
49
+ }), /*#__PURE__*/ react.createElement("path", {
50
+ d: "M15 4v5h5",
51
+ stroke: color,
52
+ strokeWidth: "2",
53
+ strokeLinecap: "round",
54
+ strokeLinejoin: "round"
55
+ }), /*#__PURE__*/ react.createElement("path", {
56
+ d: "M9 20v-5H4",
57
+ stroke: color,
58
+ strokeWidth: "2",
59
+ strokeLinecap: "round",
60
+ strokeLinejoin: "round"
61
+ }), /*#__PURE__*/ react.createElement("path", {
62
+ d: "M15 20v-5h5",
63
+ stroke: color,
64
+ strokeWidth: "2",
65
+ strokeLinecap: "round",
66
+ strokeLinejoin: "round"
67
+ }));
68
+ export { MaximizeIcon, MinimizeIcon };
@@ -26,3 +26,4 @@ export { VariablesSection } from './variables-section';
26
26
  export type { VariablesSectionProps } from './variables-section';
27
27
  export { DataTypesSection } from './data-types-section';
28
28
  export type { DataTypesSectionProps } from './data-types-section';
29
+ export { MaximizeIcon, MinimizeIcon } from './fullscreen-icon';
@@ -12,3 +12,4 @@ export { TypeSection } from "./type-section.js";
12
12
  export { MainFunctionSection } from "./main-function-section.js";
13
13
  export { VariablesSection } from "./variables-section.js";
14
14
  export { DataTypesSection } from "./data-types-section.js";
15
+ export { MaximizeIcon, MinimizeIcon } from "./fullscreen-icon.js";
@@ -3,6 +3,14 @@ export interface ToolbarProps {
3
3
  title?: string;
4
4
  theme: 'dark' | 'light';
5
5
  onThemeChange?: (theme: 'dark' | 'light') => void;
6
+ enableThemeToggle?: boolean;
7
+ enableFormat?: boolean;
8
+ onFormat?: () => void;
9
+ enableCompile?: boolean;
6
10
  onCompile?: () => void;
11
+ enableFullscreen?: boolean;
12
+ isFullscreen?: boolean;
13
+ onToggleFullscreen?: () => void;
14
+ toolbarExtra?: React.ReactNode;
7
15
  }
8
16
  export declare const Toolbar: React.FC<ToolbarProps>;
@@ -1,5 +1,7 @@
1
1
  import react from "react";
2
2
  import { ToolbarButton } from "./toolbar-button.js";
3
+ import { FormatIcon } from "./format-icon.js";
4
+ import { MaximizeIcon, MinimizeIcon } from "./fullscreen-icon.js";
3
5
  const HammerIcon = ({ color })=>/*#__PURE__*/ react.createElement("svg", {
4
6
  width: "14",
5
7
  height: "14",
@@ -26,7 +28,7 @@ const HammerIcon = ({ color })=>/*#__PURE__*/ react.createElement("svg", {
26
28
  opacity: 0.8,
27
29
  transform: "rotate(-25 12.25 14.5)"
28
30
  }));
29
- const Toolbar = ({ title, theme, onThemeChange, onCompile })=>{
31
+ const Toolbar = ({ title, theme, onThemeChange, enableThemeToggle, enableFormat, onFormat, enableCompile, onCompile, enableFullscreen, isFullscreen, onToggleFullscreen, toolbarExtra })=>{
30
32
  const isDark = 'dark' === theme;
31
33
  const borderColor = isDark ? '#434343' : '#d9d9d9';
32
34
  const toolbarBg = isDark ? '#282c34' : '#fafafa';
@@ -62,7 +64,7 @@ const Toolbar = ({ title, theme, onThemeChange, onCompile })=>{
62
64
  style: {
63
65
  marginRight: 'auto'
64
66
  }
65
- }), /*#__PURE__*/ react.createElement(ToolbarButton, {
67
+ }), enableThemeToggle && /*#__PURE__*/ react.createElement(ToolbarButton, {
66
68
  label: isDark ? '🌞 浅色模式' : '🌙 深色模式',
67
69
  title: isDark ? '切换到浅色主题' : '切换到深色主题',
68
70
  bg: btnBg,
@@ -70,7 +72,17 @@ const Toolbar = ({ title, theme, onThemeChange, onCompile })=>{
70
72
  color: toolbarText,
71
73
  border: borderColor,
72
74
  onClick: handleThemeToggle
73
- }), onCompile && /*#__PURE__*/ react.createElement(ToolbarButton, {
75
+ }), enableFormat && onFormat && /*#__PURE__*/ react.createElement(ToolbarButton, {
76
+ label: /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(FormatIcon, {
77
+ color: isDark ? '#61afef' : '#1677ff'
78
+ }), "格式化"),
79
+ title: "格式化代码",
80
+ bg: isDark ? '#1e3a5f' : '#e6f4ff',
81
+ hoverBg: isDark ? '#264a73' : '#bae0ff',
82
+ color: isDark ? '#61afef' : '#1677ff',
83
+ border: isDark ? '#1e3a5f' : '#91caff',
84
+ onClick: onFormat
85
+ }), enableCompile && onCompile && /*#__PURE__*/ react.createElement(ToolbarButton, {
74
86
  label: /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(HammerIcon, {
75
87
  color: isDark ? '#98c379' : '#389e0d'
76
88
  }), "编译验证"),
@@ -80,6 +92,18 @@ const Toolbar = ({ title, theme, onThemeChange, onCompile })=>{
80
92
  color: isDark ? '#98c379' : '#389e0d',
81
93
  border: isDark ? '#2d5a27' : '#b7eb8f',
82
94
  onClick: onCompile
83
- }));
95
+ }), enableFullscreen && /*#__PURE__*/ react.createElement(ToolbarButton, {
96
+ label: /*#__PURE__*/ react.createElement(react.Fragment, null, isFullscreen ? /*#__PURE__*/ react.createElement(MinimizeIcon, {
97
+ color: isDark ? '#c678dd' : '#722ed1'
98
+ }) : /*#__PURE__*/ react.createElement(MaximizeIcon, {
99
+ color: isDark ? '#c678dd' : '#722ed1'
100
+ }), isFullscreen ? '退出全屏' : '全屏模式'),
101
+ title: isFullscreen ? '退出全屏(ESC)' : '全屏显示',
102
+ bg: isDark ? '#3a2d5a' : '#f3e8ff',
103
+ hoverBg: isDark ? '#4a3d6a' : '#e9d5ff',
104
+ color: isDark ? '#c678dd' : '#722ed1',
105
+ border: isDark ? '#3a2d5a' : '#d3adf7',
106
+ onClick: ()=>onToggleFullscreen?.()
107
+ }), toolbarExtra);
84
108
  };
85
109
  export { Toolbar };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from "./script-code";
2
2
  export * from "./types";
3
+ export { GroovyFormatter, GroovyScriptConvertorUtil } from "./utils/groovy-formatter";
4
+ export type { FormatOptions } from "./utils/groovy-formatter";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./script-code.js";
2
2
  export * from "./types/index.js";
3
+ export { GroovyFormatter, GroovyScriptConvertorUtil } from "./utils/groovy-formatter.js";
@@ -11,6 +11,7 @@ import { createGroovyCompletionSource, createGroovyKeywordSource } from "./autoc
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";
14
15
  const darkHighlightStyle = HighlightStyle.define([
15
16
  {
16
17
  tag: tags.keyword,
@@ -146,23 +147,86 @@ function buildAutocompleteExt(metadata) {
146
147
  icons: true
147
148
  });
148
149
  }
150
+ function buildLayoutExtensions(fontSize, minHeight, maxHeight, isFullscreen) {
151
+ if (isFullscreen) return EditorView.theme({
152
+ '&': {
153
+ fontSize: `${fontSize}px`,
154
+ height: '100%'
155
+ },
156
+ '.cm-scroller': {
157
+ overflow: 'auto',
158
+ flex: '1',
159
+ minHeight: '0'
160
+ },
161
+ '.cm-content': {
162
+ fontFamily: 'monospace'
163
+ }
164
+ });
165
+ return EditorView.theme({
166
+ '&': {
167
+ fontSize: `${fontSize}px`
168
+ },
169
+ '.cm-scroller': {
170
+ overflow: 'auto',
171
+ minHeight: `${minHeight}px`,
172
+ maxHeight: `${maxHeight}px`
173
+ },
174
+ '.cm-content': {
175
+ fontFamily: 'monospace',
176
+ minHeight: `${minHeight}px`
177
+ },
178
+ '.cm-gutters': {
179
+ minHeight: `${minHeight}px`
180
+ }
181
+ });
182
+ }
149
183
  const ScriptCodeEditor = (props)=>{
150
- const { value, readonly = false, onChange, onCompile, onThemeChange, placeholder = '请输入 Groovy 脚本...', theme = 'dark', title, metadata, defaultSidebarOpen, options = {} } = props;
184
+ const { value, readonly = false, onChange, onCompile, onFormat, onThemeChange, placeholder = '请输入 Groovy 脚本...', defaultTheme, title, metadata, defaultSidebarOpen, enableThemeToggle, enableFormat, enableCompile, enableFullscreen, toolbarExtra, options = {} } = props;
151
185
  const { fontSize = 14, minHeight = 300, maxHeight = 300 } = options;
186
+ const [internalTheme, setInternalTheme] = useState(defaultTheme ?? 'dark');
187
+ useEffect(()=>{
188
+ if (void 0 !== defaultTheme) setInternalTheme(defaultTheme);
189
+ }, [
190
+ defaultTheme
191
+ ]);
152
192
  const editorContainerRef = useRef(null);
153
193
  const viewRef = useRef(null);
154
194
  const themeCompartmentRef = useRef(new Compartment());
155
195
  const autocompleteCompartmentRef = useRef(new Compartment());
196
+ const layoutCompartmentRef = useRef(new Compartment());
156
197
  const onChangeRef = useRef(onChange);
157
198
  onChangeRef.current = onChange;
158
199
  const metadataRef = useRef(metadata);
159
200
  metadataRef.current = metadata;
160
201
  const [sidebarOpen, setSidebarOpen] = useState(defaultSidebarOpen ?? null != metadata);
161
202
  const [panelWidth, setPanelWidth] = useState(300);
203
+ const [isFullscreen, setIsFullscreen] = useState(false);
204
+ useEffect(()=>{
205
+ if (isFullscreen) {
206
+ const prev = document.body.style.overflow;
207
+ document.body.style.overflow = 'hidden';
208
+ return ()=>{
209
+ document.body.style.overflow = prev;
210
+ };
211
+ }
212
+ }, [
213
+ isFullscreen
214
+ ]);
215
+ useEffect(()=>{
216
+ if (!isFullscreen) return;
217
+ const handleKeyDown = (e)=>{
218
+ if ('Escape' === e.key) setIsFullscreen(false);
219
+ };
220
+ document.addEventListener('keydown', handleKeyDown);
221
+ return ()=>document.removeEventListener('keydown', handleKeyDown);
222
+ }, [
223
+ isFullscreen
224
+ ]);
162
225
  useEffect(()=>{
163
226
  if (!editorContainerRef.current) return;
164
227
  const themeCompartment = themeCompartmentRef.current;
165
228
  const acCompartment = autocompleteCompartmentRef.current;
229
+ const layoutCompartment = layoutCompartmentRef.current;
166
230
  const extensions = [
167
231
  lineNumbers(),
168
232
  highlightActiveLineGutter(),
@@ -189,24 +253,8 @@ const ScriptCodeEditor = (props)=>{
189
253
  EditorView.updateListener.of((update)=>{
190
254
  if (update.docChanged && onChangeRef.current) onChangeRef.current(update.state.doc.toString());
191
255
  }),
192
- EditorView.theme({
193
- '&': {
194
- fontSize: `${fontSize}px`
195
- },
196
- '.cm-scroller': {
197
- overflow: 'auto',
198
- minHeight: `${minHeight}px`,
199
- maxHeight: `${maxHeight}px`
200
- },
201
- '.cm-content': {
202
- fontFamily: 'monospace',
203
- minHeight: `${minHeight}px`
204
- },
205
- '.cm-gutters': {
206
- minHeight: `${minHeight}px`
207
- }
208
- }),
209
- themeCompartment.of(buildThemeExtensions(theme)),
256
+ layoutCompartment.of(buildLayoutExtensions(fontSize, minHeight, maxHeight, false)),
257
+ themeCompartment.of(buildThemeExtensions(internalTheme)),
210
258
  acCompartment.of(buildAutocompleteExt(metadataRef.current))
211
259
  ];
212
260
  if (readonly) extensions.push(EditorState.readOnly.of(true));
@@ -243,10 +291,10 @@ const ScriptCodeEditor = (props)=>{
243
291
  useEffect(()=>{
244
292
  if (!viewRef.current) return;
245
293
  viewRef.current.dispatch({
246
- effects: themeCompartmentRef.current.reconfigure(buildThemeExtensions(theme))
294
+ effects: themeCompartmentRef.current.reconfigure(buildThemeExtensions(internalTheme))
247
295
  });
248
296
  }, [
249
- theme
297
+ internalTheme
250
298
  ]);
251
299
  useEffect(()=>{
252
300
  if (!viewRef.current) return;
@@ -256,30 +304,80 @@ const ScriptCodeEditor = (props)=>{
256
304
  }, [
257
305
  metadata
258
306
  ]);
259
- const isDark = 'dark' === theme;
307
+ useEffect(()=>{
308
+ if (!viewRef.current) return;
309
+ viewRef.current.dispatch({
310
+ effects: layoutCompartmentRef.current.reconfigure(buildLayoutExtensions(fontSize, minHeight, maxHeight, isFullscreen))
311
+ });
312
+ requestAnimationFrame(()=>{
313
+ viewRef.current?.requestMeasure();
314
+ });
315
+ }, [
316
+ isFullscreen,
317
+ fontSize,
318
+ minHeight,
319
+ maxHeight
320
+ ]);
321
+ const isDark = 'dark' === internalTheme;
260
322
  const borderColor = isDark ? '#434343' : '#d9d9d9';
261
323
  const expandBtnBg = isDark ? '#2c313a' : '#f0f0f0';
262
324
  const expandBtnColor = isDark ? '#abb2bf' : '#666';
263
325
  const handleCompile = ()=>{
264
326
  if (onCompile && viewRef.current) onCompile(viewRef.current.state.doc.toString());
265
327
  };
328
+ 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
+ }
340
+ };
266
341
  return /*#__PURE__*/ react.createElement("div", {
267
- style: {
342
+ style: isFullscreen ? {
343
+ position: 'fixed',
344
+ top: 0,
345
+ left: 0,
346
+ right: 0,
347
+ bottom: 0,
348
+ zIndex: 9999,
349
+ backgroundColor: isDark ? '#21252b' : '#ffffff',
350
+ display: 'flex',
351
+ flexDirection: 'column'
352
+ } : {
268
353
  display: 'flex',
269
354
  flexDirection: 'column'
270
355
  }
271
356
  }, /*#__PURE__*/ react.createElement(Toolbar, {
272
357
  title: title,
273
- theme: theme,
274
- onThemeChange: onThemeChange,
275
- onCompile: onCompile ? handleCompile : void 0
358
+ theme: internalTheme,
359
+ onThemeChange: (next)=>{
360
+ setInternalTheme(next);
361
+ onThemeChange?.(next);
362
+ },
363
+ enableThemeToggle: enableThemeToggle,
364
+ enableFormat: enableFormat,
365
+ onFormat: readonly ? void 0 : onFormat ?? handleFormat,
366
+ enableCompile: enableCompile,
367
+ onCompile: onCompile ? handleCompile : void 0,
368
+ enableFullscreen: enableFullscreen,
369
+ isFullscreen: isFullscreen,
370
+ onToggleFullscreen: ()=>setIsFullscreen((prev)=>!prev),
371
+ toolbarExtra: toolbarExtra
276
372
  }), /*#__PURE__*/ react.createElement("div", {
277
373
  style: {
278
374
  display: 'flex',
279
375
  border: `1px solid ${borderColor}`,
280
376
  borderTop: 'none',
281
- borderRadius: '0 0 6px 6px',
282
- overflow: 'hidden'
377
+ borderRadius: isFullscreen ? 0 : '0 0 6px 6px',
378
+ overflow: 'hidden',
379
+ flex: isFullscreen ? 1 : void 0,
380
+ minHeight: isFullscreen ? 0 : void 0
283
381
  }
284
382
  }, /*#__PURE__*/ react.createElement("div", {
285
383
  style: {
@@ -293,12 +391,15 @@ const ScriptCodeEditor = (props)=>{
293
391
  expandBtnBg: expandBtnBg,
294
392
  onClick: ()=>setSidebarOpen(true)
295
393
  }), /*#__PURE__*/ react.createElement("div", {
296
- ref: editorContainerRef
394
+ ref: editorContainerRef,
395
+ style: isFullscreen ? {
396
+ height: '100%'
397
+ } : void 0
297
398
  })), metadata && sidebarOpen && /*#__PURE__*/ react.createElement(TypePanel, {
298
399
  metadata: metadata,
299
- theme: theme,
300
- minHeight: minHeight,
301
- maxHeight: maxHeight,
400
+ theme: internalTheme,
401
+ minHeight: isFullscreen ? void 0 : minHeight,
402
+ maxHeight: isFullscreen ? void 0 : maxHeight,
302
403
  width: panelWidth,
303
404
  onWidthChange: setPanelWidth,
304
405
  onCollapse: ()=>setSidebarOpen(false)
@@ -3,8 +3,8 @@ import type { ScriptMetadata } from '../types';
3
3
  export interface TypePanelProps {
4
4
  metadata: ScriptMetadata;
5
5
  theme: 'dark' | 'light';
6
- minHeight: number;
7
- maxHeight: number;
6
+ minHeight?: number;
7
+ maxHeight?: number;
8
8
  width: number;
9
9
  onWidthChange: (width: number) => void;
10
10
  onCollapse: () => void;
@@ -78,8 +78,8 @@ const TypePanel = ({ metadata, theme, minHeight, maxHeight, width, onWidthChange
78
78
  display: 'flex',
79
79
  flexDirection: 'row',
80
80
  overflow: 'hidden',
81
- minHeight: `${minHeight}px`,
82
- maxHeight: `${maxHeight}px`,
81
+ minHeight: null != minHeight ? `${minHeight}px` : void 0,
82
+ maxHeight: null != maxHeight ? `${maxHeight}px` : void 0,
83
83
  position: 'relative'
84
84
  }
85
85
  }, /*#__PURE__*/ react.createElement(DragHandle, {
@@ -1,3 +1,4 @@
1
+ import type React from 'react';
1
2
  /** 函数参数信息 */
2
3
  export interface ScriptParameterInfo {
3
4
  dataType: string;
@@ -51,20 +52,32 @@ export interface ScriptCodeEditorProps {
51
52
  readonly?: boolean;
52
53
  /** 代码变化回调 */
53
54
  onChange?: (value: string) => void;
54
- /** 编译/测试脚本回调(后续接入 API) */
55
- onCompile?: (code: string) => void;
56
- /** 主题切换回调 */
57
- onThemeChange?: (theme: 'dark' | 'light') => void;
58
55
  /** 占位符 */
59
56
  placeholder?: string;
60
- /** 主题 */
61
- theme?: 'dark' | 'light';
57
+ /** 初始主题(默认 'dark'),编辑器内部管理状态 */
58
+ defaultTheme?: 'dark' | 'light';
59
+ /** 主题切换通知回调(可选) */
60
+ onThemeChange?: (theme: 'dark' | 'light') => void;
62
61
  /** 标题(可选) */
63
62
  title?: string;
64
63
  /** 脚本元数据 — 提供后启用类型面板和自动补全 */
65
64
  metadata?: ScriptMetadata;
66
65
  /** 类型面板侧边栏默认是否展开(默认:提供 metadata 时为 true) */
67
66
  defaultSidebarOpen?: boolean;
67
+ /** 是否显示主题切换按钮(默认 false) */
68
+ enableThemeToggle?: boolean;
69
+ /** 是否显示格式化按钮(默认 false,需配合 onFormat 使用) */
70
+ enableFormat?: boolean;
71
+ /** 是否显示编译验证按钮(默认 false,需配合 onCompile 使用) */
72
+ enableCompile?: boolean;
73
+ /** 是否显示全屏按钮(默认 false) */
74
+ enableFullscreen?: boolean;
75
+ /** 格式化代码回调(enableFormat 时需提供) */
76
+ onFormat?: () => void;
77
+ /** 编译/测试脚本回调(enableCompile 时需提供) */
78
+ onCompile?: (code: string) => void;
79
+ /** 工具栏额外内容(渲染在内置按钮之后,任意 React 节点) */
80
+ toolbarExtra?: React.ReactNode;
68
81
  /** 其他选项 */
69
82
  options?: {
70
83
  /** 字体大小 */
@@ -0,0 +1,105 @@
1
+ /** 自定义脚本标记 */
2
+ export declare const CUSTOM_SCRIPT = "@CUSTOM_SCRIPT";
3
+ /** 脚本标题标记 */
4
+ export declare const SCRIPT_TITLE = "@SCRIPT_TITLE";
5
+ /** 脚本元数据标记 */
6
+ export declare const SCRIPT_META = "@SCRIPT_META";
7
+ /**
8
+ * 格式化选项接口
9
+ */
10
+ export interface FormatOptions {
11
+ /** 缩进大小(空格数) */
12
+ indentSize?: number;
13
+ /** 操作符周围添加空格 */
14
+ addSpacesAroundOperators?: boolean;
15
+ /** 格式化注释 */
16
+ formatComments?: boolean;
17
+ /** 最大行长度 */
18
+ maxLineLength?: number;
19
+ /** 保留空行 */
20
+ preserveEmptyLines?: boolean;
21
+ }
22
+ /**
23
+ * Groovy脚本格式化器
24
+ */
25
+ export declare class GroovyFormatter {
26
+ /**
27
+ * 格式化Groovy脚本内容
28
+ * @param script Groovy脚本内容
29
+ * @returns 格式化后的脚本
30
+ */
31
+ static formatScript(script: string): string;
32
+ /**
33
+ * 规范化缩进
34
+ */
35
+ private static normalizeIndentation;
36
+ /**
37
+ * 判断是否需要增加缩进
38
+ */
39
+ private static shouldIncreaseIndent;
40
+ /**
41
+ * 判断是否需要减少缩进
42
+ */
43
+ private static shouldDecreaseIndent;
44
+ /**
45
+ * 增强版格式化:包含更多Groovy特定处理
46
+ */
47
+ static formatGroovyScript(script: string, options?: FormatOptions): string;
48
+ /**
49
+ * 格式化注释
50
+ */
51
+ private static formatComments;
52
+ /**
53
+ * 操作符周围添加空格
54
+ */
55
+ private static addSpacesAroundOperators;
56
+ /**
57
+ * 格式化Groovy特定语法
58
+ */
59
+ private static formatGroovySpecific;
60
+ /**
61
+ * 压缩脚本(移除所有非必要空白)
62
+ */
63
+ static minifyScript(script: string): string;
64
+ }
65
+ /**
66
+ * Groovy脚本转换器工具类,提供一些通用的脚本处理方法
67
+ */
68
+ export declare class GroovyScriptConvertorUtil {
69
+ /**
70
+ * 判断脚本是否包含自定义注释标记
71
+ */
72
+ static isCustomScript(script: string): boolean;
73
+ /**
74
+ * 格式化脚本内容,去除多余空白等
75
+ */
76
+ static formatScript(script: string): string;
77
+ /**
78
+ * 将普通脚本转换为包含自定义注释标记的脚本
79
+ */
80
+ static toCustomScript(script: string): string;
81
+ /**
82
+ * 获取脚本中的标题注释内容
83
+ */
84
+ static getScriptTitle(script: string): string;
85
+ /**
86
+ * 更新脚本中的标题注释内容,如果不存在则添加
87
+ */
88
+ static updateScriptTitle(script: string, title: string): string;
89
+ /**
90
+ * 获取脚本中的元数据
91
+ */
92
+ static getScriptMeta(script: string): string;
93
+ /**
94
+ * 更新脚本中的元数据内容,如果不存在则添加
95
+ */
96
+ static updateScriptMeta(script: string, meta: string): string;
97
+ /**
98
+ * 清除脚本中的注释
99
+ */
100
+ static clearComments(script: string): string;
101
+ /**
102
+ * 提取脚本中的return表达式
103
+ */
104
+ static getReturnScript(script: string): string;
105
+ }
@@ -0,0 +1,239 @@
1
+ const CUSTOM_SCRIPT = '@CUSTOM_SCRIPT';
2
+ const SCRIPT_TITLE = '@SCRIPT_TITLE';
3
+ const SCRIPT_META = '@SCRIPT_META';
4
+ class GroovyFormatter {
5
+ static formatScript(script) {
6
+ if (!script || 0 === script.trim().length) return '';
7
+ let formatted = script;
8
+ formatted = formatted.trim();
9
+ formatted = formatted.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
10
+ formatted = formatted.replace(/\n\s*\n/g, '\n\n');
11
+ const lines = formatted.split('\n');
12
+ formatted = lines.map((line)=>line.replace(/\s+$/, '')).join('\n');
13
+ formatted = formatted.replace(/\n+$/, '\n');
14
+ formatted = this.normalizeIndentation(formatted);
15
+ return formatted;
16
+ }
17
+ static normalizeIndentation(script) {
18
+ const lines = script.split('\n');
19
+ const result = [];
20
+ let indentLevel = 0;
21
+ let inMultilineString = false;
22
+ let multilineStringDelimiter = '';
23
+ for(let i = 0; i < lines.length; i++){
24
+ const line = lines[i];
25
+ const trimmedLine = line.trim();
26
+ if (0 === trimmedLine.length) {
27
+ result.push('');
28
+ continue;
29
+ }
30
+ if (inMultilineString) {
31
+ if (trimmedLine.includes(multilineStringDelimiter)) {
32
+ const count = (trimmedLine.match(new RegExp(multilineStringDelimiter, 'g')) || []).length;
33
+ if (count % 2 === 1) {
34
+ inMultilineString = false;
35
+ multilineStringDelimiter = '';
36
+ }
37
+ }
38
+ } else if (trimmedLine.includes("'''") || trimmedLine.includes('"""')) {
39
+ const delimiter = trimmedLine.includes("'''") ? "'''" : '"""';
40
+ const count = (trimmedLine.match(new RegExp(delimiter, 'g')) || []).length;
41
+ if (count % 2 === 1) {
42
+ inMultilineString = true;
43
+ multilineStringDelimiter = delimiter;
44
+ }
45
+ }
46
+ if (inMultilineString) result.push(line);
47
+ else {
48
+ const shouldDecreaseIndent = this.shouldDecreaseIndent(trimmedLine);
49
+ if (shouldDecreaseIndent) indentLevel = Math.max(0, indentLevel - 1);
50
+ const indentation = ' '.repeat(indentLevel);
51
+ if (this.shouldIncreaseIndent(trimmedLine)) indentLevel++;
52
+ result.push(indentation + trimmedLine);
53
+ }
54
+ }
55
+ return result.join('\n');
56
+ }
57
+ static shouldIncreaseIndent(line) {
58
+ const increasePatterns = [
59
+ /.*\{\s*$/,
60
+ /.*\(\s*$/,
61
+ /.*\[\s*$/,
62
+ /^if\s*\(.*\)\s*$/,
63
+ /^for\s*\(.*\)\s*$/,
64
+ /^while\s*\(.*\)\s*$/,
65
+ /^switch\s*\(.*\)\s*$/,
66
+ /^try\s*$/,
67
+ /^catch\s*\(.*\)\s*$/,
68
+ /^finally\s*$/,
69
+ /^else\s*$/,
70
+ /^else\s+if\s*\(.*\)\s*$/,
71
+ /.*->\s*$/,
72
+ /^def\s+\w+\s*=\s*\{/,
73
+ /^class\s+\w+/,
74
+ /^interface\s+\w+/,
75
+ /^enum\s+\w+/,
76
+ /^annotation\s+\w+/,
77
+ /^trait\s+\w+/,
78
+ /^static\s*\{/
79
+ ];
80
+ return increasePatterns.some((pattern)=>pattern.test(line));
81
+ }
82
+ static shouldDecreaseIndent(line) {
83
+ const decreasePatterns = [
84
+ /^\}/,
85
+ /^\]/,
86
+ /^\)/,
87
+ /^else\b/,
88
+ /^catch\b/,
89
+ /^finally\b/,
90
+ /^case\b/,
91
+ /^default\s*:/
92
+ ];
93
+ return decreasePatterns.some((pattern)=>pattern.test(line));
94
+ }
95
+ static formatGroovyScript(script, options) {
96
+ if (!script || 0 === script.trim().length) return '';
97
+ const opts = {
98
+ indentSize: 4,
99
+ addSpacesAroundOperators: true,
100
+ formatComments: true,
101
+ ...options
102
+ };
103
+ let formatted = script;
104
+ formatted = formatted.trim();
105
+ formatted = formatted.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
106
+ if (opts.formatComments) formatted = this.formatComments(formatted);
107
+ if (opts.addSpacesAroundOperators) formatted = this.addSpacesAroundOperators(formatted);
108
+ formatted = formatted.replace(/,(?!\s)/g, ', ');
109
+ formatted = formatted.replace(/;(?!\s)/g, '; ');
110
+ formatted = formatted.replace(/\s+/g, ' ');
111
+ const lines = formatted.split(';');
112
+ if (lines.length > 1) formatted = lines.map((line)=>line.trim()).filter((line)=>line.length > 0).join(';\n');
113
+ formatted = this.normalizeIndentation(formatted);
114
+ formatted = this.formatGroovySpecific(formatted);
115
+ return formatted;
116
+ }
117
+ static formatComments(script) {
118
+ let formatted = script.replace(/\/\/\s*/g, '// ');
119
+ formatted = formatted.replace(/\/\*(\s*)\*\//g, '/**$1*/');
120
+ return formatted;
121
+ }
122
+ static addSpacesAroundOperators(script) {
123
+ const operators = [
124
+ '=',
125
+ '==',
126
+ '!=',
127
+ '===',
128
+ '!==',
129
+ '>',
130
+ '<',
131
+ '>=',
132
+ '<=',
133
+ '+',
134
+ '-',
135
+ '*',
136
+ '/',
137
+ '%',
138
+ '&&',
139
+ '||',
140
+ '&',
141
+ '|',
142
+ '^',
143
+ '<<',
144
+ '>>',
145
+ '>>>',
146
+ '+=',
147
+ '-=',
148
+ '*=',
149
+ '/=',
150
+ '%=',
151
+ '&=',
152
+ '|=',
153
+ '^=',
154
+ '<<=',
155
+ '>>=',
156
+ '>>>='
157
+ ];
158
+ let formatted = script;
159
+ operators.forEach((op)=>{
160
+ const regex = new RegExp(`(?<![\\'"\\\`])\\\\${op}(?![\\'"\\\`])`, 'g');
161
+ formatted = formatted.replace(regex, ` ${op} `);
162
+ });
163
+ formatted = formatted.replace(/\s+/g, ' ');
164
+ return formatted;
165
+ }
166
+ static formatGroovySpecific(script) {
167
+ let formatted = script;
168
+ formatted = formatted.replace(/\$\{(\s*)(\w+)(\s*)\}/g, '${$2}');
169
+ formatted = formatted.replace(/\{(\s*)(\w+)(\s*)->/g, '{ $2 ->');
170
+ formatted = formatted.replace(/\[(\s*)(\w+)(\s*):/g, '[$2:');
171
+ formatted = formatted.replace(/@(\w+)\((\s*)(\w+)(\s*)=/g, '@$1($3=');
172
+ formatted = formatted.replace(/(\w+)(\s*)\?\.(\s*)(\w+)/g, '$1?.$4');
173
+ formatted = formatted.replace(/(\w+)(\s*)\*\.(\s*)(\w+)/g, '$1*.$4');
174
+ return formatted;
175
+ }
176
+ static minifyScript(script) {
177
+ if (!script || 0 === script.trim().length) return '';
178
+ let minified = script;
179
+ minified = minified.replace(/\/\/.*$/gm, '');
180
+ minified = minified.replace(/\/\*[\s\S]*?\*\//g, '');
181
+ minified = minified.replace(/\s+/g, ' ');
182
+ minified = minified.replace(/\s*([{}();,=+\-*/%&|<>!])\s*/g, '$1');
183
+ minified = minified.replace(/\n\s*\n/g, '\n');
184
+ return minified.trim();
185
+ }
186
+ }
187
+ class GroovyScriptConvertorUtil {
188
+ static isCustomScript(script) {
189
+ if (!script) return false;
190
+ return script.includes(CUSTOM_SCRIPT);
191
+ }
192
+ static formatScript(script) {
193
+ if (!script) return '';
194
+ return GroovyFormatter.formatScript(script);
195
+ }
196
+ static toCustomScript(script) {
197
+ if (GroovyScriptConvertorUtil.isCustomScript(script)) return GroovyFormatter.formatScript(script);
198
+ return GroovyFormatter.formatScript(`// ${CUSTOM_SCRIPT}\n${script}`);
199
+ }
200
+ static getScriptTitle(script) {
201
+ if (!script) return '';
202
+ const titleMatch = script.match(new RegExp(`//\\s*${SCRIPT_TITLE}\\s*(.+)`));
203
+ if (titleMatch) return titleMatch[1].trim();
204
+ return '';
205
+ }
206
+ static updateScriptTitle(script, title) {
207
+ const titleComment = `// ${SCRIPT_TITLE} ${title}`;
208
+ if (GroovyScriptConvertorUtil.getScriptTitle(script)) return script.replace(new RegExp(`//\\s*${SCRIPT_TITLE}\\s*.+`), titleComment);
209
+ return `${titleComment}\n${script}`;
210
+ }
211
+ static getScriptMeta(script) {
212
+ if (!script) return '';
213
+ const titleMatch = script.match(new RegExp(`//\\s*${SCRIPT_META}\\s*(.+)`));
214
+ if (titleMatch) return titleMatch[1].trim();
215
+ return '';
216
+ }
217
+ static updateScriptMeta(script, meta) {
218
+ const metaComment = `// ${SCRIPT_META} ${meta}`;
219
+ if (GroovyScriptConvertorUtil.getScriptMeta(script)) return script.replace(new RegExp(`//\\s*${SCRIPT_META}\\s*.+`), metaComment);
220
+ return `${metaComment}\n${script}`;
221
+ }
222
+ static clearComments(script) {
223
+ if (!script) return '';
224
+ return script.replace(/\/\/.*$/gm, '').trim();
225
+ }
226
+ static getReturnScript(script) {
227
+ try {
228
+ let result = GroovyScriptConvertorUtil.clearComments(script);
229
+ const funcMatch = result.match(/def\s+run\s*\([^)]*\)\s*\{([\s\S]*)\}/);
230
+ if (funcMatch) result = funcMatch[1];
231
+ const returnMatch = result.match(/return\s+(.+?);?\s*$/m);
232
+ result = returnMatch ? returnMatch[1].trim() : result.trim();
233
+ return result;
234
+ } catch {
235
+ return '';
236
+ }
237
+ }
238
+ }
239
+ export { CUSTOM_SCRIPT, GroovyFormatter, GroovyScriptConvertorUtil, SCRIPT_META, SCRIPT_TITLE };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coding-script/script-engine",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "script-engine components",
5
5
  "keywords": [
6
6
  "coding-script",