Liquidjs

Blog officiel du projet Web Apps Conception

layout: post title: “Développer en LiquidJs” date: 2024-10-19 23:00:00 +0002 tags: node

Développer en Liquid Js

LiquidJS est un moteur de template initialement développé par le directeur général de Shopify, Tobias Lütke et écrit en Ruby. C’est un projet open-source disponible sur Github. La première version remonte à 2008. Le balisage Liquid est très similaire au HTML avec quelques ajouts.

📝 Quelques exemples d’utilisation : webapps-conception/liquidjs-example.

Le langage du template Liquid

LiquidJS est un moteur de modèles compatible Shopify / GitHub Pages simple, expressif et sûr en JavaScript pur. Le but de ce référentiel est de fournir une implémentation Liquid standard pour la communauté JavaScript. Liquid est à l’origine implémenté dans Ruby et utilisé par GitHub Pages, Jekyll et Shopify, voir Différences avec Shopify/liquid.

La syntaxe LiquidJS est relativement simple. Il existe 2 types de balises dans LiquidJS :

Live Demo : Before going into the details, here’s a live demo to play around: https://liquidjs.com/playground.html.

Outputs

Les « Outputs » sont utilisées pour générer des variables, qui peuvent être transformées par des filtres, en HTML. Le modèle suivant insérera la valeur de username dans la valeur de l’entrée :

<input type="text" name="user" value="{{username}}">

Les valeurs en sortie peuvent être transformées par des filtres avant la sortie. Pour ajouter une chaîne après la variable :

{{ username | append: ", welcome to LiquidJS!" }}

Les filtres peuvent être chaînés :

{{ username | append: ", welcome to LiquidJS!" | capitalize }}

Une liste complète des filtres pris en charge par LiquidJS peut être trouvée ici.

Tags

Les Tags sont utilisées pour contrôler le processus de rendu du modèle, la manipulation des variables du modèle, l’interopérabilité avec d’autres modèles, etc. Par exemple, assign peut être utilisé pour définir une variable qui peut être utilisée ultérieurement dans le modèle :

{% assign foo = "FOO" %}

Généralement, les balises apparaissent par paires avec une balise de début et une balise de fin correspondante. Par exemple:

{% if foo == "FOO" %}
    Variable `foo` equals "FOO"
{% else %}
    Variable `foo` not equals "FOO"
{% endif %}

Une liste complète des balises prises en charge par LiquidJS peut être trouvée ici.

Installation

LiquidJS dans Node.js

Installer via npm :

npm install --save liquidjs
var { Liquid } = require('liquidjs');
var engine = new Liquid();

engine
    .parseAndRender('{{name | capitalize}}', {name: 'alice'})
    .then(console.log);     // outputs 'Alice'

Démo de travail : Voici une démo fonctionnelle pour l’utilisation de LiquidJS dans Node.js : liquidjs/demo/nodejs/.

Les définitions de types pour LiquidJS sont également exportées et publiées, ce qui le rend plus agréable pour les projets TypeScript :

import { Liquid } from 'liquidjs';
const engine = new Liquid();

engine
    .parseAndRender('{{name | capitalize}}', {name: 'alice'})
    .then(console.log);     // outputs 'Alice'

