kennel 0.2.3 → 0.2.5
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/README.md +23 -13
- package/dist/cli/dispatch.js +16 -3
- package/dist/cli/src/cli/dispatch.js +16 -3
- package/dist/cli/src/cli/usage.js +1 -1
- package/dist/cli/src/core/command.js +1 -0
- package/dist/cli/src/core/init.js +5 -5
- package/dist/cli/src/plugins/external.d.ts +28 -2
- package/dist/cli/src/plugins/external.js +75 -21
- package/dist/cli/src/site/server-plugins.d.ts +2 -7
- package/dist/cli/src/site/viewer.js +1 -0
- package/dist/cli/usage.js +1 -1
- package/dist/core/command.js +1 -0
- package/dist/core/init.js +5 -5
- package/dist/core/runtime.js +357 -3207
- package/dist/package/preview.js +1232 -5113
- package/dist/plugins/external.d.ts +28 -2
- package/dist/plugins/external.js +75 -21
- package/dist/site/server-plugins.d.ts +2 -7
- package/dist/site/server-plugins.js +552 -10447
- package/dist/site/viewer.js +1 -0
- package/kernel/apps/web/package.json +1 -5
- package/kernel/apps/web/scripts/dev.mjs +53 -0
- package/kernel/apps/web/src/app.css +0 -3
- package/kernel/apps/web/src/main.tsx +0 -1
- package/kernel/apps/web/src/plugins.ts +0 -30
- package/kernel/apps/web/tsconfig.json +1 -2
- package/kernel/apps/web/vite.config.ts +31 -13
- package/kernel/packages/api/package.json +0 -4
- package/kernel/packages/api/src/server/folder-metadata-plugin.ts +13 -38
- package/kernel/packages/api/src/server/index.ts +0 -1
- package/kernel/packages/api/src/server/plugins.ts +49 -16
- package/kernel/packages/api/test/core.test.ts +13 -25
- package/kernel/packages/markdown/README.md +1 -1
- package/package.json +1 -1
- package/web/assets/{abnfDiagram-VRR7QNED-Bgfb1nEB.js → abnfDiagram-VRR7QNED-xd_g1uK3.js} +1 -1
- package/web/assets/app-CX20nNIa.js +2 -0
- package/web/assets/{app-options-D-RJiQ-X.js → app-options-CkPJE5ep.js} +1 -1
- package/web/assets/{app-view-header-BLvLq02T.js → app-view-header-CNk5KnA-.js} +1 -1
- package/web/assets/{arc-Dk_KI36B.js → arc-B0u5yBJA.js} +1 -1
- package/web/assets/{architectureDiagram-ZJ3FMSHR-NR_KhRCf.js → architectureDiagram-ZJ3FMSHR-DfxOAo8P.js} +1 -1
- package/web/assets/{ask-tray-TzeTcO6-.js → ask-tray-SscJecDY.js} +1 -1
- package/web/assets/{blockDiagram-677ZJIJ3-CkLkwx9C.js → blockDiagram-677ZJIJ3-Ci3Usxh6.js} +1 -1
- package/web/assets/{c4Diagram-LMCZKHZV-CFCUH8e_.js → c4Diagram-LMCZKHZV-BqgwIVgh.js} +1 -1
- package/web/assets/channel-X7WHyxxT.js +1 -0
- package/web/assets/{chunk-2Q5K7J3B-B_BPLWsW.js → chunk-2Q5K7J3B-CfX69l2U.js} +1 -1
- package/web/assets/{chunk-32BRIVSS-B1VB1hOy.js → chunk-32BRIVSS-CuFlzZkl.js} +1 -1
- package/web/assets/{chunk-5VM5RSS4-KFcYr8Co.js → chunk-5VM5RSS4-DidCMlJa.js} +1 -1
- package/web/assets/{chunk-EX3LRPZG-CpBX3kuK.js → chunk-EX3LRPZG-B5CDm1dS.js} +1 -1
- package/web/assets/{chunk-JWPE2WC7-BPSl40wG.js → chunk-JWPE2WC7-D5fGV_sH.js} +1 -1
- package/web/assets/{chunk-MOJQB5TN-D2RcYmoU.js → chunk-MOJQB5TN-ayhH5Q-H.js} +1 -1
- package/web/assets/{chunk-RYQCIY6F-m7Gyn-t3.js → chunk-RYQCIY6F-C4BfOOZn.js} +1 -1
- package/web/assets/{chunk-V7JOEXUC-TDvdopxS.js → chunk-V7JOEXUC-DpUEIM9V.js} +1 -1
- package/web/assets/{chunk-VR4S4FIN-DZTsG51a.js → chunk-VR4S4FIN-DoQuFFRh.js} +1 -1
- package/web/assets/{chunk-XXDRQBXY-Dp0--RE2.js → chunk-XXDRQBXY-CI9eiLwn.js} +1 -1
- package/web/assets/classDiagram-OUVF2IWQ-BjsLQ3dz.js +1 -0
- package/web/assets/classDiagram-v2-EOCWNBFH-BjsLQ3dz.js +1 -0
- package/web/assets/client-7OWw5kW2.js +6 -0
- package/web/assets/client-DYj_td4Q.js +17 -0
- package/web/assets/{client-j2kQmxsO.js → client-Dk-NBvJR.js} +4 -4
- package/web/assets/command-zkzH4hVz.js +38 -0
- package/web/assets/{copy-context-C0Uqsp-3.js → copy-context-D5oiWrYC.js} +1 -1
- package/web/assets/{cose-bilkent-JH36ORCC-CPu3PI8h.js → cose-bilkent-JH36ORCC-BHRk1mO3.js} +1 -1
- package/web/assets/{cynefin-VYW2F7L2-DWxftYAm.js → cynefin-VYW2F7L2-C1B09TRE.js} +1 -1
- package/web/assets/{cynefinDiagram-TSTJHNR4-DsmN2V-p.js → cynefinDiagram-TSTJHNR4-B27qXQbv.js} +1 -1
- package/web/assets/{dagre-VKFMJZFB-COEbApyY.js → dagre-VKFMJZFB-B1qEi32g.js} +1 -1
- package/web/assets/{diagram-FQU43EPY-0O9WGWKB.js → diagram-FQU43EPY-BMkkgADD.js} +1 -1
- package/web/assets/{diagram-G47NLZAW-CutEdMVh.js → diagram-G47NLZAW-Cb8_g1A5.js} +1 -1
- package/web/assets/{diagram-NH7WQ7WH-C9KZc3B2.js → diagram-NH7WQ7WH-B51wuc5m.js} +1 -1
- package/web/assets/{diagram-OA4YK3LP-D8kkPXbY.js → diagram-OA4YK3LP-xnsecbbI.js} +1 -1
- package/web/assets/{diagram-WEI45ONY-_YqiTaGP.js → diagram-WEI45ONY-DSZdyaAV.js} +1 -1
- package/web/assets/{document-Dgt5eZFj.js → document-Dc2dftTW.js} +3 -3
- package/web/assets/{ebnfDiagram-CCIWWBDH-JO-zQh6g.js → ebnfDiagram-CCIWWBDH-aTHmcUGy.js} +1 -1
- package/web/assets/{entities-C3wz7FCx.js → entities-CCUtpskY.js} +1 -1
- package/web/assets/{erDiagram-Q63AITRT-9UjQwJ4P.js → erDiagram-Q63AITRT-CUUPBcvo.js} +1 -1
- package/web/assets/{file-viewer-DZJ8nYnl.js → file-viewer-DJ3rsmef.js} +1 -1
- package/web/assets/{flowDiagram-23GEKE2U-Ca08tnYM.js → flowDiagram-23GEKE2U-aTQXx2Mh.js} +1 -1
- package/web/assets/{folder-image-BfGgxo7i.js → folder-image-Bnv43tzP.js} +1 -1
- package/web/assets/{folder-landing-qUKBCnG-.js → folder-landing-DCnzYXXY.js} +1 -1
- package/web/assets/{folder-list-BfFjv7rR.js → folder-list-DvVkVjiR.js} +1 -1
- package/web/assets/{folder-metadata-DCw8xzHk.js → folder-metadata-C9hMMzIH.js} +1 -1
- package/web/assets/{folder-mobile-bar-Ubz4babf.js → folder-mobile-bar-VoTL1GS0.js} +1 -1
- package/web/assets/folder-plugin-view-Cvd804ZU.js +6 -0
- package/web/assets/{ganttDiagram-NO4QXBWP-KnHzX5W3.js → ganttDiagram-NO4QXBWP-DiwdUp7u.js} +1 -1
- package/web/assets/{gitGraphDiagram-IHSO6WYX-Cm8woNtK.js → gitGraphDiagram-IHSO6WYX-BL2gG_IS.js} +1 -1
- package/web/assets/{index-CNyy98F8.js → index-CUTtMSyJ.js} +2 -2
- package/web/assets/index-CegvVQ1C.js +1 -0
- package/web/assets/index-Dwhf8F_U.css +1 -0
- package/web/assets/{infoDiagram-FWYZ7A6U-yyqmUtIb.js → infoDiagram-FWYZ7A6U-B3n0zZpt.js} +1 -1
- package/web/assets/{ishikawaDiagram-FXEZZL3T-BfvR_Zy4.js → ishikawaDiagram-FXEZZL3T-D7YytbEv.js} +1 -1
- package/web/assets/{journeyDiagram-5HDEW3XC-BKuw06HM.js → journeyDiagram-5HDEW3XC-BYwqEl3C.js} +1 -1
- package/web/assets/{kanban-definition-HUTT4EX6-n0010rmQ.js → kanban-definition-HUTT4EX6-BQHNJgXZ.js} +1 -1
- package/web/assets/{linear-C0d0lT8O.js → linear-BPiQCLNk.js} +1 -1
- package/web/assets/{mermaid.core-BDVbShyO.js → mermaid.core-CD-KvBjV.js} +4 -4
- package/web/assets/{metadata-KV0dpp4G.js → metadata-COJ7fZul.js} +1 -1
- package/web/assets/{mindmap-definition-LN4V7U3C--_0ks3GF.js → mindmap-definition-LN4V7U3C-C1L_gyoj.js} +1 -1
- package/web/assets/{pegDiagram-2B236MQR-CBoNIpKR.js → pegDiagram-2B236MQR-BDAfItqS.js} +1 -1
- package/web/assets/{pieDiagram-ENE6RG2P-MA5tkYh5.js → pieDiagram-ENE6RG2P-CkZYH3Vz.js} +1 -1
- package/web/assets/{popover-DBD7FF-W.js → popover-0oA2lLUV.js} +2 -2
- package/web/assets/{project-search-dialog-DM1IkSZu.js → project-search-dialog-B2oqMT6h.js} +1 -1
- package/web/assets/{quadrantDiagram-ABIIQ3AL-CWT1ryqF.js → quadrantDiagram-ABIIQ3AL-Dy8Al3YZ.js} +1 -1
- package/web/assets/{railroadDiagram-RFXS5EU6-ZiLB5FGi.js → railroadDiagram-RFXS5EU6-Dd9_yo7B.js} +1 -1
- package/web/assets/{requirementDiagram-TGXJPOKE-CQq9uneq.js → requirementDiagram-TGXJPOKE-DPhIUsbs.js} +1 -1
- package/web/assets/{sankeyDiagram-HTMAVEWB-qDG-0JqI.js → sankeyDiagram-HTMAVEWB-B2QkYtub.js} +1 -1
- package/web/assets/{search-DNrQA5fF.js → search-C1J0-xiB.js} +1 -1
- package/web/assets/select-CtXC_PLC.js +30 -0
- package/web/assets/{selection-popup-DmtTeRs2.js → selection-popup-qhlUFzGq.js} +1 -1
- package/web/assets/{sequenceDiagram-DBY2YBRQ-CiApurLO.js → sequenceDiagram-DBY2YBRQ-Bx7fcZDx.js} +1 -1
- package/web/assets/{sizeCapture-X5ZJPWSS-DZSR5QXm.js → sizeCapture-X5ZJPWSS-CIXIhl_A.js} +1 -1
- package/web/assets/{stateDiagram-2N3HPSRC-CjJJR9_z.js → stateDiagram-2N3HPSRC-DCrdAflB.js} +1 -1
- package/web/assets/stateDiagram-v2-6OUMAXLB-0KCFLYeQ.js +1 -0
- package/web/assets/{swimlanes-5IMT3BWC-CKmo1sg6.js → swimlanes-5IMT3BWC-DA2vc7iZ.js} +2 -2
- package/web/assets/swimlanesDiagram-G3AALYLV-CAw3AV84.js +8 -0
- package/web/assets/{timeline-definition-FHXFAJF6-D-NMwS3y.js → timeline-definition-FHXFAJF6-lKfJiV9V.js} +1 -1
- package/web/assets/{vennDiagram-L72KCM5P-CfL6LBWw.js → vennDiagram-L72KCM5P-DO1q2KzG.js} +1 -1
- package/web/assets/{view-context-gyDl_p2V.js → view-context-DXKM3-l1.js} +4 -4
- package/web/assets/{viewer-capabilities-Bpp8Uvcg.js → viewer-capabilities-Cig_xM9u.js} +1 -1
- package/web/assets/{wardleyDiagram-EHGQE667-DgldMxaj.js → wardleyDiagram-EHGQE667-Dal9LkuZ.js} +1 -1
- package/web/assets/{xychartDiagram-FW5EYKEG-CIoAIFX8.js → xychartDiagram-FW5EYKEG-B3dPLPII.js} +1 -1
- package/web/index.html +2 -2
- package/kernel/packages/api/test/partial-loading.test.ts +0 -43
- package/kernel/plugins/calendar/SKILL.md +0 -173
- package/kernel/plugins/calendar/eslint.config.js +0 -10
- package/kernel/plugins/calendar/package.json +0 -73
- package/kernel/plugins/calendar/schema.json +0 -137
- package/kernel/plugins/calendar/src/calendar.tsx +0 -328
- package/kernel/plugins/calendar/src/client.tsx +0 -139
- package/kernel/plugins/calendar/src/context.ts +0 -48
- package/kernel/plugins/calendar/src/copy-button.tsx +0 -66
- package/kernel/plugins/calendar/src/event-dialog.tsx +0 -270
- package/kernel/plugins/calendar/src/index.ts +0 -4
- package/kernel/plugins/calendar/src/manifest.ts +0 -7
- package/kernel/plugins/calendar/src/parse.ts +0 -610
- package/kernel/plugins/calendar/src/server.ts +0 -160
- package/kernel/plugins/calendar/src/utils.ts +0 -80
- package/kernel/plugins/calendar/src/views.tsx +0 -418
- package/kernel/plugins/calendar/test/context.test.mjs +0 -60
- package/kernel/plugins/calendar/test/package.test.mjs +0 -43
- package/kernel/plugins/calendar/test/parse.test.mjs +0 -392
- package/kernel/plugins/calendar/test/server.test.mjs +0 -126
- package/kernel/plugins/calendar/tsconfig.json +0 -17
- package/kernel/plugins/mail/SKILL.md +0 -97
- package/kernel/plugins/mail/eslint.config.js +0 -10
- package/kernel/plugins/mail/package.json +0 -79
- package/kernel/plugins/mail/src/api.ts +0 -359
- package/kernel/plugins/mail/src/bun-ffi.d.ts +0 -14
- package/kernel/plugins/mail/src/client.tsx +0 -14
- package/kernel/plugins/mail/src/flock.ts +0 -137
- package/kernel/plugins/mail/src/folders.ts +0 -37
- package/kernel/plugins/mail/src/history.ts +0 -485
- package/kernel/plugins/mail/src/index.ts +0 -3
- package/kernel/plugins/mail/src/mail-card.tsx +0 -123
- package/kernel/plugins/mail/src/mail-conversation.tsx +0 -153
- package/kernel/plugins/mail/src/mail-detail-page.tsx +0 -467
- package/kernel/plugins/mail/src/mail-hooks.ts +0 -252
- package/kernel/plugins/mail/src/mail-page.tsx +0 -109
- package/kernel/plugins/mail/src/mail-request.ts +0 -78
- package/kernel/plugins/mail/src/mail-send-box.tsx +0 -175
- package/kernel/plugins/mail/src/mailos.ts +0 -723
- package/kernel/plugins/mail/src/manifest.ts +0 -7
- package/kernel/plugins/mail/src/node.ts +0 -171
- package/kernel/plugins/mail/src/parse.ts +0 -139
- package/kernel/plugins/mail/src/server.ts +0 -125
- package/kernel/plugins/mail/src/types.ts +0 -10
- package/kernel/plugins/mail/src/viewer.tsx +0 -147
- package/kernel/plugins/mail/test/flock.test.mjs +0 -156
- package/kernel/plugins/mail/test/folders.test.mjs +0 -138
- package/kernel/plugins/mail/test/history.test.mjs +0 -460
- package/kernel/plugins/mail/test/mailos.test.mjs +0 -752
- package/kernel/plugins/mail/test/node.test.mjs +0 -207
- package/kernel/plugins/mail/test/package.test.mjs +0 -54
- package/kernel/plugins/mail/test/parse.test.mjs +0 -32
- package/kernel/plugins/mail/test/server.test.mjs +0 -116
- package/kernel/plugins/mail/test/viewer.test.mjs +0 -192
- package/kernel/plugins/mail/tsconfig.json +0 -17
- package/kernel/plugins/projects/SKILL.md +0 -73
- package/kernel/plugins/projects/eslint.config.js +0 -10
- package/kernel/plugins/projects/package.json +0 -70
- package/kernel/plugins/projects/src/client.tsx +0 -17
- package/kernel/plugins/projects/src/data/model.ts +0 -129
- package/kernel/plugins/projects/src/index.ts +0 -17
- package/kernel/plugins/projects/src/manifest.ts +0 -7
- package/kernel/plugins/projects/src/project-context.ts +0 -41
- package/kernel/plugins/projects/src/server.ts +0 -276
- package/kernel/plugins/projects/src/ui/project-card.tsx +0 -61
- package/kernel/plugins/projects/src/ui/projects-page.tsx +0 -102
- package/kernel/plugins/projects/test/package.test.mjs +0 -51
- package/kernel/plugins/projects/tsconfig.json +0 -17
- package/kernel/plugins/tasks/SKILL.md +0 -341
- package/kernel/plugins/tasks/eslint.config.js +0 -10
- package/kernel/plugins/tasks/package.json +0 -74
- package/kernel/plugins/tasks/schema.json +0 -939
- package/kernel/plugins/tasks/src/api.ts +0 -212
- package/kernel/plugins/tasks/src/client.tsx +0 -23
- package/kernel/plugins/tasks/src/data/parse.ts +0 -544
- package/kernel/plugins/tasks/src/data/task-id.ts +0 -56
- package/kernel/plugins/tasks/src/data/todo-model.ts +0 -795
- package/kernel/plugins/tasks/src/index.ts +0 -34
- package/kernel/plugins/tasks/src/manifest.ts +0 -7
- package/kernel/plugins/tasks/src/pages/task-detail.tsx +0 -48
- package/kernel/plugins/tasks/src/pages/task-list.tsx +0 -88
- package/kernel/plugins/tasks/src/server.ts +0 -447
- package/kernel/plugins/tasks/src/styles.css +0 -9
- package/kernel/plugins/tasks/src/ui/compact-todo-list.tsx +0 -372
- package/kernel/plugins/tasks/src/ui/error-details.tsx +0 -78
- package/kernel/plugins/tasks/src/ui/task-page.tsx +0 -392
- package/kernel/plugins/tasks/src/ui/todo-agent-filter.tsx +0 -36
- package/kernel/plugins/tasks/src/ui/todo-filter-bar.tsx +0 -66
- package/kernel/plugins/tasks/src/ui/todo-item.tsx +0 -494
- package/kernel/plugins/tasks/src/ui/todo-status-filter.tsx +0 -60
- package/kernel/plugins/tasks/src/ui/todo-tag-filter.tsx +0 -48
- package/kernel/plugins/tasks/src/ui/todo-warnings.tsx +0 -106
- package/kernel/plugins/tasks/test/api.test.mjs +0 -99
- package/kernel/plugins/tasks/test/folder-sources.test.mjs +0 -46
- package/kernel/plugins/tasks/test/package.test.mjs +0 -54
- package/kernel/plugins/tasks/test/parse.test.mjs +0 -225
- package/kernel/plugins/tasks/test/server.test.mjs +0 -386
- package/kernel/plugins/tasks/tsconfig.json +0 -17
- package/web/assets/app-DoGr2Zi7.js +0 -2
- package/web/assets/channel-uMEjV8qc.js +0 -1
- package/web/assets/classDiagram-OUVF2IWQ-fbZocVor.js +0 -1
- package/web/assets/classDiagram-v2-EOCWNBFH-fbZocVor.js +0 -1
- package/web/assets/client-C6Nxdfif.js +0 -2
- package/web/assets/client-CXYfBko5.js +0 -17
- package/web/assets/client-CoKtOq-X.js +0 -7
- package/web/assets/client-DyRQz5RU.js +0 -53
- package/web/assets/client-OY6HUzuD.js +0 -98
- package/web/assets/client-YOYWZIFi.js +0 -34
- package/web/assets/command-BugZBGXE.js +0 -38
- package/web/assets/folder-plugin-view-DsWR1zmQ.js +0 -6
- package/web/assets/index-BaRnbYRw.js +0 -1
- package/web/assets/index-BexmLxIP.css +0 -1
- package/web/assets/index-DDirZMgx.js +0 -1
- package/web/assets/note-card-DBfX6gW1.js +0 -6
- package/web/assets/select-D8cqNgcp.js +0 -30
- package/web/assets/stateDiagram-v2-6OUMAXLB-BpeGSCu9.js +0 -1
- package/web/assets/swimlanesDiagram-G3AALYLV-D9RjLKb9.js +0 -8
- package/web/assets/use-folder-filter-LQyXns5Q.js +0 -1
|
@@ -1,610 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
addDays,
|
|
3
|
-
addMonths,
|
|
4
|
-
addWeeks,
|
|
5
|
-
differenceInCalendarDays,
|
|
6
|
-
differenceInCalendarMonths,
|
|
7
|
-
endOfDay,
|
|
8
|
-
isSameDay,
|
|
9
|
-
} from "date-fns"
|
|
10
|
-
|
|
11
|
-
import type { ContentIssue } from "@raggle-ai/folder-plugin/validation"
|
|
12
|
-
import {
|
|
13
|
-
issueSummary,
|
|
14
|
-
parseJsonSource,
|
|
15
|
-
} from "@raggle-ai/folder-plugin/validation"
|
|
16
|
-
|
|
17
|
-
export type CalendarColor =
|
|
18
|
-
"blue" | "green" | "red" | "yellow" | "purple" | "orange"
|
|
19
|
-
|
|
20
|
-
export type CalendarRepeating = "daily" | "weekly" | "monthly"
|
|
21
|
-
export type CalendarStatus = "confirmed" | "tentative" | "cancelled"
|
|
22
|
-
|
|
23
|
-
export interface CalendarEvent {
|
|
24
|
-
id: string
|
|
25
|
-
title: string
|
|
26
|
-
start: Date
|
|
27
|
-
end: Date
|
|
28
|
-
/**
|
|
29
|
-
* Absolute owning folder of the `calendar.json` that declared this event,
|
|
30
|
-
* stamped by the server at merge time (not part of the source document).
|
|
31
|
-
*/
|
|
32
|
-
folder?: string
|
|
33
|
-
description?: string
|
|
34
|
-
group?: string
|
|
35
|
-
groupColor?: CalendarColor
|
|
36
|
-
color?: CalendarColor
|
|
37
|
-
repeating?: CalendarRepeating
|
|
38
|
-
allDay?: boolean
|
|
39
|
-
attendees?: string[]
|
|
40
|
-
excludeDates?: Date[]
|
|
41
|
-
location?: string
|
|
42
|
-
organizer?: string
|
|
43
|
-
reminders?: number[]
|
|
44
|
-
repeatUntil?: Date
|
|
45
|
-
status?: CalendarStatus
|
|
46
|
-
tags?: string[]
|
|
47
|
-
url?: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface CalendarDocument {
|
|
51
|
-
color?: CalendarColor
|
|
52
|
-
group?: string
|
|
53
|
-
title?: string
|
|
54
|
-
/** Optional anchor date: the calendar view opens here instead of today. */
|
|
55
|
-
startDate?: string
|
|
56
|
-
events: CalendarEvent[]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface CalendarSource {
|
|
60
|
-
calendar: CalendarDocument
|
|
61
|
-
/** Absolute directory that owns the `calendar.json` file. */
|
|
62
|
-
folder: string
|
|
63
|
-
group: string
|
|
64
|
-
path: string
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
type RawEvent = Record<string, unknown>
|
|
68
|
-
|
|
69
|
-
type RawCalendar = {
|
|
70
|
-
color?: unknown
|
|
71
|
-
events?: RawEvent[]
|
|
72
|
-
group?: unknown
|
|
73
|
-
startDate?: unknown
|
|
74
|
-
title?: unknown
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const colors = new Set<CalendarColor>([
|
|
78
|
-
"blue",
|
|
79
|
-
"green",
|
|
80
|
-
"red",
|
|
81
|
-
"yellow",
|
|
82
|
-
"purple",
|
|
83
|
-
"orange",
|
|
84
|
-
])
|
|
85
|
-
|
|
86
|
-
const repeats = new Set<CalendarRepeating>(["daily", "weekly", "monthly"])
|
|
87
|
-
const statuses = new Set<CalendarStatus>([
|
|
88
|
-
"confirmed",
|
|
89
|
-
"tentative",
|
|
90
|
-
"cancelled",
|
|
91
|
-
])
|
|
92
|
-
|
|
93
|
-
function valueAt(record: RawEvent, keys: string[]) {
|
|
94
|
-
for (const key of keys) {
|
|
95
|
-
const value = record[key]
|
|
96
|
-
if (value !== undefined && value !== null && value !== "") return value
|
|
97
|
-
}
|
|
98
|
-
return undefined
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function text(record: RawEvent, keys: string[]) {
|
|
102
|
-
const value = valueAt(record, keys)
|
|
103
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function texts(value: unknown) {
|
|
107
|
-
if (!Array.isArray(value)) return undefined
|
|
108
|
-
const values = [
|
|
109
|
-
...new Set(
|
|
110
|
-
value
|
|
111
|
-
.filter((item): item is string => typeof item === "string")
|
|
112
|
-
.map((item) => item.trim())
|
|
113
|
-
.filter(Boolean),
|
|
114
|
-
),
|
|
115
|
-
]
|
|
116
|
-
return values.length ? values : undefined
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function numbers(value: unknown) {
|
|
120
|
-
if (!Array.isArray(value)) return undefined
|
|
121
|
-
const values = [
|
|
122
|
-
...new Set(
|
|
123
|
-
value.filter(
|
|
124
|
-
(item): item is number =>
|
|
125
|
-
typeof item === "number" && Number.isInteger(item) && item >= 0,
|
|
126
|
-
),
|
|
127
|
-
),
|
|
128
|
-
].sort((left, right) => left - right)
|
|
129
|
-
return values.length ? values : undefined
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/
|
|
133
|
-
|
|
134
|
-
function localDateOnly(value: string): Date | undefined {
|
|
135
|
-
const trimmed = value.trim()
|
|
136
|
-
if (!DATE_ONLY.test(trimmed)) return undefined
|
|
137
|
-
const [year, month, day] = trimmed.split("-").map(Number)
|
|
138
|
-
return new Date(year ?? 0, (month ?? 1) - 1, day ?? 1)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function date(value: unknown, fallback: Date) {
|
|
142
|
-
if (value instanceof Date && !Number.isNaN(value.getTime())) return value
|
|
143
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
144
|
-
return new Date(value)
|
|
145
|
-
if (typeof value === "string" && value.trim()) {
|
|
146
|
-
const parsed = localDateOnly(value) ?? new Date(value)
|
|
147
|
-
if (!Number.isNaN(parsed.getTime())) return parsed
|
|
148
|
-
}
|
|
149
|
-
return fallback
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function color(value: unknown): CalendarColor | undefined {
|
|
153
|
-
if (typeof value !== "string") return undefined
|
|
154
|
-
const candidate = value.toLowerCase()
|
|
155
|
-
return colors.has(candidate as CalendarColor)
|
|
156
|
-
? (candidate as CalendarColor)
|
|
157
|
-
: undefined
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function repeating(value: unknown): CalendarRepeating | undefined {
|
|
161
|
-
if (typeof value !== "string") return undefined
|
|
162
|
-
const candidate = value.toLowerCase()
|
|
163
|
-
return repeats.has(candidate as CalendarRepeating)
|
|
164
|
-
? (candidate as CalendarRepeating)
|
|
165
|
-
: undefined
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function status(value: unknown): CalendarStatus | undefined {
|
|
169
|
-
if (typeof value !== "string") return undefined
|
|
170
|
-
const candidate = value.toLowerCase()
|
|
171
|
-
return statuses.has(candidate as CalendarStatus)
|
|
172
|
-
? (candidate as CalendarStatus)
|
|
173
|
-
: undefined
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function url(value: unknown) {
|
|
177
|
-
if (typeof value !== "string" || !value.trim()) return undefined
|
|
178
|
-
const candidate = value.trim()
|
|
179
|
-
return /^https?:\/\/[^\s]+$/i.test(candidate) ? candidate : undefined
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function dates(value: unknown) {
|
|
183
|
-
if (!Array.isArray(value)) return undefined
|
|
184
|
-
const values = value
|
|
185
|
-
.map((item) => date(item, new Date(Number.NaN)))
|
|
186
|
-
.filter((item) => !Number.isNaN(item.getTime()))
|
|
187
|
-
return values.length ? values : undefined
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function until(value: unknown) {
|
|
191
|
-
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
192
|
-
const parsed = date(value, new Date(Number.NaN))
|
|
193
|
-
return Number.isNaN(parsed.getTime()) ? undefined : endOfDay(parsed)
|
|
194
|
-
}
|
|
195
|
-
const parsed = date(value, new Date(Number.NaN))
|
|
196
|
-
return Number.isNaN(parsed.getTime()) ? undefined : parsed
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export function parseCalendar(calendar: unknown): CalendarDocument | undefined {
|
|
200
|
-
if (Array.isArray(calendar)) return { events: parseEvents(calendar) }
|
|
201
|
-
if (!calendar || typeof calendar !== "object") return undefined
|
|
202
|
-
|
|
203
|
-
const record = calendar as RawCalendar
|
|
204
|
-
if (!Array.isArray(record.events)) return undefined
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
color: color(record.color),
|
|
208
|
-
startDate:
|
|
209
|
-
typeof record.startDate === "string" && record.startDate.trim()
|
|
210
|
-
? record.startDate.trim()
|
|
211
|
-
: undefined,
|
|
212
|
-
group:
|
|
213
|
-
typeof record.group === "string" && record.group.trim()
|
|
214
|
-
? record.group.trim()
|
|
215
|
-
: undefined,
|
|
216
|
-
title:
|
|
217
|
-
typeof record.title === "string" && record.title.trim()
|
|
218
|
-
? record.title.trim()
|
|
219
|
-
: undefined,
|
|
220
|
-
events: parseEvents(record.events),
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function parseCalendarJson(
|
|
225
|
-
source: string,
|
|
226
|
-
): CalendarDocument | undefined {
|
|
227
|
-
try {
|
|
228
|
-
return parseCalendar(JSON.parse(source))
|
|
229
|
-
} catch {
|
|
230
|
-
return undefined
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export function parseEvents(events: RawEvent[]): CalendarEvent[] {
|
|
235
|
-
return events.map((record, index) => normalizeEvent(record, index))
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export function normalizeEvent(record: RawEvent, index: number): CalendarEvent {
|
|
239
|
-
const id = valueAt(record, ["id", "uid", "key"])?.toString() ?? String(index)
|
|
240
|
-
|
|
241
|
-
const startValue = date(
|
|
242
|
-
valueAt(record, ["start", "startDate", "start_date", "date", "begin"]),
|
|
243
|
-
new Date(0),
|
|
244
|
-
)
|
|
245
|
-
const endValue = date(
|
|
246
|
-
valueAt(record, ["end", "endDate", "end_date", "due", "finish"]),
|
|
247
|
-
startValue,
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
const allDay = typeof record.allDay === "boolean" ? record.allDay : undefined
|
|
251
|
-
const start = startValue
|
|
252
|
-
const end =
|
|
253
|
-
endValue > start
|
|
254
|
-
? endValue
|
|
255
|
-
: new Date(start.getTime() + (allDay ? 86_400_000 : 3_600_000))
|
|
256
|
-
|
|
257
|
-
const rawColor =
|
|
258
|
-
valueAt(record, [
|
|
259
|
-
"color",
|
|
260
|
-
"colour",
|
|
261
|
-
"calendarId",
|
|
262
|
-
"calendar",
|
|
263
|
-
"category",
|
|
264
|
-
]) ?? valueAt(record, ["calendars"])
|
|
265
|
-
const eventColor =
|
|
266
|
-
color(rawColor) ?? color(Array.isArray(rawColor) ? rawColor[0] : undefined)
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
id,
|
|
270
|
-
start,
|
|
271
|
-
end,
|
|
272
|
-
color: eventColor,
|
|
273
|
-
folder: typeof record.folder === "string" ? record.folder : undefined,
|
|
274
|
-
group: text(record, ["group"]),
|
|
275
|
-
groupColor: color(record.groupColor),
|
|
276
|
-
repeating: repeating(record.repeating),
|
|
277
|
-
allDay,
|
|
278
|
-
attendees: texts(record.attendees),
|
|
279
|
-
excludeDates: dates(record.excludeDates),
|
|
280
|
-
location: text(record, ["location"]),
|
|
281
|
-
organizer: text(record, ["organizer"]),
|
|
282
|
-
reminders: numbers(record.reminders),
|
|
283
|
-
repeatUntil: until(record.repeatUntil),
|
|
284
|
-
status: status(record.status),
|
|
285
|
-
tags: texts(record.tags),
|
|
286
|
-
url: url(record.url),
|
|
287
|
-
description: text(record, [
|
|
288
|
-
"description",
|
|
289
|
-
"detail",
|
|
290
|
-
"notes",
|
|
291
|
-
"summary",
|
|
292
|
-
"body",
|
|
293
|
-
]),
|
|
294
|
-
title:
|
|
295
|
-
text(record, ["title", "name", "subject", "summary", "label"]) ??
|
|
296
|
-
"Untitled",
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const AUTO_PALETTE: CalendarColor[] = [
|
|
301
|
-
"green",
|
|
302
|
-
"orange",
|
|
303
|
-
"purple",
|
|
304
|
-
"red",
|
|
305
|
-
"yellow",
|
|
306
|
-
"blue",
|
|
307
|
-
]
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Resolve render colors for a merged event list. Precedence per event:
|
|
311
|
-
* explicit event color, then folder/group color, then an auto-assigned color
|
|
312
|
-
* for the event's group. Groups without any declared color get distinct
|
|
313
|
-
* palette colors in deterministic first-seen order, so filter chips and
|
|
314
|
-
* events for the same group always match without any configuration.
|
|
315
|
-
*/
|
|
316
|
-
export function resolveColors(events: CalendarEvent[]): CalendarEvent[] {
|
|
317
|
-
const groupColor = new Map<string, CalendarColor | undefined>()
|
|
318
|
-
const used = new Set<CalendarColor>()
|
|
319
|
-
for (const event of events) {
|
|
320
|
-
if (!event.group || groupColor.has(event.group)) continue
|
|
321
|
-
const resolved = event.color ?? event.groupColor
|
|
322
|
-
groupColor.set(event.group, resolved)
|
|
323
|
-
if (resolved) used.add(resolved)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
let next = 0
|
|
327
|
-
for (const [group, resolved] of groupColor) {
|
|
328
|
-
if (resolved !== undefined) continue
|
|
329
|
-
let chosen: CalendarColor | undefined
|
|
330
|
-
for (let attempt = 0; attempt < AUTO_PALETTE.length; attempt++) {
|
|
331
|
-
const candidate = AUTO_PALETTE[(next + attempt) % AUTO_PALETTE.length]!
|
|
332
|
-
if (!used.has(candidate)) {
|
|
333
|
-
chosen = candidate
|
|
334
|
-
next += attempt + 1
|
|
335
|
-
break
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
if (!chosen) {
|
|
339
|
-
chosen = AUTO_PALETTE[next % AUTO_PALETTE.length]!
|
|
340
|
-
next += 1
|
|
341
|
-
}
|
|
342
|
-
used.add(chosen)
|
|
343
|
-
groupColor.set(group, chosen)
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return events.map((event) => ({
|
|
347
|
-
...event,
|
|
348
|
-
color:
|
|
349
|
-
event.color ??
|
|
350
|
-
event.groupColor ??
|
|
351
|
-
groupColor.get(event.group ?? "") ??
|
|
352
|
-
"blue",
|
|
353
|
-
}))
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
export function mergeCalendars(sources: CalendarSource[]): CalendarDocument {
|
|
357
|
-
const root = sources.find((source) => source.path === "calendar.json")
|
|
358
|
-
|
|
359
|
-
return {
|
|
360
|
-
title: root?.calendar.title ?? sources[0]?.calendar.title,
|
|
361
|
-
startDate: root?.calendar.startDate ?? sources[0]?.calendar.startDate,
|
|
362
|
-
events: sources.flatMap((source) => {
|
|
363
|
-
const group = source.calendar.group ?? source.group
|
|
364
|
-
return source.calendar.events.map((event) => ({
|
|
365
|
-
...event,
|
|
366
|
-
folder: source.folder,
|
|
367
|
-
group: event.group ?? group,
|
|
368
|
-
groupColor: event.groupColor ?? source.calendar.color,
|
|
369
|
-
id: `${source.path}:${event.id}`,
|
|
370
|
-
}))
|
|
371
|
-
}),
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function repeatStart(event: CalendarEvent, index: number) {
|
|
376
|
-
if (event.repeating === "daily") return addDays(event.start, index)
|
|
377
|
-
if (event.repeating === "weekly") return addWeeks(event.start, index)
|
|
378
|
-
return addMonths(event.start, index)
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function repeatIndex(event: CalendarEvent, date: Date) {
|
|
382
|
-
if (event.repeating === "daily") {
|
|
383
|
-
return Math.max(0, differenceInCalendarDays(date, event.start) - 1)
|
|
384
|
-
}
|
|
385
|
-
if (event.repeating === "weekly") {
|
|
386
|
-
return Math.max(
|
|
387
|
-
0,
|
|
388
|
-
Math.floor(differenceInCalendarDays(date, event.start) / 7) - 1,
|
|
389
|
-
)
|
|
390
|
-
}
|
|
391
|
-
return Math.max(0, differenceInCalendarMonths(date, event.start) - 1)
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export function eventsInRange(events: CalendarEvent[], start: Date, end: Date) {
|
|
395
|
-
if (
|
|
396
|
-
Number.isNaN(start.getTime()) ||
|
|
397
|
-
Number.isNaN(end.getTime()) ||
|
|
398
|
-
end <= start
|
|
399
|
-
)
|
|
400
|
-
return []
|
|
401
|
-
|
|
402
|
-
const occurrences: CalendarEvent[] = []
|
|
403
|
-
for (const event of events) {
|
|
404
|
-
if (!event.repeating) {
|
|
405
|
-
if (event.end > start && event.start < end) occurrences.push(event)
|
|
406
|
-
continue
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const duration = event.end.getTime() - event.start.getTime()
|
|
410
|
-
const earliest = new Date(start.getTime() - duration)
|
|
411
|
-
for (let index = repeatIndex(event, earliest); ; index += 1) {
|
|
412
|
-
const occurrenceStart = repeatStart(event, index)
|
|
413
|
-
if (Number.isNaN(occurrenceStart.getTime()) || occurrenceStart >= end)
|
|
414
|
-
break
|
|
415
|
-
if (event.repeatUntil && occurrenceStart > event.repeatUntil) break
|
|
416
|
-
const occurrenceEnd = new Date(occurrenceStart.getTime() + duration)
|
|
417
|
-
if (occurrenceEnd <= start) continue
|
|
418
|
-
if (
|
|
419
|
-
event.excludeDates?.some((excluded) =>
|
|
420
|
-
isSameDay(excluded, occurrenceStart),
|
|
421
|
-
)
|
|
422
|
-
)
|
|
423
|
-
continue
|
|
424
|
-
occurrences.push({
|
|
425
|
-
...event,
|
|
426
|
-
id: `${event.id}:${occurrenceStart.toISOString()}`,
|
|
427
|
-
start: occurrenceStart,
|
|
428
|
-
end: occurrenceEnd,
|
|
429
|
-
})
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return occurrences.sort(
|
|
433
|
-
(left, right) => left.start.getTime() - right.start.getTime(),
|
|
434
|
-
)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
export function eventCount(calendar: CalendarDocument | undefined) {
|
|
438
|
-
return calendar?.events.length ?? 0
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const EVENT_TITLE_KEYS = [
|
|
442
|
-
"title",
|
|
443
|
-
"name",
|
|
444
|
-
"subject",
|
|
445
|
-
"summary",
|
|
446
|
-
"label",
|
|
447
|
-
] as const
|
|
448
|
-
const EVENT_START_KEYS = [
|
|
449
|
-
"start",
|
|
450
|
-
"startDate",
|
|
451
|
-
"start_date",
|
|
452
|
-
"date",
|
|
453
|
-
"begin",
|
|
454
|
-
] as const
|
|
455
|
-
const EVENT_END_KEYS = ["end", "endDate", "end_date", "due", "finish"] as const
|
|
456
|
-
|
|
457
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
458
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function strictDate(value: unknown): Date | undefined {
|
|
462
|
-
if (value instanceof Date && !Number.isNaN(value.getTime())) return value
|
|
463
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
464
|
-
return new Date(value)
|
|
465
|
-
if (typeof value === "string" && value.trim()) {
|
|
466
|
-
const parsed = localDateOnly(value) ?? new Date(value)
|
|
467
|
-
if (!Number.isNaN(parsed.getTime())) return parsed
|
|
468
|
-
}
|
|
469
|
-
return undefined
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Validate a parsed `calendar.json` value before tolerant rendering. Covers
|
|
474
|
-
* what the tolerant parser would silently drop or default (missing events
|
|
475
|
-
* array, events without any title, unparseable dates that would fall back to
|
|
476
|
-
* epoch, bad enums). Returns `{ path, message }` issues; empty means valid.
|
|
477
|
-
*/
|
|
478
|
-
export function validateCalendarFile(value: unknown): ContentIssue[] {
|
|
479
|
-
if (Array.isArray(value)) {
|
|
480
|
-
return value.flatMap((event, index) =>
|
|
481
|
-
validateEvent(event, `events[${index}]`),
|
|
482
|
-
)
|
|
483
|
-
}
|
|
484
|
-
if (!isRecord(value)) {
|
|
485
|
-
return [
|
|
486
|
-
{
|
|
487
|
-
path: "#",
|
|
488
|
-
message: "calendar.json must contain an object or an array of events",
|
|
489
|
-
},
|
|
490
|
-
]
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const issues: ContentIssue[] = []
|
|
494
|
-
if (!Array.isArray(value.events)) {
|
|
495
|
-
issues.push({ path: "events", message: "must be an array of events" })
|
|
496
|
-
} else {
|
|
497
|
-
value.events.forEach((event, index) => {
|
|
498
|
-
issues.push(...validateEvent(event, `events[${index}]`))
|
|
499
|
-
})
|
|
500
|
-
}
|
|
501
|
-
return issues
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function validateEvent(event: unknown, path: string): ContentIssue[] {
|
|
505
|
-
if (!isRecord(event)) {
|
|
506
|
-
return [{ path, message: "must be an event object" }]
|
|
507
|
-
}
|
|
508
|
-
const issues: ContentIssue[] = []
|
|
509
|
-
|
|
510
|
-
const hasTitle = EVENT_TITLE_KEYS.some(
|
|
511
|
-
(key) => typeof event[key] === "string" && event[key].trim().length > 0,
|
|
512
|
-
)
|
|
513
|
-
if (!hasTitle) {
|
|
514
|
-
issues.push({
|
|
515
|
-
path,
|
|
516
|
-
message:
|
|
517
|
-
"has no title (expected one of title, name, subject, summary, label)",
|
|
518
|
-
})
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const startValue = EVENT_START_KEYS.find((key) => event[key] !== undefined)
|
|
522
|
-
if (startValue !== undefined && !strictDate(event[startValue])) {
|
|
523
|
-
issues.push({
|
|
524
|
-
path: `${path}.${startValue}`,
|
|
525
|
-
message: "could not be parsed as a date",
|
|
526
|
-
})
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const endValue = EVENT_END_KEYS.find((key) => event[key] !== undefined)
|
|
530
|
-
if (
|
|
531
|
-
endValue !== undefined &&
|
|
532
|
-
event[endValue] !== null &&
|
|
533
|
-
!strictDate(event[endValue])
|
|
534
|
-
) {
|
|
535
|
-
issues.push({
|
|
536
|
-
path: `${path}.${endValue}`,
|
|
537
|
-
message: "could not be parsed as a date",
|
|
538
|
-
})
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (event.allDay !== undefined && typeof event.allDay !== "boolean") {
|
|
542
|
-
issues.push({ path: `${path}.allDay`, message: "must be a boolean" })
|
|
543
|
-
}
|
|
544
|
-
if (
|
|
545
|
-
event.repeating !== undefined &&
|
|
546
|
-
!["daily", "weekly", "monthly"].includes(String(event.repeating))
|
|
547
|
-
) {
|
|
548
|
-
issues.push({
|
|
549
|
-
path: `${path}.repeating`,
|
|
550
|
-
message: "must be one of daily, weekly, monthly",
|
|
551
|
-
})
|
|
552
|
-
}
|
|
553
|
-
if (
|
|
554
|
-
event.status !== undefined &&
|
|
555
|
-
!["confirmed", "tentative", "cancelled"].includes(String(event.status))
|
|
556
|
-
) {
|
|
557
|
-
issues.push({
|
|
558
|
-
path: `${path}.status`,
|
|
559
|
-
message: "must be one of confirmed, tentative, cancelled",
|
|
560
|
-
})
|
|
561
|
-
}
|
|
562
|
-
if (event.excludeDates !== undefined && !Array.isArray(event.excludeDates)) {
|
|
563
|
-
issues.push({
|
|
564
|
-
path: `${path}.excludeDates`,
|
|
565
|
-
message: "must be an array of dates",
|
|
566
|
-
})
|
|
567
|
-
}
|
|
568
|
-
return issues
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Parse a `calendar.json` source string and validate it. Never throws:
|
|
573
|
-
* invalid JSON and structural mistakes come back as issues with paths.
|
|
574
|
-
*/
|
|
575
|
-
export function parseCalendarSource(source: string) {
|
|
576
|
-
return parseJsonSource<unknown>(source, validateCalendarFile)
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Compact validation summary for nav badges and error headers.
|
|
581
|
-
*/
|
|
582
|
-
export function calendarIssueSummary(issues: ContentIssue[]): string {
|
|
583
|
-
return issueSummary(issues, "calendar.json")
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Starter `calendar.json` for `kennel init`: one sample event in the shape
|
|
588
|
-
* the tolerant parser reads directly.
|
|
589
|
-
*/
|
|
590
|
-
export function starterCalendarSource(): string {
|
|
591
|
-
return `${JSON.stringify(
|
|
592
|
-
{
|
|
593
|
-
color: "blue",
|
|
594
|
-
events: [
|
|
595
|
-
{
|
|
596
|
-
id: "1",
|
|
597
|
-
title: "First event",
|
|
598
|
-
description: "Describe what happens.",
|
|
599
|
-
start: "2030-01-01T09:00:00",
|
|
600
|
-
end: "2030-01-01T10:00:00",
|
|
601
|
-
allDay: false,
|
|
602
|
-
status: "confirmed",
|
|
603
|
-
attendees: [],
|
|
604
|
-
},
|
|
605
|
-
],
|
|
606
|
-
},
|
|
607
|
-
null,
|
|
608
|
-
2,
|
|
609
|
-
)}\n`
|
|
610
|
-
}
|