@nowline/core 0.5.1 → 0.7.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.
- package/dist/convert/parse-json.d.ts +7 -0
- package/dist/convert/parse-json.d.ts.map +1 -0
- package/dist/convert/parse-json.js +33 -0
- package/dist/convert/parse-json.js.map +1 -0
- package/dist/convert/printer.d.ts +6 -0
- package/dist/convert/printer.d.ts.map +1 -0
- package/dist/convert/printer.js +331 -0
- package/dist/convert/printer.js.map +1 -0
- package/dist/convert/schema.d.ts +33 -0
- package/dist/convert/schema.d.ts.map +1 -0
- package/dist/convert/schema.js +77 -0
- package/dist/convert/schema.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/language/include-resolver.js +117 -38
- package/dist/language/include-resolver.js.map +1 -1
- package/dist/templates.d.ts +3 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +2 -0
- package/dist/templates.js.map +1 -0
- package/package.json +2 -2
- package/src/convert/parse-json.ts +44 -0
- package/src/convert/printer.ts +358 -0
- package/src/convert/schema.ts +105 -0
- package/src/index.ts +11 -0
- package/src/language/include-resolver.ts +142 -38
- package/src/templates.ts +3 -0
|
@@ -373,15 +373,15 @@ function applyRoadmapMode(
|
|
|
373
373
|
sourcePath: childPath,
|
|
374
374
|
});
|
|
375
375
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
376
|
+
mergeContentMap(target.persons, child.persons, (name) => warn(name, 'Person'));
|
|
377
|
+
mergeContentMap(target.teams, child.teams, (name) => warn(name, 'Team'));
|
|
378
|
+
mergeContentMap(target.anchors, child.anchors, (name) => warn(name, 'Anchor'));
|
|
379
|
+
mergeContentMap(target.labels, child.labels, (name) => warn(name, 'Label'));
|
|
380
|
+
mergeContentMap(target.sizes, child.sizes, (name) => warn(name, 'Size'));
|
|
381
|
+
mergeContentMap(target.statuses, child.statuses, (name) => warn(name, 'Status'));
|
|
382
|
+
mergeContentMap(target.swimlanes, child.swimlanes, (name) => warn(name, 'Swimlane'));
|
|
383
|
+
mergeContentMap(target.milestones, child.milestones, (name) => warn(name, 'Milestone'));
|
|
384
|
+
mergeContentMap(target.footnotes, child.footnotes, (name) => warn(name, 'Footnote'));
|
|
385
385
|
if (child.roadmap && !target.roadmap) {
|
|
386
386
|
target.roadmap = child.roadmap;
|
|
387
387
|
}
|
|
@@ -401,6 +401,33 @@ function mergeMap<V>(
|
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Merge a child content map into the parent. Explicit-id entries keep the
|
|
406
|
+
* parent-wins-on-collision behavior (and warn). Title-only (auto-slugged)
|
|
407
|
+
* entries are internal and non-referenceable, so they never shadow and never
|
|
408
|
+
* warn — each is re-keyed around the parent's entries and kept.
|
|
409
|
+
*/
|
|
410
|
+
function mergeContentMap<V extends { name?: string; title?: string }>(
|
|
411
|
+
target: Map<string, V>,
|
|
412
|
+
source: Map<string, V>,
|
|
413
|
+
onConflict: (name: string) => void,
|
|
414
|
+
): void {
|
|
415
|
+
for (const [name, value] of source) {
|
|
416
|
+
if (!value.name && value.title) {
|
|
417
|
+
target.set(
|
|
418
|
+
uniqueMapKey(target as Map<string, unknown>, slugifyTitle(value.title)),
|
|
419
|
+
value,
|
|
420
|
+
);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (target.has(name)) {
|
|
424
|
+
onConflict(name);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
target.set(name, value);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
404
431
|
function mergeLocalConfig(config: ResolvedConfig, file: NowlineFile): void {
|
|
405
432
|
for (const entry of file.configEntries) {
|
|
406
433
|
addConfigEntry(config, entry);
|
|
@@ -427,51 +454,128 @@ function addConfigEntry(config: ResolvedConfig, entry: ConfigEntry): void {
|
|
|
427
454
|
}
|
|
428
455
|
}
|
|
429
456
|
|
|
457
|
+
/** Explicit ids declared in a file, grouped by the content map they target. */
|
|
458
|
+
interface ReservedRoadmapIds {
|
|
459
|
+
swimlanes: Set<string>;
|
|
460
|
+
persons: Set<string>;
|
|
461
|
+
teams: Set<string>;
|
|
462
|
+
anchors: Set<string>;
|
|
463
|
+
labels: Set<string>;
|
|
464
|
+
sizes: Set<string>;
|
|
465
|
+
statuses: Set<string>;
|
|
466
|
+
milestones: Set<string>;
|
|
467
|
+
footnotes: Set<string>;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function collectExplicitRoadmapIds(entries: RoadmapEntry[]): ReservedRoadmapIds {
|
|
471
|
+
const reserved: ReservedRoadmapIds = {
|
|
472
|
+
swimlanes: new Set(),
|
|
473
|
+
persons: new Set(),
|
|
474
|
+
teams: new Set(),
|
|
475
|
+
anchors: new Set(),
|
|
476
|
+
labels: new Set(),
|
|
477
|
+
sizes: new Set(),
|
|
478
|
+
statuses: new Set(),
|
|
479
|
+
milestones: new Set(),
|
|
480
|
+
footnotes: new Set(),
|
|
481
|
+
};
|
|
482
|
+
for (const entry of entries) {
|
|
483
|
+
const name = (entry as { name?: string }).name;
|
|
484
|
+
if (!name) continue;
|
|
485
|
+
if (isSwimlaneDeclaration(entry)) reserved.swimlanes.add(name);
|
|
486
|
+
else if (isPersonDeclaration(entry)) reserved.persons.add(name);
|
|
487
|
+
else if (isTeamDeclaration(entry)) reserved.teams.add(name);
|
|
488
|
+
else if (isAnchorDeclaration(entry)) reserved.anchors.add(name);
|
|
489
|
+
else if (isLabelDeclaration(entry)) reserved.labels.add(name);
|
|
490
|
+
else if (isSizeDeclaration(entry)) reserved.sizes.add(name);
|
|
491
|
+
else if (isStatusDeclaration(entry)) reserved.statuses.add(name);
|
|
492
|
+
else if (isMilestoneDeclaration(entry)) reserved.milestones.add(name);
|
|
493
|
+
else if (isFootnoteDeclaration(entry)) reserved.footnotes.add(name);
|
|
494
|
+
}
|
|
495
|
+
return reserved;
|
|
496
|
+
}
|
|
497
|
+
|
|
430
498
|
function mergeLocalContent(content: ResolvedContent, file: NowlineFile): void {
|
|
431
499
|
if (file.roadmapDecl && !content.roadmap) {
|
|
432
500
|
content.roadmap = file.roadmapDecl;
|
|
433
501
|
}
|
|
502
|
+
// Pre-scan explicit ids so a title-only slug can never displace one an
|
|
503
|
+
// author spelled out, even when the title-only entry comes first in source.
|
|
504
|
+
const reserved = collectExplicitRoadmapIds(file.roadmapEntries);
|
|
434
505
|
for (const entry of file.roadmapEntries) {
|
|
435
|
-
addRoadmapEntry(content, entry);
|
|
506
|
+
addRoadmapEntry(content, entry, reserved);
|
|
436
507
|
}
|
|
437
508
|
}
|
|
438
509
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
510
|
+
/** Kebab-case slug for title-only entities (specs/dsl.md § Identifiers). */
|
|
511
|
+
function slugifyTitle(title: string): string {
|
|
512
|
+
return (
|
|
513
|
+
title
|
|
514
|
+
.toLowerCase()
|
|
515
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
516
|
+
.replace(/^-+|-+$/g, '') || 'entity'
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Pick a map key that does not collide with an existing entry, nor with any
|
|
522
|
+
* key in `reserved` (explicit ids that will be inserted later in the same
|
|
523
|
+
* pass). Reserving keeps a title-only slug from claiming a key an author
|
|
524
|
+
* spelled out explicitly, regardless of source order.
|
|
525
|
+
*/
|
|
526
|
+
function uniqueMapKey(map: Map<string, unknown>, base: string, reserved?: Set<string>): string {
|
|
527
|
+
const taken = (key: string): boolean => map.has(key) || (reserved?.has(key) ?? false);
|
|
528
|
+
if (!taken(base)) return base;
|
|
529
|
+
let n = 2;
|
|
530
|
+
while (taken(`${base}-${n}`)) n++;
|
|
531
|
+
return `${base}-${n}`;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Insert a roadmap entity into a resolved-content map. Explicit ids always
|
|
536
|
+
* win their key (and keep today's parent-wins-on-collision behavior); title-only
|
|
537
|
+
* entries land under a slug derived from the title (internal key — not written
|
|
538
|
+
* to AST) that avoids both occupied and `reserved` explicit-id keys.
|
|
539
|
+
*/
|
|
540
|
+
function addByKey<V extends { name?: string; title?: string }>(
|
|
541
|
+
map: Map<string, V>,
|
|
542
|
+
entry: V,
|
|
543
|
+
reserved?: Set<string>,
|
|
544
|
+
): void {
|
|
545
|
+
if (entry.name) {
|
|
546
|
+
if (!map.has(entry.name)) {
|
|
547
|
+
map.set(entry.name, entry);
|
|
443
548
|
}
|
|
549
|
+
} else if (entry.title) {
|
|
550
|
+
map.set(
|
|
551
|
+
uniqueMapKey(map as Map<string, unknown>, slugifyTitle(entry.title), reserved),
|
|
552
|
+
entry,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function addRoadmapEntry(
|
|
558
|
+
content: ResolvedContent,
|
|
559
|
+
entry: RoadmapEntry,
|
|
560
|
+
reserved: ReservedRoadmapIds,
|
|
561
|
+
): void {
|
|
562
|
+
if (isSwimlaneDeclaration(entry)) {
|
|
563
|
+
addByKey(content.swimlanes, entry, reserved.swimlanes);
|
|
444
564
|
} else if (isPersonDeclaration(entry)) {
|
|
445
|
-
|
|
446
|
-
content.persons.set(entry.name, entry);
|
|
447
|
-
}
|
|
565
|
+
addByKey(content.persons, entry, reserved.persons);
|
|
448
566
|
} else if (isTeamDeclaration(entry)) {
|
|
449
|
-
|
|
450
|
-
content.teams.set(entry.name, entry);
|
|
451
|
-
}
|
|
567
|
+
addByKey(content.teams, entry, reserved.teams);
|
|
452
568
|
} else if (isAnchorDeclaration(entry)) {
|
|
453
|
-
|
|
454
|
-
content.anchors.set(entry.name, entry);
|
|
455
|
-
}
|
|
569
|
+
addByKey(content.anchors, entry, reserved.anchors);
|
|
456
570
|
} else if (isLabelDeclaration(entry)) {
|
|
457
|
-
|
|
458
|
-
content.labels.set(entry.name, entry);
|
|
459
|
-
}
|
|
571
|
+
addByKey(content.labels, entry, reserved.labels);
|
|
460
572
|
} else if (isSizeDeclaration(entry)) {
|
|
461
|
-
|
|
462
|
-
content.sizes.set(entry.name, entry);
|
|
463
|
-
}
|
|
573
|
+
addByKey(content.sizes, entry, reserved.sizes);
|
|
464
574
|
} else if (isStatusDeclaration(entry)) {
|
|
465
|
-
|
|
466
|
-
content.statuses.set(entry.name, entry);
|
|
467
|
-
}
|
|
575
|
+
addByKey(content.statuses, entry, reserved.statuses);
|
|
468
576
|
} else if (isMilestoneDeclaration(entry)) {
|
|
469
|
-
|
|
470
|
-
content.milestones.set(entry.name, entry);
|
|
471
|
-
}
|
|
577
|
+
addByKey(content.milestones, entry, reserved.milestones);
|
|
472
578
|
} else if (isFootnoteDeclaration(entry)) {
|
|
473
|
-
|
|
474
|
-
content.footnotes.set(entry.name, entry);
|
|
475
|
-
}
|
|
579
|
+
addByKey(content.footnotes, entry, reserved.footnotes);
|
|
476
580
|
}
|
|
477
581
|
}
|
package/src/templates.ts
ADDED