@iyulab/m3l 0.1.4 → 0.4.1
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/index.d.ts +25 -0
- package/index.js +12 -0
- package/package.json +22 -27
- package/README.md +0 -305
- package/dist/cli.d.ts +0 -14
- package/dist/cli.js +0 -150
- package/dist/index.d.ts +0 -23
- package/dist/index.js +0 -46
- package/dist/lexer.d.ts +0 -6
- package/dist/lexer.js +0 -452
- package/dist/parser.d.ts +0 -14
- package/dist/parser.js +0 -991
- package/dist/reader.d.ts +0 -21
- package/dist/reader.js +0 -84
- package/dist/resolver.d.ts +0 -10
- package/dist/resolver.js +0 -192
- package/dist/types.d.ts +0 -180
- package/dist/types.js +0 -1
- package/dist/validator.d.ts +0 -5
- package/dist/validator.js +0 -196
package/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a single M3L file and return the AST as JSON.
|
|
3
|
+
*
|
|
4
|
+
* @param content - M3L markdown text
|
|
5
|
+
* @param filename - Source filename for error reporting
|
|
6
|
+
* @returns JSON string with `{ success: boolean, data?: AST, error?: string }`
|
|
7
|
+
*/
|
|
8
|
+
export function parse(content: string, filename: string): string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse multiple M3L files and return the merged AST as JSON.
|
|
12
|
+
*
|
|
13
|
+
* @param filesJson - JSON array of `{ content: string, filename: string }` objects
|
|
14
|
+
* @returns JSON string with `{ success: boolean, data?: AST, error?: string }`
|
|
15
|
+
*/
|
|
16
|
+
export function parseMulti(filesJson: string): string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate M3L content and return diagnostics as JSON.
|
|
20
|
+
*
|
|
21
|
+
* @param content - M3L markdown text
|
|
22
|
+
* @param optionsJson - JSON options `{ strict?: boolean, filename?: string }`
|
|
23
|
+
* @returns JSON string with `{ success: boolean, data?: ValidateResult, error?: string }`
|
|
24
|
+
*/
|
|
25
|
+
export function validate(content: string, optionsJson: string): string;
|
package/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @iyulab/m3l — M3L parser (Rust native via NAPI)
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper that re-exports the native NAPI addon.
|
|
5
|
+
* All parsing is performed by the Rust m3l-core library.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { parse, parseMulti, validate } = require('@iyulab/m3l-napi');
|
|
9
|
+
|
|
10
|
+
module.exports.parse = parse;
|
|
11
|
+
module.exports.parseMulti = parseMulti;
|
|
12
|
+
module.exports.validate = validate;
|
package/package.json
CHANGED
|
@@ -1,34 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/m3l",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "M3L parser
|
|
5
|
-
"
|
|
6
|
-
"main": "
|
|
7
|
-
"types": "
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"build": "tsc",
|
|
13
|
-
"test": "vitest run",
|
|
14
|
-
"test:watch": "vitest",
|
|
15
|
-
"lint": "tsc --noEmit"
|
|
16
|
-
},
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "M3L parser — Markdown-based schema definition language (.m3l.md → JSON AST)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"index.d.ts"
|
|
11
|
+
],
|
|
17
12
|
"dependencies": {
|
|
18
|
-
"
|
|
19
|
-
"yaml": "^2.7.0",
|
|
20
|
-
"fast-glob": "^3.3.0"
|
|
21
|
-
},
|
|
22
|
-
"devDependencies": {
|
|
23
|
-
"typescript": "^5.7.0",
|
|
24
|
-
"vitest": "^3.0.0",
|
|
25
|
-
"@types/node": "^22.0.0"
|
|
13
|
+
"@iyulab/m3l-napi": "0.4.1"
|
|
26
14
|
},
|
|
27
15
|
"engines": {
|
|
28
|
-
"node": ">=
|
|
16
|
+
"node": ">= 18"
|
|
29
17
|
},
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/iyulab/m3l"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"m3l",
|
|
24
|
+
"markdown",
|
|
25
|
+
"schema",
|
|
26
|
+
"parser",
|
|
27
|
+
"ast"
|
|
28
|
+
]
|
|
34
29
|
}
|
package/README.md
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
# @iyulab/m3l
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@iyulab/m3l)
|
|
4
|
-
[](https://github.com/iyulab/m3l/actions/workflows/parser-publish.yml)
|
|
5
|
-
[](../../LICENSE)
|
|
6
|
-
|
|
7
|
-
M3L (Meta Model Markup Language) parser and CLI — parse `.m3l.md` / `.m3l` files into a structured JSON AST.
|
|
8
|
-
|
|
9
|
-
M3L is a Markdown-based data modeling language. You write data models in readable Markdown, and this parser converts them into a machine-processable AST with full validation.
|
|
10
|
-
|
|
11
|
-
## Install
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npm install @iyulab/m3l
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Quick Start
|
|
18
|
-
|
|
19
|
-
### As a Library
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
import { parse, parseString, validateFiles } from '@iyulab/m3l';
|
|
23
|
-
|
|
24
|
-
// Parse a file or directory
|
|
25
|
-
const ast = await parse('./models');
|
|
26
|
-
|
|
27
|
-
// Parse a string
|
|
28
|
-
const ast2 = parseString(`
|
|
29
|
-
## User
|
|
30
|
-
- name: string(100) @not_null
|
|
31
|
-
- email: string(320)? @unique
|
|
32
|
-
|
|
33
|
-
## UserRole ::enum
|
|
34
|
-
- admin: "Administrator"
|
|
35
|
-
- user: "Regular User"
|
|
36
|
-
`);
|
|
37
|
-
|
|
38
|
-
console.log(ast2.models); // [{ name: 'User', fields: [...], ... }]
|
|
39
|
-
console.log(ast2.enums); // [{ name: 'UserRole', values: [...], ... }]
|
|
40
|
-
|
|
41
|
-
// Validate with diagnostics
|
|
42
|
-
const { ast: validated, errors, warnings } = await validateFiles('./models');
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### As a CLI
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
# Parse and output JSON AST
|
|
49
|
-
npx m3l parse ./models
|
|
50
|
-
|
|
51
|
-
# Parse a single file
|
|
52
|
-
npx m3l parse ./models/user.m3l.md -o ast.json
|
|
53
|
-
|
|
54
|
-
# Validate
|
|
55
|
-
npx m3l validate ./models
|
|
56
|
-
|
|
57
|
-
# Validate with strict style checks and JSON output
|
|
58
|
-
npx m3l validate ./models --strict --format json
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## M3L Syntax
|
|
62
|
-
|
|
63
|
-
```markdown
|
|
64
|
-
# Library System
|
|
65
|
-
|
|
66
|
-
## Author
|
|
67
|
-
- name(Author Name): string(100) @not_null @idx
|
|
68
|
-
- bio(Biography): text?
|
|
69
|
-
- birth_date: date?
|
|
70
|
-
|
|
71
|
-
### Rollup
|
|
72
|
-
- book_count: integer @rollup(BookAuthor.author_id, count)
|
|
73
|
-
|
|
74
|
-
> Stores information about book authors.
|
|
75
|
-
|
|
76
|
-
## BookStatus ::enum "Status of a book"
|
|
77
|
-
- available: "Available"
|
|
78
|
-
- borrowed: "Borrowed"
|
|
79
|
-
- reserved: "Reserved"
|
|
80
|
-
|
|
81
|
-
## Book : BaseModel
|
|
82
|
-
- title: string(200) @not_null @idx
|
|
83
|
-
- isbn: string(20) @unique
|
|
84
|
-
- status: enum = "available"
|
|
85
|
-
- available: "Available"
|
|
86
|
-
- borrowed: "Borrowed"
|
|
87
|
-
- publisher_id: identifier @fk(Publisher.id)
|
|
88
|
-
|
|
89
|
-
### Lookup
|
|
90
|
-
- publisher_name: string @lookup(publisher_id.name)
|
|
91
|
-
|
|
92
|
-
### Computed
|
|
93
|
-
- is_available: boolean @computed("status = 'available' AND quantity > 0")
|
|
94
|
-
|
|
95
|
-
## OverdueLoans ::view @materialized
|
|
96
|
-
> Currently overdue book loans.
|
|
97
|
-
### Source
|
|
98
|
-
- from: Loan
|
|
99
|
-
- where: "due_date < now() AND status = 'ongoing'"
|
|
100
|
-
- order_by: due_date asc
|
|
101
|
-
- borrower_name: string @lookup(member_id.name)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
**Key syntax elements:**
|
|
105
|
-
|
|
106
|
-
| Syntax | Meaning |
|
|
107
|
-
|--------|---------|
|
|
108
|
-
| `# Title` | Document title or namespace (`# Namespace: domain.example`) |
|
|
109
|
-
| `## Name` | Model definition |
|
|
110
|
-
| `## Name : Parent` | Model with inheritance |
|
|
111
|
-
| `## Name ::enum` | Enum definition |
|
|
112
|
-
| `## Name ::interface` | Interface definition |
|
|
113
|
-
| `## Name ::view` | Derived view |
|
|
114
|
-
| `## @name ::attribute` | Attribute registry entry |
|
|
115
|
-
| `- field: type` | Field definition |
|
|
116
|
-
| `- field: type?` | Nullable field |
|
|
117
|
-
| `- field: type[]` | Array field |
|
|
118
|
-
| `- field: type?[]` | Array of nullable items |
|
|
119
|
-
| `- field: type = val` | Field with default value |
|
|
120
|
-
| `@attr` / `@attr(args)` | Attribute (constraint, index, etc.) |
|
|
121
|
-
| `` `[FrameworkAttr]` `` | Custom framework attribute |
|
|
122
|
-
| `### Lookup` / `### Rollup` / `### Computed` | Kind section for derived fields |
|
|
123
|
-
| `### Section` | Named section (Indexes, Relations, Metadata, etc.) |
|
|
124
|
-
| `> text` | Model/element description |
|
|
125
|
-
| `"text"` | Inline description on field |
|
|
126
|
-
|
|
127
|
-
## AST Output
|
|
128
|
-
|
|
129
|
-
The parser produces an `M3LAST` object:
|
|
130
|
-
|
|
131
|
-
```typescript
|
|
132
|
-
interface M3LAST {
|
|
133
|
-
parserVersion: string; // Parser package version (semver)
|
|
134
|
-
astVersion: string; // AST schema version
|
|
135
|
-
project: { name?: string; version?: string };
|
|
136
|
-
sources: string[];
|
|
137
|
-
models: ModelNode[];
|
|
138
|
-
enums: EnumNode[];
|
|
139
|
-
interfaces: ModelNode[];
|
|
140
|
-
views: ModelNode[];
|
|
141
|
-
attributeRegistry: AttributeRegistryEntry[];
|
|
142
|
-
errors: Diagnostic[];
|
|
143
|
-
warnings: Diagnostic[];
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### Key AST types
|
|
148
|
-
|
|
149
|
-
```typescript
|
|
150
|
-
interface FieldNode {
|
|
151
|
-
name: string;
|
|
152
|
-
label?: string;
|
|
153
|
-
type?: string;
|
|
154
|
-
params?: (string | number)[];
|
|
155
|
-
generic_params?: string[]; // map<K,V> -> ["K", "V"]
|
|
156
|
-
nullable: boolean;
|
|
157
|
-
array: boolean;
|
|
158
|
-
arrayItemNullable: boolean; // string?[] -> true
|
|
159
|
-
kind: 'stored' | 'computed' | 'lookup' | 'rollup';
|
|
160
|
-
default_value?: string;
|
|
161
|
-
description?: string;
|
|
162
|
-
attributes: FieldAttribute[];
|
|
163
|
-
framework_attrs?: CustomAttribute[];
|
|
164
|
-
lookup?: { path: string };
|
|
165
|
-
rollup?: { target: string; fk: string; aggregate: string; field?: string; where?: string };
|
|
166
|
-
computed?: { expression: string };
|
|
167
|
-
enum_values?: EnumValue[];
|
|
168
|
-
fields?: FieldNode[]; // sub-fields for object type
|
|
169
|
-
loc: SourceLocation;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
interface FieldAttribute {
|
|
173
|
-
name: string;
|
|
174
|
-
args?: (string | number | boolean)[];
|
|
175
|
-
cascade?: string;
|
|
176
|
-
isStandard?: boolean; // true for M3L standard attributes
|
|
177
|
-
isRegistered?: boolean; // true for attributes in the registry
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
interface CustomAttribute {
|
|
181
|
-
content: string; // e.g. "MaxLength(100)"
|
|
182
|
-
raw: string; // e.g. "[MaxLength(100)]"
|
|
183
|
-
parsed?: { // structured parse result
|
|
184
|
-
name: string;
|
|
185
|
-
arguments: (string | number | boolean)[];
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
interface AttributeRegistryEntry {
|
|
190
|
-
name: string;
|
|
191
|
-
description?: string;
|
|
192
|
-
target: ('field' | 'model')[];
|
|
193
|
-
type: string;
|
|
194
|
-
range?: [number, number];
|
|
195
|
-
required: boolean;
|
|
196
|
-
defaultValue?: string | number | boolean;
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
## Attribute Registry
|
|
201
|
-
|
|
202
|
-
Define custom attributes with validation metadata using `::attribute`:
|
|
203
|
-
|
|
204
|
-
```markdown
|
|
205
|
-
## @pii ::attribute
|
|
206
|
-
> Personal identifiable information marker
|
|
207
|
-
- target: [field]
|
|
208
|
-
- type: boolean
|
|
209
|
-
- default: false
|
|
210
|
-
|
|
211
|
-
## @audit_level ::attribute
|
|
212
|
-
> Audit compliance level
|
|
213
|
-
- target: [field, model]
|
|
214
|
-
- type: integer
|
|
215
|
-
- range: [1, 5]
|
|
216
|
-
- default: 1
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
Attributes are classified into 3 tiers:
|
|
220
|
-
|
|
221
|
-
| Tier | `isStandard` | `isRegistered` | Example |
|
|
222
|
-
|------|-------------|----------------|---------|
|
|
223
|
-
| Standard | `true` | — | `@primary`, `@unique`, `@reference` |
|
|
224
|
-
| Registered | — | `true` | `@pii`, `@audit_level` (defined via `::attribute`) |
|
|
225
|
-
| Unregistered | — | — | `@some_unknown_attr` |
|
|
226
|
-
|
|
227
|
-
## Validation
|
|
228
|
-
|
|
229
|
-
The validator checks for semantic errors and style warnings:
|
|
230
|
-
|
|
231
|
-
**Errors:**
|
|
232
|
-
| Code | Description |
|
|
233
|
-
|------|-------------|
|
|
234
|
-
| M3L-E001 | Rollup FK field missing `@reference` |
|
|
235
|
-
| M3L-E002 | Lookup FK field missing `@reference` |
|
|
236
|
-
| M3L-E004 | View references non-existent model |
|
|
237
|
-
| M3L-E005 | Duplicate model/enum name |
|
|
238
|
-
| M3L-E006 | Duplicate field name within model |
|
|
239
|
-
| M3L-E007 | Unresolved parent in inheritance |
|
|
240
|
-
|
|
241
|
-
**Warnings (--strict):**
|
|
242
|
-
| Code | Description |
|
|
243
|
-
|------|-------------|
|
|
244
|
-
| M3L-W001 | Field line exceeds 80 characters |
|
|
245
|
-
| M3L-W002 | Object nesting exceeds 3 levels |
|
|
246
|
-
| M3L-W004 | Lookup chain exceeds 3 hops |
|
|
247
|
-
|
|
248
|
-
## Multi-file Projects
|
|
249
|
-
|
|
250
|
-
Place `.m3l.md` or `.m3l` files in a directory and parse the directory path. The resolver automatically merges all files, resolves inheritance, and detects cross-file references.
|
|
251
|
-
|
|
252
|
-
Optionally, create an `m3l.config.yaml`:
|
|
253
|
-
|
|
254
|
-
```yaml
|
|
255
|
-
name: my-project
|
|
256
|
-
version: 1.0.0
|
|
257
|
-
sources:
|
|
258
|
-
- "models/**/*.m3l.md"
|
|
259
|
-
- "shared/*.m3l"
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
## API Reference
|
|
263
|
-
|
|
264
|
-
### `parse(inputPath: string): Promise<M3LAST>`
|
|
265
|
-
|
|
266
|
-
Parse M3L files from a file or directory into a merged AST.
|
|
267
|
-
|
|
268
|
-
### `parseString(content: string, filename?: string): M3LAST`
|
|
269
|
-
|
|
270
|
-
Parse an M3L content string into an AST.
|
|
271
|
-
|
|
272
|
-
### `validateFiles(inputPath: string, options?): Promise<{ ast, errors, warnings }>`
|
|
273
|
-
|
|
274
|
-
Parse and validate M3L files. Options: `{ strict?: boolean }`.
|
|
275
|
-
|
|
276
|
-
### `STANDARD_ATTRIBUTES: Set<string>`
|
|
277
|
-
|
|
278
|
-
The set of 27 M3L standard attribute names (e.g. `primary`, `unique`, `reference`, `computed`, ...).
|
|
279
|
-
|
|
280
|
-
### Lower-level API
|
|
281
|
-
|
|
282
|
-
```typescript
|
|
283
|
-
import { lex, parseTokens, resolve, validate } from '@iyulab/m3l';
|
|
284
|
-
|
|
285
|
-
const tokens = lex(content, 'file.m3l.md');
|
|
286
|
-
const parsed = parseTokens(tokens, 'file.m3l.md');
|
|
287
|
-
const ast = resolve([parsed]);
|
|
288
|
-
const diagnostics = validate(ast, { strict: true });
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
## Compatibility
|
|
292
|
-
|
|
293
|
-
This TypeScript parser and the [C# parser](../csharp/) produce equivalent AST structures. Both share the same conformance test suite.
|
|
294
|
-
|
|
295
|
-
| | TypeScript | C# |
|
|
296
|
-
|---|---|---|
|
|
297
|
-
| Package | `@iyulab/m3l` | `M3LParser` |
|
|
298
|
-
| Runtime | Node.js 20+ | .NET 8.0+ |
|
|
299
|
-
| AST Version | 1.0 | 1.0 |
|
|
300
|
-
|
|
301
|
-
Version is managed centrally via the root `VERSION` file.
|
|
302
|
-
|
|
303
|
-
## License
|
|
304
|
-
|
|
305
|
-
MIT
|
package/dist/cli.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import type { ValidateOptions } from './types.js';
|
|
3
|
-
/**
|
|
4
|
-
* Run parse command — also used for testing.
|
|
5
|
-
*/
|
|
6
|
-
export declare function runParse(inputPath: string, outputFile?: string): Promise<string>;
|
|
7
|
-
/**
|
|
8
|
-
* Run validate command — also used for testing.
|
|
9
|
-
*/
|
|
10
|
-
export declare function runValidate(inputPath: string, options?: ValidateOptions, format?: string): Promise<string>;
|
|
11
|
-
/**
|
|
12
|
-
* Run CLI with args — for testing.
|
|
13
|
-
*/
|
|
14
|
-
export declare function runCLI(args: string[]): Promise<string>;
|
package/dist/cli.js
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import { writeFileSync } from 'fs';
|
|
4
|
-
import { resolve as resolvePath } from 'path';
|
|
5
|
-
import { readM3LFiles, readProjectConfig } from './reader.js';
|
|
6
|
-
import { parseString as parseFileContent } from './parser.js';
|
|
7
|
-
import { resolve } from './resolver.js';
|
|
8
|
-
import { validate } from './validator.js';
|
|
9
|
-
const program = new Command()
|
|
10
|
-
.name('m3l')
|
|
11
|
-
.description('M3L parser and validator — parse .m3l.md files into JSON AST')
|
|
12
|
-
.version('0.1.0');
|
|
13
|
-
program
|
|
14
|
-
.command('parse [path]')
|
|
15
|
-
.description('Parse M3L files and output JSON AST')
|
|
16
|
-
.option('-o, --output <file>', 'Write output to file instead of stdout')
|
|
17
|
-
.action(async (inputPath, options) => {
|
|
18
|
-
try {
|
|
19
|
-
const output = await runParse(inputPath || '.', options?.output);
|
|
20
|
-
if (!options?.output) {
|
|
21
|
-
process.stdout.write(output + '\n');
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
catch (err) {
|
|
25
|
-
process.stderr.write(`Error: ${err.message}\n`);
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
program
|
|
30
|
-
.command('validate [path]')
|
|
31
|
-
.description('Validate M3L files')
|
|
32
|
-
.option('--strict', 'Enable strict style guidelines')
|
|
33
|
-
.option('--format <format>', 'Output format: human (default) or json', 'human')
|
|
34
|
-
.action(async (inputPath, options) => {
|
|
35
|
-
try {
|
|
36
|
-
const output = await runValidate(inputPath || '.', { strict: options?.strict }, options?.format || 'human');
|
|
37
|
-
process.stdout.write(output + '\n');
|
|
38
|
-
// Exit with error code if there are errors
|
|
39
|
-
if (output.includes('"errors":') || output.match(/\d+ error/)) {
|
|
40
|
-
const errorCount = extractErrorCount(output, options?.format || 'human');
|
|
41
|
-
if (errorCount > 0)
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
process.stderr.write(`Error: ${err.message}\n`);
|
|
47
|
-
process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
// Entry point for direct execution
|
|
51
|
-
const args = process.argv.slice(2);
|
|
52
|
-
if (args.length > 0) {
|
|
53
|
-
program.parse();
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Run parse command — also used for testing.
|
|
57
|
-
*/
|
|
58
|
-
export async function runParse(inputPath, outputFile) {
|
|
59
|
-
const ast = await buildAST(inputPath);
|
|
60
|
-
const json = JSON.stringify(ast, null, 2);
|
|
61
|
-
if (outputFile) {
|
|
62
|
-
writeFileSync(resolvePath(outputFile), json, 'utf-8');
|
|
63
|
-
return `Written to ${outputFile}`;
|
|
64
|
-
}
|
|
65
|
-
return json;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Run validate command — also used for testing.
|
|
69
|
-
*/
|
|
70
|
-
export async function runValidate(inputPath, options = {}, format = 'human') {
|
|
71
|
-
const ast = await buildAST(inputPath);
|
|
72
|
-
const result = validate(ast, options);
|
|
73
|
-
if (format === 'json') {
|
|
74
|
-
return JSON.stringify({
|
|
75
|
-
diagnostics: [...result.errors, ...result.warnings],
|
|
76
|
-
summary: {
|
|
77
|
-
errors: result.errors.length,
|
|
78
|
-
warnings: result.warnings.length,
|
|
79
|
-
files: ast.sources.length,
|
|
80
|
-
},
|
|
81
|
-
}, null, 2);
|
|
82
|
-
}
|
|
83
|
-
// Human-readable format
|
|
84
|
-
return formatHumanDiagnostics(result.errors, result.warnings, ast.sources.length);
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Run CLI with args — for testing.
|
|
88
|
-
*/
|
|
89
|
-
export async function runCLI(args) {
|
|
90
|
-
const cmd = args[0];
|
|
91
|
-
const rest = args.slice(1);
|
|
92
|
-
if (cmd === 'parse') {
|
|
93
|
-
const inputPath = rest.find(a => !a.startsWith('-')) || '.';
|
|
94
|
-
const outputIdx = rest.indexOf('-o');
|
|
95
|
-
const outputFile = outputIdx >= 0 ? rest[outputIdx + 1] : undefined;
|
|
96
|
-
const outputIdx2 = rest.indexOf('--output');
|
|
97
|
-
const outputFile2 = outputIdx2 >= 0 ? rest[outputIdx2 + 1] : undefined;
|
|
98
|
-
return runParse(inputPath, outputFile || outputFile2);
|
|
99
|
-
}
|
|
100
|
-
if (cmd === 'validate') {
|
|
101
|
-
const inputPath = rest.find(a => !a.startsWith('-')) || '.';
|
|
102
|
-
const strict = rest.includes('--strict');
|
|
103
|
-
const formatIdx = rest.indexOf('--format');
|
|
104
|
-
const format = formatIdx >= 0 ? rest[formatIdx + 1] : 'human';
|
|
105
|
-
return runValidate(inputPath, { strict }, format);
|
|
106
|
-
}
|
|
107
|
-
throw new Error(`Unknown command: ${cmd}`);
|
|
108
|
-
}
|
|
109
|
-
// --- Internal ---
|
|
110
|
-
async function buildAST(inputPath) {
|
|
111
|
-
const resolved = resolvePath(inputPath);
|
|
112
|
-
const files = await readM3LFiles(resolved);
|
|
113
|
-
if (files.length === 0) {
|
|
114
|
-
throw new Error(`No .m3l.md files found at: ${inputPath}`);
|
|
115
|
-
}
|
|
116
|
-
const parsedFiles = files.map(f => parseFileContent(f.content, f.path));
|
|
117
|
-
// Try to read project config
|
|
118
|
-
let projectInfo;
|
|
119
|
-
try {
|
|
120
|
-
const config = await readProjectConfig(resolved);
|
|
121
|
-
if (config)
|
|
122
|
-
projectInfo = config;
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
// No config — that's fine
|
|
126
|
-
}
|
|
127
|
-
return resolve(parsedFiles, projectInfo || undefined);
|
|
128
|
-
}
|
|
129
|
-
function formatHumanDiagnostics(errors, warnings, fileCount) {
|
|
130
|
-
const lines = [];
|
|
131
|
-
for (const d of [...errors, ...warnings]) {
|
|
132
|
-
const severity = d.severity === 'error' ? 'error' : 'warning';
|
|
133
|
-
lines.push(`${d.file}:${d.line}:${d.col} ${severity}[${d.code}]: ${d.message}`);
|
|
134
|
-
}
|
|
135
|
-
lines.push(`${errors.length} error${errors.length !== 1 ? 's' : ''}, ${warnings.length} warning${warnings.length !== 1 ? 's' : ''} in ${fileCount} file${fileCount !== 1 ? 's' : ''}.`);
|
|
136
|
-
return lines.join('\n');
|
|
137
|
-
}
|
|
138
|
-
function extractErrorCount(output, format) {
|
|
139
|
-
if (format === 'json') {
|
|
140
|
-
try {
|
|
141
|
-
const parsed = JSON.parse(output);
|
|
142
|
-
return parsed.summary?.errors || 0;
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
return 0;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
const match = output.match(/(\d+) error/);
|
|
149
|
-
return match ? parseInt(match[1], 10) : 0;
|
|
150
|
-
}
|
package/dist/index.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export { lex } from './lexer.js';
|
|
2
|
-
export { parseTokens, parseString as parseFileString, STANDARD_ATTRIBUTES } from './parser.js';
|
|
3
|
-
export { resolve, AST_VERSION, PARSER_VERSION } from './resolver.js';
|
|
4
|
-
export { validate } from './validator.js';
|
|
5
|
-
export { readM3LFiles, readM3LString, readProjectConfig } from './reader.js';
|
|
6
|
-
export type * from './types.js';
|
|
7
|
-
import type { M3LAST, ValidateOptions } from './types.js';
|
|
8
|
-
/**
|
|
9
|
-
* High-level API: Parse M3L files from a path (file or directory) into a merged AST.
|
|
10
|
-
*/
|
|
11
|
-
export declare function parse(inputPath: string): Promise<M3LAST>;
|
|
12
|
-
/**
|
|
13
|
-
* High-level API: Parse M3L content string into AST.
|
|
14
|
-
*/
|
|
15
|
-
export declare function parseString(content: string, filename?: string): M3LAST;
|
|
16
|
-
/**
|
|
17
|
-
* High-level API: Validate M3L files from a path.
|
|
18
|
-
*/
|
|
19
|
-
export declare function validateFiles(inputPath: string, options?: ValidateOptions): Promise<{
|
|
20
|
-
ast: M3LAST;
|
|
21
|
-
errors: import('./types.js').Diagnostic[];
|
|
22
|
-
warnings: import('./types.js').Diagnostic[];
|
|
23
|
-
}>;
|
package/dist/index.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
export { lex } from './lexer.js';
|
|
2
|
-
export { parseTokens, parseString as parseFileString, STANDARD_ATTRIBUTES } from './parser.js';
|
|
3
|
-
export { resolve, AST_VERSION, PARSER_VERSION } from './resolver.js';
|
|
4
|
-
export { validate } from './validator.js';
|
|
5
|
-
export { readM3LFiles, readM3LString, readProjectConfig } from './reader.js';
|
|
6
|
-
import { readM3LFiles, readProjectConfig } from './reader.js';
|
|
7
|
-
import { parseString as parseFileContent } from './parser.js';
|
|
8
|
-
import { resolve } from './resolver.js';
|
|
9
|
-
import { validate as validateAST } from './validator.js';
|
|
10
|
-
import { resolve as resolvePath } from 'path';
|
|
11
|
-
/**
|
|
12
|
-
* High-level API: Parse M3L files from a path (file or directory) into a merged AST.
|
|
13
|
-
*/
|
|
14
|
-
export async function parse(inputPath) {
|
|
15
|
-
const resolved = resolvePath(inputPath);
|
|
16
|
-
const files = await readM3LFiles(resolved);
|
|
17
|
-
if (files.length === 0) {
|
|
18
|
-
throw new Error(`No .m3l.md or .m3l files found at: ${inputPath}`);
|
|
19
|
-
}
|
|
20
|
-
const parsedFiles = files.map(f => parseFileContent(f.content, f.path));
|
|
21
|
-
let projectInfo;
|
|
22
|
-
try {
|
|
23
|
-
const config = await readProjectConfig(resolved);
|
|
24
|
-
if (config)
|
|
25
|
-
projectInfo = config;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// No config
|
|
29
|
-
}
|
|
30
|
-
return resolve(parsedFiles, projectInfo || undefined);
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* High-level API: Parse M3L content string into AST.
|
|
34
|
-
*/
|
|
35
|
-
export function parseString(content, filename = 'inline.m3l.md') {
|
|
36
|
-
const parsed = parseFileContent(content, filename);
|
|
37
|
-
return resolve([parsed]);
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* High-level API: Validate M3L files from a path.
|
|
41
|
-
*/
|
|
42
|
-
export async function validateFiles(inputPath, options) {
|
|
43
|
-
const ast = await parse(inputPath);
|
|
44
|
-
const result = validateAST(ast, options);
|
|
45
|
-
return { ast, ...result };
|
|
46
|
-
}
|
package/dist/lexer.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { Token } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Tokenize M3L markdown content into a sequence of tokens.
|
|
4
|
-
*/
|
|
5
|
-
export declare function lex(content: string, file: string): Token[];
|
|
6
|
-
export declare function parseTypeAndAttrs(rest: string, data: Record<string, unknown>): void;
|