Skip to content
Merged
125 changes: 125 additions & 0 deletions packages/dendron-next-server/lib/luxon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// from https://github.com/react-component/picker/pull/230

import { DateTime, Info } from "luxon";
import type { GenerateConfig } from "rc-picker/lib/generate";

/**
* Normalizes part of a moment format string that should
* not be escaped to a luxon compatible format string.
*
* @param part string
* @returns string
*/
const normalizeFormatPart = (part: string): string =>
part
.replace(/Y/g, "y")
.replace(/D/g, "d")
.replace(/gg/g, "kk")
.replace(/Q/g, "q")
.replace(/([Ww])o/g, "WW");

/**
* Normalizes a moment compatible format string to a luxon compatible format string
*
* @param format string
* @returns string
*/
const normalizeFormat = (format: string): string =>
format
// moment escapes strings contained in brackets
.split(/[[\]]/)
.map((part, index) => {
const shouldEscape = index % 2 > 0;

return shouldEscape ? part : normalizeFormatPart(part);
})
// luxon escapes strings contained in single quotes
.join("'");

/**
* Normalizes language tags used to luxon compatible
* language tags by replacing underscores with hyphen-minus.
*
* @param locale string
* @returns string
*/
const normalizeLocale = (locale: string): string => locale.replace(/_/g, "-");

const generateConfig: GenerateConfig<DateTime> = {
// get
getNow: () => DateTime.local(),
getFixedDate: (string) => DateTime.fromFormat(string, "yyyy-MM-dd"),
getEndDate: (date) => date.endOf("month"),
getWeekDay: (date) => date.weekday,
getYear: (date) => date.year,
getMonth: (date) => date.month - 1, // getMonth should return 0-11, luxon month returns 1-12
getDate: (date) => date.day,
getHour: (date) => date.hour,
getMinute: (date) => date.minute,
getSecond: (date) => date.second,

// set
addYear: (date, diff) => date.plus({ year: diff }),
addMonth: (date, diff) => date.plus({ month: diff }),
addDate: (date, diff) => date.plus({ day: diff }),
setYear: (date, year) => date.set({ year }),
setMonth: (date, month) => date.set({ month: month + 1 }), // setMonth month argument is 0-11, luxon months are 1-12
setDate: (date, day) => date.set({ day }),
setHour: (date, hour) => date.set({ hour }),
setMinute: (date, minute) => date.set({ minute }),
setSecond: (date, second) => date.set({ second }),

// Compare
isAfter: (date1, date2) => date1 > date2,
isValidate: (date) => date.isValid,

locale: {
getWeekFirstDate: (locale, date) =>
date.setLocale(normalizeLocale(locale)).startOf("week"),
getWeekFirstDay: (locale) =>
DateTime.local().setLocale(normalizeLocale(locale)).startOf("week")
.weekday,
getWeek: (locale, date) =>
date.setLocale(normalizeLocale(locale)).weekNumber,
getShortWeekDays: (locale) => {
const weekdays = Info.weekdays("short", {
locale: normalizeLocale(locale),
});

// getShortWeekDays should return weekday labels starting from Sunday.
// luxon returns them starting from Monday, so we have to shift the results.
weekdays.unshift(weekdays.pop() as string);

return weekdays
},
getShortMonths: (locale) =>
Info.months("short", { locale: normalizeLocale(locale) }),
// @ts-ignore -- allow format to return `null`
format: (locale, date, format) => {
if (!date || !date.isValid) {
return null;
}

return date
.setLocale(normalizeLocale(locale))
.toFormat(normalizeFormat(format));
},
parse: (locale, text, formats) => {
for (let i = 0; i < formats.length; i += 1) {
const normalizedFormat = normalizeFormat(formats[i]);

const date = DateTime.fromFormat(text, normalizedFormat, {
locale: normalizeLocale(locale),
});

if (date.isValid) {
return date;
}
}

return null;
},
},
};

