Skip to content

Commit ab46917

Browse files
committed
add support for improve date formating using ‘advanced formatting’ plugin
1 parent 8bfb57c commit ab46917

File tree

9 files changed

+520
-2
lines changed

9 files changed

+520
-2
lines changed

src/command/render/render-files.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from "../../config/constants.ts";
1818
import { isHtmlCompatible } from "../../config/format.ts";
1919
import { mergeConfigs } from "../../core/config.ts";
20-
import { setDateLocale } from "../../core/date.ts";
20+
import { initDayJsPlugins, setDateLocale } from "../../core/date.ts";
2121
import { initDenoDom } from "../../core/deno-dom.ts";
2222
import { HandlerContextResults } from "../../core/handlers/types.ts";
2323
import {
@@ -318,6 +318,7 @@ export async function renderFiles(
318318

319319
// Set the date locale for this render
320320
// Used for date formatting
321+
initDayJsPlugins();
321322
await setDateLocale(context.format.metadata[kLang] as string);
322323

323324
const fileLifetime = createNamedLifetime("render-file");

src/core/date.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import momentGuess from "moment-guess";
88

99
import { parse } from "datetime/mod.ts";
1010
import dayjs from "dayjs/dayjs.min.js";
11+
import advancedPlugin from "../resources/library/dayjs/plugins/advanced.js";
12+
import timezonePlugin from "../resources/library/dayjs/plugins/timezone.js";
13+
import utcPlugin from "../resources/library/dayjs/plugins/utc.js";
14+
import isoWeekPlugin from "../resources/library/dayjs/plugins/isoweek.js";
15+
import weekOfYearPlugin from "../resources/library/dayjs/plugins/weekofyear.js";
16+
import weekYearPlugin from "../resources/library/dayjs/plugins/weekyear.js";
1117
import { existsSync } from "fs/mod.ts";
1218

1319
import { toFileUrl } from "path/mod.ts";
@@ -72,6 +78,15 @@ export function parseSpecialDate(
7278
}
7379
}
7480

81+
export function initDayJsPlugins() {
82+
dayjs.extend(utcPlugin);
83+
dayjs.extend(timezonePlugin);
84+
dayjs.extend(isoWeekPlugin);
85+
dayjs.extend(weekYearPlugin);
86+
dayjs.extend(weekOfYearPlugin);
87+
dayjs.extend(advancedPlugin);
88+
}
89+
7590
export async function setDateLocale(locale: string) {
7691
if (locale !== dayjs.locale()) {
7792
const localePath = resourcePath(`library/dayjs/locale/${locale}.js`);
@@ -154,7 +169,7 @@ export const parsePandocDate = (dateRaw: string): Date => {
154169
// ambiguous. If so, just take the first format
155170
const format = Array.isArray(formats) ? formats[0] : formats;
156171
const date = dayjs(dateStr, format);
157-
return date.local();
172+
return date.toDate();
158173
} catch {
159174
// Couldn't parse, keep going
160175
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { FORMAT_DEFAULT } from "./constant.js";
2+
3+
export default (o, c, d) => {
4+
// locale needed later
5+
const proto = c.prototype;
6+
const oldFormat = proto.format;
7+
d.en.ordinal = (number) => {
8+
const s = ["th", "st", "nd", "rd"];
9+
const v = number % 100;
10+
return `[${number}${s[(v - 20) % 10] || s[v] || s[0]}]`;
11+
};
12+
// extend en locale here
13+
proto.format = function (formatStr) {
14+
const locale = this.$locale();
15+
16+
if (!this.isValid()) {
17+
return oldFormat.bind(this)(formatStr);
18+
}
19+
20+
const utils = this.$utils();
21+
const str = formatStr || FORMAT_DEFAULT;
22+
const result = str.replace(
23+
/\[([^\]]+)]|Q|wo|ww|w|WW|W|zzz|z|gggg|GGGG|Do|X|x|k{1,2}|S/g,
24+
(match) => {
25+
switch (match) {
26+
case "Q":
27+
return Math.ceil((this.$M + 1) / 3);
28+
case "Do":
29+
return locale.ordinal(this.$D);
30+
case "gggg":
31+
return this.weekYear();
32+
case "GGGG":
33+
return this.isoWeekYear();
34+
case "wo":
35+
return locale.ordinal(this.week(), "W"); // W for week
36+
case "w":
37+
case "ww":
38+
return utils.s(this.week(), match === "w" ? 1 : 2, "0");
39+
case "W":
40+
case "WW":
41+
return utils.s(this.isoWeek(), match === "W" ? 1 : 2, "0");
42+
case "k":
43+
case "kk":
44+
return utils.s(
45+
String(this.$H === 0 ? 24 : this.$H),
46+
match === "k" ? 1 : 2,
47+
"0"
48+
);
49+
case "X":
50+
return Math.floor(this.$d.getTime() / 1000);
51+
case "x":
52+
return this.$d.getTime();
53+
case "z":
54+
return `[${this.offsetName()}]`;
55+
case "zzz":
56+
return `[${this.offsetName("long")}]`;
57+
default:
58+
return match;
59+
}
60+
}
61+
);
62+
return oldFormat.bind(this)(result);
63+
};
64+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export const SECONDS_A_MINUTE = 60;
2+
export const SECONDS_A_HOUR = SECONDS_A_MINUTE * 60;
3+
export const SECONDS_A_DAY = SECONDS_A_HOUR * 24;
4+
export const SECONDS_A_WEEK = SECONDS_A_DAY * 7;
5+
6+
export const MILLISECONDS_A_SECOND = 1e3;
7+
export const MILLISECONDS_A_MINUTE = SECONDS_A_MINUTE * MILLISECONDS_A_SECOND;
8+
export const MILLISECONDS_A_HOUR = SECONDS_A_HOUR * MILLISECONDS_A_SECOND;
9+
export const MILLISECONDS_A_DAY = SECONDS_A_DAY * MILLISECONDS_A_SECOND;
10+
export const MILLISECONDS_A_WEEK = SECONDS_A_WEEK * MILLISECONDS_A_SECOND;
11+
12+
// English locales
13+
export const MS = "millisecond";
14+
export const S = "second";
15+
export const MIN = "minute";
16+
export const H = "hour";
17+
export const D = "day";
18+
export const W = "week";
19+
export const M = "month";
20+
export const Q = "quarter";
21+
export const Y = "year";
22+
export const DATE = "date";
23+
24+
export const FORMAT_DEFAULT = "YYYY-MM-DDTHH:mm:ssZ";
25+
26+
export const INVALID_DATE_STRING = "Invalid Date";
27+
28+
// regex
29+
export const REGEX_PARSE =
30+
/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/;
31+
export const REGEX_FORMAT =
32+
/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { D, W, Y } from "./constant.js";
2+
3+
const isoWeekPrettyUnit = "isoweek";
4+
5+
export default (o, c, d) => {
6+
const getYearFirstThursday = (year, isUtc) => {
7+
const yearFirstDay = (isUtc ? d.utc : d)().year(year).startOf(Y);
8+
let addDiffDays = 4 - yearFirstDay.isoWeekday();
9+
if (yearFirstDay.isoWeekday() > 4) {
10+
addDiffDays += 7;
11+
}
12+
return yearFirstDay.add(addDiffDays, D);
13+
};
14+
15+
const getCurrentWeekThursday = (ins) => ins.add(4 - ins.isoWeekday(), D);
16+
17+
const proto = c.prototype;
18+
19+
proto.isoWeekYear = function () {
20+
const nowWeekThursday = getCurrentWeekThursday(this);
21+
return nowWeekThursday.year();
22+
};
23+
24+
proto.isoWeek = function (week) {
25+
if (!this.$utils().u(week)) {
26+
return this.add((week - this.isoWeek()) * 7, D);
27+
}
28+
const nowWeekThursday = getCurrentWeekThursday(this);
29+
const diffWeekThursday = getYearFirstThursday(this.isoWeekYear(), this.$u);
30+
return nowWeekThursday.diff(diffWeekThursday, W) + 1;
31+
};
32+
33+
proto.isoWeekday = function (week) {
34+
if (!this.$utils().u(week)) {
35+
return this.day(this.day() % 7 ? week : week - 7);
36+
}
37+
return this.day() || 7;
38+
};
39+
40+
const oldStartOf = proto.startOf;
41+
proto.startOf = function (units, startOf) {
42+
const utils = this.$utils();
43+
const isStartOf = !utils.u(startOf) ? startOf : true;
44+
const unit = utils.p(units);
45+
if (unit === isoWeekPrettyUnit) {
46+
return isStartOf
47+
? this.date(this.date() - (this.isoWeekday() - 1)).startOf("day")
48+
: this.date(this.date() - 1 - (this.isoWeekday() - 1) + 7).endOf("day");
49+
}
50+
return oldStartOf.bind(this)(units, startOf);
51+
};
52+
};
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { MIN, MS } from "./constant.js";
2+
3+
const typeToPos = {
4+
year: 0,
5+
month: 1,
6+
day: 2,
7+
hour: 3,
8+
minute: 4,
9+
second: 5,
10+
};
11+
12+
// Cache time-zone lookups from Intl.DateTimeFormat,
13+
// as it is a *very* slow method.
14+
const dtfCache = {};
15+
const getDateTimeFormat = (timezone, options = {}) => {
16+
const timeZoneName = options.timeZoneName || "short";
17+
const key = `${timezone}|${timeZoneName}`;
18+
let dtf = dtfCache[key];
19+
if (!dtf) {
20+
dtf = new Intl.DateTimeFormat("en-US", {
21+
hour12: false,
22+
timeZone: timezone,
23+
year: "numeric",
24+
month: "2-digit",
25+
day: "2-digit",
26+
hour: "2-digit",
27+
minute: "2-digit",
28+
second: "2-digit",
29+
timeZoneName,
30+
});
31+
dtfCache[key] = dtf;
32+
}
33+
return dtf;
34+
};
35+
36+
export default (o, c, d) => {
37+
let defaultTimezone;
38+
39+
const makeFormatParts = (timestamp, timezone, options = {}) => {
40+
const date = new Date(timestamp);
41+
const dtf = getDateTimeFormat(timezone, options);
42+
return dtf.formatToParts(date);
43+
};
44+
45+
const tzOffset = (timestamp, timezone) => {
46+
const formatResult = makeFormatParts(timestamp, timezone);
47+
const filled = [];
48+
for (let i = 0; i < formatResult.length; i += 1) {
49+
const { type, value } = formatResult[i];
50+
const pos = typeToPos[type];
51+
52+
if (pos >= 0) {
53+
filled[pos] = parseInt(value, 10);
54+
}
55+
}
56+
const hour = filled[3];
57+
// Workaround for the same behavior in different node version
58+
// https://github.com/nodejs/node/issues/33027
59+
/* istanbul ignore next */
60+
const fixedHour = hour === 24 ? 0 : hour;
61+
const utcString = `${filled[0]}-${filled[1]}-${filled[2]} ${fixedHour}:${filled[4]}:${filled[5]}:000`;
62+
const utcTs = d.utc(utcString).valueOf();
63+
let asTS = +timestamp;
64+
const over = asTS % 1000;
65+
asTS -= over;
66+
return (utcTs - asTS) / (60 * 1000);
67+
};
68+
69+
// find the right offset a given local time. The o input is our guess, which determines which
70+
// offset we'll pick in ambiguous cases (e.g. there are two 3 AMs b/c Fallback DST)
71+
// https://github.com/moment/luxon/blob/master/src/datetime.js#L76
72+
const fixOffset = (localTS, o0, tz) => {
73+
// Our UTC time is just a guess because our offset is just a guess
74+
let utcGuess = localTS - o0 * 60 * 1000;
75+
// Test whether the zone matches the offset for this ts
76+
const o2 = tzOffset(utcGuess, tz);
77+
// If so, offset didn't change and we're done
78+
if (o0 === o2) {
79+
return [utcGuess, o0];
80+
}
81+
// If not, change the ts by the difference in the offset
82+
utcGuess -= (o2 - o0) * 60 * 1000;
83+
// If that gives us the local time we want, we're done
84+
const o3 = tzOffset(utcGuess, tz);
85+
if (o2 === o3) {
86+
return [utcGuess, o2];
87+
}
88+
// If it's different, we're in a hole time.
89+
// The offset has changed, but the we don't adjust the time
90+
return [localTS - Math.min(o2, o3) * 60 * 1000, Math.max(o2, o3)];
91+
};
92+
93+
const proto = c.prototype;
94+
95+
proto.tz = function (timezone = defaultTimezone, keepLocalTime) {
96+
const oldOffset = this.utcOffset();
97+
const date = this.toDate();
98+
const target = date.toLocaleString("en-US", { timeZone: timezone });
99+
const diff = Math.round((date - new Date(target)) / 1000 / 60);
100+
let ins = d(target)
101+
.$set(MS, this.$ms)
102+
.utcOffset(-Math.round(date.getTimezoneOffset() / 15) * 15 - diff, true);
103+
if (keepLocalTime) {
104+
const newOffset = ins.utcOffset();
105+
ins = ins.add(oldOffset - newOffset, MIN);
106+
}
107+
ins.$x.$timezone = timezone;
108+
return ins;
109+
};
110+
111+
proto.offsetName = function (type) {
112+
// type: short(default) / long
113+
const zone = this.$x?.$timezone || d.tz.guess();
114+
const result = makeFormatParts(this.valueOf(), zone, {
115+
timeZoneName: type,
116+
}).find((m) => m.type.toLowerCase() === "timezonename");
117+
return result && result.value;
118+
};
119+
120+
const oldStartOf = proto.startOf;
121+
proto.startOf = function (units, startOf) {
122+
if (!this.$x || !this.$x.$timezone) {
123+
return oldStartOf.call(this, units, startOf);
124+
}
125+
126+
const withoutTz = d(this.format("YYYY-MM-DD HH:mm:ss:SSS"));
127+
const startOfWithoutTz = oldStartOf.call(withoutTz, units, startOf);
128+
return startOfWithoutTz.tz(this.$x.$timezone, true);
129+
};
130+
131+
d.tz = function (input, arg1, arg2) {
132+
const parseFormat = arg2 && arg1;
133+
const timezone = arg2 || arg1 || defaultTimezone;
134+
const previousOffset = tzOffset(+d(), timezone);
135+
if (typeof input !== "string") {
136+
// timestamp number || js Date || Day.js
137+
return d(input).tz(timezone);
138+
}
139+
const localTs = d.utc(input, parseFormat).valueOf();
140+
const [targetTs, targetOffset] = fixOffset(
141+
localTs,
142+
previousOffset,
143+
timezone
144+
);
145+
const ins = d(targetTs).utcOffset(targetOffset);
146+
ins.$x.$timezone = timezone;
147+
return ins;
148+
};
149+
150+
d.tz.guess = function () {
151+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
152+
};
153+
154+
d.tz.setDefault = function (timezone) {
155+
defaultTimezone = timezone;
156+
};
157+
};

0 commit comments

Comments
 (0)