Démo de travail : Voici une démo fonctionnelle pour l’utilisation de LiquidJS dans TypeScript : [liquidjs/demo/typescript/}(https://github.com/harttle/liquidjs/blob/master/demo/typescript/){:target=”_blank”}

LiquidJS dans les navigateurs

Des bundles UMD prédéfinis sont également disponibles :

<!--for production-->
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.min.js"></script>
<!--for development-->
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.umd.js"></script>

Démo de travail : Voici une démo vivante sur jsFiddle: jsfiddle.net/pd4jhzLs/1/, et le code source est également disponible en liquidjs/demo/browser/.

LiquidJS en CLI

LiquidJS peut également être utilisé pour restituer un modèle directement depuis la CLI en utilisant npx :

npx liquidjs --template '{{"hello" | capitalize}}'

Vous pouvez soit transmettre le modèle en ligne (comme indiqué ci-dessus), soit le lire à partir d’un fichier en utilisant le caractère @ suivi d’un chemin, comme ceci :

npx liquidjs --template @./some-template.liquid

Vous pouvez également utiliser la syntaxe @- pour lire le modèle depuis stdin :

echo '{{"hello" | capitalize}}' | npx liquidjs --template @-

Un contexte peut être transmis de la même manière (c’est-à-dire en ligne, à partir d’un chemin ou via stdin). Les trois suivants sont équivalents :

npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Snake"}'
npx liquidjs --template 'Hello, {{ name }}!' --context @./some-context.json
echo '{"name": "Snake"}' | npx liquidjs --template 'Hello, {{ name }}!' --context @-

Notez que vous ne pouvez utiliser stdin avec le spécificateur @- que pour un seul argument. Si vous essayez de l’utiliser à la fois pour --template et --context, vous obtiendrez une erreur.

La sortie rendue est écrite sur stdout par défaut, mais vous pouvez également spécifier un fichier de sortie (si le fichier existe, il sera écrasé) :

npx liquidjs --template '{{"hello" | capitalize}}' --output ./hello.txt

Vous pouvez également transmettre un certain nombre d’options pour personnaliser le comportement de rendu du modèle. Par exemple, l’option –js-truthy peut être utilisée pour activer la véracité de JavaScript :

npx liquidjs --template @./some-template.liquid --js-truthy

La plupart des options disponibles via l’API JavaScript sont également disponibles à partir de la CLI. Pour obtenir de l’aide sur les options disponibles, utilisez npx liquidjs --help.

Une démo ReactJS est également disponible sous liquidjs/demo/reactjs/.

Options

Le constructeur Liquid accepte un objet simple comme option pour définir le comportement de LiquidJS. Toutes ces options sont facultatives, nous pouvons donc en spécifier n’importe laquelle, par exemple l’option cache :

const { Liquid } = require('liquidjs')
const engine = new Liquid({
    cache: true
})

API Document : Voici un aperçu de toutes les options. Pour les types et signatures exacts, veuillez vous référer à LiquidOptions API.

Cache

Le cache est utilisé pour améliorer les performances en mettant en cache les structures de modèles précédemment analysées, en particulier dans les cas où nous analysons ou restituons des fichiers à plusieurs reprises.

La valeur par défaut est false. Lorsque la valeur est true, un cache LRU par défaut de taille 1024 sera activé. Et bien sûr, il peut s’agir d’un nombre qui indique la taille du cache souhaitée.

De plus, il peut également s’agir d’une implémentation de cache personnalisée. Voir Mise en cache pour plus de détails.

Partials/Layouts

root est utilisé pour spécifier les répertoires de modèles permettant à LiquidJS de rechercher et de lire les fichiers de modèles. Peut être une chaîne unique ou un tableau de chaînes. Voir Render Files pour plus de détails.

layouts est utilisé pour spécifier des répertoires de modèles pour que LiquidJS recherche des fichiers pour {% layout %}. Même format que root et sera par défaut root s’il n’est pas spécifié.

partials est utilisé pour spécifier des répertoires de modèles pour que LiquidJS recherche des fichiers pour {% render %} et {% include %}. Même format que root et sera par défaut root s’il n’est pas spécifié.

relativeReference est défini sur true par défaut pour autoriser les noms de fichiers relatifs. Notez que les fichiers relativement référencés doivent également se trouver dans la racine correspondante. Par exemple, vous pouvez référencer un autre fichier comme {% render ../foo/bar %} tant que ../foo/bar se trouve également dans le répertoire partials.

dynamicPartials

Note : pour des raisons historiques, il s’appelle DynamicPartials mais il fonctionne également pour les mises en page.

dynamicPartials indique s’il faut ou non traiter les arguments de nom de fichier dans les balises include, render et layout comme une variable. La valeur par défaut est true. Par exemple, le rendu de l’extrait suivant avec scope { file: 'foo.html' } inclura le foo.html :

{% include file %}

En définissant dynamicPartials: false, LiquidJS essaiera d’inclure le fichier nommé file, ce qui est étrange mais permet une syntaxe plus simple si vos relations de modèle sont statiques :

{% liquid foo.html %}

Piège courant : LiquidJS définit par défaut cette option sur true pour être compatible avec shopify/liquid, mais si vous venez de eleventy, elle est définie sur false par défaut (voir Quoted Include Paths) qui, je crois, essaie d’être compatible avec Jekyll.

Jekyll include

jekyllInclude est utilisé pour activer la syntaxe d’inclusion de type Jekyll-like. La valeur par défaut est false, lorsqu’elle est définie sur true :

Par exemple, dans le modèle suivant, name.html n’est pas cité, header et "HEADER" sont séparés par = et le paramètre header est référencé par include.header. Pour plus de détails, veuillez consulter include.

// entry template
{% include article.html header="HEADER" content="CONTENT" %}

// article.html
<article>
  <header>{{include.header}}</header>
  {{include.content}}
</article>

extname

extname définit le nom d’extension par défaut à ajouter aux noms de fichiers si le nom de fichier n’a pas de nom d’extension. La valeur par défaut est '', ce qui signifie qu’il est désactivé par défaut. En le réglant sur .liquid :

{% render "foo" %}  there's no extname, adds `.liquid` and loads foo.liquid
{% render "foo.html" %}  there is an extname already, loads foo.html directly

Legacy Versions : Avant la version 2.0.1, extname est défini par défaut sur .liquid. Pour changer cela, vous devez définir explicitement extname: ‘’`. Voir #41 pour plus de détails.

fs

fs est utilisé pour définir une implémentation de système de fichiers personnalisée qui sera utilisée par LiquidJS pour rechercher et lire les fichiers modèles. Voir Abstract File System pour plus de détails.

globals

globals est utilisé pour définir des variables globales disponibles pour tous les modèles, même en cas de render tag. Voir 3185 pour plus de détails.

jsTruthy

jsTruthy est utilisé pour utiliser la véracité JavaScript standard plutôt que Shopify.

la valeur par défaut est false. Par exemple, lorsqu’elle est définie sur true, une chaîne vide sera évaluée comme false avec jsTruthy. Avec la véracité de Shopify, une chaîne vide est true.

outputEscape

outputEscape peut être utilisé pour échapper automatiquement les chaînes de sortie. Il peut s’agir de "escape", "json" ou (val: inconnu) => string, la valeur par défaut est undefined.

Date

timezoneOffset est utilisé pour spécifier un fuseau horaire différent pour afficher les dates, votre fuseau horaire local sera utilisé s’il n’est pas spécifié. Par exemple, définir timezoneOffset: 0 pour afficher toutes les dates en UTC/GMT 00:00.

preserveTimezones est un booléen qui n’effectue que des horodatages littéraux. Lorsqu’ils sont définis sur true, tous les horodatages littéraux resteront les mêmes lors de la sortie. Il s’agit d’une option d’analyseur, donc les objets Date transmis à LiquidJS en tant que données ne seront pas affectés. Notez que preserveTimezones a une priorité plus élevée que timezoneOffset.

dateFormat est utilisé pour spécifier un format par défaut pour les dates de sortie. %A, %B %-e, %Y at %-l:%M %P %z sera utilisé s’il n’est pas spécifié. Par exemple, définissez dateFormat: %Y-%m-%dT%H:%M:%S:%LZ pour afficher toutes les dates dans le format JavaScript Date.toJson().

Trimming

Les options greedy, trimOutputLeft, trimOutputRight, trimTagLeft, trimTagRight sont utilisées pour éliminer les nouvelles lignes et les retraits supplémentaires dans les modèles autour des constructions liquides. Voir Whitespace Control pour plus de détails.

Delimiter

outputDelimiterLeft, outputDelimiterRight, tagDelimiterLeft, tagDelimiterRight sont utilisés pour personnaliser les délimiteurs pour LiquidJS Tags and Filters. Par exemple avec outputDelimiterLeft : <%=, outputDelimiterRight : %> nous pouvons éviter les conflits avec d’autres langages :

<%= username | append: ", welcome to LiquidJS!" %>

Strict

strictFilters est utilisé pour affirmer l’existence du filtre. S’il est défini sur false, les filtres non définis seront ignorés. Sinon, les filtres non définis provoqueront une exception d’analyse. La valeur par défaut est false.

strictVariables est utilisé pour affirmer l’existence d’une variable. Si la valeur est false, les variables non définies seront rendues sous forme de chaîne vide. Sinon, les variables non définies provoqueront une exception de rendu. La valeur par défaut est false.

lenientIf modifie le comportement de strictVariables pour permettre la gestion des variables facultatives. Si elle est définie sur true, une variable non définie ne provoquera pas d’exception dans les deux situations suivantes : a) c’est la condition d’une balise if, elsif ou unless ; b) cela se produit juste avant un filtre par default. Sans importance si strictVariables n’est pas défini. La valeur par défaut est false.

ownPropertyOnly masque les variables de portée des prototypes, ce qui est utile lorsque vous transmettez un objet non nettoyé dans LiquidJS ou que vous devez masquer les prototypes des modèles. La valeur par défaut est true.

Nonexistent Tags : Les balises inexistantes génèrent toujours des erreurs lors de l’analyse et ce comportement ne peut pas être personnalisé.

Parameter Order

Les ordres de paramètres sont ignorés par défaut, car ea {% for i in (1..8) reversed limit:3 %} exécutera toujours la limit avant reversed, même si reversed se produit avant la limit. Pour que l’ordre des paramètres soit respecté, définissez orderedFilterParameters sur true. Sa valeur par défaut est false.

Render Files

Pour un projet typique, il peut y avoir un répertoire de fichiers modèles, vous devrez définir le template root et appeler renderFile ou renderFileSync pour restituer un fichier spécifique.

Render a File

Par exemple, vous disposez d’un répertoire de modèles comme celui-ci :

.
├── index.js
└── views/
  ├── hello.liquid
  └── world.liquid

hello.liquid contient une seule ligne name : {{name}}.

Enregistrez maintenant le contenu suivant dans index.js :

var engine = new Liquid({
    root: path.resolve(__dirname, 'views/'),  // root for layouts/includes lookup
    extname: '.liquid'          // used for layouts/includes, defaults ""
});
engine
    .renderFile("hello", {name: 'alice'})   // will read and render `views/hello.liquid`
    .then(console.log)  // outputs "Alice"

Exécutez node index.js et vous obtiendrez un résultat comme ceci :

> node index.js
name: alice

Template Lookup

Noms des fichiers modèles transmis aux balises API renderFile, parseFile, renderFileSync, parseFileSync, et include, layout sont résolues par rapport à the root option.

Il peut s’agir d’un chemin de type chaîne (voir l’exemple ci-dessus) ou d’une liste de répertoires racine, auquel cas les modèles seront recherchés dans cet ordre. par ex.

var engine = new Liquid({
    root: ['views/', 'views/partials/'],
    extname: '.liquid'
});

Chemins relatifs : Les chemins relatifs dans root seront résolus par rapport à cwd().

Lorsque {% render "foo" %} est rendu ou que liquid.renderFile('foo') est appelé, les fichiers suivants seront recherchés et le premier fichier existant sera utilisé :

Si aucun des fichiers ci-dessus n’existe, une erreur ENOENT sera générée. Voici une démo pour Node.js : demo/nodejs.

Lorsque LiquidJS est utilisé dans le navigateur, disons que l’emplacement actuel est https://example.com/bar/index.html, seule la première racine sera utilisée et le fichier à récupérer est :

Si la récupération échoue, une erreur 404/500 ou des pannes de réseau par exemple, une erreur ENOENT sera générée. Voici une démo pour les navigateurs : demo/browser.

Abstract File System

LiquidJS définit une interface de système de fichiers abstraite et l’implémentation par défaut est src/fs/fs-impl.ts pour Node.js et src/build/fs-impl-browser.ts pour le bundle de navigateur.

Le constructeur Liquid fournit une option fs pour spécifier l’implémentation du système de fichiers. Il est censé être utilisé pour définir une logique de récupération de modèle personnalisée, c’est-à-dire récupérer un modèle à partir d’une table de base de données, comme :

var engine = new Liquid({
    fs: {
        readFileSync (file) {
            return db.model('Template').findByIdSync(file).text
        },
        async readFile (file) {
            const template = await db.model('Template').findById(file)
            return template.text
        },
        existsSync () {
            return true
        },
        async exists () {
            return true
        },
        contains () {
            return true
        },
        resolve(root, file, ext) {
            return file
        }
    }
});

Vulnérabilité de traversée de chemin : La valeur par défaut de contain() renvoie toujours true. Cela signifie que lorsque vous spécifiez un système de fichiers abstrait, vous devrez fournir un contain() approprié pour éviter d’exposer de telles vulnérabilités.

In-memory Template

Pour faciliter le rendu sans fichiers, il existe une option de templates permettant de spécifier un mappage des noms de fichiers et de leur contenu. LiquidJS lira les modèles du mappage.

const engine = new Liquid({
  templates: {
    'views/entry': 'header {% include "../partials/footer" %}',
    'partials/footer': 'footer'
  }
})
engine.renderFileSync('views/entry'))
// Result: 'header footer'

Notez que les options du système de fichiers telles que root, layouts, partials, relativeReference seront ignorées lorsque les templates sont spécifiés.

Partiels et mises en page

Render Partials

Pour les fichiers modèles suivants :

// file: color.liquid
color: '{{ color }}' shape: '{{ shape }}'

// file: theme.liquid
{% assign shape = 'circle' %}
{% render 'color.liquid' %}
{% render 'color.liquid' with 'red' %}
{% render 'color.liquid', color: 'yellow', shape: 'square' %}

Le résultat sera :

color: '' shape: 'circle'
color: 'red' shape: 'circle'
color: 'yellow' shape: 'square'

Pour plus de détails, veuillez vous référer à la balise render.

L’extension “.liquid” : L’extension « .liquid » dans le layout, render etinclude peut être omise si l’instance Liquid est créée à l’aide de l’option extname : « .liquid ». Voir the extname option pour plus de détails.

Layout Templates (Extends)

Pour les fichiers modèles suivants :

// file: default-layout.liquid
Header
{% block content %}My default content{% endblock %}
Footer

// file: page.liquid
{% layout "default-layout.liquid" %}
{% block content %}My page content{% endblock %}

La sortie de page.liquid :

Header
My page content
Footer

Pour plus de détails, veuillez vous référer à la balise layout.

Utiliser dans Express.js

LiquidJS est compatible avec les express template engines. Vous pouvez définir l’instance liquidjs sur l’option du view engine :

var { Liquid } = require('liquidjs');
var engine = new Liquid();

// register liquid engine
app.engine('liquid', engine.express()); 
app.set('views', './views');            // specify the views directory
app.set('view engine', 'liquid');       // set liquid to default

Démo de travail : Voici une démo fonctionnelle pour l’utilisation de LiquidJS dans Express.js : liquidjs/demo/express/.

Template Lookup

L’option root continuera à fonctionner en tant que racine des modèles, comme vous pouvez le voir dans Render A Template File. De plus, l’option views dans express.js (comme indiqué ci-dessus) sera également respectée. Supposons que vous ayez un répertoire de modèles tel que :

.
├── views1/
│ └── hello.liquid
└── views2/
  └── world.liquid

Et vous définissez la racine du modèle pour liquidjs sur views1 et expressjs sur views2 :

var { Liquid } = require('liquidjs');
var engine = new Liquid({
    root: './views1/'
});

app.engine('liquid', engine.express()); 
app.set('views', './views2');            // specify the views directory
app.set('view engine', 'liquid');       // set liquid to default

hello.liquid et world.liquid peuvent être résolus et rendus :

res.render('hello')
res.render('world')

Caching

Définir simplement l’cache option sur true activera la mise en cache des modèles, comme expliqué dans Caching. Il est recommandé d’activer le cache dans l’environnement de production, ce qui peut être fait en :

var { Liquid } = require('liquidjs');
var engine = new Liquid({
    cache: process.env.NODE_ENV === 'production'
});

Mise en cache

Dans un projet de site Web typique, nous aurons un répertoire de modèles de vues et ils seront rendus plusieurs fois. Dans un environnement de production, les fichiers modèles ne sont pas susceptibles d’être modifiés au fil du temps (en dehors des redéploiements). Il est donc logique de mettre en cache le contenu des fichiers et les modèles analysés (dans une sorte d’AST) pour améliorer les performances.

LiquidJS propose plusieurs façons de mettre en cache les modèles analysés pour améliorer les performances.

Programmatically

Les API .parse(), .parseFile(), .parseFileSync() sont utilisées pour analyser des modèles à partir d’une chaîne ou de fichiers. Le modèle de résultat peut ensuite être rendu plusieurs fois avec un contexte différent.

Analyser à partir de la chaîne :

var tpl = engine.parse('{{name | capitalize}}');

engine.renderSync(tpl, {name: 'alice'}) // 'Alice'
engine.renderSync(tpl, {name: 'bob'}) // 'Bob'

Analyser à partir du fichier :

var tpl = engine.parseFileSync('hello');    // contents of `hello.liquid`: {{name}}

engine.renderSync(tpl, {name: 'alice'}) // 'Alice'
engine.renderSync(tpl, {name: 'bob'}) // 'Bob'

La chaîne/le fichier du modèle n’est analysé qu’une seule fois et restitué plusieurs fois en utilisant un contexte différent. Les modèles de différents fichiers peuvent être stockés dans une Map et peuvent être récupérés directement pour les rendus ultérieurs.

The cache Option

Le cache option peut être définie pour demander à liquidjs d’utiliser des modèles analysés en cache chaque fois que vous appelez renderFile ou renderFileSync.

var { Liquid } = require('liquidjs');
var engine = new Liquid({
    cache: true
});

// liquidjs parses the hello.liquid, then renders it with {name: 'alice'}
engine.renderFileSync('hello', {name: 'alice'})

// liquidjs finds the cached template, then renders it with {name: 'bob'}
engine.renderFileSync('hello', {name: 'bob'})

Escaping

L’évasion est importante dans tous les langages, y compris LiquidJS. Alors que l’échappement a 2 significations différentes pour un moteur de modèle :

HTML Escape

Par défaut, la sortie n’est pas échappée. Bien que vous puissiez utiliser le filtre escape pour cela :

Saisir :

{{ "1 < 2" | escape }}

Sortie :

1 &lt; 2

Il existe également des filtres escape_once, newline_to_br, strip_html pour vous permettre d’affiner votre sortie.

Dans les cas où les variables ne sont généralement pas fiables, outputEscape peut être défini sur "escape" pour appliquer l’échappement par défaut. Dans ce cas, lorsque vous avez besoin de ne pas échapper une sortie, un filtre raw peut être utilisé :

Saisir :

{{ "1 < 2" }}
{{ "<button>OK</button>" | raw }}

Sortie :

1 &lt; 2
<button>OK</button>

Liquid Escape

Pour désactiver le langage Liquid et les chaînes de sortie telles que { { et {%, la balise raw peut être utilisée.

Saisir :

{% raw %}
  In LiquidJS, {{ this | escape }} will be HTML-escaped, but
  {{{ that }}} will not.
{% endraw %}

Sortie :

In LiquidJS, {{ this | escape }} will be HTML-escaped, but
{{{ that }}} will not.

Dans les chaînes littérales du modèle LiquidJS, \ peut être utilisé pour échapper aux caractères spéciaux dans la syntaxe des chaînes. Par exemple:

Saisir :

{{ "\"" }}

Sortie :

"

Registre des filtres/tags

Register Tags

// Usage: {% upper name %}
import { Value, TagToken, Context, Emitter, TopLevelToken } from 'liquidjs'

engine.registerTag('upper', {
    parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) {
        this.value = new Value(token.args, liquid)
    },
    render: function*(ctx: Context) {
        const str = yield this.value.value(ctx); // 'alice'
        return str.toUpperCase() // 'ALICE'
    }
});

parse : lit les jetons de remainTokens jusqu’à votre jeton de fin. render : combinez les données de portée avec vos jetons analysés dans une chaîne HTML.

Pour une implémentation de balises complexe, vous pouvez également fournir une classe de balises :

// Usage: {% upper name:"alice" %}
import { Hash, Tag, TagToken, Context, Emitter, TopLevelToken, Liquid } from 'liquidjs'

engine.registerTag('upper', class UpperTag extends Tag {
    private hash: Hash
    constructor(tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
        super(tagToken, remainTokens, liquid)
        this.hash = new Hash(tagToken.args)
    }
    * render(ctx: Context) {
        const hash = yield this.hash.render();
        return hash.name.toUpperCase() // 'ALICE'
    }
});

Register Filters

// Usage: {{ name | upper }}
engine.registerFilter('upper', v => v.toUpperCase())

Les arguments de filtre seront transmis à la fonction de filtre enregistrée, par exemple :

// Usage: {{ 1 | add: 2, 3 }}
engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2)

Voir les implémentations de filtres existantes ici : https://github.com/harttle/liquidjs/tree/master/src/filters

Unregister Tags/Filters

Dans certains cas, il est souhaitable de désactiver certaines balises/filtres (voir #324), vous devrez enregistrer une balise/filtre factice dans lequel une erreur correspondante est générée.

// disable a tag
const disabledTag = {
    parse: function(token) {
        throw new Error(`tag "${token.name}" disabled`);
    }
}
engine.registerTag('include', disabledTag);

// disable a filter
function disabledFilter(name) {
    return function () {
        throw new Error(`filter "${name}" disabled`);
    }
}
engine.registerFilter('plus', disabledFilter('plus'));

Access Scope in Filters

Comme indiqué dans Register Filters/Tags, nous pouvons accéder aux arguments de filtre directement dans la fonction de filtre comme :

// Usage: {{ 1 | add: 2, 3 }}
// Output: 6
engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2)

Lorsqu’il s’agit de filtres avec état, par exemple pour transformer un chemin d’URL en URL complète, nous devrons accéder à une origin dans la portée actuelle :

// Usage: {{ '/index.html' | fullURL }}
// Scope: { origin: "https://liquidjs.com" }
// Output: https://liquidjs.com/index.html

engine.registerFilter('fullURL', function (path) {
    const origin = this.context.get(['origin'])
    return new URL(path, origin).toString() 
})

Voir ce JSFiddle : https://jsfiddle.net/ctj364up/1/

Arrow Functions : ceci dans les fonctions fléchées est lié au contexte JavaScript actuel, vous devrez utiliser la syntaxe function(){} au lieu de la syntaxe ()=>{} pour accéder correctement à this.context.

Paramètres d’analyse

Access Raw Parameters

Comme indiqué dans Register Filters/Tags, les paramètres de balise sont disponibles sur tagToken.args sous forme de chaîne raw. Par exemple:

// Usage: {% random foo bar coo %}
// Output: "foo", "bar" or "coo"
engine.registerTag('random', {
  parse(tagToken) {
    // tagToken.args === "foo bar coo"
    this.items = tagToken.args.split(' ')
  },
  render(context, emitter) {
    // get a random index
    const index = Math.floor(this.items.length * Math.random())
    // output that item
    emitter.write(this.items[index])
  }
})

Voici une version JSFiddle : https://jsfiddle.net/ctj364up/2/

Parse Parameters as Values

Parfois, nous avons besoin de balises plus dynamiques et souhaitons transmettre des valeurs à la balise personnalisée au lieu de chaînes statiques. Les variables dans LiquidJS peuvent être littérales (chaîne, nombre, etc.) ou une variable de la portée du contexte actuel.

Le modèle modifié suivant contient également 3 valeurs aléatoires, mais ce sont des valeurs au lieu de chaînes statiques. Le premier est une chaîne littérale, le deuxième est un identifiant, le troisième est une séquence d’accès à la propriété contenant deux identifiants.

{% random "foo" bar obj.coo %}

Il peut être difficile d’analyser tous ces cas manuellement, mais il existe une classe Tokenizer dans LiquidJS que vous pouvez utiliser.

const { Liquid, Tokenizer, evalToken } = require('liquidjs')
engine.registerTag('random', {
  parse(tagToken) {
    const tokenizer = new Tokenizer(tagToken.args)
    this.items = []
    while (!tokenizer.end()) {
      // here readValue() returns a LiteralToken or PropertyAccessToken
      this.items.push(tokenizer.readValue())
    }
  },
  * render(context, emitter) {
    const index = Math.floor(this.items.length * Math.random())
    const token = this.items[index]
    // in LiquidJS, we use yield to wait for async call
    const value = yield evalToken(token, context)
    emitter.write(value)
  }
})

L’appel de cette balise dans scope { bar: "bar", obj: { coo: "coo" } } donne exactement le même résultat que le premier exemple. Voir ce JSFiddle : https://jsfiddle.net/ctj364up/3/

Async and Promises : Les appels asynchrones dans LiquidJS sont implémentés directement par les générateurs, car nous pouvons appeler les générateurs de manière synchrone, donc cette implémentation de balise est également valable pour renderSync(), parseAndRenderSync(), renderFileSync(). Si vous devez attendre une promesse dans l’implémentation de la balise, remplacez simplement wait somePromise par yield somePromise et gardez * render() au lieu de async render() fera l’affaire. Voir Sync et Async pour plus de détails.

Parse Key-Value Pairs as Named Parameters

Les paramètres nommés deviennent très pratiques lorsqu’il y a des paramètres facultatifs ou de nombreux paramètres, auquel cas l’ordre des paramètres n’a pas d’importance. C’est exactement pour cela que la classe Hash a été inventée.

{% random from:2, to:max %}

Dans l’exemple ci-dessus, nous essayons de générer un nombre aléatoire dans la plage [2, max]. Nous utiliserons Hash pour analyser les paramètres from et to.

const { Liquid, Hash } = require('liquidjs')

engine.registerTag('random', {
  parse(tagToken) {
    // parse the parameters structure into `this.args`
    this.args = new Hash(tagToken.args)
  },
  * render(context, emitter) {
    // evaluate the parameters in `context`
    const {from, to} = yield this.args.render(context)
    const length = to - from + 1
    const value = from + Math.floor(length * Math.random())
    emitter.write(value)
  }
})

Le rendu de {% random from:2, to:max %} dans la portée { max: 10 } générera un nombre aléatoire dans la plage [2, 10]. Voir ce JSFiddle : https://jsfiddle.net/ctj364up/4/

Rendre le contenu de la balise

Les balises personnalisées peuvent avoir un modèle de contenu et peuvent être imbriquées. Cet article décrit comment implémenter des balises personnalisées composées d’une balise de début , d’une balise de fin et d’un contenu de modèle entre elles.

Render Tag Content

Nous commencerons avec une balise simple wrap qui enveloppe son contenu dans un élément <div class="wrapper"></div> :

{% wrap %}
  {{ "hello world!" | capitalize }}
{% endwrap %}

Résultat attendu :

<div class='wrapper'>
  Hello world!
</div>

Tout d’abord, register une balise avec le nom wrap et analysez le contenu dans this.tpls. Ici dans parse(tagToken, remainTokens),

En gros, ce que nous devons faire est de prendre .shift() suffisamment de balises jusqu’à remainTokens ce que nous obtenions un jeton endwrap (le nom peut être arbitraire, mais par convention, nous avons besoin qu’il soit endwrap). Et s’il n’y a pas de endwrap jusqu’à la fin du fichier modèle, nous devons lancer un tag-not-closed Error.

engine.registerTag('wrap', {
  parse(tagToken, remainTokens) {
    this.tpls = []
    let closed = false
    while(remainTokens.length) {
      let token = remainTokens.shift()
      // we got the end tag! stop taking tokens
      if (token.name === 'endwrap') {
        closed = true
        break
      }
      // parse token into template
      // parseToken() may consume more than 1 tokens
      // e.g. {% if %}...{% endif %}
      let tpl = this.liquid.parser.parseToken(token, remainTokens)
      this.tpls.push(tpl)
    }
    if (!closed) throw new Error(`tag ${tagToken.getText()} not closed`)
  },
  * render(context, emitter) {
    emitter.write("<div class='wrapper'>")
    yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
    emitter.write("</div>")
  }
})

.renderTemplates() peut être asynchrone, nous devons yield attendre qu’il soit terminé. Pour plus de détails sur async dans LiquidJS, veuillez vous référer à Sync et Async. Les autres parties de render() la méthode est assez simples. Voici une version JSFiddle : https://jsfiddle.net/por0zcn1/3/

Using ParseStream

En ce qui concerne les balises complexes comme for et if, cela parse() peut être très compliqué. Il existe un utilitaire ParseStream pour organiser les balises parse() dans un style basé sur les événements. Voici une version réécrite parse() en utilisant ParseStream et qui fait exactement la même chose que l’exemple ci-dessus.

parse(tagToken, remainTokens) {
  this.tpls = []
  this.liquid.parser.parseStream(remainTokens)
    .on('template', tpl => this.tpls.push(tpl))
    // note that we cannot use arrow function because we need `this`
    .on('tag:endwrap', function () { this.stop() })
    .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
    .start()
}

Voici une version JSFiddle : https://jsfiddle.net/por0zcn1/4/. Pour plus de simplicité, les exemples suivants sont implémentés à l’aide de ParseStream.

Manipulate the Context

La balise wrap ci-dessus ne semble pas très utile, sans utiliser cette balise, nous pouvons de toute façon restituer le contenu. Nous allons maintenant implémenter une balise repeat pour restituer le contenu 2 fois (nous pouvons également ajouter un paramètre pour restituer des fois arbitraires).

{% repeat %}
  {{ repeat.i }}. {{ "hello world!" | capitalize }}
{% endrepeat %}`

Résultats attendus :

1. Hello world!
2. Hello world!

Comme vous l’avez remarqué, il existe un élément supplémentaire repeat.i dans le contexte de repeat. Cela est implémenté en manipulant le Context.

Contexte : Le contexte définit la valeur de chaque variable dans le modèle Liquid. Dans LiquidJS, un Context est constitué d’une pile de Scopes. Une portée est un objet simple comme celui spécifié dans engine.render(tpl, scope).

Chaque fois que nous entrons dans un nouveau Context, nous devons pousser un nouveau Scope. Et lorsque nous terminons le rendu et sortons du Context , nous extrayons le Scope du Context. Comme vous pouvez le voir dans l’implémentation suivante :

engine.registerTag('repeat', {
  parse(tagToken, remainTokens) {
    this.tpls = []
    this.liquid.parser.parseStream(remainTokens)
      .on('template', tpl => this.tpls.push(tpl))
      .on('tag:endrepeat', function () { this.stop() })
      .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
      .start()
  },
  * render(context, emitter) {
    const repeat = { i: 1 }
    context.push({ repeat })
    yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
    repeat.i++
    yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
    context.pop()
  }
})

C’est parse() exactement la même chose que wrap tag, on répète le contenu simplement en appelant .renderTemplates(this.tpls) deux fois pendant render(). Voici le JSFiddle : https://jsfiddle.net/por0zcn1/2/

Utilisez Push & Pop par paires : context.push() et context.pop() doivent être utilisés par paires. L’échec de pop() la portée que vous avez poussée entraînera une fuite de la portée vers les modèles ultérieurs et peut corrompre la pile de contexte.

Liquid Drops

LiquidJS fournit également un mécanisme similaire à Shopify Drops, permettant aux auteurs de modèles d’intégrer des fonctionnalités personnalisées dans la résolution des valeurs de variables.

Drop pour JavaScript : L’interface Drop est implémentée différemment dans LiquidJS par rapport aux filtres intégrés et aux autres fonctionnalités de modèle. Étant donné que LiquidJS s’exécute en JavaScript, les Drops personnalisés doivent de toute façon être réimplémentés en JavaScript. Il n’y a aucune compatibilité entre les classes JavaScript et les classes Ruby.

Utilisation de base

import { Liquid, Drop } from 'liquidjs'

class SettingsDrop extends Drop {
  constructor() {
    super()
    this.foo = 'FOO'
  }
  bar() {
    return 'BAR'
  }
}

const engine = new Liquid()
const template = `foo: {{settings.foo}}, bar: {{settings.bar}}`
const context = { settings: new SettingsDrop() }
// Outputs: "foo: FOO, bar: BAR"
engine.parseAndRender(template, context).then(html => console.log(html))

Lien vers Runkit

Comme indiqué ci-dessus, en plus de lire les propriétés à partir des portées de contexte, vous pouvez également appeler des méthodes. Il vous suffit de créer une classe personnalisée héritée de Drop.

Méthodes asynchrones : LiquidJS est entièrement compatible avec l’asynchronisme. Vous pouvez renvoyer en toute sécurité une promesse dans vos méthodes Drop ou définir vos méthodes dans Drop comme async.

liquidMethodMissing

Dans les cas où il n’existe pas d’ensemble fixe de propriétés, vous pouvez utiliser cette fonction liquidMethodMissing pour résoudre dynamiquement la valeur d’un nom de variable.

import { Liquid, Drop } from 'liquidjs'

class SettingsDrop extends Drop {
  liquidMethodMissing(key) {
    return key.toUpperCase()
  }
}

const engine = new Liquid()
// Outputs: "COO"
engine.parseAndRender("{{settings.coo}}", { settings: new SettingsDrop() })
  .then(html => console.log(html))

liquidMethodMissing prend en charge Promise, ce qui signifie que vous pouvez effectuer des appels asynchrones à l’intérieur. Un cas plus utile peut être la récupération dynamique de la valeur à partir de la base de données. En utilisant Drops, vous pouvez éviter de coder en dur chaque propriété dans le contexte. Par exemple :

import { Liquid, Drop } from 'liquidjs'

class DBDrop extends Drop {
  async liquidMethodMissing(key) {
    const record = await db.getRecordByKey(key)
    return record.value
  }
}

const engine = new Liquid()
const context = { db: new DBDrop() }
engine.parseAndRender("{{db.coo}}", context).then(html => console.log(html))

Les gouttes peuvent implémenter une valueOf() méthode dont la valeur de retour peut être utilisée pour se remplacer elle-même dans la sortie. Par exemple :

import { Liquid, Drop } from 'liquidjs'

class ColorDrop extends Drop {
  valueOf() {
    return 'red'
  }
}

const engine = new Liquid()
const context = { color: new ColorDrop() }
// Outputs: "red"
engine.parseAndRender("{{color}}", context).then(html => console.log(html))

toLiquid

toLiquid() n’est pas une méthode de Drop, mais elle peut être utilisée pour renvoyer un Drop. Dans les cas où vous avez une structure fixe dans laquelle context vous ne pouvez pas modifier ses valeurs, vous pouvez implémenter toLiquid() pour permettre à LiquidJS d’utiliser la valeur renvoyée au lieu de lui-même pour restituer les modèles.

import { Liquid, Drop } from 'liquidjs'

const context = {
  person: {
    firstName: "Jun",
    lastName: "Yang",
    name: "Jun Yang",
    toLiquid: () => ({
      firstName: this.firstName,
      lastName: this.lastName,
      // use a different `name`
      name: "Yang, Jun"
    })
  }
}

const engine = new Liquid()
// Outputs: "Yang, Jun"
engine.parseAndRender("{{person.name}}", context).then(html => console.log(html))

Bien sûr, vous pouvez également renvoyer une PersonDrop instance dans la toLiquid() méthode et implémenter cette fonctionnalité dans PersonDrop :

import { Liquid, Drop } from 'liquidjs'

class PersonDrop extends Drop {
  constructor(person) {
    super()
    this.person = person
  }
  name() {
    return this.person.lastName + ", " + this.person.firstName
  }
}

const context = {
  person: {
    firstName: "Jun",
    lastName: "Yang",
    name: "Jun Yang",
    toLiquid: function () { return new PersonDrop(this) }
  }
}

const engine = new Liquid()
// Outputs: "Yang, Jun"
engine.parseAndRender("{{person.name}}", context).then(html => console.log(html))

toLiquid() vs. valueOf() Différence

  • valueOf() est généralement utilisé pour définir comment la variable actuelle doit être rendue, tandis qu’il toLiquid() est souvent utilisé pour convertir un objet en Drop ou une autre portée fournie au modèle.
  • valueOf() est une méthode exclusive à Drops ; alors qu’elle toLiquid() peut être utilisée sur n’importe quel objet de portée.
  • valueOf() est appelé lorsque la variable elle-même est sur le point d’être rendue, se remplaçant elle-même ; alors qu’il toLiquid() est appelé lorsque ses propriétés sont sur le point d’être lues.

Special Drops

LiquidJS lui-même implémente plusieurs gouttes intégrées pour faciliter l’écriture de modèles. Cette partie est compatible avec Shopify Liquid, car nous avons besoin que les modèles soient portables.

blank

Utile pour vérifier si une variable de chaîne est false, null, undefined, une chaîne vide ou une chaîne contenant uniquement des caractères vides.

{% unless author == blank %}
    {{author}}
{% endif %}
empty

Utile pour vérifier si un tableau, une chaîne ou un objet est vide.

{% if authors == empty %}
    Author list is empty
{% endif %}

empty mise en œuvre : Pour les tableaux et les chaînes, LiquidJS vérifie leurs .length propriétés. Pour les objets, LiquidJS effectue des appels Object.keys() pour vérifier s’ils ont des clés.

nil

nil Drop est utilisé pour vérifier si une variable n’est pas définie ou définie comme null ou undefined, essentiellement équivalent à == null la vérification JavaScript.

{% if nonexistent == nil %}
    null variable
{% endif %}

Other Drops

Il existe encore plusieurs Drops pour des balises spécifiques, comme forloop, tablerowloop, block, qui sont couverts par les documents de balises respectifs.

Sync and Async

LiquidJS prend en charge les évaluations synchrones et asynchrones et peut être utilisé avec les promesses. Pour réutiliser le même ensemble d’implémentations de balises/filtres à la fois en synchronisation et en asynchrone, les balises LiquidJS sont implémentées en tant que générateurs.

Sync and Async API

Toutes les principales méthodes de Liquid prennent en charge à la fois la synchronisation et l’asynchronisation. Ces méthodes renvoient des promesses :

La version synchrone des méthodes contient un Sync suffixe :

Implement Sync-Compatible Tags

LiquidJS utilise une implémentation asynchrone basée sur un générateur pour prendre en charge à la fois l’asynchrone et la synchronisation dans une seule implémentation de balise. Par exemple, ci-dessous UpperTag peut être utilisé à la fois dans engine.renderSync() et engine.render().

import { TagToken, Context, Emitter, TopLevelToken, Value, Tag, Liquid } from 'liquidjs'

// Usage: {% upper "alice" %}
// Output: ALICE
engine.registerTag('upper', class UpperTag extends Tag {
  private value: Value
  constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
    super(token, remainTokens, liquid)
    this.value = new Value(token.args, liquid)
  }
  * render (ctx: Context, emitter: Emitter) {
    const title = yield this.value.value(ctx)
    emitter.write(title.toUpperCase())
  }
})