export default generateConfig;
82 changes: 44 additions & 38 deletions packages/dendron-next-server/pages/vscode/calendar-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
CalendarViewMessageType,
DMessageSource,
NoteProps,
Time,
} from "@dendronhq/common-all";
import {
createLogger,
Expand All @@ -18,32 +19,35 @@ import { Badge, ConfigProvider } from "antd";
import generateCalendar from "antd/lib/calendar/generateCalendar";
import classNames from "classnames";
import _ from "lodash";
import type { Moment } from "moment";
import moment from "moment";
import momentGenerateConfig from "rc-picker/lib/generate/moment";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import luxonGenerateConfig from "../../lib/luxon";
import { DendronProps } from "../../lib/types";

function isSameYear(date1: Moment, date2: Moment) {
type DateTime = InstanceType<typeof Time.DateTime>;

const Calendar = generateCalendar<DateTime>(luxonGenerateConfig);

type CalendarProps = AntdCalendarProps<DateTime>;

function isSameYear(date1: DateTime, date2: DateTime) {
return (
date1 &&
date2 &&
momentGenerateConfig.getYear(date1) === momentGenerateConfig.getYear(date2)
luxonGenerateConfig.getYear(date1) === luxonGenerateConfig.getYear(date2)
);
}

function isSameMonth(date1: Moment, date2: Moment) {
function isSameMonth(date1: DateTime, date2: DateTime) {
return (
isSameYear(date1, date2) &&
momentGenerateConfig.getMonth(date1) ===
momentGenerateConfig.getMonth(date2)
luxonGenerateConfig.getMonth(date1) === luxonGenerateConfig.getMonth(date2)
);
}

function isSameDate(date1: Moment, date2: Moment) {
function isSameDate(date1: DateTime, date2: DateTime) {
return (
isSameMonth(date1, date2) &&
momentGenerateConfig.getDate(date1) === momentGenerateConfig.getDate(date2)
luxonGenerateConfig.getDate(date1) === luxonGenerateConfig.getDate(date2)
);
}

Expand All @@ -52,12 +56,9 @@ function getMaybeDatePortion({ fname }: NoteProps, journalName: string) {
return fname.slice(journalIndex + journalName.length + 1);
}

const today = momentGenerateConfig.getNow();
const Calendar = generateCalendar<Moment>(momentGenerateConfig);
const today = luxonGenerateConfig.getNow();
const { EngineSliceUtils } = engineSlice;

type CalendarProps = AntdCalendarProps<Moment>;

function CalendarView({ engine, ide }: DendronProps) {
// --- init
const ctx = "CalendarView";
Expand All @@ -80,23 +81,21 @@ function CalendarView({ engine, ide }: DendronProps) {

const maxDots: number = 5;
const wordsPerDot: number = 250;
const dailyJournalDomain = config?.journal.dailyDomain;
const defaultJournalName = config?.journal.name;
let defaultJournalDateFormat = config?.journal.dateFormat;
const dayOfWeek = config?.journal.firstDayOfWeek;
const locale = "en-us";
const dailyJournalDomain = config?.journal.dailyDomain || "daily";
const defaultJournalName = config?.journal.name || "journal";

// luxon token format lookup https://github.com/moment/luxon/blob/master/docs/formatting.md#table-of-tokens
let defaultJournalDateFormat = config?.journal.dateFormat || "y.MM.dd";
const defaultJournalMonthDateFormat = "y.MM"; // TODO compute format for currentMode="year" from config

// Currently luxon does not support setting first day of the week (https://github.com/moment/luxon/issues/373)
// const dayOfWeek = config?.journal.firstDayOfWeek;
// const locale = "en-us";

if (defaultJournalDateFormat) {
defaultJournalDateFormat = defaultJournalDateFormat.replace(/dd/, "DD");
defaultJournalDateFormat = defaultJournalDateFormat.replace(/DD/, "dd");
}

useEffect(() => {
moment.updateLocale(locale, {
week: {
dow: dayOfWeek!,
},
});
}, [dayOfWeek, locale]);

const groupedDailyNotes = useMemo(() => {
const vaultNotes = _.values(notes).filter((notes) => {
if (currentVault) {
Expand All @@ -109,7 +108,7 @@ function CalendarView({ engine, ide }: DendronProps) {
note.fname.startsWith(`${dailyJournalDomain}.`)
);
const result = _.groupBy(dailyNotes, (note) => {
return getMaybeDatePortion(note, defaultJournalName!);
return getMaybeDatePortion(note, defaultJournalName);
});
return result;
}, [notes, defaultJournalName, currentVault?.fsPath]);
Expand All @@ -118,30 +117,37 @@ function CalendarView({ engine, ide }: DendronProps) {
if (noteActive) {
const maybeDatePortion = getMaybeDatePortion(
noteActive,
defaultJournalName!
defaultJournalName
);

// check if daily file is `y.MM` instead of `y.MM.dd` to apply proper format string.
// unlike moment luxon marks the date as invalid if date and dateformat do not match
const isMonthly = maybeDatePortion.split(".").length === 2;

return maybeDatePortion && _.first(groupedDailyNotes[maybeDatePortion])
? moment(maybeDatePortion, defaultJournalDateFormat)
? Time.DateTime.fromFormat(
maybeDatePortion,
isMonthly ? defaultJournalMonthDateFormat : defaultJournalDateFormat
)
: undefined;
}
}, [noteActive, groupedDailyNotes]);

const getDateKey = useCallback<
(date: Moment, mode?: CalendarProps["mode"]) => string
(date: DateTime, mode?: CalendarProps["mode"]) => string
>(
(date, mode) => {
const format =
(mode || activeMode) === "month"
? defaultJournalDateFormat || "y.MM.dd"
: "y.MM"; // TODO compute format for currentMode="year" from config
return date.format(format);
: defaultJournalMonthDateFormat;
return date.toFormat(format);
},
[activeMode, defaultJournalDateFormat]
);

const onSelect = useCallback<
(date: Moment, mode?: CalendarProps["mode"]) => void
(date: DateTime, mode?: CalendarProps["mode"]) => void
>(
(date, mode) => {
logger.info({ ctx: "onSelect", date });
Expand Down Expand Up @@ -170,7 +176,7 @@ function CalendarView({ engine, ide }: DendronProps) {
const onClickToday = useCallback(() => {
const mode = "month";
setActiveMode(mode);
onSelect(moment(), mode);
onSelect(Time.now(), mode);
}, [onSelect]);

const dateFullCellRender = useCallback<
Expand Down Expand Up @@ -240,7 +246,7 @@ function CalendarView({ engine, ide }: DendronProps) {
className={`${calendarPrefixCls}-date-value`}
style={{ color: !dailyNote ? "gray" : undefined }}
>
{_.padStart(String(momentGenerateConfig.getDate(date)), 2, "0")}
{_.padStart(String(luxonGenerateConfig.getDate(date)), 2, "0")}
</div>
<div className={`${calendarPrefixCls}-date-content`}>{dateCell}</div>
</div>
Expand Down Expand Up @@ -303,7 +309,7 @@ function CalendarView({ engine, ide }: DendronProps) {
}

function areEqual(prevProps: DendronProps, nextProps: DendronProps) {
const logger = createLogger("treeViewContainer");
const logger = createLogger("calendarViewContainer");
const isDiff = _.some([
// active note changed
prevProps.ide.noteActive?.id !== nextProps.ide.noteActive?.id,
Expand Down