Skip to content

Commit

Permalink
Adds addDateParsing configuration method to allow custom date parsi…
Browse files Browse the repository at this point in the history
…ng formats. Fixes #867
  • Loading branch information
zachleat committed Jun 27, 2024
1 parent 047f661 commit 8a79ee8
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 33 deletions.
84 changes: 52 additions & 32 deletions src/Template.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,10 @@ class Template extends TemplateContent {
let newDate = await this.getMappedDate(data);

// Make sure to keep these keys synchronized in src/Util/ReservedData.js
data.page.date = newDate;
// Skip date assignment if custom date is falsy.
if (newDate) {
data.page.date = newDate;
}
data.page.inputPath = this.inputPath;
data.page.fileSlug = this.fileSlugStr;
data.page.filePathStem = this.filePathStem;
Expand Down Expand Up @@ -908,17 +911,31 @@ class Template extends TemplateContent {
}

async getMappedDate(data) {
if ("date" in data && data.date) {
let dateValue = data?.date;

// These can return a Date object, or a string.
for (let fn of this.config.customDateParsing) {
let ret = fn(dateValue);
if (ret === false) {
// Skip out, no date will be assigned to this template!
return false;
} else if (ret) {
debug("getMappedDate: date value override via `addDateParsing` callback to %o", ret);
dateValue = ret;
}
}

if (dateValue) {
debug("getMappedDate: using a date in the data for %o of %o", this.inputPath, data.date);
if (data.date instanceof Date) {
if (dateValue instanceof Date) {
// YAML does its own date parsing
debug("getMappedDate: YAML parsed it: %o", data.date);
return data.date;
debug("getMappedDate: found Date instance (maybe from YAML): %o", dateValue);
return dateValue;
}

// special strings
if (!this.isVirtualTemplate()) {
if (data.date.toLowerCase() === "git last modified") {
if (dateValue.toLowerCase() === "git last modified") {
let d = getDateFromGitLastUpdated(this.inputPath);
if (d) {
return d;
Expand All @@ -927,10 +944,10 @@ class Template extends TemplateContent {
// return now if this file is not yet available in `git`
return new Date();
}
if (data.date.toLowerCase() === "last modified") {
if (dateValue.toLowerCase() === "last modified") {
return this._getDateInstance("ctimeMs");
}
if (data.date.toLowerCase() === "git created") {
if (dateValue.toLowerCase() === "git created") {
let d = getDateFromGitFirstAdded(this.inputPath);
if (d) {
return d;
Expand All @@ -939,42 +956,45 @@ class Template extends TemplateContent {
// return now if this file is not yet available in `git`
return new Date();
}
if (data.date.toLowerCase() === "created") {
if (dateValue.toLowerCase() === "created") {
return this._getDateInstance("birthtimeMs");
}
}

// try to parse with Luxon
let date = DateTime.fromISO(data.date, { zone: "utc" });
let date = DateTime.fromISO(dateValue, { zone: "utc" });
if (!date.isValid) {
throw new Error(`date front matter value (${data.date}) is invalid for ${this.inputPath}`);
throw new Error(
`Data cascade value for \`date\` (${dateValue}) is invalid for ${this.inputPath}`,
);
}
debug("getMappedDate: Luxon parsed %o: %o and %o", data.date, date, date.toJSDate());
debug("getMappedDate: Luxon parsed %o: %o and %o", dateValue, date, date.toJSDate());

return date.toJSDate();
} else {
let filepathRegex = this.inputPath.match(/(\d{4}-\d{2}-\d{2})/);
if (filepathRegex !== null) {
// if multiple are found in the path, use the first one for the date
let dateObj = DateTime.fromISO(filepathRegex[1], {
zone: "utc",
}).toJSDate();
debug(
"getMappedDate: using filename regex time for %o of %o: %o",
this.inputPath,
filepathRegex[1],
dateObj,
);
return dateObj;
}
}

// No date was specified.
if (this.isVirtualTemplate()) {
return new Date();
}
// No Date supplied in the Data Cascade, try to find the date in the file name
let filepathRegex = this.inputPath.match(/(\d{4}-\d{2}-\d{2})/);
if (filepathRegex !== null) {
// if multiple are found in the path, use the first one for the date
let dateObj = DateTime.fromISO(filepathRegex[1], {
zone: "utc",
}).toJSDate();
debug(
"getMappedDate: using filename regex time for %o of %o: %o",
this.inputPath,
filepathRegex[1],
dateObj,
);
return dateObj;
}

return this._getDateInstance("birthtimeMs");
// No Date supplied in the Data Cascade
if (this.isVirtualTemplate()) {
return new Date();
}

return this._getDateInstance("birthtimeMs");
}

// Important reminder: Template data is first generated in TemplateMap
Expand Down
12 changes: 12 additions & 0 deletions src/UserConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class UserConfig {
// Supplementary engines
engines: {
yaml: yaml.load.bind(yaml),

node: (frontMatterCode, { filePath }) => {
let vm = new RetrieveGlobals(frontMatterCode, {
filePath,
Expand Down Expand Up @@ -161,6 +162,8 @@ class UserConfig {
this.virtualTemplates = {};

this.freezeReservedData = true;

this.customDateParsingCallbacks = new Set();
}

// compatibleRange is optional in 2.0.0-beta.2
Expand Down Expand Up @@ -997,6 +1000,14 @@ class UserConfig {
this.freezeReservedData = !!bool;
}

addDateParsing(callback) {
if (typeof callback === "function") {
this.customDateParsingCallbacks.add(callback);
} else {
throw new Error("addDateParsing expects a function argument.");
}
}

getMergingConfigObject() {
let obj = {
// filters removed in 1.0 (use addTransform instead)
Expand Down Expand Up @@ -1050,6 +1061,7 @@ class UserConfig {
virtualTemplates: this.virtualTemplates,
// `directories` and `directoryAssignments` are merged manually prior to plugin processing
freezeReservedData: this.freezeReservedData,
customDateParsing: this.customDateParsingCallbacks,
};

if (Array.isArray(this.dataFileSuffixesOverride)) {
Expand Down
138 changes: 138 additions & 0 deletions test/EleventyTest-CustomDateParsing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import test from "ava";
import Eleventy from "../src/Eleventy.js";

test("Custom date parsing callback (one, return string), Issue #867", async (t) => {
t.plan(2);

let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", {
config: eleventyConfig => {
eleventyConfig.addTemplate("test.html", `# Markdown`);
eleventyConfig.dataFilterSelectors.add("page.date");

eleventyConfig.addDateParsing((dateValue) => {
t.is(dateValue, undefined);
return "2001-01-01T12:00:00Z";
});
}
});
elev.disableLogger();

let [result] = await elev.toJSON();
t.deepEqual(result.data.page.date, new Date(Date.UTC(2001,0,1,12)));
});

test("Custom date parsing callback (one, return Date), Issue #867", async (t) => {
t.plan(2);

let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", {
config: eleventyConfig => {
eleventyConfig.addTemplate("test.html", `# Markdown`);
eleventyConfig.dataFilterSelectors.add("page.date");

eleventyConfig.addDateParsing((dateValue) => {
t.is(dateValue, undefined);
return new Date(Date.UTC(2001,0,1,12));
});
}
});
elev.disableLogger();

let [result] = await elev.toJSON();
t.deepEqual(result.data.page.date, new Date(Date.UTC(2001,0,1,12)));
});

test("Custom date parsing callback (one, using date from data cascade, return string), Issue #867", async (t) => {
t.plan(2);

let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", {
config: eleventyConfig => {
eleventyConfig.addTemplate("test.html", `# Markdown`, {
date: new Date(Date.UTC(2002, 0, 1, 12))
});
eleventyConfig.dataFilterSelectors.add("page.date");

eleventyConfig.addDateParsing((dateValue) => {
t.true(dateValue instanceof Date);
return "2001-01-01T12:00:00Z";
});
}
});
elev.disableLogger();

let [result] = await elev.toJSON();
t.deepEqual(result.data.page.date, new Date(Date.UTC(2001,0,1,12)));
});

test("Custom date parsing callback (two, return undefined/falsy), Issue #867", async (t) => {
t.plan(3);

let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", {
config: eleventyConfig => {
eleventyConfig.addTemplate("test.html", `# Markdown`, {
date: new Date(Date.UTC(2003, 0, 1, 12))
});
eleventyConfig.dataFilterSelectors.add("page.date");

eleventyConfig.addDateParsing((dateValue) => {
t.deepEqual(dateValue, new Date(Date.UTC(2003,0,1,12)));
// return nothing
});

eleventyConfig.addDateParsing((dateValue) => {
t.deepEqual(dateValue, new Date(Date.UTC(2003,0,1,12)));
// return nothing
});
}
});
elev.disableLogger();

let [result] = await elev.toJSON();
t.deepEqual(result.data.page.date, new Date(Date.UTC(2003,0,1,12)));
});

test("Custom date parsing callback (two, return explicit false), Issue #867", async (t) => {
t.plan(1);

let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", {
config: eleventyConfig => {
eleventyConfig.addTemplate("test.html", `# Markdown`, {
date: new Date(Date.UTC(2003, 0, 1, 12))
});
eleventyConfig.dataFilterSelectors.add("page.date");

eleventyConfig.addDateParsing((dateValue) => {
return false;
});
}
});

elev.disableLogger();

let [result] = await elev.toJSON();
t.deepEqual(result.data.page.date, undefined);
});

test("Custom date parsing callbacks (two, last wins, return string), Issue #867", async (t) => {
t.plan(3);

let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/", {
config: eleventyConfig => {
eleventyConfig.addTemplate("test.html", `# Markdown`);
eleventyConfig.dataFilterSelectors.add("page.date");

eleventyConfig.addDateParsing((dateValue) => {
t.is(dateValue, undefined);
return "2010-01-01T12:00:00Z";
});

eleventyConfig.addDateParsing((dateValue) => {
t.is(dateValue, "2010-01-01T12:00:00Z");
return "2001-01-01T12:00:00Z";
});
}
});
elev.disableLogger();

let [result] = await elev.toJSON();
t.deepEqual(result.data.page.date, new Date(Date.UTC(2001,0,1,12)));
});
2 changes: 1 addition & 1 deletion test/TemplateTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -1456,7 +1456,7 @@ test("Issue 413 weird date format", async (t) => {
await t.throwsAsync(async function () {
await tmpl.getData();
}, {
message: "date front matter value (2019-03-13 20:18:42 +0000) is invalid for ./test/stubs-413/date-frontmatter.md"
message: "Data cascade value for `date` (2019-03-13 20:18:42 +0000) is invalid for ./test/stubs-413/date-frontmatter.md"
});
});

Expand Down

0 comments on commit 8a79ee8

Please sign in to comment.