Skip to content

Commit

Permalink
Fixes #481
Browse files Browse the repository at this point in the history
  • Loading branch information
zachleat committed Feb 19, 2020
1 parent 99396db commit d8f941e
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 21 deletions.
3 changes: 2 additions & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ module.exports = function(config) {
layout: "layout",
permalink: "permalink",
permalinkRoot: "permalinkBypassOutputDir",
engineOverride: "templateEngineOverride"
engineOverride: "templateEngineOverride",
computed: "eleventyComputed"
},
dir: {
input: ".",
Expand Down
98 changes: 98 additions & 0 deletions src/ComputedData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
const lodashGet = require("lodash/get");
const lodashSet = require("lodash/set");
const DependencyGraph = require("dependency-graph").DepGraph;

class ComputedData {
constructor() {
this.computed = {};
this.computedKeys = new Set();
this.declaredDependencies = {};

// is this ¯\_(lisp)_/¯
// must be strings that won’t be escaped by template languages
this.prefix = "(((((hi(((((";
this.suffix = ")))))hi)))))";
}

add(key, fn, declaredDependencies = []) {
this.computedKeys.add(key);
this.declaredDependencies[key] = declaredDependencies;
lodashSet(this.computed, key, fn);
}

getProxyData(data) {
let proxyData = {};

// use these special strings as a workaround to check the rendered output
// can’t use proxies here as some template languages trigger proxy for all
// keys in data
for (let key of this.computedKeys) {
// TODO don’t allow to set eleventyComputed.page? other disallowed computed things?
lodashSet(proxyData, key, this.prefix + key + this.suffix);
}

return proxyData;
}

findVarsInOutput(output = "") {
let vars = new Set();
let splits = output.split(this.prefix);
for (let split of splits) {
let varName = split.substr(0, split.indexOf(this.suffix));
if (varName) {
vars.add(varName);
}
}
return Array.from(vars);
}

async getVarOrder(data) {
if (this.computedKeys.size > 0) {
let graph = new DependencyGraph();

let proxyData = this.getProxyData(data);

for (let key of this.computedKeys) {
let computed = lodashGet(this.computed, key);
if (typeof computed === "function") {
graph.addNode(key);
if (this.declaredDependencies[key].length) {
for (let dep of this.declaredDependencies[key]) {
graph.addNode(dep);
graph.addDependency(key, dep);
}
}

let output = await computed(proxyData);

let vars = this.findVarsInOutput(output);
for (let usesVar of vars) {
if (usesVar !== key && this.computedKeys.has(usesVar)) {
graph.addNode(usesVar);
graph.addDependency(key, usesVar);
}
}
}
}

return graph.overallOrder();
}

return [];
}

async setupData(data) {
let order = await this.getVarOrder(data);
for (let key of order) {
let computed = lodashGet(this.computed, key);

if (typeof computed === "function") {
lodashSet(data, key, await computed(data));
} else if (computed) {
lodashSet(data, key, computed);
}
}
}
}

module.exports = ComputedData;
81 changes: 62 additions & 19 deletions src/Template.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const TemplatePermalink = require("./TemplatePermalink");
const TemplatePermalinkNoWrite = require("./TemplatePermalinkNoWrite");
const TemplateLayout = require("./TemplateLayout");
const TemplateFileSlug = require("./TemplateFileSlug");
const ComputedData = require("./ComputedData");
const Pagination = require("./Plugins/Pagination");
const TemplateContentPrematureUseError = require("./Errors/TemplateContentPrematureUseError");
const debug = require("debug")("Eleventy:Template");
Expand Down Expand Up @@ -364,20 +365,71 @@ class Template extends TemplateContent {
return str;
}

_addComputedEntry(computedData, obj, parentKey, declaredDependencies) {
// this check must come before lodashIsObject
if (typeof obj === "function") {
computedData.add(parentKey, obj, declaredDependencies);
} else if (lodashIsObject(obj)) {
for (let key in obj) {
let keys = [];
if (parentKey) {
keys.push(parentKey);
}
keys.push(key);
this._addComputedEntry(
computedData,
obj[key],
keys.join("."),
declaredDependencies
);
}
} else if (typeof obj === "string") {
computedData.add(
parentKey,
async innerData => {
return await super.render(obj, innerData, true);
},
declaredDependencies
);
}
}

async augmentFinalData(data) {
// will _not_ consume renderData
let computedData = new ComputedData();
// this allows computed entries to use page.url or page.outputPath and they’ll be resolved properly
this._addComputedEntry(
computedData,
{
page: {
url: async data => await this.getOutputHref(data),
outputPath: async data => await this.getOutputPath(data)
}
},
null,
["permalink"]
); // declared dependency

if (this.config.keys.computed in data) {
this._addComputedEntry(computedData, data[this.config.keys.computed]);
}
await computedData.setupData(data);

// deprecated, use eleventyComputed instead.
if ("renderData" in data) {
data.renderData = await this.mapDataAsRenderedTemplates(
data.renderData,
data
);
}
}

async getTemplates(data) {
// TODO cache this
let results = [];

if (!Pagination.hasPagination(data)) {
data.page.url = await this.getOutputHref(data);
data.page.outputPath = await this.getOutputPath(data);

if ("renderData" in data) {
data.renderData = await this.mapDataAsRenderedTemplates(
data.renderData,
data
);
}
await this.augmentFinalData(data);

results.push({
template: this,
Expand Down Expand Up @@ -409,23 +461,14 @@ class Template extends TemplateContent {
let templates = await this.paging.getPageTemplates();
let pageNumber = 0;
for (let page of templates) {
// TODO try to reuse data instead of a new copy
let pageData = Object.assign({}, await page.getData());

// Issue #115
if (data.collections) {
pageData.collections = data.collections;
}

pageData.page.url = await page.getOutputHref(pageData);
pageData.page.outputPath = await page.getOutputPath(pageData);

if ("renderData" in pageData) {
pageData.renderData = await page.mapDataAsRenderedTemplates(
pageData.renderData,
pageData
);
}
await page.augmentFinalData(pageData);

results.push({
template: page,
Expand Down
1 change: 0 additions & 1 deletion src/TemplateMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ class DuplicatePermalinkOutputError extends EleventyBaseError {
class TemplateMap {
constructor() {
this.map = [];
this.graph = new DependencyGraph();
this.collectionsData = null;
this.cached = false;
this.configCollections = null;
Expand Down
123 changes: 123 additions & 0 deletions test/ComputedDataTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import test from "ava";
import ComputedData from "../src/ComputedData";

test("Get fake proxy data", t => {
let cd = new ComputedData();
cd.add("key1", () => {});
cd.add("key2", () => {});
t.deepEqual(cd.getProxyData({}), {
key1: `${cd.prefix}key1${cd.suffix}`,
key2: `${cd.prefix}key2${cd.suffix}`
});
});

test("Get nested fake proxy data", t => {
let cd = new ComputedData();
cd.add("key1.nested", () => {});
cd.add("key2", () => {});
t.deepEqual(cd.getProxyData({}), {
key1: {
nested: `${cd.prefix}key1.nested${cd.suffix}`
},
key2: `${cd.prefix}key2${cd.suffix}`
});
});

test("Get vars from output", t => {
let cd = new ComputedData();
t.deepEqual(cd.findVarsInOutput(""), []);
t.deepEqual(cd.findVarsInOutput("slkdjfkljdsf"), []);
t.deepEqual(
cd.findVarsInOutput(`slkdjfkljdsf${cd.prefix}${cd.suffix}sldkjflkds`),
[]
);
t.deepEqual(
cd.findVarsInOutput(
`slkdjfkljdsf${cd.prefix}firstVar${cd.suffix}sldkjflkds`
),
["firstVar"]
);
t.deepEqual(
cd.findVarsInOutput(
`slkdjfkljdsf${cd.prefix}firstVar${cd.suffix}test${cd.prefix}firstVar${cd.suffix}sldkjflkds`
),
["firstVar"]
);
t.deepEqual(
cd.findVarsInOutput(
`slkdjfkljdsf${cd.prefix}firstVar${cd.suffix}test${cd.prefix}secondVar${cd.suffix}sldkjflkds`
),
["firstVar", "secondVar"]
);
});

test("Basic get/set", async t => {
let cd = new ComputedData();

cd.add("keystr", `this is a str`);
cd.add("key1", data => {
return `this is a test ${data.key2}${data.keystr}`;
});

let data = {
key2: "inject me"
};
await cd.setupData(data);

t.is(data.key1, "this is a test inject methis is a str");
t.is(data.key2, "inject me");
t.is(data.keystr, "this is a str");
});

test("use a computed value in another computed", async t => {
let cd = new ComputedData();
cd.add("keyComputed", data => {
return `this is a test ${data.keyOriginal}`;
});
cd.add("keyComputed2nd", data => {
return `using computed ${data.keyComputed}`;
});

let data = {
keyOriginal: "inject me"
};
await cd.setupData(data);

t.is(data.keyComputed2nd, "using computed this is a test inject me");
});

test("use a computed value in another computed (out of order)", async t => {
let cd = new ComputedData();
cd.add("keyComputed2nd", data => {
return `using computed ${data.keyComputed}`;
});
cd.add("keyComputed", data => {
return `this is a test ${data.keyOriginal}`;
});

let data = {
keyOriginal: "inject me"
};
await cd.setupData(data);

t.is(data.keyComputed2nd, "using computed this is a test inject me");
});

test("use a computed value in another computed (out of order), async callbacks", async t => {
let cd = new ComputedData();
cd.add("keyComputed2nd", async data => {
// await in data.keyComputed is optional 👀
return `using computed ${data.keyComputed}`;
});
cd.add("keyComputed", async data => {
// await in data.keyOriginal is optional 👀
return `this is a test ${await data.keyOriginal}`;
});

let data = {
keyOriginal: "inject me"
};
await cd.setupData(data);

t.is(data.keyComputed2nd, "using computed this is a test inject me");
});
37 changes: 37 additions & 0 deletions test/TemplateTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2022,3 +2022,40 @@ test("Get Layout Chain", async t => {
"./test/stubs-incremental/layout-chain/_includes/parent.njk"
]);
});

test("eleventyComputed", async t => {
let tmpl = new Template(
"./test/stubs/eleventyComputed/first.njk",
"./test/stubs/",
"./dist"
);
let data = await getRenderedData(tmpl);
t.is((await tmpl.render(data)).trim(), "hi:value2-value1.css");
});

test("eleventyComputed permalink", async t => {
let tmpl = new Template(
"./test/stubs/eleventyComputed/permalink.njk",
"./test/stubs/",
"./dist"
);
let templates = await tmpl.getTemplates(await tmpl.getData());
let data = templates[0].data;
t.is(data.page.url, "/haha-value1.html");
t.is(data.page.outputPath, "./dist/haha-value1.html");
t.is(data.permalink, "haha-value1.html");
t.is(data.nested.key3, "value1");
t.is(data.nested.key4, "depends on computed value1");
t.is(data.dependsOnPage, "depends:/haha-value1.html");
});

test("eleventyComputed js front matter (function)", async t => {
let tmpl = new Template(
"./test/stubs/eleventyComputed/second.njk",
"./test/stubs/",
"./dist"
);
let data = await getRenderedData(tmpl);
t.is(data.key3, "value3-value2-value1.css");
t.is((await tmpl.render(data)).trim(), "hi:value2-value1.css");
});
Loading

0 comments on commit d8f941e

Please sign in to comment.