137 lines
3.5 KiB
TypeScript

import {
CalendarConfig,
DataAdapter,
GtdEvent,
RawNote,
RECURRING_TAG,
} from "./types";
import resolveEffectiveFolderScope from "./effectiveFolderScope";
import extractGtdBlock from "./gtdBlock";
import { resolveEventDate } from "./resolveDate";
export interface CollectResult {
events: GtdEvent[];
warnings: string[];
scannedFolders: number;
scannedNotes: number;
}
/**
* Collect calendar events per SPEC.md:
* - resolve folder scope from the calendar note's folder
* - fetch notes/todos in scope
* - apply the inclusion matrix (§3.2)
* - resolve dates (§3.1)
* - sort within the flat list (grouping happens at render time)
*
* The calendar note itself is always excluded.
*/
export default async function collectEvents(
adapter: DataAdapter,
calendarNoteId: string,
calendarFolderId: string,
config: CalendarConfig
): Promise<CollectResult> {
const warnings: string[] = [];
const folders = await adapter.getFolders();
const effectiveScope = resolveEffectiveFolderScope(
folders,
calendarFolderId,
config.scopeDepth,
config.scopeAll,
config.excludeNotebooks
);
const folderIds = effectiveScope.folderIds;
warnings.push(...effectiveScope.warnings);
let scannedNotes = 0;
const events: GtdEvent[] = [];
// Bodies are only scanned (gtd block → styling/date override) when at least
// one of notes/todos is included. When both are "none" the loop produces
// nothing, so skip fetching bodies entirely.
const needsBody = config.notes !== "none" || config.todos !== "none";
for (const folderId of folderIds) {
const notes = await adapter.getNotesInFolder(folderId, needsBody);
for (const note of notes) {
scannedNotes += 1;
if (note.id === calendarNoteId) continue;
const event = await noteToEvent(adapter, note, config, warnings);
if (event) events.push(event);
}
}
sortEvents(events, config);
return {
events,
warnings,
scannedFolders: folderIds.length,
scannedNotes,
};
}
async function noteToEvent(
adapter: DataAdapter,
note: RawNote,
config: CalendarConfig,
warnings: string[]
): Promise<GtdEvent | null> {
const mode = note.is_todo ? config.todos : config.notes;
if (mode === "none") return null;
const result = extractGtdBlock(note.body);
if (mode === "gtd-only" && !result.found) return null;
if (result.error) {
warnings.push(`"${note.title}": gtd block problem — ${result.error}`);
}
const block = result.block;
const date = resolveEventDate(note, block);
if (block && block.date && !date) {
warnings.push(`"${note.title}": unparseable date "${block.date}"`);
}
// Only to-dos can recur; skip the tag lookup for plain notes.
let isRecurring = false;
if (note.is_todo) {
const tags = await adapter.getNoteTagTitles(note.id);
isRecurring = tags.includes(RECURRING_TAG);
}
return {
id: note.id,
title: block && block.title ? block.title : note.title,
date,
isTodo: Boolean(note.is_todo),
completed: Boolean(note.is_todo && note.todo_completed > 0),
bgColour: block ? block.bgColour : null,
fgColour: block ? block.fgColour : null,
icon: block ? block.icon : null,
text: block ? block.text : null,
updatedTime: note.updated_time,
isRecurring,
};
}
function sortEvents(events: GtdEvent[], config: CalendarConfig): void {
const direction = config.sort === "desc" ? -1 : 1;
events.sort((a, b) => {
let comparison: number;
if (config.sortType === "modified_date") {
comparison = a.updatedTime - b.updatedTime;
} else {
comparison = a.title.localeCompare(b.title, undefined, {
sensitivity: "base",
});
}
return comparison * direction;
});
}