Toutes les balises intégrées sont implémentées de cette manière et peuvent être utilisées en toute sécurité à la fois en synchronisation et en asynchrone (je l’appellerai sync-compatible). Pour rendre votre balise personnalisée sync-compatible, vous devrez :

Call APIs that return a Promise

Mais LiquidJS est compatible avec Promise, n’est-ce pas ? Vous pouvez toujours appeler des fonctions basées sur Promise et attendre cette promesse dans les implémentations de balises. Remplacez simplement await par yield. par exemple, nous appelons fs.readFile() qui renvoie un Promise:

* render (ctx: Context, emitter: Emitter) {
  const file = yield this.value.value(ctx)
  const title = yield fs.readFile(file, 'utf8')
  emitter.write(title.toUpperCase())
}

Maintenant que cela * render() appelle une API qui renvoie une promesse, elle n’est plus compatible avec la synchronisation .

Balises non compatibles avec la synchronisation : Les balises non compatibles avec la synchronisation sont également des balises valides et fonctionneront parfaitement pour les appels d’API asynchrones. Lorsqu’elles sont appelées de manière synchrone, les balises qui renvoient un Promise seront rendues sous la forme [object Promise].

Convert LiquidJS async Generator to Promise

Vous pouvez convertir un générateur en promesse en utilisant toPromise, par exemple :

