liquify 1.5.0
liquify: ^1.5.0 copied to clipboard
A powerful and extensible Liquid template engine for Dart. Supports full Liquid syntax, custom tags and filters, and high-performance parsing and rendering.
Liquify - Liquid Template Engine for Dart #
Liquify is a comprehensive Dart implementation of the Liquid template language, originally created by Shopify. This high-performance library allows you to parse, render, and extend Liquid templates in your Dart and Flutter applications.
Features #
- Full support for standard Liquid syntax and semantics
- Synchronous and asynchronous rendering
- Custom delimiters (ERB-style, bracket-style, or your own)
- Environment-scoped filters and tags for security isolation
- Strict mode for security sandboxing
- Extensible architecture for custom tags and filters
- Template inheritance with layouts and blocks
- File system abstraction for template resolution
- High-performance parsing with caching
- Strong typing and null safety
Installation #
Add Liquify to your pubspec.yaml:
dependencies:
liquify: ^1.5.0
Then run dart pub get or flutter pub get.
Quick Start #
Basic Template Rendering #
import 'package:liquify/liquify.dart';
void main() {
final template = Template.parse(
'Hello, {{ name | upcase }}!',
data: {'name': 'world'},
);
print(template.render()); // Output: Hello, WORLD!
}
Using the Liquid Class #
The Liquid class provides a convenient API, especially when using custom delimiters:
final liquid = Liquid();
final result = liquid.renderString('Hello, {{ name }}!', {'name': 'World'});
print(result); // Output: Hello, World!
Async Rendering #
Use async rendering for templates with async filters or includes:
final result = await template.renderAsync();
Custom Delimiters #
Liquify supports custom delimiters for integrating with other template systems or avoiding conflicts with frontend frameworks.
ERB-Style Delimiters #
final liquid = Liquid(config: LiquidConfig.erb);
final result = liquid.renderString(
'<% if show %>Hello, <%= name %>!<% endif %>',
{'show': true, 'name': 'World'},
);
print(result); // Output: Hello, World!
Custom Delimiters #
final liquid = Liquid.withDelimiters(
tagStart: '[%',
tagEnd: '%]',
varStart: '[[',
varEnd: ']]',
);
final result = liquid.renderString(
'[% for item in items %][[ item ]] [% endfor %]',
{'items': ['a', 'b', 'c']},
);
print(result); // Output: a b c
Whitespace Control #
Whitespace stripping works with custom delimiters using the - marker:
final liquid = Liquid.withDelimiters(tagStart: '[%', tagEnd: '%]', varStart: '[[', varEnd: ']]');
final result = liquid.renderString('Hello [[- name ]]!', {'name': 'World'});
print(result); // Output: HelloWorld!
Template Inheritance #
Liquify supports template inheritance through the layout tag:
<!-- layouts/base.liquid -->
<!DOCTYPE html>
<html>
<head><title>{% block title %}Default{% endblock %}</title></head>
<body>{% block content %}{% endblock %}</body>
</html>
<!-- page.liquid -->
{% layout "layouts/base.liquid" %}
{% block title %}My Page{% endblock %}
{% block content %}<h1>Welcome!</h1>{% endblock %}
Variables can be passed to layouts:
{% layout "layouts/base.liquid", title: page_title, year: 2024 %}
File System and Template Resolution #
In-Memory Templates with MapRoot #
final fs = MapRoot({
'main.liquid': 'Hello {% render "partial.liquid" %}',
'partial.liquid': 'World!',
});
final template = Template.fromFile('main.liquid', fs);
print(template.render()); // Output: Hello World!
Custom Template Resolution #
Implement the Root class for custom template sources:
class DatabaseRoot extends Root {
@override
Future<String?> resolveAsync(String path) async {
return await database.getTemplate(path);
}
}
Security and Isolation #
Environment-Scoped Filters #
Register filters that are isolated to a specific template:
final template = Template.parse(
'{{ input | sanitize }}',
data: {'input': '<script>alert("xss")</script>'},
environmentSetup: (env) {
env.registerLocalFilter('sanitize', (value, args, namedArgs) =>
value.toString().replaceAll(RegExp(r'<[^>]*>'), ''));
},
);
print(template.render()); // Output: alert("xss")
Strict Mode #
Block access to global filters and tags for untrusted content:
final secureEnv = Environment.withStrictMode();
secureEnv.registerLocalFilter('safe_filter', myFilter);
final template = Template.parse(source, environment: secureEnv);
Environment Cloning #
Create child environments that inherit from a parent:
final baseEnv = Environment();
baseEnv.registerLocalFilter('base', baseFilter);
final childEnv = baseEnv.clone();
childEnv.registerLocalFilter('child', childFilter);
// childEnv has access to both 'base' and 'child' filters
Custom Tags and Filters #
Custom Filter #
FilterRegistry.register('reverse_words', (value, args, namedArgs) {
return value.toString().split(' ').reversed.join(' ');
});
// Usage: {{ "hello world" | reverse_words }} -> "world hello"
Custom Tag #
class ShoutTag extends AbstractTag with CustomTagParser {
ShoutTag(super.content, super.filters);
@override
dynamic evaluateWithContext(Evaluator evaluator, Buffer buffer) {
final text = evaluator.evaluate(content.first)?.toString() ?? '';
buffer.write(text.toUpperCase());
}
@override
Parser parser([LiquidConfig? config]) {
return (createTagStart(config) &
string('shout').trim() &
ref0(expression).trim() &
createTagEnd(config))
.map((values) => Tag('shout', [values[2] as ASTNode]));
}
}
TagRegistry.register('shout', (content, filters) => ShoutTag(content, filters));
// Usage: {% shout "hello" %} -> HELLO
For more examples, see the example directory.
API Documentation #
Full API documentation is available at pub.dev.
Contributing #
Contributions are welcome! Please open an issue first for major changes.
License #
This project is licensed under the MIT License.
Acknowledgements #
- Shopify for the original Liquid template language
- LiquidJS for their comprehensive filter implementations
- liquid_dart for initial Dart implementation inspiration