Skip to content

Commit

Permalink
Adds eleventyConfig.augmentFunctionContext for plugins to use on sh…
Browse files Browse the repository at this point in the history
…ortcodes and filters to establish this.page and this.eleventy (and future props too) Related to #3310 and noelforte/eleventy-plugin-vento#9
  • Loading branch information
zachleat committed Jul 8, 2024
1 parent 9961e07 commit 26d26b7
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 55 deletions.
39 changes: 13 additions & 26 deletions src/Engines/JavaScript.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ import EleventyBaseError from "../Errors/EleventyBaseError.js";
import getJavaScriptData from "../Util/GetJavaScriptData.js";
import EventBusUtil from "../Util/EventBusUtil.js";
import { EleventyImport } from "../Util/Require.js";
import { augmentFunction, augmentObject } from "./Util/ContextAugmenter.js";

class JavaScriptTemplateNotDefined extends EleventyBaseError {}

class JavaScript extends TemplateEngine {
// which data keys to bind to `this` in JavaScript template functions
static DATA_KEYS_TO_BIND = ["page", "eleventy"];

constructor(name, templateConfig) {
super(name, templateConfig);
this.instances = {};
Expand Down Expand Up @@ -129,26 +127,19 @@ class JavaScript extends TemplateEngine {

for (let key in configFns) {
// prefer pre-existing `page` javascriptFunction, if one exists
if (key === "page") {
// do nothing
} else {
// note: wrapping creates a new function
fns[key] = JavaScript.wrapJavaScriptFunction(inst, configFns[key]);
}
fns[key] = augmentFunction(configFns[key], {
source: inst,
overwrite: false,
});
}
return fns;
}

// Backwards compat
static wrapJavaScriptFunction(inst, fn) {
return function (...args) {
for (let key of JavaScript.DATA_KEYS_TO_BIND) {
if (inst?.[key]) {
this[key] = inst[key];
}
}

return fn.call(this, ...args);
};
return augmentFunction(fn, {
source: inst,
});
}

addExportsToBundles(inst, url) {
Expand Down Expand Up @@ -210,14 +201,10 @@ class JavaScript extends TemplateEngine {
this.addExportsToBundles(inst, data.page.url);
}

for (let key of JavaScript.DATA_KEYS_TO_BIND) {
if (!inst[key] && data[key]) {
// only blow away existing inst.page if it has a page.url
if (key !== "page" || !inst.page || inst.page.url) {
inst[key] = data[key];
}
}
}
augmentObject(inst, {
source: data,
overwrite: false,
});

Object.assign(inst, this.getJavaScriptFunctions(inst));

Expand Down
28 changes: 17 additions & 11 deletions src/Engines/Liquid.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TemplatePath } from "@11ty/eleventy-utils";
// import debugUtil from "debug";

import TemplateEngine from "./TemplateEngine.js";
import { augmentObject } from "./Util/ContextAugmenter.js";

// const debug = debugUtil("Eleventy:Liquid");

Expand Down Expand Up @@ -57,14 +58,13 @@ class Liquid extends TemplateEngine {

static wrapFilter(name, fn) {
return function (...args) {
if (this.context && "get" in this.context) {
for (let propertyName of ["page", "eleventy"]) {
Object.defineProperty(this, propertyName, {
configurable: true,
enumerable: true,
get: () => this.context.get([propertyName]),
});
}
// Set this.eleventy and this.page
if (typeof this.context?.get === "function") {
augmentObject(this, {
source: this.context,
getter: (key, context) => context.get([key]),
lazy: this.context.strictVariables,
});
}

// We *don’t* wrap this in an EleventyFilterError because Liquid has a better error message with line/column information in the template
Expand All @@ -76,10 +76,16 @@ class Liquid extends TemplateEngine {
static normalizeScope(context) {
let obj = {};
if (context) {
obj.ctx = context;
obj.page = context.get(["page"]);
obj.eleventy = context.get(["eleventy"]);
obj.ctx = context; // Full context available on `ctx`

// Set this.eleventy and this.page
augmentObject(obj, {
source: context,
getter: (key, context) => context.get([key]),
lazy: context.strictVariables,
});
}

return obj;
}

Expand Down
31 changes: 13 additions & 18 deletions src/Engines/Nunjucks.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import EleventyErrorUtil from "../Errors/EleventyErrorUtil.js";
import EleventyBaseError from "../Errors/EleventyBaseError.js";
import EleventyShortcodeError from "../Errors/EleventyShortcodeError.js";
import EventBusUtil from "../Util/EventBusUtil.js";

class EleventyFilterError extends EleventyBaseError {}
import { augmentObject } from "./Util/ContextAugmenter.js";

class Nunjucks extends TemplateEngine {
constructor(name, eleventyConfig) {
Expand Down Expand Up @@ -101,18 +100,16 @@ class Nunjucks extends TemplateEngine {

static wrapFilter(name, fn) {
return function (...args) {
if (this.ctx?.page) {
this.page = this.ctx.page;
}
if (this.ctx?.eleventy) {
this.eleventy = this.ctx.eleventy;
}

try {
augmentObject(this, {
source: this.ctx,
lazy: false, // context.env?.opts.throwOnUndefined,
});

return fn.call(this, ...args);
} catch (e) {
throw new EleventyFilterError(
`Error in Nunjucks filter \`${name}\`${this.page ? ` (${this.page.inputPath})` : ""}${EleventyErrorUtil.convertErrorToString(e)}`,
throw new EleventyBaseError(
`Error in Nunjucks Filter \`${name}\`${this.page ? ` (${this.page.inputPath})` : ""}${EleventyErrorUtil.convertErrorToString(e)}`,
e,
);
}
Expand All @@ -124,14 +121,12 @@ class Nunjucks extends TemplateEngine {
let obj = {};
if (context.ctx) {
obj.ctx = context.ctx;
obj.env = context.env;

if (context.ctx.page) {
obj.page = context.ctx.page;
}

if (context.ctx.eleventy) {
obj.eleventy = context.ctx.eleventy;
}
augmentObject(obj, {
source: context.ctx,
lazy: false, // context.env?.opts.throwOnUndefined,
});
}
return obj;
}
Expand Down
66 changes: 66 additions & 0 deletions src/Engines/Util/ContextAugmenter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const DATA_KEYS = ["page", "eleventy"];

function augmentFunction(fn, options = {}) {
let t = typeof fn;
if (t !== "function") {
throw new Error(
"Invalid type passed to `augmentFunction`. A function was expected and received: " + t,
);
}

return function (...args) {
let context = augmentObject(this || {}, options);
return fn.call(context, ...args);
};
}

function augmentObject(targetObject, options = {}) {
options = Object.assign(
{
source: undefined, // where to copy from
overwrite: true,
lazy: false, // lazily fetch the property
// getter: function() {},
},
options,
);

for (let key of DATA_KEYS) {
// Skip if overwrite: false and prop already exists on target
if (!options.overwrite && targetObject[key]) {
continue;
}

if (options.lazy) {
let value;
if (typeof options.getter == "function") {
value = () => options.getter(key, options.source);
} else {
value = () => options.source?.[key];
}

// lazy getter important for Liquid strictVariables support
Object.defineProperty(targetObject, key, {
writeable: true,
configurable: true,
enumerable: true,
value,
});
} else {
let value;
if (typeof options.getter == "function") {
value = options.getter(key, options.source);
} else {
value = options.source?.[key];
}

if (value) {
targetObject[key] = value;
}
}
}

return targetObject;
}

export { DATA_KEYS as augmentKeys, augmentFunction, augmentObject };
12 changes: 12 additions & 0 deletions src/UserConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import EleventyCompatibility from "./Util/Compatibility.js";
import EleventyBaseError from "./Errors/EleventyBaseError.js";
import BenchmarkManager from "./Benchmark/BenchmarkManager.js";
import JavaScriptFrontMatter from "./Engines/FrontMatter/JavaScript.js";
import { augmentFunction } from "./Engines/Util/ContextAugmenter.js";

const debug = debugUtil("Eleventy:UserConfig");

Expand Down Expand Up @@ -1018,6 +1019,17 @@ class UserConfig {
this.collections[name] = callback;
}

augmentFunctionContext(fn, options) {
let t = typeof fn;
if (t !== "function") {
throw new UserConfigError(
"Invalid type passed to `augmentFunctionContext`—function was expected and received: " + t,
);
}

return augmentFunction(fn, options);
}

getMergingConfigObject() {
let obj = {
// filters removed in 1.0 (use addTransform instead)
Expand Down
89 changes: 89 additions & 0 deletions test/EleventyTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { rimrafSync } from "rimraf";
import { z } from "zod";
import { fromZodError } from "zod-validation-error";
import { marked } from "marked";
import nunjucks from "nunjucks";

import eventBus from "../src/EventBus.js";
import Eleventy, { HtmlBasePlugin } from "../src/Eleventy.js";
Expand Down Expand Up @@ -1409,3 +1410,91 @@ test("Eleventy data schema has access to custom collections created via API #613
t.truthy(home);
t.is(home.content, "test");
});

test("Custom Nunjucks syntax has shortcode with access to `this`, Issue #3310", async (t) => {
let elev = new Eleventy("./test/stubs-virtual/", undefined, {
config: eleventyConfig => {
eleventyConfig.addShortcode("customized", function(argString) {
return `${this.page.url}:${argString}:Custom Shortcode`;
});

let njkEnv = new nunjucks.Environment();

function CustomExtension() {
this.tags = ['customized'];

this.parse = function(parser, nodes, lexer) {
let args;
let tok = parser.nextToken();
args = parser.parseSignature(true, true);
parser.advanceAfterBlockEnd(tok.value);
return new nodes.CallExtension(this, "run", args);
};

this.run = function(context, argString) {
let fn = eleventyConfig.augmentFunctionContext(
eleventyConfig.getShortcode("customized"),
{
source: context.ctx,
// lazy: false,
// getter: (key, context) => context?.[key];
// overwrite: true,
}
);

return fn(argString);
};
}

njkEnv.addExtension('CustomExtension', new CustomExtension());

eleventyConfig.addTemplateFormats("njknew");

eleventyConfig.addExtension("njknew", {
compile: (str, inputPath) => {
let tmpl = new nunjucks.Template(str, njkEnv, inputPath, false);
return function(data) {
return new Promise(function (resolve, reject) {
tmpl.render(data, function (err, res) {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
}
}
});

eleventyConfig.addTemplate("template.njknew", `<h1>{{ hello }}:{% customized "passed in" %}</h1>`, {
hello: "goodbye"
});
}
});

let results = await elev.toJSON();
t.is(results.length, 1);
t.is(results[0].content.trim(), `<h1>goodbye:/template/:passed in:Custom Shortcode</h1>`);
});

test("Related to issue 3206: Does Nunjucks throwOnUndefined variables require normalizeContext to be a lazy get", async (t) => {
let elev = new Eleventy("./test/stubs-virtual/", undefined, {
config: eleventyConfig => {
eleventyConfig.addShortcode("customized", function(argString) {
return `${this.page.url}:Custom Shortcode`;
});

eleventyConfig.setNunjucksEnvironmentOptions({
throwOnUndefined: true,
});

eleventyConfig.addTemplate("index.html", `HELLO{% customized %}:{{ page.url }}`);
}
});


let results = await elev.toJSON();
t.is(results.length, 1);
t.is(results[0].content.trim(), `HELLO/:Custom Shortcode:/`);
});

0 comments on commit 26d26b7

Please sign in to comment.