Skip to content

Commit

Permalink
feat(eleventy): provide the url filter in the visual editor
Browse files Browse the repository at this point in the history
`{{ "/url" | url }}` will now work in the visual editor,
provided you pass a `pathPrefix` option to the `pluginBookshop()`
function inside your `.eleventy.js`.
  • Loading branch information
bglw committed Mar 14, 2022
1 parent 3429542 commit bd44ae0
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 38 deletions.
18 changes: 11 additions & 7 deletions guides/eleventy.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,19 @@ TIP: If you specify multiple paths, the components will be merged.

TIP: Paths that don't exist will be skipped. If you specify local and production paths, the one that exists will be used.

NOTE: The pathPrefix provided must match the pathPrefix configured in your `.eleventy.js`. If you aren't using the `url` filter anywhere, this option can be omitted.

.*.eleventy.js*
```javascript
const pluginBookshop = require("@bookshop/eleventy-bookshop");

module.exports = function (eleventyConfig) {
// ...

eleventyConfig.addPlugin(pluginBookshop({
bookshopLocations: ["component-library"]
}));
eleventyConfig.addPlugin(pluginBookshop({
bookshopLocations: ["component-library"],
pathPrefix: '',
}));

// ...
};
Expand Down Expand Up @@ -190,10 +193,11 @@ const pluginCloudCannonBookshop = require("@bookshop/cloudcannon-eleventy-booksh
module.exports = function (eleventyConfig) {
// ...

eleventyConfig.addPlugin(pluginBookshop({
bookshopLocations: ["component-library"]
}));
eleventyConfig.addPlugin(pluginCloudCannonBookshop);
eleventyConfig.addPlugin(pluginBookshop({
bookshopLocations: ["component-library"],
pathPrefix: '',
}));
eleventyConfig.addPlugin(pluginCloudCannonBookshop);

// ...
};
Expand Down
15 changes: 12 additions & 3 deletions javascript-modules/engines/eleventy-engine/lib/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import translateLiquid from './translateLiquid.js';
import unbind from './plugins/unbind.js';
import slug from './plugins/slug-plugin.js';
import loopContext from './plugins/loop_context.js';
import urlFilterBuilder from './plugins/url.js';

export class Engine {
constructor(options) {
Expand All @@ -22,6 +23,9 @@ export class Engine {
this.plugins = options.plugins || [];
this.plugins.push(unbind, slug, loopContext);

this.meta = {};
this.plugins.push(urlFilterBuilder(this.meta));

this.initializeLiquid();
this.applyLiquidPlugins();
}
Expand Down Expand Up @@ -97,15 +101,19 @@ export class Engine {
return false;
}

injectInfo(props, info = {}) {
injectInfo(props, info = {}, meta = {}) {
return {
...(info.collections || {}),
...(info.data || {}),
...props,
};
}

async render(target, name, props, globals, cloudcannonInfo) {
async updateMeta(meta) {
this.meta.pathPrefix = meta.pathPrefix ? await this.eval(meta.pathPrefix) : undefined;
}

async render(target, name, props, globals, cloudcannonInfo, meta) {
let source = this.getComponent(name);
// TODO: Remove the below check and update the live comments to denote shared
if (!source) source = this.getShared(name);
Expand All @@ -115,7 +123,8 @@ export class Engine {
}
source = translateLiquid(source);
if (!globals || typeof globals !== "object") globals = {};
props = this.injectInfo({ ...globals, ...props }, cloudcannonInfo);
props = this.injectInfo({ ...globals, ...props }, cloudcannonInfo, meta);
await this.updateMeta(meta);
target.innerHTML = await this.liquid.parseAndRender(source || "", props);
}

Expand Down
27 changes: 27 additions & 0 deletions javascript-modules/engines/eleventy-engine/lib/plugins/url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export default (meta) => function (Liquid) {
this.registerFilter('url', (url) => {
url = url || "";

// Intentionally less-accurate than 11ty implementation. Works in most cases.
if (url.startsWith('/') && !url.startsWith('//')) {
if (meta.pathPrefix === undefined || typeof meta.pathPrefix !== "string") {
// When you retrieve this with config.getFilter("url") it
// grabs the pathPrefix argument from your config for you.
console.error([
`The Eleventy Bookshop plugin needs to be supplied a pathPrefix in order to use the url filter.`,
`e.g. in .eleventy.js:`,
``,
`eleventyConfig.addPlugin(pluginBookshop({`,
` bookshopLocations: <. . .>,`,
` pathPrefix: "/documentation/"`,
` }));`,
].join('\n'))
throw new Error("pathPrefix (String) is required in the `url` filter. This should be supplied ");
}

const baseurl = meta.pathPrefix || '';
return `${baseurl}${url}`.replace(/\/\//g, '/');
}
return url;
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const contextHunt = (ctx, hash, index) => {
return "UNKNOWN";
}

module.exports = (tagType, locations, baseLocation) => (liquidEngine) => {
module.exports = (tagType, locations, baseLocation, bookshopConfig) => (liquidEngine) => {
return {
parse: function (tagToken, remainingTokens) {
const [, component, args] = tagToken.args.match(/^['"]?([^\s'"]+)['"]?(?:[\r\n\s]+([\s\S]*))?$/);
Expand Down Expand Up @@ -84,7 +84,9 @@ module.exports = (tagType, locations, baseLocation) => (liquidEngine) => {
process.exit(1);
}

let componentScope = {};
let componentScope = {
__bookshop__nested: true
};
// Support the bookshop bind property
const tokenizer = new Tokenizer(this.args, {})
for (const hash of tokenizer.readHashes()) {
Expand All @@ -99,7 +101,12 @@ module.exports = (tagType, locations, baseLocation) => (liquidEngine) => {
const output = await liquidEngine.parseAndRender(convertedBookshopTag, ctx.getAll());

ctx.pop();
return `${preComment}${output}${postComment}`;

let metaComment = "";
if (!ctx.getAll()["__bookshop__nested"]) {
metaComment = `<!--bookshop-live meta(${bookshopConfig.pathPrefix ? `pathPrefix: "${bookshopConfig.pathPrefix}"` : ''}) -->\n`;
}
return `${metaComment}${preComment}${output}${postComment}`;
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ module.exports = (bookshopConfig) => {
? require('./lib/eleventy-one-bookshop.js')
: require('./lib/eleventy-zero-bookshop.js');
eleventyConfig.bookshopOptions = { locations, baseLocation };
eleventyConfig.addLiquidTag("bookshop", bookshopTag('component', locations, baseLocation));
eleventyConfig.addLiquidTag("bookshop_include", bookshopTag('include', locations, baseLocation));
eleventyConfig.addLiquidTag("bookshop", bookshopTag('component', locations, baseLocation, bookshopConfig));
eleventyConfig.addLiquidTag("bookshop_include", bookshopTag('include', locations, baseLocation, bookshopConfig));
eleventyConfig.addLiquidTag("bookshop_browser", browserTagHandler);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,27 @@ Feature: Eleventy Bookshop CloudCannon Integration
And site/_site/_cloudcannon/bookshop-site-data.json should contain the text "Zuchinni"

Scenario: Bookshop Live schema comments
Given a site/.eleventy.js file containing:
"""
const pluginBookshop = require("@bookshop/eleventy-bookshop");
module.exports = function (eleventyConfig) {
eleventyConfig.addPlugin(pluginBookshop({
bookshopLocations: ["../component-lib"],
pathPrefix: "/documentation/"
}));
return {
pathPrefix: "/documentation/"
}
};
"""
Given a component-lib/components/page/page.eleventy.liquid file containing:
"""
{% for block in content_blocks %}
{% assign b = block %}
<p>{{ b._bookshop_name }}</p>
{% assign b = block %}
<p>{{ b._bookshop_name }}</p>
{% bookshop "single" _bookshop_name: "inner" %}
{% endfor %}
"""
Given a component-lib/components/single/single.eleventy.liquid file containing:
Expand All @@ -57,15 +73,21 @@ Feature: Eleventy Bookshop CloudCannon Integration
---
{% bookshop "page" content_blocks: content_blocks %}
{% for block in content_blocks %}
{% bookshop "single" bind: block %}
{% bookshop "single" bind: block %}
{% endfor %}
"""
When I run "npm start" in the site directory
Then stderr should be empty
And stdout should not be empty
And site/_site/index.html should contain each row:
| text |
| <p>fake</p> |
| <!--bookshop-live name(page) params(content_blocks: content_blocks) context() --> |
| <span>fake</span> |
| <!--bookshop-live name(single) params(bind: block) context(block: content_blocks[0]) --> |
And site/_site/index.html should contain the text:
"""
<!--bookshop-live meta(pathPrefix: "/documentation/") -->
<!--bookshop-live name(page) params(content_blocks: content_blocks) context() -->
<p>fake</p>
<!--bookshop-live name(single) params(_bookshop_name: "inner") context(block: content_blocks[0]) --><span>inner</span><!--bookshop-live end-->
<!--bookshop-live end-->
<!--bookshop-live meta(pathPrefix: "/documentation/") -->
<!--bookshop-live name(single) params(bind: block) context(block: content_blocks[0]) --><span>fake</span><!--bookshop-live end-->
"""
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ Feature: Eleventy Bookshop CloudCannon Live Editing Site Data
layouts/
default.liquid from starters/eleventy/default.liquid
"""
* [front_matter]:
"""
layout: layouts/default.liquid
show: false
"""
* a site/index.html file containing:
"""
---
[front_matter]
---
{% bookshop "block" show: show %}
"""

Scenario: Bookshop live renders website data
Given a site/cloudcannon.config.yml file containing:
Expand All @@ -29,21 +41,52 @@ Feature: Eleventy Bookshop CloudCannon Live Editing Site Data
"name": "Cheeka"
}
"""
* a component-lib/components/cat/cat.eleventy.liquid file containing:
* a component-lib/components/block/block.eleventy.liquid file containing:
"""
<h1>{% if show %}{{ cat.name }}{% endif %}</h1>
"""
* [front_matter]:
* 🌐 I have loaded my site in CloudCannon
When 🌐 CloudCannon pushes new yaml:
"""
layout: layouts/default.liquid
show: false
show: true
"""
* a site/index.html file containing:
Then 🌐 There should be no errors
* 🌐 There should be no logs
* 🌐 The selector h1 should contain "Cheeka"

Scenario: Bookshop live renders special website config
Given a site/.eleventy.js file containing:
"""
---
[front_matter]
---
{% bookshop "cat" show: show %}
const pluginBookshop = require("@bookshop/eleventy-bookshop");
const pluginCloudCannon = require('eleventy-plugin-cloudcannon');
module.exports = function (eleventyConfig) {
eleventyConfig.setUseGitIgnore(false);
eleventyConfig.setDataDeepMerge(true);
eleventyConfig.addPlugin(pluginBookshop({
bookshopLocations: ["../component-lib"],
pathPrefix: "/documentation/"
}));
eleventyConfig.cloudcannonOptions = {
dir: {
pages: 'pages'
}
};
eleventyConfig.addPlugin(pluginCloudCannon);
return {
pathPrefix: "/documentation/"
}
};
"""
Given a component-lib/components/block/block.eleventy.liquid file containing:
"""
{% if show %}
<h1>{{ "/page/" | url }}</h1>
{% endif %}
"""
* 🌐 I have loaded my site in CloudCannon
When 🌐 CloudCannon pushes new yaml:
Expand All @@ -52,4 +95,4 @@ Feature: Eleventy Bookshop CloudCannon Live Editing Site Data
"""
Then 🌐 There should be no errors
* 🌐 There should be no logs
* 🌐 The selector h1 should contain "Cheeka"
* 🌐 The selector h1 should contain "/documentation/page/"
14 changes: 12 additions & 2 deletions javascript-modules/integration-tests/support/steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,15 +294,23 @@ Then(/^🌐 There should be a click listener on (\S+)$/i, { timeout: 60 * 1000 }
assert.equal(clicked, true, `Clicking the element did fire the expected handler. Expected window["${selector}:clicked"] to be true.`);
});

Then(/^🌐 The selector (\S+) should contain ['"](.+)['"]$/i, { timeout: 60 * 1000 }, async function (selector, contents) {
Then(/^🌐(debug)? The selector (\S+) should contain ['"](.+)['"]$/i, { timeout: 60 * 1000 }, async function (debug, selector, contents) {
if (!this.page) throw Error("No page open");
if (debug) {
const data = await this.page.evaluate(() => document.querySelector('body').outerHTML);
this.debugStep(data);
}
const innerText = await this.page.$eval(selector, (node) => node.innerText);
const contains = innerText.includes(unescape(contents));
assert.equal(innerText, contains ? innerText : `innerText containing \`${contents}\``);
});

