mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 23:29:02 +02:00
462 lines
16 KiB
TypeScript
462 lines
16 KiB
TypeScript
import noteService from "./notes.js";
|
|
import attributeService from "./attributes.js";
|
|
import sql from "./sql.js";
|
|
import protectedSessionService from "./protected_session.js";
|
|
import searchService from "../services/search/services/search.js";
|
|
import SearchContext from "../services/search/search_context.js";
|
|
import hoistedNoteService from "./hoisted_note.js";
|
|
import type BNote from "../becca/entities/bnote.js";
|
|
import optionService from "./options.js";
|
|
import { t } from "i18next";
|
|
import dayjs from "dayjs";
|
|
import type { Dayjs } from "dayjs";
|
|
import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js";
|
|
import quarterOfYear from "dayjs/plugin/quarterOfYear.js";
|
|
import cloningService from "./cloning.js";
|
|
|
|
dayjs.extend(isSameOrAfter);
|
|
dayjs.extend(quarterOfYear);
|
|
|
|
const CALENDAR_ROOT_LABEL = "calendarRoot";
|
|
const YEAR_LABEL = "yearNote";
|
|
const QUARTER_LABEL = "quarterNote";
|
|
const MONTH_LABEL = "monthNote";
|
|
const WEEK_LABEL = "weekNote";
|
|
const DATE_LABEL = "dateNote";
|
|
|
|
const WEEKDAY_TRANSLATION_IDS = ["weekdays.sunday", "weekdays.monday", "weekdays.tuesday", "weekdays.wednesday", "weekdays.thursday", "weekdays.friday", "weekdays.saturday", "weekdays.sunday"];
|
|
|
|
const MONTH_TRANSLATION_IDS = [
|
|
"months.january",
|
|
"months.february",
|
|
"months.march",
|
|
"months.april",
|
|
"months.may",
|
|
"months.june",
|
|
"months.july",
|
|
"months.august",
|
|
"months.september",
|
|
"months.october",
|
|
"months.november",
|
|
"months.december"
|
|
];
|
|
|
|
/** produces 1st, 2nd, 3rd, 4th, 21st, 31st for 1, 2, 3, 4, 21, 31 */
|
|
function ordinal(dayNumber: number) {
|
|
const suffixes = ["th", "st", "nd", "rd"];
|
|
const suffix = suffixes[(dayNumber - 20) % 10] || suffixes[dayNumber] || suffixes[0];
|
|
return `${dayNumber}${suffix}`;
|
|
}
|
|
|
|
type TimeUnit = 'year' | 'quarter' | 'month' | 'week' | 'day';
|
|
|
|
const baseReplacements = {
|
|
year: ['year'],
|
|
quarter: ['quarterNumber', 'shortQuarter'],
|
|
month: ['isoMonth', 'monthNumber', 'monthNumberPadded', 'month', 'shortMonth3', 'shortMonth4'],
|
|
week: ['weekNumber', 'weekNumberPadded', 'shortWeek', 'shortWeek3'],
|
|
day: ['isoDate', 'dayInMonthPadded', 'ordinal', 'weekDay', 'weekDay3', 'weekDay2']
|
|
};
|
|
|
|
function getTimeUnitReplacements(timeUnit: TimeUnit): string[] {
|
|
const units: TimeUnit[] = ['year', 'quarter', 'month', 'week', 'day'];
|
|
const index = units.indexOf(timeUnit);
|
|
return units.slice(0, index + 1).flatMap(unit => baseReplacements[unit]);
|
|
}
|
|
|
|
function getJournalNoteTitle(rootNote: BNote, timeUnit: TimeUnit, dateObj: Dayjs, number: number) {
|
|
const patterns = {
|
|
year: rootNote.getOwnedLabelValue("yearPattern") || "{year}",
|
|
quarter: rootNote.getOwnedLabelValue("quarterPattern") || "Quarter {quarterNumber}",
|
|
month: rootNote.getOwnedLabelValue("monthPattern") || "{monthNumberPadded} - {month}",
|
|
week: rootNote.getOwnedLabelValue("weekPattern") || "Week {weekNumber}",
|
|
day: rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}"
|
|
};
|
|
|
|
const pattern = patterns[timeUnit];
|
|
const monthName = t(MONTH_TRANSLATION_IDS[dateObj.month()]);
|
|
const weekDay = t(WEEKDAY_TRANSLATION_IDS[dateObj.day()]);
|
|
const numberStr = number.toString();
|
|
|
|
const allReplacements: Record<string, string> = {
|
|
// Common date formats
|
|
'{year}': dateObj.format('YYYY'),
|
|
'{isoDate}': dateObj.format('YYYY-MM-DD'),
|
|
'{isoMonth}': dateObj.format('YYYY-MM'),
|
|
|
|
// Month related
|
|
'{monthNumber}': numberStr,
|
|
'{monthNumberPadded}': numberStr.padStart(2, '0'),
|
|
'{month}': monthName,
|
|
'{shortMonth3}': monthName.slice(0, 3),
|
|
'{shortMonth4}': monthName.slice(0, 4),
|
|
|
|
// Quarter related
|
|
'{quarterNumber}': numberStr,
|
|
'{shortQuarter}': `Q${numberStr}`,
|
|
|
|
// Week related
|
|
'{weekNumber}': numberStr,
|
|
'{weekNumberPadded}': numberStr.padStart(2, '0'),
|
|
'{shortWeek}': `W${numberStr}`,
|
|
'{shortWeek3}': `W${numberStr.padStart(2, '0')}`,
|
|
|
|
// Day related
|
|
'{dayInMonthPadded}': numberStr.padStart(2, '0'),
|
|
'{ordinal}': ordinal(number),
|
|
'{weekDay}': weekDay,
|
|
'{weekDay3}': weekDay.substring(0, 3),
|
|
'{weekDay2}': weekDay.substring(0, 2)
|
|
};
|
|
|
|
const allowedReplacements = Object.entries(allReplacements).reduce((acc, [key, value]) => {
|
|
const replacementKey = key.slice(1, -1);
|
|
if (getTimeUnitReplacements(timeUnit).includes(replacementKey)) {
|
|
acc[key] = value;
|
|
}
|
|
return acc;
|
|
}, {} as Record<string, string>);
|
|
|
|
return Object.entries(allowedReplacements).reduce(
|
|
(title, [key, value]) => title.replace(new RegExp(key, 'g'), value),
|
|
pattern
|
|
);
|
|
}
|
|
|
|
function createNote(parentNote: BNote, noteTitle: string) {
|
|
return noteService.createNewNote({
|
|
parentNoteId: parentNote.noteId,
|
|
title: noteTitle,
|
|
content: "",
|
|
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
|
type: "text"
|
|
}).note;
|
|
}
|
|
|
|
function getRootCalendarNote(): BNote {
|
|
let rootNote;
|
|
|
|
const workspaceNote = hoistedNoteService.getWorkspaceNote();
|
|
|
|
if (!workspaceNote || !workspaceNote.isRoot()) {
|
|
rootNote = searchService.findFirstNoteWithQuery("#workspaceCalendarRoot", new SearchContext({ ignoreHoistedNote: false }));
|
|
}
|
|
|
|
if (!rootNote) {
|
|
rootNote = attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
|
|
}
|
|
|
|
if (!rootNote) {
|
|
sql.transactional(() => {
|
|
rootNote = noteService.createNewNote({
|
|
parentNoteId: "root",
|
|
title: "Calendar",
|
|
target: "into",
|
|
isProtected: false,
|
|
type: "text",
|
|
content: ""
|
|
}).note;
|
|
|
|
attributeService.createLabel(rootNote.noteId, CALENDAR_ROOT_LABEL);
|
|
attributeService.createLabel(rootNote.noteId, "sorted");
|
|
});
|
|
}
|
|
|
|
return rootNote as BNote;
|
|
}
|
|
|
|
function getYearNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
|
const rootNote = _rootNote || getRootCalendarNote();
|
|
|
|
const yearStr = dateStr.trim().substring(0, 4);
|
|
|
|
let yearNote = searchService.findFirstNoteWithQuery(`#${YEAR_LABEL}="${yearStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
|
|
|
|
if (yearNote) {
|
|
return yearNote;
|
|
}
|
|
|
|
sql.transactional(() => {
|
|
yearNote = createNote(rootNote, yearStr);
|
|
|
|
attributeService.createLabel(yearNote.noteId, YEAR_LABEL, yearStr);
|
|
attributeService.createLabel(yearNote.noteId, "sorted");
|
|
|
|
const yearTemplateAttr = rootNote.getOwnedAttribute("relation", "yearTemplate");
|
|
|
|
if (yearTemplateAttr) {
|
|
attributeService.createRelation(yearNote.noteId, "template", yearTemplateAttr.value);
|
|
}
|
|
});
|
|
|
|
return yearNote as unknown as BNote;
|
|
}
|
|
|
|
function getQuarterNumberStr(date: Dayjs) {
|
|
return `${date.year()}-Q${date.quarter()}`;
|
|
}
|
|
|
|
function getQuarterNote(quarterStr: string, _rootNote: BNote | null = null): BNote {
|
|
const rootNote = _rootNote || getRootCalendarNote();
|
|
|
|
quarterStr = quarterStr.trim().substring(0, 7);
|
|
|
|
let quarterNote = searchService.findFirstNoteWithQuery(`#${QUARTER_LABEL}="${quarterStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
|
|
|
|
if (quarterNote) {
|
|
return quarterNote;
|
|
}
|
|
|
|
const [yearStr, quarterNumberStr] = quarterStr.trim().split('-Q');
|
|
const quarterNumber = parseInt(quarterNumberStr);
|
|
const firstMonth = (quarterNumber - 1) * 3;
|
|
const quarterStartDate = dayjs().year(parseInt(yearStr)).month(firstMonth).date(1);
|
|
|
|
const yearNote = getYearNote(yearStr, rootNote);
|
|
const noteTitle = getJournalNoteTitle(rootNote, 'quarter', quarterStartDate, quarterNumber);
|
|
|
|
sql.transactional(() => {
|
|
quarterNote = createNote(yearNote, noteTitle);
|
|
|
|
attributeService.createLabel(quarterNote.noteId, QUARTER_LABEL, quarterStr);
|
|
attributeService.createLabel(quarterNote.noteId, "sorted");
|
|
|
|
const quarterTemplateAttr = rootNote.getOwnedAttribute("relation", "quarterTemplate");
|
|
|
|
if (quarterTemplateAttr) {
|
|
attributeService.createRelation(quarterNote.noteId, "template", quarterTemplateAttr.value);
|
|
}
|
|
});
|
|
|
|
return quarterNote as unknown as BNote;
|
|
}
|
|
|
|
function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
|
const rootNote = _rootNote || getRootCalendarNote();
|
|
|
|
const monthStr = dateStr.substring(0, 7);
|
|
const monthNumber = dateStr.substring(5, 7);
|
|
|
|
let monthNote = searchService.findFirstNoteWithQuery(`#${MONTH_LABEL}="${monthStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
|
|
|
|
if (monthNote) {
|
|
return monthNote;
|
|
}
|
|
|
|
let monthParentNote;
|
|
|
|
if (rootNote.hasLabel('enableQuarterNote')) {
|
|
monthParentNote = getQuarterNote(getQuarterNumberStr(dayjs(dateStr)), rootNote);
|
|
} else {
|
|
monthParentNote = getYearNote(dateStr, rootNote);
|
|
}
|
|
|
|
const noteTitle = getJournalNoteTitle(rootNote, 'month', dayjs(dateStr), parseInt(monthNumber));
|
|
|
|
sql.transactional(() => {
|
|
monthNote = createNote(monthParentNote, noteTitle);
|
|
|
|
attributeService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr);
|
|
attributeService.createLabel(monthNote.noteId, "sorted");
|
|
|
|
const monthTemplateAttr = rootNote.getOwnedAttribute("relation", "monthTemplate");
|
|
|
|
if (monthTemplateAttr) {
|
|
attributeService.createRelation(monthNote.noteId, "template", monthTemplateAttr.value);
|
|
}
|
|
});
|
|
|
|
return monthNote as unknown as BNote;
|
|
}
|
|
|
|
function getWeekStartDate(date: Dayjs): Dayjs {
|
|
const day = date.day();
|
|
let diff;
|
|
|
|
if (optionService.getOption("firstDayOfWeek") === "0") { // Sunday
|
|
diff = date.date() - day + (day === 0 ? -6 : 1); // adjust when day is sunday
|
|
} else { // Monday
|
|
diff = date.date() - day;
|
|
}
|
|
|
|
const startDate = date.clone().date(diff);
|
|
return startDate;
|
|
}
|
|
|
|
// TODO: Duplicated with getWeekNumber in src/public/app/widgets/buttons/calendar.ts
|
|
// Maybe can be merged later in monorepo setup
|
|
function getWeekNumberStr(date: Dayjs): string {
|
|
const year = date.year();
|
|
const dayOfWeek = (day: number) => (day - parseInt(optionService.getOption("firstDayOfWeek")) + 7) % 7;
|
|
|
|
// Get first day of the year and adjust to first week start
|
|
const jan1 = date.clone().year(year).month(0).date(1);
|
|
const jan1Weekday = jan1.day();
|
|
const dayOffset = dayOfWeek(jan1Weekday);
|
|
let firstWeekStart = jan1.clone().subtract(dayOffset, 'day');
|
|
|
|
// Adjust based on week rule
|
|
switch (parseInt(optionService.getOption("firstWeekOfYear"))) {
|
|
case 1: { // ISO 8601: first week contains Thursday
|
|
const thursday = firstWeekStart.clone().add(3, 'day'); // Monday + 3 = Thursday
|
|
if (thursday.year() < year) {
|
|
firstWeekStart = firstWeekStart.add(7, 'day');
|
|
}
|
|
break;
|
|
}
|
|
case 2: { // minDaysInFirstWeek rule
|
|
const daysInFirstWeek = 7 - dayOffset;
|
|
if (daysInFirstWeek < parseInt(optionService.getOption("minDaysInFirstWeek"))) {
|
|
firstWeekStart = firstWeekStart.add(7, 'day');
|
|
}
|
|
break;
|
|
}
|
|
// default case 0: week containing Jan 1 → already handled
|
|
}
|
|
|
|
const diffDays = date.startOf('day').diff(firstWeekStart.startOf('day'), 'day');
|
|
const weekNumber = Math.floor(diffDays / 7) + 1;
|
|
|
|
// Handle case when date is before first week start → belongs to last week of previous year
|
|
if (weekNumber <= 0) {
|
|
return getWeekNumberStr(date.subtract(1, 'day'));
|
|
}
|
|
|
|
// Handle case when date belongs to first week of next year
|
|
const nextYear = year + 1;
|
|
const jan1Next = date.clone().year(nextYear).month(0).date(1);
|
|
const jan1WeekdayNext = jan1Next.day();
|
|
const offsetNext = dayOfWeek(jan1WeekdayNext);
|
|
let nextYearWeekStart = jan1Next.clone().subtract(offsetNext, 'day');
|
|
|
|
switch (parseInt(optionService.getOption("firstWeekOfYear"))) {
|
|
case 1: {
|
|
const thursday = nextYearWeekStart.clone().add(3, 'day');
|
|
if (thursday.year() < nextYear) {
|
|
nextYearWeekStart = nextYearWeekStart.add(7, 'day');
|
|
}
|
|
break;
|
|
}
|
|
case 2: {
|
|
const daysInFirstWeek = 7 - offsetNext;
|
|
if (daysInFirstWeek < parseInt(optionService.getOption("minDaysInFirstWeek"))) {
|
|
nextYearWeekStart = nextYearWeekStart.add(7, 'day');
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (date.isSameOrAfter(nextYearWeekStart)) {
|
|
return `${nextYear}-W01`;
|
|
}
|
|
|
|
return `${year}-W${weekNumber.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
function getWeekFirstDayNote(dateStr: string, rootNote: BNote | null = null) {
|
|
const weekStartDate = getWeekStartDate(dayjs(dateStr));
|
|
return getDayNote(weekStartDate.format('YYYY-MM-DD'), rootNote);
|
|
}
|
|
|
|
function getWeekNote(weekStr: string, _rootNote: BNote | null = null): BNote | null {
|
|
const rootNote = _rootNote || getRootCalendarNote();
|
|
if (!rootNote.hasLabel('enableWeekNote')) {
|
|
return null;
|
|
}
|
|
|
|
weekStr = weekStr.trim().substring(0, 8);
|
|
|
|
let weekNote = searchService.findFirstNoteWithQuery(`#${WEEK_LABEL}="${weekStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
|
|
|
|
if (weekNote) {
|
|
return weekNote;
|
|
}
|
|
|
|
const [yearStr, weekNumStr] = weekStr.trim().split('-W');
|
|
const weekNumber = parseInt(weekNumStr);
|
|
|
|
const firstDayOfYear = dayjs().year(parseInt(yearStr)).month(0).date(1);
|
|
const weekStartDate = firstDayOfYear.add(weekNumber - 1, 'week');
|
|
const startDate = getWeekStartDate(weekStartDate);
|
|
const endDate = dayjs(startDate).add(6, 'day');
|
|
|
|
const startMonth = startDate.month();
|
|
const endMonth = endDate.month();
|
|
|
|
const monthNote = getMonthNote(startDate.format('YYYY-MM-DD'), rootNote);
|
|
const noteTitle = getJournalNoteTitle(rootNote, 'week', startDate, weekNumber);
|
|
|
|
sql.transactional(() => {
|
|
weekNote = createNote(monthNote, noteTitle);
|
|
|
|
attributeService.createLabel(weekNote.noteId, WEEK_LABEL, weekStr);
|
|
attributeService.createLabel(weekNote.noteId, "sorted");
|
|
|
|
const weekTemplateAttr = rootNote.getOwnedAttribute("relation", "weekTemplate");
|
|
|
|
if (weekTemplateAttr) {
|
|
attributeService.createRelation(weekNote.noteId, "template", weekTemplateAttr.value);
|
|
}
|
|
|
|
// If the week spans different months, clone the week note in the other month as well
|
|
if (startMonth !== endMonth) {
|
|
const secondMonthNote = getMonthNote(endDate.format('YYYY-MM-DD'), rootNote);
|
|
cloningService.cloneNoteToParentNote(weekNote.noteId, secondMonthNote.noteId);
|
|
}
|
|
});
|
|
|
|
return weekNote as unknown as BNote;
|
|
}
|
|
|
|
function getDayNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
|
const rootNote = _rootNote || getRootCalendarNote();
|
|
|
|
dateStr = dateStr.trim().substring(0, 10);
|
|
|
|
let dateNote = searchService.findFirstNoteWithQuery(`#${DATE_LABEL}="${dateStr}"`, new SearchContext({ ancestorNoteId: rootNote.noteId }));
|
|
|
|
if (dateNote) {
|
|
return dateNote;
|
|
}
|
|
|
|
let dateParentNote;
|
|
|
|
if (rootNote.hasLabel('enableWeekNote')) {
|
|
dateParentNote = getWeekNote(getWeekNumberStr(dayjs(dateStr)), rootNote);
|
|
} else {
|
|
dateParentNote = getMonthNote(dateStr, rootNote);
|
|
}
|
|
|
|
const dayNumber = dateStr.substring(8, 10);
|
|
const noteTitle = getJournalNoteTitle(rootNote, 'day', dayjs(dateStr), parseInt(dayNumber));
|
|
|
|
sql.transactional(() => {
|
|
dateNote = createNote(dateParentNote as BNote, noteTitle);
|
|
|
|
attributeService.createLabel(dateNote.noteId, DATE_LABEL, dateStr.substring(0, 10));
|
|
|
|
const dateTemplateAttr = rootNote.getOwnedAttribute("relation", "dateTemplate");
|
|
|
|
if (dateTemplateAttr) {
|
|
attributeService.createRelation(dateNote.noteId, "template", dateTemplateAttr.value);
|
|
}
|
|
});
|
|
|
|
return dateNote as unknown as BNote;
|
|
}
|
|
|
|
function getTodayNote(rootNote: BNote | null = null) {
|
|
return getDayNote(dayjs().format('YYYY-MM-DD'), rootNote);
|
|
}
|
|
|
|
export default {
|
|
getRootCalendarNote,
|
|
getYearNote,
|
|
getQuarterNote,
|
|
getMonthNote,
|
|
getWeekNote,
|
|
getWeekFirstDayNote,
|
|
getDayNote,
|
|
getTodayNote
|
|
};
|