@morphql/language-definitions 0.1.3
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 +146 -0
- package/export-json.ts +107 -0
- package/package.json +38 -0
- package/src/functions.ts +170 -0
- package/src/index.ts +202 -0
- package/src/keywords.ts +196 -0
- package/src/operators.ts +50 -0
- package/src/types.ts +51 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# @morphql/language-definitions
|
|
2
|
+
|
|
3
|
+
**Single source of truth** for MorphQL language definitions across all platforms.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This package centralizes all MorphQL language definitions (keywords, functions, operators, documentation) in TypeScript, eliminating duplication across:
|
|
8
|
+
|
|
9
|
+
- VSCode extension
|
|
10
|
+
- Monaco Editor (playground)
|
|
11
|
+
- Documentation
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @morphql/language-definitions
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Get Language Data
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import {
|
|
25
|
+
KEYWORDS,
|
|
26
|
+
FUNCTIONS,
|
|
27
|
+
OPERATORS,
|
|
28
|
+
getKeywordNames,
|
|
29
|
+
getFunctionNames,
|
|
30
|
+
getOperatorSymbols,
|
|
31
|
+
} from "@morphql/language-definitions";
|
|
32
|
+
|
|
33
|
+
// Get all keyword names
|
|
34
|
+
const keywords = getKeywordNames();
|
|
35
|
+
// ['from', 'to', 'transform', 'set', ...]
|
|
36
|
+
|
|
37
|
+
// Get all function names
|
|
38
|
+
const functions = getFunctionNames();
|
|
39
|
+
// ['substring', 'split', 'replace', ...]
|
|
40
|
+
|
|
41
|
+
// Get documentation for a keyword
|
|
42
|
+
import { getKeywordDoc } from "@morphql/language-definitions";
|
|
43
|
+
const doc = getKeywordDoc("set");
|
|
44
|
+
// { signature: 'set <target> = <expression>', description: '...', ... }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Generate VSCode TextMate Grammar
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { generateTextMateKeywordsPattern } from "@morphql/language-definitions";
|
|
51
|
+
|
|
52
|
+
const keywordsPattern = generateTextMateKeywordsPattern();
|
|
53
|
+
// Use in morphql.tmLanguage.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Generate Monaco Language Config
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { generateMonacoLanguageConfig } from "@morphql/language-definitions";
|
|
60
|
+
|
|
61
|
+
const monacoConfig = generateMonacoLanguageConfig();
|
|
62
|
+
monaco.languages.register({ id: "morphql" });
|
|
63
|
+
monaco.languages.setMonarchTokensProvider("morphql", monacoConfig);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Generate Hover Documentation
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { generateHoverDocs } from "@morphql/language-definitions";
|
|
70
|
+
|
|
71
|
+
const { keywordDocs, functionDocs } = generateHoverDocs();
|
|
72
|
+
// Use in VSCode HoverProvider or Monaco HoverProvider
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Adding New Language Features
|
|
76
|
+
|
|
77
|
+
### 1. Add to This Package
|
|
78
|
+
|
|
79
|
+
Edit the appropriate file:
|
|
80
|
+
|
|
81
|
+
- **Keywords**: `src/keywords.ts`
|
|
82
|
+
- **Functions**: `src/functions.ts`
|
|
83
|
+
- **Operators**: `src/operators.ts`
|
|
84
|
+
|
|
85
|
+
### 2. Update the Lexer
|
|
86
|
+
|
|
87
|
+
Update `@morphql/core/src/core/lexer.ts` with the new token.
|
|
88
|
+
|
|
89
|
+
### 3. Rebuild
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm run build
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 4. Update Consumers
|
|
96
|
+
|
|
97
|
+
The VSCode extension and playground will automatically use the new definitions on their next build.
|
|
98
|
+
|
|
99
|
+
## Structure
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
src/
|
|
103
|
+
├── types.ts # TypeScript interfaces
|
|
104
|
+
├── keywords.ts # Keyword definitions + docs
|
|
105
|
+
├── functions.ts # Function definitions + docs
|
|
106
|
+
├── operators.ts # Operator definitions
|
|
107
|
+
└── index.ts # Exports + generators
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Benefits
|
|
111
|
+
|
|
112
|
+
✅ **Single source of truth** - Edit once, use everywhere
|
|
113
|
+
✅ **Type-safe** - Full TypeScript support
|
|
114
|
+
✅ **Auto-generated** - Configs generated from definitions
|
|
115
|
+
✅ **Consistent** - No more sync issues between platforms
|
|
116
|
+
✅ **Documented** - All definitions include documentation
|
|
117
|
+
|
|
118
|
+
## Example: Adding a New Keyword
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// 1. Edit src/keywords.ts
|
|
122
|
+
export const KEYWORDS: KeywordDef[] = [
|
|
123
|
+
// ... existing keywords ...
|
|
124
|
+
{
|
|
125
|
+
name: "loop",
|
|
126
|
+
category: "control",
|
|
127
|
+
doc: {
|
|
128
|
+
signature: "loop <count> ( <actions> )",
|
|
129
|
+
description: "Repeats actions a specified number of times.",
|
|
130
|
+
parameters: [
|
|
131
|
+
{ name: "count", description: "Number of iterations" },
|
|
132
|
+
{ name: "actions", description: "Actions to repeat" },
|
|
133
|
+
],
|
|
134
|
+
example: "loop 5 (\n set item = value\n)",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
// 2. Update lexer in @morphql/core
|
|
140
|
+
// 3. Rebuild this package: npm run build
|
|
141
|
+
// 4. VSCode and Monaco will pick it up automatically!
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
package/export-json.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { MORPHQL_LANGUAGE } from "./src/index";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
const distDir = path.resolve(__dirname, "dist");
|
|
10
|
+
if (!fs.existsSync(distDir)) {
|
|
11
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 1. Export JSON
|
|
15
|
+
const jsonPath = path.resolve(distDir, "morphql-lang.json");
|
|
16
|
+
fs.writeFileSync(jsonPath, JSON.stringify(MORPHQL_LANGUAGE, null, 2));
|
|
17
|
+
console.log(`Language definitions exported to ${jsonPath}`);
|
|
18
|
+
|
|
19
|
+
// 2. Export Kotlin Constants
|
|
20
|
+
const constantsPath = path.resolve(
|
|
21
|
+
__dirname,
|
|
22
|
+
"../jetbrains-extension/src/main/kotlin/org/morphql/jetbrains/MorphQLConstants.kt",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const keywords = MORPHQL_LANGUAGE.keywords.map((k) => k.name);
|
|
26
|
+
const functions = MORPHQL_LANGUAGE.functions.map((f) => f.name);
|
|
27
|
+
const operators = MORPHQL_LANGUAGE.operators.map((o) => o.symbol);
|
|
28
|
+
|
|
29
|
+
const constantsContent = `package org.morphql.jetbrains
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* GENERATED FILE - DO NOT EDIT MANUALLY
|
|
33
|
+
* Generated by packages/language-definitions/export-json.ts
|
|
34
|
+
*/
|
|
35
|
+
object MorphQLConstants {
|
|
36
|
+
val KEYWORDS = setOf(
|
|
37
|
+
${keywords.map((k) => ` "${k}"`).join(",\n")}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
val FUNCTIONS = setOf(
|
|
41
|
+
${functions.map((f) => ` "${f}"`).join(",\n")}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
val OPERATORS = setOf(
|
|
45
|
+
${operators.map((o) => ` "${o}"`).join(",\n")}
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
if (fs.existsSync(path.dirname(constantsPath))) {
|
|
51
|
+
fs.writeFileSync(constantsPath, constantsContent);
|
|
52
|
+
console.log(`Kotlin constants exported to ${constantsPath}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 3. Export Kotlin Documentation
|
|
56
|
+
const docsPath = path.resolve(
|
|
57
|
+
__dirname,
|
|
58
|
+
"../jetbrains-extension/src/main/kotlin/org/morphql/jetbrains/MorphQLDocumentation.kt",
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const allDocs: Record<string, any> = {};
|
|
62
|
+
MORPHQL_LANGUAGE.keywords.forEach((k) => {
|
|
63
|
+
allDocs[k.name] = k.doc;
|
|
64
|
+
});
|
|
65
|
+
MORPHQL_LANGUAGE.functions.forEach((f) => {
|
|
66
|
+
allDocs[f.name] = f.doc;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const escapeQuotes = (str: string) =>
|
|
70
|
+
str.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
71
|
+
|
|
72
|
+
const docsEntries = Object.entries(allDocs)
|
|
73
|
+
.map(([name, doc]) => {
|
|
74
|
+
return ` "${name}" to """
|
|
75
|
+
<b>${doc.signature}</b><br/>
|
|
76
|
+
${doc.description}<br/><br/>
|
|
77
|
+
${
|
|
78
|
+
doc.parameters && doc.parameters.length > 0
|
|
79
|
+
? `<b>Parameters:</b><ul>${doc.parameters
|
|
80
|
+
.map(
|
|
81
|
+
(p: any) => `<li><b>${p.name}:</b> ${p.description}</li>`,
|
|
82
|
+
)
|
|
83
|
+
.join("")}</ul>`
|
|
84
|
+
: ""
|
|
85
|
+
}
|
|
86
|
+
${doc.example ? `<b>Example:</b><pre>${doc.example}</pre>` : ""}
|
|
87
|
+
""".trimIndent()`;
|
|
88
|
+
})
|
|
89
|
+
.join(",\n");
|
|
90
|
+
|
|
91
|
+
const docsContent = `package org.morphql.jetbrains
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* GENERATED FILE - DO NOT EDIT MANUALLY
|
|
95
|
+
* Generated by packages/language-definitions/export-json.ts
|
|
96
|
+
*/
|
|
97
|
+
object MorphQLDocumentation {
|
|
98
|
+
val DOCS = mapOf(
|
|
99
|
+
${docsEntries}
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
if (fs.existsSync(path.dirname(docsPath))) {
|
|
105
|
+
fs.writeFileSync(docsPath, docsContent);
|
|
106
|
+
console.log(`Kotlin documentation exported to ${docsPath}`);
|
|
107
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@morphql/language-definitions",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Shared language definitions for MorphQL across VSCode, Monaco, and documentation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.cjs",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs",
|
|
16
|
+
"types": "./dist/index.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean && npm run export-json",
|
|
21
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
22
|
+
"export-json": "tsx export-json.ts"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"morphql",
|
|
26
|
+
"language",
|
|
27
|
+
"definitions",
|
|
28
|
+
"vscode",
|
|
29
|
+
"monaco"
|
|
30
|
+
],
|
|
31
|
+
"author": "",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"tsx": "^4.0.0",
|
|
36
|
+
"typescript": "^5.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/functions.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { FunctionDef } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MorphQL Functions - Single source of truth
|
|
5
|
+
*
|
|
6
|
+
* When adding a new function:
|
|
7
|
+
* 1. Add it here
|
|
8
|
+
* 2. Implement in @morphql/core/src/functions.ts
|
|
9
|
+
* 3. Run build to regenerate VSCode/Monaco configs
|
|
10
|
+
*/
|
|
11
|
+
export const FUNCTIONS: FunctionDef[] = [
|
|
12
|
+
{
|
|
13
|
+
name: "substring",
|
|
14
|
+
doc: {
|
|
15
|
+
signature: "substring(str, start, [length])",
|
|
16
|
+
description: "Extracts a portion of a string. Supports negative indices.",
|
|
17
|
+
parameters: [
|
|
18
|
+
{ name: "str", description: "The source string" },
|
|
19
|
+
{
|
|
20
|
+
name: "start",
|
|
21
|
+
description: "Starting index (0-based, negative counts from end)",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "length",
|
|
25
|
+
description: "(Optional) Number of characters to extract",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
returns: "string",
|
|
29
|
+
example:
|
|
30
|
+
'substring("Hello World", 0, 5) // "Hello"\nsubstring("Hello World", -5) // "World"',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "split",
|
|
35
|
+
doc: {
|
|
36
|
+
signature: "split(str, [separator], [limit])",
|
|
37
|
+
description: "Splits a string into an array.",
|
|
38
|
+
parameters: [
|
|
39
|
+
{ name: "str", description: "The string to split" },
|
|
40
|
+
{
|
|
41
|
+
name: "separator",
|
|
42
|
+
description: '(Optional) Delimiter string. Default: ""',
|
|
43
|
+
},
|
|
44
|
+
{ name: "limit", description: "(Optional) Maximum number of splits" },
|
|
45
|
+
],
|
|
46
|
+
returns: "array",
|
|
47
|
+
example: 'split("a,b,c", ",") // ["a", "b", "c"]',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "replace",
|
|
52
|
+
doc: {
|
|
53
|
+
signature: "replace(str, search, replacement)",
|
|
54
|
+
description: "Replaces occurrences in a string.",
|
|
55
|
+
parameters: [
|
|
56
|
+
{ name: "str", description: "The source string" },
|
|
57
|
+
{ name: "search", description: "The substring to find" },
|
|
58
|
+
{ name: "replacement", description: "The replacement string" },
|
|
59
|
+
],
|
|
60
|
+
returns: "string",
|
|
61
|
+
example: 'replace("Hello World", "World", "MorphQL") // "Hello MorphQL"',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "text",
|
|
66
|
+
doc: {
|
|
67
|
+
signature: "text(value)",
|
|
68
|
+
description: "Converts a value to a string.",
|
|
69
|
+
parameters: [{ name: "value", description: "The value to convert" }],
|
|
70
|
+
returns: "string",
|
|
71
|
+
example: 'text(123) // "123"',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "number",
|
|
76
|
+
doc: {
|
|
77
|
+
signature: "number(value)",
|
|
78
|
+
description: "Converts a value to a number.",
|
|
79
|
+
parameters: [{ name: "value", description: "The value to convert" }],
|
|
80
|
+
returns: "number",
|
|
81
|
+
example: 'number("42") // 42',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "uppercase",
|
|
86
|
+
doc: {
|
|
87
|
+
signature: "uppercase(str)",
|
|
88
|
+
description: "Converts a string to uppercase.",
|
|
89
|
+
parameters: [{ name: "str", description: "The string to convert" }],
|
|
90
|
+
returns: "string",
|
|
91
|
+
example: 'uppercase("hello") // "HELLO"',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "lowercase",
|
|
96
|
+
doc: {
|
|
97
|
+
signature: "lowercase(str)",
|
|
98
|
+
description: "Converts a string to lowercase.",
|
|
99
|
+
parameters: [{ name: "str", description: "The string to convert" }],
|
|
100
|
+
returns: "string",
|
|
101
|
+
example: 'lowercase("HELLO") // "hello"',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "extractnumber",
|
|
106
|
+
doc: {
|
|
107
|
+
signature: "extractnumber(str)",
|
|
108
|
+
description: "Extracts the first numeric sequence from a string.",
|
|
109
|
+
parameters: [{ name: "str", description: "The string to extract from" }],
|
|
110
|
+
returns: "number",
|
|
111
|
+
example: 'extractnumber("Price: 100USD") // 100',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "xmlnode",
|
|
116
|
+
doc: {
|
|
117
|
+
signature: "xmlnode(value, [attrKey, attrVal, ...])",
|
|
118
|
+
description: "Wraps a value for XML output with optional attributes.",
|
|
119
|
+
parameters: [
|
|
120
|
+
{ name: "value", description: "The node content" },
|
|
121
|
+
{
|
|
122
|
+
name: "attrKey, attrVal",
|
|
123
|
+
description: "(Optional) Pairs of attribute keys and values",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
returns: "XML node",
|
|
127
|
+
example: 'xmlnode(content, "id", 1, "type", "text")',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "to_base64",
|
|
132
|
+
doc: {
|
|
133
|
+
signature: "to_base64(value)",
|
|
134
|
+
description: "Encodes a string value to Base64.",
|
|
135
|
+
parameters: [{ name: "value", description: "The string to encode" }],
|
|
136
|
+
returns: "string",
|
|
137
|
+
example: 'to_base64("hello") // "aGVsbG8="',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "from_base64",
|
|
142
|
+
doc: {
|
|
143
|
+
signature: "from_base64(value)",
|
|
144
|
+
description: "Decodes a Base64 string value.",
|
|
145
|
+
parameters: [
|
|
146
|
+
{ name: "value", description: "The Base64 string to decode" },
|
|
147
|
+
],
|
|
148
|
+
returns: "string",
|
|
149
|
+
example: 'from_base64("aGVsbG8=") // "hello"',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "aslist",
|
|
154
|
+
doc: {
|
|
155
|
+
signature: "aslist(value)",
|
|
156
|
+
description:
|
|
157
|
+
"Ensures a value is an array. Useful for XML nodes that might be a single object or an array.",
|
|
158
|
+
parameters: [{ name: "value", description: "The value to normalize" }],
|
|
159
|
+
returns: "array",
|
|
160
|
+
example: "aslist(items) // Always returns an array",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
// Helper to get all function names
|
|
166
|
+
export const getFunctionNames = () => FUNCTIONS.map((f) => f.name);
|
|
167
|
+
|
|
168
|
+
// Helper to get function documentation
|
|
169
|
+
export const getFunctionDoc = (name: string) =>
|
|
170
|
+
FUNCTIONS.find((f) => f.name.toLowerCase() === name.toLowerCase())?.doc;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import {
|
|
2
|
+
KEYWORDS,
|
|
3
|
+
getKeywordNames,
|
|
4
|
+
getKeywordsByCategory,
|
|
5
|
+
getKeywordDoc,
|
|
6
|
+
} from "./keywords";
|
|
7
|
+
import { FUNCTIONS, getFunctionNames, getFunctionDoc } from "./functions";
|
|
8
|
+
import {
|
|
9
|
+
OPERATORS,
|
|
10
|
+
getOperatorsByCategory,
|
|
11
|
+
getOperatorSymbols,
|
|
12
|
+
getMultiCharOperators,
|
|
13
|
+
getSingleCharOperators,
|
|
14
|
+
} from "./operators";
|
|
15
|
+
import type {
|
|
16
|
+
LanguageDefinition,
|
|
17
|
+
KeywordDef,
|
|
18
|
+
FunctionDef,
|
|
19
|
+
OperatorDef,
|
|
20
|
+
DocEntry,
|
|
21
|
+
} from "./types";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Complete MorphQL language definition
|
|
25
|
+
*/
|
|
26
|
+
export const MORPHQL_LANGUAGE: LanguageDefinition = {
|
|
27
|
+
keywords: KEYWORDS,
|
|
28
|
+
functions: FUNCTIONS,
|
|
29
|
+
operators: OPERATORS,
|
|
30
|
+
comments: {
|
|
31
|
+
line: "//",
|
|
32
|
+
blockStart: "/*",
|
|
33
|
+
blockEnd: "*/",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Re-export everything
|
|
38
|
+
export {
|
|
39
|
+
// Data
|
|
40
|
+
KEYWORDS,
|
|
41
|
+
FUNCTIONS,
|
|
42
|
+
OPERATORS,
|
|
43
|
+
|
|
44
|
+
// Helpers
|
|
45
|
+
getKeywordNames,
|
|
46
|
+
getKeywordsByCategory,
|
|
47
|
+
getKeywordDoc,
|
|
48
|
+
getFunctionNames,
|
|
49
|
+
getFunctionDoc,
|
|
50
|
+
getOperatorsByCategory,
|
|
51
|
+
getOperatorSymbols,
|
|
52
|
+
getMultiCharOperators,
|
|
53
|
+
getSingleCharOperators,
|
|
54
|
+
|
|
55
|
+
// Types
|
|
56
|
+
type LanguageDefinition,
|
|
57
|
+
type KeywordDef,
|
|
58
|
+
type FunctionDef,
|
|
59
|
+
type OperatorDef,
|
|
60
|
+
type DocEntry,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generators for different platforms
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generate TextMate grammar keywords pattern
|
|
69
|
+
*/
|
|
70
|
+
export function generateTextMateKeywordsPattern(): string {
|
|
71
|
+
const controlKeywords = getKeywordsByCategory("control").map((k) => k.name);
|
|
72
|
+
const actionKeywords = getKeywordsByCategory("action").map((k) => k.name);
|
|
73
|
+
|
|
74
|
+
return JSON.stringify(
|
|
75
|
+
{
|
|
76
|
+
patterns: [
|
|
77
|
+
{
|
|
78
|
+
name: "keyword.control.morphql",
|
|
79
|
+
match: `\\b(${controlKeywords.join("|")})\\b`,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "keyword.other.morphql",
|
|
83
|
+
match: `\\b(${actionKeywords.join("|")})\\b`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
null,
|
|
88
|
+
2,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generate TextMate grammar functions pattern
|
|
94
|
+
*/
|
|
95
|
+
export function generateTextMateFunctionsPattern(): string {
|
|
96
|
+
const functionNames = getFunctionNames();
|
|
97
|
+
|
|
98
|
+
return JSON.stringify(
|
|
99
|
+
{
|
|
100
|
+
patterns: [
|
|
101
|
+
{
|
|
102
|
+
name: "entity.name.function.morphql",
|
|
103
|
+
match: `\\b(${functionNames.join("|")})(?=\\s*\\()`,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
null,
|
|
108
|
+
2,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Generate Monaco language configuration
|
|
114
|
+
*/
|
|
115
|
+
export function generateMonacoLanguageConfig() {
|
|
116
|
+
return {
|
|
117
|
+
keywords: getKeywordNames(),
|
|
118
|
+
builtinFunctions: getFunctionNames(),
|
|
119
|
+
operators: getOperatorSymbols(),
|
|
120
|
+
symbols: /[=><!~?:&|+\-*\/\^%]+/,
|
|
121
|
+
escapes:
|
|
122
|
+
/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
|
|
123
|
+
|
|
124
|
+
tokenizer: {
|
|
125
|
+
root: [
|
|
126
|
+
// Comments
|
|
127
|
+
[/\/\/.*$/, "comment"],
|
|
128
|
+
[/\/\*/, "comment", "@comment"],
|
|
129
|
+
|
|
130
|
+
// Keywords
|
|
131
|
+
[
|
|
132
|
+
/[a-zA-Z_$][\w$]*/,
|
|
133
|
+
{
|
|
134
|
+
cases: {
|
|
135
|
+
"@keywords": "keyword",
|
|
136
|
+
"@builtinFunctions": "predefined",
|
|
137
|
+
"@default": "identifier",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
|
|
142
|
+
// Strings
|
|
143
|
+
[/"([^"\\]|\\.)*$/, "string.invalid"],
|
|
144
|
+
[/'([^'\\]|\\.)*$/, "string.invalid"],
|
|
145
|
+
[/"/, "string", "@string_double"],
|
|
146
|
+
[/'/, "string", "@string_single"],
|
|
147
|
+
|
|
148
|
+
// Numbers
|
|
149
|
+
[/-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/, "number"],
|
|
150
|
+
|
|
151
|
+
// Operators
|
|
152
|
+
[/[{}()\[\]]/, "@brackets"],
|
|
153
|
+
[
|
|
154
|
+
/@symbols/,
|
|
155
|
+
{
|
|
156
|
+
cases: {
|
|
157
|
+
"@operators": "operator",
|
|
158
|
+
"@default": "",
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
],
|
|
163
|
+
|
|
164
|
+
comment: [
|
|
165
|
+
[/[^\/*]+/, "comment"],
|
|
166
|
+
[/\*\//, "comment", "@pop"],
|
|
167
|
+
[/[\/*]/, "comment"],
|
|
168
|
+
],
|
|
169
|
+
|
|
170
|
+
string_double: [
|
|
171
|
+
[/[^\\"]+/, "string"],
|
|
172
|
+
[/@escapes/, "string.escape"],
|
|
173
|
+
[/\\./, "string.escape.invalid"],
|
|
174
|
+
[/"/, "string", "@pop"],
|
|
175
|
+
],
|
|
176
|
+
|
|
177
|
+
string_single: [
|
|
178
|
+
[/[^\\']+/, "string"],
|
|
179
|
+
[/@escapes/, "string.escape"],
|
|
180
|
+
[/\\./, "string.escape.invalid"],
|
|
181
|
+
[/'/, "string", "@pop"],
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Generate hover documentation map for VSCode
|
|
189
|
+
*/
|
|
190
|
+
export function generateHoverDocs() {
|
|
191
|
+
const keywordDocs: Record<string, DocEntry> = {};
|
|
192
|
+
KEYWORDS.forEach((k) => {
|
|
193
|
+
keywordDocs[k.name] = k.doc;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const functionDocs: Record<string, DocEntry> = {};
|
|
197
|
+
FUNCTIONS.forEach((f) => {
|
|
198
|
+
functionDocs[f.name] = f.doc;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return { keywordDocs, functionDocs };
|
|
202
|
+
}
|
package/src/keywords.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { KeywordDef } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MorphQL Keywords - Single source of truth
|
|
5
|
+
*
|
|
6
|
+
* When adding a new keyword:
|
|
7
|
+
* 1. Add it here
|
|
8
|
+
* 2. Update the lexer in @morphql/core
|
|
9
|
+
* 3. Run build to regenerate VSCode/Monaco configs
|
|
10
|
+
*/
|
|
11
|
+
export const KEYWORDS: KeywordDef[] = [
|
|
12
|
+
{
|
|
13
|
+
name: "from",
|
|
14
|
+
category: "control",
|
|
15
|
+
doc: {
|
|
16
|
+
signature: "from <format>",
|
|
17
|
+
description: "Specifies the input data format.",
|
|
18
|
+
parameters: [
|
|
19
|
+
{
|
|
20
|
+
name: "format",
|
|
21
|
+
description:
|
|
22
|
+
"If used as first keyword: The starting format, one of `json`, `xml`, or `object`. When used after a section, defines its source",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
example: "from json to xml",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "to",
|
|
30
|
+
category: "control",
|
|
31
|
+
doc: {
|
|
32
|
+
signature: "to <format>",
|
|
33
|
+
description: "Specifies the output data format.",
|
|
34
|
+
parameters: [
|
|
35
|
+
{ name: "format", description: "One of: `json`, `xml`, or `object`" },
|
|
36
|
+
],
|
|
37
|
+
example: "from json to xml",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "transform",
|
|
42
|
+
category: "control",
|
|
43
|
+
doc: {
|
|
44
|
+
signature: "transform",
|
|
45
|
+
description: "Begins the transformation block containing actions.",
|
|
46
|
+
example: "transform\n set name = firstName",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "set",
|
|
51
|
+
category: "action",
|
|
52
|
+
doc: {
|
|
53
|
+
signature: "set <target> = <expression>",
|
|
54
|
+
description: "Assigns a value to a field in the output.",
|
|
55
|
+
parameters: [
|
|
56
|
+
{ name: "target", description: "The field name to set" },
|
|
57
|
+
{
|
|
58
|
+
name: "expression",
|
|
59
|
+
description: "The value or expression to assign",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
example: 'set fullName = firstName + " " + lastName',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "section",
|
|
67
|
+
category: "action",
|
|
68
|
+
doc: {
|
|
69
|
+
signature:
|
|
70
|
+
"section [multiple] <name>( [subquery] <actions> ) [from <path>]",
|
|
71
|
+
description:
|
|
72
|
+
"Creates a nested object or array in the output. Can optionally include a subquery for format conversion.",
|
|
73
|
+
parameters: [
|
|
74
|
+
{ name: "multiple", description: "(Optional) Treat as array mapping" },
|
|
75
|
+
{ name: "name", description: "The section/field name" },
|
|
76
|
+
{
|
|
77
|
+
name: "subquery",
|
|
78
|
+
description:
|
|
79
|
+
"(Optional) Nested query: from <format> to <format> [transform]",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "actions",
|
|
83
|
+
description: "Actions to perform within the section",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "from",
|
|
87
|
+
description: "(Optional) Source path for the section data",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
example:
|
|
91
|
+
"section metadata(\n from xml to object\n transform\n set name = root.productName\n) from xmlString",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "multiple",
|
|
96
|
+
category: "action",
|
|
97
|
+
doc: {
|
|
98
|
+
signature: "section multiple <name>(...)",
|
|
99
|
+
description: "Modifier for `section` to map over an array.",
|
|
100
|
+
example: "section multiple items(\n set id = itemId\n) from products",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "clone",
|
|
105
|
+
category: "action",
|
|
106
|
+
doc: {
|
|
107
|
+
signature: "clone([field1, field2, ...])",
|
|
108
|
+
description: "Copies fields from the source to the output.",
|
|
109
|
+
parameters: [
|
|
110
|
+
{
|
|
111
|
+
name: "fields",
|
|
112
|
+
description:
|
|
113
|
+
"(Optional) Specific fields to clone. If omitted, clones all fields.",
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
example: "clone(id, name, email)",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "delete",
|
|
121
|
+
category: "action",
|
|
122
|
+
doc: {
|
|
123
|
+
signature: "delete <field>",
|
|
124
|
+
description: "Removes a field from the output (useful after `clone`).",
|
|
125
|
+
parameters: [{ name: "field", description: "The field name to delete" }],
|
|
126
|
+
example: "clone()\ndelete password",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "define",
|
|
131
|
+
category: "action",
|
|
132
|
+
doc: {
|
|
133
|
+
signature: "define <alias> = <expression>",
|
|
134
|
+
description:
|
|
135
|
+
"Creates a local variable/alias for use in subsequent expressions.",
|
|
136
|
+
parameters: [
|
|
137
|
+
{ name: "alias", description: "The variable name" },
|
|
138
|
+
{ name: "expression", description: "The value to assign" },
|
|
139
|
+
],
|
|
140
|
+
example:
|
|
141
|
+
"define taxRate = 0.22\nset totalWithTax = total * (1 + taxRate)",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "if",
|
|
146
|
+
category: "control",
|
|
147
|
+
doc: {
|
|
148
|
+
signature: "if (condition) ( actions ) [else ( actions )]",
|
|
149
|
+
description: "Conditional execution of action blocks.",
|
|
150
|
+
parameters: [
|
|
151
|
+
{ name: "condition", description: "Boolean expression" },
|
|
152
|
+
{ name: "actions", description: "Actions to execute if true/false" },
|
|
153
|
+
],
|
|
154
|
+
example:
|
|
155
|
+
'if (age >= 18) (\n set status = "adult"\n) else (\n set status = "minor"\n)',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "else",
|
|
160
|
+
category: "control",
|
|
161
|
+
doc: {
|
|
162
|
+
signature: "else ( actions )",
|
|
163
|
+
description: "Defines the else branch of an `if` statement.",
|
|
164
|
+
example: "if (condition) (\n ...\n) else (\n ...\n)",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "modify",
|
|
169
|
+
category: "action",
|
|
170
|
+
doc: {
|
|
171
|
+
signature: "modify <target> = <expression>",
|
|
172
|
+
description:
|
|
173
|
+
"Modifies a field in the output by reading from the target (not source). Useful for post-processing already-mapped values.",
|
|
174
|
+
parameters: [
|
|
175
|
+
{ name: "target", description: "The field name to modify" },
|
|
176
|
+
{
|
|
177
|
+
name: "expression",
|
|
178
|
+
description:
|
|
179
|
+
"The expression to assign (reads from target, not source)",
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
example: "set total = price * quantity\nmodify total = total * 1.10",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
// Helper to get keywords by category
|
|
188
|
+
export const getKeywordsByCategory = (category: "control" | "action") =>
|
|
189
|
+
KEYWORDS.filter((k) => k.category === category);
|
|
190
|
+
|
|
191
|
+
// Helper to get all keyword names
|
|
192
|
+
export const getKeywordNames = () => KEYWORDS.map((k) => k.name);
|
|
193
|
+
|
|
194
|
+
// Helper to get keyword documentation
|
|
195
|
+
export const getKeywordDoc = (name: string) =>
|
|
196
|
+
KEYWORDS.find((k) => k.name.toLowerCase() === name.toLowerCase())?.doc;
|
package/src/operators.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { OperatorDef } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MorphQL Operators - Single source of truth
|
|
5
|
+
*
|
|
6
|
+
* When adding a new operator:
|
|
7
|
+
* 1. Add it here
|
|
8
|
+
* 2. Update the lexer in @morphql/core (MIND THE ORDER!)
|
|
9
|
+
* 3. Run build to regenerate VSCode/Monaco configs
|
|
10
|
+
*/
|
|
11
|
+
export const OPERATORS: OperatorDef[] = [
|
|
12
|
+
// Comparison operators
|
|
13
|
+
{ symbol: "===", category: "comparison", precedence: 7 },
|
|
14
|
+
{ symbol: "!==", category: "comparison", precedence: 7 },
|
|
15
|
+
{ symbol: "==", category: "comparison", precedence: 7 },
|
|
16
|
+
{ symbol: "!=", category: "comparison", precedence: 7 },
|
|
17
|
+
{ symbol: "<=", category: "comparison", precedence: 6 },
|
|
18
|
+
{ symbol: ">=", category: "comparison", precedence: 6 },
|
|
19
|
+
{ symbol: "<", category: "comparison", precedence: 6 },
|
|
20
|
+
{ symbol: ">", category: "comparison", precedence: 6 },
|
|
21
|
+
|
|
22
|
+
// Logical operators
|
|
23
|
+
{ symbol: "&&", category: "logical", precedence: 5 },
|
|
24
|
+
{ symbol: "||", category: "logical", precedence: 4 },
|
|
25
|
+
{ symbol: "!", category: "logical", precedence: 9 },
|
|
26
|
+
|
|
27
|
+
// Arithmetic operators
|
|
28
|
+
{ symbol: "+", category: "arithmetic", precedence: 10 },
|
|
29
|
+
{ symbol: "-", category: "arithmetic", precedence: 10 },
|
|
30
|
+
{ symbol: "*", category: "arithmetic", precedence: 11 },
|
|
31
|
+
{ symbol: "/", category: "arithmetic", precedence: 11 },
|
|
32
|
+
|
|
33
|
+
// Assignment
|
|
34
|
+
{ symbol: "=", category: "assignment", precedence: 1 },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// Helper to get operators by category
|
|
38
|
+
export const getOperatorsByCategory = (category: OperatorDef["category"]) =>
|
|
39
|
+
OPERATORS.filter((op) => op.category === category);
|
|
40
|
+
|
|
41
|
+
// Helper to get all operator symbols
|
|
42
|
+
export const getOperatorSymbols = () => OPERATORS.map((op) => op.symbol);
|
|
43
|
+
|
|
44
|
+
// Helper to get multi-character operators (for lexer ordering)
|
|
45
|
+
export const getMultiCharOperators = () =>
|
|
46
|
+
OPERATORS.filter((op) => op.symbol.length > 1).map((op) => op.symbol);
|
|
47
|
+
|
|
48
|
+
// Helper to get single-character operators
|
|
49
|
+
export const getSingleCharOperators = () =>
|
|
50
|
+
OPERATORS.filter((op) => op.symbol.length === 1).map((op) => op.symbol);
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documentation entry for a keyword or function
|
|
3
|
+
*/
|
|
4
|
+
export interface DocEntry {
|
|
5
|
+
signature: string;
|
|
6
|
+
description: string;
|
|
7
|
+
parameters?: { name: string; description: string }[];
|
|
8
|
+
returns?: string;
|
|
9
|
+
example?: string;
|
|
10
|
+
category?: "control" | "action" | "function" | "operator";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Keyword definition
|
|
15
|
+
*/
|
|
16
|
+
export interface KeywordDef {
|
|
17
|
+
name: string;
|
|
18
|
+
category: "control" | "action";
|
|
19
|
+
doc: DocEntry;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Function definition
|
|
24
|
+
*/
|
|
25
|
+
export interface FunctionDef {
|
|
26
|
+
name: string;
|
|
27
|
+
doc: DocEntry;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Operator definition
|
|
32
|
+
*/
|
|
33
|
+
export interface OperatorDef {
|
|
34
|
+
symbol: string;
|
|
35
|
+
category: "arithmetic" | "comparison" | "logical" | "assignment";
|
|
36
|
+
precedence?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Complete language definition
|
|
41
|
+
*/
|
|
42
|
+
export interface LanguageDefinition {
|
|
43
|
+
keywords: KeywordDef[];
|
|
44
|
+
functions: FunctionDef[];
|
|
45
|
+
operators: OperatorDef[];
|
|
46
|
+
comments: {
|
|
47
|
+
line: string;
|
|
48
|
+
blockStart: string;
|
|
49
|
+
blockEnd: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"rootDir": "./src"
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|