Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exec plugin #86

Closed
boehs opened this issue Nov 20, 2024 · 7 comments
Closed

exec plugin #86

boehs opened this issue Nov 20, 2024 · 7 comments

Comments

@boehs
Copy link

boehs commented Nov 20, 2024

Hi, another odd request, sorry 😅. I have the following code:

{{for name,content of metaTags }}
    <meta name="{{name}}" content="{{content |> escape}}">
{{ /for }}

where metaTags is defined like

{
    "viewport": "width=device-width,initial-scale=1",
    "theme-color": "{{color}}",
    "author": "{{config.author.name}}",
    "generator": "Eleventy"
}

Looking at the plugins, it would seem as if this could easily be another small plugin? something like {{ exec "{{ blah blah }}" }}

I'm trying to figure out the plugin development process for this

@boehs
Copy link
Author

boehs commented Nov 20, 2024

I'm starting to think this will be problematic. As I understand it, vento runs in two stages

  1. Template built into javascript
  2. Template executed with a given context

If you have code like

{{for name,content of metaTags }}
    <meta name="{{name}}" content="{{content |> escape}}">
{{ /for }}

You don't know what content is until you're in the execution stage. Because you don't know what content is until you're executing, you don't know how to compile whatever the value of content may be. If you want to do that compilation during execution, you would need to embed all of vento inside the vento output

Edit: or maybe that's not fully true? Kinda mindbending stuff

@oscarotero
Copy link
Collaborator

Yes, that's true.
Vento compiles your template:

{{for name,content of metaTags }}
    <meta name="{{name}}" content="{{content |> escape}}">
{{ /for }}

to something like this:

for (let [name, content] of __env.utils.toIterator(metaTags)) {
  __exports.content += `    <meta name="`;
  __exports.content += name;
  __exports.content += `" content="`;
  __exports.content += __env.filters.escape(content);
  __exports.content += `">`;
}

Both __env.utils.toIterator and __env.filters.escape are executed at runtime and they are stored in the __env variable (the environment instance of Vento).

I don't understand the plugin {{ exec "{{ blah blah }}" }}. What is this suposed to do?

@boehs
Copy link
Author

boehs commented Nov 20, 2024

The point is that I have a metaTags json file for easy configuration, and template strings can be defined in that JSON file (eg. "author": "{{config.author.name}}") Where I am now is I'm patching the vento eleventy library to include the entire context

diff --git a/dist/index.js b/dist/index.js
index b1f84226292a45d5e2df6e17fb16528b0d0df641..40cd9673090792c25914b267cf7d2ebca5503788 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -84,9 +84,7 @@ function createVentoInterface(options) {
     if (env.utils._11tyCtx?.page?.inputPath === newContext?.page?.inputPath) {
       return;
     }
-    for (const K of CONTEXT_DATA_KEYS) {
-      env.utils._11tyCtx[K] = newContext[K];
-    }
+    env.utils._11tyCtx = newContext;
     DEBUG.setup("Reload context, new context is: %o", env.utils._11tyCtx);
   }
   function loadPlugins(plugins) {

I then do this in eleventy

import { VentoPlugin } from "eleventy-plugin-vento";
import vento from "ventojs";

export default function ventoCfg(eleventyConfig) {
    const env = vento({
        autoDataVarname: true,
        autoescape: true,
    });
    eleventyConfig.addShortcode("interpolate", function (str) {
        return env.runStringSync(str, this).content;
    });
    eleventyConfig.addPlugin(VentoPlugin, {});
}

and the code above works. With the realization I made above, I'm not sure if there's anything that can be done on your end (I've opened noelforte/eleventy-plugin-vento#72 to remove the need for this patch), but certainly as it stands this code is ugly and probably slow and would be better suited for the library to handle, if technically feasible

@oscarotero
Copy link
Collaborator

Ah, got it.

I guess this is possible but possibly slow because Vento has to compile and run every item in the loop. I'm not sure but I think Eleventy has an option to compute these values automatically.

@boehs
Copy link
Author

boehs commented Nov 20, 2024

Yeah, there should be (practically, 11ty's implementation is also... flawed). There are workarounds, though the question still stands on if there is any way Vento can provide something analogous to eval('') in javascript, for any other usecase that may demand it. The conclusion "we don't want to support this due to (complexity|no good way to implement|...)" is perfectly satisfactory if that's where you're at, I just wanted to raise the question

@oscarotero
Copy link
Collaborator

I think it could be a perfect use case for a filter. Something like

{{for name,content of metaTags }}
    <meta name="{{name}}" content="{{content |> vento |> escape}}">
{{ /for }}

The vento filter could be implemented as this:

env.filters.vento = async function (code, data = {}) {
  return await env.runString(code, data).content;
};

For performance, we can generate a fake path hashing the code, if the same code is executed twice, it's only compiled the first time:

env.filters.vento = async function (code, data = {}) {
  const file = `memory:${hash(code).vto`; // hash() function to be implemented
  return await env.runString(code, data, file).content;
};

@boehs
Copy link
Author

boehs commented Nov 20, 2024

Ah brilliant! This is what I wanted!

async function hash(message, algo = "SHA-1") {
  return Array.from(
    new Uint8Array(
      await crypto.subtle.digest(algo, new TextEncoder().encode(message)),
    ),
    (byte) => byte.toString(16).padStart(2, "0"),
  ).join("");
}

eleventyConfig.addPlugin(VentoPlugin, {
  plugins: [
    (env) => {
      env.filters.exec = async function (code) {
        const file = `memory:${await hash(code)}.vto`;
        let res = (await env.runString(code, this.data, file))
            .content;
        return res;
      };
    },
  ],
});

just a couple small changes (this.data instead of the data arg, await content properly, hash impl provided)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants