calendaryjs 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.
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/dist/index.cjs +1356 -0
- package/dist/index.d.cts +1289 -0
- package/dist/index.d.ts +1289 -0
- package/dist/index.js +1341 -0
- package/package.json +79 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1341 @@
|
|
|
1
|
+
// src/cache/event-cache.ts
|
|
2
|
+
var EventCache = class {
|
|
3
|
+
cache = /* @__PURE__ */ new Map();
|
|
4
|
+
/**
|
|
5
|
+
* Get cached events or compute and cache them
|
|
6
|
+
*/
|
|
7
|
+
getOrCompute(key, computeFn) {
|
|
8
|
+
if (!this.cache.has(key)) {
|
|
9
|
+
this.cache.set(key, computeFn());
|
|
10
|
+
}
|
|
11
|
+
return this.cache.get(key);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Set cached events
|
|
15
|
+
*/
|
|
16
|
+
set(key, events) {
|
|
17
|
+
this.cache.set(key, events);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get cached events if available
|
|
21
|
+
*/
|
|
22
|
+
get(key) {
|
|
23
|
+
return this.cache.get(key);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Check if key exists in cache
|
|
27
|
+
*/
|
|
28
|
+
has(key) {
|
|
29
|
+
return this.cache.has(key);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Invalidate cache entry or entire cache
|
|
33
|
+
*/
|
|
34
|
+
invalidate(key) {
|
|
35
|
+
if (key) {
|
|
36
|
+
this.cache.delete(key);
|
|
37
|
+
} else {
|
|
38
|
+
this.cache.clear();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get all cached keys
|
|
43
|
+
*/
|
|
44
|
+
keys() {
|
|
45
|
+
return Array.from(this.cache.keys());
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get cache size
|
|
49
|
+
*/
|
|
50
|
+
get size() {
|
|
51
|
+
return this.cache.size;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/utils/date.ts
|
|
56
|
+
function formatDate(date) {
|
|
57
|
+
const year = date.getFullYear();
|
|
58
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
59
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
60
|
+
return `${year}-${month}-${day}`;
|
|
61
|
+
}
|
|
62
|
+
function parseDate(dateStr) {
|
|
63
|
+
const [year, month, day] = dateStr.split("-").map(Number);
|
|
64
|
+
return new Date(year, month - 1, day);
|
|
65
|
+
}
|
|
66
|
+
function getYear(date) {
|
|
67
|
+
if (typeof date === "string") {
|
|
68
|
+
return parseInt(date.split("-")[0], 10);
|
|
69
|
+
}
|
|
70
|
+
return date.getFullYear();
|
|
71
|
+
}
|
|
72
|
+
function isDateInRange(date, from, to) {
|
|
73
|
+
return date >= from && date <= to;
|
|
74
|
+
}
|
|
75
|
+
function getYearsInRange(from, to) {
|
|
76
|
+
const fromYear = getYear(from);
|
|
77
|
+
const toYear = getYear(to);
|
|
78
|
+
const years = [];
|
|
79
|
+
for (let year = fromYear; year <= toYear; year++) {
|
|
80
|
+
years.push(year);
|
|
81
|
+
}
|
|
82
|
+
return years;
|
|
83
|
+
}
|
|
84
|
+
function createDate(year, month, day) {
|
|
85
|
+
return new Date(year, month - 1, day);
|
|
86
|
+
}
|
|
87
|
+
function addDays(date, days) {
|
|
88
|
+
const result = new Date(date);
|
|
89
|
+
result.setDate(result.getDate() + days);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
function today() {
|
|
93
|
+
return formatDate(/* @__PURE__ */ new Date());
|
|
94
|
+
}
|
|
95
|
+
function daysFromToday(days) {
|
|
96
|
+
return formatDate(addDays(/* @__PURE__ */ new Date(), days));
|
|
97
|
+
}
|
|
98
|
+
function daysBetween(date1, date2) {
|
|
99
|
+
const oneDay = 24 * 60 * 60 * 1e3;
|
|
100
|
+
return Math.round((date2.getTime() - date1.getTime()) / oneDay);
|
|
101
|
+
}
|
|
102
|
+
function normalizeDateInput(input) {
|
|
103
|
+
if (typeof input === "string") {
|
|
104
|
+
return input;
|
|
105
|
+
}
|
|
106
|
+
const month = String(input.month).padStart(2, "0");
|
|
107
|
+
const day = String(input.day).padStart(2, "0");
|
|
108
|
+
return `${input.year}-${month}-${day}`;
|
|
109
|
+
}
|
|
110
|
+
function normalizeDateToDate(input) {
|
|
111
|
+
if (typeof input === "string") {
|
|
112
|
+
return parseDate(input);
|
|
113
|
+
}
|
|
114
|
+
return new Date(input.year, input.month - 1, input.day);
|
|
115
|
+
}
|
|
116
|
+
function isValidDateString(date) {
|
|
117
|
+
const match = /^\d{4}-\d{2}-\d{2}$/.exec(date);
|
|
118
|
+
if (!match) return false;
|
|
119
|
+
const d = new Date(date);
|
|
120
|
+
return !isNaN(d.getTime()) && d.toISOString().startsWith(date);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/generators/const.ts
|
|
124
|
+
var constEventHandler = {
|
|
125
|
+
validate(event) {
|
|
126
|
+
if (typeof event !== "object" || event === null) return false;
|
|
127
|
+
const e = event;
|
|
128
|
+
return e.type === "const" && typeof e.month === "number" && e.month >= 1 && e.month <= 12 && typeof e.day === "number" && e.day >= 1 && e.day <= 31 && typeof e.title === "string" && typeof e.id === "string";
|
|
129
|
+
},
|
|
130
|
+
generate(event, year) {
|
|
131
|
+
if (event.startYear !== void 0 && year < event.startYear) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
if (event.endYear !== void 0 && year > event.endYear) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
if (event.excludeYears?.includes(year)) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
return [createDate(year, event.month, event.day)];
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/generators/fixed.ts
|
|
145
|
+
var fixedEventHandler = {
|
|
146
|
+
validate(event) {
|
|
147
|
+
if (typeof event !== "object" || event === null) return false;
|
|
148
|
+
const e = event;
|
|
149
|
+
return e.type === "fixed" && typeof e.year === "number" && typeof e.month === "number" && e.month >= 1 && e.month <= 12 && typeof e.day === "number" && e.day >= 1 && e.day <= 31 && typeof e.title === "string" && typeof e.id === "string";
|
|
150
|
+
},
|
|
151
|
+
generate(event, year) {
|
|
152
|
+
if (event.year !== year) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
return [createDate(event.year, event.month, event.day)];
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/generators/monthly.ts
|
|
160
|
+
function getDaysInMonth(year, month) {
|
|
161
|
+
return new Date(year, month, 0).getDate();
|
|
162
|
+
}
|
|
163
|
+
var monthlyEventHandler = {
|
|
164
|
+
validate(event) {
|
|
165
|
+
if (typeof event !== "object" || event === null) return false;
|
|
166
|
+
const e = event;
|
|
167
|
+
return e.type === "monthly" && typeof e.day === "number" && e.day >= 1 && e.day <= 31 && typeof e.title === "string" && typeof e.id === "string";
|
|
168
|
+
},
|
|
169
|
+
generate(event, year) {
|
|
170
|
+
const dates = [];
|
|
171
|
+
const interval = event.interval ?? 1;
|
|
172
|
+
const startMonth = event.startMonth ?? 1;
|
|
173
|
+
for (let month = 1; month <= 12; month++) {
|
|
174
|
+
if (interval > 1) {
|
|
175
|
+
const monthOffset = month - startMonth;
|
|
176
|
+
const normalizedOffset = (monthOffset % 12 + 12) % 12;
|
|
177
|
+
if (normalizedOffset % interval !== 0) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (event.excludeMonths?.includes(month)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const daysInMonth = getDaysInMonth(year, month);
|
|
185
|
+
if (event.day > daysInMonth) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const date = createDate(year, month, event.day);
|
|
189
|
+
const dateStr = formatDate(date);
|
|
190
|
+
if (event.excludeDates?.includes(dateStr)) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
dates.push(date);
|
|
194
|
+
}
|
|
195
|
+
return dates;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// src/generators/weekly.ts
|
|
200
|
+
function getFirstDayOfWeekInYear(year, dayOfWeek) {
|
|
201
|
+
const jan1 = createDate(year, 1, 1);
|
|
202
|
+
const jan1DayOfWeek = jan1.getDay();
|
|
203
|
+
const daysUntilTarget = (dayOfWeek - jan1DayOfWeek + 7) % 7;
|
|
204
|
+
return addDays(jan1, daysUntilTarget);
|
|
205
|
+
}
|
|
206
|
+
function weekStart(date) {
|
|
207
|
+
return addDays(date, -date.getDay());
|
|
208
|
+
}
|
|
209
|
+
function weekdays(dayOfWeek) {
|
|
210
|
+
const list = Array.isArray(dayOfWeek) ? dayOfWeek : [dayOfWeek];
|
|
211
|
+
return [...new Set(list)].sort((a, b) => a - b);
|
|
212
|
+
}
|
|
213
|
+
function isWeekdayIndex(d) {
|
|
214
|
+
return typeof d === "number" && d >= 0 && d <= 6;
|
|
215
|
+
}
|
|
216
|
+
var weeklyEventHandler = {
|
|
217
|
+
validate(event) {
|
|
218
|
+
if (typeof event !== "object" || event === null) return false;
|
|
219
|
+
const e = event;
|
|
220
|
+
const dow = e.dayOfWeek;
|
|
221
|
+
const validDays = Array.isArray(dow) ? dow.length > 0 && dow.every(isWeekdayIndex) : isWeekdayIndex(dow);
|
|
222
|
+
return e.type === "weekly" && validDays && typeof e.title === "string" && typeof e.id === "string";
|
|
223
|
+
},
|
|
224
|
+
generate(event, year) {
|
|
225
|
+
const days = weekdays(event.dayOfWeek);
|
|
226
|
+
if (days.length === 0) return [];
|
|
227
|
+
const interval = event.interval ?? 1;
|
|
228
|
+
const yearEnd = createDate(year, 12, 31);
|
|
229
|
+
const startDateLimit = event.startDate ? parseDate(event.startDate) : null;
|
|
230
|
+
const endDateLimit = event.endDate ? parseDate(event.endDate) : null;
|
|
231
|
+
const referenceWeek = weekStart(startDateLimit ?? getFirstDayOfWeekInYear(year, days[0]));
|
|
232
|
+
const seen = /* @__PURE__ */ new Set();
|
|
233
|
+
const dates = [];
|
|
234
|
+
for (const day of days) {
|
|
235
|
+
let currentDate = getFirstDayOfWeekInYear(year, day);
|
|
236
|
+
while (currentDate <= yearEnd) {
|
|
237
|
+
const dateStr = formatDate(currentDate);
|
|
238
|
+
if (startDateLimit && currentDate < startDateLimit) {
|
|
239
|
+
currentDate = addDays(currentDate, 7);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (endDateLimit && currentDate > endDateLimit) {
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
if (interval > 1) {
|
|
246
|
+
const weeksFromReference = Math.round(
|
|
247
|
+
daysBetween(referenceWeek, weekStart(currentDate)) / 7
|
|
248
|
+
);
|
|
249
|
+
if (weeksFromReference < 0 || weeksFromReference % interval !== 0) {
|
|
250
|
+
currentDate = addDays(currentDate, 7);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!event.excludeDates?.includes(dateStr) && !seen.has(dateStr)) {
|
|
255
|
+
seen.add(dateStr);
|
|
256
|
+
dates.push(new Date(currentDate));
|
|
257
|
+
}
|
|
258
|
+
currentDate = addDays(currentDate, 7);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
dates.sort((a, b) => a.getTime() - b.getTime());
|
|
262
|
+
return dates;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// src/generators/nth-weekday.ts
|
|
267
|
+
function nthWeekdayInMonth(year, month, dayOfWeek, nth) {
|
|
268
|
+
if (nth === -1) {
|
|
269
|
+
const lastDay = new Date(year, month, 0).getDate();
|
|
270
|
+
for (let d = lastDay; d >= 1; d--) {
|
|
271
|
+
const date = createDate(year, month, d);
|
|
272
|
+
if (date.getDay() === dayOfWeek) return date;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
let count = 0;
|
|
277
|
+
const daysInMonth = new Date(year, month, 0).getDate();
|
|
278
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
279
|
+
const date = createDate(year, month, d);
|
|
280
|
+
if (date.getDay() === dayOfWeek) {
|
|
281
|
+
count++;
|
|
282
|
+
if (count === nth) return date;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
function firstWeekdayOnOrAfter(year, anchor, dayOfWeek) {
|
|
288
|
+
for (let offset = 0; offset <= 6; offset++) {
|
|
289
|
+
const date = createDate(year, anchor.month, anchor.day + offset);
|
|
290
|
+
if (date.getDay() === dayOfWeek) return date;
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
function anchorToDate(year, anchor) {
|
|
295
|
+
return createDate(year, anchor.month, anchor.day);
|
|
296
|
+
}
|
|
297
|
+
var nthWeekdayEventHandler = {
|
|
298
|
+
validate(event) {
|
|
299
|
+
if (typeof event !== "object" || event === null) return false;
|
|
300
|
+
const e = event;
|
|
301
|
+
if (e.type !== "nth-weekday") return false;
|
|
302
|
+
if (typeof e.id !== "string" || typeof e.title !== "string") return false;
|
|
303
|
+
if (typeof e.dayOfWeek !== "number" || e.dayOfWeek < 0 || e.dayOfWeek > 6) return false;
|
|
304
|
+
const hasSimple = e.nth !== void 0 || e.month !== void 0;
|
|
305
|
+
const hasAnchored = e.after !== void 0;
|
|
306
|
+
if (hasSimple && !hasAnchored) {
|
|
307
|
+
if (typeof e.nth !== "number" || ![1, 2, 3, 4, -1].includes(e.nth)) return false;
|
|
308
|
+
if (typeof e.month !== "number" || e.month < 1 || e.month > 12)
|
|
309
|
+
return false;
|
|
310
|
+
} else if (!hasAnchored) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
},
|
|
315
|
+
generate(event, year) {
|
|
316
|
+
if (event.startYear !== void 0 && year < event.startYear) return [];
|
|
317
|
+
if (event.endYear !== void 0 && year > event.endYear) return [];
|
|
318
|
+
if (event.excludeYears?.includes(year)) return [];
|
|
319
|
+
if (event.after !== void 0) {
|
|
320
|
+
const candidate = firstWeekdayOnOrAfter(year, event.after, event.dayOfWeek);
|
|
321
|
+
if (event.before !== void 0) {
|
|
322
|
+
const beforeDate = anchorToDate(year, event.before);
|
|
323
|
+
if (candidate === null || candidate >= beforeDate) {
|
|
324
|
+
if (event.fallback !== void 0) {
|
|
325
|
+
return [anchorToDate(year, event.fallback)];
|
|
326
|
+
}
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return candidate ? [candidate] : [];
|
|
331
|
+
}
|
|
332
|
+
if (event.nth !== void 0 && event.month !== void 0) {
|
|
333
|
+
const date = nthWeekdayInMonth(year, event.month, event.dayOfWeek, event.nth);
|
|
334
|
+
return date ? [date] : [];
|
|
335
|
+
}
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// src/generators/formula.ts
|
|
341
|
+
function createFormulaEventHandler(getFormula) {
|
|
342
|
+
return {
|
|
343
|
+
validate(event) {
|
|
344
|
+
if (typeof event !== "object" || event === null) return false;
|
|
345
|
+
const e = event;
|
|
346
|
+
return e.type === "formula" && typeof e.formula === "string" && typeof e.title === "string" && typeof e.id === "string";
|
|
347
|
+
},
|
|
348
|
+
generate(event, year) {
|
|
349
|
+
const formula = getFormula(event.formula);
|
|
350
|
+
if (!formula) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`Formula "${event.formula}" is not registered. Register it before generating events.`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
return [formula(year, event.params)];
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/generators/relative.ts
|
|
361
|
+
function createRelativeEventHandler(getAnchor) {
|
|
362
|
+
return {
|
|
363
|
+
validate(event) {
|
|
364
|
+
if (typeof event !== "object" || event === null) return false;
|
|
365
|
+
const e = event;
|
|
366
|
+
return e.type === "relative" && typeof e.anchor === "string" && typeof e.id === "string" && typeof e.title === "string";
|
|
367
|
+
},
|
|
368
|
+
generate(event, year) {
|
|
369
|
+
const resolve = getAnchor(event.anchor);
|
|
370
|
+
if (!resolve) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`Anchor "${event.anchor}" is not registered. Register it (e.g. cal.registerFormula) before generating events.`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const base = resolve(year);
|
|
376
|
+
const days = (event.offset.days ?? 0) + (event.offset.weeks ?? 0) * 7;
|
|
377
|
+
return [addDays(base, days)];
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/query/event-index.ts
|
|
383
|
+
var EventIndex = class {
|
|
384
|
+
byDate = /* @__PURE__ */ new Map();
|
|
385
|
+
byGroup = /* @__PURE__ */ new Map();
|
|
386
|
+
allEvents = [];
|
|
387
|
+
constructor(events = []) {
|
|
388
|
+
this.index(events);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Index events for fast lookups
|
|
392
|
+
*/
|
|
393
|
+
index(events) {
|
|
394
|
+
this.allEvents = events;
|
|
395
|
+
this.byDate.clear();
|
|
396
|
+
this.byGroup.clear();
|
|
397
|
+
for (const event of events) {
|
|
398
|
+
const dateEvents = this.byDate.get(event.date) || [];
|
|
399
|
+
dateEvents.push(event);
|
|
400
|
+
this.byDate.set(event.date, dateEvents);
|
|
401
|
+
const groupEvents = this.byGroup.get(event.groupId) || [];
|
|
402
|
+
groupEvents.push(event);
|
|
403
|
+
this.byGroup.set(event.groupId, groupEvents);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Query events by date
|
|
408
|
+
*/
|
|
409
|
+
queryByDate(date) {
|
|
410
|
+
return this.byDate.get(date) || [];
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Query events by group
|
|
414
|
+
*/
|
|
415
|
+
queryByGroup(groupId) {
|
|
416
|
+
return this.byGroup.get(groupId) || [];
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Query events in date range
|
|
420
|
+
*/
|
|
421
|
+
queryByDateRange(from, to) {
|
|
422
|
+
return this.allEvents.filter((event) => event.date >= from && event.date <= to);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Get all indexed events
|
|
426
|
+
*/
|
|
427
|
+
getAll() {
|
|
428
|
+
return this.allEvents;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Get count of indexed events
|
|
432
|
+
*/
|
|
433
|
+
get size() {
|
|
434
|
+
return this.allEvents.length;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// src/query/search-builder.ts
|
|
439
|
+
var SPECIAL_CHAR_MAP = {
|
|
440
|
+
\u0111: "d",
|
|
441
|
+
\u0110: "D",
|
|
442
|
+
\u0142: "l",
|
|
443
|
+
\u0141: "L",
|
|
444
|
+
\u00F8: "o",
|
|
445
|
+
\u00D8: "O",
|
|
446
|
+
\u00DF: "ss",
|
|
447
|
+
\u00E6: "ae",
|
|
448
|
+
\u00C6: "AE",
|
|
449
|
+
\u0153: "oe",
|
|
450
|
+
\u0152: "OE"
|
|
451
|
+
};
|
|
452
|
+
function normalizeText(text) {
|
|
453
|
+
let result = text;
|
|
454
|
+
for (const [char, replacement] of Object.entries(SPECIAL_CHAR_MAP)) {
|
|
455
|
+
result = result.replaceAll(char, replacement);
|
|
456
|
+
}
|
|
457
|
+
return result.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
458
|
+
}
|
|
459
|
+
function normalizeTextCompact(text) {
|
|
460
|
+
return normalizeText(text).replace(/\s+/g, "");
|
|
461
|
+
}
|
|
462
|
+
function parseDatePattern(search) {
|
|
463
|
+
const match = search.match(/^(\d{1,2})[/\-.](\d{1,2})$/);
|
|
464
|
+
if (!match) return null;
|
|
465
|
+
const num1 = parseInt(match[1], 10);
|
|
466
|
+
const num2 = parseInt(match[2], 10);
|
|
467
|
+
if (num1 >= 1 && num1 <= 31 && num2 >= 1 && num2 <= 12) {
|
|
468
|
+
return { day: num1, month: num2 };
|
|
469
|
+
}
|
|
470
|
+
if (num1 >= 1 && num1 <= 12 && num2 >= 1 && num2 <= 31) {
|
|
471
|
+
return { day: num2, month: num1 };
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
function matchesDatePattern(event, datePattern) {
|
|
476
|
+
const [, month, day] = event.date.split("-").map(Number);
|
|
477
|
+
return day === datePattern.day && month === datePattern.month;
|
|
478
|
+
}
|
|
479
|
+
function containsTextFuzzy(text, search) {
|
|
480
|
+
if (!text) return false;
|
|
481
|
+
const textLower = text.toLowerCase();
|
|
482
|
+
const searchLower = search.toLowerCase();
|
|
483
|
+
if (textLower.includes(searchLower)) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
const textNormalized = normalizeText(text);
|
|
487
|
+
const searchNormalized = normalizeText(search);
|
|
488
|
+
if (textNormalized.includes(searchNormalized)) {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
const textCompact = normalizeTextCompact(text);
|
|
492
|
+
const searchCompact = normalizeTextCompact(search);
|
|
493
|
+
return textCompact.includes(searchCompact);
|
|
494
|
+
}
|
|
495
|
+
function matchesMetadata(metadata, query) {
|
|
496
|
+
if (!metadata) return false;
|
|
497
|
+
for (const [key, queryValue] of Object.entries(query)) {
|
|
498
|
+
const metadataValue = metadata[key];
|
|
499
|
+
if (queryValue !== null && typeof queryValue === "object" && !Array.isArray(queryValue)) {
|
|
500
|
+
if (typeof metadataValue !== "object" || metadataValue === null || Array.isArray(metadataValue)) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
if (!matchesMetadata(metadataValue, queryValue)) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
} else if (Array.isArray(queryValue)) {
|
|
507
|
+
if (!queryValue.includes(metadataValue)) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
} else if (typeof queryValue === "string" && typeof metadataValue === "string") {
|
|
511
|
+
if (!metadataValue.toLowerCase().includes(queryValue.toLowerCase())) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
} else {
|
|
515
|
+
if (metadataValue !== queryValue) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
function compareEvents(a, b, sortBy, sortOrder) {
|
|
523
|
+
let comparison = 0;
|
|
524
|
+
switch (sortBy) {
|
|
525
|
+
case "date":
|
|
526
|
+
comparison = a.date.localeCompare(b.date);
|
|
527
|
+
break;
|
|
528
|
+
case "priority":
|
|
529
|
+
comparison = (a.priority ?? 0) - (b.priority ?? 0);
|
|
530
|
+
break;
|
|
531
|
+
case "id":
|
|
532
|
+
comparison = a.id.localeCompare(b.id);
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
return sortOrder === "desc" ? -comparison : comparison;
|
|
536
|
+
}
|
|
537
|
+
var SearchBuilder = class {
|
|
538
|
+
options = {};
|
|
539
|
+
executor;
|
|
540
|
+
constructor(executor) {
|
|
541
|
+
this.executor = executor;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Search text in title and description (case-insensitive)
|
|
545
|
+
*/
|
|
546
|
+
text(search) {
|
|
547
|
+
this.options.search = search;
|
|
548
|
+
return this;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Search text in title only (case-insensitive)
|
|
552
|
+
*/
|
|
553
|
+
title(search) {
|
|
554
|
+
this.options.titleContains = search;
|
|
555
|
+
return this;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Search text in id only (case-insensitive)
|
|
559
|
+
*/
|
|
560
|
+
id(search) {
|
|
561
|
+
this.options.idContains = search;
|
|
562
|
+
return this;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Search text in description only (case-insensitive)
|
|
566
|
+
*/
|
|
567
|
+
description(search) {
|
|
568
|
+
this.options.descriptionContains = search;
|
|
569
|
+
return this;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Search by keywords (match any)
|
|
573
|
+
*/
|
|
574
|
+
keywords(keywords) {
|
|
575
|
+
this.options.keywordsContain = keywords;
|
|
576
|
+
return this;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Search by single keyword
|
|
580
|
+
*/
|
|
581
|
+
keyword(keyword) {
|
|
582
|
+
this.options.keywordsContain = [keyword];
|
|
583
|
+
return this;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Filter by date range (inclusive on both ends)
|
|
587
|
+
*/
|
|
588
|
+
range(from, to) {
|
|
589
|
+
this.options.from = from;
|
|
590
|
+
this.options.to = to;
|
|
591
|
+
return this;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Filter events from this date onwards (inclusive, uses >=)
|
|
595
|
+
* @param date - Start date in YYYY-MM-DD format
|
|
596
|
+
*/
|
|
597
|
+
dateFrom(date) {
|
|
598
|
+
this.options.from = date;
|
|
599
|
+
return this;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Filter events up to this date (inclusive, uses <=)
|
|
603
|
+
* @param date - End date in YYYY-MM-DD format
|
|
604
|
+
*/
|
|
605
|
+
dateTo(date) {
|
|
606
|
+
this.options.to = date;
|
|
607
|
+
return this;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Filter by specific date
|
|
611
|
+
*/
|
|
612
|
+
date(date) {
|
|
613
|
+
this.options.date = date;
|
|
614
|
+
return this;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Filter by groups
|
|
618
|
+
*/
|
|
619
|
+
groups(groupIds) {
|
|
620
|
+
this.options.groups = groupIds;
|
|
621
|
+
return this;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Filter by single group
|
|
625
|
+
*/
|
|
626
|
+
group(groupId) {
|
|
627
|
+
this.options.groups = [groupId];
|
|
628
|
+
return this;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Filter by categories (match any)
|
|
632
|
+
*/
|
|
633
|
+
categories(categories) {
|
|
634
|
+
this.options.categories = categories;
|
|
635
|
+
return this;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Filter by category (single)
|
|
639
|
+
*/
|
|
640
|
+
category(category) {
|
|
641
|
+
this.options.categories = [category];
|
|
642
|
+
return this;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Filter by categories (must have all)
|
|
646
|
+
*/
|
|
647
|
+
hasAllCategories(categories) {
|
|
648
|
+
this.options.hasAllCategories = categories;
|
|
649
|
+
return this;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Search by metadata key-value pairs.
|
|
653
|
+
* Supports nested objects and case-insensitive string matching.
|
|
654
|
+
*
|
|
655
|
+
* @example
|
|
656
|
+
* ```typescript
|
|
657
|
+
* // Simple key-value
|
|
658
|
+
* .metadata({ season: 'advent' })
|
|
659
|
+
*
|
|
660
|
+
* // Nested object
|
|
661
|
+
* .metadata({ location: { country: 'fr' } })
|
|
662
|
+
*
|
|
663
|
+
* // Multiple conditions (AND)
|
|
664
|
+
* .metadata({ rank: 'solemnity', vestmentColor: 'white' })
|
|
665
|
+
* ```
|
|
666
|
+
*/
|
|
667
|
+
metadata(query) {
|
|
668
|
+
this.options.metadata = { ...this.options.metadata, ...query };
|
|
669
|
+
return this;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Filter by minimum priority (higher = more important; default 0)
|
|
673
|
+
*/
|
|
674
|
+
minPriority(priority) {
|
|
675
|
+
this.options.minPriority = priority;
|
|
676
|
+
return this;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Filter by maximum priority (higher = more important; default 0)
|
|
680
|
+
*/
|
|
681
|
+
maxPriority(priority) {
|
|
682
|
+
this.options.maxPriority = priority;
|
|
683
|
+
return this;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Sort results by field and order
|
|
687
|
+
*/
|
|
688
|
+
sortBy(field, order = "asc") {
|
|
689
|
+
this.options.sortBy = field;
|
|
690
|
+
this.options.sortOrder = order;
|
|
691
|
+
return this;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Limit number of results
|
|
695
|
+
*/
|
|
696
|
+
limit(count) {
|
|
697
|
+
this.options.limit = count;
|
|
698
|
+
return this;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Offset results (for pagination)
|
|
702
|
+
*/
|
|
703
|
+
offset(count) {
|
|
704
|
+
this.options.offset = count;
|
|
705
|
+
return this;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Get the first matching event or undefined
|
|
709
|
+
*/
|
|
710
|
+
first() {
|
|
711
|
+
this.options.limit = 1;
|
|
712
|
+
const results = this.getEvents();
|
|
713
|
+
return results[0];
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Check if any events match the search criteria
|
|
717
|
+
*/
|
|
718
|
+
exists() {
|
|
719
|
+
this.options.limit = 1;
|
|
720
|
+
return this.getEvents().length > 0;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Count matching events
|
|
724
|
+
*/
|
|
725
|
+
count() {
|
|
726
|
+
const savedLimit = this.options.limit;
|
|
727
|
+
const savedOffset = this.options.offset;
|
|
728
|
+
this.options.limit = void 0;
|
|
729
|
+
this.options.offset = void 0;
|
|
730
|
+
const count = this.getEvents().length;
|
|
731
|
+
this.options.limit = savedLimit;
|
|
732
|
+
this.options.offset = savedOffset;
|
|
733
|
+
return count;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Get matching events with typed result.
|
|
737
|
+
* Returns only the events that match the search criteria.
|
|
738
|
+
*
|
|
739
|
+
* @returns Array of matching events
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* ```typescript
|
|
743
|
+
* // Basic usage
|
|
744
|
+
* const events = cal.search().text('christmas').getEvents();
|
|
745
|
+
*
|
|
746
|
+
* // With typed calendar (generics flow automatically)
|
|
747
|
+
* const cal = calendary<LiturgicalMetadata>();
|
|
748
|
+
* const events = cal.search().metadata({ rank: 'solemnity' }).getEvents();
|
|
749
|
+
* // events[0].metadata?.rank is typed!
|
|
750
|
+
* ```
|
|
751
|
+
*/
|
|
752
|
+
getEvents() {
|
|
753
|
+
return this.executor(this.options);
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Get calendar days containing matching events.
|
|
757
|
+
* Returns days with their events sorted by keyword match score (highest first),
|
|
758
|
+
* then by priority.
|
|
759
|
+
*
|
|
760
|
+
* @returns Array of calendar days containing matching events
|
|
761
|
+
*
|
|
762
|
+
* @example
|
|
763
|
+
* ```typescript
|
|
764
|
+
* // Basic usage - get days with matching events
|
|
765
|
+
* const days = cal.search().text('christmas').getDays();
|
|
766
|
+
*
|
|
767
|
+
* // With typed calendar (generics flow automatically)
|
|
768
|
+
* const cal = calendary<LiturgicalMetadata>();
|
|
769
|
+
* const days = cal.search().category('holiday').getDays();
|
|
770
|
+
* // days[0].events[0].metadata?.rank is typed!
|
|
771
|
+
* ```
|
|
772
|
+
*/
|
|
773
|
+
getDays() {
|
|
774
|
+
const events = this.executor(this.options);
|
|
775
|
+
const searchKeywords = this.options.keywordsContain ?? [];
|
|
776
|
+
const searchText = this.options.search?.trim().toLowerCase();
|
|
777
|
+
const eventsByDate = /* @__PURE__ */ new Map();
|
|
778
|
+
for (const event of events) {
|
|
779
|
+
const existing = eventsByDate.get(event.date);
|
|
780
|
+
if (existing) {
|
|
781
|
+
existing.push(event);
|
|
782
|
+
} else {
|
|
783
|
+
eventsByDate.set(event.date, [event]);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const days = [];
|
|
787
|
+
for (const [date, dayEvents] of eventsByDate) {
|
|
788
|
+
const [year, month, day] = date.split("-").map(Number);
|
|
789
|
+
const dateObj = new Date(year, month - 1, day);
|
|
790
|
+
const dayOfWeek = dateObj.getDay();
|
|
791
|
+
const sortedEvents = dayEvents.sort((a, b) => {
|
|
792
|
+
const scoreA = calculateKeywordMatchScore(a, searchKeywords, searchText);
|
|
793
|
+
const scoreB = calculateKeywordMatchScore(b, searchKeywords, searchText);
|
|
794
|
+
if (scoreB !== scoreA) {
|
|
795
|
+
return scoreB - scoreA;
|
|
796
|
+
}
|
|
797
|
+
return (b.priority ?? 0) - (a.priority ?? 0);
|
|
798
|
+
});
|
|
799
|
+
days.push({
|
|
800
|
+
date,
|
|
801
|
+
year,
|
|
802
|
+
month,
|
|
803
|
+
day,
|
|
804
|
+
dayOfWeek,
|
|
805
|
+
isSunday: dayOfWeek === 0,
|
|
806
|
+
isWeekday: dayOfWeek >= 1 && dayOfWeek <= 6,
|
|
807
|
+
events: sortedEvents
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
days.sort((a, b) => a.date.localeCompare(b.date));
|
|
811
|
+
return days;
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
function calculateKeywordMatchScore(event, searchKeywords, searchText) {
|
|
815
|
+
let score = 0;
|
|
816
|
+
const eventKeywords = event.keywords ?? [];
|
|
817
|
+
for (const searchKw of searchKeywords) {
|
|
818
|
+
const searchNormalized = normalizeText(searchKw);
|
|
819
|
+
for (const eventKw of eventKeywords) {
|
|
820
|
+
const eventNormalized = normalizeText(eventKw);
|
|
821
|
+
if (eventNormalized === searchNormalized) {
|
|
822
|
+
score += 3;
|
|
823
|
+
} else if (eventNormalized.includes(searchNormalized)) {
|
|
824
|
+
score += 1;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (searchText) {
|
|
829
|
+
const searchNormalized = normalizeText(searchText);
|
|
830
|
+
for (const eventKw of eventKeywords) {
|
|
831
|
+
const eventNormalized = normalizeText(eventKw);
|
|
832
|
+
if (eventNormalized === searchNormalized) {
|
|
833
|
+
score += 3;
|
|
834
|
+
} else if (eventNormalized.includes(searchNormalized)) {
|
|
835
|
+
score += 1;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
const titleNormalized = normalizeText(event.title);
|
|
839
|
+
if (titleNormalized.includes(searchNormalized)) {
|
|
840
|
+
score += 2;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return score;
|
|
844
|
+
}
|
|
845
|
+
function executeSearch(events, options) {
|
|
846
|
+
let results = [...events];
|
|
847
|
+
if (options.date) {
|
|
848
|
+
results = results.filter((e) => e.date === options.date);
|
|
849
|
+
} else {
|
|
850
|
+
if (options.from) {
|
|
851
|
+
results = results.filter((e) => e.date >= options.from);
|
|
852
|
+
}
|
|
853
|
+
if (options.to) {
|
|
854
|
+
results = results.filter((e) => e.date <= options.to);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (options.groups && options.groups.length > 0) {
|
|
858
|
+
results = results.filter((e) => options.groups.includes(e.groupId));
|
|
859
|
+
}
|
|
860
|
+
if (options.search) {
|
|
861
|
+
const search = options.search.trim();
|
|
862
|
+
const datePattern = parseDatePattern(search);
|
|
863
|
+
if (datePattern) {
|
|
864
|
+
results = results.filter((e) => matchesDatePattern(e, datePattern));
|
|
865
|
+
} else {
|
|
866
|
+
results = results.filter(
|
|
867
|
+
(e) => containsTextFuzzy(e.title, search) || containsTextFuzzy(e.id, search) || containsTextFuzzy(e.description, search) || e.keywords && e.keywords.some((k) => containsTextFuzzy(k, search))
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (options.titleContains) {
|
|
872
|
+
results = results.filter((e) => containsTextFuzzy(e.title, options.titleContains));
|
|
873
|
+
}
|
|
874
|
+
if (options.idContains) {
|
|
875
|
+
results = results.filter((e) => containsTextFuzzy(e.id, options.idContains));
|
|
876
|
+
}
|
|
877
|
+
if (options.descriptionContains) {
|
|
878
|
+
results = results.filter((e) => containsTextFuzzy(e.description, options.descriptionContains));
|
|
879
|
+
}
|
|
880
|
+
if (options.keywordsContain && options.keywordsContain.length > 0) {
|
|
881
|
+
results = results.filter((e) => {
|
|
882
|
+
if (!e.keywords) return false;
|
|
883
|
+
return options.keywordsContain.some((kw) => e.keywords.some((k) => containsTextFuzzy(k, kw)));
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
if (options.categories && options.categories.length > 0) {
|
|
887
|
+
results = results.filter((e) => {
|
|
888
|
+
if (!e.categories) return false;
|
|
889
|
+
return options.categories.some((cat) => e.categories.includes(cat));
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
if (options.hasAllCategories && options.hasAllCategories.length > 0) {
|
|
893
|
+
results = results.filter((e) => {
|
|
894
|
+
if (!e.categories) return false;
|
|
895
|
+
return options.hasAllCategories.every((cat) => e.categories.includes(cat));
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
if (options.metadata) {
|
|
899
|
+
results = results.filter((e) => matchesMetadata(e.metadata, options.metadata));
|
|
900
|
+
}
|
|
901
|
+
if (options.minPriority !== void 0) {
|
|
902
|
+
results = results.filter((e) => (e.priority ?? 0) >= options.minPriority);
|
|
903
|
+
}
|
|
904
|
+
if (options.maxPriority !== void 0) {
|
|
905
|
+
results = results.filter((e) => (e.priority ?? 0) <= options.maxPriority);
|
|
906
|
+
}
|
|
907
|
+
if (options.sortBy) {
|
|
908
|
+
const sortOrder = options.sortOrder ?? "asc";
|
|
909
|
+
results.sort((a, b) => compareEvents(a, b, options.sortBy, sortOrder));
|
|
910
|
+
}
|
|
911
|
+
if (options.offset && options.offset > 0) {
|
|
912
|
+
results = results.slice(options.offset);
|
|
913
|
+
}
|
|
914
|
+
if (options.limit && options.limit > 0) {
|
|
915
|
+
results = results.slice(0, options.limit);
|
|
916
|
+
}
|
|
917
|
+
return results;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/utils/conflict.ts
|
|
921
|
+
var DEFAULT_ACTION = { action: "keep" };
|
|
922
|
+
function resolveConflict(resolver, date) {
|
|
923
|
+
if (!resolver) {
|
|
924
|
+
return DEFAULT_ACTION;
|
|
925
|
+
}
|
|
926
|
+
if (typeof resolver === "function") {
|
|
927
|
+
return resolver(date);
|
|
928
|
+
}
|
|
929
|
+
return resolver[date] ?? DEFAULT_ACTION;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/utils/id.ts
|
|
933
|
+
function generateEventId(sourceEventId, date) {
|
|
934
|
+
return `${sourceEventId}:${date}`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/utils/template.ts
|
|
938
|
+
function interpolateDateTokens(template, date) {
|
|
939
|
+
const [y, m, d] = date.split("-");
|
|
940
|
+
const tokens = {
|
|
941
|
+
YYYY: y,
|
|
942
|
+
MM: m,
|
|
943
|
+
DD: d,
|
|
944
|
+
M: String(Number(m)),
|
|
945
|
+
D: String(Number(d))
|
|
946
|
+
};
|
|
947
|
+
return template.replace(/\{(YYYY|MM|DD|M|D)\}/g, (_match, token) => tokens[token]);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/core.ts
|
|
951
|
+
var IS_CALENDARY = "$isCalendary";
|
|
952
|
+
var PASSTHROUGH_KEYS = [
|
|
953
|
+
"keywords",
|
|
954
|
+
"allDay",
|
|
955
|
+
"startTime",
|
|
956
|
+
"endTime",
|
|
957
|
+
"duration",
|
|
958
|
+
"description",
|
|
959
|
+
"location",
|
|
960
|
+
"url",
|
|
961
|
+
"source",
|
|
962
|
+
"categories",
|
|
963
|
+
"priority",
|
|
964
|
+
"status",
|
|
965
|
+
"icon",
|
|
966
|
+
"metadata",
|
|
967
|
+
"reminders"
|
|
968
|
+
];
|
|
969
|
+
var isCalendary = (d) => d instanceof CalendaryInstance || !!(d && d[IS_CALENDARY]);
|
|
970
|
+
var isBuildable = (e) => typeof e.build === "function";
|
|
971
|
+
var CalendaryInstance = class _CalendaryInstance {
|
|
972
|
+
[IS_CALENDARY] = true;
|
|
973
|
+
plugins = /* @__PURE__ */ new Map();
|
|
974
|
+
eventTypeHandlers = /* @__PURE__ */ new Map();
|
|
975
|
+
formulas = /* @__PURE__ */ new Map();
|
|
976
|
+
groups = /* @__PURE__ */ new Map();
|
|
977
|
+
cache = new EventCache();
|
|
978
|
+
index = new EventIndex();
|
|
979
|
+
dirty = true;
|
|
980
|
+
extensionData = {};
|
|
981
|
+
dayEnrichers = [];
|
|
982
|
+
constructor(cfg = {}) {
|
|
983
|
+
this.extensionData = cfg.x || {};
|
|
984
|
+
this.registerCoreHandlers();
|
|
985
|
+
}
|
|
986
|
+
registerCoreHandlers() {
|
|
987
|
+
this.eventTypeHandlers.set("const", constEventHandler);
|
|
988
|
+
this.eventTypeHandlers.set("fixed", fixedEventHandler);
|
|
989
|
+
this.eventTypeHandlers.set("monthly", monthlyEventHandler);
|
|
990
|
+
this.eventTypeHandlers.set("weekly", weeklyEventHandler);
|
|
991
|
+
this.eventTypeHandlers.set("nth-weekday", nthWeekdayEventHandler);
|
|
992
|
+
this.eventTypeHandlers.set(
|
|
993
|
+
"formula",
|
|
994
|
+
createFormulaEventHandler((name) => this.formulas.get(name))
|
|
995
|
+
);
|
|
996
|
+
this.eventTypeHandlers.set(
|
|
997
|
+
"relative",
|
|
998
|
+
createRelativeEventHandler((name) => this.formulas.get(name))
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Clone this instance
|
|
1003
|
+
*/
|
|
1004
|
+
clone() {
|
|
1005
|
+
const cloned = new _CalendaryInstance({
|
|
1006
|
+
x: { ...this.extensionData }
|
|
1007
|
+
});
|
|
1008
|
+
cloned.plugins = new Map(this.plugins);
|
|
1009
|
+
cloned.eventTypeHandlers = new Map(this.eventTypeHandlers);
|
|
1010
|
+
cloned.formulas = new Map(this.formulas);
|
|
1011
|
+
cloned.groups = new Map(this.groups);
|
|
1012
|
+
cloned.dayEnrichers = [...this.dayEnrichers];
|
|
1013
|
+
return cloned;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Register a plugin (Day.js style: use/extend)
|
|
1017
|
+
*/
|
|
1018
|
+
use(plugin) {
|
|
1019
|
+
for (const dep of plugin.dependencies || []) {
|
|
1020
|
+
if (!this.plugins.has(dep)) {
|
|
1021
|
+
throw new Error(`Plugin "${plugin.name}" requires "${dep}"`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (plugin.eventTypes) {
|
|
1025
|
+
for (const [type, handler] of Object.entries(plugin.eventTypes)) {
|
|
1026
|
+
this.eventTypeHandlers.set(type, handler);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (plugin.formulas) {
|
|
1030
|
+
for (const [name, fn] of Object.entries(plugin.formulas)) {
|
|
1031
|
+
this.formulas.set(name, fn);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const context = {};
|
|
1035
|
+
plugin.install(this, context);
|
|
1036
|
+
this.plugins.set(plugin.name, plugin);
|
|
1037
|
+
this.dirty = true;
|
|
1038
|
+
return this;
|
|
1039
|
+
}
|
|
1040
|
+
hasPlugin(name) {
|
|
1041
|
+
return this.plugins.has(name);
|
|
1042
|
+
}
|
|
1043
|
+
registerFormula(name, fn) {
|
|
1044
|
+
this.formulas.set(name, fn);
|
|
1045
|
+
this.dirty = true;
|
|
1046
|
+
return this;
|
|
1047
|
+
}
|
|
1048
|
+
addGroup(config) {
|
|
1049
|
+
const events = config.events.map((e) => isBuildable(e) ? e.build() : e);
|
|
1050
|
+
const group = {
|
|
1051
|
+
...config,
|
|
1052
|
+
events,
|
|
1053
|
+
enabled: config.enabled ?? true
|
|
1054
|
+
};
|
|
1055
|
+
for (const event of group.events) {
|
|
1056
|
+
if (!this.eventTypeHandlers.has(event.type)) {
|
|
1057
|
+
throw new Error(
|
|
1058
|
+
`Event type "${event.type}" is not supported. Did you forget to register a plugin?`
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
this.groups.set(group.id, group);
|
|
1063
|
+
this.dirty = true;
|
|
1064
|
+
return this;
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Load a {@link Collection} — a portable bundle of events (a `.cdy` document
|
|
1068
|
+
* is its JSON form). Pass an object or a JSON string. The declared plugins must
|
|
1069
|
+
* already be registered (`cal.use(...)`); a clear error lists any that aren't.
|
|
1070
|
+
* Nothing is fetched or executed behind your back.
|
|
1071
|
+
*/
|
|
1072
|
+
load(collection) {
|
|
1073
|
+
const c = typeof collection === "string" ? JSON.parse(collection) : collection;
|
|
1074
|
+
const missing = (c.plugins ?? []).filter((name) => !this.hasPlugin(name));
|
|
1075
|
+
if (missing.length > 0) {
|
|
1076
|
+
const which = c.collection ? `"${c.collection}" ` : "";
|
|
1077
|
+
throw new Error(
|
|
1078
|
+
`Collection ${which}requires plugin(s) not registered: ${missing.join(", ")}. Install them and call cal.use(...) before loading.`
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
return this.addGroup({
|
|
1082
|
+
id: c.id ?? c.collection ?? "collection",
|
|
1083
|
+
name: c.name,
|
|
1084
|
+
events: c.events
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
removeGroup(groupId) {
|
|
1088
|
+
this.groups.delete(groupId);
|
|
1089
|
+
this.dirty = true;
|
|
1090
|
+
return this;
|
|
1091
|
+
}
|
|
1092
|
+
getGroup(groupId) {
|
|
1093
|
+
return this.groups.get(groupId);
|
|
1094
|
+
}
|
|
1095
|
+
getGroups() {
|
|
1096
|
+
return Array.from(this.groups.values());
|
|
1097
|
+
}
|
|
1098
|
+
setGroupEnabled(groupId, enabled) {
|
|
1099
|
+
const group = this.groups.get(groupId);
|
|
1100
|
+
if (group) {
|
|
1101
|
+
group.enabled = enabled;
|
|
1102
|
+
this.dirty = true;
|
|
1103
|
+
}
|
|
1104
|
+
return this;
|
|
1105
|
+
}
|
|
1106
|
+
generateEvents(from, to) {
|
|
1107
|
+
const cacheKey = `${from}:${to}`;
|
|
1108
|
+
if (!this.dirty && this.cache.has(cacheKey)) {
|
|
1109
|
+
return this.cache.get(cacheKey);
|
|
1110
|
+
}
|
|
1111
|
+
const events = [];
|
|
1112
|
+
const years = getYearsInRange(from, to);
|
|
1113
|
+
for (const group of this.groups.values()) {
|
|
1114
|
+
if (!group.enabled) continue;
|
|
1115
|
+
for (const eventConfig of group.events) {
|
|
1116
|
+
const handler = this.eventTypeHandlers.get(eventConfig.type);
|
|
1117
|
+
if (!handler) continue;
|
|
1118
|
+
const directives = eventConfig;
|
|
1119
|
+
for (const year of years) {
|
|
1120
|
+
const dates = handler.generate(eventConfig, year);
|
|
1121
|
+
for (const date of dates) {
|
|
1122
|
+
let dateStr = formatDate(date);
|
|
1123
|
+
if (directives.startDate && dateStr < directives.startDate) continue;
|
|
1124
|
+
if (directives.endDate && dateStr > directives.endDate) continue;
|
|
1125
|
+
const exception = directives.exceptions?.[dateStr];
|
|
1126
|
+
if (exception && "skip" in exception) {
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
const override = exception && "override" in exception ? exception.override : void 0;
|
|
1130
|
+
const rescheduled = directives.overrideDates?.[dateStr];
|
|
1131
|
+
if (rescheduled) {
|
|
1132
|
+
dateStr = rescheduled;
|
|
1133
|
+
}
|
|
1134
|
+
if (directives.onConflict) {
|
|
1135
|
+
const conflictAction = resolveConflict(directives.onConflict, dateStr);
|
|
1136
|
+
if (conflictAction.action === "drop") {
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
if (conflictAction.action === "reschedule" && conflictAction.to) {
|
|
1140
|
+
dateStr = conflictAction.to;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (!isDateInRange(dateStr, from, to)) continue;
|
|
1144
|
+
const calendarEvent = this.createCalendarEvent(
|
|
1145
|
+
eventConfig,
|
|
1146
|
+
group,
|
|
1147
|
+
dateStr,
|
|
1148
|
+
override
|
|
1149
|
+
);
|
|
1150
|
+
events.push(calendarEvent);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
events.sort((a, b) => {
|
|
1156
|
+
const dateCompare = a.date.localeCompare(b.date);
|
|
1157
|
+
if (dateCompare !== 0) return dateCompare;
|
|
1158
|
+
return (b.priority ?? 0) - (a.priority ?? 0);
|
|
1159
|
+
});
|
|
1160
|
+
this.cache.set(cacheKey, events);
|
|
1161
|
+
this.index.index(events);
|
|
1162
|
+
this.dirty = false;
|
|
1163
|
+
return events;
|
|
1164
|
+
}
|
|
1165
|
+
createCalendarEvent(config, group, dateStr, override) {
|
|
1166
|
+
const base = config;
|
|
1167
|
+
const event = {
|
|
1168
|
+
id: generateEventId(base.id, dateStr),
|
|
1169
|
+
sourceEventId: base.id,
|
|
1170
|
+
title: base.title,
|
|
1171
|
+
date: dateStr,
|
|
1172
|
+
groupId: group.id
|
|
1173
|
+
};
|
|
1174
|
+
const target = event;
|
|
1175
|
+
for (const key of PASSTHROUGH_KEYS) {
|
|
1176
|
+
const value = base[key];
|
|
1177
|
+
if (value !== void 0) target[key] = value;
|
|
1178
|
+
}
|
|
1179
|
+
if (group.name) event.groupName = group.name;
|
|
1180
|
+
const color = base.color ?? group.color;
|
|
1181
|
+
if (color) event.color = color;
|
|
1182
|
+
if (base.templates) {
|
|
1183
|
+
for (const [key, tpl] of Object.entries(base.templates)) {
|
|
1184
|
+
target[key] = interpolateDateTokens(tpl, dateStr);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if (override) Object.assign(event, override);
|
|
1188
|
+
return event;
|
|
1189
|
+
}
|
|
1190
|
+
getEvents(date) {
|
|
1191
|
+
return this.search().date(date).getEvents();
|
|
1192
|
+
}
|
|
1193
|
+
getEventsInRange(from, to) {
|
|
1194
|
+
return this.search().range(from, to).getEvents();
|
|
1195
|
+
}
|
|
1196
|
+
getEventsByGroup(groupId, from, to) {
|
|
1197
|
+
const builder = this.search().group(groupId);
|
|
1198
|
+
if (from && to) {
|
|
1199
|
+
builder.range(from, to);
|
|
1200
|
+
}
|
|
1201
|
+
return builder.getEvents();
|
|
1202
|
+
}
|
|
1203
|
+
getUpcomingEvents(days) {
|
|
1204
|
+
return this.search().range(today(), daysFromToday(days)).getEvents();
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Register a day enricher to add custom data to CalendarDay objects
|
|
1208
|
+
*/
|
|
1209
|
+
registerDayEnricher(enricher) {
|
|
1210
|
+
this.dayEnrichers.push(enricher);
|
|
1211
|
+
this.dayEnrichers.sort((a, b) => a.priority - b.priority);
|
|
1212
|
+
return this;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Check if a day enricher is registered
|
|
1216
|
+
*/
|
|
1217
|
+
hasDayEnricher(name) {
|
|
1218
|
+
return this.dayEnrichers.some((e) => e.name === name);
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Get calendar days for a date range
|
|
1222
|
+
*/
|
|
1223
|
+
getDays(options) {
|
|
1224
|
+
const { groups } = options;
|
|
1225
|
+
const fromStr = normalizeDateInput(options.from);
|
|
1226
|
+
const toStr = normalizeDateInput(options.to);
|
|
1227
|
+
let events = this.generateEvents(fromStr, toStr);
|
|
1228
|
+
if (groups && groups.length > 0) {
|
|
1229
|
+
events = events.filter((e) => groups.includes(e.groupId));
|
|
1230
|
+
}
|
|
1231
|
+
const eventsByDate = /* @__PURE__ */ new Map();
|
|
1232
|
+
for (const event of events) {
|
|
1233
|
+
const existing = eventsByDate.get(event.date) || [];
|
|
1234
|
+
existing.push(event);
|
|
1235
|
+
eventsByDate.set(event.date, existing);
|
|
1236
|
+
}
|
|
1237
|
+
const days = [];
|
|
1238
|
+
const startDate = normalizeDateToDate(options.from);
|
|
1239
|
+
const endDate = normalizeDateToDate(options.to);
|
|
1240
|
+
const current = new Date(startDate);
|
|
1241
|
+
while (current <= endDate) {
|
|
1242
|
+
const dateStr = formatDate(current);
|
|
1243
|
+
const dayOfWeek = current.getDay();
|
|
1244
|
+
const dayEvents = eventsByDate.get(dateStr) || [];
|
|
1245
|
+
dayEvents.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
1246
|
+
let day = {
|
|
1247
|
+
date: dateStr,
|
|
1248
|
+
year: current.getFullYear(),
|
|
1249
|
+
month: current.getMonth() + 1,
|
|
1250
|
+
day: current.getDate(),
|
|
1251
|
+
dayOfWeek,
|
|
1252
|
+
events: dayEvents,
|
|
1253
|
+
isSunday: dayOfWeek === 0,
|
|
1254
|
+
isWeekday: dayOfWeek !== 0
|
|
1255
|
+
};
|
|
1256
|
+
for (const enricher of this.dayEnrichers) {
|
|
1257
|
+
day = enricher.enrich(day);
|
|
1258
|
+
}
|
|
1259
|
+
days.push(day);
|
|
1260
|
+
current.setDate(current.getDate() + 1);
|
|
1261
|
+
}
|
|
1262
|
+
return days;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Get a single calendar day
|
|
1266
|
+
*/
|
|
1267
|
+
getDay(date) {
|
|
1268
|
+
const days = this.getDays({ from: date, to: date });
|
|
1269
|
+
return days[0];
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Create a search builder for advanced event queries.
|
|
1273
|
+
* Supports text search, metadata search, categories, and more.
|
|
1274
|
+
*
|
|
1275
|
+
* @example
|
|
1276
|
+
* ```typescript
|
|
1277
|
+
* // Search by text (case-insensitive)
|
|
1278
|
+
* cal.search().text('christmas').getEvents();
|
|
1279
|
+
*
|
|
1280
|
+
* // Search by metadata
|
|
1281
|
+
* cal.search().metadata({ season: 'advent' }).getEvents();
|
|
1282
|
+
*
|
|
1283
|
+
* // Combined search
|
|
1284
|
+
* cal.search()
|
|
1285
|
+
* .text('easter')
|
|
1286
|
+
* .categories(['liturgical'])
|
|
1287
|
+
* .range('2025-01-01', '2025-12-31')
|
|
1288
|
+
* .sortBy('date', 'asc')
|
|
1289
|
+
* .getEvents();
|
|
1290
|
+
* ```
|
|
1291
|
+
*/
|
|
1292
|
+
search() {
|
|
1293
|
+
return new SearchBuilder(
|
|
1294
|
+
(options) => this.executeSearch(options)
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
executeSearch(options) {
|
|
1298
|
+
const from = options.date ?? options.from ?? "1970-01-01";
|
|
1299
|
+
const to = options.date ?? options.to ?? "2100-12-31";
|
|
1300
|
+
const events = this.generateEvents(from, to);
|
|
1301
|
+
return executeSearch(events, options);
|
|
1302
|
+
}
|
|
1303
|
+
invalidateCache() {
|
|
1304
|
+
this.cache.invalidate();
|
|
1305
|
+
this.dirty = true;
|
|
1306
|
+
return this;
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
var calendary = function(cfg) {
|
|
1310
|
+
return new CalendaryInstance(cfg);
|
|
1311
|
+
};
|
|
1312
|
+
calendary.isCalendary = isCalendary;
|
|
1313
|
+
|
|
1314
|
+
// src/utils/merge.ts
|
|
1315
|
+
function shouldSkipValue(value) {
|
|
1316
|
+
if (value === void 0) return true;
|
|
1317
|
+
if (value === "") return true;
|
|
1318
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
function mergeEventConfig(base, custom, options = {}) {
|
|
1322
|
+
if (!custom) return base;
|
|
1323
|
+
const { skipFields = [] } = options;
|
|
1324
|
+
const result = { ...base };
|
|
1325
|
+
const { metadata: customMetadata, ...customFields } = custom;
|
|
1326
|
+
for (const [key, value] of Object.entries(customFields)) {
|
|
1327
|
+
if (skipFields.includes(key)) continue;
|
|
1328
|
+
if (shouldSkipValue(value)) continue;
|
|
1329
|
+
result[key] = value;
|
|
1330
|
+
}
|
|
1331
|
+
if (customMetadata && typeof customMetadata === "object" && Object.keys(customMetadata).length > 0) {
|
|
1332
|
+
const baseMetadata = base.metadata;
|
|
1333
|
+
result.metadata = {
|
|
1334
|
+
...baseMetadata,
|
|
1335
|
+
...customMetadata
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
return result;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
export { CalendaryInstance as Calendary, CalendaryInstance, SearchBuilder, addDays, calendary, daysBetween, formatDate, getYear, interpolateDateTokens, isCalendary, isValidDateString, mergeEventConfig, parseDate, resolveConflict };
|