From cffce45d0ccad8a83881cd62cf5b48f65a9129bf Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Sat, 15 Jun 2024 23:55:23 +0200 Subject: [PATCH] feat: 1.0.0 commit --- .github/workflows/build.yml | 38 ++++++++ .gitignore | 3 + bun.lockb | Bin 0 -> 11258 bytes index.ts | 60 ++++++++++++ package.json | 21 +++++ tsconfig.json | 27 ++++++ utils/getAverage.ts | 3 + utils/ics.ts | 153 +++++++++++++++++++++++++++++++ utils/jsonToFrontmatter.ts | 7 ++ utils/removeLastSlash.ts | 3 + utils/types/calendar_settings.ts | 75 +++++++++++++++ utils/types/index.ts | 29 ++++++ utils/types/schema.ts | 136 +++++++++++++++++++++++++++ 13 files changed, 555 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100755 bun.lockb create mode 100644 index.ts create mode 100644 package.json create mode 100644 tsconfig.json create mode 100644 utils/getAverage.ts create mode 100644 utils/ics.ts create mode 100644 utils/jsonToFrontmatter.ts create mode 100644 utils/removeLastSlash.ts create mode 100644 utils/types/calendar_settings.ts create mode 100644 utils/types/index.ts create mode 100644 utils/types/schema.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c3b2ecf --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +name: Build and Upload Artifacts + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - uses: oven-sh/setup-bun@v1 + + - name: Build for Linux x64 + run: bun build --compile --target=bun-linux-x64 ./index.ts --outfile mycli-linux-x64 + + - name: Build for Linux ARM64 + run: bun build --compile --target=bun-linux-arm64 ./index.ts --outfile mycli-linux-arm64 + + - name: Build for Windows x64 + run: bun build --compile --target=bun-windows-x64 ./index.ts --outfile mycli-windows-x64.exe + + - name: Build for macOS x64 + run: bun build --compile --target=bun-darwin-x64 ./index.ts --outfile mycli-macos-x64 + + - name: Build for macOS arm64 + run: bun build --compile --target=bun-darwin-arm64 ./index.ts --outfile mycli-macos-arm64 + + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + with: + name: mycli + path: | + mycli-linux-x64 + mycli-linux-arm64 + mycli-windows-x64.exe + mycli-macos-x64 + mycli-macos-arm64 \ No newline at end of file diff --git a/.gitignore b/.gitignore index ed0d3c8..91cfaf8 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,6 @@ typings/ # DynamoDB Local files .dynamodb/ + +converted/ +calendar.ics \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..973949fa99b5e71834668f94e13ab74064f9ba1e GIT binary patch literal 11258 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 { + const [ics, uid, date, type] = event.id!.split('::') + return { + fileName: `${date} ${event.title.trim()}.md`, + frontmatterContent: jsonToFrontmatter(event) + } +}) + +const writePerfArray: number[] = [] +for (const event of events) { + if (writePerfArray.length > 0) { + process.stdout.moveCursor(0, -1) // up one line + process.stdout.clearLine(1) + } + console.log(`[#${writePerfArray.length + 1}] Writing ${event.fileName}`) + + const initialWritePerf = performance.now() + + await Bun.write(`${removeLastSlash(parsedValues.data.out)}/${event.fileName}`, event.frontmatterContent) + + const finalWritePerf = performance.now() + writePerfArray.push(finalWritePerf - initialWritePerf) +} + +const finalPerf = performance.now() + +console.log(`[DONE] Finished in ${(finalPerf - initialPerf).toFixed(2)}ms`) +console.log(`[DONE] Average write time: ${(getAverage(writePerfArray) * 1000).toFixed(2)}µs`) \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..a904d2a --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "full-calendar-ics-converter", + "version": "1.0.0", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest", + "@types/ical": "^0.8.3", + "@types/luxon": "^3.4.2" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "ical.js": "^2.0.1", + "luxon": "^3.4.4", + "node-ical": "^0.18.0", + "rrule": "^2.8.1", + "zod": "^3.23.8" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/utils/getAverage.ts b/utils/getAverage.ts new file mode 100644 index 0000000..30e9dd8 --- /dev/null +++ b/utils/getAverage.ts @@ -0,0 +1,3 @@ +export default function getAverage(arr: number[]): number { + return arr.reduce((a, b) => a + b, 0) / arr.length; +} \ No newline at end of file diff --git a/utils/ics.ts b/utils/ics.ts new file mode 100644 index 0000000..74f052e --- /dev/null +++ b/utils/ics.ts @@ -0,0 +1,153 @@ +// @ts-nocheck +import ical from "ical.js"; +import { type OFCEvent, validateEvent } from "./types"; +import { DateTime } from "luxon"; +import { rrulestr } from "rrule"; + +function getDate(t: ical.Time): string { + return DateTime.fromSeconds(t.toUnixTime(), { zone: "UTC" }).toISODate(); +} + +function getTime(t: ical.Time): string { + if (t.isDate) { + return "00:00"; + } + return DateTime.fromSeconds(t.toUnixTime(), { zone: "UTC" }).toISOTime({ + includeOffset: false, + includePrefix: false, + suppressMilliseconds: true, + suppressSeconds: true, + }); +} + +function extractEventUrl(iCalEvent: ical.Event): string { + let urlProp = iCalEvent.component.getFirstProperty("url"); + return urlProp ? urlProp.getFirstValue() : ""; +} + +function specifiesEnd(iCalEvent: ical.Event) { + return ( + Boolean(iCalEvent.component.getFirstProperty("dtend")) || + Boolean(iCalEvent.component.getFirstProperty("duration")) + ); +} + +function icsToOFC(input: ical.Event): OFCEvent { + if (input.isRecurring()) { + const rrule = rrulestr( + input.component.getFirstProperty("rrule").getFirstValue().toString() + ); + const allDay = input.startDate.isDate; + const exdates = input.component + .getAllProperties("exdate") + .map((exdateProp) => { + const exdate = exdateProp.getFirstValue(); + // NOTE: We only store the date from an exdate and recreate the full datetime exdate later, + // so recurring events with exclusions that happen more than once per day are not supported. + return getDate(exdate); + }); + + return { + type: "rrule", + title: input.summary, + id: `ics::${input.uid}::${getDate(input.startDate)}::recurring`, + rrule: rrule.toString(), + skipDates: exdates, + startDate: getDate( + input.startDate.convertToZone(ical.Timezone.utcTimezone) + ), + ...(allDay + ? { allDay: true } + : { + allDay: false, + startTime: getTime( + input.startDate.convertToZone( + ical.Timezone.utcTimezone + ) + ), + endTime: getTime( + input.endDate.convertToZone(ical.Timezone.utcTimezone) + ), + }), + }; + } else { + const date = getDate(input.startDate); + const endDate = + specifiesEnd(input) && input.endDate + ? getDate(input.endDate) + : undefined; + const allDay = input.startDate.isDate; + return { + type: "single", + id: `ics::${input.uid}::${date}::single`, + title: input.summary, + date, + endDate: date !== endDate ? endDate || null : null, + ...(allDay + ? { allDay: true } + : { + allDay: false, + startTime: getTime(input.startDate), + endTime: getTime(input.endDate), + }), + }; + } +} + +export function getEventsFromICS(text: string): OFCEvent[] { + const jCalData = ical.parse(text); + const component = new ical.Component(jCalData); + + // TODO: Timezone support + // const tzc = component.getAllSubcomponents("vtimezone"); + // const tz = new ical.Timezone(tzc[0]); + + const events: ical.Event[] = component + .getAllSubcomponents("vevent") + .map((vevent) => new ical.Event(vevent)) + .filter((evt) => { + evt.iterator; + try { + evt.startDate.toJSDate(); + evt.endDate.toJSDate(); + return true; + } catch (err) { + // skipping events with invalid time + return false; + } + }); + + // Events with RECURRENCE-ID will have duplicated UIDs. + // We need to modify the base event to exclude those recurrence exceptions. + const baseEvents = Object.fromEntries( + events + .filter((e) => e.recurrenceId === null) + .map((e) => [e.uid, icsToOFC(e)]) + ); + + const recurrenceExceptions = events + .filter((e) => e.recurrenceId !== null) + .map((e): [string, OFCEvent] => [e.uid, icsToOFC(e)]); + + for (const [uid, event] of recurrenceExceptions) { + const baseEvent = baseEvents[uid]; + if (!baseEvent) { + continue; + } + + if (baseEvent.type !== "rrule" || event.type !== "single") { + console.warn( + "Recurrence exception was recurring or base event was not recurring", + { baseEvent, recurrenceException: event } + ); + continue; + } + baseEvent.skipDates.push(event.date); + } + + const allEvents = Object.values(baseEvents).concat( + recurrenceExceptions.map((e) => e[1]) + ); + + return allEvents.map(validateEvent).flatMap((e) => (e ? [e] : [])); +} diff --git a/utils/jsonToFrontmatter.ts b/utils/jsonToFrontmatter.ts new file mode 100644 index 0000000..792d8d2 --- /dev/null +++ b/utils/jsonToFrontmatter.ts @@ -0,0 +1,7 @@ +export default function jsonToFrontmatter(json: any): string { + return `--- +${Object.entries(json) + .map(([key, value]) => `${key}: ${JSON.stringify(value)}`) + .join("\n")} +---`; +} \ No newline at end of file diff --git a/utils/removeLastSlash.ts b/utils/removeLastSlash.ts new file mode 100644 index 0000000..286e99c --- /dev/null +++ b/utils/removeLastSlash.ts @@ -0,0 +1,3 @@ +export default function removeLastSlash(str: string): string { + return str.replace(/\/$/, ""); +} \ No newline at end of file diff --git a/utils/types/calendar_settings.ts b/utils/types/calendar_settings.ts new file mode 100644 index 0000000..c3837cd --- /dev/null +++ b/utils/types/calendar_settings.ts @@ -0,0 +1,75 @@ +import { ZodError, z } from "zod"; +import type { OFCEvent } from "./schema"; + +const calendarOptionsSchema = z.discriminatedUnion("type", [ + z.object({ type: z.literal("local"), directory: z.string() }), + z.object({ type: z.literal("dailynote"), heading: z.string() }), + z.object({ type: z.literal("ical"), url: z.string().url() }), + z.object({ + type: z.literal("caldav"), + name: z.string(), + url: z.string().url(), + homeUrl: z.string().url(), + username: z.string(), + password: z.string(), + }), +]); + +const colorValidator = z.object({ color: z.string() }); + +export type TestSource = { + type: "FOR_TEST_ONLY"; + id: string; + events?: OFCEvent[]; +}; + +export type CalendarInfo = ( + | z.infer + | TestSource +) & + z.infer; + +export function parseCalendarInfo(obj: unknown): CalendarInfo { + const options = calendarOptionsSchema.parse(obj); + const color = colorValidator.parse(obj); + + return { ...options, ...color }; +} + +export function safeParseCalendarInfo(obj: unknown): CalendarInfo | null { + try { + return parseCalendarInfo(obj); + } catch (e) { + if (e instanceof ZodError) { + console.debug("Parsing calendar info failed with errors", { + obj, + error: e.message, + }); + } + return null; + } +} + +/** + * Construct a partial calendar source of the specified type + */ +export function makeDefaultPartialCalendarSource( + type: CalendarInfo["type"] | "icloud" +): Partial { + if (type === "icloud") { + return { + type: "caldav", + color: getComputedStyle(document.body) + .getPropertyValue("--interactive-accent") + .trim(), + url: "https://caldav.icloud.com", + }; + } + + return { + type: type, + color: getComputedStyle(document.body) + .getPropertyValue("--interactive-accent") + .trim(), + }; +} diff --git a/utils/types/index.ts b/utils/types/index.ts new file mode 100644 index 0000000..460bb43 --- /dev/null +++ b/utils/types/index.ts @@ -0,0 +1,29 @@ +import type { CalendarInfo } from "./calendar_settings"; + +export type { OFCEvent } from "./schema"; +export { validateEvent } from "./schema"; + +export { makeDefaultPartialCalendarSource } from "./calendar_settings"; +export type { CalendarInfo } from "./calendar_settings"; + +export const PLUGIN_SLUG = "full-calendar-plugin"; + +export class FCError { + message: string; + constructor(message: string) { + this.message = message; + } +} + +export type EventLocation = { + file: { path: string }; + lineNumber: number | undefined; +}; + +export type Authentication = { + type: "basic"; + username: string; + password: string; +}; + +export type CalDAVSource = Extract; diff --git a/utils/types/schema.ts b/utils/types/schema.ts new file mode 100644 index 0000000..c89e787 --- /dev/null +++ b/utils/types/schema.ts @@ -0,0 +1,136 @@ +import { z, ZodError } from "zod"; +import { DateTime, Duration } from "luxon"; + +const stripTime = (date: DateTime) => { + // Strip time from luxon dateTime. + return DateTime.fromObject( + { + year: date.year, + month: date.month, + day: date.day, + }, + { zone: "utc" } + ); +}; + +export const ParsedDate = z.string(); +// z.string().transform((val, ctx) => { +// const parsed = DateTime.fromISO(val, { zone: "utc" }); +// if (parsed.invalidReason) { +// ctx.addIssue({ +// code: z.ZodIssueCode.custom, +// message: parsed.invalidReason, +// }); +// return z.NEVER; +// } +// return stripTime(parsed); +// }); + +export const ParsedTime = z.string(); +// z.string().transform((val, ctx) => { +// let parsed = DateTime.fromFormat(val, "h:mm a"); +// if (parsed.invalidReason) { +// parsed = DateTime.fromFormat(val, "HH:mm"); +// } + +// if (parsed.invalidReason) { +// ctx.addIssue({ +// code: z.ZodIssueCode.custom, +// message: parsed.invalidReason, +// }); +// return z.NEVER; +// } + +// return Duration.fromISOTime( +// parsed.toISOTime({ +// includeOffset: false, +// includePrefix: false, +// }) +// ); +// }); + +export const TimeSchema = z.discriminatedUnion("allDay", [ + z.object({ allDay: z.literal(true) }), + z.object({ + allDay: z.literal(false), + startTime: ParsedTime, + endTime: ParsedTime.nullable().default(null), + }), +]); + +export const CommonSchema = z.object({ + title: z.string(), + id: z.string().optional(), +}); + +export const EventSchema = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("single"), + date: ParsedDate, + endDate: ParsedDate.nullable().default(null), + completed: ParsedDate.or(z.literal(false)) + .or(z.literal(null)) + .optional(), + }), + z.object({ + type: z.literal("recurring"), + daysOfWeek: z.array(z.enum(["U", "M", "T", "W", "R", "F", "S"])), + startRecur: ParsedDate.optional(), + endRecur: ParsedDate.optional(), + }), + z.object({ + type: z.literal("rrule"), + startDate: ParsedDate, + rrule: z.string(), + skipDates: z.array(ParsedDate), + }), +]); + +type EventType = z.infer; +type TimeType = z.infer; +type CommonType = z.infer; + +export type OFCEvent = CommonType & TimeType & EventType; + +export function parseEvent(obj: unknown): OFCEvent { + if (typeof obj !== "object") { + throw new Error("value for parsing was not an object."); + } + const objectWithDefaults = { type: "single", allDay: false, ...obj }; + return { + ...CommonSchema.parse(objectWithDefaults), + ...TimeSchema.parse(objectWithDefaults), + ...EventSchema.parse(objectWithDefaults), + }; +} + +export function validateEvent(obj: unknown): OFCEvent | null { + try { + return parseEvent(obj); + } catch (e) { + if (e instanceof ZodError) { + console.debug("Parsing failed with errors", { + obj, + message: e.message, + }); + } + return null; + } +} +type Json = + | { [key: string]: Json } + | Json[] + | string + | number + | true + | false + | null; + +export function serializeEvent(obj: OFCEvent): Json { + return { ...obj }; +} + +export const argsSchema = z.object({ + out: z.string(), + ics: z.string(), +}); \ No newline at end of file