@xdbml/parse 0.6.2 → 0.6.4

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 CHANGED
@@ -48,6 +48,15 @@ try {
48
48
 
49
49
  The package also exports `tokenize`, `LexError`, `Parser`, `SymbolTable`, and the AST types.
50
50
 
51
+ For diagram views (spec §18), `diagramViews(doc)` lists the `DiagramView` declarations, and `diagramViewMembers(doc, name)` returns the entities, database views and sticky notes a diagram view contains, with the Containers, TableGroups and supertype groups a renderer frames or draws around them.
52
+
53
+ ```ts
54
+ import { parse, diagramViewMembers } from '@xdbml/parse';
55
+
56
+ const members = diagramViewMembers(parse(source), 'order_flow');
57
+ console.log(members?.entities); // ['sales.orders', 'billing.invoices', ...]
58
+ ```
59
+
51
60
  ## Learn more
52
61
 
53
62
  - [xdbml.org](https://xdbml.org), and the language [in 5 minutes](https://xdbml.org/learn/)
package/dist/ast.d.ts CHANGED
@@ -38,7 +38,7 @@ export interface ExperimentalDeclaration {
38
38
  features: string[];
39
39
  span: Span;
40
40
  }
41
- export type TopLevelStatement = ProjectDeclaration | ContainerDeclaration | EntityDeclaration | TypeDeclaration | EdgeDeclaration | ViewDeclaration | EnumDeclaration | RefDeclaration | TablePartialDeclaration | TableGroupDeclaration | SupertypeGroupDeclaration | NoteDeclaration | TopLevelRecordsDeclaration | ModuleImportDirective;
41
+ export type TopLevelStatement = ProjectDeclaration | ContainerDeclaration | EntityDeclaration | TypeDeclaration | EdgeDeclaration | ViewDeclaration | EnumDeclaration | RefDeclaration | TablePartialDeclaration | TableGroupDeclaration | SupertypeGroupDeclaration | DiagramViewDeclaration | NoteDeclaration | TopLevelRecordsDeclaration | ModuleImportDirective;
42
42
  export interface ProjectDeclaration {
43
43
  kind: 'ProjectDeclaration';
44
44
  name: string;
@@ -385,6 +385,36 @@ export interface SupertypeGroupMember {
385
385
  settings: Setting[];
386
386
  span: Span;
387
387
  }
388
+ export type DiagramViewCategoryName = 'Tables' | 'Views' | 'Containers' | 'TableGroups' | 'SupertypeGroups' | 'Notes';
389
+ export interface DiagramViewDeclaration {
390
+ kind: 'DiagramViewDeclaration';
391
+ name: string;
392
+ /** Bracket settings: `note` and custom `x_` properties (spec §18.1). */
393
+ settings: Setting[];
394
+ /** `Note:` / `Note { }` elements of the body. `diagramViewNote()` gives the note. */
395
+ notes: NoteBlock[];
396
+ /** True when the whole body is `{ * }`: every element of every category. */
397
+ wildcardBody: boolean;
398
+ categories: DiagramViewCategory[];
399
+ span: Span;
400
+ }
401
+ export interface DiagramViewCategory {
402
+ kind: 'DiagramViewCategory';
403
+ /** Canonical category: `Schemas` reads as `Containers`. */
404
+ category: DiagramViewCategoryName;
405
+ /** The keyword as written, for round trips and the DBML-form check of §18.5. */
406
+ keyword: string;
407
+ /** True when the list holds `*`; names written beside it have no effect. */
408
+ wildcard: boolean;
409
+ items: DiagramViewItem[];
410
+ span: Span;
411
+ }
412
+ export interface DiagramViewItem {
413
+ kind: 'DiagramViewItem';
414
+ /** Name as written: `orders` or `sales.orders`. */
415
+ name: string;
416
+ span: Span;
417
+ }
388
418
  export interface PartialInjection {
389
419
  kind: 'PartialInjection';
390
420
  /** Identifier after the `~`. */
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Diagram views (spec §18, defined in v0.6.3).
3
+ *
4
+ * A diagram view is a named subset of the model's diagram. Its body lists
5
+ * categories of elements; `diagramViewMembers()` turns those lists into the
6
+ * members of §18.2, and `checkDiagramViews()` reports the conditions of
7
+ * §18.6. Both take a document and flatten it, so elements imported from
8
+ * other files count like the ones declared in place (§18.3).
9
+ *
10
+ * Element ids follow the renderer and the supertype group resolver: an
11
+ * entity or database view inside a Container is `container.name`, one at
12
+ * the top level is its name as declared, which for the DBML form
13
+ * `Table core.users` is `core.users` in the implicit Container `core`.
14
+ */
15
+ import type { DiagramViewDeclaration, XDbmlDocument } from './ast.ts';
16
+ import type { Diagnostic } from './name-resolver.ts';
17
+ /** The members of a diagram view (spec §18.2) and what a renderer draws with them (§18.4). */
18
+ export interface DiagramViewMembers {
19
+ /** The diagram view these members belong to. */
20
+ declaration: DiagramViewDeclaration;
21
+ /** Ids of the member entities, in declaration order. */
22
+ entities: string[];
23
+ /** Ids of the member database views, in declaration order. */
24
+ views: string[];
25
+ /** Names of the member sticky notes, in declaration order. */
26
+ notes: string[];
27
+ /** Containers holding at least one member: their frames surround those members only. */
28
+ containers: string[];
29
+ /** TableGroups with at least one member entity, named in the diagram view or not. */
30
+ tableGroups: string[];
31
+ /** Supertype groups whose supertype and at least one subtype are members (§12.9). */
32
+ supertypeGroups: string[];
33
+ }
34
+ /**
35
+ * The note of a diagram view (spec §18.1): its first `Note` in the body,
36
+ * otherwise the `note` setting in its brackets, otherwise undefined.
37
+ */
38
+ export declare function diagramViewNote(view: DiagramViewDeclaration): string | undefined;
39
+ /** Every DiagramView of a document, in declaration order, after module resolution. */
40
+ export declare function diagramViews(doc: XDbmlDocument): DiagramViewDeclaration[];
41
+ /**
42
+ * The members of the diagram view named `name`, or undefined when the
43
+ * document declares none of that name. With two of one name (an error),
44
+ * the first one counts.
45
+ */
46
+ export declare function diagramViewMembers(doc: XDbmlDocument, name: string): DiagramViewMembers | undefined;
47
+ /** The conditions of spec §18.6 that a parse leaves to name resolution. */
48
+ export declare function checkDiagramViews(doc: XDbmlDocument): Diagnostic[];
@@ -0,0 +1,411 @@
1
+ /**
2
+ * Diagram views (spec §18, defined in v0.6.3).
3
+ *
4
+ * A diagram view is a named subset of the model's diagram. Its body lists
5
+ * categories of elements; `diagramViewMembers()` turns those lists into the
6
+ * members of §18.2, and `checkDiagramViews()` reports the conditions of
7
+ * §18.6. Both take a document and flatten it, so elements imported from
8
+ * other files count like the ones declared in place (§18.3).
9
+ *
10
+ * Element ids follow the renderer and the supertype group resolver: an
11
+ * entity or database view inside a Container is `container.name`, one at
12
+ * the top level is its name as declared, which for the DBML form
13
+ * `Table core.users` is `core.users` in the implicit Container `core`.
14
+ */
15
+ import { flatten } from "./module-resolver.js";
16
+ import { resolveSupertypeGroups } from "./supertypes.js";
17
+ /**
18
+ * The note of a diagram view (spec §18.1): its first `Note` in the body,
19
+ * otherwise the `note` setting in its brackets, otherwise undefined.
20
+ */
21
+ export function diagramViewNote(view) {
22
+ if (view.notes.length > 0)
23
+ return view.notes[0].body;
24
+ const s = view.settings.find((x) => x.name === 'note');
25
+ const v = s?.value;
26
+ return v && v.kind === 'StringValue' ? v.value : undefined;
27
+ }
28
+ /** Every DiagramView of a document, in declaration order, after module resolution. */
29
+ export function diagramViews(doc) {
30
+ return flatten(doc).statements.filter((s) => s.kind === 'DiagramViewDeclaration');
31
+ }
32
+ /**
33
+ * The members of the diagram view named `name`, or undefined when the
34
+ * document declares none of that name. With two of one name (an error),
35
+ * the first one counts.
36
+ */
37
+ export function diagramViewMembers(doc, name) {
38
+ const flat = flatten(doc);
39
+ const decl = flat.statements.find((s) => s.kind === 'DiagramViewDeclaration' && s.name === name);
40
+ if (!decl)
41
+ return undefined;
42
+ return computeMembers(decl, buildIndex(flat), flat);
43
+ }
44
+ const key = (kind, id) => `${kind}:${id}`;
45
+ function buildIndex(doc) {
46
+ const elements = [];
47
+ const byKindId = new Map();
48
+ const entitiesOf = new Map();
49
+ const viewsOf = new Map();
50
+ const tableGroupMembers = new Map();
51
+ const aliasCandidates = new Map();
52
+ const takenNames = new Set();
53
+ const add = (kind, id, name, container, topLevel) => {
54
+ if (byKindId.has(key(kind, id)))
55
+ return;
56
+ const el = { kind, id, name, container, topLevel };
57
+ elements.push(el);
58
+ byKindId.set(key(kind, id), el);
59
+ if (container && (kind === 'entity' || kind === 'view')) {
60
+ const map = kind === 'entity' ? entitiesOf : viewsOf;
61
+ const list = map.get(container) ?? [];
62
+ list.push(id);
63
+ map.set(container, list);
64
+ if (!byKindId.has(key('container', container))) {
65
+ add('container', container, container, undefined, true);
66
+ }
67
+ }
68
+ };
69
+ const addBoxed = (kind, decl, container) => {
70
+ let id;
71
+ let bare;
72
+ let owner = container;
73
+ if (container) {
74
+ id = `${container}.${decl.name}`;
75
+ bare = decl.name;
76
+ }
77
+ else {
78
+ id = decl.name;
79
+ const dot = decl.name.lastIndexOf('.');
80
+ bare = dot > 0 ? decl.name.slice(dot + 1) : decl.name;
81
+ owner = dot > 0 ? decl.name.slice(0, dot) : undefined;
82
+ }
83
+ add(kind, id, bare, owner, owner === undefined);
84
+ takenNames.add(bare);
85
+ if (kind === 'entity') {
86
+ const alias = decl.alias;
87
+ if (alias)
88
+ aliasCandidates.set(alias, aliasCandidates.has(alias) ? null : id);
89
+ }
90
+ };
91
+ for (const stmt of doc.statements) {
92
+ switch (stmt.kind) {
93
+ case 'ContainerDeclaration': {
94
+ const c = stmt;
95
+ add('container', c.name, c.name, undefined, true);
96
+ takenNames.add(c.name);
97
+ for (const item of c.body) {
98
+ if (item.kind === 'EntityDeclaration')
99
+ addBoxed('entity', item, c.name);
100
+ else if (item.kind === 'ViewDeclaration')
101
+ addBoxed('view', item, c.name);
102
+ else if (item.kind === 'EdgeDeclaration')
103
+ add('edge', `${c.name}.${item.name}`, item.name, c.name, false);
104
+ }
105
+ break;
106
+ }
107
+ case 'EntityDeclaration':
108
+ addBoxed('entity', stmt);
109
+ break;
110
+ case 'ViewDeclaration':
111
+ addBoxed('view', stmt);
112
+ break;
113
+ case 'EdgeDeclaration':
114
+ add('edge', stmt.name, stmt.name, undefined, true);
115
+ break;
116
+ case 'TableGroupDeclaration':
117
+ add('tablegroup', stmt.name, stmt.name, undefined, true);
118
+ if (!tableGroupMembers.has(stmt.name))
119
+ tableGroupMembers.set(stmt.name, stmt.members);
120
+ break;
121
+ case 'SupertypeGroupDeclaration':
122
+ add('supertypegroup', stmt.name, stmt.name, undefined, true);
123
+ break;
124
+ case 'NoteDeclaration':
125
+ if (stmt.name)
126
+ add('note', stmt.name, stmt.name, undefined, true);
127
+ break;
128
+ default: break;
129
+ }
130
+ }
131
+ // An alias that repeats the name of an entity or a Container, or that two
132
+ // entities declare, names nothing (§7.4).
133
+ const aliases = new Map();
134
+ for (const [alias, id] of aliasCandidates) {
135
+ if (id !== null && !takenNames.has(alias))
136
+ aliases.set(alias, id);
137
+ }
138
+ return { elements, byKindId, entitiesOf, viewsOf, aliases, tableGroupMembers };
139
+ }
140
+ /* --------------------------------------------------------- resolution */
141
+ const CATEGORY_KIND = {
142
+ Tables: 'entity',
143
+ Views: 'view',
144
+ Containers: 'container',
145
+ TableGroups: 'tablegroup',
146
+ SupertypeGroups: 'supertypegroup',
147
+ Notes: 'note',
148
+ };
149
+ const KIND_CATEGORY = {
150
+ entity: 'Tables',
151
+ view: 'Views',
152
+ container: 'Containers',
153
+ tablegroup: 'TableGroups',
154
+ supertypegroup: 'SupertypeGroups',
155
+ note: 'Notes',
156
+ };
157
+ const KIND_LABEL = {
158
+ entity: 'an entity',
159
+ view: 'a database view',
160
+ container: 'a Container',
161
+ tablegroup: 'a TableGroup',
162
+ supertypegroup: 'a supertype group',
163
+ note: 'a sticky note',
164
+ edge: 'an Edge',
165
+ };
166
+ /**
167
+ * Resolve a name of one kind (spec §18.3). A qualified name names the
168
+ * element with that id. An unqualified name names the element declared
169
+ * outside any Container under that name, if any, and otherwise the one
170
+ * element of that name inside a Container. Under Tables, an alias names
171
+ * its entity.
172
+ */
173
+ function resolveName(name, kind, index) {
174
+ const direct = index.byKindId.get(key(kind, name));
175
+ if (direct)
176
+ return { ok: true, id: direct.id };
177
+ if (!name.includes('.') && (kind === 'entity' || kind === 'view')) {
178
+ const top = index.elements.find((e) => e.kind === kind && e.topLevel && e.name === name);
179
+ if (top)
180
+ return { ok: true, id: top.id };
181
+ const inside = index.elements.filter((e) => e.kind === kind && e.name === name);
182
+ if (inside.length === 1)
183
+ return { ok: true, id: inside[0].id };
184
+ if (inside.length > 1)
185
+ return { ok: false, reason: 'ambiguous', candidates: inside.map((e) => e.id) };
186
+ }
187
+ if (kind === 'entity') {
188
+ const aliased = index.aliases.get(name);
189
+ if (aliased)
190
+ return { ok: true, id: aliased };
191
+ }
192
+ for (const other of ['entity', 'view', 'container', 'tablegroup', 'supertypegroup', 'note', 'edge']) {
193
+ if (other === kind)
194
+ continue;
195
+ if (resolveSimple(name, other, index))
196
+ return { ok: false, reason: 'other-kind', kind: other };
197
+ }
198
+ return { ok: false, reason: 'unresolved' };
199
+ }
200
+ function resolveSimple(name, kind, index) {
201
+ if (index.byKindId.has(key(kind, name)))
202
+ return true;
203
+ if (!name.includes('.'))
204
+ return index.elements.some((e) => e.kind === kind && e.name === name);
205
+ return false;
206
+ }
207
+ /** The categories of a diagram view, with a body-level `*` read as `*` in every category (§28.8). */
208
+ function effectiveCategories(decl) {
209
+ const out = new Map();
210
+ if (decl.wildcardBody) {
211
+ for (const name of Object.keys(CATEGORY_KIND)) {
212
+ out.set(name, { wildcard: true, items: [] });
213
+ }
214
+ return out;
215
+ }
216
+ // A category written twice is reported by checkDiagramViews(); its lists combine.
217
+ for (const c of decl.categories) {
218
+ const prev = out.get(c.category);
219
+ out.set(c.category, {
220
+ wildcard: (prev?.wildcard ?? false) || c.wildcard,
221
+ items: [...(prev?.items ?? []), ...c.items],
222
+ });
223
+ }
224
+ return out;
225
+ }
226
+ function computeMembers(decl, index, doc) {
227
+ const cats = effectiveCategories(decl);
228
+ const all = (kind) => index.elements.filter((e) => e.kind === kind).map((e) => e.id);
229
+ const named = (category) => {
230
+ const c = cats.get(category);
231
+ if (!c)
232
+ return new Set();
233
+ if (c.wildcard)
234
+ return new Set(all(CATEGORY_KIND[category]));
235
+ const ids = new Set();
236
+ for (const item of c.items) {
237
+ const r = resolveName(item.name, CATEGORY_KIND[category], index);
238
+ if (r.ok)
239
+ ids.add(r.id);
240
+ }
241
+ return ids;
242
+ };
243
+ const entities = named('Tables');
244
+ const views = named('Views');
245
+ // Rule 2: a listed Container contributes its entities and its database
246
+ // views, each list narrowed separately to the names under Tables or Views
247
+ // when they name at least one of the Container's own.
248
+ for (const container of named('Containers')) {
249
+ const ownEntities = index.entitiesOf.get(container) ?? [];
250
+ if (!ownEntities.some((id) => entities.has(id)))
251
+ ownEntities.forEach((id) => entities.add(id));
252
+ const ownViews = index.viewsOf.get(container) ?? [];
253
+ if (!ownViews.some((id) => views.has(id)))
254
+ ownViews.forEach((id) => views.add(id));
255
+ }
256
+ // Rule 3: each named TableGroup contributes all of its members.
257
+ for (const group of named('TableGroups')) {
258
+ for (const member of index.tableGroupMembers.get(group) ?? []) {
259
+ const r = resolveName(member, 'entity', index);
260
+ if (r.ok)
261
+ entities.add(r.id);
262
+ }
263
+ }
264
+ // Rule 4: each named supertype group contributes its supertype and subtypes.
265
+ const groups = resolveSupertypeGroups(doc);
266
+ const namedGroups = named('SupertypeGroups');
267
+ for (const g of groups) {
268
+ if (!namedGroups.has(g.declaration.name))
269
+ continue;
270
+ if (g.supertype)
271
+ entities.add(g.supertype);
272
+ for (const s of g.subtypes)
273
+ if (s.entity)
274
+ entities.add(s.entity);
275
+ }
276
+ const notes = named('Notes');
277
+ const ordered = (kind, set) => all(kind).filter((id) => set.has(id));
278
+ const memberEntities = ordered('entity', entities);
279
+ const memberViews = ordered('view', views);
280
+ const boxed = new Set([...memberEntities, ...memberViews]);
281
+ const containers = all('container').filter((c) => [...(index.entitiesOf.get(c) ?? []), ...(index.viewsOf.get(c) ?? [])].some((id) => boxed.has(id)));
282
+ const tableGroups = all('tablegroup').filter((g) => (index.tableGroupMembers.get(g) ?? []).some((m) => {
283
+ const r = resolveName(m, 'entity', index);
284
+ return r.ok && entities.has(r.id);
285
+ }));
286
+ const supertypeGroups = groups
287
+ .filter((g) => g.supertype && entities.has(g.supertype) && g.subtypes.some((s) => s.entity && entities.has(s.entity)))
288
+ .map((g) => g.declaration.name);
289
+ return {
290
+ declaration: decl,
291
+ entities: memberEntities,
292
+ views: memberViews,
293
+ notes: ordered('note', notes),
294
+ containers,
295
+ tableGroups,
296
+ supertypeGroups: [...new Set(supertypeGroups)],
297
+ };
298
+ }
299
+ /* --------------------------------------------------------- diagnostics */
300
+ /** The conditions of spec §18.6 that a parse leaves to name resolution. */
301
+ export function checkDiagramViews(doc) {
302
+ const views = doc.statements.filter((s) => s.kind === 'DiagramViewDeclaration');
303
+ if (views.length === 0)
304
+ return [];
305
+ const index = buildIndex(doc);
306
+ const diagnostics = [];
307
+ const declared = !!doc.version;
308
+ const seen = new Set();
309
+ for (const view of views) {
310
+ if (seen.has(view.name)) {
311
+ diagnostics.push({
312
+ severity: 'error',
313
+ code: 'duplicate-diagram-view',
314
+ message: `A diagram view named '${view.name}' is already declared. Diagram view names are unique (spec §18.1).`,
315
+ span: view.span,
316
+ });
317
+ }
318
+ seen.add(view.name);
319
+ // §18.1: the brackets take `note` and custom properties only.
320
+ for (const setting of view.settings) {
321
+ if (setting.name === 'note' || setting.name.startsWith('x_'))
322
+ continue;
323
+ diagnostics.push({
324
+ severity: 'error',
325
+ code: 'unknown-diagram-view-setting',
326
+ message: `Diagram view '${view.name}' writes the setting '${setting.nameSource}'. A diagram view takes ` +
327
+ 'a note and custom x_ properties in its brackets, and no other setting (spec §18.1).',
328
+ span: setting.span,
329
+ });
330
+ }
331
+ // §18.5: a note and the brackets are xDBML extensions.
332
+ if (!declared && (view.settings.length > 0 || view.notes.length > 0)) {
333
+ diagnostics.push({
334
+ severity: 'error',
335
+ code: 'construct-requires-version',
336
+ message: `Diagram view '${view.name}' carries a note or settings, an xDBML extension that needs a ` +
337
+ 'version declaration such as xdbml: 0.6 at the top of the document (spec §18.5).',
338
+ span: (view.settings[0] ?? view.notes[0]).span,
339
+ });
340
+ }
341
+ const written = new Map();
342
+ for (const c of view.categories) {
343
+ const first = written.get(c.category);
344
+ if (first) {
345
+ const same = first.keyword.toLowerCase() === c.keyword.toLowerCase();
346
+ diagnostics.push({
347
+ severity: declared ? 'error' : 'warning',
348
+ code: 'duplicate-diagram-view-category',
349
+ message: same
350
+ ? `Diagram view '${view.name}' writes ${c.keyword} twice. Write each category once (spec §18.1).`
351
+ : `Diagram view '${view.name}' writes both ${first.keyword} and ${c.keyword}, which are one ` +
352
+ 'category: Schemas is the DBML name of Containers (spec §18.1).',
353
+ span: c.span,
354
+ });
355
+ }
356
+ else {
357
+ written.set(c.category, c);
358
+ }
359
+ // §18.5: Containers, Views and SupertypeGroups are xDBML extensions.
360
+ if (!declared && c.keyword.toLowerCase() !== 'schemas'
361
+ && (c.category === 'Containers' || c.category === 'Views' || c.category === 'SupertypeGroups')) {
362
+ diagnostics.push({
363
+ severity: 'error',
364
+ code: 'construct-requires-version',
365
+ message: `The ${c.keyword} category of a diagram view is an xDBML extension and needs a version ` +
366
+ 'declaration such as xdbml: 0.6 at the top of the document' +
367
+ (c.category === 'Containers' ? ', or the DBML name Schemas' : '') + ' (spec §18.5).',
368
+ span: c.span,
369
+ });
370
+ }
371
+ if (c.wildcard)
372
+ continue;
373
+ for (const item of c.items) {
374
+ const r = resolveName(item.name, CATEGORY_KIND[c.category], index);
375
+ if (r.ok)
376
+ continue;
377
+ if (r.reason === 'ambiguous') {
378
+ diagnostics.push({
379
+ severity: 'error',
380
+ code: 'ambiguous-diagram-view-name',
381
+ message: `'${item.name}' under ${c.keyword} matches ${r.candidates.map((x) => `'${x}'`).join(' and ')}, ` +
382
+ 'and none is declared outside a Container: write the qualified name (spec §18.3).',
383
+ span: item.span,
384
+ });
385
+ }
386
+ else if (r.reason === 'other-kind') {
387
+ const right = KIND_CATEGORY[r.kind];
388
+ diagnostics.push({
389
+ severity: 'error',
390
+ code: 'diagram-view-wrong-category',
391
+ message: `'${item.name}' under ${c.keyword} is ${KIND_LABEL[r.kind]}` +
392
+ (right
393
+ ? `: list it under ${right} (spec §18.3).`
394
+ : ', which appears in a diagram view when both of its ends do and is never listed (spec §18.1, §18.4).'),
395
+ span: item.span,
396
+ });
397
+ }
398
+ else {
399
+ diagnostics.push({
400
+ severity: 'error',
401
+ code: 'unresolved-diagram-view-name',
402
+ message: `'${item.name}' under ${c.keyword} matches no ${KIND_LABEL[CATEGORY_KIND[c.category]].replace(/^an? /, '')} ` +
403
+ 'in the project (spec §18.3).',
404
+ span: item.span,
405
+ });
406
+ }
407
+ }
408
+ }
409
+ }
410
+ return diagnostics;
411
+ }
package/dist/index.d.ts CHANGED
@@ -33,6 +33,8 @@ export { classifyModuleSource, isUrlKey, ModuleSourceError, } from './module-res
33
33
  export type { ModuleSource } from './module-resolver.ts';
34
34
  export { resolveNames, SymbolTable } from './name-resolver.ts';
35
35
  export { checkViews, viewSourceQuery } from './views.ts';
36
+ export { checkDiagramViews, diagramViewMembers, diagramViewNote, diagramViews } from './diagram-views.ts';
37
+ export type { DiagramViewMembers } from './diagram-views.ts';
36
38
  export { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from './constraints.ts';
37
39
  export type { CheckConstraint, Constraint, ConstraintSource, KeyConstraint } from './constraints.ts';
38
40
  export { FOREIGN_MASTER_FLAG, checkRelationships, hasForeignMasterFlag, isForeignMaster, pathToString, refChildEndpoint, refParentEndpoint, relationshipType, versionAtLeast, CONSTRAINT_TYPES, V04_RELATIONSHIP_SETTINGS, constraintType, entityNames, isEntityLevelEndpoint, isUndirected, } from './relationships.ts';
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ export { flatten } from "./module-resolver.js";
31
31
  export { classifyModuleSource, isUrlKey, ModuleSourceError, } from "./module-resolver.js";
32
32
  export { resolveNames, SymbolTable } from "./name-resolver.js";
33
33
  export { checkViews, viewSourceQuery } from "./views.js";
34
+ export { checkDiagramViews, diagramViewMembers, diagramViewNote, diagramViews } from "./diagram-views.js";
34
35
  export { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from "./constraints.js";
35
36
  export { FOREIGN_MASTER_FLAG, checkRelationships, hasForeignMasterFlag, isForeignMaster, pathToString, refChildEndpoint, refParentEndpoint, relationshipType, versionAtLeast, CONSTRAINT_TYPES, V04_RELATIONSHIP_SETTINGS, constraintType, entityNames, isEntityLevelEndpoint, isUndirected, } from "./relationships.js";
36
37
  export { SUPERTYPE_GROUP_VALUES, canonicalSupertypeGroupValue, checkSupertypeGroups, resolveSupertypeGroups, subtypeStrategy, supertypeChains, supertypeGroupSettings, } from "./supertypes.js";
@@ -39,6 +39,7 @@ export declare const POLYMORPHISM_KEYWORDS: readonly ["union", "oneof", "anyof",
39
39
  export declare const SCALAR_TYPES: readonly ["tinyint", "smallint", "mediumint", "int", "integer", "bigint", "int32", "int64", "float", "double", "decimal", "dec", "numeric", "real", "bit", "bool", "boolean", "char", "varchar", "varchar2", "nvarchar", "nvarchar2", "nchar", "text", "mediumtext", "longtext", "string", "ntext", "binary", "varbinary", "blob", "mediumblob", "longblob", "tinyblob", "tinytext", "json", "jsonb", "variant", "xml", "date", "time", "datetime", "datetime2", "timestamp", "timestamptz", "year", "uuid", "inet6", "money", "smallmoney", "enum"];
40
40
  export declare const BSON_TYPES: readonly ["objectid", "decimal128", "bindata", "minkey", "maxkey", "symbol", "regex", "long", "double"];
41
41
  export declare const BLOCK_KEYWORDS: readonly ["indexes", "checks", "constraints", "records"];
42
+ export declare const DIAGRAM_VIEW_CATEGORIES: readonly ["tables", "views", "containers", "schemas", "tablegroups", "supertypegroups", "notes"];
42
43
  export declare const TARGET_NATIVE_TYPES: readonly ["number", "binary_float", "binary_double", "clob", "nclob", "bfile", "raw", "rowid", "urowid", "xmltype", "sdo_geometry", "interval", "serial", "smallserial", "bigserial", "serial2", "serial4", "serial8", "int2", "int4", "int8", "float4", "float8", "bytea", "citext", "inet", "cidr", "macaddr", "macaddr8", "tsvector", "tsquery", "hstore", "jsonpath", "timetz", "varbit", "int4range", "int8range", "numrange", "tsrange", "tstzrange", "daterange", "uniqueidentifier", "datetimeoffset", "smalldatetime", "rowversion", "hierarchyid", "sql_variant", "timestamp_ntz", "timestamp_ltz", "timestamp_tz", "vector", "float64", "bignumeric", "bigdecimal", "bytes", "geometry", "geography", "ascii", "varint", "timeuuid", "uint32", "uint64", "sint32", "sint64", "fixed32", "fixed64", "sfixed32", "sfixed64", "uint8", "uint16", "int16", "float32"];
43
44
  export declare const SETTING_FLAGS: readonly ["pk", "primary", "key", "unique", "null", "not", "required", "increment", "inactive", "foreign_master"];
44
45
  export declare const SETTING_KEYS: readonly ["note", "default", "ref", "name", "color", "headercolor", "as", "check", "type", "target", "targets", "database_type", "source", "source_cardinality", "target_cardinality", "min_source", "max_source", "min_target", "max_target", "undirected", "source_role", "target_role", "source_verb", "target_verb", "constraint_type", "discriminator", "supertype", "completeness", "exclusivity", "strategy", "merge", "source_query", "materialized", "refresh_schedule", "refresh_on", "source_database", "storage_options", "pattern", "format", "minlength", "maxlength", "minimum", "maximum", "exclusiveminimum", "exclusivemaximum", "multipleof", "minitems", "maxitems", "uniqueitems", "minproperties", "maxproperties", "synonyms", "business_term", "granularity", "tags", "delete", "update", "indexes", "checks", "constraints", "replication", "location", "default_charset", "cloned_at"];
package/dist/keywords.js CHANGED
@@ -170,6 +170,22 @@ export const BSON_TYPES = [
170
170
  * and only when `{` follows, since a field may carry one of these names.
171
171
  * ----------------------------------------------------------------------- */
172
172
  export const BLOCK_KEYWORDS = ['indexes', 'checks', 'constraints', 'records'];
173
+ /* -------------------------------------------------------------------------
174
+ * DiagramView categories (spec §18.1, v0.6.3)
175
+ *
176
+ * The words that open a category inside a DiagramView body. `schemas` is
177
+ * the DBML name of `containers`. Both highlighters color them only when `{`
178
+ * follows, since an entity or a field may carry one of these names.
179
+ * ----------------------------------------------------------------------- */
180
+ export const DIAGRAM_VIEW_CATEGORIES = [
181
+ 'tables',
182
+ 'views',
183
+ 'containers',
184
+ 'schemas',
185
+ 'tablegroups',
186
+ 'supertypegroups',
187
+ 'notes',
188
+ ];
173
189
  export const TARGET_NATIVE_TYPES = [
174
190
  // Oracle
175
191
  'number', 'binary_float', 'binary_double', 'clob', 'nclob', 'bfile',
@@ -107,6 +107,12 @@ export declare function isUrlKey(s: string): boolean;
107
107
  * union; throws ModuleSourceError for disallowed forms. Pure and sync.
108
108
  */
109
109
  export declare function classifyModuleSource(from: string): ModuleSource;
110
+ /**
111
+ * A local file path with Windows backslashes turned into forward slashes;
112
+ * a URL key is returned unchanged. The parser keys modules, and detects
113
+ * import cycles, on paths in this form.
114
+ */
115
+ export declare function toForwardSlashes(p: string): string;
110
116
  /**
111
117
  * Default recursion depth limit for module resolution. Enough for any
112
118
  * realistic module graph; small enough to bound stack usage on
@@ -78,7 +78,18 @@ function flattenTopLevel(stmt, out) {
78
78
  return;
79
79
  }
80
80
  if (stmt.kind === 'ContainerDeclaration') {
81
- out.push(flattenContainer(stmt));
81
+ // A `reuse *` inside a Container body can clone a DiagramView into it.
82
+ // A DiagramView is top-level only (spec §18.1), so it moves out to the
83
+ // top level, after its Container.
84
+ const flat = flattenContainer(stmt);
85
+ const hoisted = flat.body.filter((b) => b.kind === 'DiagramViewDeclaration');
86
+ if (hoisted.length === 0) {
87
+ out.push(flat);
88
+ return;
89
+ }
90
+ out.push({ ...flat, body: flat.body.filter((b) => !hoisted.includes(b)) });
91
+ for (const view of hoisted)
92
+ out.push(view);
82
93
  return;
83
94
  }
84
95
  // All other top-level statement kinds pass through unchanged.
@@ -376,8 +387,13 @@ function findImportTarget(item, doc) {
376
387
  }
377
388
  return undefined;
378
389
  }
379
- // View / DiagramView: top-level OR container-scoped.
380
- if (item.elementType === 'view' || item.elementType === 'diagramview') {
390
+ // DiagramView (spec §18): top-level only, bare name. Its names resolve
391
+ // in the importing document, like the members of a TableGroup.
392
+ if (item.elementType === 'diagramview') {
393
+ return doc.statements.find((s) => s.kind === 'DiagramViewDeclaration' && s.name === path);
394
+ }
395
+ // View: top-level OR container-scoped.
396
+ if (item.elementType === 'view') {
381
397
  if (segments.length === 1) {
382
398
  return doc.statements.find((s) => s.kind === 'ViewDeclaration' && s.name === segments[0]);
383
399
  }
@@ -614,6 +630,8 @@ function applyAlias(stmt, item) {
614
630
  return { ...stmt, name: item.alias };
615
631
  case 'SupertypeGroupDeclaration':
616
632
  return { ...stmt, name: item.alias };
633
+ case 'DiagramViewDeclaration':
634
+ return { ...stmt, name: item.alias };
617
635
  case 'TablePartialDeclaration':
618
636
  return { ...stmt, name: item.alias };
619
637
  case 'NoteDeclaration':
@@ -713,8 +731,11 @@ export function classifyModuleSource(from) {
713
731
  * exactly as in v0.2.
714
732
  *
715
733
  * Uses pure string / WHATWG-URL manipulation (no node:path) so the same
716
- * code runs in Node and the browser. Forward slashes only; Windows paths
717
- * with backslashes should be normalized before reaching the parser.
734
+ * code runs in Node and the browser. A local path may use backslashes, as
735
+ * a native Windows path does (`C:\\repo\\main.xdbml`): they read as forward
736
+ * slashes, and the key handed to `readFile` uses forward slashes, which
737
+ * Node's fs accepts on Windows. Until v0.6.4 a backslash path lost its
738
+ * directory, so './lib' was read from the working directory instead.
718
739
  */
719
740
  function resolveModulePath(fromClause, importerPath) {
720
741
  const source = classifyModuleSource(fromClause);
@@ -731,21 +752,30 @@ function resolveModulePath(fromClause, importerPath) {
731
752
  // also what we want for the cycle-detection / de-duplication key.
732
753
  return new URL(rel, importerPath).href;
733
754
  }
734
- // Local relative source (unchanged v0.2 behavior).
735
- let withExt = source.from;
755
+ // Local relative source (v0.2 behavior, with Windows separators read as
756
+ // forward slashes from v0.6.4).
757
+ let withExt = toForwardSlashes(source.from);
736
758
  if (!withExt.endsWith('.xdbml')) {
737
759
  withExt = `${withExt}.xdbml`;
738
760
  }
739
- // Absolute path: return as-is.
740
- if (withExt.startsWith('/'))
761
+ // Absolute path, POSIX or with a Windows drive letter: return as-is.
762
+ if (withExt.startsWith('/') || /^[A-Za-z]:\//.test(withExt))
741
763
  return withExt;
742
764
  // No importer context: return as-is (resolver handles it).
743
765
  if (!importerPath)
744
766
  return withExt;
745
767
  // Resolve relative to importer's directory.
746
- const importerDir = posixDirname(importerPath);
768
+ const importerDir = posixDirname(toForwardSlashes(importerPath));
747
769
  return posixJoin(importerDir, withExt);
748
770
  }
771
+ /**
772
+ * A local file path with Windows backslashes turned into forward slashes;
773
+ * a URL key is returned unchanged. The parser keys modules, and detects
774
+ * import cycles, on paths in this form.
775
+ */
776
+ export function toForwardSlashes(p) {
777
+ return isUrlKey(p) ? p : p.replace(/\\/g, '/');
778
+ }
749
779
  function posixDirname(p) {
750
780
  const idx = p.lastIndexOf('/');
751
781
  if (idx < 0)
package/dist/monarch.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * dependency on consumers that just want to parse. The Monaco types
19
19
  * are structurally compatible -- the consumer casts at the boundary.
20
20
  */
21
- import { CONTAINER_KEYWORDS, ENTITY_KEYWORDS, DECLARATION_KEYWORDS, STRUCTURAL_TYPE_KEYWORDS, POLYMORPHISM_KEYWORDS, SCALAR_TYPES, BSON_TYPES, TARGET_NATIVE_TYPES, SETTING_FLAGS, SETTING_KEYS, GRANULARITY_VALUES, } from "./keywords.js";
21
+ import { CONTAINER_KEYWORDS, ENTITY_KEYWORDS, DECLARATION_KEYWORDS, STRUCTURAL_TYPE_KEYWORDS, POLYMORPHISM_KEYWORDS, SCALAR_TYPES, BSON_TYPES, TARGET_NATIVE_TYPES, SETTING_FLAGS, SETTING_KEYS, GRANULARITY_VALUES, DIAGRAM_VIEW_CATEGORIES, } from "./keywords.js";
22
22
  /* -------------------------------------------------------------------------
23
23
  * Language configuration
24
24
  * ----------------------------------------------------------------------- */
@@ -160,6 +160,9 @@ export const xdbmlMonarchTokensProvider = {
160
160
  // or at the top level before a name and '{'. Anywhere else it names a
161
161
  // field, so it takes the identifier color like any other field name.
162
162
  [/note(?![\w$])(?!\s*[:{])(?!\s+(?:[A-Za-z_][\w$]*|"[^"]*")\s*\{)/, 'identifier'],
163
+ // Spec §18.1 (v0.6.3): a DiagramView category keyword opens its list
164
+ // only before '{'. Elsewhere the same word names an entity or a field.
165
+ [new RegExp(`(?:${DIAGRAM_VIEW_CATEGORIES.join('|')})(?![\\w$])(?=\\s*\\{)`), 'keyword.declaration'],
163
166
  // Identifiers and keyword recognition.
164
167
  // The parser is the authority on keyword vs identifier disambiguation;
165
168
  // Monarch does coarse highlighting based on lowercase comparison.
@@ -67,7 +67,7 @@ export interface SymbolEntry {
67
67
  * Stable diagnostic code. Tooling can match on these to filter or style
68
68
  * messages without parsing the human-readable text.
69
69
  */
70
- export type DiagnosticCode = 'duplicate-declaration' | 'name-collision' | 'container-target-missing' | 'container-target-not-in-project' | 'unresolved-type' | 'possible-type-typo' | 'unresolved-key-field' | 'unresolved-index-field' | 'key-path-crosses-collection' | 'invalid-key-flags' | 'duplicate-primary-key' | 'duplicate-unique-key' | 'null-in-primary-key' | 'duplicate-constraints-block' | 'ref-target-not-key' | 'unresolved-entity' | 'unresolved-field' | 'unresolved-partial' | 'unresolved-tablegroup-member' | 'unresolved-records-entity' | 'unresolved-records-column' | 'empty-import' | 'invalid-nested-path' | 'foreign-master-composite' | 'foreign-master-duplicate-child' | 'foreign-master-without-ref' | 'construct-requires-version' | 'ambiguous-ref-endpoint' | 'invalid-constraint-type' | 'constraint-type-on-foreign-master' | 'invalid-undirected' | 'entity-level-many-to-many' | 'missing-supertype' | 'unresolved-supertype-group-member' | 'invalid-supertype-group-value' | 'duplicate-subtype' | 'supertype-is-subtype' | 'subtype-in-multiple-groups' | 'supertype-cycle' | 'supertype-attribute-redeclared' | 'discriminator-on-overlapping-group' | 'merge-without-roll-up' | 'empty-supertype-group' | 'source-query-in-settings' | 'duplicate-source-query';
70
+ export type DiagnosticCode = 'duplicate-declaration' | 'name-collision' | 'container-target-missing' | 'container-target-not-in-project' | 'unresolved-type' | 'possible-type-typo' | 'unresolved-key-field' | 'unresolved-index-field' | 'key-path-crosses-collection' | 'invalid-key-flags' | 'duplicate-primary-key' | 'duplicate-unique-key' | 'null-in-primary-key' | 'duplicate-constraints-block' | 'ref-target-not-key' | 'unresolved-entity' | 'unresolved-field' | 'unresolved-partial' | 'unresolved-tablegroup-member' | 'unresolved-records-entity' | 'unresolved-records-column' | 'empty-import' | 'invalid-nested-path' | 'foreign-master-composite' | 'foreign-master-duplicate-child' | 'foreign-master-without-ref' | 'construct-requires-version' | 'ambiguous-ref-endpoint' | 'invalid-constraint-type' | 'constraint-type-on-foreign-master' | 'invalid-undirected' | 'entity-level-many-to-many' | 'missing-supertype' | 'unresolved-supertype-group-member' | 'invalid-supertype-group-value' | 'duplicate-subtype' | 'supertype-is-subtype' | 'subtype-in-multiple-groups' | 'supertype-cycle' | 'supertype-attribute-redeclared' | 'discriminator-on-overlapping-group' | 'merge-without-roll-up' | 'empty-supertype-group' | 'source-query-in-settings' | 'duplicate-source-query' | 'duplicate-diagram-view' | 'duplicate-diagram-view-category' | 'unresolved-diagram-view-name' | 'ambiguous-diagram-view-name' | 'diagram-view-wrong-category' | 'unknown-diagram-view-setting' | 'invalid-tuple-positions' | 'named-type-shadows-builtin';
71
71
  /**
72
72
  * A single resolution diagnostic. Severity is currently always `error`,
73
73
  * but the field is included to leave room for future warnings (e.g.,
@@ -45,6 +45,7 @@ import { checkSupertypeGroups } from "./supertypes.js";
45
45
  import { checkConstraints, checkTargets } from "./constraints.js";
46
46
  import { versionAtLeast } from "./relationships.js";
47
47
  import { checkViews } from "./views.js";
48
+ import { checkDiagramViews } from "./diagram-views.js";
48
49
  /**
49
50
  * Read-only handle on the collected symbol table.
50
51
  *
@@ -236,6 +237,30 @@ export function resolveNames(doc) {
236
237
  diagnostics.push(...checkTargets(flat));
237
238
  // Pass 6: view source queries (spec §14.7).
238
239
  diagnostics.push(...checkViews(flat));
240
+ // Pass 7: diagram views (spec §18.6).
241
+ diagnostics.push(...checkDiagramViews(flat));
242
+ // Pass 8: Types under the name of a built-in type (spec §15.2).
243
+ for (const t of flat.statements) {
244
+ if (t.kind !== 'TypeDeclaration' || !BUILTIN_TYPES.has(t.name.toLowerCase()))
245
+ continue;
246
+ {
247
+ diagnostics.push({
248
+ severity: 'error',
249
+ code: 'named-type-shadows-builtin',
250
+ message: `Type '${t.name}' takes the name of a built-in type. Built-in types take precedence, so a field ` +
251
+ `typed ${t.name.toLowerCase()} keeps the built-in type: rename the Type (spec §15.2).`,
252
+ span: t.span,
253
+ });
254
+ }
255
+ }
256
+ // The parser checked neither rule before v0.6.4, so a document declaring
257
+ // a version earlier than 0.6 draws a warning and stays valid (§8.6, §15.2).
258
+ if (!versionAtLeast(flat, '0.6')) {
259
+ for (const d of diagnostics) {
260
+ if (d.code === 'invalid-tuple-positions' || d.code === 'named-type-shadows-builtin')
261
+ d.severity = 'warning';
262
+ }
263
+ }
239
264
  return { diagnostics, symbols };
240
265
  }
241
266
  /* -------------------------------------------------------------------------
@@ -566,11 +591,24 @@ function resolveTypeExpression(expr, symbols, diagnostics) {
566
591
  case 'SetType':
567
592
  resolveTypeExpression(expr.elementType, symbols, diagnostics);
568
593
  return;
569
- case 'TupleType':
594
+ case 'TupleType': {
595
+ // Spec §8.6: positions form a contiguous range from 0, each once.
596
+ const positions = expr.elements.map((e) => e.position);
597
+ const sorted = [...positions].sort((a, b) => a - b);
598
+ if (sorted.some((p, i) => p !== i)) {
599
+ diagnostics.push({
600
+ severity: 'error',
601
+ code: 'invalid-tuple-positions',
602
+ message: `Tuple positions ${positions.map((p) => `[${p}]`).join(', ')} do not run from [0] to ` +
603
+ `[${positions.length - 1}], each once: a tuple numbers its elements from 0 without a gap (spec §8.6).`,
604
+ span: expr.span,
605
+ });
606
+ }
570
607
  for (const elem of expr.elements) {
571
608
  resolveTypeExpression(elem.type, symbols, diagnostics);
572
609
  }
573
610
  return;
611
+ }
574
612
  case 'JsonType':
575
613
  // No nested type expressions.
576
614
  return;
package/dist/parser.d.ts CHANGED
@@ -26,7 +26,7 @@ export declare class ParseError extends Error {
26
26
  * declaring a later version is refused (spec 4.1) rather than parsed with
27
27
  * semantics it does not have.
28
28
  */
29
- export declare const SUPPORTED_XDBML_VERSION = "0.6.2";
29
+ export declare const SUPPORTED_XDBML_VERSION = "0.6.4";
30
30
  /** Compare dotted version strings numerically: -1, 0 or 1. */
31
31
  export declare function compareVersions(a: string, b: string): number;
32
32
  export declare class Parser {
@@ -40,6 +40,8 @@ export declare class Parser {
40
40
  * 1-arg form).
41
41
  */
42
42
  private options;
43
+ /** The version the document declares, once read; undefined without a declaration. */
44
+ private declaredVersion;
43
45
  /**
44
46
  * The set of file paths currently being parsed in the resolution chain.
45
47
  * Used for cycle detection: when resolving a directive whose `from` path
@@ -345,6 +347,21 @@ export declare class Parser {
345
347
  * rather than stopping the parse.
346
348
  */
347
349
  private parseSupertypeGroup;
350
+ /**
351
+ * `DiagramView <name> { <Category> { <names> | * } ... }`, or
352
+ * `DiagramView <name> { * }` for every element of every category.
353
+ * Category keywords are case-insensitive; `Schemas` reads as `Containers`.
354
+ * Lists follow spec §3.9: whitespace between names, with commas and
355
+ * semicolons allowed and meaningless. Names stay unresolved here; the
356
+ * checks of §18.6 run after parsing, in `checkDiagramViews()`, so a bad
357
+ * name gets a located diagnostic and the rest of the diagram still draws.
358
+ * Two conditions stop the parse because they leave no category to read:
359
+ * an unknown category, `Edges` included, and a body-level `*` with a
360
+ * category beside it. A note may sit in the brackets or in the body
361
+ * (spec §18.1, v0.6.3).
362
+ */
363
+ private parseDiagramView;
364
+ private parseDiagramViewCategory;
348
365
  private parseIndexes;
349
366
  private parseIndexEntry;
350
367
  private parseIndexComponent;
package/dist/parser.js CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { markPrimaryKeyNotNull } from "./constraints.js";
13
13
  import { TokenKind, tokenize, } from "./lexer.js";
14
- import { resolveImport, classifyModuleSource, ModuleSourceError } from "./module-resolver.js";
14
+ import { resolveImport, classifyModuleSource, ModuleSourceError, toForwardSlashes } from "./module-resolver.js";
15
15
  export class ParseError extends Error {
16
16
  position;
17
17
  /**
@@ -32,7 +32,7 @@ export class ParseError extends Error {
32
32
  * declaring a later version is refused (spec 4.1) rather than parsed with
33
33
  * semantics it does not have.
34
34
  */
35
- export const SUPPORTED_XDBML_VERSION = '0.6.2';
35
+ export const SUPPORTED_XDBML_VERSION = '0.6.4';
36
36
  /** Compare dotted version strings numerically: -1, 0 or 1. */
37
37
  export function compareVersions(a, b) {
38
38
  const pa = a.split('.').map((n) => Number(n) || 0);
@@ -74,6 +74,19 @@ const IMPORT_ELEMENT_TYPES = new Set([
74
74
  'type', 'edge', 'view', 'diagramview',
75
75
  'field', 'supertypegroup',
76
76
  ]);
77
+ /**
78
+ * DiagramView categories (spec §18.1), by lowercased keyword. `Schemas`, the
79
+ * DBML name, reads as `Containers`, after the `Schema` keyword (§6.2).
80
+ */
81
+ const DIAGRAM_VIEW_CATEGORIES = {
82
+ tables: 'Tables',
83
+ views: 'Views',
84
+ containers: 'Containers',
85
+ schemas: 'Containers',
86
+ tablegroups: 'TableGroups',
87
+ supertypegroups: 'SupertypeGroups',
88
+ notes: 'Notes',
89
+ };
77
90
  const STRUCTURAL_TYPE_KEYWORDS = new Set([
78
91
  'object', 'struct', 'record', 'array', 'list', 'map', 'dict', 'dictionary',
79
92
  'set', 'union', 'oneof', 'anyof', 'allof', 'json', 'jsonb', 'variant',
@@ -121,6 +134,8 @@ export class Parser {
121
134
  * 1-arg form).
122
135
  */
123
136
  options;
137
+ /** The version the document declares, once read; undefined without a declaration. */
138
+ declaredVersion;
124
139
  /**
125
140
  * The set of file paths currently being parsed in the resolution chain.
126
141
  * Used for cycle detection: when resolving a directive whose `from` path
@@ -261,6 +276,7 @@ export class Parser {
261
276
  this.advance(); // .
262
277
  version += `.${this.advance().text}`;
263
278
  }
279
+ this.declaredVersion = version;
264
280
  if (compareVersions(version, SUPPORTED_XDBML_VERSION) > 0) {
265
281
  throw new ParseError(`This document declares 'xdbml: ${version}', which is newer than the ` +
266
282
  `latest version this parser supports (${SUPPORTED_XDBML_VERSION}). ` +
@@ -320,6 +336,8 @@ export class Parser {
320
336
  return this.parseTableGroup();
321
337
  if (k === 'supertypegroup')
322
338
  return this.parseSupertypeGroup();
339
+ if (k === 'diagramview')
340
+ return this.parseDiagramView();
323
341
  if (k === 'note')
324
342
  return this.parseNoteDeclaration();
325
343
  if (k === 'records')
@@ -471,6 +489,10 @@ export class Parser {
471
489
  else if (k === 'use' || k === 'reuse') {
472
490
  body.push(this.parseModuleDirective('container-body'));
473
491
  }
492
+ else if (k === 'diagramview') {
493
+ throw new ParseError(`A DiagramView is a top-level declaration and never appears in a Container (spec §18.1). ` +
494
+ `Move it out of Container '${name}'.`, t.start);
495
+ }
474
496
  else {
475
497
  // Unknown line; tolerate as no-op rather than fail the whole parse.
476
498
  throw new ParseError(`Unexpected token in Container body: ${t.kind} ${JSON.stringify(t.text)}`, t.start);
@@ -714,6 +736,12 @@ export class Parser {
714
736
  const start = this.peek().start;
715
737
  const modeTok = this.advance(); // 'use' or 'reuse'
716
738
  const mode = modeTok.text.toLowerCase();
739
+ // Appendix D, item 3: the module system belongs to the v0.2 feature set,
740
+ // so a document declaring xdbml: 0.1 cannot use it (checked from v0.6.4).
741
+ if (this.declaredVersion && compareVersions(this.declaredVersion, '0.2') < 0) {
742
+ throw new ParseError(`The module system (use and reuse) needs xdbml: 0.2 or later; this document declares ` +
743
+ `xdbml: ${this.declaredVersion} (spec §27, Appendix D).`, modeTok.start);
744
+ }
717
745
  // Import spec: '*' or '{ ... }'
718
746
  let spec;
719
747
  if (this.check(TokenKind.Star)) {
@@ -723,16 +751,15 @@ export class Parser {
723
751
  else if (this.check(TokenKind.LBrace)) {
724
752
  this.advance();
725
753
  const items = [];
726
- // Skip leading whitespace/newlines (already handled by lexer).
727
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
728
- items.push(this.parseImportItem(context));
729
- if (this.match(TokenKind.Comma)) {
730
- // Tolerate trailing comma before the closing brace.
731
- continue;
732
- }
733
- else {
754
+ // A list body (spec §3.9, §27.2, v0.6.4): items one per line, or
755
+ // separated by commas, with commas and semicolons optional anywhere
756
+ // between items. Until v0.6.4 the parser required a comma between
757
+ // two items, against the multi-line form §27.2 shows.
758
+ for (;;) {
759
+ this.skipListSeparators();
760
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
734
761
  break;
735
- }
762
+ items.push(this.parseImportItem(context));
736
763
  }
737
764
  this.expect(TokenKind.RBrace, "Expected '}' closing import item list");
738
765
  if (items.length === 0) {
@@ -860,6 +887,11 @@ export class Parser {
860
887
  throw new ParseError(`Unknown import element type '${elemTok.text}'. ` +
861
888
  `Expected one of: ${Array.from(IMPORT_ELEMENT_TYPES).join(', ')}.`, elemTok.start);
862
889
  }
890
+ if (elementType === 'diagramview' && context !== 'file-scope') {
891
+ // Spec §18.1, §27.5: a DiagramView is a top-level declaration, so its
892
+ // import goes at file scope, where the declaration itself would.
893
+ throw new ParseError('A DiagramView is a top-level declaration: import it at file scope, not inside a Container body (spec §18.1, §27.5).', elemTok.start);
894
+ }
863
895
  if (elementType === 'field' && context !== 'file-scope') {
864
896
  // Spec §27.8: field imports must appear at file scope. Inside a
865
897
  // Container body, the field's eventual placement (as a Named Type)
@@ -1951,6 +1983,120 @@ export class Parser {
1951
1983
  span: this.spanFrom(start),
1952
1984
  };
1953
1985
  }
1986
+ /* ----- DiagramView (spec §18, defined in v0.6.3) ----- */
1987
+ /**
1988
+ * `DiagramView <name> { <Category> { <names> | * } ... }`, or
1989
+ * `DiagramView <name> { * }` for every element of every category.
1990
+ * Category keywords are case-insensitive; `Schemas` reads as `Containers`.
1991
+ * Lists follow spec §3.9: whitespace between names, with commas and
1992
+ * semicolons allowed and meaningless. Names stay unresolved here; the
1993
+ * checks of §18.6 run after parsing, in `checkDiagramViews()`, so a bad
1994
+ * name gets a located diagnostic and the rest of the diagram still draws.
1995
+ * Two conditions stop the parse because they leave no category to read:
1996
+ * an unknown category, `Edges` included, and a body-level `*` with a
1997
+ * category beside it. A note may sit in the brackets or in the body
1998
+ * (spec §18.1, v0.6.3).
1999
+ */
2000
+ parseDiagramView() {
2001
+ const start = this.peek().start;
2002
+ this.advance(); // DiagramView
2003
+ const nameTok = this.peek();
2004
+ if (nameTok.kind !== TokenKind.Identifier && nameTok.kind !== TokenKind.QuotedIdentifier) {
2005
+ throw new ParseError('Expected a name after DiagramView (spec §18.1)', nameTok.start);
2006
+ }
2007
+ const name = this.parseIdentLikeName('DiagramView name');
2008
+ // Spec §18.1 (v0.6.3): `note` and custom properties in the brackets.
2009
+ // Other settings are reported by checkDiagramViews(), not here.
2010
+ const settings = this.maybeSettingsBlock();
2011
+ this.expect(TokenKind.LBrace, `Expected '{' after DiagramView ${name}`);
2012
+ const categories = [];
2013
+ const notes = [];
2014
+ let wildcardBody = false;
2015
+ for (;;) {
2016
+ this.skipListSeparators();
2017
+ const t = this.peek();
2018
+ if (t.kind === TokenKind.RBrace || t.kind === TokenKind.EOF)
2019
+ break;
2020
+ if (this.atNoteElement()) {
2021
+ notes.push(this.parseNoteBlockOrSetting());
2022
+ continue;
2023
+ }
2024
+ if (t.kind === TokenKind.Star) {
2025
+ if (categories.length > 0 || wildcardBody) {
2026
+ throw new ParseError(`A '*' body lists every element of every category, so no category goes beside it in DiagramView ${name} (spec §18.1)`, t.start);
2027
+ }
2028
+ this.advance();
2029
+ wildcardBody = true;
2030
+ continue;
2031
+ }
2032
+ const k = kw(t);
2033
+ const category = k ? DIAGRAM_VIEW_CATEGORIES[k] : undefined;
2034
+ if (!category) {
2035
+ if (k === 'edges') {
2036
+ throw new ParseError('Edges is not a DiagramView category: a Ref or an Edge appears in a diagram view when both ' +
2037
+ 'of its ends do, so there is nothing to list (spec §18.1, §18.4)', t.start);
2038
+ }
2039
+ throw new ParseError(`Unknown DiagramView category ${JSON.stringify(t.text)}. Expected Tables, Views, Containers ` +
2040
+ '(or Schemas), TableGroups, SupertypeGroups or Notes, each followed by { names }, or a Note (spec §18.1)', t.start);
2041
+ }
2042
+ if (wildcardBody) {
2043
+ throw new ParseError(`A '*' body lists every element of every category, so no category goes beside it in DiagramView ${name} (spec §18.1)`, t.start);
2044
+ }
2045
+ categories.push(this.parseDiagramViewCategory(category));
2046
+ }
2047
+ this.expect(TokenKind.RBrace, "Expected '}' closing DiagramView");
2048
+ return {
2049
+ kind: 'DiagramViewDeclaration',
2050
+ name,
2051
+ settings,
2052
+ notes,
2053
+ wildcardBody,
2054
+ categories,
2055
+ span: this.spanFrom(start),
2056
+ };
2057
+ }
2058
+ parseDiagramViewCategory(category) {
2059
+ const start = this.peek().start;
2060
+ const keywordTok = this.advance();
2061
+ this.expect(TokenKind.LBrace, `Expected '{' after ${keywordTok.text} in a DiagramView`);
2062
+ const items = [];
2063
+ let wildcard = false;
2064
+ for (;;) {
2065
+ this.skipListSeparators();
2066
+ const t = this.peek();
2067
+ if (t.kind === TokenKind.RBrace || t.kind === TokenKind.EOF)
2068
+ break;
2069
+ if (t.kind === TokenKind.Star) {
2070
+ this.advance();
2071
+ wildcard = true;
2072
+ continue;
2073
+ }
2074
+ if (t.kind !== TokenKind.Identifier && t.kind !== TokenKind.QuotedIdentifier) {
2075
+ throw new ParseError(`Expected a name or '*' in ${keywordTok.text} { }, got ${t.kind} ${JSON.stringify(t.text)}`, t.start);
2076
+ }
2077
+ this.advance();
2078
+ let n = t.kind === TokenKind.QuotedIdentifier ? (t.value ?? '') : t.text;
2079
+ while (this.check(TokenKind.Dot)) {
2080
+ this.advance();
2081
+ const next = this.peek();
2082
+ if (next.kind !== TokenKind.Identifier && next.kind !== TokenKind.QuotedIdentifier) {
2083
+ throw new ParseError(`Expected identifier after '.' in ${keywordTok.text} { }`, next.start);
2084
+ }
2085
+ this.advance();
2086
+ n += `.${next.kind === TokenKind.QuotedIdentifier ? (next.value ?? '') : next.text}`;
2087
+ }
2088
+ items.push({ kind: 'DiagramViewItem', name: n, span: this.spanFrom(t.start) });
2089
+ }
2090
+ this.expect(TokenKind.RBrace, `Expected '}' closing ${keywordTok.text}`);
2091
+ return {
2092
+ kind: 'DiagramViewCategory',
2093
+ category,
2094
+ keyword: keywordTok.text,
2095
+ wildcard,
2096
+ items,
2097
+ span: this.spanFrom(start),
2098
+ };
2099
+ }
1954
2100
  /* ----- Indexes ----- */
1955
2101
  parseIndexes() {
1956
2102
  const start = this.peek().start;
@@ -2386,7 +2532,7 @@ export function parse(source, options = {}) {
2386
2532
  // the outer level too). If no filePath is provided, the stack is empty.
2387
2533
  const initialStack = new Set();
2388
2534
  if (options.filePath)
2389
- initialStack.add(options.filePath);
2535
+ initialStack.add(toForwardSlashes(options.filePath));
2390
2536
  const doc = new Parser(tokens, options, initialStack, 0).parseDocument();
2391
2537
  applyEntityAliases(doc);
2392
2538
  return doc;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdbml/parse",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Parser for xDBML (eXtended Database Markup Language), a strict superset of DBML 3.13.6: tokenizer, parser, module resolver, and name resolver.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hackolade",