import { TagToken, Context, Emitter, TopLevelToken, Value, Tag, Liquid, toPromise } from 'liquidjs'

// Usage: {% upper "alice" %}
// Output: ALICE
engine.registerTag('upper', class UpperTag extends Tag {
  private value: Value
  constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
    super(token, remainTokens, liquid)
    this.value = new Value(token.args, liquid)
  }
  async render (ctx: Context, emitter: Emitter) {
    const title = await toPromise(this.value.value(ctx))
    emitter.write(title.toUpperCase())
  }
})

Async only Tags

Si votre balise est destinée à être utilisée uniquement de manière asynchrone, elle peut être déclarée ainsi async render() afin que vous puissiez l’utiliser await directement dans son implémentation :

import { toPromise, TagToken, Context, Emitter, TopLevelToken, Value, Tag, Liquid } from 'liquidjs'

// Usage: {% upper "alice" %}
// Output: ALICE
engine.registerTag('upper', class UpperTag extends Tag {
  private value: Value
  constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
    super(token, remainTokens, liquid)
    this.value = new Value(token.args, liquid)
  }
  async render (ctx: Context, emitter: Emitter) {
    const title = await toPromise(this.value.value(ctx))
    emitter.write(`<h1>${title}</h1>`)
  }
})

Contrôle des espaces

Pour garder le code source propre et indenté, nous ajoutons des espaces à nos modèles. LiquidJS offre des fonctionnalités de contrôle des espaces pour éliminer ces espaces indésirables dans le code HTML de sortie.

via Markups

Par défaut, toutes les balises et lignes de balisage de sortie génèrent un NL (\n) et des espaces s’il y a une indentation. Par exemple :

{%  author = "harttle" %}
{{ author }}

Sorties (notez le lien vide) :

harttle

Nous pouvons inclure des tirets dans la syntaxe de votre balise ({{-, -}}, {%-, -%}) pour supprimer les espaces à gauche ou à droite. Par exemple :

{% assign author = "harttle" -%}
{{ author }}

Résultats :

harttle

Dans ce cas, il -%} supprime l’espace blanc du côté droit de la assign balise.

via Options

Alternativement, LiquidJS fournit ces options par moteur pour permettre le contrôle des espaces blancs sans modifier radicalement vos modèles :

LiquidJS ne supprimera aucun espace par défaut, c’est-à-dire que toutes les options ci-dessus sont par défaut à false. Pour plus de détails sur ces options, consultez les options.

Greedy Mode

En mode gourmand (activé par l’option gourmande), tous les caractères d’espacement consécutifs (y compris \n) seront supprimés. Le mode gourmand est activé par défaut pour être conforme à Shopify/Liquid.

Plugins

Un certain nombre de balises et de filtres peuvent être encapsulés dans un plugin, qui sera généralement installé via npm. Cet article fournit des informations sur la création et l’utilisation d’un plugin.

Écrire un plugin

Un plugin liquidjs est une fonction simple qui prend la classe Liquid comme premier paramètre et l’instance Liquid pour this. Nous pouvons appeler les API liquidjs this pour effectuer certaines modifications, notamment les register filters and tags.

Nous allons maintenant créer un plugin pour mettre en majuscules chaque lettre de l’entrée, enregistrez l’extrait suivant dans upup.js :

/**
 * Inside the plugin function, `this` refers to the Liquid instance.
 *
 * @param Liquid: provides facilities to implement tags and filters.
 */
module.exports = function (Liquid) {
    this.registerFilter('upup', x => x.toUpperCase());
}

Utiliser un plugin

Il suffit de transmettre la fonction du plugin dans la .plugin() méthode :

const engine = new Liquid()

engine.plugin(require('./upup.js'));
engine
    .parseAndRender('{{ "foo" | upup }}')
    .then(console.log)  // outputs "FOO"

Liste des plugins

Étant donné que cette bibliothèque exclut certaines fonctionnalités disponibles sur la plateforme Shopify mais pas sur le référentiel Shopify/liquid, consultez Différences avec Shopify/liquid.

