@nowline/layout 0.5.0 → 0.6.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/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/layout-context.d.ts +8 -0
- package/dist/layout-context.d.ts.map +1 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +68 -47
- package/dist/layout.js.map +1 -1
- package/dist/nodes/group-node.d.ts.map +1 -1
- package/dist/nodes/group-node.js +2 -1
- package/dist/nodes/group-node.js.map +1 -1
- package/dist/nodes/include-node.d.ts.map +1 -1
- package/dist/nodes/include-node.js +4 -0
- package/dist/nodes/include-node.js.map +1 -1
- package/dist/nodes/parallel-node.d.ts.map +1 -1
- package/dist/nodes/parallel-node.js +2 -1
- package/dist/nodes/parallel-node.js.map +1 -1
- package/dist/nodes/roadmap-node.d.ts.map +1 -1
- package/dist/nodes/roadmap-node.js +4 -0
- package/dist/nodes/roadmap-node.js.map +1 -1
- package/dist/resolve-today.d.ts +90 -0
- package/dist/resolve-today.d.ts.map +1 -0
- package/dist/resolve-today.js +197 -0
- package/dist/resolve-today.js.map +1 -0
- package/dist/schedule.d.ts +38 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +158 -0
- package/dist/schedule.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +10 -0
- package/src/layout-context.ts +8 -0
- package/src/layout.ts +69 -46
- package/src/nodes/group-node.ts +2 -1
- package/src/nodes/include-node.ts +4 -0
- package/src/nodes/parallel-node.ts +2 -1
- package/src/nodes/roadmap-node.ts +4 -0
- package/src/resolve-today.ts +266 -0
- package/src/schedule.ts +220 -0
package/src/schedule.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// scheduleRoadmap — compute the floating calendar start/end date for every
|
|
2
|
+
// named entity (items, milestones, anchors) without running the full layout.
|
|
3
|
+
//
|
|
4
|
+
// The sequencing rules exactly mirror `computeContentEndDay` in `layout.ts`
|
|
5
|
+
// plus the roadmap-start resolution in `RoadmapNode.place`:
|
|
6
|
+
// - date: / start: — absolute pin (date: wins over start:)
|
|
7
|
+
// - after:[id|DATE] — start after the maximum predecessor end
|
|
8
|
+
// - sequential default — start where the previous item in the lane ended
|
|
9
|
+
//
|
|
10
|
+
// Used by the XLSX exporter to populate the "Start" / "End" date columns and
|
|
11
|
+
// the milestone "Date" cell when no explicit `date:` is set. Keeping it
|
|
12
|
+
// separate from `computeContentEndDay` avoids mutating the byte-stable snapshot
|
|
13
|
+
// pipeline.
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
GroupBlock,
|
|
17
|
+
ItemDeclaration,
|
|
18
|
+
MilestoneDeclaration,
|
|
19
|
+
NowlineFile,
|
|
20
|
+
ParallelBlock,
|
|
21
|
+
ResolveResult,
|
|
22
|
+
SwimlaneDeclaration,
|
|
23
|
+
} from '@nowline/core';
|
|
24
|
+
import { isGroupBlock, isItemDeclaration, isParallelBlock } from '@nowline/core';
|
|
25
|
+
import {
|
|
26
|
+
addDays,
|
|
27
|
+
daysBetween,
|
|
28
|
+
deriveItemDurationDays,
|
|
29
|
+
resolveCalendar,
|
|
30
|
+
resolveSizes,
|
|
31
|
+
} from './calendar.js';
|
|
32
|
+
import { parseDate, propValue, propValues } from './dsl-utils.js';
|
|
33
|
+
|
|
34
|
+
/** Per-item scheduled interval, keyed by item id (name). */
|
|
35
|
+
export interface ScheduledItem {
|
|
36
|
+
start: Date;
|
|
37
|
+
end: Date;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Result of scheduling a roadmap. All dates are UTC midnight.
|
|
42
|
+
*
|
|
43
|
+
* `items` is keyed by DSL id and only contains **named** items.
|
|
44
|
+
* `byNode` is keyed by AST node identity and contains **every** item,
|
|
45
|
+
* including anonymous ones — use this when you have the AST node in hand
|
|
46
|
+
* and want dates regardless of whether an id was declared.
|
|
47
|
+
*/
|
|
48
|
+
export interface RoadmapSchedule {
|
|
49
|
+
/** Resolved roadmap start date (UTC midnight). */
|
|
50
|
+
startDate: Date;
|
|
51
|
+
/** Named items keyed by their DSL id (`name`). */
|
|
52
|
+
items: Map<string, ScheduledItem>;
|
|
53
|
+
/** Every item keyed by AST node identity (named and anonymous). */
|
|
54
|
+
byNode: WeakMap<ItemDeclaration, ScheduledItem>;
|
|
55
|
+
/** Named milestones keyed by their DSL id. */
|
|
56
|
+
milestones: Map<string, Date>;
|
|
57
|
+
/** Every milestone keyed by AST node identity (named and anonymous). */
|
|
58
|
+
milestoneByNode: WeakMap<MilestoneDeclaration, Date>;
|
|
59
|
+
/** Named anchors: their declared or computed date. */
|
|
60
|
+
anchors: Map<string, Date>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ScheduleOptions {
|
|
64
|
+
/** Passed as the reference date when the roadmap omits `start:`. */
|
|
65
|
+
today?: Date;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compute the scheduled start/end date for every named entity in the roadmap.
|
|
70
|
+
* Does NOT run the full layout (no SVG geometry, no pixel coordinates).
|
|
71
|
+
*/
|
|
72
|
+
export function scheduleRoadmap(
|
|
73
|
+
file: NowlineFile,
|
|
74
|
+
resolved: ResolveResult,
|
|
75
|
+
options: ScheduleOptions = {},
|
|
76
|
+
): RoadmapSchedule {
|
|
77
|
+
const cal = resolveCalendar(file, resolved.config.calendar);
|
|
78
|
+
const sizes = resolveSizes(resolved.content.sizes, cal);
|
|
79
|
+
|
|
80
|
+
// Resolve roadmap start date — same precedence as RoadmapNode.place.
|
|
81
|
+
const startRaw = propValue(file.roadmapDecl?.properties ?? [], 'start');
|
|
82
|
+
const startDate = parseDate(startRaw) ?? utcMidnight(options.today ?? new Date());
|
|
83
|
+
|
|
84
|
+
// These maps accumulate end-day offsets (from startDate) for cross-entity
|
|
85
|
+
// `after:` resolution, matching computeContentEndDay exactly.
|
|
86
|
+
const itemEnd = new Map<string, number>(); // id → end day
|
|
87
|
+
const anchorEnd = new Map<string, number>(); // id → date day
|
|
88
|
+
const milestoneEnd = new Map<string, number>(); // id → date day
|
|
89
|
+
|
|
90
|
+
// Result maps (Date objects).
|
|
91
|
+
const itemResults = new Map<string, ScheduledItem>();
|
|
92
|
+
const itemByNode = new WeakMap<ItemDeclaration, ScheduledItem>();
|
|
93
|
+
const milestoneResults = new Map<string, Date>();
|
|
94
|
+
const milestoneByNode = new WeakMap<MilestoneDeclaration, Date>();
|
|
95
|
+
const anchorResults = new Map<string, Date>();
|
|
96
|
+
|
|
97
|
+
// Resolve a single `after:` element to a day-offset.
|
|
98
|
+
const resolveAfterDay = (ref: string): number => {
|
|
99
|
+
const inlineDate = parseDate(ref);
|
|
100
|
+
if (inlineDate) return daysBetween(startDate, inlineDate);
|
|
101
|
+
if (itemEnd.has(ref)) return itemEnd.get(ref)!;
|
|
102
|
+
if (anchorEnd.has(ref)) return anchorEnd.get(ref)!;
|
|
103
|
+
if (milestoneEnd.has(ref)) return milestoneEnd.get(ref)!;
|
|
104
|
+
return 0;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Pre-seed named anchors (they may be referenced by item after: before we
|
|
108
|
+
// walk the lanes).
|
|
109
|
+
for (const [id, anchor] of resolved.content.anchors) {
|
|
110
|
+
const d = parseDate(propValue(anchor.properties, 'date'));
|
|
111
|
+
if (d) {
|
|
112
|
+
const day = daysBetween(startDate, d);
|
|
113
|
+
anchorEnd.set(id, day);
|
|
114
|
+
anchorResults.set(id, addDays(startDate, day));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Walk a sequential lane, returning the end-day of the last child.
|
|
119
|
+
const walkLane = (children: SwimlaneDeclaration['content'], baselineEnd: number): number => {
|
|
120
|
+
let prevEnd = baselineEnd;
|
|
121
|
+
for (const child of children) {
|
|
122
|
+
if (child.$type === 'DescriptionDirective') continue;
|
|
123
|
+
prevEnd = walkNode(child as ItemDeclaration | GroupBlock | ParallelBlock, prevEnd);
|
|
124
|
+
}
|
|
125
|
+
return prevEnd;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const walkNode = (
|
|
129
|
+
node: ItemDeclaration | GroupBlock | ParallelBlock,
|
|
130
|
+
prevEnd: number,
|
|
131
|
+
): number => {
|
|
132
|
+
if (isItemDeclaration(node)) {
|
|
133
|
+
const dur = deriveItemDurationDays(node.properties, sizes, cal);
|
|
134
|
+
const dateProp = parseDate(propValue(node.properties, 'date'));
|
|
135
|
+
const startProp = parseDate(propValue(node.properties, 'start'));
|
|
136
|
+
const afterRefs = propValues(node.properties, 'after');
|
|
137
|
+
let start = prevEnd;
|
|
138
|
+
if (dateProp) {
|
|
139
|
+
start = daysBetween(startDate, dateProp);
|
|
140
|
+
} else if (startProp) {
|
|
141
|
+
start = daysBetween(startDate, startProp);
|
|
142
|
+
} else if (afterRefs.length > 0) {
|
|
143
|
+
start = Math.max(prevEnd, ...afterRefs.map(resolveAfterDay));
|
|
144
|
+
}
|
|
145
|
+
const end = start + dur;
|
|
146
|
+
const scheduled: ScheduledItem = {
|
|
147
|
+
start: addDays(startDate, start),
|
|
148
|
+
end: addDays(startDate, end),
|
|
149
|
+
};
|
|
150
|
+
itemByNode.set(node, scheduled);
|
|
151
|
+
if (node.name) {
|
|
152
|
+
itemEnd.set(node.name, end);
|
|
153
|
+
itemResults.set(node.name, scheduled);
|
|
154
|
+
}
|
|
155
|
+
return end;
|
|
156
|
+
}
|
|
157
|
+
if (isParallelBlock(node)) {
|
|
158
|
+
const afterRefs = propValues(node.properties, 'after');
|
|
159
|
+
const containerStart =
|
|
160
|
+
afterRefs.length > 0
|
|
161
|
+
? Math.max(prevEnd, ...afterRefs.map(resolveAfterDay))
|
|
162
|
+
: prevEnd;
|
|
163
|
+
let parallelEnd = containerStart;
|
|
164
|
+
for (const child of node.content) {
|
|
165
|
+
if (child.$type === 'DescriptionDirective') continue;
|
|
166
|
+
const childEnd = walkNode(child as ItemDeclaration | GroupBlock, containerStart);
|
|
167
|
+
parallelEnd = Math.max(parallelEnd, childEnd);
|
|
168
|
+
}
|
|
169
|
+
return parallelEnd;
|
|
170
|
+
}
|
|
171
|
+
if (isGroupBlock(node)) {
|
|
172
|
+
const afterRefs = propValues(node.properties, 'after');
|
|
173
|
+
const containerStart =
|
|
174
|
+
afterRefs.length > 0
|
|
175
|
+
? Math.max(prevEnd, ...afterRefs.map(resolveAfterDay))
|
|
176
|
+
: prevEnd;
|
|
177
|
+
return walkLane(node.content as SwimlaneDeclaration['content'], containerStart);
|
|
178
|
+
}
|
|
179
|
+
return prevEnd;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
for (const lane of resolved.content.swimlanes.values()) {
|
|
183
|
+
walkLane(lane.content, 0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Milestones — same pass order as computeContentEndDay (after items so
|
|
187
|
+
// after: can resolve item end-days).
|
|
188
|
+
for (const [id, ms] of resolved.content.milestones) {
|
|
189
|
+
const d = parseDate(propValue(ms.properties, 'date'));
|
|
190
|
+
if (d) {
|
|
191
|
+
const day = daysBetween(startDate, d);
|
|
192
|
+
const resolved2 = addDays(startDate, day);
|
|
193
|
+
milestoneEnd.set(id, day);
|
|
194
|
+
milestoneResults.set(id, resolved2);
|
|
195
|
+
milestoneByNode.set(ms, resolved2);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const after = propValues(ms.properties, 'after');
|
|
199
|
+
if (after.length > 0) {
|
|
200
|
+
const day = Math.max(0, ...after.map(resolveAfterDay));
|
|
201
|
+
const resolved2 = addDays(startDate, day);
|
|
202
|
+
milestoneEnd.set(id, day);
|
|
203
|
+
milestoneResults.set(id, resolved2);
|
|
204
|
+
milestoneByNode.set(ms, resolved2);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
startDate,
|
|
210
|
+
items: itemResults,
|
|
211
|
+
byNode: itemByNode,
|
|
212
|
+
milestones: milestoneResults,
|
|
213
|
+
milestoneByNode,
|
|
214
|
+
anchors: anchorResults,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function utcMidnight(d: Date): Date {
|
|
219
|
+
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
220
|
+
}
|