Then(/^🌐 The selector (\S+) should match ['"](.+)['"]$/i, { timeout: 60 * 1000 }, async function (selector, contents) {
Then(/^🌐(debug)? The selector (\S+) should match ['"](.+)['"]$/i, { timeout: 60 * 1000 }, async function (debug, selector, contents) {
if (!this.page) throw Error("No page open");
if (debug) {
const data = await this.page.evaluate(() => document.querySelector('body').outerHTML);
this.debugStep(data);
}
const outerHTML = await this.page.$eval(selector, (node) => node.outerHTML);
const contains = outerHTML.includes(unescape(contents));
assert.equal(outerHTML, contains ? outerHTML : `outerHTML containing \`${contents}\``);
Expand Down Expand Up @@ -367,6 +375,8 @@ Given(/^🌐 I (?:have loaded|load) my site( in CloudCannon)?$/i, { timeout: 60

// @bookshop/generate
await this.runCommand(`npm start`, `.`);
assert.strictEqual(this.stderr, "");
assert.strictEqual(this.commandError, "");

// Open the site in a browser
switch (ssg) {
Expand Down
12 changes: 11 additions & 1 deletion javascript-modules/integration-tests/support/world.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,18 @@ class CustomWorld {
this.page_errors.push(e);
}

// If errors exist, returns the errors (and also the logs, to help with debugging)
puppeteerErrors() {
return this.page_errors;
if (this.page_errors.length) {
return [
"LOGS:",
...this.page_logs,
"ERRORS:",
...this.page_errors,
];
} else {
return this.page_errors;
}
}

trackPuppeteerLog(e) {
Expand Down
2 changes: 1 addition & 1 deletion javascript-modules/live/lib/app/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const evaluateTemplate = async (liveInstance, documentNode, parentPathStack, tem
const pathStack = parentPathStack || [{}]; // The paths from the root to any assigned variables
let stashedNodes = []; // bookshop_bindings tags that we should keep track of for the next component
let stashedParams = []; // Params from the bookshop_bindings tag that we should include in the next component tag
let meta = []; // Metadata set in the teplate
let meta = {}; // Metadata set in the teplate

const combinedScope = () => [liveInstance.data, ...stack.map(s => s.scope)];
const currentScope = () => stack[stack.length - 1];
Expand Down

0 comments on commit bd44ae0

Please sign in to comment.