harttle / liquidjs

A simple, expressive, safe and Shopify compatible template engine in pure JavaScript.
https://liquidjs.com
MIT License
1.52k stars 238 forks source link

Allow passing arbitrary JavaScript objects as context, or introduce a separate hash for contextual data #466

Closed Nowaker closed 2 years ago

Nowaker commented 2 years ago

Basic example:

const fs = require('fs')
liquidjs.parseAndRender(layoutContent, {key: 'val', __fs: fs}) // option 1 - part of regular context
liquidjs.parseAndRender(layoutContent, {key: 'val'}, {fs}) // option 2 - third parameter for context data not exposed as {{ variables }}, accessible programatically in tags/filters only

Currently: Error: Cannot convert object to primitive value

Reasoning: I'd like to pass some contextual data to every parseAndRender call that I would access in tag/filter handlers.

Sophisticated example in Gatsby:

  engine.registerTag('asset', {
    // ...
    render: async function (context, emitter, scope, jsContext) { // jsContext, if option 2 is picked
      const assetNode = {
        children: [],
        id: gatsbyApi.createNodeId(`asset-${this.input}`),
        internal: {
          type: 'JekyllAsset',
          contentDigest: gatsbyApi.createContentDigest(this.input),
        },
        outputPath: this.output,
        originalPath: this.originalPath,
      }

      actions.createNode(assetNode)
      actions.createParentChildLink({
        parent: context.getAll().__node, // or jsContext, if option 2 is picked
        child: assetNode
      })
    }
  }

// Then use it in createPages hook:
liquidjs.parseAndRender(template, {page, site, node})

This allows me to perform sophisticated actions inside tags/filters - not just to return a value.

harttle commented 2 years ago

Why not just expose it as ordinary scope variables? Scope is designed to carry variables for filter use.

If you need to expose it to all filters without explicitly pass to them, there's a globals option:

liquidjs.parseAndRender(tpl, scope, {globals})
Nowaker commented 2 years ago

Ok, I'll check that, but the way I understand globals, it's no different than passing it in scope, which requires primitive values only.

harttle commented 2 years ago

Sorry I missed this error, I suppose it's not expected. Do you know where it's from?

Error: Cannot convert object to primitive value

harttle commented 2 years ago

After digging into this a bit, I find the "cannot convert object to primitive value" happens when implicitly converting objects without toString method to string. Like

Object.create(null) + '' 

I checked the codebase and didn't find similiar expressions. I tried the following code which didn't throw such an error:

const fs = require('fs')
const { Liquid } = require('.')
const engine = new Liquid();

// [object Object]
console.log(engine.parseAndRenderSync('{{fs}}', {fs}))
// [object Object]foo
console.log(engine.parseAndRenderSync('{{fs | append: "foo"}}', {fs}))

Closing this issue since LiquidJS actually allows arbitrary JavaScript objects as contexts. Feel free to open another issue with a runnable snippet if somewhere in LiquidJS throws "Error: Cannot convert object to primitive value".

Nowaker commented 2 years ago

Ah, sorry, I forgot to close this. Yeah, it came from somewhere else, not from Liquidjs.