Voici une liste de plugins qui complètent ces fonctionnalités. N’hésitez pas à ajouter les vôtres, ce fichier est modifiable publiquement.

Opérateurs

Les opérateurs LiquidJS sont très simples et différents. Il existe 2 types d’opérateurs pris en charge :

Ainsi, les opérateurs numériques ne sont pas pris en charge et vous ne pouvez même pas ajouter deux nombres comme cela {{a + b}}, à la place nous avons besoin d’un filtre {{ a | plus: b}}. En fait, + c’est un nom de variable valide dans LiquidJS.

Priorité

  1. Opérateurs de comparaison. Toutes les opérations de comparaison ont la même priorité et sont supérieures aux opérateurs logiques.
  2. Opérateurs logiques. Tous les opérateurs logiques ont la même priorité.

Associativité

Les opérateurs logiques sont évalués de droite à gauche, voir la documentation Shopify.

Vrai et faux

Bien que Liquid soit indépendant de la plate-forme, il existe certaines différences avec la version Ruby, dont l’une est la truthy valeur.

La table de vérité

D’après le document Shopify, tout ce qui n’est pas false et nil est vrai. Mais en JavaScript, nous avons un système de types totalement différent, nous avons des types comme undefined et nous ne faisons pas de distinction entre integer et float, donc les choses sont légèrement différentes :

