@lydt/geogebra-studio 0.0.0-stage → 0.4.0-rc.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/.agents/plugins/marketplace.json +21 -0
- package/.codex-plugin/plugin.json +21 -0
- package/.mcp.json +18 -0
- package/LICENSE +339 -0
- package/LICENSE-MIT +21 -0
- package/README.md +108 -2
- package/README.zh-CN.md +89 -0
- package/THIRD_PARTY_NOTICES.md +35 -0
- package/dist/cli.d.ts +27 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +234 -0
- package/dist/cli.js.map +1 -0
- package/dist/export/files.d.ts +4 -0
- package/dist/export/files.d.ts.map +1 -0
- package/dist/export/files.js +44 -0
- package/dist/export/files.js.map +1 -0
- package/dist/export/gikz.d.ts +11 -0
- package/dist/export/gikz.d.ts.map +1 -0
- package/dist/export/gikz.js +28 -0
- package/dist/export/gikz.js.map +1 -0
- package/dist/export/html.d.ts +14 -0
- package/dist/export/html.d.ts.map +1 -0
- package/dist/export/html.js +97 -0
- package/dist/export/html.js.map +1 -0
- package/dist/export/semantic.d.ts +43 -0
- package/dist/export/semantic.d.ts.map +1 -0
- package/dist/export/semantic.js +212 -0
- package/dist/export/semantic.js.map +1 -0
- package/dist/export/viewer-themes.d.ts +76 -0
- package/dist/export/viewer-themes.d.ts.map +1 -0
- package/dist/export/viewer-themes.js +169 -0
- package/dist/export/viewer-themes.js.map +1 -0
- package/dist/export/workspace.d.ts +4 -0
- package/dist/export/workspace.d.ts.map +1 -0
- package/dist/export/workspace.js +25 -0
- package/dist/export/workspace.js.map +1 -0
- package/dist/gikz-browser.js +770 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +35 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +187 -0
- package/dist/server.js.map +1 -0
- package/dist/studio/artifact.d.ts +37 -0
- package/dist/studio/artifact.d.ts.map +1 -0
- package/dist/studio/artifact.js +61 -0
- package/dist/studio/artifact.js.map +1 -0
- package/dist/studio/browser.d.ts +7 -0
- package/dist/studio/browser.d.ts.map +1 -0
- package/dist/studio/browser.js +67 -0
- package/dist/studio/browser.js.map +1 -0
- package/dist/studio/install.d.ts +4 -0
- package/dist/studio/install.d.ts.map +1 -0
- package/dist/studio/install.js +60 -0
- package/dist/studio/install.js.map +1 -0
- package/dist/tools/apps-api-tools.d.ts +4 -0
- package/dist/tools/apps-api-tools.d.ts.map +1 -0
- package/dist/tools/apps-api-tools.js +96 -0
- package/dist/tools/apps-api-tools.js.map +1 -0
- package/dist/tools/educational-templates.d.ts +33 -0
- package/dist/tools/educational-templates.d.ts.map +1 -0
- package/dist/tools/educational-templates.js +261 -0
- package/dist/tools/educational-templates.js.map +1 -0
- package/dist/tools/export-tools.d.ts +3 -0
- package/dist/tools/export-tools.d.ts.map +1 -0
- package/dist/tools/export-tools.js +118 -0
- package/dist/tools/export-tools.js.map +1 -0
- package/dist/tools/geogebra-tools.d.ts +15 -0
- package/dist/tools/geogebra-tools.d.ts.map +1 -0
- package/dist/tools/geogebra-tools.js +3442 -0
- package/dist/tools/geogebra-tools.js.map +1 -0
- package/dist/tools/index.d.ts +43 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +183 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/performance-tools.d.ts +7 -0
- package/dist/tools/performance-tools.d.ts.map +1 -0
- package/dist/tools/performance-tools.js +312 -0
- package/dist/tools/performance-tools.js.map +1 -0
- package/dist/tools/templates/algebra-templates.d.ts +7 -0
- package/dist/tools/templates/algebra-templates.d.ts.map +1 -0
- package/dist/tools/templates/algebra-templates.js +568 -0
- package/dist/tools/templates/algebra-templates.js.map +1 -0
- package/dist/tools/templates/geometry-templates.d.ts +7 -0
- package/dist/tools/templates/geometry-templates.d.ts.map +1 -0
- package/dist/tools/templates/geometry-templates.js +492 -0
- package/dist/tools/templates/geometry-templates.js.map +1 -0
- package/dist/types/geogebra.d.ts +103 -0
- package/dist/types/geogebra.d.ts.map +1 -0
- package/dist/types/geogebra.js +34 -0
- package/dist/types/geogebra.js.map +1 -0
- package/dist/types/mcp.d.ts +125 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +19 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/utils/app-info.d.ts +12 -0
- package/dist/utils/app-info.d.ts.map +1 -0
- package/dist/utils/app-info.js +30 -0
- package/dist/utils/app-info.js.map +1 -0
- package/dist/utils/errors.d.ts +37 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +72 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/gemini-compatibility.d.ts +39 -0
- package/dist/utils/gemini-compatibility.d.ts.map +1 -0
- package/dist/utils/gemini-compatibility.js +154 -0
- package/dist/utils/gemini-compatibility.js.map +1 -0
- package/dist/utils/geogebra-instance.d.ts +252 -0
- package/dist/utils/geogebra-instance.d.ts.map +1 -0
- package/dist/utils/geogebra-instance.js +1560 -0
- package/dist/utils/geogebra-instance.js.map +1 -0
- package/dist/utils/geogebra-mock.d.ts +172 -0
- package/dist/utils/geogebra-mock.d.ts.map +1 -0
- package/dist/utils/geogebra-mock.js +732 -0
- package/dist/utils/geogebra-mock.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +46 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/performance/index.d.ts +76 -0
- package/dist/utils/performance/index.d.ts.map +1 -0
- package/dist/utils/performance/index.js +163 -0
- package/dist/utils/performance/index.js.map +1 -0
- package/dist/utils/performance/instance-pool.d.ts +78 -0
- package/dist/utils/performance/instance-pool.d.ts.map +1 -0
- package/dist/utils/performance/instance-pool.js +268 -0
- package/dist/utils/performance/instance-pool.js.map +1 -0
- package/dist/utils/validation.d.ts +92 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +557 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/widget.html +110 -0
- package/docs/APPS-API-BRIDGE.md +28 -0
- package/docs/MODERNIZATION.md +15 -0
- package/docs/TANGENT-REGRESSION.md +34 -0
- package/docs/VALIDATION.md +26 -0
- package/docs/VISUAL-STANDARD.md +26 -0
- package/docs/images/native-construction.png +0 -0
- package/examples/tangent-construction.json +26 -0
- package/gikz/LICENSE +339 -0
- package/gikz/PROVENANCE.md +8 -0
- package/gikz/browser.cjs +2 -0
- package/gikz/cli.cjs +195 -0
- package/gikz/core.cjs +806 -0
- package/mcp.json +20 -0
- package/package.json +107 -4
- package/plugin.json +35 -0
- package/runtime/package.json +4 -0
- package/scripts/build-plugin.cjs +15 -0
- package/scripts/build-widget.cjs +7 -0
- package/skills/geogebra-construct/SKILL.md +40 -0
- package/skills/geogebra-setup/SKILL.md +37 -0
- package/skills/geometry-export/SKILL.md +31 -0
- package/src/cli.ts +212 -0
- package/src/export/files.ts +26 -0
- package/src/export/gikz.ts +22 -0
- package/src/export/html.ts +108 -0
- package/src/export/semantic.ts +180 -0
- package/src/export/viewer-themes.ts +165 -0
- package/src/export/workspace.ts +15 -0
- package/src/index.ts +71 -0
- package/src/server.ts +168 -0
- package/src/studio/artifact.ts +50 -0
- package/src/studio/browser.ts +38 -0
- package/src/studio/install.ts +43 -0
- package/src/tools/apps-api-tools.ts +96 -0
- package/src/tools/educational-templates.ts +299 -0
- package/src/tools/export-tools.ts +110 -0
- package/src/tools/geogebra-tools.ts +3836 -0
- package/src/tools/index.ts +192 -0
- package/src/tools/performance-tools.ts +322 -0
- package/src/tools/templates/algebra-templates.ts +619 -0
- package/src/tools/templates/geometry-templates.ts +550 -0
- package/src/types/geogebra.ts +140 -0
- package/src/types/global.d.ts +31 -0
- package/src/types/mcp.ts +141 -0
- package/src/utils/app-info.ts +35 -0
- package/src/utils/errors.ts +97 -0
- package/src/utils/gemini-compatibility.ts +193 -0
- package/src/utils/geogebra-instance.ts +1712 -0
- package/src/utils/geogebra-mock.ts +867 -0
- package/src/utils/logger.ts +60 -0
- package/src/utils/performance/index.ts +211 -0
- package/src/utils/performance/instance-pool.ts +323 -0
- package/src/utils/validation.ts +647 -0
- package/tsconfig.json +35 -0
- package/widget/main.ts +12 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for GeoGebra MCP tools
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ValidationResult {
|
|
6
|
+
isValid: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate point name (GeoGebra naming conventions)
|
|
12
|
+
*/
|
|
13
|
+
export function validatePointName(name: string): ValidationResult {
|
|
14
|
+
if (!name || typeof name !== 'string') {
|
|
15
|
+
return { isValid: false, error: 'Point name must be a non-empty string' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (name.length === 0) {
|
|
19
|
+
return { isValid: false, error: 'Point name cannot be empty' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// GeoGebra naming rules: start with letter, can contain letters, numbers, underscore
|
|
23
|
+
const namePattern = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
24
|
+
if (!namePattern.test(name)) {
|
|
25
|
+
return { isValid: false, error: 'Point name must start with a letter and contain only letters, numbers, and underscores' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { isValid: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validate object name (general GeoGebra naming conventions)
|
|
33
|
+
*/
|
|
34
|
+
export function validateObjectName(name: string): ValidationResult {
|
|
35
|
+
return validatePointName(name); // Same rules apply
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate coordinates
|
|
40
|
+
*/
|
|
41
|
+
export function validateCoordinates(x: number, y: number): ValidationResult {
|
|
42
|
+
if (typeof x !== 'number' || typeof y !== 'number') {
|
|
43
|
+
return { isValid: false, error: 'Coordinates must be numbers' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!isFinite(x) || !isFinite(y)) {
|
|
47
|
+
return { isValid: false, error: 'Coordinates must be finite numbers' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Reasonable coordinate range for visualization
|
|
51
|
+
const maxCoord = 1000000;
|
|
52
|
+
if (Math.abs(x) > maxCoord || Math.abs(y) > maxCoord) {
|
|
53
|
+
return { isValid: false, error: `Coordinates must be within range [-${maxCoord}, ${maxCoord}]` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { isValid: true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate radius
|
|
61
|
+
*/
|
|
62
|
+
export function validateRadius(radius: number): ValidationResult {
|
|
63
|
+
if (typeof radius !== 'number') {
|
|
64
|
+
return { isValid: false, error: 'Radius must be a number' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!isFinite(radius)) {
|
|
68
|
+
return { isValid: false, error: 'Radius must be a finite number' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (radius < 0) {
|
|
72
|
+
return { isValid: false, error: 'Radius must be non-negative' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (radius > 1000000) {
|
|
76
|
+
return { isValid: false, error: 'Radius must be reasonable (≤ 1,000,000)' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { isValid: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate polygon vertices
|
|
84
|
+
*/
|
|
85
|
+
export function validatePolygonVertices(vertices: string[]): ValidationResult {
|
|
86
|
+
if (!Array.isArray(vertices)) {
|
|
87
|
+
return { isValid: false, error: 'Vertices must be an array' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (vertices.length < 3) {
|
|
91
|
+
return { isValid: false, error: 'Polygon must have at least 3 vertices' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (vertices.length > 100) {
|
|
95
|
+
return { isValid: false, error: 'Polygon cannot have more than 100 vertices' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check for duplicate vertices
|
|
99
|
+
const uniqueVertices = new Set(vertices);
|
|
100
|
+
if (uniqueVertices.size !== vertices.length) {
|
|
101
|
+
return { isValid: false, error: 'Polygon vertices must be unique' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate each vertex name
|
|
105
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
106
|
+
const vertex = vertices[i];
|
|
107
|
+
if (!vertex) {
|
|
108
|
+
return { isValid: false, error: `Vertex ${i + 1} is undefined or empty` };
|
|
109
|
+
}
|
|
110
|
+
const validation = validatePointName(vertex);
|
|
111
|
+
if (!validation.isValid) {
|
|
112
|
+
return { isValid: false, error: `Vertex ${i + 1}: ${validation.error}` };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { isValid: true };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate linear equation format
|
|
121
|
+
*/
|
|
122
|
+
export function validateLinearEquation(equation: string): ValidationResult {
|
|
123
|
+
if (!equation || typeof equation !== 'string') {
|
|
124
|
+
return { isValid: false, error: 'Equation must be a non-empty string' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!equation.includes('=')) {
|
|
128
|
+
return { isValid: false, error: 'Equation must contain an equals sign (=)' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Basic validation for common linear equation formats
|
|
132
|
+
const patterns = [
|
|
133
|
+
/^y\s*=\s*[+-]?\s*\d*\.?\d*\s*\*?\s*x\s*[+-]\s*\d+\.?\d*$/, // y = mx + b
|
|
134
|
+
/^y\s*=\s*[+-]?\s*\d+\.?\d*\s*\*?\s*x$/, // y = mx
|
|
135
|
+
/^y\s*=\s*[+-]?\s*\d+\.?\d*$/, // y = b (horizontal line)
|
|
136
|
+
/^x\s*=\s*[+-]?\s*\d+\.?\d*$/, // x = a (vertical line)
|
|
137
|
+
/^[+-]?\s*\d*\.?\d*\s*\*?\s*x\s*[+-]\s*\d*\.?\d*\s*\*?\s*y\s*=\s*[+-]?\s*\d+\.?\d*$/, // ax + by = c
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const normalizedEq = equation.replace(/\s+/g, ' ').trim();
|
|
141
|
+
const isValidFormat = patterns.some(pattern => pattern.test(normalizedEq));
|
|
142
|
+
|
|
143
|
+
if (!isValidFormat) {
|
|
144
|
+
return {
|
|
145
|
+
isValid: false,
|
|
146
|
+
error: 'Equation format not recognized. Use formats like "y = 2x + 3", "x = 5", or "2x + 3y = 6"'
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { isValid: true };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Validate that required parameters are provided for a given method
|
|
155
|
+
*/
|
|
156
|
+
export function validateRequiredParams(params: Record<string, unknown>, required: string[]): ValidationResult {
|
|
157
|
+
for (const param of required) {
|
|
158
|
+
if (!(param in params) || params[param] === undefined || params[param] === null) {
|
|
159
|
+
return { isValid: false, error: `Missing required parameter: ${param}` };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { isValid: true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Validate function expression for standard functions f(x) = ...
|
|
167
|
+
*/
|
|
168
|
+
export function validateFunctionExpression(expression: string): ValidationResult {
|
|
169
|
+
if (!expression || typeof expression !== 'string') {
|
|
170
|
+
return { isValid: false, error: 'Function expression must be a non-empty string' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const trimmed = expression.trim();
|
|
174
|
+
if (trimmed.length === 0) {
|
|
175
|
+
return { isValid: false, error: 'Function expression cannot be empty' };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Basic validation for function expressions
|
|
179
|
+
// Allow common mathematical functions and operators
|
|
180
|
+
const allowedPattern = /^[x\d\+\-\*\/\^\(\)\.\s,sincotaglnbsqrtepie]*$/i;
|
|
181
|
+
if (!allowedPattern.test(trimmed)) {
|
|
182
|
+
return { isValid: false, error: 'Function expression contains invalid characters. Use only x, numbers, +, -, *, /, ^, (), sin, cos, tan, log, ln, sqrt, abs, e, pi' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check for balanced parentheses
|
|
186
|
+
let parenCount = 0;
|
|
187
|
+
for (const char of trimmed) {
|
|
188
|
+
if (char === '(') parenCount++;
|
|
189
|
+
if (char === ')') parenCount--;
|
|
190
|
+
if (parenCount < 0) {
|
|
191
|
+
return { isValid: false, error: 'Unbalanced parentheses in function expression' };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (parenCount !== 0) {
|
|
195
|
+
return { isValid: false, error: 'Unbalanced parentheses in function expression' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { isValid: true };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validate parametric function expressions
|
|
203
|
+
*/
|
|
204
|
+
export function validateParametricExpressions(xExpr: string, yExpr: string, parameter: string): ValidationResult {
|
|
205
|
+
if (!parameter || typeof parameter !== 'string') {
|
|
206
|
+
return { isValid: false, error: 'Parameter name must be a non-empty string' };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Validate parameter name (typically t, u, s, etc.)
|
|
210
|
+
const paramPattern = /^[a-zA-Z][a-zA-Z0-9]*$/;
|
|
211
|
+
if (!paramPattern.test(parameter)) {
|
|
212
|
+
return { isValid: false, error: 'Parameter name must start with a letter and contain only letters and numbers' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Validate x expression
|
|
216
|
+
const xValidation = validateParametricExpression(xExpr, parameter);
|
|
217
|
+
if (!xValidation.isValid) {
|
|
218
|
+
return { isValid: false, error: `X expression: ${xValidation.error}` };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Validate y expression
|
|
222
|
+
const yValidation = validateParametricExpression(yExpr, parameter);
|
|
223
|
+
if (!yValidation.isValid) {
|
|
224
|
+
return { isValid: false, error: `Y expression: ${yValidation.error}` };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { isValid: true };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Validate a single parametric expression
|
|
232
|
+
*/
|
|
233
|
+
function validateParametricExpression(expression: string, parameter: string): ValidationResult {
|
|
234
|
+
if (!expression || typeof expression !== 'string') {
|
|
235
|
+
return { isValid: false, error: 'Expression must be a non-empty string' };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const trimmed = expression.trim();
|
|
239
|
+
if (trimmed.length === 0) {
|
|
240
|
+
return { isValid: false, error: 'Expression cannot be empty' };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Allow the parameter variable and common mathematical functions
|
|
244
|
+
const escapedParam = parameter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
245
|
+
const allowedPattern = new RegExp(`^[${escapedParam}\\d\\+\\-\\*\\/\\^\\(\\)\\.\\s,sincotaglnbsqrtepie]*$`, 'i');
|
|
246
|
+
|
|
247
|
+
if (!allowedPattern.test(trimmed)) {
|
|
248
|
+
return { isValid: false, error: `Expression contains invalid characters. Use only ${parameter}, numbers, +, -, *, /, ^, (), sin, cos, tan, log, ln, sqrt, abs, e, pi` };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check for balanced parentheses
|
|
252
|
+
let parenCount = 0;
|
|
253
|
+
for (const char of trimmed) {
|
|
254
|
+
if (char === '(') parenCount++;
|
|
255
|
+
if (char === ')') parenCount--;
|
|
256
|
+
if (parenCount < 0) {
|
|
257
|
+
return { isValid: false, error: 'Unbalanced parentheses in expression' };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (parenCount !== 0) {
|
|
261
|
+
return { isValid: false, error: 'Unbalanced parentheses in expression' };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { isValid: true };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Validate implicit function expression
|
|
269
|
+
*/
|
|
270
|
+
export function validateImplicitExpression(expression: string): ValidationResult {
|
|
271
|
+
if (!expression || typeof expression !== 'string') {
|
|
272
|
+
return { isValid: false, error: 'Implicit expression must be a non-empty string' };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const trimmed = expression.trim();
|
|
276
|
+
if (trimmed.length === 0) {
|
|
277
|
+
return { isValid: false, error: 'Implicit expression cannot be empty' };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Must contain both x and y variables
|
|
281
|
+
if (!trimmed.includes('x') || !trimmed.includes('y')) {
|
|
282
|
+
return { isValid: false, error: 'Implicit expression must contain both x and y variables' };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Allow x, y, numbers, and mathematical functions
|
|
286
|
+
const allowedPattern = /^[xy\d\+\-\*\/\^\(\)\.\s,sincotaglnbsqrtepie=]*$/i;
|
|
287
|
+
if (!allowedPattern.test(trimmed)) {
|
|
288
|
+
return { isValid: false, error: 'Implicit expression contains invalid characters. Use only x, y, numbers, +, -, *, /, ^, (), sin, cos, tan, log, ln, sqrt, abs, e, pi, =' };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Check for balanced parentheses
|
|
292
|
+
let parenCount = 0;
|
|
293
|
+
for (const char of trimmed) {
|
|
294
|
+
if (char === '(') parenCount++;
|
|
295
|
+
if (char === ')') parenCount--;
|
|
296
|
+
if (parenCount < 0) {
|
|
297
|
+
return { isValid: false, error: 'Unbalanced parentheses in implicit expression' };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (parenCount !== 0) {
|
|
301
|
+
return { isValid: false, error: 'Unbalanced parentheses in implicit expression' };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { isValid: true };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Validate domain range parameters
|
|
309
|
+
*/
|
|
310
|
+
export function validateDomainRange(min: number, max: number, paramName: string = 'parameter'): ValidationResult {
|
|
311
|
+
if (typeof min !== 'number' || typeof max !== 'number') {
|
|
312
|
+
return { isValid: false, error: `${paramName} range bounds must be numbers` };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!isFinite(min) || !isFinite(max)) {
|
|
316
|
+
return { isValid: false, error: `${paramName} range bounds must be finite numbers` };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (min >= max) {
|
|
320
|
+
return { isValid: false, error: `${paramName} minimum must be less than maximum` };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Reasonable range limits
|
|
324
|
+
const maxRange = 1000000;
|
|
325
|
+
if (Math.abs(min) > maxRange || Math.abs(max) > maxRange) {
|
|
326
|
+
return { isValid: false, error: `${paramName} range must be within [-${maxRange}, ${maxRange}]` };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const rangeDiff = max - min;
|
|
330
|
+
if (rangeDiff < 0.001) {
|
|
331
|
+
return { isValid: false, error: `${paramName} range must be at least 0.001 units wide` };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (rangeDiff > maxRange * 2) {
|
|
335
|
+
return { isValid: false, error: `${paramName} range cannot exceed ${maxRange * 2} units wide` };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { isValid: true };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Validate function styling parameters
|
|
343
|
+
*/
|
|
344
|
+
export function validateFunctionStyling(color?: string, thickness?: number, style?: string): ValidationResult {
|
|
345
|
+
if (color !== undefined) {
|
|
346
|
+
if (typeof color !== 'string') {
|
|
347
|
+
return { isValid: false, error: 'Color must be a string' };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Basic color validation (hex, named colors, rgb)
|
|
351
|
+
const colorPattern = /^(#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}|rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)|[a-zA-Z]+)$/;
|
|
352
|
+
if (!colorPattern.test(color.trim())) {
|
|
353
|
+
return { isValid: false, error: 'Color must be a valid hex color (#RRGGBB), RGB value (rgb(r,g,b)), or color name' };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (thickness !== undefined) {
|
|
358
|
+
if (typeof thickness !== 'number') {
|
|
359
|
+
return { isValid: false, error: 'Line thickness must be a number' };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (!isFinite(thickness)) {
|
|
363
|
+
return { isValid: false, error: 'Line thickness must be a finite number' };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (thickness < 1 || thickness > 10) {
|
|
367
|
+
return { isValid: false, error: 'Line thickness must be between 1 and 10' };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (style !== undefined) {
|
|
372
|
+
if (typeof style !== 'string') {
|
|
373
|
+
return { isValid: false, error: 'Line style must be a string' };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const validStyles = ['solid', 'dashed', 'dotted'];
|
|
377
|
+
if (!validStyles.includes(style.toLowerCase())) {
|
|
378
|
+
return { isValid: false, error: `Line style must be one of: ${validStyles.join(', ')}` };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return { isValid: true };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Validate algebraic expression for CAS operations
|
|
387
|
+
*/
|
|
388
|
+
export function validateAlgebraicExpression(expression: string): ValidationResult {
|
|
389
|
+
if (!expression || typeof expression !== 'string') {
|
|
390
|
+
return { isValid: false, error: 'Expression must be a non-empty string' };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const trimmed = expression.trim();
|
|
394
|
+
if (trimmed.length === 0) {
|
|
395
|
+
return { isValid: false, error: 'Expression cannot be empty' };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Allow common mathematical symbols, variables, and functions
|
|
399
|
+
const allowedPattern = /^[a-zA-Z\d\+\-\*\/\^\(\)\.\s,=]*$/;
|
|
400
|
+
if (!allowedPattern.test(trimmed)) {
|
|
401
|
+
return {
|
|
402
|
+
isValid: false,
|
|
403
|
+
error: 'Expression contains invalid characters. Use only variables, numbers, +, -, *, /, ^, =, ()'
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Check for balanced parentheses
|
|
408
|
+
let parenCount = 0;
|
|
409
|
+
for (const char of trimmed) {
|
|
410
|
+
if (char === '(') parenCount++;
|
|
411
|
+
if (char === ')') parenCount--;
|
|
412
|
+
if (parenCount < 0) {
|
|
413
|
+
return { isValid: false, error: 'Unbalanced parentheses in expression' };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (parenCount !== 0) {
|
|
417
|
+
return { isValid: false, error: 'Unbalanced parentheses in expression' };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return { isValid: true };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Validate equation format for solving
|
|
425
|
+
*/
|
|
426
|
+
export function validateEquation(equation: string): ValidationResult {
|
|
427
|
+
if (!equation || typeof equation !== 'string') {
|
|
428
|
+
return { isValid: false, error: 'Equation must be a non-empty string' };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const trimmed = equation.trim();
|
|
432
|
+
if (trimmed.length === 0) {
|
|
433
|
+
return { isValid: false, error: 'Equation cannot be empty' };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Check if equation contains exactly one equals sign
|
|
437
|
+
const equalsSigns = (trimmed.match(/=/g) || []).length;
|
|
438
|
+
if (equalsSigns === 0) {
|
|
439
|
+
return { isValid: false, error: 'Equation must contain an equals sign (=)' };
|
|
440
|
+
}
|
|
441
|
+
if (equalsSigns > 1) {
|
|
442
|
+
return { isValid: false, error: 'Equation cannot contain multiple equals signs' };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Validate both sides of the equation
|
|
446
|
+
const parts = trimmed.split('=');
|
|
447
|
+
const leftSide = parts[0]?.trim() || '';
|
|
448
|
+
const rightSide = parts[1]?.trim() || '';
|
|
449
|
+
|
|
450
|
+
const leftValidation = validateAlgebraicExpression(leftSide);
|
|
451
|
+
if (!leftValidation.isValid) {
|
|
452
|
+
return { isValid: false, error: `Left side of equation: ${leftValidation.error}` };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const rightValidation = validateAlgebraicExpression(rightSide);
|
|
456
|
+
if (!rightValidation.isValid) {
|
|
457
|
+
return { isValid: false, error: `Right side of equation: ${rightValidation.error}` };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return { isValid: true };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Validate variable name for CAS operations
|
|
465
|
+
*/
|
|
466
|
+
export function validateVariableName(variable: string): ValidationResult {
|
|
467
|
+
if (!variable || typeof variable !== 'string') {
|
|
468
|
+
return { isValid: false, error: 'Variable name must be a non-empty string' };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const trimmed = variable.trim();
|
|
472
|
+
if (trimmed.length === 0) {
|
|
473
|
+
return { isValid: false, error: 'Variable name cannot be empty' };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Variable must be a single letter or letter followed by digits/underscores
|
|
477
|
+
const variablePattern = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
478
|
+
if (!variablePattern.test(trimmed)) {
|
|
479
|
+
return {
|
|
480
|
+
isValid: false,
|
|
481
|
+
error: 'Variable name must start with a letter and contain only letters, numbers, and underscores'
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Check for reserved function names
|
|
486
|
+
const reservedNames = ['sin', 'cos', 'tan', 'log', 'ln', 'abs', 'sqrt', 'exp', 'pi', 'e'];
|
|
487
|
+
if (reservedNames.includes(trimmed.toLowerCase())) {
|
|
488
|
+
return { isValid: false, error: `"${trimmed}" is a reserved function name and cannot be used as a variable` };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return { isValid: true };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Validate system of equations
|
|
496
|
+
*/
|
|
497
|
+
export function validateSystemOfEquations(equations: string[]): ValidationResult {
|
|
498
|
+
if (!Array.isArray(equations)) {
|
|
499
|
+
return { isValid: false, error: 'Equations must be provided as an array' };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (equations.length === 0) {
|
|
503
|
+
return { isValid: false, error: 'At least one equation must be provided' };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (equations.length > 10) {
|
|
507
|
+
return { isValid: false, error: 'Cannot solve more than 10 equations simultaneously' };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Validate each equation
|
|
511
|
+
for (let i = 0; i < equations.length; i++) {
|
|
512
|
+
const equation = equations[i];
|
|
513
|
+
if (equation === undefined || equation === null) {
|
|
514
|
+
return { isValid: false, error: `Equation ${i + 1} is undefined or null` };
|
|
515
|
+
}
|
|
516
|
+
const validation = validateEquation(equation);
|
|
517
|
+
if (!validation.isValid) {
|
|
518
|
+
return { isValid: false, error: `Equation ${i + 1}: ${validation.error}` };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return { isValid: true };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Validate variables list for system solving
|
|
527
|
+
*/
|
|
528
|
+
export function validateVariablesList(variables: string[]): ValidationResult {
|
|
529
|
+
if (!Array.isArray(variables)) {
|
|
530
|
+
return { isValid: false, error: 'Variables must be provided as an array' };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (variables.length === 0) {
|
|
534
|
+
return { isValid: false, error: 'At least one variable must be specified' };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (variables.length > 10) {
|
|
538
|
+
return { isValid: false, error: 'Cannot solve for more than 10 variables simultaneously' };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Validate each variable name
|
|
542
|
+
for (let i = 0; i < variables.length; i++) {
|
|
543
|
+
const variable = variables[i];
|
|
544
|
+
if (variable === undefined || variable === null) {
|
|
545
|
+
return { isValid: false, error: `Variable ${i + 1} is undefined or null` };
|
|
546
|
+
}
|
|
547
|
+
const validation = validateVariableName(variable);
|
|
548
|
+
if (!validation.isValid) {
|
|
549
|
+
return { isValid: false, error: `Variable ${i + 1}: ${validation.error}` };
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Check for duplicate variables
|
|
554
|
+
const uniqueVars = new Set(variables);
|
|
555
|
+
if (uniqueVars.size !== variables.length) {
|
|
556
|
+
return { isValid: false, error: 'Variables list cannot contain duplicates' };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return { isValid: true };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Validates slider parameters for creation
|
|
564
|
+
*/
|
|
565
|
+
export function validateSliderParameters(
|
|
566
|
+
name: string,
|
|
567
|
+
min: number,
|
|
568
|
+
max: number,
|
|
569
|
+
increment?: number,
|
|
570
|
+
defaultValue?: number
|
|
571
|
+
): ValidationResult {
|
|
572
|
+
// Validate name
|
|
573
|
+
const nameValidation = validateObjectName(name);
|
|
574
|
+
if (!nameValidation.isValid) {
|
|
575
|
+
return nameValidation;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Validate range
|
|
579
|
+
if (min >= max) {
|
|
580
|
+
return { isValid: false, error: 'Minimum value must be less than maximum value' };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Validate increment
|
|
584
|
+
if (increment !== undefined && increment <= 0) {
|
|
585
|
+
return { isValid: false, error: 'Increment must be positive' };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Validate default value
|
|
589
|
+
if (defaultValue !== undefined && (defaultValue < min || defaultValue > max)) {
|
|
590
|
+
return { isValid: false, error: 'Default value must be within the specified range' };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return { isValid: true };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Validates animation speed parameter
|
|
598
|
+
*/
|
|
599
|
+
export function validateAnimationSpeed(speed: number): ValidationResult {
|
|
600
|
+
if (speed <= 0) {
|
|
601
|
+
return { isValid: false, error: 'Animation speed must be positive' };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (speed > 10) {
|
|
605
|
+
return { isValid: false, error: 'Animation speed should not exceed 10 for reasonable performance' };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return { isValid: true };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Validates animation direction parameter
|
|
613
|
+
*/
|
|
614
|
+
export function validateAnimationDirection(direction?: string): ValidationResult {
|
|
615
|
+
if (direction && !['forward', 'backward', 'oscillating'].includes(direction)) {
|
|
616
|
+
return { isValid: false, error: 'Animation direction must be "forward", "backward", or "oscillating"' };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return { isValid: true };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Validates parameters for animation export
|
|
624
|
+
*/
|
|
625
|
+
export function validateAnimationExportParameters(
|
|
626
|
+
frameCount: number,
|
|
627
|
+
frameDelay: number,
|
|
628
|
+
totalDuration?: number
|
|
629
|
+
): ValidationResult {
|
|
630
|
+
if (frameCount <= 0 || !Number.isInteger(frameCount)) {
|
|
631
|
+
return { isValid: false, error: 'Frame count must be a positive integer' };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (frameCount > 300) {
|
|
635
|
+
return { isValid: false, error: 'Frame count should not exceed 300 for reasonable file size' };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (frameDelay <= 0) {
|
|
639
|
+
return { isValid: false, error: 'Frame delay must be positive' };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (totalDuration !== undefined && totalDuration <= 0) {
|
|
643
|
+
return { isValid: false, error: 'Total duration must be positive' };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return { isValid: true };
|
|
647
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"removeComments": false,
|
|
17
|
+
"noImplicitAny": true,
|
|
18
|
+
"noImplicitReturns": true,
|
|
19
|
+
"noImplicitThis": true,
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true,
|
|
22
|
+
"exactOptionalPropertyTypes": true,
|
|
23
|
+
"noImplicitOverride": true,
|
|
24
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
25
|
+
"noUncheckedIndexedAccess": true
|
|
26
|
+
},
|
|
27
|
+
"include": [
|
|
28
|
+
"src/**/*"
|
|
29
|
+
],
|
|
30
|
+
"exclude": [
|
|
31
|
+
"node_modules",
|
|
32
|
+
"dist",
|
|
33
|
+
"tests"
|
|
34
|
+
]
|
|
35
|
+
}
|
package/widget/main.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare const __STUDIO_VERSION__:string;
|
|
2
|
+
import { App } from '@modelcontextprotocol/ext-apps';
|
|
3
|
+
const app = new App({name:'GeoGebra construction preview',version:__STUDIO_VERSION__},{});
|
|
4
|
+
const frame = document.querySelector<HTMLIFrameElement>('#preview')!;
|
|
5
|
+
const status = document.querySelector<HTMLElement>('#status')!;
|
|
6
|
+
app.addEventListener('toolresult', result => {
|
|
7
|
+
const data=result.structuredContent as {html?:string;error?:string}|undefined;
|
|
8
|
+
if(result.isError || !data?.html){status.textContent=data?.error||'No construction HTML returned.';return;}
|
|
9
|
+
frame.srcdoc=data.html;
|
|
10
|
+
status.textContent='Interactive GeoGebra preview. The applet requires internet access.';
|
|
11
|
+
});
|
|
12
|
+
app.connect().catch(e=>{status.textContent='Host connection failed: '+String(e);});
|