liquid_dart 1.0.1
liquid_dart: ^1.0.1 copied to clipboard
Liquid template engine for Dart and Flutter, compatible with LiquidJS 10.24.0 and Shopify-style templates.
liquid_dart #
Liquid template engine for Dart and Flutter.
A Dart port of LiquidJS 10.24.0 with a goal of behavioral compatibility (LiquidJS and Shopify-style templates). Built and maintained with test-driven development and a strong focus on real-world compatibility.
Status #
- Current version:
1.0.1 - Platforms: Dart VM, Flutter (iOS, Android, web, desktop)
- Null safety: yes
- Rendering: async (template loading is async)
Installation #
dart pub add liquid_dart
Or add to pubspec.yaml:
dependencies:
liquid_dart: ^1.0.1
Quick start #
import "package:liquid_dart/liquid_dart.dart";
Future<void> main() async {
final engine = LiquidEngine();
final out = await engine.parseAndRender(
"Hello {{ name | upcase }}!",
{"name": "Ada"},
);
print(out); // Hello ADA!
}
Template loading: include, render, layout #
liquid_dart does not read the disk directly. You provide a LiquidFileSystem implementation.
FileSystem (include/render/layout from files) #
final fs = InMemoryFileSystem({
"base": "Header|{% block content %}DEFAULT{% endblock %}|Footer",
"child": "{% layout 'base' %}{% block content %}Hello {{ name }}{% endblock %}",
});
final engine = LiquidEngine(fileSystem: fs);
final out = await engine.renderFile("child", {"name": "Ada"});
// Header|Hello Ada|Footer
include vs render #
{% include "partial" %}shares the parent scope.{% render "partial" %}runs in an isolated scope, but accepts named parameters.
{% assign x = "A" %}
{% include "p" %}
{% render "p" %}
Named parameters:
{% render "p", title: "Hello", user: user %}
Options #
final engine = LiquidEngine(
options: const LiquidOptions(
strictVariables: false,
strictFilters: false,
cacheTemplates: true,
dateFormat: "%Y-%m-%d",
timezoneOffset: 0,
moneyFormat: r"${{amount}}",
maxRenderDepth: 50,
maxRenderSteps: 200000,
maxOutputSize: 5 * 1024 * 1024,
allowDrops: true,
),
);
Shopify URL resolvers #
final engine = LiquidEngine(
options: LiquidOptions(
assetUrlResolver: (k) => "https://cdn.example/assets/$k",
fileUrlResolver: (k) => "https://cdn.example/files/$k",
imageUrlResolver: (k, size) => "https://img.example/$size/$k",
),
);
Drops (Dart objects) #
liquid_dart supports objects via an explicit interface, without reflection.
class ProductDrop implements LiquidDrop {
final String title;
final int priceCents;
ProductDrop(this.title, this.priceCents);
@override
Object? get(String key) {
switch (key) {
case "title":
return title;
case "price_cents":
return priceCents;
case "full_title":
return () => "Product: $title"; // 0-arg callable
default:
return null;
}
}
}
Usage:
final out = await engine.parseAndRender(
"{{ p.full_title }}",
{"p": ProductDrop("Hat", 12345)},
);
Disabling:
final engine = LiquidEngine(options: const LiquidOptions(allowDrops: false));
Errors: location + snippet #
Errors can include:
- position
line:col - the affected line
- a caret
^pointing to the area
Examples:
- parse: unknown tag
- render: missing variable (in strict mode)
- render: unknown filter (in strict mode)
- include/render/layout: missing template (with position of the calling tag)
Supported features #
Tags #
assigncapture ... endcaptureif / elsif / else / endiffor / else / endforbreak,continuecycletablerow ... endtablerowincluderenderlayoutblock ... endblockraw ... endrawcomment ... endcommentliquid(multi-line,echo, statements separated by newline or;)
Loop variables:
forloop.index,forloop.index0,forloop.rindex,forloop.rindex0,forloop.first,forloop.last,forloop.lengthforloop.parentlooptablerowloop.index,tablerowloop.row,tablerowloop.col,tablerowloop.length
Whitespace control:
{{- ... -}}{%- ... -%}
Expressions #
- literals: string, number, bool, nil
- paths:
a.b.c - comparisons:
== != > >= < <= - booleans:
and,or,not - ranges:
(1..n) - filters via pipe:
{{ x | upcase | append: "!" }}
Filters #
Text:
upcase,downcase,capitalizeappend,prependstrip,lstrip,rstrip,strip_newlinesreplace,replace_first,remove,remove_firstsplit,joinslice(string)truncate,truncatewordsescape,strip_html,newline_to_brhandleizelink_tourl_encode,url_escape
Numeric:
plus,minus,times,divided_by,moduloabs,floor,ceil,round
Collections:
size(filter) and.size(property)first,last,reverse,compactmapwhere,rejectwhere_exp,reject_expsort,sort_naturaluniq(optionuniq: 'prop', stable)concatdig
Misc:
defaultjsondatemoneyasset_url,shopify_asset_url,file_url,img_url
Known limitations #
LiquidJS / Shopify compatibility:
- Shopify Liquid is very broad. This library covers a usable core, but not the entire Shopify runtime.
where_expandreject_expare a limited subset.dateaims for practical compatibility. Handling named timezones without a TZ database is not complete.img_urldefaults to applying a simple suffix before the extension. For CDN behavior, useimageUrlResolver.
Performance and security:
- Filters are synchronous.
- Render limits: depth, steps, output size.
- Template cache is enabled when
cacheTemplates: true.
API:
- In
0.x, the public API may change.
Roadmap (suggestion) #
- Common Shopify tags:
paginate,case/when - Advanced include compat:
include ... for,include ... with - Additional Shopify filters:
t,money_with_currency, etc. - Benchmarks and optimizations
Contributing #
PRs are welcome.
Recommended:
- Add features via tests first.
- Compatibility approach: test expected behaviors against LiquidJS / Shopify expectations.