valeur véridique fausseté
true ✔️  
false   ✔️
null   ✔️
undefined   ✔️
string ✔️  
empty string ✔️  
0 ✔️  
integer ✔️  
float ✔️  
array ✔️  
empty array ✔️  

Utilisez JavaScript Truthy

Notez que LiquidJS utilise la vérité de Shopify par défaut. Mais il peut être basculé pour utiliser la vérité JavaScript standard en définissant l’ option jsTruthy sur true.

valeur véridique fausseté
true ✔️  
false   ✔️
null   ✔️
undefined   ✔️
string ✔️  
empty string   ✔️
0   ✔️
integer ✔️  
float ✔️  
array ✔️  
empty array ✔️  

Prévention des attaques DoS

Lorsque le modèle ou le contexte de données ne sont pas fiables, il est essentiel d’activer les options de prévention DoS. LiquidJS propose 3 options à cet effet : parseLimit, renderLimit, et memoryLimit.

TL;DR

La définition de ces options peut en grande partie garantir que votre instance LiquidJS ne se bloquera pas pendant des périodes prolongées ou ne consommera pas de mémoire excessive. Ces limites sont basées sur les API JavaScript disponibles, il ne s’agit donc pas de limites strictes précises, mais de seuils permettant d’éviter que votre processus ne tombe en panne ou ne se bloque.

const liquid = new Liquid({
    parseLimit: 1e8, // typical size of your templates in each render
    renderLimit: 1000, // limit each render to be completed in 1s
    memoryLimit: 1e9, // memory available for LiquidJS (1e9 for 1GB)
})

Lorsqu’un parse() ou render() ne peut pas être complété dans le cadre d’une ressource donnée, il est rejeté.

parseLimit

parseLimit limite la taille (longueur des caractères) des modèles analysés à chaque .parse() appel, y compris les partiels et les mises en page référencés. Étant donné que LiquidJS analyse les chaînes de modèles en un temps proche de O(n), la limitation de la longueur totale du modèle est généralement suffisante.

Un PC typique gère 1e8 (100 M) de caractères sans problème.

renderLimit

Restreindre la taille du modèle seul n’est pas suffisant car des boucles dynamiques avec un grand nombre peuvent se produire au moment du rendu. renderLimit atténue ce problème en limitant le temps consommé par chaque render() appel.

{%- for i in (1..10000000) -%}
    order: {{i}}
{%- endfor -%}

Le temps de rendu est vérifié pour chaque modèle (avant le rendu de chaque modèle). Dans l’exemple ci-dessus, il y a 2 modèles dans la boucle : order: et {{i}}, le temps de rendu sera vérifié 10000000x2 fois.

Pour les balises et les filtres chronophages au sein d’un seul modèle, le processus peut toujours se bloquer. Pour un rendu entièrement contrôlé, pensez à utiliser un gestionnaire de processus comme paralleljs.

memoryLimit

Même avec un nombre réduit de modèles et d’itérations, l’utilisation de la mémoire peut augmenter de manière exponentielle. Dans l’exemple suivant, la mémoire double à chaque itération :

{% assign array = "1,2,3" | split: "," %}
{% for i in (1..32) %}
    {% assign array = array | concat: array %}
{% endfor %}

memoryLimit limite les filtres sensibles à la mémoire pour éviter une allocation de mémoire excessive. Comme JavaScript utilise GC pour gérer la mémoire, memoryLimit limite uniquement le nombre total d’objets alloués par les filtres sensibles à la mémoire dans LiquidJS et peut donc ne pas refléter l’empreinte mémoire réelle.