@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 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
+ }
@@ -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
+ }
@@ -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;
@@ -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
+ }