mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-14 16:30:40 -07:00
Compare commits
90
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70ddccd88f | ||
|
|
6bc76a92ea | ||
|
|
74f478679f | ||
|
|
7b50ceb955 | ||
|
|
7fe3bfcb98 | ||
|
|
4aacf3f9a1 | ||
|
|
ea16fe9b24 | ||
|
|
f0883806c4 | ||
|
|
1aa4d93160 | ||
|
|
f5391d0621 | ||
|
|
4d1378b4b3 | ||
|
|
2790e8c339 | ||
|
|
1579545be2 | ||
|
|
f49b990e94 | ||
|
|
66b98ca025 | ||
|
|
92f9636ebe | ||
|
|
74eb3418b6 | ||
|
|
6ef9c176dd | ||
|
|
6bf836b719 | ||
|
|
1ce90ed355 | ||
|
|
90825277d3 | ||
|
|
95dc90e503 | ||
|
|
def9795c8f | ||
|
|
1e769d9263 | ||
|
|
c51257692d | ||
|
|
a33a127f81 | ||
|
|
1504e5ad89 | ||
|
|
a55d638690 | ||
|
|
88e774cb40 | ||
|
|
8e3dcf49ba | ||
|
|
f0c7a601c3 | ||
|
|
ce1d460d8a | ||
|
|
fc80f30981 | ||
|
|
ffe3ee3754 | ||
|
|
0147832a64 | ||
|
|
f2a1925033 | ||
|
|
d63213d463 | ||
|
|
68e39b624a | ||
|
|
ea54777dd2 | ||
|
|
34def78926 | ||
|
|
c744422d0e | ||
|
|
67b80e35f9 | ||
|
|
57ccbd4898 | ||
|
|
d4c1429ea7 | ||
|
|
ada2a87d8c | ||
|
|
6028ddb189 | ||
|
|
d9b2d50045 | ||
|
|
7f550c8bce | ||
|
|
a3ceafbcd0 | ||
|
|
d9a08801c5 | ||
|
|
0ca571f58c | ||
|
|
82249c99b3 | ||
|
|
ae3d14bef5 | ||
|
|
ec38c46ff6 | ||
|
|
13532f7768 | ||
|
|
0359e54999 | ||
|
|
b899c61bbc | ||
|
|
1aeb8a3305 | ||
|
|
99db066e2a | ||
|
|
2937f1988b | ||
|
|
d65511e328 | ||
|
|
601e49dc55 | ||
|
|
2232f9157c | ||
|
|
c09cb6e05a | ||
|
|
fe0809e2fa | ||
|
|
f21311b946 | ||
|
|
adff96b4cf | ||
|
|
43c2aab84b | ||
|
|
dc7cd7704f | ||
|
|
57f83933fd | ||
|
|
48b5f59b8e | ||
|
|
1b5ff4c86f | ||
|
|
963cd58ab6 | ||
|
|
f302fce89e | ||
|
|
5342f6f55a | ||
|
|
5f1624d6f5 | ||
|
|
447dd346c1 | ||
|
|
26f12734f8 | ||
|
|
67865368aa | ||
|
|
1fad0e2638 | ||
|
|
fe4253f872 | ||
|
|
a8dc349f01 | ||
|
|
79ec1506de | ||
|
|
e661b5a275 | ||
|
|
955a1fae98 | ||
|
|
ecda8ff57b | ||
|
|
ba56db81e1 | ||
|
|
8de5aa2fb4 | ||
|
|
64b297a180 | ||
|
|
0239efd826 |
@@ -1,22 +0,0 @@
|
||||
name: Contributor License Agreement (CLA)
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
cla:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event.issue.pull_request
|
||||
&& !github.event.issue.pull_request.merged_at
|
||||
&& contains(github.event.comment.body, 'signed')
|
||||
)
|
||||
|| (github.event.pull_request && !github.event.pull_request.merged)
|
||||
steps:
|
||||
- uses: Shopify/shopify-cla-action@v1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
cla-token: ${{ secrets.CLA_TOKEN }}
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Liquid
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
BUNDLE_JOBS: 4
|
||||
BUNDLE_RETRY: 3
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
entry:
|
||||
- { ruby: 3.0, allowed-failure: false } # minimum supported
|
||||
- { ruby: 3.2, allowed-failure: false }
|
||||
- { ruby: 3.3, allowed-failure: false }
|
||||
- { ruby: 3.3, allowed-failure: false }
|
||||
- { ruby: 3.4, allowed-failure: false } # latest
|
||||
- {
|
||||
ruby: 3.4,
|
||||
allowed-failure: false,
|
||||
rubyopt: "--enable-frozen-string-literal",
|
||||
}
|
||||
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
|
||||
- { ruby: ruby-head, allowed-failure: false }
|
||||
- {
|
||||
ruby: ruby-head,
|
||||
allowed-failure: false,
|
||||
rubyopt: "--enable-frozen-string-literal",
|
||||
}
|
||||
- { ruby: ruby-head, allowed-failure: false, rubyopt: "--yjit" }
|
||||
name: Test Ruby ${{ matrix.entry.ruby }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: ${{ matrix.entry.ruby }}
|
||||
bundler-cache: true
|
||||
bundler: latest
|
||||
- run: bundle exec rake
|
||||
continue-on-error: ${{ matrix.entry.allowed-failure }}
|
||||
env:
|
||||
RUBYOPT: ${{ matrix.entry.rubyopt }}
|
||||
|
||||
memory_profile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
- run: bundle exec rake memory_profile:run
|
||||
+1
-9
@@ -1,9 +1 @@
|
||||
*~
|
||||
*.gem
|
||||
*.swp
|
||||
pkg
|
||||
*.rbc
|
||||
.rvmrc
|
||||
.bundle
|
||||
.byebug_history
|
||||
Gemfile.lock
|
||||
_site
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
inherit_gem:
|
||||
rubocop-shopify: rubocop.yml
|
||||
|
||||
inherit_from:
|
||||
- .rubocop_todo.yml
|
||||
|
||||
require: rubocop-performance
|
||||
|
||||
Performance:
|
||||
Enabled: true
|
||||
|
||||
AllCops:
|
||||
NewCops: disable
|
||||
SuggestExtensions: false
|
||||
Exclude:
|
||||
- 'vendor/bundle/**/*'
|
||||
|
||||
Naming/MethodName:
|
||||
Exclude:
|
||||
- 'example/server/liquid_servlet.rb'
|
||||
|
||||
Style/ClassMethodsDefinitions:
|
||||
Enabled: false
|
||||
|
||||
# liquid filter calls were being mistaken to be calls on arrays
|
||||
Style/ConcatArrayLiterals:
|
||||
Exclude:
|
||||
- 'test/integration/standard_filter_test.rb'
|
||||
@@ -1,180 +0,0 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config`
|
||||
# on 2022-05-18 19:25:47 UTC using RuboCop version 1.29.1.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
|
||||
# Include: **/*.gemspec
|
||||
Gemspec/OrderedDependencies:
|
||||
Exclude:
|
||||
- 'liquid.gemspec'
|
||||
|
||||
# Offense count: 6
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
Layout/ClosingHeredocIndentation:
|
||||
Exclude:
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
|
||||
# Offense count: 34
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
Layout/EmptyLineAfterGuardClause:
|
||||
Exclude:
|
||||
- 'lib/liquid/block.rb'
|
||||
- 'lib/liquid/block_body.rb'
|
||||
- 'lib/liquid/context.rb'
|
||||
- 'lib/liquid/drop.rb'
|
||||
- 'lib/liquid/lexer.rb'
|
||||
- 'lib/liquid/parser.rb'
|
||||
- 'lib/liquid/profiler/hooks.rb'
|
||||
- 'lib/liquid/standardfilters.rb'
|
||||
- 'lib/liquid/tags/for.rb'
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'lib/liquid/utils.rb'
|
||||
- 'lib/liquid/variable.rb'
|
||||
- 'lib/liquid/variable_lookup.rb'
|
||||
- 'performance/shopify/money_filter.rb'
|
||||
- 'performance/shopify/paginate.rb'
|
||||
|
||||
# Offense count: 8
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: AllowAliasSyntax, AllowedMethods.
|
||||
# AllowedMethods: alias_method, public, protected, private
|
||||
Layout/EmptyLinesAroundAttributeAccessor:
|
||||
Exclude:
|
||||
- 'lib/liquid/template.rb'
|
||||
- 'test/integration/filter_test.rb'
|
||||
- 'test/integration/tags/include_tag_test.rb'
|
||||
- 'test/unit/strainer_template_unit_test.rb'
|
||||
|
||||
# Offense count: 17
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
# SupportedStyles: aligned, indented
|
||||
Layout/LineEndStringConcatenationIndentation:
|
||||
Exclude:
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
- 'test/integration/tags/increment_tag_test.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
# SupportedStyles: aligned, indented
|
||||
Layout/MultilineOperationIndentation:
|
||||
Exclude:
|
||||
- 'lib/liquid/expression.rb'
|
||||
|
||||
# Offense count: 9
|
||||
Lint/MissingSuper:
|
||||
Exclude:
|
||||
- 'lib/liquid/forloop_drop.rb'
|
||||
- 'lib/liquid/tablerowloop_drop.rb'
|
||||
- 'test/integration/assign_test.rb'
|
||||
- 'test/integration/context_test.rb'
|
||||
- 'test/integration/filter_test.rb'
|
||||
- 'test/integration/standard_filter_test.rb'
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
- 'test/integration/tags/table_row_test.rb'
|
||||
|
||||
# Offense count: 44
|
||||
Naming/ConstantName:
|
||||
Exclude:
|
||||
- 'lib/liquid.rb'
|
||||
- 'lib/liquid/block_body.rb'
|
||||
- 'lib/liquid/tags/assign.rb'
|
||||
- 'lib/liquid/tags/capture.rb'
|
||||
- 'lib/liquid/tags/case.rb'
|
||||
- 'lib/liquid/tags/cycle.rb'
|
||||
- 'lib/liquid/tags/for.rb'
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'lib/liquid/tags/raw.rb'
|
||||
- 'lib/liquid/tags/table_row.rb'
|
||||
- 'lib/liquid/variable.rb'
|
||||
- 'performance/shopify/comment_form.rb'
|
||||
- 'performance/shopify/paginate.rb'
|
||||
- 'test/integration/tags/include_tag_test.rb'
|
||||
|
||||
# Offense count: 9
|
||||
# Configuration parameters: CheckIdentifiers, CheckConstants, CheckVariables, CheckStrings, CheckSymbols, CheckComments, CheckFilepaths, FlaggedTerms.
|
||||
Naming/InclusiveLanguage:
|
||||
Exclude:
|
||||
- 'lib/liquid/drop.rb'
|
||||
- 'lib/liquid/parse_context.rb'
|
||||
- 'test/integration/drop_test.rb'
|
||||
- 'test/integration/tags/if_else_tag_test.rb'
|
||||
|
||||
# Offense count: 2
|
||||
Style/ClassVars:
|
||||
Exclude:
|
||||
- 'lib/liquid/condition.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
Style/ExplicitBlockArgument:
|
||||
Exclude:
|
||||
- 'test/integration/context_test.rb'
|
||||
- 'test/integration/tag/disableable_test.rb'
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
|
||||
# Offense count: 2982
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
|
||||
# SupportedStyles: single_quotes, double_quotes
|
||||
Style/StringLiterals:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 20
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: single_quotes, double_quotes
|
||||
Style/StringLiteralsInInterpolation:
|
||||
Exclude:
|
||||
- 'lib/liquid/condition.rb'
|
||||
- 'lib/liquid/strainer_template.rb'
|
||||
- 'lib/liquid/tag/disableable.rb'
|
||||
- 'performance/shopify/shop_filter.rb'
|
||||
- 'performance/shopify/tag_filter.rb'
|
||||
|
||||
# Offense count: 6
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyleForMultiline.
|
||||
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
||||
Style/TrailingCommaInArrayLiteral:
|
||||
Exclude:
|
||||
- 'example/server/example_servlet.rb'
|
||||
- 'lib/liquid/condition.rb'
|
||||
- 'test/integration/context_test.rb'
|
||||
- 'test/integration/standard_filter_test.rb'
|
||||
- 'test/unit/parse_tree_visitor_test.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyleForMultiline.
|
||||
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
||||
Style/TrailingCommaInHashLiteral:
|
||||
Exclude:
|
||||
- 'lib/liquid/expression.rb'
|
||||
|
||||
# Offense count: 19
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: EnforcedStyle, MinSize, WordRegex.
|
||||
# SupportedStyles: percent, brackets
|
||||
Style/WordArray:
|
||||
Exclude:
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'liquid.gemspec'
|
||||
- 'test/integration/assign_test.rb'
|
||||
- 'test/integration/context_test.rb'
|
||||
- 'test/integration/drop_test.rb'
|
||||
- 'test/integration/standard_filter_test.rb'
|
||||
|
||||
# Offense count: 117
|
||||
# This cop supports safe auto-correction (--auto-correct).
|
||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
|
||||
# URISchemes: http, https
|
||||
Layout/LineLength:
|
||||
Max: 260
|
||||
@@ -1 +0,0 @@
|
||||
3.4.1
|
||||
@@ -1,36 +0,0 @@
|
||||
# How to contribute
|
||||
|
||||
## Things we will merge
|
||||
|
||||
* Bugfixes
|
||||
* Performance improvements
|
||||
* Features that are likely to be useful to the majority of Liquid users
|
||||
* Documentation updates that are concise and likely to be useful to the majority of Liquid users
|
||||
|
||||
## Things we won't merge
|
||||
|
||||
* Code that introduces considerable performance degrations
|
||||
* Code that touches performance-critical parts of Liquid and comes without benchmarks
|
||||
* Features that are not important for most people (we want to keep the core Liquid code small and tidy)
|
||||
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
|
||||
* Code that does not include tests
|
||||
* Code that breaks existing tests
|
||||
* Documentation changes that are verbose, incorrect or not important to most people (we want to keep it simple and easy to understand)
|
||||
|
||||
## Workflow
|
||||
|
||||
* [Sign the CLA](https://cla.shopify.com/) if you haven't already
|
||||
* Fork the Liquid repository
|
||||
* Create a new branch in your fork
|
||||
* For updating [Liquid documentation](https://shopify.github.io/liquid/), create it from `gh-pages` branch. (You can skip tests.)
|
||||
* If it makes sense, add tests for your code and/or run a performance benchmark
|
||||
* Make sure all tests pass (`bundle exec rake`)
|
||||
* Create a pull request
|
||||
|
||||
## Releasing
|
||||
|
||||
* Bump the version in `lib/liquid/version.rb`
|
||||
* Update the `History.md` file
|
||||
* Open a PR like [this one](https://github.com/Shopify/liquid/pull/1894) and merge it to `main`
|
||||
* Create a new release using the [GitHub UI](https://github.com/Shopify/liquid/releases/new)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
git_source(:github) do |repo_name|
|
||||
"https://github.com/#{repo_name}.git"
|
||||
end
|
||||
|
||||
gemspec
|
||||
|
||||
gem "base64"
|
||||
|
||||
group :benchmark, :test do
|
||||
gem 'benchmark-ips'
|
||||
gem 'memory_profiler'
|
||||
gem 'terminal-table'
|
||||
gem "lru_redux"
|
||||
|
||||
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
|
||||
gem 'stackprof'
|
||||
end
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem "webrick"
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 1.61.0'
|
||||
gem 'rubocop-shopify', '~> 2.12.0', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
end
|
||||
-452
@@ -1,452 +0,0 @@
|
||||
# Liquid Change Log
|
||||
|
||||
## 5.8.1 (unreleased)
|
||||
|
||||
## 5.8.1
|
||||
|
||||
* Fix `{% doc %}` tag to be visitable [Guilherme Carreiro]
|
||||
|
||||
## 5.8.0
|
||||
|
||||
* Introduce the new `{% doc %}` tag [Guilherme Carreiro]
|
||||
|
||||
## 5.7.3
|
||||
|
||||
* Raise Liquid::SyntaxError when parsing invalidly encoded strings [Chris AtLee]
|
||||
|
||||
## 5.7.2 2025-01-31
|
||||
|
||||
* Fix array filters to not support nested properties [Guilherme Carreiro]
|
||||
|
||||
## 5.7.1 2025-01-24
|
||||
|
||||
* Fix the `find` and `find_index`filters to return `nil` when filtering empty arrays [Guilherme Carreiro]
|
||||
* Fix the `has` filter to return `false` when filtering empty arrays [Guilherme Carreiro]
|
||||
|
||||
## 5.7.0 2025-01-16
|
||||
|
||||
### Features
|
||||
|
||||
* Add `find`, `find_index`, `has`, and `reject` filters to arrays [Guilherme Carreiro]
|
||||
* Compatibility with Ruby 3.4 [Ian Ker-Seymer]
|
||||
|
||||
## 5.6.4 2025-01-14
|
||||
|
||||
### Fixes
|
||||
* Add a default `string_scanner` to avoid errors with `Liquid::VariableLookup.parse("foo.bar")` [Ian Ker-Seymer]
|
||||
|
||||
## 5.6.3 2025-01-13
|
||||
* Remove `lru_redux` dependency [Michael Go]
|
||||
|
||||
## 5.6.2 2025-01-13
|
||||
|
||||
### Fixes
|
||||
* Preserve the old behavior of requiring floats to start with a digit [Michael Go]
|
||||
|
||||
## 5.6.1 2025-01-13
|
||||
|
||||
### Performance improvements
|
||||
* Faster Expression parser / Tokenizer with StringScanner [Michael Go]
|
||||
|
||||
## 5.6.0 2024-12-19
|
||||
|
||||
### Architectural changes
|
||||
* Added new `Environment` class to manage configuration and state that was previously stored in `Template` [Ian Ker-Seymer]
|
||||
* Moved tag registration from `Template` to `Environment` [Ian Ker-Seymer]
|
||||
* Removed `StrainerFactory` in favor of `Environment`-based strainer creation [Ian Ker-Seymer]
|
||||
* Consolidated standard tags into a new `Tags` module with `STANDARD_TAGS` constant [Ian Ker-Seymer]
|
||||
|
||||
### Performance improvements
|
||||
* Optimized `Lexer` with a new `Lexer2` implementation using jump tables for faster tokenization, requires Ruby 3.4 [Ian Ker-Seymer]
|
||||
* Improved variable rendering with specialized handling for different types [Michael Go]
|
||||
* Reduced array allocations by using frozen empty constants [Michael Go]
|
||||
|
||||
### API changes
|
||||
* Deprecated several `Template` class methods in favor of `Environment` methods [Ian Ker-Seymer]
|
||||
* Added deprecation warnings system [Ian Ker-Seymer]
|
||||
* Changed how filters and tags are registered to use Environment [Ian Ker-Seymer]
|
||||
|
||||
### Fixes
|
||||
* Fixed table row handling of break interrupts [Alex Coco]
|
||||
* Improved variable output handling for arrays [Ian Ker-Seymer]
|
||||
* Fix Tokenizer to handle null source value (#1873) [Bahar Pourazar]
|
||||
|
||||
## 5.5.0 2024-03-21
|
||||
|
||||
Please reference the GitHub release for more information.
|
||||
|
||||
## 5.4.0 2022-07-29
|
||||
|
||||
### Breaking Changes
|
||||
* Drop support for end-of-life Ruby versions (2.5 and 2.6) (#1578) [Andy Waite]
|
||||
|
||||
### Features
|
||||
* Allow `#` to be used as an inline comment tag (#1498) [CP Clermont]
|
||||
|
||||
### Fixes
|
||||
* `PartialCache` now shares snippet cache with subcontexts by default (#1553) [Chris AtLee]
|
||||
* Hash registers no longer leak into subcontexts as static registers (#1564) [Chris AtLee]
|
||||
* Fix `ParseTreeVisitor` for `with` variable expressions in `Render` tag (#1596) [CP Clermont]
|
||||
|
||||
### Changed
|
||||
* Liquid::Context#registers now always returns a Liquid::Registers object, though supports the most used Hash functions for compatibility (#1553)
|
||||
|
||||
## 5.3.0 2022-03-22
|
||||
|
||||
### Fixes
|
||||
* StandardFilter: Fix missing @context on iterations (#1525) [Thierry Joyal]
|
||||
* Fix warning about block and default value in `static_registers.rb` (#1531) [Peter Zhu]
|
||||
|
||||
### Deprecation
|
||||
* Condition#evaluate to require mandatory context argument in Liquid 6.0.0 (#1527) [Thierry Joyal]
|
||||
|
||||
## 5.2.0 2022-03-01
|
||||
|
||||
### Features
|
||||
* Add `remove_last`, and `replace_last` filters (#1422) [Anders Hagbard]
|
||||
* Eagerly cache global filters (#1524) [Jean Boussier]
|
||||
|
||||
### Fixes
|
||||
* Fix some internal errors in filters from invalid input (#1476) [Dylan Thacker-Smith]
|
||||
* Allow dash in filter kwarg name for consistency with Liquid::C (#1518) [CP Clermont]
|
||||
|
||||
## 5.1.0 / 2021-09-09
|
||||
|
||||
### Features
|
||||
* Add `base64_encode`, `base64_decode`, `base64_url_safe_encode`, and `base64_url_safe_decode` filters (#1450) [Daniel Insley]
|
||||
* Introduce `to_liquid_value` in `Liquid::Drop` (#1441) [Michael Go]
|
||||
|
||||
### Fixes
|
||||
* Fix support for using a String subclass for the liquid source (#1421) [Dylan Thacker-Smith]
|
||||
* Add `ParseTreeVisitor` to `RangeLookup` (#1470) [CP Clermont]
|
||||
* Translate `RangeError` to `Liquid::Error` for `truncatewords` with large int (#1431) [Dylan Thacker-Smith]
|
||||
|
||||
## 5.0.1 / 2021-03-24
|
||||
|
||||
### Fixes
|
||||
* Add ParseTreeVisitor to Echo tag (#1414) [CP Clermont]
|
||||
* Test with ruby 3.0 as the latest ruby version (#1398) [Dylan Thacker-Smith]
|
||||
* Handle carriage return in newlines_to_br (#1391) [Unending]
|
||||
|
||||
### Performance Improvements
|
||||
* Use split limit in truncatewords (#1361) [Dylan Thacker-Smith]
|
||||
|
||||
## 5.0.0 / 2021-01-06
|
||||
|
||||
### Features
|
||||
* Add new `{% render %}` tag (#1122) [Samuel Doiron]
|
||||
* Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell]
|
||||
* Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li]
|
||||
* Add [usage tracking](README.md#usage-tracking) [Mike Angell]
|
||||
* Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell]
|
||||
* Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith]
|
||||
|
||||
### Fixes
|
||||
* Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith]
|
||||
* Make sure the for tag's limit and offset are integers (#1094) [David Cornu]
|
||||
* Invokable methods for enumerable reject include (#1151) [Thierry Joyal]
|
||||
* Allow `default` filter to handle `false` as value (#1144) [Mike Angell]
|
||||
* Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith]
|
||||
* Fix duplication of text in raw tags (#1304) [Peter Zhu]
|
||||
* Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith]
|
||||
* Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith]
|
||||
|
||||
### Breaking Changes
|
||||
* Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith]
|
||||
* Remove support for taint checking (#1268) [Dylan Thacker-Smith]
|
||||
* Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal]
|
||||
* Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
|
||||
* Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
|
||||
* Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
|
||||
* And several internal changes
|
||||
|
||||
### Performance Improvements
|
||||
* Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
|
||||
* Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
|
||||
|
||||
## 4.0.3 / 2019-03-12
|
||||
|
||||
### Fixed
|
||||
* Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
|
||||
|
||||
## 4.0.2 / 2019-03-08
|
||||
|
||||
### Changed
|
||||
* Add `where` filter (#1026) [Samuel Doiron]
|
||||
* Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber]
|
||||
* Improve `strip_html` performance (#1032) [printercu]
|
||||
|
||||
### Fixed
|
||||
* Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang]
|
||||
* Validate the character encoding in url_decode (#1070) [Clayton Smith]
|
||||
|
||||
## 4.0.1 / 2018-10-09
|
||||
|
||||
### Changed
|
||||
* Add benchmark group in Gemfile (#855) [Jerry Liu]
|
||||
* Allow benchmarks to benchmark render by itself (#851) [Jerry Liu]
|
||||
* Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith]
|
||||
* Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith]
|
||||
* Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield]
|
||||
* Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith]
|
||||
* Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith]
|
||||
* Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith]
|
||||
* Remove Spy Gem (#896) [Dylan Thacker-Smith]
|
||||
* Add `collection_name` and `variable_name` reader to `For` block (#909)
|
||||
* Symbols render as strings (#920) [Justin Li]
|
||||
* Remove default value from Hash objects (#932) [Maxime Bedard]
|
||||
* Remove one level of nesting (#944) [Dylan Thacker-Smith]
|
||||
* Update Rubocop version (#952) [Justin Li]
|
||||
* Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal]
|
||||
* Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith]
|
||||
* Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith]
|
||||
* Add tests against Ruby 2.4 (#963) and 2.5 (#981)
|
||||
* Replace RegExp literals with constants (#988) [Ashwin Maroli]
|
||||
* Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli]
|
||||
* Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith]
|
||||
* Refactor and optimize rendering (#1005) [Christopher Aue]
|
||||
* Add installation instruction (#1006) [Ben Gift]
|
||||
* Remove Circle CI (#1010)
|
||||
* Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO]
|
||||
* Rename deprecated Rubocop name (#1027) [Justin Li]
|
||||
|
||||
### Fixed
|
||||
* Handle `join` filter on non String joiners (#857) [Richard Monette]
|
||||
* Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861)
|
||||
* Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal]
|
||||
* Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz]
|
||||
* Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan]
|
||||
|
||||
## 4.0.0 / 2016-12-14 / branch "4-0-stable"
|
||||
|
||||
### Changed
|
||||
* Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
|
||||
* Ruby 2.0 support dropped (#832) [Dylan Thacker-Smith]
|
||||
* Add to_number Drop method to allow custom drops to work with number filters (#731)
|
||||
* Add strict_variables and strict_filters options to detect undefined references (#691)
|
||||
* Improve loop performance (#681) [Florian Weingarten]
|
||||
* Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal]
|
||||
* Add url_decode filter to invert url_encode (#645) [Larry Archer]
|
||||
* Add global_filter to apply a filter to all output (#610) [Loren Hale]
|
||||
* Add compact filter (#600) [Carson Reinke]
|
||||
* Rename deprecated "has_key?" and "has_interrupt?" methods (#593) [Florian Weingarten]
|
||||
* Include template name with line numbers in render errors (574) [Dylan Thacker-Smith]
|
||||
* Add sort_natural filter (#554) [Martin Hanzel]
|
||||
* Add forloop.parentloop as a reference to the parent loop (#520) [Justin Li]
|
||||
* Block parsing moved to BlockBody class (#458) [Dylan Thacker-Smith]
|
||||
* Add concat filter to concatenate arrays (#429) [Diogo Beato]
|
||||
* Ruby 1.9 support dropped (#491) [Justin Li]
|
||||
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
|
||||
* Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
|
||||
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
|
||||
* Fix include tag used with strict_variables (#828) [QuickPay]
|
||||
* Fix map filter when value is a Proc (#672) [Guillaume Malette]
|
||||
* Fix truncate filter when value is not a string (#672) [Guillaume Malette]
|
||||
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
|
||||
* Fix sort filter behaviour with empty array input (#652) [Marcel Cary]
|
||||
* Fix test failure under certain timezones (#631) [Dylan Thacker-Smith]
|
||||
* Fix bug in uniq filter (#595) [Florian Weingarten]
|
||||
* Fix bug when "blank" and "empty" are used as variable names (#592) [Florian Weingarten]
|
||||
* Fix condition parse order in strict mode (#569) [Justin Li]
|
||||
* Fix naming of the "context variable" when dynamically including a template (#559) [Justin Li]
|
||||
* Gracefully accept empty strings in the date filter (#555) [Loren Hale]
|
||||
* Fix capturing into variables with a hyphen in the name (#505) [Florian Weingarten]
|
||||
* Fix case sensitivity regression in date standard filter (#499) [Kelley Reynolds]
|
||||
* Disallow filters with no variable in strict mode (#475) [Justin Li]
|
||||
* Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li]
|
||||
* Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li]
|
||||
|
||||
## 3.0.5 / 2015-07-23 / branch "3-0-stable"
|
||||
|
||||
* Fix test failure under certain timezones [Dylan Thacker-Smith]
|
||||
|
||||
## 3.0.4 / 2015-07-17
|
||||
|
||||
* Fix chained access to multi-dimensional hashes [Florian Weingarten]
|
||||
|
||||
## 3.0.3 / 2015-05-28
|
||||
|
||||
* Fix condition parse order in strict mode (#569) [Justin Li]
|
||||
|
||||
## 3.0.2 / 2015-04-24
|
||||
|
||||
* Expose VariableLookup private members (#551) [Justin Li]
|
||||
* Documentation fixes
|
||||
|
||||
## 3.0.1 / 2015-01-23
|
||||
|
||||
* Remove duplicate `index0` key in TableRow tag (#502) [Alfred Xing]
|
||||
|
||||
## 3.0.0 / 2014-11-12
|
||||
|
||||
* Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith]
|
||||
* Fixed condition with wrong data types (#423) [Bogdan Gusiev]
|
||||
* Add url_encode to standard filters (#421) [Derrick Reimer]
|
||||
* Add uniq to standard filters [Florian Weingarten]
|
||||
* Add exception_handler feature (#397) and #254 [Bogdan Gusiev, Florian Weingarten]
|
||||
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge]
|
||||
* Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge]
|
||||
* Properly set context rethrow_errors on render! #349 [Thierry Joyal]
|
||||
* Fix broken rendering of variables which are equal to false (#345) [Florian Weingarten]
|
||||
* Remove ActionView template handler [Dylan Thacker-Smith]
|
||||
* Freeze lots of string literals for new Ruby 2.1 optimization (#297) [Florian Weingarten]
|
||||
* Allow newlines in tags and variables (#324) [Dylan Thacker-Smith]
|
||||
* Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith]
|
||||
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev]
|
||||
* Add a to_s default for liquid drops (#306) [Adam Doeler]
|
||||
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten]
|
||||
* Make if, for & case tags return complete and consistent nodelists (#250) [Nick Jones]
|
||||
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
|
||||
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
|
||||
* Fix resource counting bug with respond_to?(:length) (#263) [Florian Weingarten]
|
||||
* Allow specifying custom patterns for template filenames (#284) [Andrei Gladkyi]
|
||||
* Allow drops to optimize loading a slice of elements (#282) [Tom Burns]
|
||||
* Support for passing variables to snippets in subdirs (#271) [Joost Hietbrink]
|
||||
* Add a class cache to avoid runtime extend calls (#249) [James Tucker]
|
||||
* Remove some legacy Ruby 1.8 compatibility code (#276) [Florian Weingarten]
|
||||
* Add default filter to standard filters (#267) [Derrick Reimer]
|
||||
* Add optional strict parsing and warn parsing (#235) [Tristan Hume]
|
||||
* Add I18n syntax error translation (#241) [Simon Hørup Eskildsen, Sirupsen]
|
||||
* Make sort filter work on enumerable drops (#239) [Florian Weingarten]
|
||||
* Fix clashing method names in enumerable drops (#238) [Florian Weingarten]
|
||||
* Make map filter work on enumerable drops (#233) [Florian Weingarten]
|
||||
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten]
|
||||
|
||||
## 2.6.3 / 2015-07-23 / branch "2-6-stable"
|
||||
|
||||
* Fix test failure under certain timezones [Dylan Thacker-Smith]
|
||||
|
||||
## 2.6.2 / 2015-01-23
|
||||
|
||||
* Remove duplicate hash key [Parker Moore]
|
||||
|
||||
## 2.6.1 / 2014-01-10
|
||||
|
||||
Security fix, cherry-picked from master (4e14a65):
|
||||
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
|
||||
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
|
||||
|
||||
## 2.6.0 / 2013-11-25
|
||||
|
||||
IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains explicit Ruby 1.8 compatability.
|
||||
The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8.
|
||||
|
||||
* Bugfix for #106: fix example servlet [gnowoel]
|
||||
* Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss]
|
||||
* Bugfix for #114: strip_html filter supports style tags [James Allardice]
|
||||
* Bugfix for #117: 'now' support for date filter in Ruby 1.9 [Notre Dame Webgroup]
|
||||
* Bugfix for #166: truncate filter on UTF-8 strings with Ruby 1.8 [Florian Weingarten]
|
||||
* Bugfix for #204: 'raw' parsing bug [Florian Weingarten]
|
||||
* Bugfix for #150: 'for' parsing bug [Peter Schröder]
|
||||
* Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder]
|
||||
* Bugfix for #174, "can't convert Fixnum into String" for "replace" [jsw0528]
|
||||
* Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep]
|
||||
* Resource limits [Florian Weingarten]
|
||||
* Add reverse filter [Jay Strybis]
|
||||
* Add utf-8 support
|
||||
* Use array instead of Hash to keep the registered filters [Tasos Stathopoulos]
|
||||
* Cache tokenized partial templates [Tom Burns]
|
||||
* Avoid warnings in Ruby 1.9.3 [Marcus Stollsteimer]
|
||||
* Better documentation for 'include' tag (closes #163) [Peter Schröder]
|
||||
* Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves]
|
||||
|
||||
## 2.5.5 / 2014-01-10 / branch "2-5-stable"
|
||||
|
||||
Security fix, cherry-picked from master (4e14a65):
|
||||
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
|
||||
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
|
||||
|
||||
## 2.5.4 / 2013-11-11
|
||||
|
||||
* Fix "can't convert Fixnum into String" for "replace" (#173), [jsw0528]
|
||||
|
||||
## 2.5.3 / 2013-10-09
|
||||
|
||||
* #232, #234, #237: Fix map filter bugs [Florian Weingarten]
|
||||
|
||||
## 2.5.2 / 2013-09-03 / deleted
|
||||
|
||||
Yanked from rubygems, as it contained too many changes that broke compatibility. Those changes will be on following major releases.
|
||||
|
||||
## 2.5.1 / 2013-07-24
|
||||
|
||||
* #230: Fix security issue with map filter, Use invoke_drop in map filter [Florian Weingarten]
|
||||
|
||||
## 2.5.0 / 2013-03-06
|
||||
|
||||
* Prevent Object methods from being called on drops
|
||||
* Avoid symbol injection from liquid
|
||||
* Added break and continue statements
|
||||
* Fix filter parser for args without space separators
|
||||
* Add support for filter keyword arguments
|
||||
|
||||
|
||||
## 2.4.0 / 2012-08-03
|
||||
|
||||
* Performance improvements
|
||||
* Allow filters in `assign`
|
||||
* Add `modulo` filter
|
||||
* Ruby 1.8, 1.9, and Rubinius compatibility fixes
|
||||
* Add support for `quoted['references']` in `tablerow`
|
||||
* Add support for Enumerable to `tablerow`
|
||||
* `strip_html` filter removes html comments
|
||||
|
||||
|
||||
## 2.3.0 / 2011-10-16
|
||||
|
||||
* Several speed/memory improvements
|
||||
* Numerous bug fixes
|
||||
* Added support for MRI 1.9, Rubinius, and JRuby
|
||||
* Added support for integer drop parameters
|
||||
* Added epoch support to `date` filter
|
||||
* New `raw` tag that suppresses parsing
|
||||
* Added `else` option to `for` tag
|
||||
* New `increment` tag
|
||||
* New `split` filter
|
||||
|
||||
|
||||
## 2.2.1 / 2010-08-23
|
||||
|
||||
* Added support for literal tags
|
||||
|
||||
|
||||
## 2.2.0 / 2010-08-22
|
||||
|
||||
* Compatible with Ruby 1.8.7, 1.9.1 and 1.9.2-p0
|
||||
* Merged some changed made by the community
|
||||
|
||||
|
||||
## 1.9.0 / 2008-03-04
|
||||
|
||||
* Fixed gem install rake task
|
||||
* Improve Error encapsulation in liquid by maintaining a own set of exceptions instead of relying on ruby build ins
|
||||
|
||||
|
||||
## Before 1.9.0
|
||||
|
||||
* Added If with or / and expressions
|
||||
* Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods.
|
||||
* Added more tags to standard library
|
||||
* Added include tag ( like partials in rails )
|
||||
* [...] Gazillion of detail improvements
|
||||
* Added strainers as filter hosts for better security [Tobias Luetke]
|
||||
* Fixed that rails integration would call filter with the wrong "self" [Michael Geary]
|
||||
* Fixed bad error reporting when a filter called a method which doesn't exist. Liquid told you that it couldn't find the filter which was obviously misleading [Tobias Luetke]
|
||||
* Removed count helper from standard lib. use size [Tobias Luetke]
|
||||
* Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond]
|
||||
* Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ArgumentSeparator: , by default [Paul Hammond]
|
||||
{{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
|
||||
* Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke]
|
||||
|
||||
class ProductDrop < Liquid::Drop
|
||||
def top_sales
|
||||
Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
|
||||
end
|
||||
end
|
||||
t = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {% endfor %} ' )
|
||||
t.render('product' => ProductDrop.new )
|
||||
* Added filter parameters support. Example: {{ date | format_date: "%Y" }} [Paul Hammond]
|
||||
@@ -1,20 +0,0 @@
|
||||
Copyright (c) 2005, 2006 Tobias Luetke
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,70 +0,0 @@
|
||||
# Liquid Compiled Template Optimization Log
|
||||
|
||||
This document tracks optimizations made to the compiled Liquid template engine.
|
||||
Each entry shows before/after code and measured impact.
|
||||
|
||||
---
|
||||
|
||||
## Baseline Measurement
|
||||
|
||||
**Date:** 2024-12-31
|
||||
**Commit:** (pending profiler implementation)
|
||||
|
||||
### Current State
|
||||
|
||||
The compiled template engine generates Ruby code from Liquid templates.
|
||||
Before optimizations, here's a sample of generated code for a simple loop:
|
||||
|
||||
```ruby
|
||||
# Template: {% for product in products %}{{ forloop.index }}: {{ product.name }}{% endfor %}
|
||||
|
||||
->(assigns, __context__, __external__) do
|
||||
__output__ = +""
|
||||
|
||||
__coll1__ = assigns["products"]
|
||||
__coll1__ = __coll1__.to_a if __coll1__.is_a?(Range)
|
||||
__len3__ = __coll1__.respond_to?(:length) ? __coll1__.length : 0
|
||||
__idx2__ = 0
|
||||
catch(:__loop__break__) do
|
||||
(__coll1__.respond_to?(:each) ? __coll1__ : []).each do |__item__|
|
||||
catch(:__loop__continue__) do
|
||||
assigns["product"] = __item__
|
||||
assigns['forloop'] = {
|
||||
'name' => "product-products",
|
||||
'length' => __len3__,
|
||||
'index' => __idx2__ + 1,
|
||||
'index0' => __idx2__,
|
||||
'rindex' => __len3__ - __idx2__,
|
||||
'rindex0' => __len3__ - __idx2__ - 1,
|
||||
'first' => __idx2__ == 0,
|
||||
'last' => __idx2__ == __len3__ - 1,
|
||||
}
|
||||
__output__ << LR.output(LR.lookup(assigns["forloop"], "index", __context__))
|
||||
__output__ << ": "
|
||||
__output__ << LR.output(LR.lookup(assigns["product"], "name", __context__))
|
||||
end
|
||||
__idx2__ += 1
|
||||
end
|
||||
end
|
||||
assigns.delete("product")
|
||||
assigns.delete('forloop')
|
||||
|
||||
__output__
|
||||
end
|
||||
```
|
||||
|
||||
### Issues Identified
|
||||
|
||||
1. **catch/throw overhead** - Used even when no break/continue in loop
|
||||
2. **Hash allocation per iteration** - 8 key/value pairs computed every time
|
||||
3. **respond_to? checks** - Redundant after type is known
|
||||
4. **LR.lookup for forloop** - Unnecessary indirection for known hash
|
||||
5. **String literals not frozen** - Allocates on each render
|
||||
6. **Output buffer grows dynamically** - No pre-allocation
|
||||
|
||||
---
|
||||
|
||||
## Optimization Log
|
||||
|
||||
<!-- Entries will be added here as optimizations are implemented -->
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
[](https://github.com/Shopify/liquid/actions/workflows/liquid.yml)
|
||||
[](http://inch-ci.org/github/Shopify/liquid)
|
||||
|
||||
# Liquid template engine
|
||||
|
||||
* [Contributing guidelines](CONTRIBUTING.md)
|
||||
* [Version history](History.md)
|
||||
* [Liquid documentation from Shopify](https://shopify.dev/docs/api/liquid)
|
||||
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
|
||||
* [Website](http://liquidmarkup.org/)
|
||||
|
||||
## Introduction
|
||||
|
||||
Liquid is a template engine which was written with very specific requirements:
|
||||
|
||||
* It has to have beautiful and simple markup. Template engines which don't produce good looking markup are no fun to use.
|
||||
* It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don't want to run code on your server which your users wrote.
|
||||
* It has to be stateless. Compile and render steps have to be separate so that the expensive parsing and compiling can be done once and later on you can just render it passing in a hash with local variables and objects.
|
||||
|
||||
## Why you should use Liquid
|
||||
|
||||
* You want to allow your users to edit the appearance of your application but don't want them to run **insecure code on your server**.
|
||||
* You want to render templates directly from the database.
|
||||
* You like smarty (PHP) style template engines.
|
||||
* You need a template engine which does HTML just as well as emails.
|
||||
* You don't like the markup of your current templating engine.
|
||||
|
||||
## What does it look like?
|
||||
|
||||
```html
|
||||
<ul id="products">
|
||||
{% for product in products %}
|
||||
<li>
|
||||
<h2>{{ product.name }}</h2>
|
||||
Only {{ product.price | price }}
|
||||
|
||||
{{ product.description | prettyprint | paragraph }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
```
|
||||
|
||||
## How to use Liquid
|
||||
|
||||
Install Liquid by adding `gem 'liquid'` to your gemfile.
|
||||
|
||||
Liquid supports a very simple API based around the Liquid::Template class.
|
||||
For standard use you can just pass it the content of a file and call render with a parameters hash.
|
||||
|
||||
```ruby
|
||||
@template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template
|
||||
@template.render('name' => 'tobi') # => "hi tobi"
|
||||
```
|
||||
|
||||
### Concept of Environments
|
||||
|
||||
In Liquid, a "Environment" is a scoped environment that encapsulates custom tags, filters, and other configurations. This allows you to define and isolate different sets of functionality for different contexts, avoiding global overrides that can lead to conflicts and unexpected behavior.
|
||||
|
||||
By using environments, you can:
|
||||
|
||||
1. **Encapsulate Logic**: Keep the logic for different parts of your application separate.
|
||||
2. **Avoid Conflicts**: Prevent custom tags and filters from clashing with each other.
|
||||
3. **Improve Maintainability**: Make it easier to manage and understand the scope of customizations.
|
||||
4. **Enhance Security**: Limit the availability of certain tags and filters to specific contexts.
|
||||
|
||||
We encourage the use of Environments over globally overriding things because it promotes better software design principles such as modularity, encapsulation, and separation of concerns.
|
||||
|
||||
Here's an example of how you can define and use Environments in Liquid:
|
||||
|
||||
```ruby
|
||||
user_environment = Liquid::Environment.build do |environment|
|
||||
environment.register_tag("renderobj", RenderObjTag)
|
||||
end
|
||||
|
||||
Liquid::Template.parse(<<~LIQUID, environment: user_environment)
|
||||
{% renderobj src: "path/to/model.obj" %}
|
||||
LIQUID
|
||||
```
|
||||
|
||||
In this example, `RenderObjTag` is a custom tag that is only available within the `user_environment`.
|
||||
|
||||
Similarly, you can define another environment for a different context, such as email templates:
|
||||
|
||||
```ruby
|
||||
email_environment = Liquid::Environment.build do |environment|
|
||||
environment.register_tag("unsubscribe_footer", UnsubscribeFooter)
|
||||
end
|
||||
|
||||
Liquid::Template.parse(<<~LIQUID, environment: email_environment)
|
||||
{% unsubscribe_footer %}
|
||||
LIQUID
|
||||
```
|
||||
|
||||
By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage. For smaller projects, a global environment is available via `Liquid::Environment.default`.
|
||||
|
||||
### Error Modes
|
||||
|
||||
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
|
||||
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
|
||||
it very hard to debug and can lead to unexpected behaviour.
|
||||
|
||||
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
|
||||
when templates are invalid. You can enable this new parser like this:
|
||||
|
||||
```ruby
|
||||
Liquid::Environment.default.error_mode = :strict
|
||||
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
|
||||
Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
|
||||
Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
|
||||
```
|
||||
|
||||
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
|
||||
```ruby
|
||||
Liquid::Template.parse(source, error_mode: :strict)
|
||||
```
|
||||
This is useful for doing things like enabling strict mode only in the theme editor.
|
||||
|
||||
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
|
||||
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
|
||||
|
||||
### Undefined variables and filters
|
||||
|
||||
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
|
||||
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
|
||||
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance.
|
||||
Here are some examples:
|
||||
|
||||
```ruby
|
||||
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
|
||||
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
|
||||
#=> '1 2 ' # when a variable is undefined, it's rendered as nil
|
||||
template.errors
|
||||
#=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
|
||||
```
|
||||
|
||||
```ruby
|
||||
template = Liquid::Template.parse("{{x | filter1 | upcase}}")
|
||||
template.render({ 'x' => 'foo' }, { strict_filters: true })
|
||||
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
|
||||
template.errors
|
||||
#=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
|
||||
```
|
||||
|
||||
If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method:
|
||||
|
||||
```ruby
|
||||
template = Liquid::Template.parse("{{x}} {{y}}")
|
||||
template.render!({ 'x' => 1}, { strict_variables: true })
|
||||
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
|
||||
```
|
||||
|
||||
### Usage tracking
|
||||
|
||||
To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
|
||||
|
||||
Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.
|
||||
@@ -1,137 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
|
||||
require "liquid/version"
|
||||
|
||||
task(default: [:test, :rubocop])
|
||||
|
||||
desc('run test suite with default parser')
|
||||
Rake::TestTask.new(:base_test) do |t|
|
||||
t.libs << 'lib' << 'test'
|
||||
t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
|
||||
t.verbose = false
|
||||
end
|
||||
|
||||
Rake::TestTask.new(:integration_test) do |t|
|
||||
t.libs << 'lib' << 'test'
|
||||
t.test_files = FileList['test/integration/**/*_test.rb']
|
||||
t.verbose = false
|
||||
end
|
||||
|
||||
desc('run test suite with warn error mode')
|
||||
task :warn_test do
|
||||
ENV['LIQUID_PARSER_MODE'] = 'warn'
|
||||
Rake::Task['base_test'].invoke
|
||||
end
|
||||
|
||||
task :rubocop do
|
||||
if RUBY_ENGINE == 'ruby'
|
||||
require 'rubocop/rake_task'
|
||||
RuboCop::RakeTask.new
|
||||
end
|
||||
end
|
||||
|
||||
desc('runs test suite with both strict and lax parsers')
|
||||
task :test do
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
end
|
||||
end
|
||||
|
||||
task(gem: :build)
|
||||
task :build do
|
||||
system "gem build liquid.gemspec"
|
||||
end
|
||||
|
||||
task install: :build do
|
||||
system "gem install liquid-#{Liquid::VERSION}.gem"
|
||||
end
|
||||
|
||||
task release: :build do
|
||||
system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'"
|
||||
system "git push --tags"
|
||||
system "gem push liquid-#{Liquid::VERSION}.gem"
|
||||
system "rm liquid-#{Liquid::VERSION}.gem"
|
||||
end
|
||||
|
||||
namespace :benchmark do
|
||||
desc "Run the liquid benchmark with lax parsing"
|
||||
task :lax do
|
||||
ruby "./performance/benchmark.rb lax"
|
||||
end
|
||||
|
||||
desc "Run the liquid benchmark with strict parsing"
|
||||
task :strict do
|
||||
ruby "./performance/benchmark.rb strict"
|
||||
end
|
||||
|
||||
desc "Run the liquid benchmark with both lax and strict parsing"
|
||||
task run: [:lax, :strict]
|
||||
|
||||
desc "Run unit benchmarks"
|
||||
namespace :unit do
|
||||
task :all do
|
||||
Dir["./performance/unit/*_benchmark.rb"].each do |file|
|
||||
puts "🧪 Running #{file}"
|
||||
ruby file
|
||||
end
|
||||
end
|
||||
|
||||
task :lexer do
|
||||
Dir["./performance/unit/lexer_benchmark.rb"].each do |file|
|
||||
puts "🧪 Running #{file}"
|
||||
ruby file
|
||||
end
|
||||
end
|
||||
|
||||
task :expression do
|
||||
Dir["./performance/unit/expression_benchmark.rb"].each do |file|
|
||||
puts "🧪 Running #{file}"
|
||||
ruby file
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
namespace :profile do
|
||||
desc "Run the liquid profile/performance coverage"
|
||||
task :run do
|
||||
ruby "./performance/profile.rb"
|
||||
end
|
||||
|
||||
desc "Run the liquid profile/performance coverage with strict parsing"
|
||||
task :strict do
|
||||
ruby "./performance/profile.rb strict"
|
||||
end
|
||||
end
|
||||
|
||||
namespace :memory_profile do
|
||||
desc "Run memory profiler"
|
||||
task :run do
|
||||
ruby "./performance/memory_profile.rb"
|
||||
end
|
||||
end
|
||||
|
||||
desc("Run example")
|
||||
task :example do
|
||||
ruby "-w -d -Ilib example/server/server.rb"
|
||||
end
|
||||
|
||||
task :console do
|
||||
exec 'irb -I lib -r liquid'
|
||||
end
|
||||
@@ -1,50 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ProductsFilter
|
||||
def price(integer)
|
||||
format("$%.2d USD", integer / 100.0)
|
||||
end
|
||||
|
||||
def prettyprint(text)
|
||||
text.gsub(/\*(.*)\*/, '<b>\1</b>')
|
||||
end
|
||||
|
||||
def count(array)
|
||||
array.size
|
||||
end
|
||||
|
||||
def paragraph(p)
|
||||
"<p>#{p}</p>"
|
||||
end
|
||||
end
|
||||
|
||||
class Servlet < LiquidServlet
|
||||
def index
|
||||
{ 'date' => Time.now }
|
||||
end
|
||||
|
||||
def products
|
||||
{ 'products' => products_list, 'more_products' => more_products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def products_list
|
||||
[
|
||||
{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
|
||||
{ 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' },
|
||||
{ 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }
|
||||
]
|
||||
end
|
||||
|
||||
def more_products_list
|
||||
[
|
||||
{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
|
||||
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }
|
||||
]
|
||||
end
|
||||
|
||||
def description
|
||||
"List of Products ~ This is a list of products with price and description."
|
||||
end
|
||||
end
|
||||
@@ -1,30 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
def do_GET(req, res)
|
||||
handle(:get, req, res)
|
||||
end
|
||||
|
||||
def do_POST(req, res)
|
||||
handle(:post, req, res)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle(_type, req, res)
|
||||
@request = req
|
||||
@response = res
|
||||
|
||||
@request.path_info =~ /(\w+)\z/
|
||||
@action = Regexp.last_match(1) || 'index'
|
||||
@assigns = send(@action) if respond_to?(@action)
|
||||
|
||||
@response['Content-Type'] = "text/html"
|
||||
@response.status = 200
|
||||
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
|
||||
end
|
||||
|
||||
def read_template(filename = @action)
|
||||
File.read("#{__dir__}/templates/#{filename}.liquid")
|
||||
end
|
||||
end
|
||||
@@ -1,14 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'webrick'
|
||||
require 'rexml/document'
|
||||
|
||||
require_relative '../../lib/liquid'
|
||||
require_relative 'liquid_servlet'
|
||||
require_relative 'example_servlet'
|
||||
|
||||
# Setup webrick
|
||||
server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000)
|
||||
server.mount('/', Servlet)
|
||||
trap("INT") { server.shutdown }
|
||||
server.start
|
||||
@@ -1,6 +0,0 @@
|
||||
<p>Hello world!</p>
|
||||
|
||||
<p>It is {{date}}</p>
|
||||
|
||||
|
||||
<p>Check out the <a href="/products">Products</a> screen </p>
|
||||
@@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Language" content="en-us" />
|
||||
|
||||
<title>products</title>
|
||||
|
||||
<meta name="ROBOTS" content="ALL" />
|
||||
<meta http-equiv="imagetoolbar" content="no" />
|
||||
<meta name="MSSmartTagsPreventParsing" content="true" />
|
||||
<meta name="Copyright" content="(c) 2005 Copyright content: Copyright design: Tobias Luetke" />
|
||||
<!-- (c) Copyright 2005 by Tobias Luetke All Rights Reserved. -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% assign all_products = products | concat: more_products %}
|
||||
<h1>{{ description | split: '~' | first }}</h1>
|
||||
|
||||
<h2>{{ description | split: '~' | last }}</h2>
|
||||
|
||||
<h2>There are currently {{all_products | count}} products in the {{section}} catalog</h2>
|
||||
|
||||
{% if cool_products %}
|
||||
Cool products :)
|
||||
{% else %}
|
||||
Uncool products :(
|
||||
{% endif %}
|
||||
|
||||
<ul id="products">
|
||||
|
||||
{% for product in all_products %}
|
||||
<li>
|
||||
<h2>{{product.name}}</h2>
|
||||
Only {{product.price | price }}
|
||||
|
||||
{{product.description | prettyprint | paragraph }}
|
||||
|
||||
{{ 'it rocks!' | paragraph }}
|
||||
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
+277
@@ -0,0 +1,277 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=us-ascii" />
|
||||
|
||||
<title>Liquid Templating language</title>
|
||||
<style type="text/css" media="screen">
|
||||
/*<![CDATA[*/
|
||||
|
||||
body { margin: 0; margin-bottom: 25px; padding: 0; background-color: #f0f0f0; font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana"; font-size: 13px; color: #333; }
|
||||
h1 { font-size: 28px; color: #000; }
|
||||
h2 { font-size: 15px; color: #000; margin-top: 2em; margin-bottom: 0.5em; }
|
||||
a {color: #03c}
|
||||
a:hover { background-color: #03c; color: white; text-decoration: none; }
|
||||
|
||||
#page { width: 900px; margin: 0; margin-top: 20px; margin-left: auto; margin-right: auto; }
|
||||
#content { float: left; background-color: white; border: 3px solid #aaa; padding: 25px; width: 650px; }
|
||||
#sidebar { float: right; width: 175px; }
|
||||
#footer { clear: both; padding: 5px 2px; color: #999; font-size: 9px; }
|
||||
|
||||
#header, #intro { padding-left: 10px; padding-right: 30px; }
|
||||
#header h1, #header h2 { margin: 0 }
|
||||
#header h2 { color: #888; font-weight: normal; font-size: 16px; }
|
||||
|
||||
#intro { border-top: 1px solid #ccc; margin-top: 25px; padding-top: 15px; }
|
||||
#intro h1 { margin: 0; font-size: 20px; }
|
||||
#intro ol { margin-left: 0; padding-left: 0; }
|
||||
#intro li { font-size: 18px; color: #888; margin-bottom: 25px; }
|
||||
#intro li p { color: #555; font-size: 13px; }
|
||||
|
||||
#sidebar ul { margin-left: 0; padding-left: 0; }
|
||||
#sidebar ul h3 { margin-top: 25px; font-size: 16px; padding-bottom: 10px; border-bottom: 1px solid #ccc; }
|
||||
#sidebar li { list-style-type: none; }
|
||||
#sidebar ul.links li { margin-bottom: 5px; }
|
||||
|
||||
#outtro { border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; background: #eee; color: #555; font-size: 9px; padding: 10px; margin: 5em 0 0 0; text-align: center; }
|
||||
#outtro a { color: #333; }
|
||||
#outtro a:hover { color: white; }
|
||||
|
||||
span.highlight { background: #ffc; }
|
||||
|
||||
|
||||
table.downloads { width: 100%; border-spacing: 0 4px; }
|
||||
table.downloads tr { height: 80px; }
|
||||
table.downloads td { background: #efefef; }
|
||||
table.downloads h2 { margin: 0; width: 100%; }
|
||||
table.downloads .icon { text-align: center; width: 105px; }
|
||||
table.downloads p.code { margin-right:10px; padding: 4px 10px; overflow: auto; border: 1px solid #aaa; border-left: 4px solid #aaa; }
|
||||
|
||||
table.code { margin: 0; padding: 0.5em; width: 100%; border-left: 4px solid #ddd; background: #efefef; }
|
||||
/* Pygments */
|
||||
.c { color: #999988; font-style: italic } /* Comment */
|
||||
.err { color: #a61717; background-color: #e3d2d2 } /* Error */
|
||||
.k { font-weight: bold } /* Keyword */
|
||||
.o { font-weight: bold } /* Operator */
|
||||
.cm { color: #999988; font-style: italic } /* Comment.Multiline */
|
||||
.cp { color: #999999; font-weight: bold } /* Comment.Preproc */
|
||||
.c1 { color: #999988; font-style: italic } /* Comment.Single */
|
||||
.cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */
|
||||
.gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
|
||||
.ge { font-style: italic } /* Generic.Emph */
|
||||
.gr { color: #aa0000 } /* Generic.Error */
|
||||
.gh { color: #999999 } /* Generic.Heading */
|
||||
.gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
|
||||
.go { color: #888888 } /* Generic.Output */
|
||||
.gp { color: #555555 } /* Generic.Prompt */
|
||||
.gs { font-weight: bold } /* Generic.Strong */
|
||||
.gu { color: #aaaaaa } /* Generic.Subheading */
|
||||
.gt { color: #aa0000 } /* Generic.Traceback */
|
||||
.kc { font-weight: bold } /* Keyword.Constant */
|
||||
.kd { font-weight: bold } /* Keyword.Declaration */
|
||||
.kn { font-weight: bold } /* Keyword.Namespace */
|
||||
.kp { font-weight: bold } /* Keyword.Pseudo */
|
||||
.kr { font-weight: bold } /* Keyword.Reserved */
|
||||
.kt { color: #445588; font-weight: bold } /* Keyword.Type */
|
||||
.m { color: #009999 } /* Literal.Number */
|
||||
.s { color: #bb8844 } /* Literal.String */
|
||||
.na { color: #008080 } /* Name.Attribute */
|
||||
.nb { color: #999999 } /* Name.Builtin */
|
||||
.nc { color: #445588; font-weight: bold } /* Name.Class */
|
||||
.no { color: #008080 } /* Name.Constant */
|
||||
.ni { color: #800080 } /* Name.Entity */
|
||||
.ne { color: #990000; font-weight: bold } /* Name.Exception */
|
||||
.nf { color: #990000; font-weight: bold } /* Name.Function */
|
||||
.nn { color: #555555 } /* Name.Namespace */
|
||||
.nt { color: #000080 } /* Name.Tag */
|
||||
.nv { color: #008080 } /* Name.Variable */
|
||||
.ow { font-weight: bold } /* Operator.Word */
|
||||
.w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.mf { color: #009999 } /* Literal.Number.Float */
|
||||
.mh { color: #009999 } /* Literal.Number.Hex */
|
||||
.mi { color: #009999 } /* Literal.Number.Integer */
|
||||
.mo { color: #009999 } /* Literal.Number.Oct */
|
||||
.sb { color: #bb8844 } /* Literal.String.Backtick */
|
||||
.sc { color: #bb8844 } /* Literal.String.Char */
|
||||
.sd { color: #bb8844 } /* Literal.String.Doc */
|
||||
.s2 { color: #bb8844 } /* Literal.String.Double */
|
||||
.se { color: #bb8844 } /* Literal.String.Escape */
|
||||
.sh { color: #bb8844 } /* Literal.String.Heredoc */
|
||||
.si { color: #bb8844 } /* Literal.String.Interpol */
|
||||
.sx { color: #bb8844 } /* Literal.String.Other */
|
||||
.sr { color: #808000 } /* Literal.String.Regex */
|
||||
.s1 { color: #bb8844 } /* Literal.String.Single */
|
||||
.ss { color: #bb8844 } /* Literal.String.Symbol */
|
||||
.bp { color: #999999 } /* Name.Builtin.Pseudo */
|
||||
.vc { color: #008080 } /* Name.Variable.Class */
|
||||
.vg { color: #008080 } /* Name.Variable.Global */
|
||||
.vi { color: #008080 } /* Name.Variable.Instance */
|
||||
.il { color: #009999 } /* Literal.Number.Integer.Long */
|
||||
|
||||
/*]]>*/
|
||||
</style>
|
||||
<script type="text/javascript" src="javascripts/prototype.js">
|
||||
</script>
|
||||
<script type="text/javascript" src="javascripts/effects.js">
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page">
|
||||
<div id="sidebar">
|
||||
<ul id="sidebar-items">
|
||||
<li>
|
||||
<h3>General</h3>
|
||||
|
||||
<ul class="links">
|
||||
<li><a href="#download">Download</a></li>
|
||||
|
||||
<li><a href="https://groups.google.com/forum/#!forum/liquid-templates">Discussion</a></li>
|
||||
|
||||
<li><a href="https://github.com/Shopify/liquid/wiki">Documentation</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<h3>Development</h3>
|
||||
|
||||
<ul class="links">
|
||||
<li><a href="https://github.com/Shopify/liquid">GitHub</a></li>
|
||||
|
||||
<li><a href="https://github.com/Shopify/liquid/issues">Issues</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<h3>Developers</h3>
|
||||
|
||||
<ul class="links">
|
||||
<li><a href="http://www.shopify.com">Shopify</a></li>
|
||||
|
||||
<li><a href="http://blog.leetsoft.com">Tobias Lütke</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<div id="header">
|
||||
<h1>Liquid</h1>
|
||||
|
||||
<h2>Ruby library for rendering safe templates which cannot affect the security of the server they are rendered
|
||||
on.</h2>
|
||||
</div>
|
||||
|
||||
<div id="intro">
|
||||
<p>Liquid is an <span class="highlight">extraction from the e-commerce system <a href=
|
||||
"http://shopify.com">Shopify</a></span>. Shopify powers many thousands of e-commerce stores which all call for
|
||||
unique designs. For this we developed Liquid which allows our customers complete design freedom while
|
||||
maintaining the integrity of our servers.</p>
|
||||
|
||||
<p>Liquid has been in <span class="highlight">production use since June 2006</span> and is now used by many
|
||||
other hosted web applications.</p>
|
||||
|
||||
<p>It was developed for usage in <span class="highlight">Ruby on Rails</span> web applications and
|
||||
integrates seamlessly as a plugin but it also works excellently as a stand alone library.</p>
|
||||
|
||||
<h2>What does it look like?</h2>
|
||||
|
||||
<p>Example snippet:</p>
|
||||
|
||||
<table class="code">
|
||||
<tr>
|
||||
<td class="code">
|
||||
<pre>
|
||||
<span class="nt"><ul</span> <span class="na">id=</span><span class="s">"products"</span><span class="nt">></span>
|
||||
{% for product in products %}
|
||||
<span class="nt"><li></span>
|
||||
<span class="nt"><h2></span>{{ product.title }}<span class="nt"></h2></span>
|
||||
Only {{ product.price | format_as_money }}
|
||||
|
||||
<span class="nt"><p></span>{{ product.description | prettyprint | truncate: 200 }}<span class="nt"></p></span>
|
||||
|
||||
<span class="nt"></li></span>
|
||||
{% endfor %}
|
||||
<span class="nt"></ul></span>
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>Code to render</p>
|
||||
|
||||
<table class="code">
|
||||
<tr>
|
||||
<td class="code">
|
||||
<pre>
|
||||
<span class="no">Liquid</span><span class="o">::</span><span class="no">Template</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">template</span><span class="p">)</span><span class="o">.</span><span class="n">render</span> <span class="s1">'products'</span> <span class="o">=></span> <span class="no">Product</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="ss">:all</span><span class="p">)</span>
|
||||
</pre>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2 id="download">How do I get it?</h2>
|
||||
|
||||
<table class="downloads">
|
||||
<tr>
|
||||
<td class="icon"><img src="images/rubygems.png" width="100" height="100" alt="Rubygems" align=
|
||||
"middle" /></td>
|
||||
|
||||
<td>
|
||||
<h2>Rubygems</h2>
|
||||
|
||||
<p class="code"><code>gem install liquid</code></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="icon"><img src="images/rails.png" width="60" alt="Ruby on Rails" /></td>
|
||||
|
||||
<td>
|
||||
<h2>Ruby on Rails plugin</h2>
|
||||
<h4>Rails 3.0</h4>
|
||||
<p>Add <tt>gem 'liquid'</tt> to your Gemfile.</p>
|
||||
|
||||
<h4>Rails 2.3</h4>
|
||||
<p class="code"><code>./script/plugin install \</code>
|
||||
<code> git://github.com/Shopify/liquid.git</code></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="icon"><img src="images/ruby.png" width="60" height="60" alt="Ruby" /></td>
|
||||
|
||||
<td>
|
||||
<h2>Download Release</h2>
|
||||
|
||||
<p><a href="http://rubygems.org/gems/liquid">from RubyGems</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="icon"><img src="images/ruby.png" width="60" height="60" alt="Ruby" /></td>
|
||||
|
||||
<td>
|
||||
<h2>Git Clone</h2>
|
||||
|
||||
<p><a href="http://github.com/Shopify/liquid/commits/master">from Github</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="outtro">
|
||||
<strong>Liquid</strong> is a sponsored and made possible by constant development from <a href=
|
||||
"http://www.shopify.com">Shopify</a>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
© 2011
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,92 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Copyright (c) 2005 Tobias Luetke
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
require "strscan"
|
||||
|
||||
module Liquid
|
||||
FilterSeparator = /\|/
|
||||
ArgumentSeparator = ','
|
||||
FilterArgumentSeparator = ':'
|
||||
VariableAttributeSeparator = '.'
|
||||
WhitespaceControl = '-'
|
||||
TagStart = /\{\%/
|
||||
TagEnd = /\%\}/
|
||||
TagName = /#|\w+/
|
||||
VariableSignature = /\(?[\w\-\.\[\]]\)?/
|
||||
VariableSegment = /[\w\-]/
|
||||
VariableStart = /\{\{/
|
||||
VariableEnd = /\}\}/
|
||||
VariableIncompleteEnd = /\}\}?/
|
||||
QuotedString = /"[^"]*"|'[^']*'/
|
||||
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
||||
TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o
|
||||
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
|
||||
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
|
||||
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
|
||||
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
|
||||
|
||||
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
|
||||
HAS_STRING_SCANNER_SCAN_BYTE = StringScanner.instance_methods.include?(:scan_byte)
|
||||
end
|
||||
|
||||
require "liquid/version"
|
||||
require "liquid/deprecations"
|
||||
require "liquid/const"
|
||||
require 'liquid/standardfilters'
|
||||
require 'liquid/file_system'
|
||||
require 'liquid/parser_switching'
|
||||
require 'liquid/tag'
|
||||
require 'liquid/block'
|
||||
require 'liquid/parse_tree_visitor'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/tags'
|
||||
require "liquid/environment"
|
||||
require 'liquid/lexer'
|
||||
require 'liquid/parser'
|
||||
require 'liquid/i18n'
|
||||
require 'liquid/drop'
|
||||
require 'liquid/tablerowloop_drop'
|
||||
require 'liquid/forloop_drop'
|
||||
require 'liquid/extensions'
|
||||
require 'liquid/errors'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/strainer_template'
|
||||
require 'liquid/context'
|
||||
require 'liquid/tag'
|
||||
require 'liquid/block_body'
|
||||
require 'liquid/document'
|
||||
require 'liquid/variable'
|
||||
require 'liquid/variable_lookup'
|
||||
require 'liquid/range_lookup'
|
||||
require 'liquid/resource_limits'
|
||||
require 'liquid/expression'
|
||||
require 'liquid/template'
|
||||
require 'liquid/condition'
|
||||
require 'liquid/utils'
|
||||
require 'liquid/tokenizer'
|
||||
require 'liquid/parse_context'
|
||||
require 'liquid/partial_cache'
|
||||
require 'liquid/usage'
|
||||
require 'liquid/registers'
|
||||
require 'liquid/template_factory'
|
||||
require 'liquid/box'
|
||||
@@ -1,98 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Block < Tag
|
||||
MAX_DEPTH = 100
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@blank = true
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
@body = new_body
|
||||
while parse_body(@body, tokens)
|
||||
end
|
||||
@body.freeze
|
||||
end
|
||||
|
||||
# For backwards compatibility
|
||||
def render(context)
|
||||
@body.render(context)
|
||||
end
|
||||
|
||||
def blank?
|
||||
@blank
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@body.nodelist
|
||||
end
|
||||
|
||||
def unknown_tag(tag_name, _markup, _tokenizer)
|
||||
Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
|
||||
if tag == 'else'
|
||||
raise SyntaxError, parse_context.locale.t(
|
||||
"errors.syntax.unexpected_else",
|
||||
block_name: block_name,
|
||||
)
|
||||
elsif tag.start_with?('end')
|
||||
raise SyntaxError, parse_context.locale.t(
|
||||
"errors.syntax.invalid_delimiter",
|
||||
tag: tag,
|
||||
block_name: block_name,
|
||||
block_delimiter: block_delimiter,
|
||||
)
|
||||
else
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
|
||||
end
|
||||
end
|
||||
|
||||
def raise_tag_never_closed(block_name)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
|
||||
end
|
||||
|
||||
def block_name
|
||||
@tag_name
|
||||
end
|
||||
|
||||
def block_delimiter
|
||||
@block_delimiter ||= "end#{block_name}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @api public
|
||||
def new_body
|
||||
parse_context.new_block_body
|
||||
end
|
||||
|
||||
# @api public
|
||||
def parse_body(body, tokens)
|
||||
if parse_context.depth >= MAX_DEPTH
|
||||
raise StackLevelError, "Nesting too deep"
|
||||
end
|
||||
parse_context.depth += 1
|
||||
begin
|
||||
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
|
||||
@blank &&= body.blank?
|
||||
|
||||
return false if end_tag_name == block_delimiter
|
||||
raise_tag_never_closed(block_name) unless end_tag_name
|
||||
|
||||
# this tag is not registered with the system
|
||||
# pass it to the current block for special handling or error reporting
|
||||
unknown_tag(end_tag_name, end_tag_params, tokens)
|
||||
end
|
||||
ensure
|
||||
parse_context.depth -= 1
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,273 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'English'
|
||||
|
||||
module Liquid
|
||||
class BlockBody
|
||||
LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
|
||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
||||
WhitespaceOrNothing = /\A\s*\z/
|
||||
TAGSTART = "{%"
|
||||
VARSTART = "{{"
|
||||
|
||||
attr_reader :nodelist
|
||||
|
||||
def initialize
|
||||
@nodelist = []
|
||||
@blank = true
|
||||
end
|
||||
|
||||
def parse(tokenizer, parse_context, &block)
|
||||
raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
|
||||
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
|
||||
if tokenizer.for_liquid_tag
|
||||
parse_for_liquid_tag(tokenizer, parse_context, &block)
|
||||
else
|
||||
parse_for_document(tokenizer, parse_context, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def freeze
|
||||
@nodelist.freeze
|
||||
super
|
||||
end
|
||||
|
||||
private def parse_for_liquid_tag(tokenizer, parse_context)
|
||||
while (token = tokenizer.shift)
|
||||
unless token.empty? || token.match?(WhitespaceOrNothing)
|
||||
unless token =~ LiquidTagToken
|
||||
# line isn't empty but didn't match tag syntax, yield and let the
|
||||
# caller raise a syntax error
|
||||
return yield token, token
|
||||
end
|
||||
tag_name = Regexp.last_match(1)
|
||||
markup = Regexp.last_match(2)
|
||||
|
||||
if tag_name == 'liquid'
|
||||
parse_context.line_number -= 1
|
||||
next parse_liquid_tag(markup, parse_context)
|
||||
end
|
||||
|
||||
unless (tag = parse_context.environment.tag_for_name(tag_name))
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
end
|
||||
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
||||
@blank &&= new_tag.blank?
|
||||
@nodelist << new_tag
|
||||
end
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
end
|
||||
|
||||
yield nil, nil
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.unknown_tag_in_liquid_tag(tag, parse_context)
|
||||
Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.raise_missing_tag_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.raise_missing_variable_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.render_node(context, output, node)
|
||||
node.render_to_output_buffer(context, output)
|
||||
rescue => exc
|
||||
blank_tag = !node.instance_of?(Variable) && node.blank?
|
||||
rescue_render_node(context, output, node.line_number, exc, blank_tag)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.rescue_render_node(context, output, line_number, exc, blank_tag)
|
||||
case exc
|
||||
when MemoryError
|
||||
raise
|
||||
when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
|
||||
context.handle_error(exc, line_number)
|
||||
else
|
||||
error_message = context.handle_error(exc, line_number)
|
||||
unless blank_tag # conditional for backwards compatibility
|
||||
output << error_message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private def parse_liquid_tag(markup, parse_context)
|
||||
liquid_tag_tokenizer = parse_context.new_tokenizer(
|
||||
markup, start_line_number: parse_context.line_number, for_liquid_tag: true
|
||||
)
|
||||
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
|
||||
if end_tag_name
|
||||
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private def handle_invalid_tag_token(token, parse_context)
|
||||
if token.end_with?('%}')
|
||||
yield token, token
|
||||
else
|
||||
BlockBody.raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
end
|
||||
|
||||
private def parse_for_document(tokenizer, parse_context, &block)
|
||||
while (token = tokenizer.shift)
|
||||
next if token.empty?
|
||||
case
|
||||
when token.start_with?(TAGSTART)
|
||||
whitespace_handler(token, parse_context)
|
||||
unless token =~ FullToken
|
||||
return handle_invalid_tag_token(token, parse_context, &block)
|
||||
end
|
||||
tag_name = Regexp.last_match(2)
|
||||
markup = Regexp.last_match(4)
|
||||
|
||||
if parse_context.line_number
|
||||
# newlines inside the tag should increase the line number,
|
||||
# particularly important for multiline {% liquid %} tags
|
||||
parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
|
||||
end
|
||||
|
||||
if tag_name == 'liquid'
|
||||
parse_liquid_tag(markup, parse_context)
|
||||
next
|
||||
end
|
||||
|
||||
unless (tag = parse_context.environment.tag_for_name(tag_name))
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
end
|
||||
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
||||
@blank &&= new_tag.blank?
|
||||
@nodelist << new_tag
|
||||
when token.start_with?(VARSTART)
|
||||
whitespace_handler(token, parse_context)
|
||||
@nodelist << create_variable(token, parse_context)
|
||||
@blank = false
|
||||
else
|
||||
if parse_context.trim_whitespace
|
||||
token.lstrip!
|
||||
end
|
||||
parse_context.trim_whitespace = false
|
||||
@nodelist << token
|
||||
@blank &&= token.match?(WhitespaceOrNothing)
|
||||
end
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
end
|
||||
|
||||
yield nil, nil
|
||||
end
|
||||
|
||||
def whitespace_handler(token, parse_context)
|
||||
if token[2] == WhitespaceControl
|
||||
previous_token = @nodelist.last
|
||||
if previous_token.is_a?(String)
|
||||
first_byte = previous_token.getbyte(0)
|
||||
previous_token.rstrip!
|
||||
if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
|
||||
previous_token << first_byte
|
||||
end
|
||||
end
|
||||
end
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||
end
|
||||
|
||||
def blank?
|
||||
@blank
|
||||
end
|
||||
|
||||
# Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
|
||||
# with a blank body.
|
||||
#
|
||||
# For example, in a conditional assignment like the following
|
||||
#
|
||||
# ```
|
||||
# {% if size > max_size %}
|
||||
# {% assign size = max_size %}
|
||||
# {% endif %}
|
||||
# ```
|
||||
#
|
||||
# we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
|
||||
# will remove them to reduce the render output size.
|
||||
#
|
||||
# Note that it is now preferred to use the `liquid` tag for this use case.
|
||||
def remove_blank_strings
|
||||
raise "remove_blank_strings only support being called on a blank block body" unless @blank
|
||||
@nodelist.reject! { |node| node.instance_of?(String) }
|
||||
end
|
||||
|
||||
def render(context)
|
||||
render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
freeze unless frozen?
|
||||
|
||||
context.resource_limits.increment_render_score(@nodelist.length)
|
||||
|
||||
idx = 0
|
||||
while (node = @nodelist[idx])
|
||||
if node.instance_of?(String)
|
||||
output << node
|
||||
else
|
||||
render_node(context, output, node)
|
||||
# If we get an Interrupt that means the block must stop processing. An
|
||||
# Interrupt is any command that stops block execution such as {% break %}
|
||||
# or {% continue %}. These tags may also occur through Block or Include tags.
|
||||
break if context.interrupt? # might have happened in a for-block
|
||||
end
|
||||
idx += 1
|
||||
|
||||
context.resource_limits.increment_write_score(output)
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_node(context, output, node)
|
||||
BlockBody.render_node(context, output, node)
|
||||
end
|
||||
|
||||
def create_variable(token, parse_context)
|
||||
if token.end_with?("}}")
|
||||
i = 2
|
||||
i = 3 if token[i] == "-"
|
||||
parse_end = token.length - 3
|
||||
parse_end -= 1 if token[parse_end] == "-"
|
||||
markup_end = parse_end - i + 1
|
||||
markup = markup_end <= 0 ? "" : token.slice(i, markup_end)
|
||||
|
||||
return Variable.new(markup, parse_context)
|
||||
end
|
||||
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
# @deprecated Use {.raise_missing_tag_terminator} instead
|
||||
def raise_missing_tag_terminator(token, parse_context)
|
||||
BlockBody.raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
# @deprecated Use {.raise_missing_variable_terminator} instead
|
||||
def raise_missing_variable_terminator(token, parse_context)
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,507 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Liquid::Box - Secure sandboxed execution environment for compiled Liquid templates
|
||||
#
|
||||
# On Ruby 4.0+, this uses the native Ruby::Box for true isolation.
|
||||
# On earlier Ruby versions, this provides a polyfill that WARNS about insecurity.
|
||||
#
|
||||
# == SECURITY WARNING
|
||||
#
|
||||
# The polyfill on Ruby < 4.0 provides NO REAL SECURITY. It's a compatibility shim
|
||||
# that allows code to run, but malicious templates could potentially escape.
|
||||
# Use Ruby 4.0+ in production for actual sandboxing.
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# template = Liquid::Template.parse("Hello {{ name }}!")
|
||||
# compiled = template.compile_to_ruby
|
||||
#
|
||||
# # compiled is a Liquid::CompiledTemplate which wraps a Box
|
||||
# result = compiled.render({ "name" => "World" })
|
||||
# # => "Hello World!"
|
||||
#
|
||||
# # Access the generated Ruby code:
|
||||
# puts compiled.source
|
||||
#
|
||||
|
||||
module Liquid
|
||||
# Check if we have Ruby 4.0's native Box AND it's enabled
|
||||
# Ruby::Box may be defined but disabled (requires RUBY_BOX=1 env var before Ruby starts)
|
||||
RUBY_BOX_AVAILABLE = begin
|
||||
if defined?(Ruby::Box)
|
||||
# Try to create a box to see if it's actually enabled
|
||||
test_box = Ruby::Box.new
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
rescue RuntimeError => e
|
||||
# Ruby::Box exists but is disabled
|
||||
false
|
||||
end
|
||||
|
||||
unless RUBY_BOX_AVAILABLE
|
||||
# Warn once on load that we're using the insecure polyfill
|
||||
warn "[Liquid::Box] WARNING: Ruby::Box not available or disabled. " \
|
||||
"Using INSECURE polyfill. Compiled templates are NOT sandboxed! " \
|
||||
"(Ruby 4.0+ with RUBY_BOX=1 required for secure execution)" if $VERBOSE
|
||||
end
|
||||
|
||||
# Polyfill for Ruby::Box when not available (Ruby < 4.0)
|
||||
# This provides the same API but NO SECURITY - it just evals code in the main environment.
|
||||
module BoxPolyfill
|
||||
class Box
|
||||
def initialize
|
||||
@constants = {}
|
||||
@binding = TOPLEVEL_BINDING.dup
|
||||
end
|
||||
|
||||
def eval(code)
|
||||
::Kernel.eval(code, @binding)
|
||||
end
|
||||
|
||||
def const_get(name)
|
||||
::Object.const_get(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Liquid::Box wraps either Ruby::Box (secure) or BoxPolyfill::Box (insecure)
|
||||
#
|
||||
# This provides a secure sandboxed environment for executing compiled Liquid templates.
|
||||
# On Ruby 4.0+, templates run in true isolation with dangerous methods removed.
|
||||
# On Ruby < 4.0, templates run without sandboxing (development/testing only).
|
||||
#
|
||||
class Box
|
||||
attr_reader :box
|
||||
|
||||
class << self
|
||||
# Returns true if we have real sandboxing (Ruby 4.0+)
|
||||
def secure?
|
||||
RUBY_BOX_AVAILABLE
|
||||
end
|
||||
|
||||
# Create a pre-configured box for Liquid template execution.
|
||||
# This is the recommended way to get a Box instance.
|
||||
def create_for_liquid
|
||||
box = new
|
||||
box.load_liquid_runtime!
|
||||
box.lock!
|
||||
box
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@box = RUBY_BOX_AVAILABLE ? Ruby::Box.new : BoxPolyfill::Box.new
|
||||
@locked = false
|
||||
@user_constants = []
|
||||
@warned_insecure = false
|
||||
end
|
||||
|
||||
# Load code into the sandbox before locking.
|
||||
# Use this to define filters, helpers, and the Liquid runtime.
|
||||
def load_runtime(code)
|
||||
raise "Cannot load runtime after lock!" if @locked
|
||||
before = @box.eval("Object.constants")
|
||||
@box.eval(code)
|
||||
after = @box.eval("Object.constants")
|
||||
@user_constants += (after - before).map(&:to_s)
|
||||
end
|
||||
|
||||
# Load the standard Liquid runtime helpers (LR module).
|
||||
# Call this before lock! to set up the execution environment.
|
||||
# The runtime provides all helper methods that compiled templates use.
|
||||
def load_liquid_runtime!
|
||||
raise "Cannot load runtime after lock!" if @locked
|
||||
|
||||
if RUBY_BOX_AVAILABLE
|
||||
# Add gem paths to box's load_path so it can find base64, bigdecimal, etc.
|
||||
# These are safe, side-effect-free libs that only provide pure functions
|
||||
setup_gem_load_paths!
|
||||
|
||||
# Load dependencies INTO the box - we need the actual libraries,
|
||||
# not reimplementations, to handle all edge cases correctly
|
||||
@box.require('cgi')
|
||||
@box.require('base64')
|
||||
@box.require('bigdecimal')
|
||||
@box.require('bigdecimal/util') # For String#to_d etc.
|
||||
@box.require('date') # For date filter
|
||||
@box.require('time') # For Time.parse
|
||||
|
||||
# Now load the runtime which captures method references from these
|
||||
@box.require(RUNTIME_PATH)
|
||||
else
|
||||
# Polyfill: just require it normally
|
||||
require 'cgi'
|
||||
require 'base64'
|
||||
require 'bigdecimal'
|
||||
require 'bigdecimal/util'
|
||||
require 'date'
|
||||
require 'time'
|
||||
require RUNTIME_PATH
|
||||
end
|
||||
|
||||
# Track constants to preserve after lock
|
||||
@user_constants << "LR"
|
||||
@user_constants << "LiquidRuntime"
|
||||
@user_constants << "CGI"
|
||||
@user_constants << "Base64"
|
||||
@user_constants << "BigDecimal"
|
||||
@user_constants << "Date"
|
||||
@user_constants << "DateTime"
|
||||
@user_constants << "Time"
|
||||
@user_constants << "Liquid" # For Liquid::Compile::CompiledContext
|
||||
end
|
||||
|
||||
# Add gem paths to the box's load_path so require works for gems
|
||||
def setup_gem_load_paths!
|
||||
return unless RUBY_BOX_AVAILABLE
|
||||
|
||||
# Find gem paths from the main environment's $LOAD_PATH
|
||||
# and from Gem.path if available
|
||||
gem_lib_paths = []
|
||||
|
||||
# Method 1: Find from $LOAD_PATH entries containing "gems"
|
||||
$LOAD_PATH.each do |path|
|
||||
gem_lib_paths << path if path.include?('/gems/')
|
||||
end
|
||||
|
||||
# Method 2: Use Gem.path if available
|
||||
if defined?(Gem) && Gem.respond_to?(:path)
|
||||
Gem.path.each do |gem_path|
|
||||
Dir.glob("#{gem_path}/gems/*/lib").each do |lib_path|
|
||||
gem_lib_paths << lib_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Add unique paths to box's load_path
|
||||
gem_lib_paths.uniq.each do |path|
|
||||
@box.load_path << path unless @box.load_path.include?(path)
|
||||
end
|
||||
end
|
||||
|
||||
# Lock the sandbox. After this:
|
||||
# - No more runtime can be loaded
|
||||
# - Dangerous methods are removed (on Ruby 4.0+)
|
||||
# - Templates can be compiled and executed
|
||||
def lock!
|
||||
return if @locked
|
||||
|
||||
if RUBY_BOX_AVAILABLE
|
||||
apply_sandbox!
|
||||
else
|
||||
warn_insecure!
|
||||
end
|
||||
|
||||
@locked = true
|
||||
end
|
||||
|
||||
def locked?
|
||||
@locked
|
||||
end
|
||||
|
||||
# Returns true if this box provides real security (Ruby 4.0+)
|
||||
def secure?
|
||||
RUBY_BOX_AVAILABLE
|
||||
end
|
||||
|
||||
# Evaluate code in the sandbox.
|
||||
# Use this to compile templates into the sandbox.
|
||||
def eval(code)
|
||||
raise "Must call lock! before eval" unless @locked
|
||||
warn_insecure! unless RUBY_BOX_AVAILABLE
|
||||
@box.eval(code)
|
||||
end
|
||||
|
||||
# Get a constant from the sandbox by name.
|
||||
def const_get(name)
|
||||
@box.const_get(name)
|
||||
end
|
||||
|
||||
def [](name)
|
||||
const_get(name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def warn_insecure!
|
||||
return if @warned_insecure
|
||||
@warned_insecure = true
|
||||
|
||||
$stderr.puts <<~WARNING
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ SECURITY WARNING: Liquid::Box running WITHOUT sandboxing ║
|
||||
║ ║
|
||||
║ Ruby::Box requires Ruby 4.0+. On earlier versions, compiled Liquid templates ║
|
||||
║ execute with FULL Ruby capabilities. This is NOT SECURE for untrusted input. ║
|
||||
║ ║
|
||||
║ For production use with untrusted templates, upgrade to Ruby 4.0+. ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
WARNING
|
||||
end
|
||||
|
||||
# Apply sandbox restrictions (Ruby 4.0+ only)
|
||||
def apply_sandbox!
|
||||
neuter_file_system!
|
||||
neuter_process_control!
|
||||
neuter_concurrency!
|
||||
neuter_introspection!
|
||||
neuter_serialization!
|
||||
neuter_time!
|
||||
neuter_environment!
|
||||
neuter_kernel!
|
||||
neuter_basic_object!
|
||||
neuter_object!
|
||||
neuter_main_singleton!
|
||||
neuter_module!
|
||||
cleanup_globals!
|
||||
remove_dangerous_constants!
|
||||
end
|
||||
|
||||
def neuter_file_system!
|
||||
@box.eval(<<~'RUBY')
|
||||
class << File
|
||||
instance_methods(false).each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
class << IO
|
||||
instance_methods(false).each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
class << Dir
|
||||
instance_methods(false).each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
class IO
|
||||
[:read, :write, :gets, :puts, :print, :readline, :readlines, :getc, :getbyte,
|
||||
:sysread, :syswrite, :close, :eof, :eof?, :rewind, :seek].each do |m|
|
||||
undef_method(m) rescue nil
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def neuter_process_control!
|
||||
@box.eval(<<~'RUBY')
|
||||
class << Process
|
||||
instance_methods(false).each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
class << Signal
|
||||
instance_methods(false).each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def neuter_concurrency!
|
||||
@box.eval(<<~'RUBY')
|
||||
class << Thread
|
||||
[:new, :start, :fork, :kill, :exit, :pass, :stop, :main, :current, :list,
|
||||
:abort_on_exception, :abort_on_exception=].each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
class << Fiber
|
||||
[:new, :yield, :current].each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
if defined?(Ractor)
|
||||
class << Ractor
|
||||
instance_methods(false).each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def neuter_introspection!
|
||||
@box.eval(<<~'RUBY')
|
||||
class << ObjectSpace
|
||||
instance_methods(false).each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
class << GC
|
||||
instance_methods(false).each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
if defined?(RubyVM)
|
||||
class << RubyVM
|
||||
instance_methods(false).each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
if defined?(RubyVM::InstructionSequence)
|
||||
class << RubyVM::InstructionSequence
|
||||
instance_methods(false).each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
class << TracePoint
|
||||
[:new, :stat, :trace].each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def neuter_serialization!
|
||||
@box.eval(<<~'RUBY')
|
||||
class << Marshal
|
||||
[:dump, :load, :restore].each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def neuter_time!
|
||||
# Time is mostly safe for date filters - only neuter methods that could be used
|
||||
# to manipulate system state or sleep/wait.
|
||||
# Keep: now, at, parse, mktime - needed for date filter
|
||||
# Remove: nothing for now - Time is pure computation
|
||||
#
|
||||
# Note: If you want stricter isolation, templates should receive "now" via assigns
|
||||
# @box.eval(<<~'RUBY')
|
||||
# class << Time
|
||||
# [:now, :new, :at, :mktime, :local, :utc, :gm].each { |m| undef_method(m) rescue nil }
|
||||
# end
|
||||
# RUBY
|
||||
end
|
||||
|
||||
def neuter_environment!
|
||||
@box.eval(<<~'RUBY')
|
||||
ENV.clear rescue nil
|
||||
class << ENV
|
||||
instance_methods(false).each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def neuter_kernel!
|
||||
@box.eval(<<~'RUBY')
|
||||
module Kernel
|
||||
[:eval, :`, :system, :exec, :spawn, :fork, :binding,
|
||||
:open, :require, :require_relative, :load, :autoload, :autoload?,
|
||||
:gets, :readline, :readlines, :select, :test,
|
||||
:trap, :exit, :exit!, :abort, :at_exit, :syscall, :sleep,
|
||||
:puts, :print, :printf, :putc, :p, :pp, :warn,
|
||||
:caller, :caller_locations, :set_trace_func, :trace_var, :untrace_var,
|
||||
:global_variables, :local_variables,
|
||||
:gem, :gem_original_require, :Pathname,
|
||||
].each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
|
||||
class << Kernel
|
||||
[:eval, :`, :system, :exec, :spawn, :fork, :binding,
|
||||
:open, :require, :require_relative, :load,
|
||||
:puts, :print, :p, :pp, :warn,
|
||||
].each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def neuter_basic_object!
|
||||
@box.eval(<<~'RUBY')
|
||||
class BasicObject
|
||||
undef_method(:instance_eval) rescue nil
|
||||
undef_method(:instance_exec) rescue nil
|
||||
# Don't undef __send__ - it causes warnings and is equivalent to send
|
||||
# which we already restrict via public_send
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def neuter_object!
|
||||
@box.eval(<<~'RUBY')
|
||||
class Object
|
||||
# Keep public_send - it's safe (only calls public methods) and useful
|
||||
[:gem, :gem_original_require, :require, :require_relative, :load,
|
||||
:display, :define_singleton_method,
|
||||
:instance_variable_set, :remove_instance_variable,
|
||||
:extend, :send,
|
||||
].each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def neuter_main_singleton!
|
||||
# 'using' is defined on main's singleton class, must remove before undef_method is gone
|
||||
@box.eval(<<~'RUBY')
|
||||
class << self
|
||||
undef_method(:using) rescue nil
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def neuter_module!
|
||||
@box.eval(<<~'RUBY')
|
||||
class Module
|
||||
undef_method(:refine) rescue nil
|
||||
undef_method(:using) rescue nil
|
||||
undef_method(:const_set) rescue nil
|
||||
undef_method(:remove_const) rescue nil
|
||||
undef_method(:include) rescue nil
|
||||
undef_method(:prepend) rescue nil
|
||||
undef_method(:extend) rescue nil
|
||||
undef_method(:class_eval) rescue nil
|
||||
undef_method(:module_eval) rescue nil
|
||||
undef_method(:class_exec) rescue nil
|
||||
undef_method(:module_exec) rescue nil
|
||||
undef_method(:define_method) rescue nil
|
||||
undef_method(:alias_method) rescue nil
|
||||
undef_method(:module_function) rescue nil
|
||||
undef_method(:prepend_features) rescue nil
|
||||
undef_method(:append_features) rescue nil
|
||||
undef_method(:extend_object) rescue nil
|
||||
undef_method(:public) rescue nil
|
||||
undef_method(:private) rescue nil
|
||||
undef_method(:protected) rescue nil
|
||||
undef_method(:attr) rescue nil
|
||||
undef_method(:attr_reader) rescue nil
|
||||
undef_method(:attr_writer) rescue nil
|
||||
undef_method(:attr_accessor) rescue nil
|
||||
# Remove escape hatches LAST
|
||||
undef_method(:remove_method) rescue nil
|
||||
undef_method(:undef_method) rescue nil
|
||||
undef_method(:send) rescue nil
|
||||
undef_method(:public_send) rescue nil
|
||||
end
|
||||
|
||||
class Class
|
||||
undef_method(:send) rescue nil
|
||||
undef_method(:public_send) rescue nil
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def cleanup_globals!
|
||||
@box.eval(<<~'RUBY')
|
||||
$stdin = nil
|
||||
$stdout = nil
|
||||
$stderr = nil
|
||||
$LOAD_PATH.clear rescue nil
|
||||
$LOAD_PATH.freeze rescue nil
|
||||
$LOADED_FEATURES.clear rescue nil
|
||||
$LOADED_FEATURES.freeze rescue nil
|
||||
ARGV.clear rescue nil
|
||||
ARGV.freeze rescue nil
|
||||
RUBY
|
||||
end
|
||||
|
||||
def remove_dangerous_constants!
|
||||
keep = %w[
|
||||
BasicObject Object Module Class Kernel
|
||||
String Integer Float Numeric Rational Complex
|
||||
Array Hash Range Set
|
||||
Symbol Regexp MatchData
|
||||
TrueClass FalseClass NilClass
|
||||
Proc Method UnboundMethod
|
||||
Struct Data
|
||||
Comparable Enumerable Enumerator
|
||||
StandardError RuntimeError ArgumentError TypeError NameError
|
||||
NoMethodError KeyError IndexError StopIteration FrozenError
|
||||
ZeroDivisionError RangeError FloatDomainError LocalJumpError
|
||||
Math Random
|
||||
Exception SystemStackError
|
||||
] + @user_constants
|
||||
|
||||
@box.eval(<<~RUBY)
|
||||
_keep = #{keep.inspect}
|
||||
(Object.constants.map(&:to_s) - _keep).each do |c|
|
||||
Object.send(:remove_const, c.to_sym) rescue nil
|
||||
end
|
||||
(Kernel.constants.map(&:to_s) - _keep).each do |c|
|
||||
Kernel.send(:remove_const, c.to_sym) rescue nil
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
# Path to the runtime file that gets loaded into the sandbox
|
||||
RUNTIME_PATH = File.expand_path('compile/runtime.rb', __dir__)
|
||||
end
|
||||
end
|
||||
@@ -1,88 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Liquid Ruby Compiler
|
||||
#
|
||||
# This module provides the ability to compile Liquid templates to pure Ruby code.
|
||||
# Compiled templates execute in a secure sandbox using Liquid::Box (on Ruby 4.0+).
|
||||
#
|
||||
# ## Usage
|
||||
#
|
||||
# template = Liquid::Template.parse("Hello, {{ name }}!")
|
||||
# compiled = template.compile_to_ruby
|
||||
#
|
||||
# # Render securely (sandboxed on Ruby 4.0+)
|
||||
# result = compiled.render({ "name" => "World" })
|
||||
# # => "Hello, World!"
|
||||
#
|
||||
# # Access the generated Ruby source
|
||||
# puts compiled.source
|
||||
#
|
||||
# # Check security status
|
||||
# compiled.secure? # => true on Ruby 4.0+
|
||||
#
|
||||
# ## Security
|
||||
#
|
||||
# On Ruby 4.0+, compiled templates execute in a Ruby::Box sandbox that prevents:
|
||||
# - File system access (File, IO, Dir)
|
||||
# - Process control (system, exec, spawn, fork)
|
||||
# - Network access (Socket, Net::HTTP)
|
||||
# - Code loading (require, load, eval)
|
||||
# - Dangerous metaprogramming (define_method, const_set, send)
|
||||
#
|
||||
# On Ruby < 4.0, a polyfill is used that prints a security warning to STDERR.
|
||||
# The polyfill provides NO ACTUAL SECURITY - use Ruby 4.0+ in production.
|
||||
#
|
||||
# ## Performance Benefits
|
||||
#
|
||||
# Compiled templates are ~1.5x faster than interpreted Liquid because:
|
||||
#
|
||||
# 1. **No Context Object**: Variables accessed directly from assigns hash
|
||||
# 2. **No Filter Dispatch**: Filters compiled to direct Ruby calls
|
||||
# 3. **No Resource Limits**: No per-node overhead for limit tracking
|
||||
# 4. **Native Scoping**: Ruby's block scoping instead of manual stacks
|
||||
# 5. **Direct Concatenation**: Output built with << operations
|
||||
# 6. **Native Control Flow**: break/continue use Ruby's throw/catch
|
||||
# 7. **No to_liquid Calls**: Values used directly
|
||||
# 8. **No Profiling Hooks**: No profiler overhead
|
||||
#
|
||||
# ## Limitations
|
||||
#
|
||||
# - {% render %} and {% include %} resolved at compile time when possible
|
||||
# - Custom tags need explicit compiler implementations
|
||||
# - Custom filters must be available at runtime
|
||||
#
|
||||
module Liquid
|
||||
module Compile
|
||||
autoload :CompiledTemplate, 'liquid/compile/compiled_template'
|
||||
autoload :CompiledContext, 'liquid/compile/compiled_context'
|
||||
autoload :CodeGenerator, 'liquid/compile/code_generator'
|
||||
autoload :RubyCompiler, 'liquid/compile/ruby_compiler'
|
||||
autoload :ExpressionCompiler, 'liquid/compile/expression_compiler'
|
||||
autoload :FilterCompiler, 'liquid/compile/filter_compiler'
|
||||
autoload :VariableCompiler, 'liquid/compile/variable_compiler'
|
||||
autoload :BlockBodyCompiler, 'liquid/compile/block_body_compiler'
|
||||
autoload :ConditionCompiler, 'liquid/compile/condition_compiler'
|
||||
autoload :SourceMapper, 'liquid/compile/source_mapper'
|
||||
|
||||
module Tags
|
||||
autoload :IfCompiler, 'liquid/compile/tags/if_compiler'
|
||||
autoload :UnlessCompiler, 'liquid/compile/tags/unless_compiler'
|
||||
autoload :CaseCompiler, 'liquid/compile/tags/case_compiler'
|
||||
autoload :ForCompiler, 'liquid/compile/tags/for_compiler'
|
||||
autoload :AssignCompiler, 'liquid/compile/tags/assign_compiler'
|
||||
autoload :CaptureCompiler, 'liquid/compile/tags/capture_compiler'
|
||||
autoload :CycleCompiler, 'liquid/compile/tags/cycle_compiler'
|
||||
autoload :IncrementCompiler, 'liquid/compile/tags/increment_compiler'
|
||||
autoload :DecrementCompiler, 'liquid/compile/tags/decrement_compiler'
|
||||
autoload :RawCompiler, 'liquid/compile/tags/raw_compiler'
|
||||
autoload :EchoCompiler, 'liquid/compile/tags/echo_compiler'
|
||||
autoload :BreakCompiler, 'liquid/compile/tags/break_compiler'
|
||||
autoload :ContinueCompiler, 'liquid/compile/tags/continue_compiler'
|
||||
autoload :CommentCompiler, 'liquid/compile/tags/comment_compiler'
|
||||
autoload :TableRowCompiler, 'liquid/compile/tags/tablerow_compiler'
|
||||
autoload :RenderCompiler, 'liquid/compile/tags/render_compiler'
|
||||
autoload :IncludeCompiler, 'liquid/compile/tags/include_compiler'
|
||||
autoload :IfchangedCompiler, 'liquid/compile/tags/ifchanged_compiler'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,49 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
# BlockBodyCompiler compiles a BlockBody (a list of nodes) to Ruby code.
|
||||
#
|
||||
# A BlockBody contains:
|
||||
# - String literals (text to output)
|
||||
# - Variable expressions ({{ ... }})
|
||||
# - Tags ({% ... %})
|
||||
class BlockBodyCompiler
|
||||
# Compile a BlockBody to Ruby code
|
||||
# @param body [Liquid::BlockBody] The block body
|
||||
# @param compiler [RubyCompiler] The main compiler instance
|
||||
# @param code [CodeGenerator] The code generator
|
||||
def self.compile(body, compiler, code)
|
||||
return if body.nil?
|
||||
|
||||
nodelist = body.nodelist
|
||||
return if nodelist.nil? || nodelist.empty?
|
||||
|
||||
nodelist.each do |node|
|
||||
compile_node(node, compiler, code)
|
||||
end
|
||||
end
|
||||
|
||||
# Compile a single node
|
||||
def self.compile_node(node, compiler, code)
|
||||
case node
|
||||
when String
|
||||
compile_string(node, code)
|
||||
when Variable
|
||||
VariableCompiler.compile(node, compiler, code)
|
||||
when Tag
|
||||
compiler.send(:compile_tag, node, code)
|
||||
else
|
||||
raise CompileError, "Unknown node type in BlockBody: #{node.class}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.compile_string(str, code)
|
||||
return if str.empty?
|
||||
code.line "__output__ << #{str.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,82 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
# CodeGenerator provides a clean interface for building Ruby code strings
|
||||
# with proper indentation and formatting.
|
||||
class CodeGenerator
|
||||
INDENT_SIZE = 2
|
||||
|
||||
def initialize
|
||||
@lines = []
|
||||
@indent_level = 0
|
||||
end
|
||||
|
||||
# Add a line of code at the current indentation level
|
||||
# @param text [String] The code to add
|
||||
def line(text)
|
||||
@lines << (" " * @indent_level) + text
|
||||
end
|
||||
|
||||
# Add a blank line
|
||||
def blank_line
|
||||
@lines << ""
|
||||
end
|
||||
|
||||
# Add multiple lines (useful for multi-line strings)
|
||||
# @param text [String] Multi-line string to add
|
||||
def lines(text)
|
||||
text.each_line do |l|
|
||||
line(l.chomp)
|
||||
end
|
||||
end
|
||||
|
||||
# Increase indentation for a block
|
||||
def indent
|
||||
@indent_level += 1
|
||||
yield
|
||||
@indent_level -= 1
|
||||
end
|
||||
|
||||
# Get the current indentation string
|
||||
def current_indent
|
||||
" " * @indent_level
|
||||
end
|
||||
|
||||
# Add raw code without indentation adjustment
|
||||
def raw(text)
|
||||
@lines << text
|
||||
end
|
||||
|
||||
# Convert to final Ruby code string
|
||||
def to_s
|
||||
@lines.join("\n")
|
||||
end
|
||||
|
||||
# Generate an inline expression (doesn't add to lines, returns string)
|
||||
# @param expr [String] The expression
|
||||
# @return [String] The expression wrapped appropriately
|
||||
def self.inline(expr)
|
||||
expr
|
||||
end
|
||||
|
||||
# Generate a string literal
|
||||
# @param str [String] The string to escape
|
||||
# @return [String] Ruby string literal
|
||||
def self.string_literal(str)
|
||||
str.inspect
|
||||
end
|
||||
|
||||
# Generate a safe variable name from a Liquid variable name
|
||||
# @param name [String] The Liquid variable name
|
||||
# @return [String] A safe Ruby variable name
|
||||
def self.safe_var_name(name)
|
||||
# Replace invalid characters with underscores
|
||||
safe = name.to_s.gsub(/[^a-zA-Z0-9_]/, '_')
|
||||
# Ensure it starts with a letter or underscore
|
||||
safe = "_#{safe}" if safe =~ /\A\d/
|
||||
safe
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,81 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
# CompiledContext is a lightweight context-like object for compiled templates.
|
||||
#
|
||||
# It duck-types to Liquid::Context well enough for Drops to work, providing:
|
||||
# - Variable lookup via [] and find_variable
|
||||
# - strict_variables flag
|
||||
# - registers hash
|
||||
# - evaluate method for expressions
|
||||
#
|
||||
# This allows Drops to access other variables and use context features
|
||||
# while still running in compiled mode.
|
||||
class CompiledContext
|
||||
attr_reader :assigns, :registers
|
||||
attr_accessor :strict_variables, :strict_filters
|
||||
|
||||
def initialize(assigns, registers: {}, strict_variables: false, strict_filters: false)
|
||||
@assigns = assigns
|
||||
@registers = registers.is_a?(Liquid::Registers) ? registers : Liquid::Registers.new(registers)
|
||||
@strict_variables = strict_variables
|
||||
@strict_filters = strict_filters
|
||||
end
|
||||
|
||||
# Variable lookup - used by Drops to access other variables
|
||||
def [](key)
|
||||
@assigns[key.to_s]
|
||||
end
|
||||
|
||||
# Find a variable by name
|
||||
def find_variable(key)
|
||||
result = @assigns[key.to_s]
|
||||
result = result.to_liquid if result.respond_to?(:to_liquid)
|
||||
result.context = self if result.respond_to?(:context=)
|
||||
result
|
||||
end
|
||||
|
||||
# Evaluate an expression (for Drops that need to evaluate sub-expressions)
|
||||
def evaluate(expr)
|
||||
case expr
|
||||
when String, Integer, Float, TrueClass, FalseClass, NilClass
|
||||
expr
|
||||
when Liquid::VariableLookup
|
||||
expr.evaluate(self)
|
||||
else
|
||||
expr
|
||||
end
|
||||
end
|
||||
|
||||
# Lookup and evaluate - handles Procs in assigns
|
||||
def lookup_and_evaluate(obj, key)
|
||||
value = obj[key]
|
||||
value = value.call(self) if value.is_a?(Proc)
|
||||
value
|
||||
end
|
||||
|
||||
# Handle errors (simplified - just return message)
|
||||
def handle_error(error, _line_number = nil)
|
||||
error.message
|
||||
end
|
||||
|
||||
# Check if execution should be interrupted
|
||||
def interrupt?
|
||||
false
|
||||
end
|
||||
|
||||
# Stub for resource limits (no-op in compiled mode)
|
||||
def resource_limits
|
||||
@resource_limits ||= ResourceLimitStub.new
|
||||
end
|
||||
end
|
||||
|
||||
# Stub for resource limits in compiled mode
|
||||
class ResourceLimitStub
|
||||
def increment_render_score(_score); end
|
||||
def increment_write_score(_output); end
|
||||
def reached?; false; end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,349 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
# CompiledTemplate represents a compiled Liquid template ready for secure execution.
|
||||
#
|
||||
# This class wraps generated Ruby code and provides a secure execution environment
|
||||
# using Liquid::Box. On Ruby 4.0+, execution happens in a true sandbox. On earlier
|
||||
# versions, a polyfill is used with a security warning.
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# template = Liquid::Template.parse("Hello, {{ name }}!")
|
||||
# compiled = template.compile_to_ruby
|
||||
#
|
||||
# # Render with a Liquid::Context (preferred)
|
||||
# context = Liquid::Context.new({ "name" => "World" })
|
||||
# result = compiled.render(context)
|
||||
# # => "Hello, World!"
|
||||
#
|
||||
# # Or render with a simple hash
|
||||
# result = compiled.render({ "name" => "World" })
|
||||
#
|
||||
class CompiledTemplate
|
||||
attr_reader :source, :external_tags
|
||||
attr_accessor :filter_handler
|
||||
|
||||
# @param source [String] The generated Ruby code
|
||||
# @param external_tags [Hash] Map of variable names to Tag objects for runtime delegation
|
||||
# @param has_external_filters [Boolean] Whether external filters are used
|
||||
def initialize(source, external_tags = {}, has_external_filters = false)
|
||||
@source = source
|
||||
@external_tags = external_tags
|
||||
@has_external_filters = has_external_filters
|
||||
@filter_handler = nil
|
||||
@proc = nil
|
||||
@box = nil
|
||||
end
|
||||
|
||||
# Returns true if this template has external tags that need runtime delegation
|
||||
def has_external_tags?
|
||||
!@external_tags.empty?
|
||||
end
|
||||
|
||||
# Returns true if this template uses external filters
|
||||
def has_external_filters?
|
||||
@has_external_filters
|
||||
end
|
||||
|
||||
# Returns true if execution will be sandboxed (Ruby 4.0+)
|
||||
def secure?
|
||||
Liquid::Box.secure?
|
||||
end
|
||||
|
||||
# Render the compiled template.
|
||||
#
|
||||
# @param context_or_assigns [Liquid::Context, Hash] A Liquid context or hash of assigns
|
||||
# @param registers [Hash] Registers (only used when passing a Hash)
|
||||
# @param filter_handler [Object] Optional filter handler module
|
||||
# @yield [call_type, *args] Called for external tags/filters only
|
||||
# @return [String] The rendered output
|
||||
#
|
||||
def render(context_or_assigns = {}, registers: {}, filter_handler: nil, &block)
|
||||
compiled_proc = to_proc
|
||||
handler = filter_handler || @filter_handler
|
||||
|
||||
# Accept either a Liquid::Context or a Hash of assigns
|
||||
if context_or_assigns.is_a?(Liquid::Context)
|
||||
liquid_context = context_or_assigns
|
||||
assigns = extract_assigns(liquid_context)
|
||||
file_system = liquid_context.registers[:file_system]
|
||||
|
||||
# Build external handler that handles include/render internally
|
||||
external_handler = build_external_handler(liquid_context, file_system, handler, &block)
|
||||
|
||||
# Use a wrapper context that delegates to the Liquid::Context
|
||||
context = ContextWrapper.new(liquid_context)
|
||||
else
|
||||
assigns = context_or_assigns
|
||||
file_system = registers[:file_system]
|
||||
|
||||
# Create a minimal context for Drop support
|
||||
context = CompiledContext.new(assigns, registers: registers)
|
||||
|
||||
# Build external handler
|
||||
external_handler = build_external_handler(nil, file_system, handler, &block)
|
||||
end
|
||||
|
||||
compiled_proc.call(assigns, context, external_handler)
|
||||
end
|
||||
|
||||
alias call render
|
||||
|
||||
def code
|
||||
@source
|
||||
end
|
||||
|
||||
def to_s
|
||||
@source
|
||||
end
|
||||
|
||||
def to_proc
|
||||
@proc ||= compile_to_proc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extract_assigns(liquid_context)
|
||||
# Get the first environment (static_environments)
|
||||
liquid_context.environments.first || {}
|
||||
end
|
||||
|
||||
# Build the external call handler
|
||||
# Handles :include and :render internally using file_system
|
||||
# Yields to block for :tag and :filter if block given
|
||||
def build_external_handler(liquid_context, file_system, filter_handler, &block)
|
||||
external_tags = @external_tags
|
||||
|
||||
->(call_type, *args) do
|
||||
case call_type
|
||||
when :include
|
||||
handle_include(liquid_context, file_system, *args)
|
||||
|
||||
when :render
|
||||
handle_render(liquid_context, file_system, *args)
|
||||
|
||||
when :tag
|
||||
if block
|
||||
block.call(call_type, *args)
|
||||
else
|
||||
handle_tag(liquid_context, external_tags, *args)
|
||||
end
|
||||
|
||||
when :filter
|
||||
if block
|
||||
block.call(call_type, *args)
|
||||
else
|
||||
handle_filter(liquid_context, filter_handler, *args)
|
||||
end
|
||||
|
||||
else
|
||||
if block
|
||||
block.call(call_type, *args)
|
||||
else
|
||||
raise ArgumentError, "Unknown external call type: #{call_type}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_include(liquid_context, file_system, template_name, variable, attrs, alias_name, assigns, context)
|
||||
raise Liquid::FileSystemError, "Could not find asset #{template_name}" unless file_system
|
||||
|
||||
snippet_source = file_system.read_template_file(template_name)
|
||||
snippet = Liquid::Template.parse(snippet_source, line_numbers: true)
|
||||
snippet.name = template_name
|
||||
|
||||
# Include shares scope with parent
|
||||
if liquid_context
|
||||
# Set attributes in context
|
||||
attrs&.each { |k, v| liquid_context[k] = v }
|
||||
|
||||
context_var_name = alias_name || template_name.to_s.split('/').last
|
||||
if variable
|
||||
if variable.is_a?(Array)
|
||||
return variable.map do |item|
|
||||
liquid_context[context_var_name] = item
|
||||
snippet.render(liquid_context)
|
||||
end.join
|
||||
else
|
||||
liquid_context[context_var_name] = variable
|
||||
end
|
||||
end
|
||||
|
||||
snippet.render(liquid_context)
|
||||
else
|
||||
# No liquid context - just use assigns
|
||||
render_assigns = assigns.merge(attrs || {})
|
||||
snippet.render(render_assigns)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_render(liquid_context, file_system, template_name, variable, attrs, alias_name, is_for_loop, context)
|
||||
raise Liquid::FileSystemError, "Could not find asset #{template_name}" unless file_system
|
||||
|
||||
snippet_source = file_system.read_template_file(template_name)
|
||||
snippet = Liquid::Template.parse(snippet_source, line_numbers: true)
|
||||
snippet.name = template_name
|
||||
|
||||
# Render creates isolated scope - only attrs are passed
|
||||
render_assigns = attrs&.dup || {}
|
||||
context_var_name = alias_name || template_name.to_s.split('/').last.sub(/\.liquid$/, '')
|
||||
|
||||
if variable
|
||||
if is_for_loop && variable.is_a?(Array)
|
||||
return variable.map do |item|
|
||||
render_assigns[context_var_name] = item
|
||||
if liquid_context
|
||||
isolated_ctx = Liquid::Context.build(
|
||||
static_environments: render_assigns,
|
||||
registers: liquid_context.registers,
|
||||
rethrow_errors: false,
|
||||
)
|
||||
isolated_ctx.exception_renderer = liquid_context.exception_renderer
|
||||
snippet.render(isolated_ctx)
|
||||
else
|
||||
snippet.render(render_assigns)
|
||||
end
|
||||
end.join
|
||||
else
|
||||
render_assigns[context_var_name] = variable
|
||||
end
|
||||
end
|
||||
|
||||
if liquid_context
|
||||
isolated_ctx = Liquid::Context.build(
|
||||
static_environments: render_assigns,
|
||||
registers: liquid_context.registers,
|
||||
rethrow_errors: false,
|
||||
)
|
||||
isolated_ctx.exception_renderer = liquid_context.exception_renderer
|
||||
snippet.render(isolated_ctx)
|
||||
else
|
||||
snippet.render(render_assigns)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_tag(liquid_context, external_tags, tag_var, tag_assigns)
|
||||
tag = external_tags[tag_var]
|
||||
return '' unless tag
|
||||
|
||||
if liquid_context
|
||||
output = +''
|
||||
tag.render_to_output_buffer(liquid_context, output)
|
||||
output
|
||||
else
|
||||
# Create a minimal context
|
||||
ctx = Liquid::Context.new([tag_assigns], {}, {}, false, nil, {}, Liquid::Environment.default)
|
||||
output = +''
|
||||
tag.render_to_output_buffer(ctx, output)
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
def handle_filter(liquid_context, filter_handler, filter_name, input, *filter_args)
|
||||
# Try filter handler first
|
||||
if filter_handler&.respond_to?(filter_name)
|
||||
return filter_handler.public_send(filter_name, input, *filter_args)
|
||||
end
|
||||
|
||||
# Try liquid context's strainer
|
||||
if liquid_context
|
||||
strainer = liquid_context.strainer
|
||||
if strainer.class.invokable?(filter_name)
|
||||
return strainer.invoke(filter_name, input, *filter_args)
|
||||
end
|
||||
end
|
||||
|
||||
# Return input unchanged if filter not found
|
||||
input
|
||||
end
|
||||
|
||||
def compile_to_proc
|
||||
if Liquid::Box.secure?
|
||||
compile_in_sandbox
|
||||
else
|
||||
compile_insecure
|
||||
end
|
||||
end
|
||||
|
||||
def compile_in_sandbox
|
||||
@box ||= begin
|
||||
box = Liquid::Box.new
|
||||
box.load_liquid_runtime!
|
||||
box.lock!
|
||||
box
|
||||
end
|
||||
|
||||
template_class_name = "CompiledTemplate_#{object_id}"
|
||||
class_code = <<~RUBY
|
||||
class #{template_class_name}
|
||||
TEMPLATE_PROC = #{@source}
|
||||
|
||||
def self.render(*args, &block)
|
||||
TEMPLATE_PROC.call(*args, &block)
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
@box.eval(class_code)
|
||||
template_class = @box[template_class_name]
|
||||
|
||||
->(assigns, context, external_handler) do
|
||||
template_class.render(assigns, context, external_handler)
|
||||
end
|
||||
end
|
||||
|
||||
def compile_insecure
|
||||
warn_once_insecure unless Liquid::Box.secure?
|
||||
|
||||
require_relative 'runtime' unless defined?(::LR)
|
||||
|
||||
# rubocop:disable Security/Eval
|
||||
eval(@source)
|
||||
# rubocop:enable Security/Eval
|
||||
end
|
||||
|
||||
def warn_once_insecure
|
||||
return if @warned_insecure
|
||||
|
||||
@warned_insecure = true
|
||||
warn "[SECURITY WARNING] Liquid compiled template running outside of Ruby::Box sandbox. " \
|
||||
"On Ruby 4.0+, this runs in a secure sandbox. On earlier versions, be cautious " \
|
||||
"about running untrusted templates."
|
||||
end
|
||||
|
||||
# Wrapper around Liquid::Context for compiled template compatibility
|
||||
class ContextWrapper
|
||||
def initialize(liquid_context)
|
||||
@liquid_context = liquid_context
|
||||
end
|
||||
|
||||
def [](key)
|
||||
@liquid_context[key]
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
@liquid_context[key] = value
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
@liquid_context.key?(key)
|
||||
end
|
||||
|
||||
def registers
|
||||
@liquid_context.registers
|
||||
end
|
||||
|
||||
def strainer
|
||||
@liquid_context.strainer
|
||||
end
|
||||
|
||||
def handle_error(e, line_number = nil)
|
||||
@liquid_context.handle_error(e, line_number)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,145 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
# ConditionCompiler compiles Liquid conditions to Ruby boolean expressions.
|
||||
#
|
||||
# Handles:
|
||||
# - Simple truthiness: {% if variable %}
|
||||
# - Comparisons: {% if a == b %}, {% if a > b %}
|
||||
# - Logical operators: {% if a and b %}, {% if a or b %}
|
||||
# - Special checks: {% if a == blank %}, {% if a == empty %}
|
||||
class ConditionCompiler
|
||||
# Operator mappings from Liquid to Ruby
|
||||
OPERATORS = {
|
||||
'==' => '==',
|
||||
'!=' => '!=',
|
||||
'<>' => '!=',
|
||||
'<' => '<',
|
||||
'>' => '>',
|
||||
'<=' => '<=',
|
||||
'>=' => '>=',
|
||||
'contains' => :contains,
|
||||
}.freeze
|
||||
|
||||
# Compile a Condition to a Ruby boolean expression
|
||||
# @param condition [Liquid::Condition] The condition
|
||||
# @param compiler [RubyCompiler] The main compiler instance
|
||||
# @return [String] Ruby code expression that evaluates to true/false
|
||||
def self.compile(condition, compiler)
|
||||
if condition.is_a?(ElseCondition)
|
||||
return "true"
|
||||
end
|
||||
|
||||
compile_condition_chain(condition, compiler)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.compile_condition_chain(condition, compiler)
|
||||
# Compile the current condition
|
||||
current = compile_single_condition(condition, compiler)
|
||||
|
||||
# Check for chained conditions (and/or)
|
||||
if condition.child_condition
|
||||
child = compile_condition_chain(condition.child_condition, compiler)
|
||||
child_relation = condition.send(:child_relation)
|
||||
|
||||
case child_relation
|
||||
when :and
|
||||
"(#{current} && #{child})"
|
||||
when :or
|
||||
"(#{current} || #{child})"
|
||||
else
|
||||
current
|
||||
end
|
||||
else
|
||||
current
|
||||
end
|
||||
end
|
||||
|
||||
def self.compile_single_condition(condition, compiler)
|
||||
left = condition.left
|
||||
op = condition.operator
|
||||
right = condition.right
|
||||
|
||||
# If no operator, just check truthiness
|
||||
# Inline: Liquid truthiness is "not nil and not false"
|
||||
if op.nil?
|
||||
left_expr = ExpressionCompiler.compile(left, compiler)
|
||||
# For simple variable access, we can use a more compact form
|
||||
# Complex expressions need temp variable to avoid double evaluation
|
||||
if simple_expression?(left)
|
||||
return "(#{left_expr} != nil && #{left_expr} != false)"
|
||||
else
|
||||
return "((__v__ = #{left_expr}) != nil && __v__ != false)"
|
||||
end
|
||||
end
|
||||
|
||||
# Compile left and right expressions
|
||||
left_expr = compile_condition_value(left, compiler)
|
||||
right_expr = compile_condition_value(right, compiler)
|
||||
|
||||
# Handle special operators
|
||||
case OPERATORS[op]
|
||||
when :contains
|
||||
compile_contains(left_expr, right_expr, compiler)
|
||||
when '=='
|
||||
compile_equality(left, right, left_expr, right_expr, compiler)
|
||||
when '!='
|
||||
"!(#{compile_equality(left, right, left_expr, right_expr, compiler)})"
|
||||
else
|
||||
# Standard comparison
|
||||
ruby_op = OPERATORS[op] || op
|
||||
"(#{left_expr} #{ruby_op} #{right_expr} rescue false)"
|
||||
end
|
||||
end
|
||||
|
||||
def self.compile_condition_value(expr, compiler)
|
||||
if expr.is_a?(Condition::MethodLiteral)
|
||||
# For blank/empty checks, we return a special marker
|
||||
# The equality handler will deal with this
|
||||
":__method_literal_#{expr.method_name}__"
|
||||
else
|
||||
ExpressionCompiler.compile(expr, compiler)
|
||||
end
|
||||
end
|
||||
|
||||
def self.compile_equality(left, right, left_expr, right_expr, compiler)
|
||||
# Handle blank/empty method literals
|
||||
if left.is_a?(Condition::MethodLiteral)
|
||||
method_name = left.method_name
|
||||
"(#{right_expr}.respond_to?(:#{method_name}) ? #{right_expr}.#{method_name} : nil)"
|
||||
elsif right.is_a?(Condition::MethodLiteral)
|
||||
method_name = right.method_name
|
||||
"(#{left_expr}.respond_to?(:#{method_name}) ? #{left_expr}.#{method_name} : nil)"
|
||||
else
|
||||
"(#{left_expr} == #{right_expr})"
|
||||
end
|
||||
end
|
||||
|
||||
def self.compile_contains(left_expr, right_expr, compiler)
|
||||
# The contains operator checks if left includes right
|
||||
# For strings, right is converted to a string
|
||||
"(lambda { |left, right| " \
|
||||
"return false if left.nil? || right.nil? || !left.respond_to?(:include?); " \
|
||||
"right = right.to_s if left.is_a?(String); " \
|
||||
"left.include?(right) rescue false " \
|
||||
"}.call(#{left_expr}, #{right_expr}))"
|
||||
end
|
||||
|
||||
# Check if an expression is simple (doesn't need temp variable to avoid double evaluation)
|
||||
def self.simple_expression?(expr)
|
||||
case expr
|
||||
when nil, true, false, Integer, Float, String
|
||||
true
|
||||
when VariableLookup
|
||||
# Simple variable or property access is safe to evaluate twice
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,155 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
# ExpressionCompiler compiles Liquid expressions to Ruby code.
|
||||
#
|
||||
# Expressions include:
|
||||
# - Literals: nil, true, false, strings, numbers
|
||||
# - Variable lookups: foo, foo.bar, foo[0], foo["key"]
|
||||
# - Ranges: (1..10), (start..end)
|
||||
class ExpressionCompiler
|
||||
# Compile an expression to a Ruby code string
|
||||
# @param expr [Object] The parsed expression
|
||||
# @param compiler [RubyCompiler] The main compiler instance
|
||||
# @return [String] Ruby code that evaluates to the expression value
|
||||
def self.compile(expr, compiler)
|
||||
case expr
|
||||
when nil
|
||||
"nil"
|
||||
when true
|
||||
"true"
|
||||
when false
|
||||
"false"
|
||||
when String
|
||||
expr.inspect
|
||||
when Integer, Float
|
||||
expr.inspect
|
||||
when Range
|
||||
"(#{expr.begin.inspect}..#{expr.end.inspect})"
|
||||
when VariableLookup
|
||||
compile_variable_lookup(expr, compiler)
|
||||
when RangeLookup
|
||||
compile_range_lookup(expr, compiler)
|
||||
when Condition::MethodLiteral
|
||||
# Handle blank/empty method literals
|
||||
compile_method_literal(expr, compiler)
|
||||
else
|
||||
raise CompileError, "Unknown expression type: #{expr.class} (#{expr.inspect})"
|
||||
end
|
||||
end
|
||||
|
||||
# Compile a variable lookup expression
|
||||
# @param lookup [VariableLookup] The variable lookup
|
||||
# @param compiler [RubyCompiler] The main compiler instance
|
||||
# @return [String] Ruby code that evaluates to the variable value
|
||||
def self.compile_variable_lookup(lookup, compiler)
|
||||
# Start with the base variable
|
||||
name = lookup.name
|
||||
|
||||
# Check for forloop property inlining
|
||||
if name == 'forloop' && lookup.lookups.length == 1
|
||||
loop_ctx = compiler.current_loop_context
|
||||
if loop_ctx && loop_ctx[:idx_var]
|
||||
inlined = compile_forloop_property(lookup.lookups.first, loop_ctx)
|
||||
return inlined if inlined
|
||||
end
|
||||
end
|
||||
|
||||
# Handle dynamic name (expression in brackets)
|
||||
base = if name.is_a?(VariableLookup) || name.is_a?(RangeLookup)
|
||||
# Dynamic name like [expr].foo
|
||||
"assigns[#{compile(name, compiler)}]"
|
||||
elsif name.is_a?(String)
|
||||
"assigns[#{name.inspect}]"
|
||||
elsif name.is_a?(Integer)
|
||||
"assigns[#{name.inspect}]"
|
||||
else
|
||||
compile(name, compiler)
|
||||
end
|
||||
|
||||
# Apply each lookup in the chain
|
||||
lookup.lookups.each_with_index do |key, index|
|
||||
if key.is_a?(VariableLookup) || key.is_a?(RangeLookup)
|
||||
# Dynamic key like foo[expr]
|
||||
base = "LR.lookup(#{base}, #{compile(key, compiler)}, __context__)"
|
||||
elsif key.is_a?(Integer)
|
||||
# Numeric index like foo[0]
|
||||
base = "LR.lookup(#{base}, #{key}, __context__)"
|
||||
elsif key.is_a?(String)
|
||||
# Always use LR.lookup which tries key access first,
|
||||
# then falls back to method call for command methods (first, last, size)
|
||||
base = "LR.lookup(#{base}, #{key.inspect}, __context__)"
|
||||
else
|
||||
base = "LR.lookup(#{base}, #{compile(key, compiler)}, __context__)"
|
||||
end
|
||||
end
|
||||
|
||||
base
|
||||
end
|
||||
|
||||
# Inline forloop property access to avoid hash allocation
|
||||
# @param prop [String] Property name (index, index0, first, last, etc.)
|
||||
# @param loop_ctx [Hash] Loop context with idx_var, len_var, loop_name
|
||||
# @return [String, nil] Inlined Ruby code or nil if can't inline
|
||||
def self.compile_forloop_property(prop, loop_ctx)
|
||||
idx = loop_ctx[:idx_var]
|
||||
len = loop_ctx[:len_var]
|
||||
name = loop_ctx[:loop_name]
|
||||
|
||||
case prop
|
||||
when 'index'
|
||||
"(#{idx} + 1)"
|
||||
when 'index0'
|
||||
idx
|
||||
when 'rindex'
|
||||
"(#{len} - #{idx})"
|
||||
when 'rindex0'
|
||||
"(#{len} - #{idx} - 1)"
|
||||
when 'first'
|
||||
"(#{idx} == 0)"
|
||||
when 'last'
|
||||
"(#{idx} == #{len} - 1)"
|
||||
when 'length'
|
||||
len
|
||||
when 'name'
|
||||
name ? name.inspect : "nil"
|
||||
else
|
||||
nil # Unknown property, fall back to hash lookup
|
||||
end
|
||||
end
|
||||
|
||||
# Compile a range lookup expression
|
||||
# @param range [RangeLookup] The range lookup
|
||||
# @param compiler [RubyCompiler] The main compiler instance
|
||||
# @return [String] Ruby code that evaluates to the range
|
||||
def self.compile_range_lookup(range, compiler)
|
||||
start_expr = compile(range.start_obj, compiler)
|
||||
end_expr = compile(range.end_obj, compiler)
|
||||
|
||||
# Convert to integers and create range
|
||||
"(LR.to_integer(#{start_expr})...LR.to_integer(#{end_expr})).to_a"
|
||||
end
|
||||
|
||||
# Compile a method literal (blank/empty)
|
||||
def self.compile_method_literal(literal, compiler)
|
||||
# These are used in conditions like `if foo == blank`
|
||||
# They represent special method calls
|
||||
literal.to_s.inspect
|
||||
end
|
||||
|
||||
# Compile an expression for use in a condition
|
||||
# @param expr [Object] The expression
|
||||
# @param compiler [RubyCompiler] The main compiler
|
||||
# @return [String] Ruby code for the condition value
|
||||
def self.compile_for_condition(expr, compiler)
|
||||
if expr.is_a?(Condition::MethodLiteral)
|
||||
# Return the method name symbol for special handling
|
||||
":#{expr.method_name}"
|
||||
else
|
||||
compile(expr, compiler)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,230 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
# FilterCompiler compiles Liquid filter chains to Ruby code.
|
||||
#
|
||||
# Filters are applied in sequence: {{ value | filter1: arg1 | filter2: arg2 }}
|
||||
# becomes a chain of method calls on the value.
|
||||
class FilterCompiler
|
||||
# Standard filters that map directly to Ruby methods or simple expressions
|
||||
SIMPLE_FILTERS = {
|
||||
'size' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:size) ? #{input}.size : 0)" },
|
||||
'downcase' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).downcase" },
|
||||
'upcase' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).upcase" },
|
||||
'capitalize' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).capitalize" },
|
||||
'strip' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).strip" },
|
||||
'lstrip' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).lstrip" },
|
||||
'rstrip' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).rstrip" },
|
||||
'reverse' => ->(input, _args, _kwargs, _compiler) { "(#{input}.is_a?(Array) ? #{input}.reverse : LR.to_s(#{input}).reverse)" },
|
||||
'first' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:first) ? #{input}.first : nil)" },
|
||||
'last' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:last) ? #{input}.last : nil)" },
|
||||
'uniq' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:uniq) ? #{input}.uniq : #{input})" },
|
||||
'compact' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:compact) ? #{input}.compact : #{input})" },
|
||||
'flatten' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:flatten) ? #{input}.flatten : #{input})" },
|
||||
'sort' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:sort) ? #{input}.sort : #{input})" },
|
||||
'abs' => ->(input, _args, _kwargs, _compiler) { "LR.to_number(#{input}).abs" },
|
||||
'ceil' => ->(input, _args, _kwargs, _compiler) { "LR.to_number(#{input}).ceil.to_i" },
|
||||
'floor' => ->(input, _args, _kwargs, _compiler) { "LR.to_number(#{input}).floor.to_i" },
|
||||
'escape' => ->(input, _args, _kwargs, _compiler) { "(#{input}.nil? ? nil : LR.escape_html(#{input}))" },
|
||||
'h' => ->(input, _args, _kwargs, _compiler) { "(#{input}.nil? ? nil : LR.escape_html(#{input}))" },
|
||||
'url_encode' => ->(input, _args, _kwargs, _compiler) { "(#{input}.nil? ? nil : LR.url_encode(#{input}))" },
|
||||
'url_decode' => ->(input, _args, _kwargs, _compiler) { "(#{input}.nil? ? nil : LR.url_decode(#{input}))" },
|
||||
'base64_encode' => ->(input, _args, _kwargs, _compiler) { "LR.base64_encode(#{input})" },
|
||||
'base64_decode' => ->(input, _args, _kwargs, _compiler) { "LR.base64_decode(#{input})" },
|
||||
'base64_url_safe_encode' => ->(input, _args, _kwargs, _compiler) { "LR.base64_url_safe_encode(#{input})" },
|
||||
'base64_url_safe_decode' => ->(input, _args, _kwargs, _compiler) { "LR.base64_url_safe_decode(#{input})" },
|
||||
'strip_html' => ->(input, _args, _kwargs, _compiler) { "LR.strip_html(#{input})" },
|
||||
'strip_newlines' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).gsub(/\\r?\\n/, '')" },
|
||||
'newline_to_br' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).gsub(/\\r?\\n/, \"<br />\\n\")" },
|
||||
}.freeze
|
||||
|
||||
# Filters with arguments that need special handling
|
||||
# All use LR.method() calls to pre-loaded runtime
|
||||
PARAMETERIZED_FILTERS = {
|
||||
'append' => ->(input, args, _kwargs, compiler) {
|
||||
arg = compile_arg(args[0], compiler)
|
||||
"LR.to_s(#{input}) + LR.to_s(#{arg})"
|
||||
},
|
||||
'prepend' => ->(input, args, _kwargs, compiler) {
|
||||
arg = compile_arg(args[0], compiler)
|
||||
"LR.to_s(#{arg}) + LR.to_s(#{input})"
|
||||
},
|
||||
'plus' => ->(input, args, _kwargs, compiler) {
|
||||
arg = compile_arg(args[0], compiler)
|
||||
"(LR.to_number(#{input}) + LR.to_number(#{arg}))"
|
||||
},
|
||||
'minus' => ->(input, args, _kwargs, compiler) {
|
||||
arg = compile_arg(args[0], compiler)
|
||||
"(LR.to_number(#{input}) - LR.to_number(#{arg}))"
|
||||
},
|
||||
'times' => ->(input, args, _kwargs, compiler) {
|
||||
arg = compile_arg(args[0], compiler)
|
||||
"(LR.to_number(#{input}) * LR.to_number(#{arg}))"
|
||||
},
|
||||
'divided_by' => ->(input, args, _kwargs, compiler) {
|
||||
arg = compile_arg(args[0], compiler)
|
||||
"(LR.to_number(#{input}) / LR.to_number(#{arg}))"
|
||||
},
|
||||
'modulo' => ->(input, args, _kwargs, compiler) {
|
||||
arg = compile_arg(args[0], compiler)
|
||||
"(LR.to_number(#{input}) % LR.to_number(#{arg}))"
|
||||
},
|
||||
'round' => ->(input, args, _kwargs, compiler) {
|
||||
if args.empty?
|
||||
"LR.to_number(#{input}).round.to_i"
|
||||
else
|
||||
arg = compile_arg(args[0], compiler)
|
||||
"LR.to_number(#{input}).round(LR.to_number(#{arg}))"
|
||||
end
|
||||
},
|
||||
'at_least' => ->(input, args, _kwargs, compiler) {
|
||||
arg = compile_arg(args[0], compiler)
|
||||
"[LR.to_number(#{input}), LR.to_number(#{arg})].max"
|
||||
},
|
||||
'at_most' => ->(input, args, _kwargs, compiler) {
|
||||
arg = compile_arg(args[0], compiler)
|
||||
"[LR.to_number(#{input}), LR.to_number(#{arg})].min"
|
||||
},
|
||||
'default' => ->(input, args, kwargs, compiler) {
|
||||
default_val = args.empty? ? "''" : compile_arg(args[0], compiler)
|
||||
allow_false = kwargs && kwargs['allow_false'] ? "allow_false: #{compile_arg(kwargs['allow_false'], compiler)}" : ''
|
||||
"LR.default(#{input}, #{default_val}#{allow_false.empty? ? '' : ', ' + allow_false})"
|
||||
},
|
||||
'split' => ->(input, args, _kwargs, compiler) {
|
||||
pattern = args.empty? ? "' '" : compile_arg(args[0], compiler)
|
||||
"LR.to_s(#{input}).split(LR.to_s(#{pattern}))"
|
||||
},
|
||||
'join' => ->(input, args, _kwargs, compiler) {
|
||||
glue = args.empty? ? "' '" : compile_arg(args[0], compiler)
|
||||
"(#{input}.is_a?(Array) ? #{input}.map { |i| LR.to_s(i) }.join(LR.to_s(#{glue})) : LR.to_s(#{input}))"
|
||||
},
|
||||
'replace' => ->(input, args, _kwargs, compiler) {
|
||||
string = compile_arg(args[0], compiler)
|
||||
replacement = args.length > 1 ? compile_arg(args[1], compiler) : "''"
|
||||
"LR.to_s(#{input}).gsub(LR.to_s(#{string}), LR.to_s(#{replacement}))"
|
||||
},
|
||||
'replace_first' => ->(input, args, _kwargs, compiler) {
|
||||
string = compile_arg(args[0], compiler)
|
||||
replacement = args.length > 1 ? compile_arg(args[1], compiler) : "''"
|
||||
"LR.to_s(#{input}).sub(LR.to_s(#{string}), LR.to_s(#{replacement}))"
|
||||
},
|
||||
'remove' => ->(input, args, _kwargs, compiler) {
|
||||
string = compile_arg(args[0], compiler)
|
||||
"LR.to_s(#{input}).gsub(LR.to_s(#{string}), '')"
|
||||
},
|
||||
'remove_first' => ->(input, args, _kwargs, compiler) {
|
||||
string = compile_arg(args[0], compiler)
|
||||
"LR.to_s(#{input}).sub(LR.to_s(#{string}), '')"
|
||||
},
|
||||
'truncate' => ->(input, args, _kwargs, compiler) {
|
||||
length = args.empty? ? "50" : compile_arg(args[0], compiler)
|
||||
ellipsis = args.length > 1 ? compile_arg(args[1], compiler) : "'...'"
|
||||
"LR.truncate(#{input}, #{length}, #{ellipsis})"
|
||||
},
|
||||
'truncatewords' => ->(input, args, _kwargs, compiler) {
|
||||
words = args.empty? ? "15" : compile_arg(args[0], compiler)
|
||||
ellipsis = args.length > 1 ? compile_arg(args[1], compiler) : "'...'"
|
||||
"LR.truncatewords(#{input}, #{words}, #{ellipsis})"
|
||||
},
|
||||
'slice' => ->(input, args, _kwargs, compiler) {
|
||||
offset = compile_arg(args[0], compiler)
|
||||
length = args.length > 1 ? compile_arg(args[1], compiler) : "1"
|
||||
"LR.slice(#{input}, #{offset}, #{length})"
|
||||
},
|
||||
'map' => ->(input, args, _kwargs, compiler) {
|
||||
property = compile_arg(args[0], compiler)
|
||||
"(#{input}.is_a?(Array) ? #{input}.map { |item| item.respond_to?(:[]) ? item[#{property}] : nil } : [])"
|
||||
},
|
||||
'where' => ->(input, args, _kwargs, compiler) {
|
||||
property = compile_arg(args[0], compiler)
|
||||
if args.length > 1
|
||||
target = compile_arg(args[1], compiler)
|
||||
"(#{input}.is_a?(Array) ? #{input}.select { |item| item.respond_to?(:[]) && item[#{property}] == #{target} } : [])"
|
||||
else
|
||||
"(#{input}.is_a?(Array) ? #{input}.select { |item| item.respond_to?(:[]) && LR.truthy?(item[#{property}]) } : [])"
|
||||
end
|
||||
},
|
||||
'reject' => ->(input, args, _kwargs, compiler) {
|
||||
property = compile_arg(args[0], compiler)
|
||||
if args.length > 1
|
||||
target = compile_arg(args[1], compiler)
|
||||
"(#{input}.is_a?(Array) ? #{input}.reject { |item| item.respond_to?(:[]) && item[#{property}] == #{target} } : [])"
|
||||
else
|
||||
"(#{input}.is_a?(Array) ? #{input}.reject { |item| item.respond_to?(:[]) && LR.truthy?(item[#{property}]) } : [])"
|
||||
end
|
||||
},
|
||||
'concat' => ->(input, args, _kwargs, compiler) {
|
||||
arr = compile_arg(args[0], compiler)
|
||||
"(#{input}.is_a?(Array) ? #{input} + (#{arr}.respond_to?(:to_ary) ? #{arr}.to_ary : []) : [])"
|
||||
},
|
||||
'sort_natural' => ->(input, args, _kwargs, compiler) {
|
||||
if args.empty?
|
||||
"(#{input}.is_a?(Array) ? #{input}.sort_by { |a| a.to_s.downcase } : #{input})"
|
||||
else
|
||||
property = compile_arg(args[0], compiler)
|
||||
"(#{input}.is_a?(Array) ? #{input}.sort_by { |a| a.respond_to?(:[]) ? a[#{property}].to_s.downcase : '' } : #{input})"
|
||||
end
|
||||
},
|
||||
'date' => ->(input, args, _kwargs, compiler) {
|
||||
format = compile_arg(args[0], compiler)
|
||||
"LR.date(#{input}, #{format})"
|
||||
},
|
||||
'escape_once' => ->(input, _args, _kwargs, _compiler) {
|
||||
"LR.escape_once(#{input})"
|
||||
},
|
||||
}.freeze
|
||||
|
||||
# Compile a filter chain
|
||||
# @param input_expr [String] Ruby expression for the input value
|
||||
# @param filters [Array] Array of filter definitions [name, args, kwargs]
|
||||
# @param compiler [RubyCompiler] The main compiler instance
|
||||
# @return [String] Ruby code that applies all filters
|
||||
def self.compile(input_expr, filters, compiler)
|
||||
result = input_expr
|
||||
|
||||
filters.each do |filter_name, filter_args, filter_kwargs|
|
||||
result = compile_filter(result, filter_name, filter_args || [], filter_kwargs, compiler)
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
# Compile a single filter application
|
||||
def self.compile_filter(input, name, args, kwargs, compiler)
|
||||
if SIMPLE_FILTERS.key?(name)
|
||||
SIMPLE_FILTERS[name].call(input, args, kwargs, compiler)
|
||||
elsif PARAMETERIZED_FILTERS.key?(name)
|
||||
PARAMETERIZED_FILTERS[name].call(input, args, kwargs, compiler)
|
||||
else
|
||||
# Fall back to a generic filter call
|
||||
compile_generic_filter(input, name, args, kwargs, compiler)
|
||||
end
|
||||
end
|
||||
|
||||
# Compile a filter that's not built-in
|
||||
# Yields [:filter, name, input, args] to the external handler
|
||||
def self.compile_generic_filter(input, name, args, kwargs, compiler)
|
||||
# Mark that we're using external filters
|
||||
compiler.register_external_filter
|
||||
|
||||
compiled_args = args.map { |arg| compile_arg(arg, compiler) }
|
||||
|
||||
if kwargs && !kwargs.empty?
|
||||
kwargs_hash = kwargs.map { |k, v| "#{k.inspect} => #{compile_arg(v, compiler)}" }.join(", ")
|
||||
compiled_args << "{ #{kwargs_hash} }"
|
||||
end
|
||||
|
||||
args_str = compiled_args.empty? ? "[]" : "[#{compiled_args.join(', ')}]"
|
||||
|
||||
# Yield to the external handler
|
||||
"__external__.call(:filter, #{name.inspect}, #{input}, #{args_str})"
|
||||
end
|
||||
|
||||
# Compile a filter argument
|
||||
def self.compile_arg(arg, compiler)
|
||||
ExpressionCompiler.compile(arg, compiler)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,418 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
# RubyCompiler transforms a parsed Liquid template into pure Ruby code.
|
||||
#
|
||||
# The compiled code is a string that can be eval'd to create a proc/lambda
|
||||
# that takes an assigns hash and returns the rendered output string.
|
||||
#
|
||||
# ## Optimization Opportunities
|
||||
#
|
||||
# The compiled Ruby code has several significant advantages over interpreted Liquid:
|
||||
#
|
||||
# 1. **No Context Object**: Instead of using a Context for variable lookups,
|
||||
# variables are extracted directly from the assigns hash and stored in
|
||||
# local Ruby variables. This eliminates hash lookups on every access.
|
||||
#
|
||||
# 2. **No Filter Invocation Overhead**: Filters are compiled to direct Ruby
|
||||
# method calls rather than going through context.invoke().
|
||||
#
|
||||
# 3. **No Resource Limits Tracking**: The compiled code doesn't track render
|
||||
# scores, write scores, or assign scores, eliminating per-node overhead.
|
||||
#
|
||||
# 4. **No Stack-based Scoping**: Ruby's native block scoping is used instead
|
||||
# of manually managing scope stacks with push/pop operations.
|
||||
#
|
||||
# 5. **Direct String Concatenation**: Output is built with direct << operations
|
||||
# rather than through render_to_output_buffer abstractions.
|
||||
#
|
||||
# 6. **Native Control Flow**: break/continue become Ruby's break/next,
|
||||
# eliminating interrupt objects and checks.
|
||||
#
|
||||
# 7. **No to_liquid Calls**: Values are used directly without conversion.
|
||||
#
|
||||
# 8. **Potential for Constant Folding**: Expressions with only literals
|
||||
# could be evaluated at compile time (future enhancement).
|
||||
#
|
||||
# 9. **Potential for Dead Code Elimination**: Unreachable branches like
|
||||
# `{% if false %}` could be removed (future enhancement).
|
||||
#
|
||||
# 10. **No Profiling Hooks**: No profiler overhead in the generated code.
|
||||
#
|
||||
# 11. **No Exception Rendering**: Errors propagate naturally rather than
|
||||
# being caught and rendered inline.
|
||||
#
|
||||
# ## Usage
|
||||
#
|
||||
# template = Liquid::Template.parse("Hello, {{ name }}!")
|
||||
# ruby_code = template.compile_to_ruby
|
||||
# render_proc = eval(ruby_code)
|
||||
# result = render_proc.call({ "name" => "World" })
|
||||
# # => "Hello, World!"
|
||||
#
|
||||
class RubyCompiler
|
||||
attr_reader :template, :options
|
||||
|
||||
# @param template [Liquid::Template] The parsed template to compile
|
||||
# @param options [Hash] Compilation options
|
||||
# @option options [Boolean] :strict_variables Raise on undefined variables (default: false)
|
||||
# @option options [Boolean] :include_filters Include filter helper methods (default: true)
|
||||
# @option options [Boolean] :debug Emit source comments for debugging (default: false)
|
||||
# @option options [Object] :file_system File system for loading partials (default: template's environment)
|
||||
def initialize(template, options = {})
|
||||
@template = template
|
||||
@options = {
|
||||
strict_variables: false,
|
||||
include_filters: true,
|
||||
debug: false,
|
||||
file_system: nil,
|
||||
}.merge(options)
|
||||
@var_counter = 0
|
||||
@partials = {} # Registered partials: name => method_name
|
||||
@partial_sources = {} # Partial sources: name => source code
|
||||
@partial_counter = 0
|
||||
@external_tags = {} # External tags: var_name => tag object
|
||||
@external_tag_counter = 0
|
||||
@has_external_filters = false # Whether we need the filter helper
|
||||
@loop_context_stack = [] # Stack of loop contexts for break/continue
|
||||
end
|
||||
|
||||
# Push a loop context onto the stack (for nested loops)
|
||||
# @param break_var [String, nil] Variable name for break flag, or nil if no break
|
||||
# @param idx_var [String, nil] Variable name for loop index
|
||||
# @param len_var [String, nil] Variable name for collection length
|
||||
# @param loop_name [String, nil] Name of the loop (for forloop.name)
|
||||
def push_loop_context(break_var: nil, idx_var: nil, len_var: nil, loop_name: nil)
|
||||
@loop_context_stack.push({
|
||||
break_var: break_var,
|
||||
idx_var: idx_var,
|
||||
len_var: len_var,
|
||||
loop_name: loop_name
|
||||
})
|
||||
end
|
||||
|
||||
# Pop the current loop context
|
||||
def pop_loop_context
|
||||
@loop_context_stack.pop
|
||||
end
|
||||
|
||||
# Get the current loop context (for break/continue compilation)
|
||||
# @return [Hash, nil] Current loop context or nil if not in a loop
|
||||
def current_loop_context
|
||||
@loop_context_stack.last
|
||||
end
|
||||
|
||||
# Mark that we have external filters
|
||||
def register_external_filter
|
||||
@has_external_filters = true
|
||||
end
|
||||
|
||||
# Check if external filters are used
|
||||
def has_external_filters?
|
||||
@has_external_filters
|
||||
end
|
||||
|
||||
# Register an external tag that will be called at runtime
|
||||
# @param tag [Liquid::Tag] The tag to register
|
||||
# @return [String] The variable name for this tag
|
||||
def register_external_tag(tag)
|
||||
@external_tag_counter += 1
|
||||
var_name = "__ext_tag_#{@external_tag_counter}__"
|
||||
@external_tags[var_name] = tag
|
||||
var_name
|
||||
end
|
||||
|
||||
# Get all registered external tags
|
||||
# @return [Hash] Map of variable names to tag objects
|
||||
def external_tags
|
||||
@external_tags
|
||||
end
|
||||
|
||||
# Get the file system for loading partials
|
||||
def file_system
|
||||
@options[:file_system] || @template.instance_variable_get(:@environment)&.file_system
|
||||
end
|
||||
|
||||
# Load a partial source from the file system
|
||||
# @param name [String] The partial name
|
||||
# @return [String, nil] The partial source or nil if not found
|
||||
def load_partial(name)
|
||||
return @partial_sources[name] if @partial_sources.key?(name)
|
||||
|
||||
fs = file_system
|
||||
return nil unless fs && fs.respond_to?(:read_template_file)
|
||||
|
||||
begin
|
||||
source = fs.read_template_file(name)
|
||||
@partial_sources[name] = source
|
||||
source
|
||||
rescue Liquid::FileSystemError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Register a partial and return its method name
|
||||
# @param name [String] The partial name
|
||||
# @param source [String] The partial source
|
||||
# @return [String] The method name for this partial
|
||||
def register_partial(name, source)
|
||||
return @partials[name] if @partials.key?(name)
|
||||
|
||||
@partial_counter += 1
|
||||
method_name = "__partial_#{@partial_counter}__"
|
||||
@partials[name] = method_name
|
||||
method_name
|
||||
end
|
||||
|
||||
# Get all registered partials
|
||||
def registered_partials
|
||||
@partials
|
||||
end
|
||||
|
||||
# Check if debug mode is enabled
|
||||
def debug?
|
||||
@options[:debug]
|
||||
end
|
||||
|
||||
# Emit a debug comment with source location info
|
||||
# This creates a lightweight source map that allows tracing errors
|
||||
# back to the original Liquid source
|
||||
def emit_debug_comment(code, node, description = nil)
|
||||
return unless debug?
|
||||
|
||||
line_number = node.respond_to?(:line_number) ? node.line_number : nil
|
||||
raw_markup = extract_raw_markup(node)
|
||||
|
||||
comment_parts = []
|
||||
comment_parts << "LIQUID"
|
||||
comment_parts << "L#{line_number}" if line_number
|
||||
comment_parts << description if description
|
||||
comment_parts << raw_markup.inspect if raw_markup && raw_markup.length < 80
|
||||
|
||||
code.line "# #{comment_parts.join(' | ')}"
|
||||
end
|
||||
|
||||
# Extract the raw markup from a node for debug output
|
||||
def extract_raw_markup(node)
|
||||
case node
|
||||
when Variable
|
||||
"{{ #{node.raw} }}"
|
||||
when Tag
|
||||
if node.respond_to?(:raw)
|
||||
"{% #{node.tag_name} #{node.raw} %}"
|
||||
elsif node.respond_to?(:markup)
|
||||
"{% #{node.tag_name} #{node.markup} %}"
|
||||
else
|
||||
"{% #{node.class.name.split('::').last.downcase} %}"
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Compile the template to a Ruby code string
|
||||
# @return [String] Ruby code that can be eval'd to create a render proc
|
||||
# @return [Hash] If external tags are used, returns { code: String, external_tags: Hash }
|
||||
def compile
|
||||
code = CodeGenerator.new
|
||||
|
||||
# Add debug header if enabled
|
||||
if debug?
|
||||
code.line "# Compiled from Liquid template: #{@template.name || '(unnamed)'}"
|
||||
code.line "# Debug mode enabled - comments contain source locations"
|
||||
code.line "# Format: # LIQUID | L<line> | <description> | <source>"
|
||||
code.blank_line
|
||||
end
|
||||
|
||||
# First pass: compile the document body to discover partials and external tags
|
||||
main_code = CodeGenerator.new
|
||||
compile_node(@template.root, main_code)
|
||||
|
||||
# Lambda signature: (assigns, context, external_handler)
|
||||
# - assigns: Hash of template variables
|
||||
# - context: CompiledContext for Drop support
|
||||
# - external_handler: Proc that handles [:tag, ...] and [:filter, ...] calls
|
||||
code.line "->(assigns, __context__, __external__) do"
|
||||
|
||||
code.indent do
|
||||
# Initialize the output buffer
|
||||
code.line '__output__ = +""'
|
||||
code.blank_line
|
||||
|
||||
# Compile partial methods (before main body so they're available)
|
||||
compile_partials(code)
|
||||
|
||||
# Add the main body code
|
||||
code.raw(main_code.to_s)
|
||||
|
||||
code.blank_line
|
||||
code.line "__output__"
|
||||
end
|
||||
code.line "end"
|
||||
|
||||
code.to_s
|
||||
end
|
||||
|
||||
# Compile all registered partials as inner methods
|
||||
def compile_partials(code)
|
||||
@partials.each do |name, method_name|
|
||||
source = @partial_sources[name]
|
||||
next unless source
|
||||
|
||||
code.line "# Partial: #{name}"
|
||||
code.line "#{method_name} = ->(partial_assigns) do"
|
||||
code.indent do
|
||||
code.line "__partial_output__ = +''"
|
||||
code.line "# Merge partial assigns with parent assigns"
|
||||
code.line "__assigns_backup__ = assigns.dup"
|
||||
code.line "assigns.merge!(partial_assigns)"
|
||||
code.blank_line
|
||||
|
||||
# Parse and compile the partial
|
||||
# Note: We need to handle this carefully to avoid circular references
|
||||
begin
|
||||
compile_partial_source(source, code)
|
||||
rescue => e
|
||||
code.line "# Error compiling partial: #{e.message.inspect}"
|
||||
code.line "__partial_output__ << '[PARTIAL ERROR: ' + #{name.inspect} + ']'"
|
||||
end
|
||||
|
||||
code.blank_line
|
||||
code.line "# Restore assigns"
|
||||
code.line "assigns.replace(__assigns_backup__)"
|
||||
code.line "__partial_output__"
|
||||
end
|
||||
code.line "end"
|
||||
code.blank_line
|
||||
end
|
||||
end
|
||||
|
||||
# Compile a partial source string
|
||||
def compile_partial_source(source, code)
|
||||
# Parse the partial source using the same environment
|
||||
environment = @template.instance_variable_get(:@environment) || Liquid::Environment.default
|
||||
parse_context = Liquid::ParseContext.new(environment: environment)
|
||||
tokenizer = parse_context.new_tokenizer(source)
|
||||
document = Liquid::Document.parse(tokenizer, parse_context)
|
||||
|
||||
# Compile the partial's body, but swap output variable
|
||||
code.line "__saved_output__ = __output__"
|
||||
code.line "__output__ = __partial_output__"
|
||||
|
||||
# Compile the document
|
||||
BlockBodyCompiler.compile(document.body, self, code)
|
||||
|
||||
code.line "__output__ = __saved_output__"
|
||||
end
|
||||
|
||||
# Generate a unique variable name for internal use
|
||||
def generate_var_name(prefix = "v")
|
||||
@var_counter += 1
|
||||
"__#{prefix}#{@var_counter}__"
|
||||
end
|
||||
|
||||
# Compile a single node
|
||||
def compile_node(node, code)
|
||||
case node
|
||||
when Document
|
||||
BlockBodyCompiler.compile(node.body, self, code)
|
||||
when BlockBody
|
||||
BlockBodyCompiler.compile(node, self, code)
|
||||
when String
|
||||
compile_string(node, code)
|
||||
when Variable
|
||||
emit_debug_comment(code, node, "variable")
|
||||
VariableCompiler.compile(node, self, code)
|
||||
when Tag
|
||||
emit_debug_comment(code, node, node.class.name.split('::').last.downcase)
|
||||
compile_tag(node, code)
|
||||
else
|
||||
raise CompileError, "Unknown node type: #{node.class}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compile_string(str, code)
|
||||
return if str.empty?
|
||||
if debug? && str.length < 40
|
||||
code.line "# LIQUID | text | #{str.inspect}"
|
||||
end
|
||||
code.line "__output__ << #{str.inspect}"
|
||||
end
|
||||
|
||||
def compile_tag(tag, code)
|
||||
# First, check if the tag implements to_ruby (custom compilation)
|
||||
if tag.respond_to?(:to_ruby)
|
||||
tag.to_ruby(code, self)
|
||||
# Then check for a built-in compiler class
|
||||
elsif (compiler_class = find_tag_compiler(tag))
|
||||
compiler_class.compile(tag, self, code)
|
||||
else
|
||||
# Unknown tag - yield to caller at runtime
|
||||
compile_external_tag(tag, code)
|
||||
end
|
||||
end
|
||||
|
||||
def compile_external_tag(tag, code)
|
||||
tag_var = register_external_tag(tag)
|
||||
tag_name = tag.class.name.split('::').last
|
||||
if debug?
|
||||
code.line "# External tag: #{tag_name} (yields to caller)"
|
||||
end
|
||||
# Yield [:tag, tag_var, assigns] to the external handler
|
||||
code.line "__output__ << __external__.call(:tag, #{tag_var.inspect}, assigns)"
|
||||
end
|
||||
|
||||
def find_tag_compiler(tag)
|
||||
case tag
|
||||
when Liquid::Unless # Check Unless before If since Unless < If
|
||||
Tags::UnlessCompiler
|
||||
when Liquid::If
|
||||
Tags::IfCompiler
|
||||
when Liquid::Case
|
||||
Tags::CaseCompiler
|
||||
when Liquid::For
|
||||
Tags::ForCompiler
|
||||
when Liquid::Assign
|
||||
Tags::AssignCompiler
|
||||
when Liquid::Capture
|
||||
Tags::CaptureCompiler
|
||||
when Liquid::Cycle
|
||||
Tags::CycleCompiler
|
||||
when Liquid::Increment
|
||||
Tags::IncrementCompiler
|
||||
when Liquid::Decrement
|
||||
Tags::DecrementCompiler
|
||||
when Liquid::Raw
|
||||
Tags::RawCompiler
|
||||
when Liquid::Echo
|
||||
Tags::EchoCompiler
|
||||
when Liquid::Break
|
||||
Tags::BreakCompiler
|
||||
when Liquid::Continue
|
||||
Tags::ContinueCompiler
|
||||
when Liquid::Comment, Liquid::InlineComment, Liquid::Doc
|
||||
Tags::CommentCompiler
|
||||
when Liquid::TableRow
|
||||
Tags::TableRowCompiler
|
||||
when Liquid::Render
|
||||
Tags::RenderCompiler
|
||||
when Liquid::Include
|
||||
Tags::IncludeCompiler
|
||||
when Liquid::Ifchanged
|
||||
Tags::IfchangedCompiler
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# NOTE: compile_helper_methods was removed - helpers are now provided by the
|
||||
# pre-loaded LR module (compile/runtime.rb). Templates call LR.to_s(), LR.lookup(), etc.
|
||||
end
|
||||
|
||||
# Custom error for compilation issues
|
||||
class CompileError < StandardError; end
|
||||
end
|
||||
end
|
||||
@@ -1,252 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Liquid Compiled Template Runtime
|
||||
#
|
||||
# This module provides ALL helpers for compiled Liquid templates.
|
||||
# It is loaded into the sandbox BEFORE lock!, so these methods
|
||||
# are available to all compiled templates.
|
||||
#
|
||||
# Templates reference these as LR.method_name(args)
|
||||
#
|
||||
# == Security Note
|
||||
#
|
||||
# It is SAFE to expose side-effect-free, non-IO methods that don't leak
|
||||
# objects with dangerous methods. For example:
|
||||
#
|
||||
# - CGI.escapeHTML(str) -> returns a String (safe)
|
||||
# - Base64.strict_encode64(str) -> returns a String (safe)
|
||||
# - BigDecimal(str) -> returns a Numeric (safe)
|
||||
#
|
||||
# These are pure functions that take values and return values.
|
||||
# They don't provide any capability to escape the sandbox.
|
||||
#
|
||||
# We capture METHOD REFERENCES (e.g., CGI.method(:escapeHTML)) before the
|
||||
# sandbox locks. This means templates can use the functionality without
|
||||
# having direct access to the CGI, Base64, or BigDecimal constants.
|
||||
#
|
||||
# Dependencies (CGI, Base64, BigDecimal) are loaded by box.rb into the
|
||||
# sandbox BEFORE this file runs. The constants are preserved after lock.
|
||||
|
||||
module LR
|
||||
# === Type Conversion ===
|
||||
|
||||
# Convert to string like Liquid does
|
||||
def self.to_s(obj)
|
||||
case obj
|
||||
when nil then ''
|
||||
when Array then obj.join
|
||||
else obj.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# Convert to number for arithmetic
|
||||
# Use Kernel.BigDecimal to access from root namespace
|
||||
def self.to_number(obj)
|
||||
case obj
|
||||
when Numeric then obj
|
||||
when String
|
||||
if obj.strip =~ /\A-?\d+\.\d+\z/
|
||||
Kernel.BigDecimal(obj)
|
||||
else
|
||||
obj.to_i
|
||||
end
|
||||
else 0
|
||||
end
|
||||
end
|
||||
|
||||
# Convert to integer
|
||||
def self.to_integer(obj)
|
||||
return obj if obj.is_a?(Integer)
|
||||
Integer(obj.to_s) rescue 0
|
||||
end
|
||||
|
||||
# === Truthiness ===
|
||||
|
||||
# Liquid truthiness: only nil and false are falsy
|
||||
def self.truthy?(obj)
|
||||
obj != nil && obj != false
|
||||
end
|
||||
|
||||
# === Output ===
|
||||
|
||||
# Output a value, converting to string safely (handles arrays recursively)
|
||||
def self.output(obj)
|
||||
case obj
|
||||
when nil then ''
|
||||
when Array then obj.map { |o| output(o) }.join
|
||||
when BigDecimal
|
||||
# Format BigDecimal like Liquid does - avoid scientific notation
|
||||
obj.to_s('F')
|
||||
else obj.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# === Lookup ===
|
||||
|
||||
# Variable lookup - handles hash/array access, method calls, to_liquid, and drop context
|
||||
def self.lookup(obj, key, context = nil)
|
||||
return nil if obj.nil?
|
||||
|
||||
# Set context on Drops BEFORE accessing their methods
|
||||
obj = obj.to_liquid if obj.respond_to?(:to_liquid)
|
||||
obj.context = context if context && obj.respond_to?(:context=)
|
||||
|
||||
# Perform the lookup
|
||||
result = if obj.respond_to?(:[]) && (obj.respond_to?(:key?) && obj.key?(key) || obj.respond_to?(:fetch) && key.is_a?(Integer))
|
||||
obj[key]
|
||||
elsif obj.respond_to?(key)
|
||||
obj.public_send(key)
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
# Convert result to liquid and set context for nested Drops
|
||||
result = result.to_liquid if result.respond_to?(:to_liquid)
|
||||
result.context = context if context && result.respond_to?(:context=)
|
||||
result
|
||||
end
|
||||
|
||||
# === HTML/URL Encoding ===
|
||||
# Capture method references before sandbox locks - only the specific methods we need
|
||||
CGI_ESCAPE_HTML = CGI.method(:escapeHTML)
|
||||
CGI_ESCAPE = CGI.method(:escape)
|
||||
CGI_UNESCAPE = CGI.method(:unescape)
|
||||
|
||||
HTML_ESCAPE_MAP = { '&' => '&', '<' => '<', '>' => '>', '"' => '"', "'" => ''' }.freeze
|
||||
|
||||
def self.escape_html(obj)
|
||||
CGI_ESCAPE_HTML.call(to_s(obj))
|
||||
end
|
||||
|
||||
def self.url_encode(obj)
|
||||
CGI_ESCAPE.call(to_s(obj))
|
||||
end
|
||||
|
||||
def self.url_decode(obj)
|
||||
CGI_UNESCAPE.call(to_s(obj))
|
||||
end
|
||||
|
||||
def self.escape_once(obj)
|
||||
# Only escape if not already escaped
|
||||
to_s(obj).gsub(/["><']|&(?!([a-zA-Z]+|(#\d+));)/, HTML_ESCAPE_MAP)
|
||||
end
|
||||
|
||||
# === Base64 ===
|
||||
# Capture method references before sandbox locks
|
||||
BASE64_ENCODE = Base64.method(:strict_encode64)
|
||||
BASE64_DECODE = Base64.method(:strict_decode64)
|
||||
BASE64_URL_ENCODE = Base64.method(:urlsafe_encode64)
|
||||
BASE64_URL_DECODE = Base64.method(:urlsafe_decode64)
|
||||
|
||||
def self.base64_encode(obj)
|
||||
BASE64_ENCODE.call(to_s(obj))
|
||||
end
|
||||
|
||||
def self.base64_decode(obj)
|
||||
BASE64_DECODE.call(to_s(obj))
|
||||
end
|
||||
|
||||
def self.base64_url_safe_encode(obj)
|
||||
BASE64_URL_ENCODE.call(to_s(obj))
|
||||
end
|
||||
|
||||
def self.base64_url_safe_decode(obj)
|
||||
BASE64_URL_DECODE.call(to_s(obj))
|
||||
end
|
||||
|
||||
# === String Manipulation ===
|
||||
|
||||
def self.strip_html(obj)
|
||||
to_s(obj).gsub(%r{<script.*?</script>|<!--.*?-->|<style.*?</style>}m, '').gsub(/<.*?>/m, '')
|
||||
end
|
||||
|
||||
# Truncate string to length with ellipsis
|
||||
def self.truncate(input, length = 50, ellipsis = '...')
|
||||
str = to_s(input)
|
||||
ell_str = to_s(ellipsis)
|
||||
len = length.to_i
|
||||
l = [len - ell_str.length, 0].max
|
||||
str.length > len ? str[0, l] + ell_str : str
|
||||
end
|
||||
|
||||
# Truncate to word count
|
||||
def self.truncatewords(input, num_words = 15, ellipsis = '...')
|
||||
max_words = [num_words.to_i, 1].max
|
||||
words = to_s(input).split(' ', max_words + 1)
|
||||
if words.length > max_words
|
||||
words[0, max_words].join(' ') + to_s(ellipsis)
|
||||
else
|
||||
input.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# === Date Formatting ===
|
||||
|
||||
def self.date(input, format)
|
||||
return input if format.to_s.empty?
|
||||
d = case input
|
||||
when Time, Date then input
|
||||
when 'now', 'today' then Time.now
|
||||
when /\A\d+\z/, Integer then Time.at(input.to_i)
|
||||
when String then (Time.parse(input) rescue input)
|
||||
else input
|
||||
end
|
||||
d.respond_to?(:strftime) ? d.strftime(format.to_s) : input
|
||||
end
|
||||
|
||||
# === Collection Helpers ===
|
||||
|
||||
# Convert to array for iteration - returns Array or empty Array
|
||||
# This guarantees the result supports [], .length, .empty? without respond_to? checks
|
||||
def self.to_array(collection)
|
||||
case collection
|
||||
when Array then collection
|
||||
when Range then collection.to_a
|
||||
when nil then EMPTY_ARRAY
|
||||
else
|
||||
collection.respond_to?(:to_a) ? collection.to_a : EMPTY_ARRAY
|
||||
end
|
||||
end
|
||||
|
||||
# Frozen empty array to avoid allocations
|
||||
EMPTY_ARRAY = [].freeze
|
||||
|
||||
# Iterate safely, handling ranges and non-iterables
|
||||
def self.iterate(collection)
|
||||
case collection
|
||||
when Range then collection.to_a
|
||||
when nil then EMPTY_ARRAY
|
||||
else collection.respond_to?(:each) ? collection : EMPTY_ARRAY
|
||||
end
|
||||
end
|
||||
|
||||
# Get collection length safely
|
||||
def self.size(collection)
|
||||
collection.respond_to?(:size) ? collection.size : 0
|
||||
end
|
||||
|
||||
# === Default Filter ===
|
||||
|
||||
def self.default(input, default_value, allow_false: false)
|
||||
if allow_false
|
||||
(input.nil? || (input.respond_to?(:empty?) && input.empty?)) ? default_value : input
|
||||
else
|
||||
(!truthy?(input) || (input.respond_to?(:empty?) && input.empty?)) ? default_value : input
|
||||
end
|
||||
end
|
||||
|
||||
# === Array/String Slice ===
|
||||
|
||||
def self.slice(input, offset, length = 1)
|
||||
off = to_integer(offset)
|
||||
len = to_integer(length)
|
||||
if input.is_a?(Array)
|
||||
input.slice(off, len) || []
|
||||
else
|
||||
to_s(input).slice(off, len) || ''
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Alias for backwards compatibility
|
||||
LiquidRuntime = LR
|
||||
@@ -1,130 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
# SourceMapper provides utilities for mapping errors in compiled Ruby code
|
||||
# back to the original Liquid source.
|
||||
#
|
||||
# When code is compiled with debug: true, comments are embedded that contain
|
||||
# source location information. This class can parse those comments and
|
||||
# help trace errors back to the original Liquid template.
|
||||
#
|
||||
# ## Usage
|
||||
#
|
||||
# template = Liquid::Template.parse(source, line_numbers: true)
|
||||
# ruby_code = template.compile_to_ruby(debug: true)
|
||||
# render_proc = eval(ruby_code)
|
||||
#
|
||||
# begin
|
||||
# result = render_proc.call(assigns)
|
||||
# rescue => e
|
||||
# location = SourceMapper.find_source_location(ruby_code, e)
|
||||
# puts "Error at Liquid line #{location[:liquid_line]}: #{location[:source]}"
|
||||
# end
|
||||
#
|
||||
class SourceMapper
|
||||
# Pattern to match LIQUID debug comments
|
||||
LIQUID_COMMENT_PATTERN = /^(\s*)# LIQUID(?: \| L(\d+))?(?: \| ([^|]+))?(?: \| (.+))?$/
|
||||
|
||||
# Parse compiled Ruby code and extract source mapping entries
|
||||
# @param ruby_code [String] The compiled Ruby code with debug comments
|
||||
# @return [Array<Hash>] Array of source mapping entries
|
||||
def self.parse(ruby_code)
|
||||
entries = []
|
||||
ruby_line = 0
|
||||
|
||||
ruby_code.each_line do |line|
|
||||
ruby_line += 1
|
||||
|
||||
if line =~ LIQUID_COMMENT_PATTERN
|
||||
entries << {
|
||||
ruby_line: ruby_line,
|
||||
liquid_line: $2&.to_i,
|
||||
type: $3&.strip,
|
||||
source: parse_source_string($4),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
entries
|
||||
end
|
||||
|
||||
# Find the source location for an error based on Ruby line number
|
||||
# @param ruby_code [String] The compiled Ruby code
|
||||
# @param error [Exception] The exception that was raised
|
||||
# @return [Hash, nil] Source location info or nil if not found
|
||||
def self.find_source_location(ruby_code, error)
|
||||
# Extract the line number from the error
|
||||
ruby_line = extract_error_line(error)
|
||||
return nil unless ruby_line
|
||||
|
||||
find_source_for_ruby_line(ruby_code, ruby_line)
|
||||
end
|
||||
|
||||
# Find the source location for a specific Ruby line number
|
||||
# @param ruby_code [String] The compiled Ruby code
|
||||
# @param target_line [Integer] The Ruby line number
|
||||
# @return [Hash, nil] Source location info or nil if not found
|
||||
def self.find_source_for_ruby_line(ruby_code, target_line)
|
||||
entries = parse(ruby_code)
|
||||
|
||||
# Find the closest LIQUID comment at or before the target line
|
||||
closest = nil
|
||||
entries.each do |entry|
|
||||
break if entry[:ruby_line] > target_line
|
||||
closest = entry
|
||||
end
|
||||
|
||||
closest
|
||||
end
|
||||
|
||||
# Format an error message with source location info
|
||||
# @param ruby_code [String] The compiled Ruby code
|
||||
# @param error [Exception] The exception
|
||||
# @return [String] Formatted error message
|
||||
def self.format_error(ruby_code, error)
|
||||
location = find_source_location(ruby_code, error)
|
||||
|
||||
message = "#{error.class}: #{error.message}"
|
||||
|
||||
if location
|
||||
liquid_line = location[:liquid_line] ? "line #{location[:liquid_line]}" : "unknown line"
|
||||
source = location[:source] || location[:type] || "unknown"
|
||||
message += "\n in Liquid template at #{liquid_line}"
|
||||
message += "\n source: #{source}" if location[:source]
|
||||
end
|
||||
|
||||
message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.extract_error_line(error)
|
||||
# Look for (eval):N in backtrace
|
||||
error.backtrace&.each do |frame|
|
||||
if frame =~ /\(eval.*?\):(\d+)/
|
||||
return $1.to_i
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def self.parse_source_string(str)
|
||||
return nil unless str
|
||||
|
||||
# Remove surrounding quotes if present
|
||||
str = str.strip
|
||||
if str.start_with?('"') && str.end_with?('"')
|
||||
# Unescape the string
|
||||
begin
|
||||
eval(str)
|
||||
rescue
|
||||
str[1..-2]
|
||||
end
|
||||
else
|
||||
str
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% assign var = expression %} tags
|
||||
class AssignCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
var_name = tag.to
|
||||
# Use VariableCompiler to get the expression with filters applied
|
||||
value_expr = VariableCompiler.compile_to_expression(tag.from, compiler)
|
||||
|
||||
# Store in the assigns hash
|
||||
code.line "assigns[#{var_name.inspect}] = #{value_expr}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,28 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% break %} tags
|
||||
#
|
||||
# Break is implemented with a flag variable that's checked in the while condition.
|
||||
# This avoids catch/throw overhead entirely.
|
||||
#
|
||||
# Generated code sets the break flag and uses `next` to exit the current iteration.
|
||||
# The while loop condition checks the flag and exits if set.
|
||||
class BreakCompiler
|
||||
def self.compile(_tag, compiler, code)
|
||||
loop_ctx = compiler.current_loop_context
|
||||
if loop_ctx && loop_ctx[:break_var]
|
||||
# Set the break flag and exit this iteration
|
||||
code.line "#{loop_ctx[:break_var]} = true"
|
||||
code.line "next"
|
||||
else
|
||||
# Fallback: shouldn't happen if contains_tag? works correctly
|
||||
code.line "break"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,31 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% capture var %}...{% endcapture %} tags
|
||||
#
|
||||
# Captures the output of the block into a variable
|
||||
class CaptureCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
var_name = tag.instance_variable_get(:@to)
|
||||
capture_var = compiler.generate_var_name("capture")
|
||||
|
||||
# Save current output, create new buffer for capture
|
||||
code.line "#{capture_var} = __output__"
|
||||
code.line "__output__ = +''"
|
||||
|
||||
# Compile the body - access the @body BlockBody directly
|
||||
body = tag.instance_variable_get(:@body)
|
||||
if body
|
||||
BlockBodyCompiler.compile(body, compiler, code)
|
||||
end
|
||||
|
||||
# Save captured content and restore output buffer
|
||||
code.line "assigns[#{var_name.inspect}] = __output__"
|
||||
code.line "__output__ = #{capture_var}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,47 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% case %} / {% when %} / {% else %} / {% endcase %} tags
|
||||
class CaseCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
# Compile the case expression and store it in a variable
|
||||
# to avoid evaluating it multiple times
|
||||
case_var = compiler.generate_var_name("case")
|
||||
case_expr = ExpressionCompiler.compile(tag.left, compiler)
|
||||
code.line "#{case_var} = #{case_expr}"
|
||||
|
||||
blocks = tag.blocks
|
||||
is_first = true
|
||||
has_else = false
|
||||
|
||||
blocks.each do |block|
|
||||
if block.else?
|
||||
has_else = true
|
||||
code.line "else"
|
||||
else
|
||||
# 'when' condition - compare case expression with the when value
|
||||
when_expr = ExpressionCompiler.compile(block.right, compiler)
|
||||
|
||||
if is_first
|
||||
code.line "if #{case_var} == #{when_expr}"
|
||||
is_first = false
|
||||
else
|
||||
code.line "elsif #{case_var} == #{when_expr}"
|
||||
end
|
||||
end
|
||||
|
||||
code.indent do
|
||||
if block.attachment
|
||||
BlockBodyCompiler.compile(block.attachment, compiler, code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
code.line "end" unless blocks.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,18 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% comment %}...{% endcomment %} tags
|
||||
# Also handles inline_comment (#) and doc tags
|
||||
#
|
||||
# Comments produce no output
|
||||
class CommentCompiler
|
||||
def self.compile(_tag, _compiler, code)
|
||||
# Comments produce no output
|
||||
code.line "# (liquid comment)"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% continue %} tags
|
||||
#
|
||||
# Continue is implemented with Ruby's native `next` statement.
|
||||
# Since we use a while loop (not each), `next` correctly skips
|
||||
# to the next iteration, but we must increment the index first.
|
||||
class ContinueCompiler
|
||||
def self.compile(_tag, compiler, code)
|
||||
# Get the index variable from the loop context
|
||||
loop_ctx = compiler.current_loop_context
|
||||
if loop_ctx && loop_ctx[:idx_var]
|
||||
# Increment index before next, otherwise we'd infinite loop
|
||||
code.line "#{loop_ctx[:idx_var]} += 1"
|
||||
end
|
||||
code.line "next"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,45 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% cycle 'a', 'b', 'c' %} tags
|
||||
#
|
||||
# Cycles through a list of values, outputting the next one each iteration
|
||||
class CycleCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
variables = tag.variables
|
||||
name_expr = ExpressionCompiler.compile(tag.instance_variable_get(:@name), compiler)
|
||||
is_named = tag.named?
|
||||
|
||||
cycle_var = compiler.generate_var_name("cycle")
|
||||
key_var = compiler.generate_var_name("cycle_key")
|
||||
|
||||
# Initialize cycle storage if needed
|
||||
code.line "assigns[:__cycle__] ||= {}"
|
||||
|
||||
# Get the cycle key
|
||||
if is_named
|
||||
code.line "#{key_var} = #{name_expr}"
|
||||
else
|
||||
code.line "#{key_var} = #{variables.object_id}"
|
||||
end
|
||||
|
||||
# Get current index
|
||||
code.line "#{cycle_var} = assigns[:__cycle__][#{key_var}].to_i"
|
||||
|
||||
# Get the value at current index
|
||||
code.line "case #{cycle_var} % #{variables.size}"
|
||||
variables.each_with_index do |var, idx|
|
||||
var_expr = ExpressionCompiler.compile(var, compiler)
|
||||
code.line "when #{idx} then __output__ << LR.to_s(#{var_expr})"
|
||||
end
|
||||
code.line "end"
|
||||
|
||||
# Increment counter
|
||||
code.line "assigns[:__cycle__][#{key_var}] = #{cycle_var} + 1"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% decrement var %} tags
|
||||
#
|
||||
# Decrements a counter and outputs its value.
|
||||
# Uses a separate namespace from regular assigns (shares with increment).
|
||||
class DecrementCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
var_name = tag.variable_name
|
||||
|
||||
# Initialize counter storage if needed
|
||||
code.line "assigns[:__counters__] ||= {}"
|
||||
|
||||
# Get current value (default 0), decrement it, output, then store
|
||||
dec_var = compiler.generate_var_name("dec")
|
||||
code.line "#{dec_var} = (assigns[:__counters__][#{var_name.inspect}] || 0) - 1"
|
||||
code.line "assigns[:__counters__][#{var_name.inspect}] = #{dec_var}"
|
||||
code.line "__output__ << #{dec_var}.to_s"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% echo expression %} tags
|
||||
#
|
||||
# Same as {{ expression }} but usable in {% liquid %} blocks
|
||||
class EchoCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
variable = tag.variable
|
||||
VariableCompiler.compile(variable, compiler, code)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,339 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% for %} / {% else %} / {% endfor %} tags
|
||||
#
|
||||
# Supports:
|
||||
# - Iteration: {% for item in collection %}
|
||||
# - Limit/Offset: {% for item in collection limit:3 offset:2 %}
|
||||
# - Reversed: {% for item in collection reversed %}
|
||||
# - Forloop object: forloop.index, forloop.first, forloop.last, etc.
|
||||
# - Else block: {% for item in collection %}...{% else %}empty{% endfor %}
|
||||
#
|
||||
# Optimizations:
|
||||
# - Detects break/continue usage at compile time
|
||||
# - Uses while loop with index for minimal overhead
|
||||
# - Break implemented with flag variable (no catch/throw)
|
||||
# - Continue implemented with next (native Ruby)
|
||||
# - Avoids Hash allocation for forloop when not used
|
||||
class ForCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
var_name = tag.variable_name
|
||||
collection_expr = ExpressionCompiler.compile(tag.collection_name, compiler)
|
||||
|
||||
# Generate unique variable names for this loop
|
||||
coll_var = compiler.generate_var_name("coll")
|
||||
idx_var = compiler.generate_var_name("idx")
|
||||
len_var = compiler.generate_var_name("len")
|
||||
|
||||
# Evaluate the collection and convert to array for indexed access
|
||||
# After this, coll_var is guaranteed to be an Array (or nil)
|
||||
code.line "#{coll_var} = #{collection_expr}"
|
||||
code.line "#{coll_var} = LR.to_array(#{coll_var})"
|
||||
|
||||
# Handle limit and offset
|
||||
if tag.from || tag.limit
|
||||
compile_slice(tag, coll_var, compiler, code)
|
||||
end
|
||||
|
||||
# Handle reversed
|
||||
if tag.instance_variable_get(:@reversed)
|
||||
code.line "#{coll_var} = #{coll_var}.reverse"
|
||||
end
|
||||
|
||||
# Check if collection is empty for else block
|
||||
for_block = tag.instance_variable_get(:@for_block)
|
||||
else_block = tag.instance_variable_get(:@else_block)
|
||||
|
||||
if else_block
|
||||
code.line "if #{coll_var}.nil? || #{coll_var}.empty?"
|
||||
code.indent do
|
||||
BlockBodyCompiler.compile(else_block, compiler, code)
|
||||
end
|
||||
code.line "else"
|
||||
code.indent do
|
||||
compile_loop(tag, var_name, coll_var, idx_var, len_var, for_block, compiler, code)
|
||||
end
|
||||
code.line "end"
|
||||
else
|
||||
code.line "if #{coll_var} && !#{coll_var}.empty?"
|
||||
code.indent do
|
||||
compile_loop(tag, var_name, coll_var, idx_var, len_var, for_block, compiler, code)
|
||||
end
|
||||
code.line "end"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.compile_slice(tag, coll_var, compiler, code)
|
||||
from_expr = if tag.from == :continue
|
||||
# Continue from previous offset - we'd need register tracking for this
|
||||
# For now, default to 0
|
||||
"0"
|
||||
elsif tag.from
|
||||
ExpressionCompiler.compile(tag.from, compiler)
|
||||
else
|
||||
"0"
|
||||
end
|
||||
|
||||
if tag.limit
|
||||
limit_expr = ExpressionCompiler.compile(tag.limit, compiler)
|
||||
code.line "#{coll_var} = #{coll_var}[LR.to_integer(#{from_expr}), LR.to_integer(#{limit_expr})] || []"
|
||||
else
|
||||
code.line "#{coll_var} = #{coll_var}.drop(LR.to_integer(#{from_expr}))"
|
||||
end
|
||||
end
|
||||
|
||||
def self.compile_loop(tag, var_name, coll_var, idx_var, len_var, for_block, compiler, code)
|
||||
# Analyze loop body for break/continue usage and forloop access
|
||||
has_break = contains_tag?(for_block, Break)
|
||||
forloop_props = detect_forloop_properties(for_block)
|
||||
uses_forloop = !forloop_props.empty?
|
||||
|
||||
# Calculate length (needed for forloop or bounds checking)
|
||||
code.line "#{len_var} = #{coll_var}.length"
|
||||
code.line "#{idx_var} = 0"
|
||||
|
||||
# Break uses a flag variable - no catch/throw overhead
|
||||
if has_break
|
||||
break_var = compiler.generate_var_name("brk")
|
||||
code.line "#{break_var} = false"
|
||||
code.line "while #{idx_var} < #{len_var} && !#{break_var}"
|
||||
else
|
||||
code.line "while #{idx_var} < #{len_var}"
|
||||
end
|
||||
|
||||
# Check if all forloop properties can be inlined (no hash needed)
|
||||
inlinable_props = %w[index index0 rindex rindex0 first last length name]
|
||||
needs_forloop_hash = uses_forloop && !forloop_props.all? { |p| inlinable_props.include?(p) }
|
||||
|
||||
code.indent do
|
||||
# Set the loop variable directly from array index
|
||||
code.line "assigns[#{var_name.inspect}] = #{coll_var}[#{idx_var}]"
|
||||
|
||||
# Only create forloop hash if we have properties that can't be inlined
|
||||
if needs_forloop_hash
|
||||
compile_forloop_hash(tag, idx_var, len_var, code)
|
||||
end
|
||||
|
||||
# Compile the loop body
|
||||
# The BreakCompiler/ContinueCompiler will emit the right code
|
||||
# based on the context we pass through the compiler
|
||||
compiler.push_loop_context(
|
||||
break_var: has_break ? break_var : nil,
|
||||
idx_var: idx_var,
|
||||
len_var: len_var,
|
||||
loop_name: tag.instance_variable_get(:@name)
|
||||
)
|
||||
BlockBodyCompiler.compile(for_block, compiler, code)
|
||||
compiler.pop_loop_context
|
||||
|
||||
# Increment index
|
||||
code.line "#{idx_var} += 1"
|
||||
end
|
||||
code.line "end"
|
||||
|
||||
# Clean up
|
||||
code.line "assigns.delete(#{var_name.inspect})"
|
||||
code.line "assigns.delete('forloop')" if needs_forloop_hash
|
||||
end
|
||||
|
||||
def self.compile_forloop_hash(tag, idx_var, len_var, code)
|
||||
loop_name = tag.instance_variable_get(:@name)
|
||||
code.line "assigns['forloop'] = {"
|
||||
code.indent do
|
||||
code.line "'name' => #{loop_name.inspect},"
|
||||
code.line "'length' => #{len_var},"
|
||||
code.line "'index' => #{idx_var} + 1,"
|
||||
code.line "'index0' => #{idx_var},"
|
||||
code.line "'rindex' => #{len_var} - #{idx_var},"
|
||||
code.line "'rindex0' => #{len_var} - #{idx_var} - 1,"
|
||||
code.line "'first' => #{idx_var} == 0,"
|
||||
code.line "'last' => #{idx_var} == #{len_var} - 1,"
|
||||
end
|
||||
code.line "}"
|
||||
end
|
||||
|
||||
# Check if a block body contains a specific tag type (recursively)
|
||||
def self.contains_tag?(body, tag_class)
|
||||
return false if body.nil?
|
||||
nodelist = body.nodelist
|
||||
return false if nodelist.nil?
|
||||
|
||||
nodelist.any? do |node|
|
||||
case node
|
||||
when tag_class
|
||||
true
|
||||
when Block
|
||||
# Check nested blocks (if, for, case, etc.)
|
||||
contains_tag?(node.instance_variable_get(:@body), tag_class) ||
|
||||
(node.respond_to?(:nodelist) && contains_tag_in_nodelist?(node.nodelist, tag_class))
|
||||
when Tag
|
||||
# Tags with blocks
|
||||
check_tag_for_nested(node, tag_class)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.check_tag_for_nested(tag, tag_class)
|
||||
# Check various block-holding tags
|
||||
[:@for_block, :@else_block, :@body, :@consequent, :@alternative].each do |ivar|
|
||||
if tag.instance_variable_defined?(ivar)
|
||||
block = tag.instance_variable_get(ivar)
|
||||
return true if contains_tag?(block, tag_class)
|
||||
end
|
||||
end
|
||||
|
||||
# Check If tag's blocks array
|
||||
if tag.respond_to?(:blocks)
|
||||
tag.blocks.each do |block|
|
||||
if block.respond_to?(:attachment)
|
||||
return true if contains_tag?(block.attachment, tag_class)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def self.contains_tag_in_nodelist?(nodelist, tag_class)
|
||||
return false if nodelist.nil?
|
||||
nodelist.any? { |n| n.is_a?(tag_class) || (n.is_a?(Tag) && check_tag_for_nested(n, tag_class)) }
|
||||
end
|
||||
|
||||
# Check if the loop body accesses forloop variable
|
||||
def self.uses_forloop_var?(body)
|
||||
return false if body.nil?
|
||||
nodelist = body.nodelist
|
||||
return false if nodelist.nil?
|
||||
|
||||
nodelist.any? do |node|
|
||||
case node
|
||||
when Variable
|
||||
# Check if variable references forloop
|
||||
lookup = node.name
|
||||
if lookup.is_a?(VariableLookup)
|
||||
return true if lookup.name == 'forloop'
|
||||
end
|
||||
false
|
||||
when Tag
|
||||
# Recursively check tag bodies and conditions
|
||||
check_tag_for_forloop(node)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.check_tag_for_forloop(tag)
|
||||
# Check block bodies
|
||||
[:@for_block, :@else_block, :@body, :@consequent, :@alternative].each do |ivar|
|
||||
if tag.instance_variable_defined?(ivar)
|
||||
block = tag.instance_variable_get(ivar)
|
||||
return true if uses_forloop_var?(block)
|
||||
end
|
||||
end
|
||||
|
||||
# Check If/Unless/Case conditions
|
||||
if tag.respond_to?(:blocks)
|
||||
tag.blocks.each do |block|
|
||||
# Check condition expressions
|
||||
if block.respond_to?(:left) && variable_references_forloop?(block.left)
|
||||
return true
|
||||
end
|
||||
if block.respond_to?(:right) && variable_references_forloop?(block.right)
|
||||
return true
|
||||
end
|
||||
# Check block attachment (body)
|
||||
if block.respond_to?(:attachment)
|
||||
return true if uses_forloop_var?(block.attachment)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
# Check if an expression references forloop variable
|
||||
def self.variable_references_forloop?(expr)
|
||||
case expr
|
||||
when VariableLookup
|
||||
expr.name == 'forloop'
|
||||
when Variable
|
||||
expr.name.is_a?(VariableLookup) && expr.name.name == 'forloop'
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Detect which forloop properties are used (for potential future optimization)
|
||||
# Returns Set of property names like 'index', 'first', 'last', etc.
|
||||
def self.detect_forloop_properties(body)
|
||||
props = Set.new
|
||||
collect_forloop_properties(body, props)
|
||||
props
|
||||
end
|
||||
|
||||
def self.collect_forloop_properties(body, props)
|
||||
return if body.nil?
|
||||
nodelist = body.nodelist
|
||||
return if nodelist.nil?
|
||||
|
||||
nodelist.each do |node|
|
||||
case node
|
||||
when Variable
|
||||
collect_forloop_from_variable(node, props)
|
||||
when Tag
|
||||
collect_forloop_from_tag(node, props)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.collect_forloop_from_variable(var, props)
|
||||
lookup = var.name
|
||||
if lookup.is_a?(VariableLookup) && lookup.name == 'forloop'
|
||||
lookup.lookups.each do |prop|
|
||||
props << prop if prop.is_a?(String)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.collect_forloop_from_tag(tag, props)
|
||||
# Check block bodies
|
||||
[:@for_block, :@else_block, :@body, :@consequent, :@alternative].each do |ivar|
|
||||
if tag.instance_variable_defined?(ivar)
|
||||
collect_forloop_properties(tag.instance_variable_get(ivar), props)
|
||||
end
|
||||
end
|
||||
|
||||
# Check conditions
|
||||
if tag.respond_to?(:blocks)
|
||||
tag.blocks.each do |block|
|
||||
collect_forloop_from_condition(block, props) if block.respond_to?(:left)
|
||||
collect_forloop_properties(block.attachment, props) if block.respond_to?(:attachment)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.collect_forloop_from_condition(condition, props)
|
||||
[condition.left, condition.right].compact.each do |expr|
|
||||
if expr.is_a?(VariableLookup) && expr.name == 'forloop'
|
||||
expr.lookups.each do |prop|
|
||||
props << prop if prop.is_a?(String)
|
||||
end
|
||||
end
|
||||
end
|
||||
# Check child conditions
|
||||
collect_forloop_from_condition(condition.child_condition, props) if condition.respond_to?(:child_condition) && condition.child_condition
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% if %} / {% elsif %} / {% else %} / {% endif %} tags
|
||||
class IfCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
blocks = tag.blocks
|
||||
|
||||
blocks.each_with_index do |block, index|
|
||||
condition_expr = ConditionCompiler.compile(block, compiler)
|
||||
|
||||
if index == 0
|
||||
code.line "if #{condition_expr}"
|
||||
elsif block.else?
|
||||
code.line "else"
|
||||
else
|
||||
code.line "elsif #{condition_expr}"
|
||||
end
|
||||
|
||||
code.indent do
|
||||
if block.attachment
|
||||
BlockBodyCompiler.compile(block.attachment, compiler, code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
code.line "end"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,40 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% ifchanged %}...{% endifchanged %} tags
|
||||
#
|
||||
# Only outputs if the content has changed since last render
|
||||
class IfchangedCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
capture_var = compiler.generate_var_name("ifchanged")
|
||||
|
||||
# Capture the block output
|
||||
code.line "#{capture_var} = +''"
|
||||
code.line "begin"
|
||||
code.indent do
|
||||
code.line "__saved_output__ = __output__"
|
||||
code.line "__output__ = #{capture_var}"
|
||||
|
||||
# Compile the body
|
||||
tag.nodelist.each do |body|
|
||||
BlockBodyCompiler.compile(body, compiler, code)
|
||||
end
|
||||
|
||||
code.line "__output__ = __saved_output__"
|
||||
end
|
||||
code.line "end"
|
||||
|
||||
# Only output if changed
|
||||
code.line "if #{capture_var} != assigns[:__ifchanged__]"
|
||||
code.indent do
|
||||
code.line "assigns[:__ifchanged__] = #{capture_var}"
|
||||
code.line "__output__ << #{capture_var}"
|
||||
end
|
||||
code.line "end"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,119 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% include 'partial' %} tags
|
||||
#
|
||||
# Include is deprecated in favor of render, but we support it for compatibility.
|
||||
# Unlike render, include shares the outer scope with the partial.
|
||||
#
|
||||
# For static template names, the partial is loaded and inlined at compile time.
|
||||
# For dynamic template names, a runtime fallback is generated.
|
||||
class IncludeCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
template_name_expr = tag.template_name_expr
|
||||
variable_name_expr = tag.variable_name_expr
|
||||
attributes = tag.attributes
|
||||
alias_name = tag.instance_variable_get(:@alias_name)
|
||||
|
||||
# Check if the template name is a static string
|
||||
if template_name_expr.is_a?(String)
|
||||
compile_static_include(tag, template_name_expr, compiler, code)
|
||||
else
|
||||
compile_dynamic_include(tag, compiler, code)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.compile_static_include(tag, template_name, compiler, code)
|
||||
variable_name_expr = tag.variable_name_expr
|
||||
attributes = tag.attributes
|
||||
alias_name = tag.instance_variable_get(:@alias_name)
|
||||
|
||||
# Try to load the partial at compile time
|
||||
partial_source = compiler.load_partial(template_name)
|
||||
|
||||
if partial_source
|
||||
if compiler.debug?
|
||||
code.line "# Inlined partial #{template_name.inspect} at compile time"
|
||||
code.line "$stderr.puts '* WARN: Liquid file system access - inlined partial ' + #{template_name.inspect} + ' at compile time' if $VERBOSE"
|
||||
end
|
||||
# Generate a unique method name for this partial
|
||||
method_name = compiler.register_partial(template_name, partial_source)
|
||||
context_var_name = alias_name || template_name.split('/').last
|
||||
|
||||
# Include shares scope, so we just set variables directly
|
||||
code.line "# Include: #{template_name}"
|
||||
|
||||
# Set attributes
|
||||
attributes.each do |key, value|
|
||||
value_expr = ExpressionCompiler.compile(value, compiler)
|
||||
code.line "assigns[#{key.inspect}] = #{value_expr}"
|
||||
end
|
||||
|
||||
# Set the context variable
|
||||
if variable_name_expr
|
||||
var_expr = ExpressionCompiler.compile(variable_name_expr, compiler)
|
||||
var_var = compiler.generate_var_name("incvar")
|
||||
code.line "#{var_var} = #{var_expr}"
|
||||
|
||||
# If it's an array, loop through it
|
||||
code.line "if #{var_var}.is_a?(Array)"
|
||||
code.indent do
|
||||
code.line "#{var_var}.each do |__include_item__|"
|
||||
code.indent do
|
||||
code.line "assigns[#{context_var_name.inspect}] = __include_item__"
|
||||
code.line "__output__ << #{method_name}({})"
|
||||
end
|
||||
code.line "end"
|
||||
end
|
||||
code.line "else"
|
||||
code.indent do
|
||||
code.line "assigns[#{context_var_name.inspect}] = #{var_var}"
|
||||
code.line "__output__ << #{method_name}({})"
|
||||
end
|
||||
code.line "end"
|
||||
else
|
||||
# Try to find a variable with the same name as the template
|
||||
code.line "assigns[#{context_var_name.inspect}] = assigns[#{template_name.inspect}] if assigns.key?(#{template_name.inspect})"
|
||||
code.line "__output__ << #{method_name}({})"
|
||||
end
|
||||
else
|
||||
# Partial not found at compile time
|
||||
code.line "# Partial '#{template_name}' not found at compile time"
|
||||
compile_dynamic_include(tag, compiler, code)
|
||||
end
|
||||
end
|
||||
|
||||
def self.compile_dynamic_include(tag, compiler, code)
|
||||
template_name_expr = tag.template_name_expr
|
||||
variable_name_expr = tag.variable_name_expr
|
||||
attributes = tag.attributes
|
||||
alias_name = tag.instance_variable_get(:@alias_name)
|
||||
|
||||
if compiler.debug?
|
||||
code.line "# Dynamic include (template name from variable)"
|
||||
end
|
||||
|
||||
name_expr = ExpressionCompiler.compile(template_name_expr, compiler)
|
||||
|
||||
# Build attributes hash
|
||||
attrs_var = compiler.generate_var_name("attrs")
|
||||
code.line "#{attrs_var} = {}"
|
||||
attributes.each do |key, value|
|
||||
value_expr = ExpressionCompiler.compile(value, compiler)
|
||||
code.line "#{attrs_var}[#{key.inspect}] = #{value_expr}"
|
||||
end
|
||||
|
||||
var_expr = variable_name_expr ? ExpressionCompiler.compile(variable_name_expr, compiler) : "nil"
|
||||
alias_expr = alias_name ? alias_name.inspect : "nil"
|
||||
|
||||
# Call the external handler for dynamic includes
|
||||
code.line "__output__ << __external__.call(:include, #{name_expr}, #{var_expr}, #{attrs_var}, #{alias_expr}, assigns, __context__)"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% increment var %} tags
|
||||
#
|
||||
# Outputs the current counter value, then increments it.
|
||||
# Uses a separate namespace from regular assigns (shares with decrement).
|
||||
class IncrementCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
var_name = tag.variable_name
|
||||
|
||||
# Initialize counter storage if needed
|
||||
code.line "assigns[:__counters__] ||= {}"
|
||||
|
||||
# Get current value (default 0), output it, then increment
|
||||
inc_var = compiler.generate_var_name("inc")
|
||||
code.line "#{inc_var} = assigns[:__counters__][#{var_name.inspect}] || 0"
|
||||
code.line "__output__ << #{inc_var}.to_s"
|
||||
code.line "assigns[:__counters__][#{var_name.inspect}] = #{inc_var} + 1"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% raw %}...{% endraw %} tags
|
||||
#
|
||||
# Outputs the content as-is without parsing Liquid syntax
|
||||
class RawCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
body = tag.instance_variable_get(:@body)
|
||||
return if body.nil? || body.empty?
|
||||
|
||||
code.line "__output__ << #{body.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,176 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% render 'partial' %} tags
|
||||
#
|
||||
# For static template names (string literals), the partial is loaded at
|
||||
# compile time and inlined as a method.
|
||||
#
|
||||
# For dynamic template names (variables), a runtime fallback is generated
|
||||
# that calls __render_dynamic__ which must be provided by the runtime.
|
||||
class RenderCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
template_name_expr = tag.template_name_expr
|
||||
variable_name_expr = tag.variable_name_expr
|
||||
attributes = tag.attributes
|
||||
alias_name = tag.alias_name
|
||||
is_for_loop = tag.for_loop?
|
||||
|
||||
# Check if the template name is a static string
|
||||
if template_name_expr.is_a?(String)
|
||||
compile_static_render(tag, template_name_expr, compiler, code)
|
||||
else
|
||||
compile_dynamic_render(tag, compiler, code)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.compile_static_render(tag, template_name, compiler, code)
|
||||
variable_name_expr = tag.variable_name_expr
|
||||
attributes = tag.attributes
|
||||
alias_name = tag.alias_name
|
||||
is_for_loop = tag.for_loop?
|
||||
|
||||
# Try to load the partial at compile time
|
||||
partial_source = compiler.load_partial(template_name)
|
||||
|
||||
if partial_source
|
||||
if compiler.debug?
|
||||
code.line "# Inlined partial #{template_name.inspect} at compile time"
|
||||
code.line "$stderr.puts '* WARN: Liquid file system access - inlined partial ' + #{template_name.inspect} + ' at compile time' if $VERBOSE"
|
||||
end
|
||||
# Generate a unique method name for this partial
|
||||
method_name = compiler.register_partial(template_name, partial_source)
|
||||
context_var_name = alias_name || template_name.split('/').last
|
||||
|
||||
if is_for_loop && variable_name_expr
|
||||
# Render for each item in collection
|
||||
compile_for_loop_render(tag, method_name, context_var_name, compiler, code)
|
||||
else
|
||||
# Single render
|
||||
compile_single_render(tag, method_name, context_var_name, compiler, code)
|
||||
end
|
||||
else
|
||||
# Partial not found at compile time - generate runtime fallback
|
||||
code.line "# Partial '#{template_name}' not found at compile time"
|
||||
compile_dynamic_render(tag, compiler, code)
|
||||
end
|
||||
end
|
||||
|
||||
def self.compile_single_render(tag, method_name, context_var_name, compiler, code)
|
||||
variable_name_expr = tag.variable_name_expr
|
||||
attributes = tag.attributes
|
||||
|
||||
# Build the inner assigns hash
|
||||
inner_assigns_var = compiler.generate_var_name("inner")
|
||||
code.line "#{inner_assigns_var} = {}"
|
||||
|
||||
# Copy attributes
|
||||
attributes.each do |key, value|
|
||||
value_expr = ExpressionCompiler.compile(value, compiler)
|
||||
code.line "#{inner_assigns_var}[#{key.inspect}] = #{value_expr}"
|
||||
end
|
||||
|
||||
# Set the context variable if provided
|
||||
if variable_name_expr
|
||||
var_expr = ExpressionCompiler.compile(variable_name_expr, compiler)
|
||||
code.line "#{inner_assigns_var}[#{context_var_name.inspect}] = #{var_expr}"
|
||||
end
|
||||
|
||||
# Call the partial method
|
||||
code.line "__output__ << #{method_name}(#{inner_assigns_var})"
|
||||
end
|
||||
|
||||
def self.compile_for_loop_render(tag, method_name, context_var_name, compiler, code)
|
||||
variable_name_expr = tag.variable_name_expr
|
||||
attributes = tag.attributes
|
||||
template_name = tag.template_name_expr
|
||||
|
||||
coll_var = compiler.generate_var_name("coll")
|
||||
coll_expr = ExpressionCompiler.compile(variable_name_expr, compiler)
|
||||
code.line "#{coll_var} = #{coll_expr}"
|
||||
|
||||
code.line "if #{coll_var}.respond_to?(:each) && #{coll_var}.respond_to?(:count)"
|
||||
code.indent do
|
||||
len_var = compiler.generate_var_name("len")
|
||||
idx_var = compiler.generate_var_name("idx")
|
||||
code.line "#{len_var} = #{coll_var}.count"
|
||||
code.line "#{idx_var} = 0"
|
||||
|
||||
code.line "#{coll_var}.each do |__item__|"
|
||||
code.indent do
|
||||
inner_assigns_var = compiler.generate_var_name("inner")
|
||||
code.line "#{inner_assigns_var} = {}"
|
||||
|
||||
# Copy attributes
|
||||
attributes.each do |key, value|
|
||||
value_expr = ExpressionCompiler.compile(value, compiler)
|
||||
code.line "#{inner_assigns_var}[#{key.inspect}] = #{value_expr}"
|
||||
end
|
||||
|
||||
# Set the context variable
|
||||
code.line "#{inner_assigns_var}[#{context_var_name.inspect}] = __item__"
|
||||
|
||||
# Set forloop
|
||||
code.line "#{inner_assigns_var}['forloop'] = {"
|
||||
code.indent do
|
||||
code.line "'name' => #{template_name.inspect},"
|
||||
code.line "'length' => #{len_var},"
|
||||
code.line "'index' => #{idx_var} + 1,"
|
||||
code.line "'index0' => #{idx_var},"
|
||||
code.line "'rindex' => #{len_var} - #{idx_var},"
|
||||
code.line "'rindex0' => #{len_var} - #{idx_var} - 1,"
|
||||
code.line "'first' => #{idx_var} == 0,"
|
||||
code.line "'last' => #{idx_var} == #{len_var} - 1,"
|
||||
end
|
||||
code.line "}"
|
||||
|
||||
# Call the partial method
|
||||
code.line "__output__ << #{method_name}(#{inner_assigns_var})"
|
||||
|
||||
code.line "#{idx_var} += 1"
|
||||
end
|
||||
code.line "end"
|
||||
end
|
||||
code.line "else"
|
||||
code.indent do
|
||||
# Single render if not a collection
|
||||
compile_single_render(tag, method_name, context_var_name, compiler, code)
|
||||
end
|
||||
code.line "end"
|
||||
end
|
||||
|
||||
def self.compile_dynamic_render(tag, compiler, code)
|
||||
template_name_expr = tag.template_name_expr
|
||||
variable_name_expr = tag.variable_name_expr
|
||||
attributes = tag.attributes
|
||||
alias_name = tag.alias_name
|
||||
is_for_loop = tag.for_loop?
|
||||
|
||||
if compiler.debug?
|
||||
code.line "# Dynamic render (template name from variable)"
|
||||
end
|
||||
|
||||
name_expr = ExpressionCompiler.compile(template_name_expr, compiler)
|
||||
|
||||
# Build attributes hash
|
||||
attrs_var = compiler.generate_var_name("attrs")
|
||||
code.line "#{attrs_var} = {}"
|
||||
attributes.each do |key, value|
|
||||
value_expr = ExpressionCompiler.compile(value, compiler)
|
||||
code.line "#{attrs_var}[#{key.inspect}] = #{value_expr}"
|
||||
end
|
||||
|
||||
var_expr = variable_name_expr ? ExpressionCompiler.compile(variable_name_expr, compiler) : "nil"
|
||||
alias_expr = alias_name ? alias_name.inspect : "nil"
|
||||
|
||||
# Call the external handler for dynamic renders
|
||||
code.line "__output__ << __external__.call(:render, #{name_expr}, #{var_expr}, #{attrs_var}, #{alias_expr}, #{is_for_loop}, __context__)"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,117 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% tablerow %} tags
|
||||
#
|
||||
# Creates HTML table rows from a collection
|
||||
class TableRowCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
var_name = tag.variable_name
|
||||
collection_expr = ExpressionCompiler.compile(tag.collection_name, compiler)
|
||||
attributes = tag.attributes
|
||||
|
||||
# Generate unique variable names
|
||||
coll_var = compiler.generate_var_name("coll")
|
||||
idx_var = compiler.generate_var_name("idx")
|
||||
len_var = compiler.generate_var_name("len")
|
||||
cols_var = compiler.generate_var_name("cols")
|
||||
row_var = compiler.generate_var_name("row")
|
||||
col_var = compiler.generate_var_name("col")
|
||||
|
||||
# Get parameters from attributes hash
|
||||
cols = attributes['cols']
|
||||
cols_expr = cols ? ExpressionCompiler.compile(cols, compiler) : "nil"
|
||||
|
||||
# Evaluate the collection
|
||||
code.line "#{coll_var} = #{collection_expr}"
|
||||
code.line "#{coll_var} = #{coll_var}.to_a if #{coll_var}.is_a?(Range)"
|
||||
|
||||
# Handle limit and offset from attributes
|
||||
offset = attributes['offset']
|
||||
limit = attributes['limit']
|
||||
if offset || limit
|
||||
if offset
|
||||
offset_expr = ExpressionCompiler.compile(offset, compiler)
|
||||
if limit
|
||||
limit_expr = ExpressionCompiler.compile(limit, compiler)
|
||||
code.line "#{coll_var} = #{coll_var}.slice(LR.to_integer(#{offset_expr}), LR.to_integer(#{limit_expr})) || []"
|
||||
else
|
||||
code.line "#{coll_var} = #{coll_var}.drop(LR.to_integer(#{offset_expr}))"
|
||||
end
|
||||
elsif limit
|
||||
limit_expr = ExpressionCompiler.compile(limit, compiler)
|
||||
code.line "#{coll_var} = #{coll_var}.first(LR.to_integer(#{limit_expr}))"
|
||||
end
|
||||
end
|
||||
|
||||
# Setup loop variables
|
||||
code.line "#{len_var} = #{coll_var}.respond_to?(:length) ? #{coll_var}.length : 0"
|
||||
code.line "#{cols_var} = #{cols_expr} || #{len_var}"
|
||||
code.line "#{idx_var} = 0"
|
||||
code.line "#{row_var} = 1"
|
||||
code.line "#{col_var} = 0"
|
||||
|
||||
body = tag.instance_variable_get(:@body)
|
||||
|
||||
# Output initial row (matches interpreter behavior: outputs before loop)
|
||||
code.line "__output__ << \"<tr class=\\\"row1\\\">\\n\""
|
||||
|
||||
# The loop
|
||||
code.line "(#{coll_var}.respond_to?(:each) ? #{coll_var} : []).each do |__item__|"
|
||||
code.indent do
|
||||
code.line "#{col_var} += 1"
|
||||
|
||||
# Output cell start
|
||||
code.line "__output__ << \"<td class=\\\"col\#{#{col_var}}\\\">\""
|
||||
|
||||
# Set loop variables
|
||||
code.line "assigns[#{var_name.inspect}] = __item__"
|
||||
code.line "assigns['tablerowloop'] = {"
|
||||
code.indent do
|
||||
code.line "'length' => #{len_var},"
|
||||
code.line "'index' => #{idx_var} + 1,"
|
||||
code.line "'index0' => #{idx_var},"
|
||||
code.line "'rindex' => #{len_var} - #{idx_var},"
|
||||
code.line "'rindex0' => #{len_var} - #{idx_var} - 1,"
|
||||
code.line "'first' => #{idx_var} == 0,"
|
||||
code.line "'last' => #{idx_var} == #{len_var} - 1,"
|
||||
code.line "'col' => #{col_var},"
|
||||
code.line "'col0' => #{col_var} - 1,"
|
||||
code.line "'row' => #{row_var},"
|
||||
code.line "'col_first' => #{col_var} == 1,"
|
||||
code.line "'col_last' => #{col_var} == #{cols_var},"
|
||||
end
|
||||
code.line "}"
|
||||
|
||||
# Compile the body
|
||||
BlockBodyCompiler.compile(body, compiler, code)
|
||||
|
||||
# Output cell end
|
||||
code.line "__output__ << '</td>'"
|
||||
|
||||
# End row and start new row if needed (but not on last item)
|
||||
code.line "if #{col_var} == #{cols_var} && #{idx_var} != #{len_var} - 1"
|
||||
code.indent do
|
||||
code.line "__output__ << \"</tr>\\n<tr class=\\\"row\#{#{row_var} + 1}\\\">\""
|
||||
code.line "#{col_var} = 0"
|
||||
code.line "#{row_var} += 1"
|
||||
end
|
||||
code.line "end"
|
||||
|
||||
code.line "#{idx_var} += 1"
|
||||
end
|
||||
code.line "end"
|
||||
|
||||
# Close the final row
|
||||
code.line "__output__ << \"</tr>\\n\""
|
||||
|
||||
# Clean up
|
||||
code.line "assigns.delete(#{var_name.inspect})"
|
||||
code.line "assigns.delete('tablerowloop')"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,38 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
module Tags
|
||||
# Compiles {% unless %} / {% elsif %} / {% else %} / {% endunless %} tags
|
||||
#
|
||||
# Unless is like if, but the first condition is negated
|
||||
class UnlessCompiler
|
||||
def self.compile(tag, compiler, code)
|
||||
blocks = tag.blocks
|
||||
|
||||
blocks.each_with_index do |block, index|
|
||||
condition_expr = ConditionCompiler.compile(block, compiler)
|
||||
|
||||
if index == 0
|
||||
# First block is negated (unless = if not)
|
||||
code.line "unless #{condition_expr}"
|
||||
elsif block.else?
|
||||
code.line "else"
|
||||
else
|
||||
# Subsequent blocks (elsif) are normal
|
||||
code.line "elsif #{condition_expr}"
|
||||
end
|
||||
|
||||
code.indent do
|
||||
if block.attachment
|
||||
BlockBodyCompiler.compile(block.attachment, compiler, code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
code.line "end"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Compile
|
||||
# VariableCompiler compiles Liquid variable expressions ({{ ... }}) to Ruby code.
|
||||
#
|
||||
# A Variable consists of:
|
||||
# - A name expression (the value to output)
|
||||
# - Zero or more filters to apply
|
||||
class VariableCompiler
|
||||
# Compile a Variable node and append the result to the output buffer
|
||||
# @param variable [Liquid::Variable] The variable node
|
||||
# @param compiler [RubyCompiler] The main compiler instance
|
||||
# @param code [CodeGenerator] The code generator
|
||||
def self.compile(variable, compiler, code)
|
||||
value_expr = compile_to_expression(variable, compiler)
|
||||
code.line "__output__ << LR.output(#{value_expr})"
|
||||
end
|
||||
|
||||
# Compile a Variable node to a Ruby expression (without output)
|
||||
# @param variable [Liquid::Variable] The variable node
|
||||
# @param compiler [RubyCompiler] The main compiler instance
|
||||
# @return [String] Ruby code expression
|
||||
def self.compile_to_expression(variable, compiler)
|
||||
# Compile the base name expression
|
||||
base_expr = ExpressionCompiler.compile(variable.name, compiler)
|
||||
|
||||
# Apply filters if any
|
||||
if variable.filters && !variable.filters.empty?
|
||||
FilterCompiler.compile(base_expr, variable.filters, compiler)
|
||||
else
|
||||
base_expr
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,183 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Container for liquid nodes which conveniently wraps decision making logic
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# c = Condition.new(1, '==', 1)
|
||||
# c.evaluate #=> true
|
||||
#
|
||||
class Condition # :nodoc:
|
||||
@@operators = {
|
||||
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
||||
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
'<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
'<' => :<,
|
||||
'>' => :>,
|
||||
'>=' => :>=,
|
||||
'<=' => :<=,
|
||||
'contains' => lambda do |_cond, left, right|
|
||||
if left && right && left.respond_to?(:include?)
|
||||
right = right.to_s if left.is_a?(String)
|
||||
left.include?(right)
|
||||
else
|
||||
false
|
||||
end
|
||||
rescue Encoding::CompatibilityError
|
||||
# "✅".b.include?("✅") raises Encoding::CompatibilityError despite being materially equal
|
||||
left.b.include?(right.b)
|
||||
end,
|
||||
}
|
||||
|
||||
class MethodLiteral
|
||||
attr_reader :method_name, :to_s
|
||||
|
||||
def initialize(method_name, to_s)
|
||||
@method_name = method_name
|
||||
@to_s = to_s
|
||||
end
|
||||
end
|
||||
|
||||
@@method_literals = {
|
||||
'blank' => MethodLiteral.new(:blank?, '').freeze,
|
||||
'empty' => MethodLiteral.new(:empty?, '').freeze,
|
||||
}
|
||||
|
||||
def self.operators
|
||||
@@operators
|
||||
end
|
||||
|
||||
def self.parse_expression(parse_context, markup)
|
||||
@@method_literals[markup] || parse_context.parse_expression(markup)
|
||||
end
|
||||
|
||||
attr_reader :attachment, :child_condition
|
||||
attr_accessor :left, :operator, :right
|
||||
|
||||
def initialize(left = nil, operator = nil, right = nil)
|
||||
@left = left
|
||||
@operator = operator
|
||||
@right = right
|
||||
|
||||
@child_relation = nil
|
||||
@child_condition = nil
|
||||
end
|
||||
|
||||
def evaluate(context = deprecated_default_context)
|
||||
condition = self
|
||||
result = nil
|
||||
loop do
|
||||
result = interpret_condition(condition.left, condition.right, condition.operator, context)
|
||||
|
||||
case condition.child_relation
|
||||
when :or
|
||||
break if Liquid::Utils.to_liquid_value(result)
|
||||
when :and
|
||||
break unless Liquid::Utils.to_liquid_value(result)
|
||||
else
|
||||
break
|
||||
end
|
||||
condition = condition.child_condition
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def or(condition)
|
||||
@child_relation = :or
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
def and(condition)
|
||||
@child_relation = :and
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
def attach(attachment)
|
||||
@attachment = attachment
|
||||
end
|
||||
|
||||
def else?
|
||||
false
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :child_relation
|
||||
|
||||
private
|
||||
|
||||
def equal_variables(left, right)
|
||||
if left.is_a?(MethodLiteral)
|
||||
if right.respond_to?(left.method_name)
|
||||
return right.send(left.method_name)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
if right.is_a?(MethodLiteral)
|
||||
if left.respond_to?(right.method_name)
|
||||
return left.send(right.method_name)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
left == right
|
||||
end
|
||||
|
||||
def interpret_condition(left, right, op, context)
|
||||
# If the operator is empty this means that the decision statement is just
|
||||
# a single variable. We can just poll this variable from the context and
|
||||
# return this as the result.
|
||||
return context.evaluate(left) if op.nil?
|
||||
|
||||
left = Liquid::Utils.to_liquid_value(context.evaluate(left))
|
||||
right = Liquid::Utils.to_liquid_value(context.evaluate(right))
|
||||
|
||||
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
|
||||
|
||||
if operation.respond_to?(:call)
|
||||
operation.call(self, left, right)
|
||||
elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
|
||||
begin
|
||||
left.send(operation, right)
|
||||
rescue ::ArgumentError => e
|
||||
raise Liquid::ArgumentError, e.message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def deprecated_default_context
|
||||
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
|
||||
" and will be removed from Liquid 6.0.0.")
|
||||
Context.new
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
@node.left,
|
||||
@node.right,
|
||||
@node.child_condition,
|
||||
@node.attachment
|
||||
].compact
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ElseCondition < Condition
|
||||
def else?
|
||||
true
|
||||
end
|
||||
|
||||
def evaluate(_context)
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Const
|
||||
EMPTY_HASH = {}.freeze
|
||||
EMPTY_ARRAY = [].freeze
|
||||
end
|
||||
end
|
||||
@@ -1,295 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Context keeps the variable stack and resolves variables, as well as keywords
|
||||
#
|
||||
# context['variable'] = 'testing'
|
||||
# context['variable'] #=> 'testing'
|
||||
# context['true'] #=> true
|
||||
# context['10.2232'] #=> 10.2232
|
||||
#
|
||||
# context.stack do
|
||||
# context['bob'] = 'bobsen'
|
||||
# end
|
||||
#
|
||||
# context['bob'] #=> nil class Context
|
||||
class Context
|
||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
|
||||
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters, :environment
|
||||
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
def self.build(environment: Environment.default, environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
|
||||
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, environment, &block)
|
||||
end
|
||||
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}, environment = Environment.default)
|
||||
@environment = environment
|
||||
@environments = [environments]
|
||||
@environments.flatten!
|
||||
|
||||
@static_environments = [static_environments].flatten(1).freeze
|
||||
@scopes = [outer_scope || {}]
|
||||
@registers = registers.is_a?(Registers) ? registers : Registers.new(registers)
|
||||
@errors = []
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(environment.default_resource_limits)
|
||||
@base_scope_depth = 0
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@global_filter = nil
|
||||
@disabled_tags = {}
|
||||
|
||||
# Instead of constructing new StringScanner objects for each Expression parse,
|
||||
# we recycle the same one.
|
||||
@string_scanner = StringScanner.new("")
|
||||
|
||||
@registers.static[:cached_partials] ||= {}
|
||||
@registers.static[:file_system] ||= environment.file_system
|
||||
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
|
||||
|
||||
self.exception_renderer = environment.exception_renderer
|
||||
if rethrow_errors
|
||||
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
|
||||
end
|
||||
|
||||
yield self if block_given?
|
||||
|
||||
# Do this last, since it could result in this object being passed to a Proc in the environment
|
||||
squash_instance_assigns_with_environments
|
||||
end
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
def warnings
|
||||
@warnings ||= []
|
||||
end
|
||||
|
||||
def strainer
|
||||
@strainer ||= @environment.create_strainer(self, @filters)
|
||||
end
|
||||
|
||||
# Adds filters to this context.
|
||||
#
|
||||
# Note that this does not register the filters with the main Template object. see <tt>Template.register_filter</tt>
|
||||
# for that
|
||||
def add_filters(filters)
|
||||
filters = [filters].flatten.compact
|
||||
@filters += filters
|
||||
@strainer = nil
|
||||
end
|
||||
|
||||
def apply_global_filter(obj)
|
||||
global_filter.nil? ? obj : global_filter.call(obj)
|
||||
end
|
||||
|
||||
# are there any not handled interrupts?
|
||||
def interrupt?
|
||||
!@interrupts.empty?
|
||||
end
|
||||
|
||||
# push an interrupt to the stack. this interrupt is considered not handled.
|
||||
def push_interrupt(e)
|
||||
@interrupts.push(e)
|
||||
end
|
||||
|
||||
# pop an interrupt from the stack
|
||||
def pop_interrupt
|
||||
@interrupts.pop
|
||||
end
|
||||
|
||||
def handle_error(e, line_number = nil)
|
||||
e = internal_error unless e.is_a?(Liquid::Error)
|
||||
e.template_name ||= template_name
|
||||
e.line_number ||= line_number
|
||||
errors.push(e)
|
||||
exception_renderer.call(e).to_s
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
strainer.invoke(method, *args).to_liquid
|
||||
end
|
||||
|
||||
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
||||
def push(new_scope = {})
|
||||
@scopes.unshift(new_scope)
|
||||
check_overflow
|
||||
end
|
||||
|
||||
# Merge a hash of variables in the current local scope
|
||||
def merge(new_scopes)
|
||||
@scopes[0].merge!(new_scopes)
|
||||
end
|
||||
|
||||
# Pop from the stack. use <tt>Context#stack</tt> instead
|
||||
def pop
|
||||
raise ContextError if @scopes.size == 1
|
||||
@scopes.shift
|
||||
end
|
||||
|
||||
# Pushes a new local scope on the stack, pops it at the end of the block
|
||||
#
|
||||
# Example:
|
||||
# context.stack do
|
||||
# context['var'] = 'hi'
|
||||
# end
|
||||
#
|
||||
# context['var'] #=> nil
|
||||
def stack(new_scope = {})
|
||||
push(new_scope)
|
||||
yield
|
||||
ensure
|
||||
pop
|
||||
end
|
||||
|
||||
# Creates a new context inheriting resource limits, filters, environment etc.,
|
||||
# but with an isolated scope.
|
||||
def new_isolated_subcontext
|
||||
check_overflow
|
||||
|
||||
self.class.build(
|
||||
environment: @environment,
|
||||
resource_limits: resource_limits,
|
||||
static_environments: static_environments,
|
||||
registers: Registers.new(registers),
|
||||
).tap do |subcontext|
|
||||
subcontext.base_scope_depth = base_scope_depth + 1
|
||||
subcontext.exception_renderer = exception_renderer
|
||||
subcontext.filters = @filters
|
||||
subcontext.strainer = nil
|
||||
subcontext.errors = errors
|
||||
subcontext.warnings = warnings
|
||||
subcontext.disabled_tags = @disabled_tags
|
||||
end
|
||||
end
|
||||
|
||||
def clear_instance_assigns
|
||||
@scopes[0] = {}
|
||||
end
|
||||
|
||||
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
|
||||
def []=(key, value)
|
||||
@scopes[0][key] = value
|
||||
end
|
||||
|
||||
# Look up variable, either resolve directly after considering the name. We can directly handle
|
||||
# Strings, digits, floats and booleans (true,false).
|
||||
# If no match is made we lookup the variable in the current scope and
|
||||
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
|
||||
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
|
||||
#
|
||||
# Example:
|
||||
# products == empty #=> products.empty?
|
||||
def [](expression)
|
||||
evaluate(Expression.parse(expression, @string_scanner))
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
self[key] != nil
|
||||
end
|
||||
|
||||
def evaluate(object)
|
||||
object.respond_to?(:evaluate) ? object.evaluate(self) : object
|
||||
end
|
||||
|
||||
# Fetches an object starting at the local scope and then moving up the hierachy
|
||||
def find_variable(key, raise_on_not_found: true)
|
||||
# This was changed from find() to find_index() because this is a very hot
|
||||
# path and find_index() is optimized in MRI to reduce object allocation
|
||||
index = @scopes.find_index { |s| s.key?(key) }
|
||||
|
||||
variable = if index
|
||||
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
|
||||
else
|
||||
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
|
||||
end
|
||||
|
||||
# update variable's context before invoking #to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
|
||||
liquid_variable = variable.to_liquid
|
||||
|
||||
liquid_variable.context = self if variable != liquid_variable && liquid_variable.respond_to?(:context=)
|
||||
|
||||
liquid_variable
|
||||
end
|
||||
|
||||
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
|
||||
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
|
||||
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
||||
end
|
||||
|
||||
value = obj[key]
|
||||
|
||||
if value.is_a?(Proc) && obj.respond_to?(:[]=)
|
||||
obj[key] = value.arity == 0 ? value.call : value.call(self)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
def with_disabled_tags(tag_names)
|
||||
tag_names.each do |name|
|
||||
@disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
|
||||
end
|
||||
yield
|
||||
ensure
|
||||
tag_names.each do |name|
|
||||
@disabled_tags[name] -= 1
|
||||
end
|
||||
end
|
||||
|
||||
def tag_disabled?(tag_name)
|
||||
@disabled_tags.fetch(tag_name, 0) > 0
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
|
||||
|
||||
private
|
||||
|
||||
attr_reader :base_scope_depth
|
||||
|
||||
def try_variable_find_in_environments(key, raise_on_not_found:)
|
||||
@environments.each do |environment|
|
||||
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
|
||||
if !found_variable.nil? || @strict_variables && raise_on_not_found
|
||||
return found_variable
|
||||
end
|
||||
end
|
||||
@static_environments.each do |environment|
|
||||
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
|
||||
if !found_variable.nil? || @strict_variables && raise_on_not_found
|
||||
return found_variable
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def check_overflow
|
||||
raise StackLevelError, "Nesting too deep" if overflow?
|
||||
end
|
||||
|
||||
def overflow?
|
||||
base_scope_depth + @scopes.length > Block::MAX_DEPTH
|
||||
end
|
||||
|
||||
def internal_error
|
||||
# raise and catch to set backtrace and cause on exception
|
||||
raise Liquid::InternalError, 'internal'
|
||||
rescue Liquid::InternalError => exc
|
||||
exc
|
||||
end
|
||||
|
||||
def squash_instance_assigns_with_environments
|
||||
@scopes.last.each_key do |k|
|
||||
@environments.each do |env|
|
||||
if env.key?(k)
|
||||
scopes.last[k] = lookup_and_evaluate(env, k)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end # squash_instance_assigns_with_environments
|
||||
end # Context
|
||||
end # Liquid
|
||||
@@ -1,22 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "set"
|
||||
|
||||
module Liquid
|
||||
class Deprecations
|
||||
class << self
|
||||
attr_accessor :warned
|
||||
|
||||
Deprecations.warned = Set.new
|
||||
|
||||
def warn(name, alternative)
|
||||
return if warned.include?(name)
|
||||
|
||||
warned << name
|
||||
|
||||
caller_location = caller_locations(2, 1).first
|
||||
Warning.warn("[DEPRECATION] #{name} is deprecated. Use #{alternative} instead. Called from #{caller_location}\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,65 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Document
|
||||
def self.parse(tokens, parse_context)
|
||||
doc = new(parse_context)
|
||||
doc.parse(tokens, parse_context)
|
||||
doc
|
||||
end
|
||||
|
||||
attr_reader :parse_context, :body
|
||||
|
||||
def initialize(parse_context)
|
||||
@parse_context = parse_context
|
||||
@body = new_body
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@body.nodelist
|
||||
end
|
||||
|
||||
def parse(tokenizer, parse_context)
|
||||
while parse_body(tokenizer)
|
||||
end
|
||||
@body.freeze
|
||||
rescue SyntaxError => e
|
||||
e.line_number ||= parse_context.line_number
|
||||
raise
|
||||
end
|
||||
|
||||
def unknown_tag(tag, _markup, _tokenizer)
|
||||
case tag
|
||||
when 'else', 'end'
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
|
||||
else
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@body.render_to_output_buffer(context, output)
|
||||
end
|
||||
|
||||
def render(context)
|
||||
render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def new_body
|
||||
parse_context.new_block_body
|
||||
end
|
||||
|
||||
def parse_body(tokenizer)
|
||||
@body.parse(tokenizer, parse_context) do |unknown_tag_name, unknown_tag_markup|
|
||||
if unknown_tag_name
|
||||
unknown_tag(unknown_tag_name, unknown_tag_markup, tokenizer)
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,84 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
# A drop in liquid is a class which allows you to export DOM like things to liquid.
|
||||
# Methods of drops are callable.
|
||||
# The main use for liquid drops is to implement lazy loaded objects.
|
||||
# If you would like to make data available to the web designers which you don't want loaded unless needed then
|
||||
# a drop is a great way to do that.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# class ProductDrop < Liquid::Drop
|
||||
# def top_sales
|
||||
# Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
|
||||
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
|
||||
#
|
||||
# Your drop can either implement the methods sans any parameters
|
||||
# or implement the liquid_method_missing(name) method which is a catch all.
|
||||
class Drop
|
||||
attr_writer :context
|
||||
|
||||
def initialize
|
||||
@context = nil
|
||||
end
|
||||
|
||||
# Catch all for the method
|
||||
def liquid_method_missing(method)
|
||||
return nil unless @context&.strict_variables
|
||||
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
|
||||
end
|
||||
|
||||
# called by liquid to invoke a drop
|
||||
def invoke_drop(method_or_key)
|
||||
if self.class.invokable?(method_or_key)
|
||||
send(method_or_key)
|
||||
else
|
||||
liquid_method_missing(method_or_key)
|
||||
end
|
||||
end
|
||||
|
||||
def key?(_name)
|
||||
true
|
||||
end
|
||||
|
||||
def inspect
|
||||
self.class.to_s
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
|
||||
def to_s
|
||||
self.class.name
|
||||
end
|
||||
|
||||
alias_method :[], :invoke_drop
|
||||
|
||||
# Check for method existence without invoking respond_to?, which creates symbols
|
||||
def self.invokable?(method_name)
|
||||
invokable_methods.include?(method_name.to_s)
|
||||
end
|
||||
|
||||
def self.invokable_methods
|
||||
@invokable_methods ||= begin
|
||||
blacklist = Liquid::Drop.public_instance_methods + [:each]
|
||||
|
||||
if include?(Enumerable)
|
||||
blacklist += Enumerable.public_instance_methods
|
||||
blacklist -= [:sort, :count, :first, :min, :max]
|
||||
end
|
||||
|
||||
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
|
||||
Set.new(whitelist.map(&:to_s))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,159 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# The Environment is the container for all configuration options of Liquid, such as
|
||||
# the registered tags, filters, and the default error mode.
|
||||
class Environment
|
||||
# The default error mode for all templates. This can be overridden on a
|
||||
# per-template basis.
|
||||
attr_accessor :error_mode
|
||||
|
||||
# The tags that are available to use in the template.
|
||||
attr_accessor :tags
|
||||
|
||||
# The strainer template which is used to store filters that are available to
|
||||
# use in templates.
|
||||
attr_accessor :strainer_template
|
||||
|
||||
# The exception renderer that is used to render exceptions that are raised
|
||||
# when rendering a template
|
||||
attr_accessor :exception_renderer
|
||||
|
||||
# The default file system that is used to load templates from.
|
||||
attr_accessor :file_system
|
||||
|
||||
# The default resource limits that are used to limit the resources that a
|
||||
# template can consume.
|
||||
attr_accessor :default_resource_limits
|
||||
|
||||
class << self
|
||||
# Creates a new environment instance.
|
||||
#
|
||||
# @param tags [Hash] The tags that are available to use in
|
||||
# the template.
|
||||
# @param file_system The default file system that is used
|
||||
# to load templates from.
|
||||
# @param error_mode [Symbol] The default error mode for all templates
|
||||
# (either :strict, :warn, or :lax).
|
||||
# @param exception_renderer [Proc] The exception renderer that is used to
|
||||
# render exceptions.
|
||||
# @yieldparam environment [Environment] The environment instance that is being built.
|
||||
# @return [Environment] The new environment instance.
|
||||
def build(tags: nil, file_system: nil, error_mode: nil, exception_renderer: nil)
|
||||
ret = new
|
||||
ret.tags = tags if tags
|
||||
ret.file_system = file_system if file_system
|
||||
ret.error_mode = error_mode if error_mode
|
||||
ret.exception_renderer = exception_renderer if exception_renderer
|
||||
yield ret if block_given?
|
||||
ret.freeze
|
||||
end
|
||||
|
||||
# Returns the default environment instance.
|
||||
#
|
||||
# @return [Environment] The default environment instance.
|
||||
def default
|
||||
@default ||= new
|
||||
end
|
||||
|
||||
# Sets the default environment instance for the duration of the block
|
||||
#
|
||||
# @param environment [Environment] The environment instance to use as the default for the
|
||||
# duration of the block.
|
||||
# @yield
|
||||
# @return [Object] The return value of the block.
|
||||
def dangerously_override(environment)
|
||||
original_default = @default
|
||||
@default = environment
|
||||
yield
|
||||
ensure
|
||||
@default = original_default
|
||||
end
|
||||
end
|
||||
|
||||
# Initializes a new environment instance.
|
||||
# @api private
|
||||
def initialize
|
||||
@tags = Tags::STANDARD_TAGS.dup
|
||||
@error_mode = :lax
|
||||
@strainer_template = Class.new(StrainerTemplate).tap do |klass|
|
||||
klass.add_filter(StandardFilters)
|
||||
end
|
||||
@exception_renderer = ->(exception) { exception }
|
||||
@file_system = BlankFileSystem.new
|
||||
@default_resource_limits = Const::EMPTY_HASH
|
||||
@strainer_template_class_cache = {}
|
||||
end
|
||||
|
||||
# Registers a new tag with the environment.
|
||||
#
|
||||
# @param name [String] The name of the tag.
|
||||
# @param klass [Liquid::Tag] The class that implements the tag.
|
||||
# @return [void]
|
||||
def register_tag(name, klass)
|
||||
@tags[name] = klass
|
||||
end
|
||||
|
||||
# Registers a new filter with the environment.
|
||||
#
|
||||
# @param filter [Module] The module that contains the filter methods.
|
||||
# @return [void]
|
||||
def register_filter(filter)
|
||||
@strainer_template_class_cache.clear
|
||||
@strainer_template.add_filter(filter)
|
||||
end
|
||||
|
||||
# Registers multiple filters with this environment.
|
||||
#
|
||||
# @param filters [Array<Module>] The modules that contain the filter methods.
|
||||
# @return [self]
|
||||
def register_filters(filters)
|
||||
@strainer_template_class_cache.clear
|
||||
filters.each { |f| @strainer_template.add_filter(f) }
|
||||
self
|
||||
end
|
||||
|
||||
# Creates a new strainer instance with the given filters, caching the result
|
||||
# for faster lookup.
|
||||
#
|
||||
# @param context [Liquid::Context] The context that the strainer will be
|
||||
# used in.
|
||||
# @param filters [Array<Module>] The filters that the strainer will have
|
||||
# access to.
|
||||
# @return [Liquid::Strainer] The new strainer instance.
|
||||
def create_strainer(context, filters = Const::EMPTY_ARRAY)
|
||||
return @strainer_template.new(context) if filters.empty?
|
||||
|
||||
strainer_template = @strainer_template_class_cache[filters] ||= begin
|
||||
klass = Class.new(@strainer_template)
|
||||
filters.each { |f| klass.add_filter(f) }
|
||||
klass
|
||||
end
|
||||
|
||||
strainer_template.new(context)
|
||||
end
|
||||
|
||||
# Returns the names of all the filter methods that are available to use in
|
||||
# the strainer template.
|
||||
#
|
||||
# @return [Array<String>] The names of all the filter methods.
|
||||
def filter_method_names
|
||||
@strainer_template.filter_method_names
|
||||
end
|
||||
|
||||
# Returns the tag class for the given tag name.
|
||||
#
|
||||
# @param name [String] The name of the tag.
|
||||
# @return [Liquid::Tag] The tag class.
|
||||
def tag_for_name(name)
|
||||
@tags[name]
|
||||
end
|
||||
|
||||
def freeze
|
||||
@tags.freeze
|
||||
# TODO: freeze the tags, currently this is not possible because of liquid-c
|
||||
# @strainer_template.freeze
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,59 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Error < ::StandardError
|
||||
attr_accessor :line_number
|
||||
attr_accessor :template_name
|
||||
attr_accessor :markup_context
|
||||
|
||||
def to_s(with_prefix = true)
|
||||
str = +""
|
||||
str << message_prefix if with_prefix
|
||||
str << super()
|
||||
|
||||
if markup_context
|
||||
str << " "
|
||||
str << markup_context
|
||||
end
|
||||
|
||||
str
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_prefix
|
||||
str = +""
|
||||
str << if is_a?(SyntaxError)
|
||||
"Liquid syntax error"
|
||||
else
|
||||
"Liquid error"
|
||||
end
|
||||
|
||||
if line_number
|
||||
str << " ("
|
||||
str << template_name << " " if template_name
|
||||
str << "line " << line_number.to_s << ")"
|
||||
end
|
||||
|
||||
str << ": "
|
||||
str
|
||||
end
|
||||
end
|
||||
|
||||
ArgumentError = Class.new(Error)
|
||||
ContextError = Class.new(Error)
|
||||
FileSystemError = Class.new(Error)
|
||||
StandardError = Class.new(Error)
|
||||
SyntaxError = Class.new(Error)
|
||||
StackLevelError = Class.new(Error)
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
UndefinedDropMethod = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
MethodOverrideError = Class.new(Error)
|
||||
DisabledError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
TemplateEncodingError = Class.new(Error)
|
||||
end
|
||||
@@ -1,124 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Expression
|
||||
LITERALS = {
|
||||
nil => nil,
|
||||
'nil' => nil,
|
||||
'null' => nil,
|
||||
'' => nil,
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'blank' => '',
|
||||
'empty' => '',
|
||||
# in lax mode, minus sign can be a VariableLookup
|
||||
# For simplicity and performace, we treat it like a literal
|
||||
'-' => VariableLookup.parse("-", nil).freeze,
|
||||
}.freeze
|
||||
|
||||
DOT = ".".ord
|
||||
ZERO = "0".ord
|
||||
NINE = "9".ord
|
||||
DASH = "-".ord
|
||||
|
||||
# Use an atomic group (?>...) to avoid pathological backtracing from
|
||||
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
|
||||
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
||||
INTEGER_REGEX = /\A(-?\d+)\z/
|
||||
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
|
||||
|
||||
class << self
|
||||
def parse(markup, ss = StringScanner.new(""), cache = nil)
|
||||
return unless markup
|
||||
|
||||
markup = markup.strip # markup can be a frozen string
|
||||
|
||||
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
||||
(markup.start_with?("'") && markup.end_with?("'"))
|
||||
return markup[1..-2]
|
||||
elsif LITERALS.key?(markup)
|
||||
return LITERALS[markup]
|
||||
end
|
||||
|
||||
# Cache only exists during parsing
|
||||
if cache
|
||||
return cache[markup] if cache.key?(markup)
|
||||
|
||||
cache[markup] = inner_parse(markup, ss, cache).freeze
|
||||
else
|
||||
inner_parse(markup, ss, nil).freeze
|
||||
end
|
||||
end
|
||||
|
||||
def inner_parse(markup, ss, cache)
|
||||
if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
|
||||
return RangeLookup.parse(
|
||||
Regexp.last_match(1),
|
||||
Regexp.last_match(2),
|
||||
ss,
|
||||
cache,
|
||||
)
|
||||
end
|
||||
|
||||
if (num = parse_number(markup, ss))
|
||||
num
|
||||
else
|
||||
VariableLookup.parse(markup, ss, cache)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_number(markup, ss)
|
||||
# check if the markup is simple integer or float
|
||||
case markup
|
||||
when INTEGER_REGEX
|
||||
return Integer(markup, 10)
|
||||
when FLOAT_REGEX
|
||||
return markup.to_f
|
||||
end
|
||||
|
||||
ss.string = markup
|
||||
# the first byte must be a digit or a dash
|
||||
byte = ss.scan_byte
|
||||
|
||||
return false if byte != DASH && (byte < ZERO || byte > NINE)
|
||||
|
||||
if byte == DASH
|
||||
peek_byte = ss.peek_byte
|
||||
|
||||
# if it starts with a dash, the next byte must be a digit
|
||||
return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
|
||||
end
|
||||
|
||||
# The markup could be a float with multiple dots
|
||||
first_dot_pos = nil
|
||||
num_end_pos = nil
|
||||
|
||||
while (byte = ss.scan_byte)
|
||||
return false if byte != DOT && (byte < ZERO || byte > NINE)
|
||||
|
||||
# we found our number and now we are just scanning the rest of the string
|
||||
next if num_end_pos
|
||||
|
||||
if byte == DOT
|
||||
if first_dot_pos.nil?
|
||||
first_dot_pos = ss.pos
|
||||
else
|
||||
# we found another dot, so we know that the number ends here
|
||||
num_end_pos = ss.pos - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
num_end_pos = markup.length if ss.eos?
|
||||
|
||||
if num_end_pos
|
||||
# number ends with a number "123.123"
|
||||
markup.byteslice(0, num_end_pos).to_f
|
||||
else
|
||||
# number ends with a dot "123."
|
||||
markup.byteslice(0, first_dot_pos).to_f
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,76 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'time'
|
||||
require 'date'
|
||||
|
||||
class String # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Symbol # :nodoc:
|
||||
def to_liquid
|
||||
to_s
|
||||
end
|
||||
end
|
||||
|
||||
class Array # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Hash # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Numeric # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Range # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Time # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class DateTime < Date # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Date # :nodoc:
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class TrueClass
|
||||
def to_liquid # :nodoc:
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class FalseClass
|
||||
def to_liquid # :nodoc:
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class NilClass
|
||||
def to_liquid # :nodoc:
|
||||
self
|
||||
end
|
||||
end
|
||||
@@ -1,75 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
|
||||
#
|
||||
# You can implement subclasses that retrieve templates from the database, from the file system using a different
|
||||
# path structure, you can provide them as hard-coded inline strings, or any manner that you see fit.
|
||||
#
|
||||
# You can add additional instance variables, arguments, or methods as needed.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
|
||||
# liquid = Liquid::Template.parse(template)
|
||||
#
|
||||
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
|
||||
class BlankFileSystem
|
||||
# Called by Liquid to retrieve a template file
|
||||
def read_template_file(_template_path)
|
||||
raise FileSystemError, "This liquid context does not allow includes."
|
||||
end
|
||||
end
|
||||
|
||||
# This implements an abstract file system which retrieves template files named in a manner similar to Rails partials,
|
||||
# ie. with the template name prefixed with an underscore. The extension ".liquid" is also added.
|
||||
#
|
||||
# For security reasons, template paths are only allowed to contain letters, numbers, and underscore.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# file_system = Liquid::LocalFileSystem.new("/some/path")
|
||||
#
|
||||
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
|
||||
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
||||
#
|
||||
# Optionally in the second argument you can specify a custom pattern for template filenames.
|
||||
# The Kernel::sprintf format specification is used.
|
||||
# Default pattern is "_%s.liquid".
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
|
||||
#
|
||||
# file_system.full_path("index") # => "/some/path/index.html"
|
||||
#
|
||||
class LocalFileSystem
|
||||
attr_accessor :root
|
||||
|
||||
def initialize(root, pattern = "_%s.liquid")
|
||||
@root = root
|
||||
@pattern = pattern
|
||||
end
|
||||
|
||||
def read_template_file(template_path)
|
||||
full_path = full_path(template_path)
|
||||
raise FileSystemError, "No such template '#{template_path}'" unless File.exist?(full_path)
|
||||
|
||||
File.read(full_path)
|
||||
end
|
||||
|
||||
def full_path(template_path)
|
||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
|
||||
|
||||
full_path = if template_path.include?('/')
|
||||
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
|
||||
else
|
||||
File.join(root, @pattern % template_path)
|
||||
end
|
||||
|
||||
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
|
||||
|
||||
full_path
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,89 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type object
|
||||
# @liquid_name forloop
|
||||
# @liquid_summary
|
||||
# Information about a parent [`for` loop](/docs/api/liquid/tags/for).
|
||||
class ForloopDrop < Drop
|
||||
def initialize(name, length, parentloop)
|
||||
@name = name
|
||||
@length = length
|
||||
@parentloop = parentloop
|
||||
@index = 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_name length
|
||||
# @liquid_summary
|
||||
# The total number of iterations in the loop.
|
||||
# @liquid_return [number]
|
||||
attr_reader :length
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_name parentloop
|
||||
# @liquid_summary
|
||||
# The parent `forloop` object.
|
||||
# @liquid_description
|
||||
# If the current `for` loop isn't nested inside another `for` loop, then `nil` is returned.
|
||||
# @liquid_return [forloop]
|
||||
attr_reader :parentloop
|
||||
|
||||
attr_reader :name
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index
|
||||
@index + 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index0
|
||||
@index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex
|
||||
@length - @index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex0
|
||||
@length - @index - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the first. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def first
|
||||
@index == 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the last. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def last
|
||||
@index == @length - 1
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def increment!
|
||||
@index += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,41 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'yaml'
|
||||
|
||||
module Liquid
|
||||
class I18n
|
||||
DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml")
|
||||
|
||||
TranslationError = Class.new(StandardError)
|
||||
|
||||
attr_reader :path
|
||||
|
||||
def initialize(path = DEFAULT_LOCALE)
|
||||
@path = path
|
||||
end
|
||||
|
||||
def translate(name, vars = {})
|
||||
interpolate(deep_fetch_translation(name), vars)
|
||||
end
|
||||
alias_method :t, :translate
|
||||
|
||||
def locale
|
||||
@locale ||= YAML.load_file(@path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def interpolate(name, vars)
|
||||
name.gsub(/%\{(\w+)\}/) do
|
||||
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
|
||||
(vars[Regexp.last_match(1).to_sym]).to_s
|
||||
end
|
||||
end
|
||||
|
||||
def deep_fetch_translation(name)
|
||||
name.split('.').reduce(locale) do |level, cur|
|
||||
level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,18 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# An interrupt is any command that breaks processing of a block (ex: a for loop).
|
||||
class Interrupt
|
||||
attr_reader :message
|
||||
|
||||
def initialize(message = nil)
|
||||
@message = message || "interrupt"
|
||||
end
|
||||
end
|
||||
|
||||
# Interrupt that is thrown whenever a {% break %} is called.
|
||||
class BreakInterrupt < Interrupt; end
|
||||
|
||||
# Interrupt that is thrown whenever a {% continue %} is called.
|
||||
class ContinueInterrupt < Interrupt; end
|
||||
end
|
||||
@@ -1,179 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Lexer
|
||||
CLOSE_ROUND = [:close_round, ")"].freeze
|
||||
CLOSE_SQUARE = [:close_square, "]"].freeze
|
||||
COLON = [:colon, ":"].freeze
|
||||
COMMA = [:comma, ","].freeze
|
||||
COMPARISION_NOT_EQUAL = [:comparison, "!="].freeze
|
||||
COMPARISON_CONTAINS = [:comparison, "contains"].freeze
|
||||
COMPARISON_EQUAL = [:comparison, "=="].freeze
|
||||
COMPARISON_GREATER_THAN = [:comparison, ">"].freeze
|
||||
COMPARISON_GREATER_THAN_OR_EQUAL = [:comparison, ">="].freeze
|
||||
COMPARISON_LESS_THAN = [:comparison, "<"].freeze
|
||||
COMPARISON_LESS_THAN_OR_EQUAL = [:comparison, "<="].freeze
|
||||
COMPARISON_NOT_EQUAL_ALT = [:comparison, "<>"].freeze
|
||||
DASH = [:dash, "-"].freeze
|
||||
DOT = [:dot, "."].freeze
|
||||
DOTDOT = [:dotdot, ".."].freeze
|
||||
DOT_ORD = ".".ord
|
||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||
EOS = [:end_of_string].freeze
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
OPEN_ROUND = [:open_round, "("].freeze
|
||||
OPEN_SQUARE = [:open_square, "["].freeze
|
||||
PIPE = [:pipe, "|"].freeze
|
||||
QUESTION = [:question, "?"].freeze
|
||||
RUBY_WHITESPACE = [" ", "\t", "\r", "\n", "\f"].freeze
|
||||
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||
WHITESPACE_OR_NOTHING = /\s*/
|
||||
|
||||
SINGLE_COMPARISON_TOKENS = [].tap do |table|
|
||||
table["<".ord] = COMPARISON_LESS_THAN
|
||||
table[">".ord] = COMPARISON_GREATER_THAN
|
||||
table.freeze
|
||||
end
|
||||
|
||||
TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
|
||||
table["=".ord] = [].tap do |sub_table|
|
||||
sub_table["=".ord] = COMPARISON_EQUAL
|
||||
sub_table.freeze
|
||||
end
|
||||
table["!".ord] = [].tap do |sub_table|
|
||||
sub_table["=".ord] = COMPARISION_NOT_EQUAL
|
||||
sub_table.freeze
|
||||
end
|
||||
table.freeze
|
||||
end
|
||||
|
||||
COMPARISON_JUMP_TABLE = [].tap do |table|
|
||||
table["<".ord] = [].tap do |sub_table|
|
||||
sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
|
||||
sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
|
||||
sub_table.freeze
|
||||
end
|
||||
table[">".ord] = [].tap do |sub_table|
|
||||
sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
|
||||
sub_table.freeze
|
||||
end
|
||||
table.freeze
|
||||
end
|
||||
|
||||
NEXT_MATCHER_JUMP_TABLE = [].tap do |table|
|
||||
"a".upto("z") do |c|
|
||||
table[c.ord] = [:id, IDENTIFIER].freeze
|
||||
table[c.upcase.ord] = [:id, IDENTIFIER].freeze
|
||||
end
|
||||
table["_".ord] = [:id, IDENTIFIER].freeze
|
||||
|
||||
"0".upto("9") do |c|
|
||||
table[c.ord] = [:number, NUMBER_LITERAL].freeze
|
||||
end
|
||||
table["-".ord] = [:number, NUMBER_LITERAL].freeze
|
||||
|
||||
table["'".ord] = [:string, SINGLE_STRING_LITERAL].freeze
|
||||
table["\"".ord] = [:string, DOUBLE_STRING_LITERAL].freeze
|
||||
table.freeze
|
||||
end
|
||||
|
||||
SPECIAL_TABLE = [].tap do |table|
|
||||
table["|".ord] = PIPE
|
||||
table[".".ord] = DOT
|
||||
table[":".ord] = COLON
|
||||
table[",".ord] = COMMA
|
||||
table["[".ord] = OPEN_SQUARE
|
||||
table["]".ord] = CLOSE_SQUARE
|
||||
table["(".ord] = OPEN_ROUND
|
||||
table[")".ord] = CLOSE_ROUND
|
||||
table["?".ord] = QUESTION
|
||||
table["-".ord] = DASH
|
||||
end
|
||||
|
||||
NUMBER_TABLE = [].tap do |table|
|
||||
"0".upto("9") do |c|
|
||||
table[c.ord] = true
|
||||
end
|
||||
table.freeze
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/BlockNesting
|
||||
class << self
|
||||
def tokenize(ss)
|
||||
output = []
|
||||
|
||||
until ss.eos?
|
||||
ss.skip(WHITESPACE_OR_NOTHING)
|
||||
|
||||
break if ss.eos?
|
||||
|
||||
start_pos = ss.pos
|
||||
peeked = ss.peek_byte
|
||||
|
||||
if (special = SPECIAL_TABLE[peeked])
|
||||
ss.scan_byte
|
||||
# Special case for ".."
|
||||
if special == DOT && ss.peek_byte == DOT_ORD
|
||||
ss.scan_byte
|
||||
output << DOTDOT
|
||||
elsif special == DASH
|
||||
# Special case for negative numbers
|
||||
if (peeked_byte = ss.peek_byte) && NUMBER_TABLE[peeked_byte]
|
||||
ss.pos -= 1
|
||||
output << [:number, ss.scan(NUMBER_LITERAL)]
|
||||
else
|
||||
output << special
|
||||
end
|
||||
else
|
||||
output << special
|
||||
end
|
||||
elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
|
||||
ss.scan_byte
|
||||
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
|
||||
output << found
|
||||
ss.scan_byte
|
||||
else
|
||||
raise_syntax_error(start_pos, ss)
|
||||
end
|
||||
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
|
||||
ss.scan_byte
|
||||
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
|
||||
output << found
|
||||
ss.scan_byte
|
||||
else
|
||||
output << SINGLE_COMPARISON_TOKENS[peeked]
|
||||
end
|
||||
else
|
||||
type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
|
||||
|
||||
if type && (t = ss.scan(pattern))
|
||||
# Special case for "contains"
|
||||
output << if type == :id && t == "contains" && output.last&.first != :dot
|
||||
COMPARISON_CONTAINS
|
||||
else
|
||||
[type, t]
|
||||
end
|
||||
else
|
||||
raise_syntax_error(start_pos, ss)
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/BlockNesting
|
||||
output << EOS
|
||||
rescue ::ArgumentError => e
|
||||
if e.message == "invalid byte sequence in #{ss.string.encoding}"
|
||||
raise SyntaxError, "Invalid byte sequence in #{ss.string.encoding}"
|
||||
else
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
def raise_syntax_error(start_pos, ss)
|
||||
ss.pos = start_pos
|
||||
# the character could be a UTF-8 character, use getch to get all the bytes
|
||||
raise SyntaxError, "Unexpected character #{ss.getch}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
errors:
|
||||
syntax:
|
||||
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
|
||||
block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{tag} %}"
|
||||
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
|
||||
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
|
||||
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
|
||||
case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
|
||||
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
|
||||
cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
|
||||
doc_invalid_nested: "Syntax Error in 'doc' - Nested doc tags are not allowed"
|
||||
for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
|
||||
for_invalid_in: "For loops require an 'in' clause"
|
||||
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
|
||||
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
||||
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
|
||||
inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character"
|
||||
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
||||
invalid_template_encoding: "Invalid template encoding"
|
||||
render: "Syntax error in tag 'render' - Template name must be a quoted string"
|
||||
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
||||
tag_never_closed: "'%{block_name}' tag was never closed"
|
||||
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||
unexpected_else: "%{block_name} tag does not expect 'else' tag"
|
||||
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
|
||||
unknown_tag: "Unknown tag '%{tag}'"
|
||||
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
|
||||
argument:
|
||||
include: "Argument error in tag 'include' - Illegal template name"
|
||||
disabled:
|
||||
tag: "usage is not allowed in this context"
|
||||
@@ -1,77 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class ParseContext
|
||||
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
||||
attr_reader :partial, :warnings, :error_mode, :environment
|
||||
|
||||
def initialize(options = Const::EMPTY_HASH)
|
||||
@environment = options.fetch(:environment, Environment.default)
|
||||
@template_options = options ? options.dup : {}
|
||||
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@warnings = []
|
||||
|
||||
# constructing new StringScanner in Lexer, Tokenizer, etc is expensive
|
||||
# This StringScanner will be shared by all of them
|
||||
@string_scanner = StringScanner.new("")
|
||||
|
||||
@expression_cache = if options[:expression_cache].nil?
|
||||
{}
|
||||
elsif options[:expression_cache].respond_to?(:[]) && options[:expression_cache].respond_to?(:[]=)
|
||||
options[:expression_cache]
|
||||
elsif options[:expression_cache]
|
||||
{}
|
||||
end
|
||||
|
||||
self.depth = 0
|
||||
self.partial = false
|
||||
end
|
||||
|
||||
def [](option_key)
|
||||
@options[option_key]
|
||||
end
|
||||
|
||||
def new_block_body
|
||||
Liquid::BlockBody.new
|
||||
end
|
||||
|
||||
def new_parser(input)
|
||||
@string_scanner.string = input
|
||||
Parser.new(@string_scanner)
|
||||
end
|
||||
|
||||
def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
|
||||
Tokenizer.new(
|
||||
source: source,
|
||||
string_scanner: @string_scanner,
|
||||
line_number: start_line_number,
|
||||
for_liquid_tag: for_liquid_tag,
|
||||
)
|
||||
end
|
||||
|
||||
def parse_expression(markup)
|
||||
Expression.parse(markup, @string_scanner, @expression_cache)
|
||||
end
|
||||
|
||||
def partial=(value)
|
||||
@partial = value
|
||||
@options = value ? partial_options : @template_options
|
||||
|
||||
@error_mode = @options[:error_mode] || @environment.error_mode
|
||||
end
|
||||
|
||||
def partial_options
|
||||
@partial_options ||= begin
|
||||
dont_pass = @template_options[:include_options_blacklist]
|
||||
if dont_pass == true
|
||||
{ locale: locale }
|
||||
elsif dont_pass.is_a?(Array)
|
||||
@template_options.reject { |k, _v| dont_pass.include?(k) }
|
||||
else
|
||||
@template_options
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,42 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class ParseTreeVisitor
|
||||
def self.for(node, callbacks = Hash.new(proc {}))
|
||||
if defined?(node.class::ParseTreeVisitor)
|
||||
node.class::ParseTreeVisitor
|
||||
else
|
||||
self
|
||||
end.new(node, callbacks)
|
||||
end
|
||||
|
||||
def initialize(node, callbacks)
|
||||
@node = node
|
||||
@callbacks = callbacks
|
||||
end
|
||||
|
||||
def add_callback_for(*classes, &block)
|
||||
callback = block
|
||||
callback = ->(node, _) { yield node } if block.arity.abs == 1
|
||||
callback = ->(_, _) { yield } if block.arity.zero?
|
||||
classes.each { |klass| @callbacks[klass] = callback }
|
||||
self
|
||||
end
|
||||
|
||||
def visit(context = nil)
|
||||
children.map do |node|
|
||||
item, new_context = @callbacks[node.class].call(node, context)
|
||||
[
|
||||
item,
|
||||
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context),
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def children
|
||||
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : Const::EMPTY_ARRAY
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,102 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Parser
|
||||
def initialize(input)
|
||||
ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
|
||||
@tokens = Lexer.tokenize(ss)
|
||||
@p = 0 # pointer to current location
|
||||
end
|
||||
|
||||
def jump(point)
|
||||
@p = point
|
||||
end
|
||||
|
||||
def consume(type = nil)
|
||||
token = @tokens[@p]
|
||||
if type && token[0] != type
|
||||
raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}"
|
||||
end
|
||||
@p += 1
|
||||
token[1]
|
||||
end
|
||||
|
||||
# Only consumes the token if it matches the type
|
||||
# Returns the token's contents if it was consumed
|
||||
# or false otherwise.
|
||||
def consume?(type)
|
||||
token = @tokens[@p]
|
||||
return false unless token && token[0] == type
|
||||
@p += 1
|
||||
token[1]
|
||||
end
|
||||
|
||||
# Like consume? Except for an :id token of a certain name
|
||||
def id?(str)
|
||||
token = @tokens[@p]
|
||||
return false unless token && token[0] == :id
|
||||
return false unless token[1] == str
|
||||
@p += 1
|
||||
token[1]
|
||||
end
|
||||
|
||||
def look(type, ahead = 0)
|
||||
tok = @tokens[@p + ahead]
|
||||
return false unless tok
|
||||
tok[0] == type
|
||||
end
|
||||
|
||||
def expression
|
||||
token = @tokens[@p]
|
||||
case token[0]
|
||||
when :id
|
||||
str = consume
|
||||
str << variable_lookups
|
||||
when :open_square
|
||||
str = consume.dup
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
str << variable_lookups
|
||||
when :string, :number
|
||||
consume
|
||||
when :open_round
|
||||
consume
|
||||
first = expression
|
||||
consume(:dotdot)
|
||||
last = expression
|
||||
consume(:close_round)
|
||||
"(#{first}..#{last})"
|
||||
else
|
||||
raise SyntaxError, "#{token} is not a valid expression"
|
||||
end
|
||||
end
|
||||
|
||||
def argument
|
||||
str = +""
|
||||
# might be a keyword argument (identifier: expression)
|
||||
if look(:id) && look(:colon, 1)
|
||||
str << consume << consume << ' '
|
||||
end
|
||||
|
||||
str << expression
|
||||
str
|
||||
end
|
||||
|
||||
def variable_lookups
|
||||
str = +""
|
||||
loop do
|
||||
if look(:open_square)
|
||||
str << consume
|
||||
str << expression
|
||||
str << consume(:close_square)
|
||||
elsif look(:dot)
|
||||
str << consume
|
||||
str << consume(:id)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
str
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,45 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module ParserSwitching
|
||||
def strict_parse_with_error_mode_fallback(markup)
|
||||
strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
case parse_context.error_mode
|
||||
when :strict
|
||||
raise
|
||||
when :warn
|
||||
parse_context.warnings << e
|
||||
end
|
||||
lax_parse(markup)
|
||||
end
|
||||
|
||||
def parse_with_selected_parser(markup)
|
||||
case parse_context.error_mode
|
||||
when :strict then strict_parse_with_error_context(markup)
|
||||
when :lax then lax_parse(markup)
|
||||
when :warn
|
||||
begin
|
||||
strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
parse_context.warnings << e
|
||||
lax_parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def strict_parse_with_error_context(markup)
|
||||
strict_parse(markup)
|
||||
rescue SyntaxError => e
|
||||
e.line_number = line_number
|
||||
e.markup_context = markup_context(markup)
|
||||
raise e
|
||||
end
|
||||
|
||||
def markup_context(markup)
|
||||
"in \"#{markup.strip}\""
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,33 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class PartialCache
|
||||
def self.load(template_name, context:, parse_context:)
|
||||
cached_partials = context.registers[:cached_partials]
|
||||
cache_key = "#{template_name}:#{parse_context.error_mode}"
|
||||
cached = cached_partials[cache_key]
|
||||
return cached if cached
|
||||
|
||||
file_system = context.registers[:file_system]
|
||||
source = file_system.read_template_file(template_name)
|
||||
|
||||
parse_context.partial = true
|
||||
|
||||
template_factory = context.registers[:template_factory]
|
||||
template = template_factory.for(template_name)
|
||||
|
||||
begin
|
||||
partial = template.parse(source, parse_context)
|
||||
rescue Liquid::Error => e
|
||||
e.template_name = template&.name || template_name
|
||||
raise e
|
||||
end
|
||||
|
||||
partial.name ||= template_name
|
||||
|
||||
cached_partials[cache_key] = partial
|
||||
ensure
|
||||
parse_context.partial = false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,139 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'liquid/profiler/hooks'
|
||||
|
||||
module Liquid
|
||||
# Profiler enables support for profiling template rendering to help track down performance issues.
|
||||
#
|
||||
# To enable profiling, first require 'liquid/profiler'.
|
||||
# Then, to profile a parse/render cycle, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>.
|
||||
# After <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
|
||||
# class via the <tt>Liquid::Template#profiler</tt> method.
|
||||
#
|
||||
# template = Liquid::Template.parse(template_content, profile: true)
|
||||
# output = template.render
|
||||
# profile = template.profiler
|
||||
#
|
||||
# This object contains all profiling information, containing information on what tags were rendered,
|
||||
# where in the templates these tags live, and how long each tag took to render.
|
||||
#
|
||||
# This is a tree structure that is Enumerable all the way down, and keeps track of tags and rendering times
|
||||
# inside of <tt>{% include %}</tt> tags.
|
||||
#
|
||||
# profile.each do |node|
|
||||
# # Access to the node itself
|
||||
# node.code
|
||||
#
|
||||
# # Which template and line number of this node.
|
||||
# # The top-level template name is `nil` by default, but can be set in the Liquid::Context before rendering.
|
||||
# node.partial
|
||||
# node.line_number
|
||||
#
|
||||
# # Render time in seconds of this node
|
||||
# node.render_time
|
||||
#
|
||||
# # If the template used {% include %}, this node will also have children.
|
||||
# node.children.each do |child2|
|
||||
# # ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Profiler also exposes the total time of the template's render in <tt>Liquid::Profiler#total_render_time</tt>.
|
||||
#
|
||||
# All render times are in seconds. There is a small performance hit when profiling is enabled.
|
||||
#
|
||||
class Profiler
|
||||
include Enumerable
|
||||
|
||||
class Timing
|
||||
attr_reader :code, :template_name, :line_number, :children
|
||||
attr_accessor :total_time
|
||||
alias_method :render_time, :total_time
|
||||
alias_method :partial, :template_name
|
||||
|
||||
def initialize(code: nil, template_name: nil, line_number: nil)
|
||||
@code = code
|
||||
@template_name = template_name
|
||||
@line_number = line_number
|
||||
@children = []
|
||||
end
|
||||
|
||||
def self_time
|
||||
@self_time ||= begin
|
||||
total_children_time = 0.0
|
||||
@children.each do |child|
|
||||
total_children_time += child.total_time
|
||||
end
|
||||
@total_time - total_children_time
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :total_time
|
||||
alias_method :total_render_time, :total_time
|
||||
|
||||
def initialize
|
||||
@root_children = []
|
||||
@current_children = nil
|
||||
@total_time = 0.0
|
||||
end
|
||||
|
||||
def profile(template_name, &block)
|
||||
# nested renders are done from a tag that already has a timing node
|
||||
return yield if @current_children
|
||||
|
||||
root_children = @root_children
|
||||
render_idx = root_children.length
|
||||
begin
|
||||
@current_children = root_children
|
||||
profile_node(template_name, &block)
|
||||
ensure
|
||||
@current_children = nil
|
||||
if (timing = root_children[render_idx])
|
||||
@total_time += timing.total_time
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def children
|
||||
children = @root_children
|
||||
if children.length == 1
|
||||
children.first.children
|
||||
else
|
||||
children
|
||||
end
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
children.each(&block)
|
||||
end
|
||||
|
||||
def [](idx)
|
||||
children[idx]
|
||||
end
|
||||
|
||||
def length
|
||||
children.length
|
||||
end
|
||||
|
||||
def profile_node(template_name, code: nil, line_number: nil)
|
||||
timing = Timing.new(code: code, template_name: template_name, line_number: line_number)
|
||||
parent_children = @current_children
|
||||
start_time = monotonic_time
|
||||
begin
|
||||
@current_children = timing.children
|
||||
yield
|
||||
ensure
|
||||
@current_children = parent_children
|
||||
timing.total_time = monotonic_time - start_time
|
||||
parent_children << timing
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def monotonic_time
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,35 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module BlockBodyProfilingHook
|
||||
def render_node(context, output, node)
|
||||
if (profiler = context.profiler)
|
||||
profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do
|
||||
super
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
BlockBody.prepend(BlockBodyProfilingHook)
|
||||
|
||||
module DocumentProfilingHook
|
||||
def render_to_output_buffer(context, output)
|
||||
return super unless context.profiler
|
||||
context.profiler.profile(context.template_name) { super }
|
||||
end
|
||||
end
|
||||
Document.prepend(DocumentProfilingHook)
|
||||
|
||||
module ContextProfilingHook
|
||||
attr_accessor :profiler
|
||||
|
||||
def new_isolated_subcontext
|
||||
new_context = super
|
||||
new_context.profiler = profiler
|
||||
new_context
|
||||
end
|
||||
end
|
||||
Context.prepend(ContextProfilingHook)
|
||||
end
|
||||
@@ -1,57 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class RangeLookup
|
||||
def self.parse(start_markup, end_markup, string_scanner, cache = nil)
|
||||
start_obj = Expression.parse(start_markup, string_scanner, cache)
|
||||
end_obj = Expression.parse(end_markup, string_scanner, cache)
|
||||
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
||||
new(start_obj, end_obj)
|
||||
else
|
||||
begin
|
||||
start_obj.to_i..end_obj.to_i
|
||||
rescue NoMethodError
|
||||
invalid_expr = start_markup unless start_obj.respond_to?(:to_i)
|
||||
invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
|
||||
if invalid_expr
|
||||
raise Liquid::SyntaxError, "Invalid expression type '#{invalid_expr}' in range expression"
|
||||
end
|
||||
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :start_obj, :end_obj
|
||||
|
||||
def initialize(start_obj, end_obj)
|
||||
@start_obj = start_obj
|
||||
@end_obj = end_obj
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
start_int = to_integer(context.evaluate(@start_obj))
|
||||
end_int = to_integer(context.evaluate(@end_obj))
|
||||
start_int..end_int
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_integer(input)
|
||||
case input
|
||||
when Integer
|
||||
input
|
||||
when NilClass, String
|
||||
input.to_i
|
||||
else
|
||||
Utils.to_integer(input)
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.start_obj, @node.end_obj]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Registers
|
||||
attr_reader :static
|
||||
|
||||
def initialize(registers = {})
|
||||
@static = registers.is_a?(Registers) ? registers.static : registers
|
||||
@changes = {}
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
@changes[key] = value
|
||||
end
|
||||
|
||||
def [](key)
|
||||
if @changes.key?(key)
|
||||
@changes[key]
|
||||
else
|
||||
@static[key]
|
||||
end
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
@changes.delete(key)
|
||||
end
|
||||
|
||||
UNDEFINED = Object.new
|
||||
|
||||
def fetch(key, default = UNDEFINED, &block)
|
||||
if @changes.key?(key)
|
||||
@changes.fetch(key)
|
||||
elsif default != UNDEFINED
|
||||
if block_given?
|
||||
@static.fetch(key, &block)
|
||||
else
|
||||
@static.fetch(key, default)
|
||||
end
|
||||
else
|
||||
@static.fetch(key, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
@changes.key?(key) || @static.key?(key)
|
||||
end
|
||||
end
|
||||
|
||||
# Alias for backwards compatibility
|
||||
StaticRegisters = Registers
|
||||
end
|
||||
@@ -1,62 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class ResourceLimits
|
||||
attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
|
||||
attr_reader :render_score, :assign_score
|
||||
|
||||
def initialize(limits)
|
||||
@render_length_limit = limits[:render_length_limit]
|
||||
@render_score_limit = limits[:render_score_limit]
|
||||
@assign_score_limit = limits[:assign_score_limit]
|
||||
reset
|
||||
end
|
||||
|
||||
def increment_render_score(amount)
|
||||
@render_score += amount
|
||||
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
|
||||
end
|
||||
|
||||
def increment_assign_score(amount)
|
||||
@assign_score += amount
|
||||
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
|
||||
end
|
||||
|
||||
# update either render_length or assign_score based on whether or not the writes are captured
|
||||
def increment_write_score(output)
|
||||
if (last_captured = @last_capture_length)
|
||||
captured = output.bytesize
|
||||
increment = captured - last_captured
|
||||
@last_capture_length = captured
|
||||
increment_assign_score(increment)
|
||||
elsif @render_length_limit && output.bytesize > @render_length_limit
|
||||
raise_limits_reached
|
||||
end
|
||||
end
|
||||
|
||||
def raise_limits_reached
|
||||
@reached_limit = true
|
||||
raise MemoryError, "Memory limits exceeded"
|
||||
end
|
||||
|
||||
def reached?
|
||||
@reached_limit
|
||||
end
|
||||
|
||||
def reset
|
||||
@reached_limit = false
|
||||
@last_capture_length = nil
|
||||
@render_score = @assign_score = 0
|
||||
end
|
||||
|
||||
def with_capture
|
||||
old_capture_length = @last_capture_length
|
||||
begin
|
||||
@last_capture_length = 0
|
||||
yield
|
||||
ensure
|
||||
@last_capture_length = old_capture_length
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
# StrainerTemplate is the computed class for the filters system.
|
||||
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
|
||||
#
|
||||
# The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,
|
||||
# Context#add_filters or Template.register_filter
|
||||
class StrainerTemplate
|
||||
def initialize(context)
|
||||
@context = context
|
||||
end
|
||||
|
||||
class << self
|
||||
def add_filter(filter)
|
||||
return if include?(filter)
|
||||
|
||||
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
|
||||
if invokable_non_public_methods.any?
|
||||
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
|
||||
end
|
||||
|
||||
include(filter)
|
||||
|
||||
filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
||||
end
|
||||
|
||||
def invokable?(method)
|
||||
filter_methods.include?(method.to_s)
|
||||
end
|
||||
|
||||
def inherited(subclass)
|
||||
super
|
||||
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
|
||||
end
|
||||
|
||||
def filter_method_names
|
||||
filter_methods.map(&:to_s).to_a
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_methods
|
||||
@filter_methods ||= Set.new
|
||||
end
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
if self.class.invokable?(method)
|
||||
send(method, *args)
|
||||
elsif @context.strict_filters
|
||||
raise Liquid::UndefinedFilter, "undefined filter #{method}"
|
||||
else
|
||||
args.first
|
||||
end
|
||||
rescue ::ArgumentError => e
|
||||
raise Liquid::ArgumentError, e.message, e.backtrace
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,121 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type object
|
||||
# @liquid_name tablerowloop
|
||||
# @liquid_summary
|
||||
# Information about a parent [`tablerow` loop](/docs/api/liquid/tags/tablerow).
|
||||
class TablerowloopDrop < Drop
|
||||
def initialize(length, cols)
|
||||
@length = length
|
||||
@row = 1
|
||||
@col = 1
|
||||
@cols = cols
|
||||
@index = 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The total number of iterations in the loop.
|
||||
# @liquid_return [number]
|
||||
attr_reader :length
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current column.
|
||||
# @liquid_return [number]
|
||||
attr_reader :col
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of current row.
|
||||
# @liquid_return [number]
|
||||
attr_reader :row
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index
|
||||
@index + 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration.
|
||||
# @liquid_return [number]
|
||||
def index0
|
||||
@index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current column.
|
||||
# @liquid_return [number]
|
||||
def col0
|
||||
@col - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 1-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex
|
||||
@length - @index
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# The 0-based index of the current iteration, in reverse order.
|
||||
# @liquid_return [number]
|
||||
def rindex0
|
||||
@length - @index - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the first. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def first
|
||||
@index == 0
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current iteration is the last. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def last
|
||||
@index == @length - 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current column is the first in the row. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def col_first
|
||||
@col == 1
|
||||
end
|
||||
|
||||
# @liquid_public_docs
|
||||
# @liquid_summary
|
||||
# Returns `true` if the current column is the last in the row. Returns `false` if not.
|
||||
# @liquid_return [boolean]
|
||||
def col_last
|
||||
@col == @cols
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def increment!
|
||||
@index += 1
|
||||
|
||||
if @col == @cols
|
||||
@col = 1
|
||||
@row += 1
|
||||
else
|
||||
@col += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,75 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'liquid/tag/disabler'
|
||||
require 'liquid/tag/disableable'
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
attr_reader :nodelist, :tag_name, :line_number, :parse_context
|
||||
alias_method :options, :parse_context
|
||||
include ParserSwitching
|
||||
|
||||
class << self
|
||||
def parse(tag_name, markup, tokenizer, parse_context)
|
||||
tag = new(tag_name, markup, parse_context)
|
||||
tag.parse(tokenizer)
|
||||
tag
|
||||
end
|
||||
|
||||
def disable_tags(*tag_names)
|
||||
tag_names += disabled_tags
|
||||
define_singleton_method(:disabled_tags) { tag_names }
|
||||
prepend(Disabler)
|
||||
end
|
||||
|
||||
private :new
|
||||
|
||||
protected
|
||||
|
||||
def disabled_tags
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
@tag_name = tag_name
|
||||
@markup = markup
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
end
|
||||
|
||||
def parse(_tokens)
|
||||
end
|
||||
|
||||
def raw
|
||||
"#{@tag_name} #{@markup}"
|
||||
end
|
||||
|
||||
def name
|
||||
self.class.name.downcase
|
||||
end
|
||||
|
||||
def render(_context)
|
||||
''
|
||||
end
|
||||
|
||||
# For backwards compatibility with custom tags. In a future release, the semantics
|
||||
# of the `render_to_output_buffer` method will become the default and the `render`
|
||||
# method will be removed.
|
||||
def render_to_output_buffer(context, output)
|
||||
render_result = render(context)
|
||||
output << render_result if render_result
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_expression(markup)
|
||||
parse_context.parse_expression(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,22 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
module Disableable
|
||||
def render_to_output_buffer(context, output)
|
||||
if context.tag_disabled?(tag_name)
|
||||
output << disabled_error(context)
|
||||
return
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
def disabled_error(context)
|
||||
# raise then rescue the exception so that the Context#exception_renderer can re-raise it
|
||||
raise DisabledError, "#{tag_name} #{parse_context[:locale].t('errors.disabled.tag')}"
|
||||
rescue DisabledError => exc
|
||||
context.handle_error(exc, line_number)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
module Disabler
|
||||
def render_to_output_buffer(context, output)
|
||||
context.with_disabled_tags(self.class.disabled_tags) do
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,49 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "tags/table_row"
|
||||
require_relative "tags/echo"
|
||||
require_relative "tags/if"
|
||||
require_relative "tags/break"
|
||||
require_relative "tags/inline_comment"
|
||||
require_relative "tags/for"
|
||||
require_relative "tags/assign"
|
||||
require_relative "tags/ifchanged"
|
||||
require_relative "tags/case"
|
||||
require_relative "tags/include"
|
||||
require_relative "tags/continue"
|
||||
require_relative "tags/capture"
|
||||
require_relative "tags/decrement"
|
||||
require_relative "tags/unless"
|
||||
require_relative "tags/increment"
|
||||
require_relative "tags/comment"
|
||||
require_relative "tags/raw"
|
||||
require_relative "tags/render"
|
||||
require_relative "tags/cycle"
|
||||
require_relative "tags/doc"
|
||||
|
||||
module Liquid
|
||||
module Tags
|
||||
STANDARD_TAGS = {
|
||||
'cycle' => Cycle,
|
||||
'render' => Render,
|
||||
'raw' => Raw,
|
||||
'comment' => Comment,
|
||||
'increment' => Increment,
|
||||
'unless' => Unless,
|
||||
'decrement' => Decrement,
|
||||
'capture' => Capture,
|
||||
'continue' => Continue,
|
||||
'include' => Include,
|
||||
'case' => Case,
|
||||
'ifchanged' => Ifchanged,
|
||||
'assign' => Assign,
|
||||
'for' => For,
|
||||
'#' => InlineComment,
|
||||
'break' => Break,
|
||||
'if' => If,
|
||||
'echo' => Echo,
|
||||
'tablerow' => TableRow,
|
||||
'doc' => Doc,
|
||||
}.freeze
|
||||
end
|
||||
end
|
||||
@@ -1,75 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name assign
|
||||
# @liquid_summary
|
||||
# Creates a new variable.
|
||||
# @liquid_description
|
||||
# You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property.
|
||||
# @liquid_syntax
|
||||
# {% assign variable_name = value %}
|
||||
# @liquid_syntax_keyword variable_name The name of the variable being created.
|
||||
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
||||
class Assign < Tag
|
||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
||||
|
||||
# @api private
|
||||
def self.raise_syntax_error(parse_context)
|
||||
raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
|
||||
end
|
||||
|
||||
attr_reader :to, :from
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@to = Regexp.last_match(1)
|
||||
@from = Variable.new(Regexp.last_match(2), parse_context)
|
||||
else
|
||||
self.class.raise_syntax_error(parse_context)
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
val = @from.render(context)
|
||||
context.scopes.last[@to] = val
|
||||
context.resource_limits.increment_assign_score(assign_score_of(val))
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_score_of(val)
|
||||
if val.instance_of?(String)
|
||||
val.bytesize
|
||||
elsif val.instance_of?(Array)
|
||||
sum = 1
|
||||
# Uses #each to avoid extra allocations.
|
||||
val.each { |child| sum += assign_score_of(child) }
|
||||
sum
|
||||
elsif val.instance_of?(Hash)
|
||||
sum = 1
|
||||
val.each do |key, entry_value|
|
||||
sum += assign_score_of(key)
|
||||
sum += assign_score_of(entry_value)
|
||||
end
|
||||
sum
|
||||
else
|
||||
1
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.from]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,29 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Break tag to be used to break out of a for loop.
|
||||
#
|
||||
# == Basic Usage:
|
||||
# {% for item in collection %}
|
||||
# {% if item.condition %}
|
||||
# {% break %}
|
||||
# {% endif %}
|
||||
# {% endfor %}
|
||||
#
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name break
|
||||
# @liquid_summary
|
||||
# Stops a [`for` loop](/docs/api/liquid/tags/for) from iterating.
|
||||
# @liquid_syntax
|
||||
# {% break %}
|
||||
class Break < Tag
|
||||
INTERRUPT = BreakInterrupt.new.freeze
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.push_interrupt(INTERRUPT)
|
||||
output
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,42 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name capture
|
||||
# @liquid_summary
|
||||
# Creates a new variable with a string value.
|
||||
# @liquid_description
|
||||
# You can create complex strings with Liquid logic and variables.
|
||||
# @liquid_syntax
|
||||
# {% capture variable %}
|
||||
# value
|
||||
# {% endcapture %}
|
||||
# @liquid_syntax_keyword variable The name of the variable being created.
|
||||
# @liquid_syntax_keyword value The value you want to assign to the variable.
|
||||
class Capture < Block
|
||||
Syntax = /(#{VariableSignature}+)/o
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@to = Regexp.last_match(1)
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.capture")
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.resource_limits.with_capture do
|
||||
capture_output = render(context)
|
||||
context.scopes.last[@to] = capture_output
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,126 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category conditional
|
||||
# @liquid_name case
|
||||
# @liquid_summary
|
||||
# Renders a specific expression depending on the value of a specific variable.
|
||||
# @liquid_syntax
|
||||
# {% case variable %}
|
||||
# {% when first_value %}
|
||||
# first_expression
|
||||
# {% when second_value %}
|
||||
# second_expression
|
||||
# {% else %}
|
||||
# third_expression
|
||||
# {% endcase %}
|
||||
# @liquid_syntax_keyword variable The name of the variable you want to base your case statement on.
|
||||
# @liquid_syntax_keyword first_value A specific value to check for.
|
||||
# @liquid_syntax_keyword second_value A specific value to check for.
|
||||
# @liquid_syntax_keyword first_expression An expression to be rendered when the variable's value matches `first_value`.
|
||||
# @liquid_syntax_keyword second_expression An expression to be rendered when the variable's value matches `second_value`.
|
||||
# @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
|
||||
class Case < Block
|
||||
Syntax = /(#{QuotedFragment})/o
|
||||
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
||||
|
||||
attr_reader :blocks, :left
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@blocks = []
|
||||
|
||||
if markup =~ Syntax
|
||||
@left = parse_expression(Regexp.last_match(1))
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case")
|
||||
end
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
body = case_body = new_body
|
||||
body = @blocks.last.attachment while parse_body(body, tokens)
|
||||
@blocks.reverse_each do |condition|
|
||||
body = condition.attachment
|
||||
unless body.frozen?
|
||||
body.remove_blank_strings if blank?
|
||||
body.freeze
|
||||
end
|
||||
end
|
||||
case_body.freeze
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@blocks.map(&:attachment)
|
||||
end
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
case tag
|
||||
when 'when'
|
||||
record_when_condition(markup)
|
||||
when 'else'
|
||||
record_else_condition(markup)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
execute_else_block = true
|
||||
|
||||
@blocks.each do |block|
|
||||
if block.else?
|
||||
block.attachment.render_to_output_buffer(context, output) if execute_else_block
|
||||
next
|
||||
end
|
||||
|
||||
result = Liquid::Utils.to_liquid_value(
|
||||
block.evaluate(context),
|
||||
)
|
||||
|
||||
if result
|
||||
execute_else_block = false
|
||||
block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def record_when_condition(markup)
|
||||
body = new_body
|
||||
|
||||
while markup
|
||||
unless markup =~ WhenSyntax
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
|
||||
end
|
||||
|
||||
markup = Regexp.last_match(2)
|
||||
|
||||
block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1)))
|
||||
block.attach(body)
|
||||
@blocks << block
|
||||
end
|
||||
end
|
||||
|
||||
def record_else_condition(markup)
|
||||
unless markup.strip.empty?
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
|
||||
end
|
||||
|
||||
block = ElseCondition.new
|
||||
block.attach(new_body)
|
||||
@blocks << block
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.left] + @node.blocks
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,88 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category syntax
|
||||
# @liquid_name comment
|
||||
# @liquid_summary
|
||||
# Prevents an expression from being rendered or output.
|
||||
# @liquid_description
|
||||
# Any text inside `comment` tags won't be output, and any Liquid code will be parsed, but not executed.
|
||||
# @liquid_syntax
|
||||
# {% comment %}
|
||||
# content
|
||||
# {% endcomment %}
|
||||
# @liquid_syntax_keyword content The content of the comment.
|
||||
class Comment < Block
|
||||
def render_to_output_buffer(_context, output)
|
||||
output
|
||||
end
|
||||
|
||||
def unknown_tag(_tag, _markup, _tokens)
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_body(body, tokenizer)
|
||||
if parse_context.depth >= MAX_DEPTH
|
||||
raise StackLevelError, "Nesting too deep"
|
||||
end
|
||||
|
||||
parse_context.depth += 1
|
||||
comment_tag_depth = 1
|
||||
|
||||
begin
|
||||
# Consume tokens without creating child nodes.
|
||||
# The children tag doesn't require to be a valid Liquid except the comment and raw tag.
|
||||
# The child comment and raw tag must be closed.
|
||||
while (token = tokenizer.send(:shift))
|
||||
tag_name = if tokenizer.for_liquid_tag
|
||||
next if token.empty? || token.match?(BlockBody::WhitespaceOrNothing)
|
||||
|
||||
tag_name_match = BlockBody::LiquidTagToken.match(token)
|
||||
|
||||
next if tag_name_match.nil?
|
||||
|
||||
tag_name_match[1]
|
||||
else
|
||||
token =~ BlockBody::FullToken
|
||||
Regexp.last_match(2)
|
||||
end
|
||||
|
||||
case tag_name
|
||||
when "raw"
|
||||
parse_raw_tag_body(tokenizer)
|
||||
when "comment"
|
||||
comment_tag_depth += 1
|
||||
when "endcomment"
|
||||
comment_tag_depth -= 1
|
||||
end
|
||||
|
||||
if comment_tag_depth.zero?
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) unless tokenizer.for_liquid_tag
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
raise_tag_never_closed(block_name)
|
||||
ensure
|
||||
parse_context.depth -= 1
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def parse_raw_tag_body(tokenizer)
|
||||
while (token = tokenizer.send(:shift))
|
||||
return if token =~ BlockBody::FullTokenPossiblyInvalid && "endraw" == Regexp.last_match(2)
|
||||
end
|
||||
|
||||
raise_tag_never_closed("raw")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name continue
|
||||
# @liquid_summary
|
||||
# Causes a [`for` loop](/docs/api/liquid/tags/for) to skip to the next iteration.
|
||||
# @liquid_syntax
|
||||
# {% continue %}
|
||||
class Continue < Tag
|
||||
INTERRUPT = ContinueInterrupt.new.freeze
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.push_interrupt(INTERRUPT)
|
||||
output
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,87 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category iteration
|
||||
# @liquid_name cycle
|
||||
# @liquid_summary
|
||||
# Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/docs/api/liquid/tags/for).
|
||||
# @liquid_description
|
||||
# The `cycle` tag must be used inside a `for` loop.
|
||||
#
|
||||
# > Tip:
|
||||
# > Use the `cycle` tag to output text in a predictable pattern. For example, to apply odd/even classes to rows in a table.
|
||||
# @liquid_syntax
|
||||
# {% cycle string, string, ... %}
|
||||
class Cycle < Tag
|
||||
SimpleSyntax = /\A#{QuotedFragment}+/o
|
||||
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
||||
|
||||
attr_reader :variables
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
case markup
|
||||
when NamedSyntax
|
||||
@variables = variables_from_string(Regexp.last_match(2))
|
||||
@name = parse_expression(Regexp.last_match(1))
|
||||
@is_named = true
|
||||
when SimpleSyntax
|
||||
@variables = variables_from_string(markup)
|
||||
@name = @variables.to_s
|
||||
@is_named = !@name.match?(/\w+:0x\h{8}/)
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
|
||||
end
|
||||
end
|
||||
|
||||
def named?
|
||||
@is_named
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.registers[:cycle] ||= {}
|
||||
|
||||
key = context.evaluate(@name)
|
||||
iteration = context.registers[:cycle][key].to_i
|
||||
|
||||
val = context.evaluate(@variables[iteration])
|
||||
|
||||
if val.is_a?(Array)
|
||||
val = val.join
|
||||
elsif !val.is_a?(String)
|
||||
val = val.to_s
|
||||
end
|
||||
|
||||
output << val
|
||||
|
||||
iteration += 1
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
|
||||
context.registers[:cycle][key] = iteration
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def variables_from_string(markup)
|
||||
markup.split(',').collect do |var|
|
||||
var =~ /\s*(#{QuotedFragment})\s*/o
|
||||
next unless Regexp.last_match(1)
|
||||
|
||||
# Expression Parser returns cached objects, and we need to dup them to
|
||||
# start the cycle over for each new cycle call.
|
||||
# Liquid-C does not have a cache, so we don't need to dup the object.
|
||||
var = parse_expression(Regexp.last_match(1))
|
||||
var.is_a?(VariableLookup) ? var.dup : var
|
||||
end.compact
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
Array(@node.variables)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,38 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category variable
|
||||
# @liquid_name decrement
|
||||
# @liquid_summary
|
||||
# Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
|
||||
# @liquid_description
|
||||
# Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
|
||||
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
|
||||
# [snippets](/themes/architecture/snippets) included in the file.
|
||||
#
|
||||
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
|
||||
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share
|
||||
# variables.
|
||||
# @liquid_syntax
|
||||
# {% decrement variable_name %}
|
||||
# @liquid_syntax_keyword variable_name The name of the variable being decremented.
|
||||
class Decrement < Tag
|
||||
attr_reader :variable_name
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@variable_name = markup.strip
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
counter_environment = context.environments.first
|
||||
value = counter_environment[@variable_name] || 0
|
||||
value -= 1
|
||||
counter_environment[@variable_name] = value
|
||||
output << value.to_s
|
||||
output
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,77 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category syntax
|
||||
# @liquid_name doc
|
||||
# @liquid_summary
|
||||
# Documents template elements with annotations.
|
||||
# @liquid_description
|
||||
# The `doc` tag allows developers to include documentation within Liquid
|
||||
# templates. Any content inside `doc` tags is not rendered or outputted.
|
||||
# Liquid code inside will be parsed but not executed. This facilitates
|
||||
# tooling support for features like code completion, linting, and inline
|
||||
# documentation.
|
||||
#
|
||||
# For detailed documentation syntax and examples, see the
|
||||
# [`LiquidDoc` reference](/docs/storefronts/themes/tools/liquid-doc).
|
||||
#
|
||||
# @liquid_syntax
|
||||
# {% doc %}
|
||||
# Renders a message.
|
||||
#
|
||||
# @param {string} foo - A string value.
|
||||
# @param {string} [bar] - An optional string value.
|
||||
#
|
||||
# @example
|
||||
# {% render 'message', foo: 'Hello', bar: 'World' %}
|
||||
# {% enddoc %}
|
||||
class Doc < Block
|
||||
NO_UNEXPECTED_ARGS = /\A\s*\z/
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
ensure_valid_markup(tag_name, markup, parse_context)
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
while (token = tokens.shift)
|
||||
tag_name = token =~ BlockBody::FullTokenPossiblyInvalid && Regexp.last_match(2)
|
||||
|
||||
raise_nested_doc_error if tag_name == @tag_name
|
||||
|
||||
if tag_name == block_delimiter
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
raise_tag_never_closed(block_name)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(_context, output)
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
|
||||
def nodelist
|
||||
[]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_valid_markup(tag_name, markup, parse_context)
|
||||
unless NO_UNEXPECTED_ARGS.match?(markup)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.block_tag_unexpected_args", tag: tag_name)
|
||||
end
|
||||
end
|
||||
|
||||
def raise_nested_doc_error
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.doc_invalid_nested")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,39 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# @liquid_public_docs
|
||||
# @liquid_type tag
|
||||
# @liquid_category syntax
|
||||
# @liquid_name echo
|
||||
# @liquid_summary
|
||||
# Outputs an expression.
|
||||
# @liquid_description
|
||||
# Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly
|
||||
# bracket method, you can use the `echo` tag inside [`liquid` tags](/docs/api/liquid/tags/liquid).
|
||||
#
|
||||
# > Tip:
|
||||
# > You can use [filters](/docs/api/liquid/filters) on expressions inside `echo` tags.
|
||||
# @liquid_syntax
|
||||
# {% liquid
|
||||
# echo expression
|
||||
# %}
|
||||
# @liquid_syntax_keyword expression The expression to be output.
|
||||
class Echo < Tag
|
||||
attr_reader :variable
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@variable = Variable.new(markup, parse_context)
|
||||
end
|
||||
|
||||
def render(context)
|
||||
@variable.render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[@node.variable]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user