@iyulab/m3l 0.1.2 → 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 +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/lexer.js +4 -3
- package/dist/parser.d.ts +5 -0
- package/dist/parser.js +159 -1
- package/dist/reader.d.ts +1 -1
- package/dist/reader.js +1 -1
- package/dist/resolver.d.ts +1 -1
- package/dist/resolver.js +28 -1
- package/dist/types.d.ts +29 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -238,7 +238,7 @@ This TypeScript parser and the [C# parser](../csharp/) produce equivalent AST st
|
|
|
238
238
|
|
|
239
239
|
| | TypeScript | C# |
|
|
240
240
|
|---|---|---|
|
|
241
|
-
| Package | `@iyulab/m3l` | `
|
|
241
|
+
| Package | `@iyulab/m3l` | `M3LParser` |
|
|
242
242
|
| Runtime | Node.js 20+ | .NET 8.0+ |
|
|
243
243
|
| AST Version | 1.0 | 1.0 |
|
|
244
244
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { lex } from './lexer.js';
|
|
2
|
-
export { parseTokens, parseString as parseFileString } from './parser.js';
|
|
2
|
+
export { parseTokens, parseString as parseFileString, STANDARD_ATTRIBUTES } from './parser.js';
|
|
3
3
|
export { resolve, AST_VERSION, PARSER_VERSION } from './resolver.js';
|
|
4
4
|
export { validate } from './validator.js';
|
|
5
5
|
export { readM3LFiles, readM3LString, readProjectConfig } from './reader.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { lex } from './lexer.js';
|
|
2
|
-
export { parseTokens, parseString as parseFileString } from './parser.js';
|
|
2
|
+
export { parseTokens, parseString as parseFileString, STANDARD_ATTRIBUTES } from './parser.js';
|
|
3
3
|
export { resolve, AST_VERSION, PARSER_VERSION } from './resolver.js';
|
|
4
4
|
export { validate } from './validator.js';
|
|
5
5
|
export { readM3LFiles, readM3LString, readProjectConfig } from './reader.js';
|
|
@@ -15,7 +15,7 @@ export async function parse(inputPath) {
|
|
|
15
15
|
const resolved = resolvePath(inputPath);
|
|
16
16
|
const files = await readM3LFiles(resolved);
|
|
17
17
|
if (files.length === 0) {
|
|
18
|
-
throw new Error(`No .m3l.md files found at: ${inputPath}`);
|
|
18
|
+
throw new Error(`No .m3l.md or .m3l files found at: ${inputPath}`);
|
|
19
19
|
}
|
|
20
20
|
const parsedFiles = files.map(f => parseFileContent(f.content, f.path));
|
|
21
21
|
let projectInfo;
|
package/dist/lexer.js
CHANGED
|
@@ -7,7 +7,7 @@ const RE_BLOCKQUOTE = /^> (.+)$/;
|
|
|
7
7
|
const RE_LIST_ITEM = /^(\s*)- (.+)$/;
|
|
8
8
|
const RE_BLANK = /^\s*$/;
|
|
9
9
|
// H2 sub-patterns
|
|
10
|
-
const RE_TYPE_INDICATOR = /^([\w][\w.]*(?:\([^)]*\))?)\s*::(\w+)(.*)$/;
|
|
10
|
+
const RE_TYPE_INDICATOR = /^(@?[\w][\w.]*(?:\([^)]*\))?)\s*::(\w+)(.*)$/;
|
|
11
11
|
const RE_MODEL_DEF = /^([\w][\w.]*(?:\([^)]*\))?)\s*(?::\s*(.+?))?(\s+@.+)?$/;
|
|
12
12
|
// Field line patterns
|
|
13
13
|
const RE_FIELD_NAME = /^([\w]+)(?:\(([^)]*)\))?\s*(?::\s*(.+))?$/;
|
|
@@ -135,7 +135,7 @@ export function lex(content, file) {
|
|
|
135
135
|
return tokens;
|
|
136
136
|
}
|
|
137
137
|
function tokenizeH2(content, raw, line) {
|
|
138
|
-
// Check for type indicator: ## Name ::enum, ::interface, ::view
|
|
138
|
+
// Check for type indicator: ## Name ::enum, ::interface, ::view, ::attribute
|
|
139
139
|
const typeMatch = content.match(RE_TYPE_INDICATOR);
|
|
140
140
|
if (typeMatch) {
|
|
141
141
|
const namepart = typeMatch[1];
|
|
@@ -151,7 +151,8 @@ function tokenizeH2(content, raw, line) {
|
|
|
151
151
|
if (descMatch) {
|
|
152
152
|
data.description = descMatch[1];
|
|
153
153
|
}
|
|
154
|
-
|
|
154
|
+
const tokenType = typeIndicator === 'attribute' ? 'attribute_def' : typeIndicator;
|
|
155
|
+
return { type: tokenType, raw, line, indent: 0, data };
|
|
155
156
|
}
|
|
156
157
|
// Regular model: ## Name : Parent1, Parent2
|
|
157
158
|
const modelMatch = content.match(RE_MODEL_DEF);
|
package/dist/parser.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { Token, ParsedFile } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Standard M3L attribute catalog.
|
|
4
|
+
* These are the officially defined attributes in the M3L specification.
|
|
5
|
+
*/
|
|
6
|
+
export declare const STANDARD_ATTRIBUTES: Set<string>;
|
|
2
7
|
/**
|
|
3
8
|
* Parse M3L content string into a ParsedFile AST.
|
|
4
9
|
*/
|
package/dist/parser.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { lex, parseTypeAndAttrs } from './lexer.js';
|
|
2
|
+
/**
|
|
3
|
+
* Standard M3L attribute catalog.
|
|
4
|
+
* These are the officially defined attributes in the M3L specification.
|
|
5
|
+
*/
|
|
6
|
+
export const STANDARD_ATTRIBUTES = new Set([
|
|
7
|
+
// Field constraints
|
|
8
|
+
'primary', 'unique', 'required', 'index', 'generated', 'immutable',
|
|
9
|
+
// References / relations
|
|
10
|
+
'reference', 'fk', 'relation', 'on_update', 'on_delete',
|
|
11
|
+
// Search / display
|
|
12
|
+
'searchable', 'description', 'visibility',
|
|
13
|
+
// Validation
|
|
14
|
+
'min', 'max', 'validate', 'not_null',
|
|
15
|
+
// Derived fields
|
|
16
|
+
'computed', 'computed_raw', 'lookup', 'rollup', 'from', 'persisted',
|
|
17
|
+
// Model-level
|
|
18
|
+
'public', 'private', 'materialized', 'meta', 'behavior', 'override', 'default_attribute',
|
|
19
|
+
]);
|
|
2
20
|
/**
|
|
3
21
|
* Parse M3L content string into a ParsedFile AST.
|
|
4
22
|
*/
|
|
@@ -21,6 +39,8 @@ export function parseTokens(tokens, file) {
|
|
|
21
39
|
enums: [],
|
|
22
40
|
interfaces: [],
|
|
23
41
|
views: [],
|
|
42
|
+
attributeRegistry: [],
|
|
43
|
+
currentAttrDef: null,
|
|
24
44
|
sourceDirectivesDone: false,
|
|
25
45
|
};
|
|
26
46
|
for (const token of tokens) {
|
|
@@ -35,6 +55,7 @@ export function parseTokens(tokens, file) {
|
|
|
35
55
|
enums: state.enums,
|
|
36
56
|
interfaces: state.interfaces,
|
|
37
57
|
views: state.views,
|
|
58
|
+
attributeRegistry: state.attributeRegistry,
|
|
38
59
|
};
|
|
39
60
|
}
|
|
40
61
|
function processToken(token, state) {
|
|
@@ -52,6 +73,9 @@ function processToken(token, state) {
|
|
|
52
73
|
case 'view':
|
|
53
74
|
handleViewStart(token, state);
|
|
54
75
|
break;
|
|
76
|
+
case 'attribute_def':
|
|
77
|
+
handleAttributeDefStart(token, state);
|
|
78
|
+
break;
|
|
55
79
|
case 'section':
|
|
56
80
|
handleSection(token, state);
|
|
57
81
|
break;
|
|
@@ -181,6 +205,18 @@ function handleSection(token, state) {
|
|
|
181
205
|
}
|
|
182
206
|
}
|
|
183
207
|
function handleField(token, state) {
|
|
208
|
+
// Handle attribute definition fields (- target: [field, model])
|
|
209
|
+
if (state.currentAttrDef) {
|
|
210
|
+
const data = token.data;
|
|
211
|
+
const name = data.name;
|
|
212
|
+
const raw = token.raw.trim().replace(/^-\s*/, '');
|
|
213
|
+
const colonIdx = raw.indexOf(':');
|
|
214
|
+
if (colonIdx >= 0) {
|
|
215
|
+
const value = raw.substring(colonIdx + 1).trim();
|
|
216
|
+
state.currentAttrDef.fields.set(name, value);
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
184
220
|
if (!state.currentElement)
|
|
185
221
|
return;
|
|
186
222
|
const data = token.data;
|
|
@@ -455,6 +491,12 @@ function handleNestedItem(token, state) {
|
|
|
455
491
|
}
|
|
456
492
|
}
|
|
457
493
|
function handleBlockquote(token, state) {
|
|
494
|
+
// Handle attribute definition description
|
|
495
|
+
if (state.currentAttrDef) {
|
|
496
|
+
const text = token.data.text;
|
|
497
|
+
state.currentAttrDef.description = text;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
458
500
|
if (!state.currentElement)
|
|
459
501
|
return;
|
|
460
502
|
const text = token.data.text;
|
|
@@ -478,6 +520,8 @@ function handleText(token, state) {
|
|
|
478
520
|
}
|
|
479
521
|
}
|
|
480
522
|
function finalizeElement(state) {
|
|
523
|
+
// Finalize pending attribute definition
|
|
524
|
+
finalizeAttrDef(state);
|
|
481
525
|
if (!state.currentElement)
|
|
482
526
|
return;
|
|
483
527
|
if (isEnumNode(state.currentElement)) {
|
|
@@ -502,6 +546,70 @@ function finalizeElement(state) {
|
|
|
502
546
|
state.currentKind = 'stored';
|
|
503
547
|
state.lastField = null;
|
|
504
548
|
}
|
|
549
|
+
function handleAttributeDefStart(token, state) {
|
|
550
|
+
finalizeElement(state);
|
|
551
|
+
const data = token.data || {};
|
|
552
|
+
const name = (data.name || '').replace(/^@/, '');
|
|
553
|
+
state.currentAttrDef = {
|
|
554
|
+
name,
|
|
555
|
+
description: data.description,
|
|
556
|
+
fields: new Map(),
|
|
557
|
+
};
|
|
558
|
+
state.currentElement = null;
|
|
559
|
+
}
|
|
560
|
+
function finalizeAttrDef(state) {
|
|
561
|
+
if (!state.currentAttrDef)
|
|
562
|
+
return;
|
|
563
|
+
const def = state.currentAttrDef;
|
|
564
|
+
const fields = def.fields;
|
|
565
|
+
const targetRaw = fields.get('target');
|
|
566
|
+
const target = [];
|
|
567
|
+
if (targetRaw) {
|
|
568
|
+
const cleaned = targetRaw.replace(/^\[|\]$/g, '').split(',').map(s => s.trim());
|
|
569
|
+
for (const t of cleaned) {
|
|
570
|
+
if (t === 'field' || t === 'model')
|
|
571
|
+
target.push(t);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const rangeRaw = fields.get('range');
|
|
575
|
+
let range;
|
|
576
|
+
if (rangeRaw) {
|
|
577
|
+
const nums = rangeRaw.replace(/^\[|\]$/g, '').split(',').map(s => Number(s.trim()));
|
|
578
|
+
if (nums.length === 2 && !isNaN(nums[0]) && !isNaN(nums[1])) {
|
|
579
|
+
range = [nums[0], nums[1]];
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const requiredRaw = fields.get('required');
|
|
583
|
+
const required = requiredRaw === 'true' || requiredRaw === true;
|
|
584
|
+
const defaultRaw = fields.get('default');
|
|
585
|
+
let defaultValue;
|
|
586
|
+
if (defaultRaw !== undefined) {
|
|
587
|
+
if (defaultRaw === 'true')
|
|
588
|
+
defaultValue = true;
|
|
589
|
+
else if (defaultRaw === 'false')
|
|
590
|
+
defaultValue = false;
|
|
591
|
+
else if (typeof defaultRaw === 'string' && !isNaN(Number(defaultRaw)))
|
|
592
|
+
defaultValue = Number(defaultRaw);
|
|
593
|
+
else if (typeof defaultRaw === 'string')
|
|
594
|
+
defaultValue = defaultRaw;
|
|
595
|
+
else if (typeof defaultRaw === 'number' || typeof defaultRaw === 'boolean')
|
|
596
|
+
defaultValue = defaultRaw;
|
|
597
|
+
}
|
|
598
|
+
const entry = {
|
|
599
|
+
name: def.name,
|
|
600
|
+
target: target.length > 0 ? target : ['field'],
|
|
601
|
+
type: fields.get('type') || 'boolean',
|
|
602
|
+
required,
|
|
603
|
+
};
|
|
604
|
+
if (def.description)
|
|
605
|
+
entry.description = def.description;
|
|
606
|
+
if (range)
|
|
607
|
+
entry.range = range;
|
|
608
|
+
if (defaultValue !== undefined)
|
|
609
|
+
entry.defaultValue = defaultValue;
|
|
610
|
+
state.attributeRegistry.push(entry);
|
|
611
|
+
state.currentAttrDef = null;
|
|
612
|
+
}
|
|
505
613
|
// --- Helpers ---
|
|
506
614
|
function buildFieldNode(data, token, state) {
|
|
507
615
|
const attrs = parseAttributes(data.attributes);
|
|
@@ -602,6 +710,8 @@ function parseAttributes(rawAttrs) {
|
|
|
602
710
|
attr.args = [a.args];
|
|
603
711
|
if (a.cascade)
|
|
604
712
|
attr.cascade = a.cascade;
|
|
713
|
+
if (STANDARD_ATTRIBUTES.has(a.name))
|
|
714
|
+
attr.isStandard = true;
|
|
605
715
|
return attr;
|
|
606
716
|
});
|
|
607
717
|
}
|
|
@@ -798,9 +908,57 @@ function parseCustomAttributes(rawAttrs) {
|
|
|
798
908
|
return rawAttrs.map(raw => {
|
|
799
909
|
// raw is "[MaxLength(100)]" — strip brackets to get content
|
|
800
910
|
const content = raw.replace(/^\[|\]$/g, '');
|
|
801
|
-
|
|
911
|
+
const attr = { content, raw };
|
|
912
|
+
// Try to parse "Name(arg1, arg2)" or "Name" pattern
|
|
913
|
+
const match = content.match(/^([A-Za-z_][\w.]*)(?:\((.+)\))?$/);
|
|
914
|
+
if (match) {
|
|
915
|
+
const name = match[1];
|
|
916
|
+
const argsStr = match[2];
|
|
917
|
+
const args = [];
|
|
918
|
+
if (argsStr) {
|
|
919
|
+
for (const part of splitBalanced(argsStr)) {
|
|
920
|
+
const trimmed = part.trim();
|
|
921
|
+
args.push(parseArgValue(trimmed));
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
attr.parsed = { name, arguments: args };
|
|
925
|
+
}
|
|
926
|
+
return attr;
|
|
802
927
|
});
|
|
803
928
|
}
|
|
929
|
+
/** Split a string by commas, respecting balanced parentheses */
|
|
930
|
+
function splitBalanced(s) {
|
|
931
|
+
const parts = [];
|
|
932
|
+
let depth = 0;
|
|
933
|
+
let start = 0;
|
|
934
|
+
for (let i = 0; i < s.length; i++) {
|
|
935
|
+
if (s[i] === '(')
|
|
936
|
+
depth++;
|
|
937
|
+
else if (s[i] === ')')
|
|
938
|
+
depth--;
|
|
939
|
+
else if (s[i] === ',' && depth === 0) {
|
|
940
|
+
parts.push(s.substring(start, i));
|
|
941
|
+
start = i + 1;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
parts.push(s.substring(start));
|
|
945
|
+
return parts;
|
|
946
|
+
}
|
|
947
|
+
/** Parse a single argument value: numbers, booleans, or strings */
|
|
948
|
+
function parseArgValue(s) {
|
|
949
|
+
if (s === 'true')
|
|
950
|
+
return true;
|
|
951
|
+
if (s === 'false')
|
|
952
|
+
return false;
|
|
953
|
+
const n = Number(s);
|
|
954
|
+
if (!isNaN(n) && s.length > 0)
|
|
955
|
+
return n;
|
|
956
|
+
// Strip surrounding quotes if present
|
|
957
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
958
|
+
return s.slice(1, -1);
|
|
959
|
+
}
|
|
960
|
+
return s;
|
|
961
|
+
}
|
|
804
962
|
function isEnumNode(el) {
|
|
805
963
|
return el.type === 'enum' && 'values' in el;
|
|
806
964
|
}
|
package/dist/reader.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface M3LFile {
|
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
6
|
* Read M3L files from a path (file or directory).
|
|
7
|
-
* If path is a directory, scans for **\/*.m3l.md files.
|
|
7
|
+
* If path is a directory, scans for **\/*.m3l.md and **\/*.m3l files.
|
|
8
8
|
* If an m3l.config.yaml exists in the directory, uses its sources patterns.
|
|
9
9
|
*/
|
|
10
10
|
export declare function readM3LFiles(inputPath: string): Promise<M3LFile[]>;
|
package/dist/reader.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join, resolve as resolvePath } from 'path';
|
|
|
3
3
|
import fg from 'fast-glob';
|
|
4
4
|
/**
|
|
5
5
|
* Read M3L files from a path (file or directory).
|
|
6
|
-
* If path is a directory, scans for **\/*.m3l.md files.
|
|
6
|
+
* If path is a directory, scans for **\/*.m3l.md and **\/*.m3l files.
|
|
7
7
|
* If an m3l.config.yaml exists in the directory, uses its sources patterns.
|
|
8
8
|
*/
|
|
9
9
|
export async function readM3LFiles(inputPath) {
|
package/dist/resolver.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ParsedFile, M3LAST, ProjectInfo } from './types.js';
|
|
|
2
2
|
/** AST schema version — bump major on breaking structure changes */
|
|
3
3
|
export declare const AST_VERSION = "1.0";
|
|
4
4
|
/** Parser package version — kept in sync with package.json */
|
|
5
|
-
export declare const PARSER_VERSION = "0.1.
|
|
5
|
+
export declare const PARSER_VERSION = "0.1.3";
|
|
6
6
|
/**
|
|
7
7
|
* Resolve and merge multiple parsed file ASTs into a single M3LAST.
|
|
8
8
|
* Handles: inheritance resolution, duplicate detection, reference validation.
|
package/dist/resolver.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** AST schema version — bump major on breaking structure changes */
|
|
2
2
|
export const AST_VERSION = '1.0';
|
|
3
3
|
/** Parser package version — kept in sync with package.json */
|
|
4
|
-
export const PARSER_VERSION = '0.1.
|
|
4
|
+
export const PARSER_VERSION = '0.1.3';
|
|
5
5
|
/**
|
|
6
6
|
* Resolve and merge multiple parsed file ASTs into a single M3LAST.
|
|
7
7
|
* Handles: inheritance resolution, duplicate detection, reference validation.
|
|
@@ -14,6 +14,7 @@ export function resolve(files, project) {
|
|
|
14
14
|
const allEnums = [];
|
|
15
15
|
const allInterfaces = [];
|
|
16
16
|
const allViews = [];
|
|
17
|
+
const allAttrRegistry = [];
|
|
17
18
|
const sources = [];
|
|
18
19
|
for (const file of files) {
|
|
19
20
|
sources.push(file.source);
|
|
@@ -21,6 +22,8 @@ export function resolve(files, project) {
|
|
|
21
22
|
allEnums.push(...file.enums);
|
|
22
23
|
allInterfaces.push(...file.interfaces);
|
|
23
24
|
allViews.push(...file.views);
|
|
25
|
+
if (file.attributeRegistry)
|
|
26
|
+
allAttrRegistry.push(...file.attributeRegistry);
|
|
24
27
|
}
|
|
25
28
|
// Build name maps
|
|
26
29
|
const modelMap = new Map();
|
|
@@ -54,6 +57,29 @@ export function resolve(files, project) {
|
|
|
54
57
|
for (const model of [...allModels, ...allViews]) {
|
|
55
58
|
checkDuplicateFields(model, errors);
|
|
56
59
|
}
|
|
60
|
+
// Tag isRegistered on attributes matching the registry
|
|
61
|
+
if (allAttrRegistry.length > 0) {
|
|
62
|
+
const registeredNames = new Set(allAttrRegistry.map(r => r.name));
|
|
63
|
+
const tagAttrs = (attrs) => {
|
|
64
|
+
for (const a of attrs) {
|
|
65
|
+
if (registeredNames.has(a.name)) {
|
|
66
|
+
a.isRegistered = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const tagModel = (m) => {
|
|
71
|
+
tagAttrs(m.attributes);
|
|
72
|
+
for (const f of m.fields) {
|
|
73
|
+
tagAttrs(f.attributes);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
for (const m of allModels)
|
|
77
|
+
tagModel(m);
|
|
78
|
+
for (const v of allViews)
|
|
79
|
+
tagModel(v);
|
|
80
|
+
for (const i of allInterfaces)
|
|
81
|
+
tagModel(i);
|
|
82
|
+
}
|
|
57
83
|
// Detect namespace from first file if available
|
|
58
84
|
const projectInfo = project || {};
|
|
59
85
|
if (!projectInfo.name) {
|
|
@@ -70,6 +96,7 @@ export function resolve(files, project) {
|
|
|
70
96
|
enums: allEnums,
|
|
71
97
|
interfaces: allInterfaces,
|
|
72
98
|
views: allViews,
|
|
99
|
+
attributeRegistry: allAttrRegistry,
|
|
73
100
|
errors,
|
|
74
101
|
warnings,
|
|
75
102
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface SourceLocation {
|
|
|
4
4
|
line: number;
|
|
5
5
|
col: number;
|
|
6
6
|
}
|
|
7
|
-
export type TokenType = 'namespace' | 'model' | 'enum' | 'interface' | 'view' | 'section' | 'field' | 'nested_item' | 'blockquote' | 'horizontal_rule' | 'blank' | 'text';
|
|
7
|
+
export type TokenType = 'namespace' | 'model' | 'enum' | 'interface' | 'view' | 'attribute_def' | 'section' | 'field' | 'nested_item' | 'blockquote' | 'horizontal_rule' | 'blank' | 'text';
|
|
8
8
|
export interface Token {
|
|
9
9
|
type: TokenType;
|
|
10
10
|
raw: string;
|
|
@@ -17,6 +17,10 @@ export interface FieldAttribute {
|
|
|
17
17
|
name: string;
|
|
18
18
|
args?: (string | number | boolean)[];
|
|
19
19
|
cascade?: string;
|
|
20
|
+
/** Whether this is a standard M3L attribute (from the official catalog) */
|
|
21
|
+
isStandard?: boolean;
|
|
22
|
+
/** Whether this attribute is registered in an Attribute Registry (::attribute definition) */
|
|
23
|
+
isRegistered?: boolean;
|
|
20
24
|
}
|
|
21
25
|
/** Structured representation of a backtick-wrapped framework attribute like `[MaxLength(100)]` */
|
|
22
26
|
export interface CustomAttribute {
|
|
@@ -24,6 +28,11 @@ export interface CustomAttribute {
|
|
|
24
28
|
content: string;
|
|
25
29
|
/** Original text including brackets, e.g., "[MaxLength(100)]" */
|
|
26
30
|
raw: string;
|
|
31
|
+
/** Parsed structure — name and arguments extracted from the content */
|
|
32
|
+
parsed?: {
|
|
33
|
+
name: string;
|
|
34
|
+
arguments: (string | number | boolean)[];
|
|
35
|
+
};
|
|
27
36
|
}
|
|
28
37
|
export interface EnumValue {
|
|
29
38
|
name: string;
|
|
@@ -127,6 +136,23 @@ export interface ParsedFile {
|
|
|
127
136
|
enums: EnumNode[];
|
|
128
137
|
interfaces: ModelNode[];
|
|
129
138
|
views: ModelNode[];
|
|
139
|
+
attributeRegistry: AttributeRegistryEntry[];
|
|
140
|
+
}
|
|
141
|
+
export interface AttributeRegistryEntry {
|
|
142
|
+
/** Attribute name (without @) */
|
|
143
|
+
name: string;
|
|
144
|
+
/** Description */
|
|
145
|
+
description?: string;
|
|
146
|
+
/** Valid targets: 'field', 'model' */
|
|
147
|
+
target: ('field' | 'model')[];
|
|
148
|
+
/** Value type: 'boolean', 'integer', 'string', etc. */
|
|
149
|
+
type: string;
|
|
150
|
+
/** Valid range for numeric types */
|
|
151
|
+
range?: [number, number];
|
|
152
|
+
/** Whether the attribute is required */
|
|
153
|
+
required: boolean;
|
|
154
|
+
/** Default value */
|
|
155
|
+
defaultValue?: string | number | boolean;
|
|
130
156
|
}
|
|
131
157
|
export interface M3LAST {
|
|
132
158
|
/** Parser package version (semver) */
|
|
@@ -139,6 +165,8 @@ export interface M3LAST {
|
|
|
139
165
|
enums: EnumNode[];
|
|
140
166
|
interfaces: ModelNode[];
|
|
141
167
|
views: ModelNode[];
|
|
168
|
+
/** Attribute registry entries parsed from ::attribute definitions */
|
|
169
|
+
attributeRegistry: AttributeRegistryEntry[];
|
|
142
170
|
errors: Diagnostic[];
|
|
143
171
|
warnings: Diagnostic[];
|
|
144
172
|
}
|