@nowline/core 0.2.0

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.
Files changed (69) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +84 -0
  3. package/dist/generated/ast.d.ts +958 -0
  4. package/dist/generated/ast.d.ts.map +1 -0
  5. package/dist/generated/ast.js +795 -0
  6. package/dist/generated/ast.js.map +1 -0
  7. package/dist/generated/grammar.d.ts +7 -0
  8. package/dist/generated/grammar.d.ts.map +1 -0
  9. package/dist/generated/grammar.js +2509 -0
  10. package/dist/generated/grammar.js.map +1 -0
  11. package/dist/generated/module.d.ts +14 -0
  12. package/dist/generated/module.d.ts.map +1 -0
  13. package/dist/generated/module.js +21 -0
  14. package/dist/generated/module.js.map +1 -0
  15. package/dist/i18n/codes.d.ts +3 -0
  16. package/dist/i18n/codes.d.ts.map +1 -0
  17. package/dist/i18n/codes.js +54 -0
  18. package/dist/i18n/codes.js.map +1 -0
  19. package/dist/i18n/index.d.ts +17 -0
  20. package/dist/i18n/index.d.ts.map +1 -0
  21. package/dist/i18n/index.js +82 -0
  22. package/dist/i18n/index.js.map +1 -0
  23. package/dist/i18n/messages.en.d.ts +96 -0
  24. package/dist/i18n/messages.en.d.ts.map +1 -0
  25. package/dist/i18n/messages.en.js +57 -0
  26. package/dist/i18n/messages.en.js.map +1 -0
  27. package/dist/i18n/messages.fr-CA.d.ts +3 -0
  28. package/dist/i18n/messages.fr-CA.d.ts.map +1 -0
  29. package/dist/i18n/messages.fr-CA.js +11 -0
  30. package/dist/i18n/messages.fr-CA.js.map +1 -0
  31. package/dist/i18n/messages.fr-FR.d.ts +3 -0
  32. package/dist/i18n/messages.fr-FR.d.ts.map +1 -0
  33. package/dist/i18n/messages.fr-FR.js +11 -0
  34. package/dist/i18n/messages.fr-FR.js.map +1 -0
  35. package/dist/i18n/messages.fr.d.ts +3 -0
  36. package/dist/i18n/messages.fr.d.ts.map +1 -0
  37. package/dist/i18n/messages.fr.js +55 -0
  38. package/dist/i18n/messages.fr.js.map +1 -0
  39. package/dist/index.d.ts +9 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +8 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/language/include-resolver.d.ts +46 -0
  44. package/dist/language/include-resolver.d.ts.map +1 -0
  45. package/dist/language/include-resolver.js +306 -0
  46. package/dist/language/include-resolver.js.map +1 -0
  47. package/dist/language/nowline-module.d.ts +18 -0
  48. package/dist/language/nowline-module.d.ts.map +1 -0
  49. package/dist/language/nowline-module.js +63 -0
  50. package/dist/language/nowline-module.js.map +1 -0
  51. package/dist/language/nowline-validator.d.ts +65 -0
  52. package/dist/language/nowline-validator.d.ts.map +1 -0
  53. package/dist/language/nowline-validator.js +1654 -0
  54. package/dist/language/nowline-validator.js.map +1 -0
  55. package/package.json +37 -0
  56. package/src/generated/ast.ts +1178 -0
  57. package/src/generated/grammar.ts +2511 -0
  58. package/src/generated/module.ts +25 -0
  59. package/src/i18n/codes.ts +102 -0
  60. package/src/i18n/index.ts +102 -0
  61. package/src/i18n/messages.en.ts +86 -0
  62. package/src/i18n/messages.fr-CA.ts +13 -0
  63. package/src/i18n/messages.fr-FR.ts +13 -0
  64. package/src/i18n/messages.fr.ts +91 -0
  65. package/src/index.ts +22 -0
  66. package/src/language/include-resolver.ts +470 -0
  67. package/src/language/nowline-module.ts +114 -0
  68. package/src/language/nowline-validator.ts +1991 -0
  69. package/src/language/nowline.langium +217 -0
