Liquify - Liquid Template Engine for Dart

Liquify Logo

GitHub release Pub Version GitHub Actions Workflow Status License issues - liquify

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

Libraries

liquify
Liquify: Core components for Liquid template parsing and rendering in Dart
parser
Liquify Parser: Core parsing, tag management, and registry for Liquid templates