@stacksjs/calendar-api 0.70.380 → 0.71.1
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/feed.d.ts +34 -0
- package/dist/generators/ics.d.ts +9 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +5 -2
- package/dist/recurrence.d.ts +27 -0
- package/package.json +4 -4
package/dist/feed.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** RFC 5545 TEXT escaping: backslash, semicolon, comma, newline. */
|
|
2
|
+
export declare function escapeIcsText(value: string): string;
|
|
3
|
+
/** Fold lines longer than 75 octets with CRLF + space continuations. */
|
|
4
|
+
export declare function foldIcsLine(line: string): string;
|
|
5
|
+
/** The complete VCALENDAR body. Serve with {@link calendarFeedHeaders}. */
|
|
6
|
+
export declare function buildCalendarFeed(options: CalendarFeedOptions): string;
|
|
7
|
+
/** Response headers for serving a feed at a stable URL. */
|
|
8
|
+
export declare function calendarFeedHeaders(filename?: string): Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Subscribable calendar feeds: many events, one `text/calendar` body a
|
|
11
|
+
* parent adds once (`webcal://.../calendar.ics`) and their calendar app
|
|
12
|
+
* refreshes forever. This is the counterpart to the single-event
|
|
13
|
+
* "add to calendar" links the generators produce.
|
|
14
|
+
*/
|
|
15
|
+
export declare interface CalendarFeedEvent {
|
|
16
|
+
uid: string
|
|
17
|
+
title: string
|
|
18
|
+
start: Date
|
|
19
|
+
end: Date
|
|
20
|
+
allDay?: boolean
|
|
21
|
+
description?: string
|
|
22
|
+
location?: string
|
|
23
|
+
url?: string
|
|
24
|
+
rrule?: string
|
|
25
|
+
sequence?: number
|
|
26
|
+
updatedAt?: Date
|
|
27
|
+
}
|
|
28
|
+
export declare interface CalendarFeedOptions {
|
|
29
|
+
name: string
|
|
30
|
+
timezone?: string
|
|
31
|
+
refreshInterval?: string
|
|
32
|
+
prodId?: string
|
|
33
|
+
events: CalendarFeedEvent[]
|
|
34
|
+
}
|
package/dist/generators/ics.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import type { CalendarLink } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* The calendar file itself, as text.
|
|
4
|
+
*
|
|
5
|
+
* `generateIcs` wraps this in a `data:` URL, which is what an
|
|
6
|
+
* "add to calendar" anchor wants. A route that serves `text/calendar` needs the
|
|
7
|
+
* body instead - and so does anything that attaches the invitation to an email,
|
|
8
|
+
* where a base64 data URL is not a file.
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateIcsBody(link: CalendarLink): string;
|
|
2
11
|
export declare function generateIcs(link: CalendarLink): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { CalendarLink } from './types';
|
|
2
|
+
export type { CalendarFeedEvent, CalendarFeedOptions } from './feed';
|
|
3
|
+
export type { RecurrenceRule } from './recurrence';
|
|
2
4
|
export declare function exportCalendarGoogle(link: CalendarLink): string;
|
|
3
5
|
export declare function exportCalendarIcs(link: CalendarLink): string;
|
|
6
|
+
/**
|
|
7
|
+
* The raw `text/calendar` body, for a route that serves an `.ics` file or an
|
|
8
|
+
* email that attaches one. `exportCalendarIcs` returns the same content as a
|
|
9
|
+
* `data:` URL, which suits an anchor but is not a file.
|
|
10
|
+
*/
|
|
11
|
+
export declare function exportCalendarIcsBody(link: CalendarLink): string;
|
|
4
12
|
export declare function exportCalendarOutlook(link: CalendarLink): string;
|
|
5
13
|
export declare function exportCalendarYahoo(link: CalendarLink): string;
|
|
14
|
+
// Subscribable multi-event feeds + server-side recurrence expansion.
|
|
15
|
+
export { buildCalendarFeed, calendarFeedHeaders, escapeIcsText, foldIcsLine } from './feed';
|
|
16
|
+
export { expandRecurrence, parseRRule } from './recurrence';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{format as
|
|
3
|
-
`)
|
|
2
|
+
import{format as E}from"@stacksjs/datetime";var p="YYYYMMDD",h="YYYYMMDDThhmmss";function z($){let M="https://calendar.google.com/calendar/render?action=TEMPLATE",K=F($.from,"UTC"),J=F($.to,"UTC"),Y=$.allDay?p:h;if(M=`${M}&dates=${E(K,Y)}/${E(J,Y)}`,$.timezone)M=`${M}&ctz=${$.timezone}`;if(M=`${M}&text=${encodeURIComponent($.title)}`,$.description)M=`${M}&details=${encodeURIComponent($.description)}`;if($.address)M=`${M}&location=${encodeURIComponent($.address)}`;return M}function F($,M){let K=typeof $==="string"?new Date($):$;if(!M)return K;let J=K.toLocaleString("en-US",{timeZone:M}),Y=new Date(J);return Number.isNaN(Y.getTime())?K:Y}import{format as j}from"@stacksjs/datetime";import{createHash as m}from"crypto";function k($){return{toString:()=>m("md5").update($).digest("hex")}}function I($){return S($).join(`\r
|
|
3
|
+
`)}function x($){return c(S($))}function S($){let J=`UID:${o($)}`,Y=`SUMMARY:${$.title}`,N=["BEGIN:VCALENDAR","VERSION:2.0","PRODID:Spatie calendar-links","BEGIN:VEVENT",J,Y],Q=`${$.timezone||"PST"}:YYYYMMDDThhmmss`,V=$.allDay?"YYYYMMDD":Q;if($.allDay)N.push(`DTSTAMP;TZID=${j($.from,V)}`),N.push(`DTSTART:${j($.from,V)}`),N.push(`DURATION:P${Math.max(1,u($.from,$.to))}D`);else N.push(`DTSTAMP;TZID=${j($.from,V)}`),N.push(`DTSTART;TZID=${j($.from,V)}`),N.push(`DTEND;TZID=${j($.to,V)}`);if($.description)N.push(`X-ALT-DESC;FMTTYPE=text/html:${$.description}`);if($.address)N.push(`LOCATION:${$.address}`);return N.push("END:VEVENT"),N.push("END:VCALENDAR"),N}function c($){return`data:text/calendar;charset=utf8;base64,${btoa($.join(`\r
|
|
4
|
+
`))}`}function u($,M){let J=Date.UTC($.getFullYear(),$.getMonth(),$.getDate()),Y=Date.UTC(M.getFullYear(),M.getMonth(),M.getDate());return Math.floor((Y-J)/86400000)}function o($){let K=j($.from,"YYYY-MM-DDThh:mm:ss+00:00"),J=j($.to,"YYYY-MM-DDThh:mm:ss+00:00");return k(`${K}${J}${$.title}`).toString()}import{format as q}from"@stacksjs/datetime";function v($){let Y="https://outlook.live.com/calendar/deeplink/compose?path=/calendar/action/compose&rru=addevent",N=T($.from,"UTC"),U=T($.to,"UTC"),Q=$.allDay?"YYYY-MM-DD":"YYYY-MM-DDThh:mm:ss";if(Y=$.allDay?`${Y}&startdt=${q(N,Q)}`:`${Y}&startdt=${q(N,Q)}Z`,Y=$.allDay?`${Y}&enddt=${q(U,Q)}`:`${Y}&enddt=${q(U,Q)}Z`,$.allDay)Y=`${Y}&allday=true`;if(Y=`${Y}&subject=${encodeURIComponent($.title)}`,$.description)Y=`${Y}&body=${encodeURIComponent($.description)}`;if($.address)Y=`${Y}&location=${encodeURIComponent($.address)}`;return Y}function T($,M){return new Date((typeof $==="string"?new Date($):$).toLocaleString("en-US",{timeZone:M}))}import{format as O}from"@stacksjs/datetime";function w($){let J="https://calendar.yahoo.com/?v=60&view=d&type=20",Y=g($.from,"UTC"),N=g($.to,"UTC"),U=$.allDay?"YYYYMMDD":"YYYYMMDDThhmmss";if($.allDay)J=`${J}&ST=${O($.from,U)}`,J=`${J}&DUR=allday`,J=`${J}&ET=${O($.to,U)}`;else J=`${J}&ST=${O(Y,U)}Z`,J=`${J}&ET=${O(N,U)}Z`;if(J=`${J}&TITLE=${encodeURIComponent($.title)}`,$.description)J=`${J}&DESC=${encodeURIComponent($.description)}`;if($.address)J=`${J}&in_loc=${encodeURIComponent($.address)}`;return J}function g($,M){return new Date((typeof $==="string"?new Date($):$).toLocaleString("en-US",{timeZone:M}))}function _($){return $.replace(/\\/g,"\\\\").replace(/;/g,"\\;").replace(/,/g,"\\,").replace(/\r?\n/g,"\\n")}function f($){if(Buffer.byteLength($,"utf8")<=75)return $;let M=[],K="";for(let J of $){let Y=M.length===0?75:74;if(Buffer.byteLength(K+J,"utf8")>Y)M.push(K),K=J;else K+=J}if(K)M.push(K);return M.map((J,Y)=>Y===0?J:` ${J}`).join(`\r
|
|
5
|
+
`)}function b($){return`${$.toISOString().replace(/[-:]/g,"").slice(0,15)}Z`}function y($){return $.toISOString().replace(/-/g,"").slice(0,8)}function s($){let M=["BEGIN:VCALENDAR","VERSION:2.0",`PRODID:${$.prodId??"-//Stacks//calendar-api//EN"}`,"CALSCALE:GREGORIAN","METHOD:PUBLISH",`X-WR-CALNAME:${_($.name)}`];if($.timezone)M.push(`X-WR-TIMEZONE:${$.timezone}`);M.push(`REFRESH-INTERVAL;VALUE=DURATION:${$.refreshInterval??"PT1H"}`);for(let K of $.events){if(M.push("BEGIN:VEVENT"),M.push(`UID:${_(K.uid)}`),M.push(`DTSTAMP:${b(K.updatedAt??K.start)}`),M.push(`SUMMARY:${_(K.title)}`),K.allDay){M.push(`DTSTART;VALUE=DATE:${y(K.start)}`);let J=new Date(K.end);J.setUTCDate(J.getUTCDate()+1),M.push(`DTEND;VALUE=DATE:${y(J)}`)}else M.push(`DTSTART:${b(K.start)}`),M.push(`DTEND:${b(K.end)}`);if(K.rrule)M.push(`RRULE:${K.rrule.replace(/^RRULE:/i,"")}`);if(K.description)M.push(`DESCRIPTION:${_(K.description)}`);if(K.location)M.push(`LOCATION:${_(K.location)}`);if(K.url)M.push(`URL:${K.url}`);if(K.sequence!==void 0)M.push(`SEQUENCE:${K.sequence}`);M.push("END:VEVENT")}return M.push("END:VCALENDAR"),M.map(f).join(`\r
|
|
6
|
+
`)}function d($="calendar.ics"){return{"Content-Type":"text/calendar; charset=utf-8","Content-Disposition":`inline; filename="${$}"`,"Cache-Control":"public, max-age=300"}}var L=["SU","MO","TU","WE","TH","FR","SA"];function l($){let M={};for(let Y of $.replace(/^RRULE:/i,"").split(";")){let N=Y.indexOf("=");if(N===-1)continue;M[Y.slice(0,N).toUpperCase()]=Y.slice(N+1)}let K=M.FREQ?.toUpperCase();if(K!=="DAILY"&&K!=="WEEKLY"&&K!=="MONTHLY"&&K!=="YEARLY")return null;let J={freq:K,interval:Math.max(1,Number(M.INTERVAL??1)||1)};if(M.COUNT)J.count=Math.max(1,Number(M.COUNT)||1);if(M.UNTIL){let Y=M.UNTIL.match(/^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})Z?)?$/);if(Y)J.until=new Date(Date.UTC(Number(Y[1]),Number(Y[2])-1,Number(Y[3]),Number(Y[4]??23),Number(Y[5]??59),Number(Y[6]??59)))}if(M.BYDAY)J.byDay=M.BYDAY.split(",").map((Y)=>Y.trim().toUpperCase()).filter(Boolean);if(M.BYMONTHDAY)J.byMonthDay=M.BYMONTHDAY.split(",").map(Number).filter((Y)=>Number.isInteger(Y)&&Y>=1&&Y<=31);return J}function C($,M){let K=new Date(M);return K.setUTCHours($.getUTCHours(),$.getUTCMinutes(),$.getUTCSeconds(),0),K}function i($,M,K,J){let Y=L.indexOf(K);if(Y===-1)return null;if(J>0){let P=new Date(Date.UTC($,M,1)),B=1+(Y-P.getUTCDay()+7)%7+(J-1)*7,G=new Date(Date.UTC($,M,B));return G.getUTCMonth()===M?G:null}let N=new Date(Date.UTC($,M+1,0)),U=(N.getUTCDay()-Y+7)%7,Q=N.getUTCDate()-U+(J+1)*7,V=new Date(Date.UTC($,M,Q));return V.getUTCMonth()===M&&Q>=1?V:null}function a($,M,K,J){let Y=[],N=1000,U=0,Q=(P)=>{if($.until&&P.getTime()>$.until.getTime())return!1;if($.count!==void 0&&U>=$.count)return!1;if(U++,P.getTime()>=K.getTime()&&P.getTime()<=J.getTime())Y.push(P);return Y.length<1000},V=(P)=>P.getTime()>J.getTime()||$.until!==void 0&&P.getTime()>$.until.getTime();if($.freq==="DAILY"){for(let P=new Date(M);;P.setUTCDate(P.getUTCDate()+$.interval))if(V(P)||!Q(new Date(P)))break;return Y}if($.freq==="WEEKLY"){let P=$.byDay?.length?$.byDay:[L[M.getUTCDay()]],X=new Date(M);X.setUTCDate(X.getUTCDate()-X.getUTCDay());$:for(let B=new Date(X);;B.setUTCDate(B.getUTCDate()+7*$.interval)){for(let G of[...P].sort((W,Z)=>L.indexOf(W)-L.indexOf(Z))){let W=L.indexOf(G);if(W===-1)continue;let Z=C(M,new Date(Date.UTC(B.getUTCFullYear(),B.getUTCMonth(),B.getUTCDate()+W)));if(Z.getTime()<M.getTime())continue;if(V(Z)||!Q(Z))break $}if(B.getTime()>J.getTime())break}return Y}if($.freq==="MONTHLY"){$:for(let P=0;;P+=$.interval){let X=M.getUTCFullYear(),B=M.getUTCMonth()+P,G=[];if($.byDay?.length)for(let H of $.byDay){let R=H.match(/^(-?\d)?([A-Z]{2})$/);if(!R)continue;let D=Number(R[1]??1),A=i(X,B,R[2],D);if(A)G.push(C(M,A))}else if($.byMonthDay?.length)for(let H of $.byMonthDay){let R=new Date(Date.UTC(X,B,H));if(R.getUTCDate()===H)G.push(C(M,R))}else{let H=new Date(Date.UTC(X,B,M.getUTCDate()));if(H.getUTCDate()===M.getUTCDate())G.push(C(M,H))}let W=!1;for(let H of G.sort((R,D)=>R.getTime()-D.getTime())){if(H.getTime()<M.getTime())continue;if(W=!0,V(H)||!Q(H))break $}if(new Date(Date.UTC(X,B,1)).getTime()>J.getTime()&&!W)break}return Y}for(let P=0;;P+=$.interval){let X=new Date(Date.UTC(M.getUTCFullYear()+P,M.getUTCMonth(),M.getUTCDate(),M.getUTCHours(),M.getUTCMinutes(),M.getUTCSeconds()));if(V(X)||!Q(X))break}return Y}function B$($){return z($)}function H$($){return x($)}function G$($){return I($)}function R$($){return v($)}function W$($){return w($)}export{l as parseRRule,f as foldIcsLine,W$ as exportCalendarYahoo,R$ as exportCalendarOutlook,G$ as exportCalendarIcsBody,H$ as exportCalendarIcs,B$ as exportCalendarGoogle,a as expandRecurrence,_ as escapeIcsText,d as calendarFeedHeaders,s as buildCalendarFeed};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Parse an RRULE value (`FREQ=WEEKLY;BYDAY=MO,WE;COUNT=10`). Null for unsupported input. */
|
|
2
|
+
export declare function parseRRule(rule: string): RecurrenceRule | null;
|
|
3
|
+
/**
|
|
4
|
+
* Occurrence start times for `rule` anchored at `dtstart`, intersected with
|
|
5
|
+
* [windowStart, windowEnd]. COUNT/UNTIL are honored from the anchor, so a
|
|
6
|
+
* window in the middle of a COUNT=10 series sees only the occurrences that
|
|
7
|
+
* fall inside it. Capped defensively at 1000 occurrences.
|
|
8
|
+
*/
|
|
9
|
+
export declare function expandRecurrence(rule: RecurrenceRule, dtstart: Date, windowStart: Date, windowEnd: Date): Date[];
|
|
10
|
+
/**
|
|
11
|
+
* RFC 5545 RRULE expansion - the subset schools actually use: FREQ
|
|
12
|
+
* DAILY/WEEKLY/MONTHLY/YEARLY, INTERVAL, COUNT, UNTIL, BYDAY (weekly lists
|
|
13
|
+
* and monthly ordinals like `2TU`), BYMONTHDAY. Calendar clients expand a
|
|
14
|
+
* feed's RRULE themselves; this expansion is for the SERVER's own needs -
|
|
15
|
+
* "next occurrences" listings and change notifications.
|
|
16
|
+
*
|
|
17
|
+
* All arithmetic is UTC-based on the provided Date objects; callers own
|
|
18
|
+
* timezone interpretation, matching the rest of calendar-api.
|
|
19
|
+
*/
|
|
20
|
+
export declare interface RecurrenceRule {
|
|
21
|
+
freq: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY'
|
|
22
|
+
interval: number
|
|
23
|
+
count?: number
|
|
24
|
+
until?: Date
|
|
25
|
+
byDay?: string[]
|
|
26
|
+
byMonthDay?: number[]
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/calendar-api",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.71.1",
|
|
5
5
|
"description": "Easily interact with calendar APIs.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
"@stacksjs/stx": "^0.2.184"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@stacksjs/browser": "0.
|
|
64
|
-
"better-dx": "^0.2.
|
|
65
|
-
"@stacksjs/security": "0.
|
|
63
|
+
"@stacksjs/browser": "0.71.1",
|
|
64
|
+
"better-dx": "^0.2.23",
|
|
65
|
+
"@stacksjs/security": "0.71.1"
|
|
66
66
|
},
|
|
67
67
|
"sideEffects": false
|
|
68
68
|
}
|