@@ -0,0 +1,470 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { URI } from 'langium';
4
+ import type {
5
+ AnchorDeclaration,
6
+ CalendarBlock,
7
+ ConfigEntry,
8
+ DefaultDeclaration,
9
+ FootnoteDeclaration,
10
+ IncludeDeclaration,
11
+ LabelDeclaration,
12
+ MilestoneDeclaration,
13
+ NowlineFile,
14
+ PersonDeclaration,
15
+ RoadmapDeclaration,
16
+ RoadmapEntry,
17
+ ScaleBlock,
18
+ SizeDeclaration,
19
+ StatusDeclaration,
20
+ StyleDeclaration,
21
+ SwimlaneDeclaration,
22
+ SymbolDeclaration,
23
+ TeamDeclaration,
24
+ } from '../generated/ast.js';
25
+ import {
26
+ isAnchorDeclaration,
27
+ isCalendarBlock,
28
+ isDefaultDeclaration,
29
+ isFootnoteDeclaration,
30
+ isLabelDeclaration,
31
+ isMilestoneDeclaration,
32
+ isPersonDeclaration,
33
+ isScaleBlock,
34
+ isSizeDeclaration,
35
+ isStatusDeclaration,
36
+ isStyleDeclaration,
37
+ isSwimlaneDeclaration,
38
+ isSymbolDeclaration,
39
+ isTeamDeclaration,
40
+ } from '../generated/ast.js';
41
+ import type { NowlineServices } from './nowline-module.js';
42
+
43
+ export type IncludeMode = 'merge' | 'ignore' | 'isolate';
44
+
45
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
46
+
47
+ function readStartProp(decl: RoadmapDeclaration | undefined): string | undefined {
48
+ const prop = decl?.properties.find((p) => p.key === 'start');
49
+ if (!prop?.value) return undefined;
50
+ if (!DATE_RE.test(prop.value)) return undefined;
51
+ if (Number.isNaN(new Date(prop.value).getTime())) return undefined;
52
+ return prop.value;
53
+ }
54
+
55
+ function formatStartMismatch(
56
+ childRelPath: string,
57
+ parentStart: string | undefined,
58
+ childStart: string | undefined,
59
+ ): string {
60
+ if (parentStart && childStart) {
61
+ return `Included "${childRelPath}" declares start:${childStart}, which doesn't match this file's start:${parentStart}. Both files must declare the same start date, or neither should.`;
62
+ }
63
+ if (parentStart) {
64
+ return `Included "${childRelPath}" has no start:, but this file declares start:${parentStart}. Both files must declare the same start date, or neither should.`;
65
+ }
66
+ return `Included "${childRelPath}" declares start:${childStart}, but this file has no start:. Both files must declare the same start date, or neither should.`;
67
+ }
68
+
69
+ export interface ResolvedConfig {
70
+ scale?: ScaleBlock;
71
+ calendar?: CalendarBlock;
72
+ styles: Map<string, StyleDeclaration>;
73
+ defaults: Map<string, DefaultDeclaration>;
74
+ // Custom symbol declarations from the `symbol` config keyword. Renderer-side
75
+ // resolution of `icon:` / `capacity-icon:` looks here when the value isn't
76
+ // a built-in identifier or an inline Unicode literal. See specs/dsl.md §
77
+ // Symbol Declaration.
78
+ symbols: Map<string, SymbolDeclaration>;
79
+ }
80
+
81
+ export interface ResolvedContent {
82
+ roadmap?: RoadmapDeclaration;
83
+ persons: Map<string, PersonDeclaration>;
84
+ teams: Map<string, TeamDeclaration>;
85
+ anchors: Map<string, AnchorDeclaration>;
86
+ labels: Map<string, LabelDeclaration>;
87
+ sizes: Map<string, SizeDeclaration>;
88
+ statuses: Map<string, StatusDeclaration>;
89
+ swimlanes: Map<string, SwimlaneDeclaration>;
90
+ milestones: Map<string, MilestoneDeclaration>;
91
+ footnotes: Map<string, FootnoteDeclaration>;
92
+ isolatedRegions: IsolatedRegion[];
93
+ }
94
+
95
+ export interface IsolatedRegion {
96
+ // The path string as written in the parent's `include "..."` directive
97
+ // (e.g. "./partner.nowline"). Used as the user-facing label/badge in the
98
+ // rendered region. The resolver's internal caching/dedup uses the
99
+ // resolved absolute path, but that's never exposed to layout/render.
100
+ sourcePath: string;
101
+ config: ResolvedConfig;
102
+ content: ResolvedContent;
103
+ }
104
+
105
+ export interface ResolveDiagnostic {
106
+ severity: 'error' | 'warning';
107
+ message: string;
108
+ sourcePath: string;
109
+ line?: number;
110
+ }
111
+
112
+ export interface ResolveResult {
113
+ config: ResolvedConfig;
114
+ content: ResolvedContent;
115
+ diagnostics: ResolveDiagnostic[];
116
+ processedFiles: Set<string>;
117
+ }
118
+
119
+ interface ResolveContext {
120
+ services: NowlineServices;
121
+ diagnostics: ResolveDiagnostic[];
122
+ resolving: string[];
123
+ processed: Map<string, { config: ResolvedConfig; content: ResolvedContent }>;
124
+ readFile: (absPath: string) => Promise<string>;
125
+ }
126
+
127
+ function emptyConfig(): ResolvedConfig {
128
+ return {
129
+ styles: new Map(),
130
+ defaults: new Map(),
131
+ symbols: new Map(),
132
+ };
133
+ }
134
+
135
+ function emptyContent(): ResolvedContent {
136
+ return {
137
+ persons: new Map(),
138
+ teams: new Map(),
139
+ anchors: new Map(),
140
+ labels: new Map(),
141
+ sizes: new Map(),
142
+ statuses: new Map(),
143
+ swimlanes: new Map(),
144
+ milestones: new Map(),
145
+ footnotes: new Map(),
146
+ isolatedRegions: [],
147
+ };
148
+ }
149
+
150
+ export interface ResolveIncludesOptions {
151
+ services: NowlineServices;
152
+ readFile?: (absPath: string) => Promise<string>;
153
+ }
154
+
155
+ export async function resolveIncludes(
156
+ file: NowlineFile,
157
+ filePath: string,
158
+ options: ResolveIncludesOptions,
159
+ ): Promise<ResolveResult> {
160
+ const readFile = options.readFile ?? ((p) => fs.readFile(p, 'utf-8'));
161
+ const ctx: ResolveContext = {
162
+ services: options.services,
163
+ diagnostics: [],
164
+ resolving: [],
165
+ processed: new Map(),
166
+ readFile,
167
+ };
168
+ const absPath = path.resolve(filePath);
169
+ const { config, content } = await resolveFile(file, absPath, ctx);
170
+ return {
171
+ config,
172
+ content,
173
+ diagnostics: ctx.diagnostics,
174
+ processedFiles: new Set(ctx.processed.keys()),
175
+ };
176
+ }
177
+
178
+ async function resolveFile(
179
+ file: NowlineFile,
180
+ absPath: string,
181
+ ctx: ResolveContext,
182
+ ): Promise<{ config: ResolvedConfig; content: ResolvedContent }> {
183
+ if (ctx.processed.has(absPath)) {
184
+ return ctx.processed.get(absPath)!;
185
+ }
186
+ if (ctx.resolving.includes(absPath)) {
187
+ ctx.diagnostics.push({
188
+ severity: 'error',
189
+ message: `Circular include detected: ${[...ctx.resolving, absPath].join(' → ')}`,
190
+ sourcePath: absPath,
191
+ });
192
+ const empty = { config: emptyConfig(), content: emptyContent() };
193
+ ctx.processed.set(absPath, empty);
194
+ return empty;
195
+ }
196
+
197
+ ctx.resolving.push(absPath);
198
+
199
+ const config = emptyConfig();
200
+ const content = emptyContent();
201
+
202
+ // Seed with the parent's own declarations first so that collisions from included
203
+ // files shadow to the parent (parent wins) and produce a warning pointing at the child.
204
+ mergeLocalConfig(config, file);
205
+ mergeLocalContent(content, file);
206
+
207
+ const seenIncludes = new Set<string>();
208
+ for (const inc of file.includes) {
209
+ const childRelPath = inc.path;
210
+ const childAbsPath = path.resolve(path.dirname(absPath), childRelPath);
211
+
212
+ if (seenIncludes.has(childAbsPath)) {
213
+ ctx.diagnostics.push({
214
+ severity: 'error',
215
+ message: `Duplicate include "${childRelPath}" in ${path.basename(absPath)}.`,
216
+ sourcePath: absPath,
217
+ line: inc.$cstNode?.range.start.line,
218
+ });
219
+ continue;
220
+ }
221
+ seenIncludes.add(childAbsPath);
222
+
223
+ const configMode = getIncludeMode(inc, 'config') ?? 'merge';
224
+ const roadmapMode = getIncludeMode(inc, 'roadmap') ?? 'merge';
225
+
226
+ let childFile: NowlineFile | undefined;
227
+ try {
228
+ const text = await ctx.readFile(childAbsPath);
229
+ childFile = await parseString(ctx.services, text, childAbsPath);
230
+ } catch (err) {
231
+ ctx.diagnostics.push({
232
+ severity: 'error',
233
+ message: `Could not read include "${childRelPath}": ${(err as Error).message}`,
234
+ sourcePath: absPath,
235
+ line: inc.$cstNode?.range.start.line,
236
+ });
237
+ continue;
238
+ }
239
+
240
+ const { config: childConfig, content: childContent } = await resolveFile(
241
+ childFile,
242
+ childAbsPath,
243
+ ctx,
244
+ );
245
+
246
+ applyConfigMode(config, childConfig, configMode, childAbsPath, ctx.diagnostics);
247
+ applyRoadmapMode(
248
+ content,
249
+ childContent,
250
+ childConfig,
251
+ roadmapMode,
252
+ childAbsPath,
253
+ childRelPath,
254
+ ctx.diagnostics,
255
+ );
256
+
257
+ if (roadmapMode === 'isolate' && !childContent.roadmap) {
258
+ ctx.diagnostics.push({
259
+ severity: 'error',
260
+ message: `Cannot isolate "${childRelPath}": it has no roadmap declaration.`,
261
+ sourcePath: absPath,
262
+ line: inc.$cstNode?.range.start.line,
263
+ });
264
+ }
265
+
266
+ // Non-ignored includes with a child roadmap must agree on start: with the parent.
267
+ // Deliberate divergence from the usual "parent wins with warning" merge behaviour
268
+ // because start: defines the shared timeline baseline.
269
+ if (roadmapMode !== 'ignore' && childFile.roadmapDecl) {
270
+ const parentStart = readStartProp(file.roadmapDecl);
271
+ const childStart = readStartProp(childFile.roadmapDecl);
272
+ if (parentStart !== childStart) {
273
+ ctx.diagnostics.push({
274
+ severity: 'error',
275
+ message: formatStartMismatch(childRelPath, parentStart, childStart),
276
+ sourcePath: absPath,
277
+ line: inc.$cstNode?.range.start.line,
278
+ });
279
+ }
280
+ }
281
+ }
282
+
283
+ ctx.resolving.pop();
284
+ const result = { config, content };
285
+ ctx.processed.set(absPath, result);
286
+ return result;
287
+ }
288
+
289
+ function getIncludeMode(
290
+ inc: IncludeDeclaration,
291
+ key: 'config' | 'roadmap',
292
+ ): IncludeMode | undefined {
293
+ const opt = inc.options.find((o) => o.key === key);
294
+ if (!opt) return undefined;
295
+ if (opt.value === 'merge' || opt.value === 'ignore' || opt.value === 'isolate') {
296
+ return opt.value;
297
+ }
298
+ return undefined;
299
+ }
300
+
301
+ async function parseString(
302
+ services: NowlineServices,
303
+ text: string,
304
+ absPath: string,
305
+ ): Promise<NowlineFile> {
306
+ const uri = URI.file(absPath);
307
+ const docFactory = services.shared.workspace.LangiumDocumentFactory;
308
+ const doc = docFactory.fromString<NowlineFile>(text, uri);
309
+ await services.shared.workspace.DocumentBuilder.build([doc], { validation: false });
310
+ return doc.parseResult.value;
311
+ }
312
+
313
+ function applyConfigMode(
314
+ target: ResolvedConfig,
315
+ child: ResolvedConfig,
316
+ mode: IncludeMode,
317
+ childPath: string,
318
+ diagnostics: ResolveDiagnostic[],
319
+ ): void {
320
+ if (mode === 'ignore' || mode === 'isolate') return;
321
+
322
+ const warn = (name: string, category: string) =>
323
+ diagnostics.push({
324
+ severity: 'warning',
325
+ message: `${category} "${name}" from ${path.basename(childPath)} is shadowed by the parent's definition.`,
326
+ sourcePath: childPath,
327
+ });
328
+
329
+ mergeMap(target.styles, child.styles, (name) => warn(name, 'Style'));
330
+ mergeMap(target.defaults, child.defaults, (name) => warn(name, 'Default'));
331
+ mergeMap(target.symbols, child.symbols, (name) => warn(name, 'Symbol'));
332
+ if (child.scale && !target.scale) {
333
+ target.scale = child.scale;
334
+ }
335
+ if (child.calendar && !target.calendar) {
336
+ target.calendar = child.calendar;
337
+ }
338
+ }
339
+
340
+ function applyRoadmapMode(
341
+ target: ResolvedContent,
342
+ child: ResolvedContent,
343
+ childConfig: ResolvedConfig,
344
+ mode: IncludeMode,
345
+ childPath: string,
346
+ childRelPath: string,
347
+ diagnostics: ResolveDiagnostic[],
348
+ ): void {
349
+ if (mode === 'ignore') return;
350
+
351
+ if (mode === 'isolate') {
352
+ if (child.roadmap) {
353
+ target.isolatedRegions.push({
354
+ sourcePath: childRelPath,
355
+ config: childConfig,
356
+ content: child,
357
+ });
358
+ }
359
+ return;
360
+ }
361
+
362
+ const warn = (name: string, category: string) =>
363
+ diagnostics.push({
364
+ severity: 'warning',
365
+ message: `${category} "${name}" from ${path.basename(childPath)} is shadowed by the parent's definition.`,
366
+ sourcePath: childPath,
367
+ });
368
+
369
+ mergeMap(target.persons, child.persons, (name) => warn(name, 'Person'));
370
+ mergeMap(target.teams, child.teams, (name) => warn(name, 'Team'));
371
+ mergeMap(target.anchors, child.anchors, (name) => warn(name, 'Anchor'));
372
+ mergeMap(target.labels, child.labels, (name) => warn(name, 'Label'));
373
+ mergeMap(target.sizes, child.sizes, (name) => warn(name, 'Size'));
374
+ mergeMap(target.statuses, child.statuses, (name) => warn(name, 'Status'));
375
+ mergeMap(target.swimlanes, child.swimlanes, (name) => warn(name, 'Swimlane'));
376
+ mergeMap(target.milestones, child.milestones, (name) => warn(name, 'Milestone'));
377
+ mergeMap(target.footnotes, child.footnotes, (name) => warn(name, 'Footnote'));
378
+ if (child.roadmap && !target.roadmap) {
379
+ target.roadmap = child.roadmap;
380
+ }
381
+ }
382
+
383
+ function mergeMap<V>(
384
+ target: Map<string, V>,
385
+ source: Map<string, V>,
386
+ onConflict: (name: string) => void,
387
+ ): void {
388
+ for (const [name, value] of source) {
389
+ if (target.has(name)) {
390
+ onConflict(name);
391
+ continue;
392
+ }
393
+ target.set(name, value);
394
+ }
395
+ }
396
+
397
+ function mergeLocalConfig(config: ResolvedConfig, file: NowlineFile): void {
398
+ for (const entry of file.configEntries) {
399
+ addConfigEntry(config, entry);
400
+ }
401
+ }
402
+
403
+ function addConfigEntry(config: ResolvedConfig, entry: ConfigEntry): void {
404
+ if (isScaleBlock(entry)) {
405
+ if (!config.scale) config.scale = entry;
406
+ } else if (isCalendarBlock(entry)) {
407
+ if (!config.calendar) config.calendar = entry;
408
+ } else if (isStyleDeclaration(entry)) {
409
+ if (entry.name && !config.styles.has(entry.name)) {
410
+ config.styles.set(entry.name, entry);
411
+ }
412
+ } else if (isSymbolDeclaration(entry)) {
413
+ if (entry.name && !config.symbols.has(entry.name)) {
414
+ config.symbols.set(entry.name, entry);
415
+ }
416
+ } else if (isDefaultDeclaration(entry)) {
417
+ if (!config.defaults.has(entry.entityType)) {
418
+ config.defaults.set(entry.entityType, entry);
419
+ }
420
+ }
421
+ }
422
+
423
+ function mergeLocalContent(content: ResolvedContent, file: NowlineFile): void {
424
+ if (file.roadmapDecl && !content.roadmap) {
425
+ content.roadmap = file.roadmapDecl;
426
+ }
427
+ for (const entry of file.roadmapEntries) {
428
+ addRoadmapEntry(content, entry);
429
+ }
430
+ }
431
+
432
+ function addRoadmapEntry(content: ResolvedContent, entry: RoadmapEntry): void {
433
+ if (isSwimlaneDeclaration(entry)) {
434
+ if (entry.name && !content.swimlanes.has(entry.name)) {
435
+ content.swimlanes.set(entry.name, entry);
436
+ }
437
+ } else if (isPersonDeclaration(entry)) {
438
+ if (entry.name && !content.persons.has(entry.name)) {
439
+ content.persons.set(entry.name, entry);
440
+ }
441
+ } else if (isTeamDeclaration(entry)) {
442
+ if (entry.name && !content.teams.has(entry.name)) {
443
+ content.teams.set(entry.name, entry);
444
+ }
445
+ } else if (isAnchorDeclaration(entry)) {
446
+ if (entry.name && !content.anchors.has(entry.name)) {
447
+ content.anchors.set(entry.name, entry);
448
+ }
449
+ } else if (isLabelDeclaration(entry)) {
450
+ if (entry.name && !content.labels.has(entry.name)) {
451
+ content.labels.set(entry.name, entry);
452
+ }
453
+ } else if (isSizeDeclaration(entry)) {
454
+ if (entry.name && !content.sizes.has(entry.name)) {
455
+ content.sizes.set(entry.name, entry);
456
+ }
457
+ } else if (isStatusDeclaration(entry)) {
458
+ if (entry.name && !content.statuses.has(entry.name)) {
459
+ content.statuses.set(entry.name, entry);
460
+ }
461
+ } else if (isMilestoneDeclaration(entry)) {
462
+ if (entry.name && !content.milestones.has(entry.name)) {
463
+ content.milestones.set(entry.name, entry);
464
+ }
465
+ } else if (isFootnoteDeclaration(entry)) {
466
+ if (entry.name && !content.footnotes.has(entry.name)) {
467
+ content.footnotes.set(entry.name, entry);
468
+ }
469
+ }
470
+ }
@@ -0,0 +1,114 @@
1
+ import type {
2
+ CstNode,
3
+ GrammarAST,
4
+ LangiumCoreServices,
5
+ LangiumSharedCoreServices,
6
+ Module,
7
+ PartialLangiumCoreServices,
8
+ ValueType,
9
+ } from 'langium';
10
+ import {
11
+ createDefaultCoreModule,
12
+ createDefaultSharedCoreModule,
13
+ DefaultValueConverter,
14
+ EmptyFileSystem,
15
+ IndentationAwareLexer,
16
+ IndentationAwareTokenBuilder,
17
+ inject,
18
+ } from 'langium';
19
+ import type {
20
+ NowlineAstType,
21
+ NowlineKeywordNames,
22
+ NowlineTerminalNames,
23
+ } from '../generated/ast.js';
24
+ import { NowlineGeneratedModule, NowlineGeneratedSharedModule } from '../generated/module.js';
25
+ import { NowlineValidator, registerValidationChecks } from './nowline-validator.js';
26
+
27
+ // Strip trailing ':' from property key tokens so AST nodes carry clean keys
28
+ // (e.g. `duration` instead of `duration:`). The grammar uses a single key-with-colon
29
+ // terminal to resolve lexer ambiguity between bare keywords like `duration` and
30
+ // property keys like `duration:`; this converter keeps that token-level workaround
31
+ // out of the AST surface.
32
+ class NowlinePropertyKeyValueConverter extends DefaultValueConverter {
33
+ protected override runConverter(
34
+ rule: GrammarAST.AbstractRule,
35
+ input: string,
36
+ cstNode: CstNode,
37
+ ): ValueType {
38
+ if (rule.name === 'PROPERTY_KEY_WITH_COLON' || rule.name === 'INCLUDE_OPTION_KEY') {
39
+ return input.endsWith(':') ? input.slice(0, -1) : input;
40
+ }
41
+ return super.runConverter(rule, input, cstNode);
42
+ }
43
+ }
44
+
45
+ // Override `isStartOfLine` so that a position immediately following a
46
+ // `\`-line-continuation (handled by the LINE_CONTINUATION hidden terminal) is
47
+ // NOT treated as the start of a new logical line. Without this, a continuation
48
+ // line with zero leading whitespace would trigger a spurious DEDENT because the
49
+ // base implementation only inspects the single character at `text[offset - 1]`.
50
+ class NowlineIndentationAwareTokenBuilder extends IndentationAwareTokenBuilder<
51
+ NowlineTerminalNames,
52
+ NowlineKeywordNames
53
+ > {
54
+ protected override isStartOfLine(text: string, offset: number): boolean {
55
+ if (!super.isStartOfLine(text, offset)) {
56
+ return false;
57
+ }
58
+ let i = offset - 1;
59
+ while (i >= 0 && (text[i] === ' ' || text[i] === '\t')) i--;
60
+ while (i >= 0 && (text[i] === '\r' || text[i] === '\n')) i--;
61
+ while (i >= 0 && (text[i] === ' ' || text[i] === '\t')) i--;
62
+ return i < 0 || text[i] !== '\\';
63
+ }
64
+ }
65
+
66
+ export type NowlineAddedServices = {
67
+ validation: {
68
+ NowlineValidator: NowlineValidator;
69
+ };
70
+ };
71
+
72
+ export type NowlineServices = LangiumCoreServices & NowlineAddedServices;
73
+
74
+ export type { NowlineAstType };
75
+
76
+ export const NowlineModule: Module<
77
+ NowlineServices,
78
+ PartialLangiumCoreServices & NowlineAddedServices
79
+ > = {
80
+ parser: {
81
+ TokenBuilder: () =>
82
+ new NowlineIndentationAwareTokenBuilder({
83
+ indentTokenName: 'INDENT',
84
+ dedentTokenName: 'DEDENT',
85
+ whitespaceTokenName: 'WS',
86
+ ignoreIndentationDelimiters: [['[', ']']],
87
+ }),
88
+ Lexer: (services) => new IndentationAwareLexer(services),
89
+ ParserConfig: () => ({
90
+ maxLookahead: 4,
91
+ }),
92
+ ValueConverter: () => new NowlinePropertyKeyValueConverter(),
93
+ },
94
+ validation: {
95
+ NowlineValidator: () => new NowlineValidator(),
96
+ },
97
+ };
98
+
99
+ export function createNowlineServices(context: { shared?: LangiumSharedCoreServices } = {}): {
100
+ shared: LangiumSharedCoreServices;
101
+ Nowline: NowlineServices;
102
+ } {
103
+ const shared =
104
+ context.shared ??
105
+ inject(createDefaultSharedCoreModule(EmptyFileSystem), NowlineGeneratedSharedModule);
106
+ const Nowline = inject(
107
+ createDefaultCoreModule({ shared }),
108
+ NowlineGeneratedModule,
109
+ NowlineModule,
110
+ );
111
+ shared.ServiceRegistry.register(Nowline);
112
+ registerValidationChecks(Nowline);
113
+ return { shared, Nowline };
114
+ }