@dytsou/calendar-build 2.0.1 → 2.1.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 (2) hide show
  1. package/package.json +1 -1
  2. package/worker.js +286 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dytsou/calendar-build",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Build script for calendar HTML from template and environment variables",
5
5
  "main": "scripts/build.js",
6
6
  "bin": {
package/worker.js CHANGED
@@ -225,11 +225,269 @@ function injectConsoleFilter(html) {
225
225
  return consoleFilterScript + html;
226
226
  }
227
227
 
228
+ /**
229
+ * Normalize date to ISO string for comparison
230
+ */
231
+ function normalizeDate(dateValue) {
232
+ if (!dateValue) return null;
233
+ if (typeof dateValue === 'string') {
234
+ // Parse and return ISO string
235
+ const date = new Date(dateValue);
236
+ return isNaN(date.getTime()) ? null : date.toISOString();
237
+ }
238
+ if (dateValue instanceof Date) {
239
+ return dateValue.toISOString();
240
+ }
241
+ // Try to convert to date
242
+ const date = new Date(dateValue);
243
+ return isNaN(date.getTime()) ? null : date.toISOString();
244
+ }
245
+
246
+ /**
247
+ * Extract event time fields (handles various field name formats)
248
+ */
249
+ function getEventTime(event, field) {
250
+ // Try common field name variations (including iCal and calendar.js formats)
251
+ const variations = {
252
+ start: ['start', 'startDate', 'start_time', 'startTime', 'dtstart', 'start_date', 'dateStart', 'date_start'],
253
+ end: ['end', 'endDate', 'end_time', 'endTime', 'dtend', 'end_date', 'dateEnd', 'date_end']
254
+ };
255
+
256
+ const fieldNames = variations[field] || [field];
257
+ for (const name of fieldNames) {
258
+ if (event[name] !== undefined && event[name] !== null) {
259
+ return event[name];
260
+ }
261
+ }
262
+ return null;
263
+ }
264
+
265
+ /**
266
+ * Extract event title (handles various field name formats)
267
+ */
268
+ function getEventTitle(event) {
269
+ return event.title || event.summary || event.name || '';
270
+ }
271
+
272
+ /**
273
+ * Extract event description (handles various field name formats)
274
+ */
275
+ function getEventDescription(event) {
276
+ return event.description || event.desc || '';
277
+ }
278
+
279
+ /**
280
+ * Extract calendar identifier (handles various field name formats)
281
+ */
282
+ function getCalendarId(event) {
283
+ return event.calendar || event.calendarId || event.calendar_id || event.source || 'default';
284
+ }
285
+
286
+ /**
287
+ * Merge consecutive events within the same calendar
288
+ * Events are consecutive if event1.end === event2.start (exact match)
289
+ */
290
+ function mergeConsecutiveEvents(events) {
291
+ if (!Array.isArray(events) || events.length === 0) {
292
+ return events;
293
+ }
294
+
295
+ // Group events by calendar identifier
296
+ const eventsByCalendar = {};
297
+ for (const event of events) {
298
+ const calendarId = getCalendarId(event);
299
+ if (!eventsByCalendar[calendarId]) {
300
+ eventsByCalendar[calendarId] = [];
301
+ }
302
+ eventsByCalendar[calendarId].push(event);
303
+ }
304
+
305
+ const mergedEvents = [];
306
+
307
+ // Process each calendar group separately
308
+ for (const calendarId in eventsByCalendar) {
309
+ const calendarEvents = eventsByCalendar[calendarId];
310
+
311
+ // Sort events by start time
312
+ calendarEvents.sort((a, b) => {
313
+ const startA = getEventTime(a, 'start');
314
+ const startB = getEventTime(b, 'start');
315
+ if (!startA || !startB) return 0;
316
+ return new Date(startA) - new Date(startB);
317
+ });
318
+
319
+ // Merge consecutive events
320
+ let currentMerge = null;
321
+
322
+ for (let i = 0; i < calendarEvents.length; i++) {
323
+ const event = calendarEvents[i];
324
+ const startTime = getEventTime(event, 'start');
325
+ const endTime = getEventTime(event, 'end');
326
+
327
+ if (!startTime || !endTime) {
328
+ // If event is missing time info, add as-is
329
+ if (currentMerge) {
330
+ mergedEvents.push(currentMerge);
331
+ currentMerge = null;
332
+ }
333
+ mergedEvents.push(event);
334
+ continue;
335
+ }
336
+
337
+ if (currentMerge === null) {
338
+ // Start a new merge group
339
+ currentMerge = { ...event };
340
+ } else {
341
+ // Check if this event is consecutive to the current merge
342
+ const currentEndTime = getEventTime(currentMerge, 'end');
343
+ // Normalize dates for comparison (handle different date formats)
344
+ const normalizedEndTime = normalizeDate(currentEndTime);
345
+ const normalizedStartTime = normalizeDate(startTime);
346
+
347
+ if (normalizedEndTime && normalizedStartTime && normalizedEndTime === normalizedStartTime) {
348
+ // Merge: combine titles and extend end time
349
+ const currentTitle = getEventTitle(currentMerge);
350
+ const eventTitle = getEventTitle(event);
351
+ const combinedTitle = currentTitle && eventTitle
352
+ ? `${currentTitle} + ${eventTitle}`
353
+ : currentTitle || eventTitle;
354
+
355
+ // Update title field (try common variations)
356
+ if (currentMerge.title !== undefined) currentMerge.title = combinedTitle;
357
+ if (currentMerge.summary !== undefined) currentMerge.summary = combinedTitle;
358
+ if (currentMerge.name !== undefined) currentMerge.name = combinedTitle;
359
+ if (!currentMerge.title && !currentMerge.summary && !currentMerge.name) {
360
+ currentMerge.title = combinedTitle;
361
+ }
362
+
363
+ // Combine descriptions
364
+ const currentDesc = getEventDescription(currentMerge);
365
+ const eventDesc = getEventDescription(event);
366
+ if (currentDesc || eventDesc) {
367
+ const combinedDesc = currentDesc && eventDesc
368
+ ? `${currentDesc}\n\n${eventDesc}`
369
+ : currentDesc || eventDesc;
370
+
371
+ if (currentMerge.description !== undefined) currentMerge.description = combinedDesc;
372
+ if (currentMerge.desc !== undefined) currentMerge.desc = combinedDesc;
373
+ if (!currentMerge.description && !currentMerge.desc) {
374
+ currentMerge.description = combinedDesc;
375
+ }
376
+ }
377
+
378
+ // Update end time - preserve original field name format
379
+ const originalEndField = getEventTime(currentMerge, 'end') !== null
380
+ ? (currentMerge.end !== undefined ? 'end' :
381
+ currentMerge.endDate !== undefined ? 'endDate' :
382
+ currentMerge.end_time !== undefined ? 'end_time' :
383
+ currentMerge.endTime !== undefined ? 'endTime' :
384
+ currentMerge.dtend !== undefined ? 'dtend' :
385
+ currentMerge.end_date !== undefined ? 'end_date' :
386
+ currentMerge.dateEnd !== undefined ? 'dateEnd' :
387
+ currentMerge.date_end !== undefined ? 'date_end' : 'end')
388
+ : 'end';
389
+
390
+ // Update all possible end time fields to ensure compatibility
391
+ if (currentMerge.end !== undefined) currentMerge.end = endTime;
392
+ if (currentMerge.endDate !== undefined) currentMerge.endDate = endTime;
393
+ if (currentMerge.end_time !== undefined) currentMerge.end_time = endTime;
394
+ if (currentMerge.endTime !== undefined) currentMerge.endTime = endTime;
395
+ if (currentMerge.dtend !== undefined) currentMerge.dtend = endTime;
396
+ if (currentMerge.end_date !== undefined) currentMerge.end_date = endTime;
397
+ if (currentMerge.dateEnd !== undefined) currentMerge.dateEnd = endTime;
398
+ if (currentMerge.date_end !== undefined) currentMerge.date_end = endTime;
399
+ if (!currentMerge.end && !currentMerge.endDate && !currentMerge.end_time &&
400
+ !currentMerge.endTime && !currentMerge.dtend && !currentMerge.end_date &&
401
+ !currentMerge.dateEnd && !currentMerge.date_end) {
402
+ currentMerge.end = endTime;
403
+ }
404
+ } else {
405
+ // Not consecutive, save current merge and start new one
406
+ mergedEvents.push(currentMerge);
407
+ currentMerge = { ...event };
408
+ }
409
+ }
410
+ }
411
+
412
+ // Add the last merge group if any
413
+ if (currentMerge !== null) {
414
+ mergedEvents.push(currentMerge);
415
+ }
416
+ }
417
+
418
+ return mergedEvents;
419
+ }
420
+
421
+ /**
422
+ * Process calendar events JSON and merge consecutive events
423
+ */
424
+ function processCalendarEventsJson(jsonData) {
425
+ if (Array.isArray(jsonData)) {
426
+ // Format: [{event1}, {event2}, ...]
427
+ return mergeConsecutiveEvents(jsonData);
428
+ } else if (jsonData && typeof jsonData === 'object') {
429
+ // Check for nested structures
430
+ if (Array.isArray(jsonData.events)) {
431
+ // Format: {events: [...], ...}
432
+ return {
433
+ ...jsonData,
434
+ events: mergeConsecutiveEvents(jsonData.events)
435
+ };
436
+ } else if (Array.isArray(jsonData.calendars)) {
437
+ // Format: {calendars: [{events: [...]}, ...], ...}
438
+ return {
439
+ ...jsonData,
440
+ calendars: jsonData.calendars.map(calendar => {
441
+ if (Array.isArray(calendar.events)) {
442
+ return {
443
+ ...calendar,
444
+ events: mergeConsecutiveEvents(calendar.events)
445
+ };
446
+ }
447
+ return calendar;
448
+ })
449
+ };
450
+ } else if (Array.isArray(jsonData.data)) {
451
+ // Format: {data: [...], ...}
452
+ return {
453
+ ...jsonData,
454
+ data: mergeConsecutiveEvents(jsonData.data)
455
+ };
456
+ } else if (Array.isArray(jsonData.items)) {
457
+ // Format: {items: [...], ...}
458
+ return {
459
+ ...jsonData,
460
+ items: mergeConsecutiveEvents(jsonData.items)
461
+ };
462
+ }
463
+ // Unknown structure, try to find any array of events
464
+ for (const key in jsonData) {
465
+ if (Array.isArray(jsonData[key]) && jsonData[key].length > 0) {
466
+ // Check if it looks like events (has time fields)
467
+ const firstItem = jsonData[key][0];
468
+ if (firstItem && typeof firstItem === 'object') {
469
+ const hasTimeField = getEventTime(firstItem, 'start') !== null ||
470
+ getEventTime(firstItem, 'end') !== null;
471
+ if (hasTimeField) {
472
+ return {
473
+ ...jsonData,
474
+ [key]: mergeConsecutiveEvents(jsonData[key])
475
+ };
476
+ }
477
+ }
478
+ }
479
+ }
480
+ }
481
+
482
+ // Unknown format, return as-is
483
+ return jsonData;
484
+ }
485
+
228
486
  /**
229
487
  * Sanitize response body to hide calendar URLs in error messages
230
488
  * Only sanitizes HTML and JSON responses to avoid breaking JavaScript code
231
489
  */
232
- async function sanitizeResponse(response) {
490
+ async function sanitizeResponse(response, pathname) {
233
491
  const contentType = response.headers.get('content-type') || '';
234
492
 
235
493
  // Only sanitize HTML and JSON responses (error messages)
@@ -250,6 +508,27 @@ async function sanitizeResponse(response) {
250
508
 
251
509
  let sanitizedBody = body;
252
510
 
511
+ // For JSON responses, check if it's a calendar events endpoint
512
+ if (contentType.includes('application/json')) {
513
+ // Check for calendar events endpoints - open-web-calendar uses /calendar.json
514
+ // Also check for any .json file that might contain calendar events
515
+ const isCalendarEventsEndpoint = pathname === '/calendar.events.json' ||
516
+ pathname === '/calendar.json' ||
517
+ pathname.endsWith('.events.json') ||
518
+ pathname.endsWith('.json');
519
+
520
+ if (isCalendarEventsEndpoint) {
521
+ try {
522
+ const jsonData = JSON.parse(body);
523
+ const processedData = processCalendarEventsJson(jsonData);
524
+ sanitizedBody = JSON.stringify(processedData);
525
+ } catch (error) {
526
+ // If JSON parsing fails, continue with original body
527
+ console.error('Failed to parse calendar events JSON:', error);
528
+ }
529
+ }
530
+ }
531
+
253
532
  // For HTML responses, inject console filtering
254
533
  if (contentType.includes('text/html')) {
255
534
  sanitizedBody = injectConsoleFilter(sanitizedBody);
@@ -390,7 +669,8 @@ export default {
390
669
  });
391
670
 
392
671
  // Sanitize response to hide calendar URLs in error messages
393
- return await sanitizeResponse(response);
672
+ // Pass pathname for calendar events processing
673
+ return await sanitizeResponse(response, pathname);
394
674
  }
395
675
 
396
676
  // Handle main calendar page requests - always add calendar URLs from secret
@@ -444,7 +724,8 @@ export default {
444
724
  });
445
725
 
446
726
  // Sanitize response to hide calendar URLs in error messages
447
- return await sanitizeResponse(response);
727
+ // Pass pathname for calendar events processing
728
+ return await sanitizeResponse(response, pathname);
448
729
  }
449
730
 
450
731
  // For all other requests (static resources, etc.), proxy directly
@@ -469,7 +750,8 @@ export default {
469
750
  });
470
751
 
471
752
  // Sanitize response to hide calendar URLs in error messages
472
- return await sanitizeResponse(response);
753
+ // Pass pathname for calendar events processing
754
+ return await sanitizeResponse(response, pathname);
473
755
  } catch (error) {
474
756
  return new Response(`Error: ${error.message}`, {
475
757
  status: 500,