Compare commits

..
Author SHA1 Message Date
tetchi 70ddccd88f Revert "Docs Update" 2016-03-18 16:18:35 -04:00
tetchi 6bc76a92ea Merge pull request #513 from Shopify/docs-update
Docs Update
2016-03-18 16:15:16 -04:00
Adam HollettandTetsuro 74f478679f A few copy-edits 2016-03-15 19:36:29 -04:00
Adam HollettandTetsuro 7b50ceb955 Remove 'codescence' 2016-03-15 19:36:29 -04:00
Adam HollettandTetsuro 7fe3bfcb98 Adds Introduction page
Simplified Introdction page

Spiced up home page more, style changes to code examples
2016-03-15 19:36:27 -04:00
Adam HollettandTetsuro 4aacf3f9a1 Don't bold every sidebar link on the index page
Fixing a bunch of broken links in Tags/Basics
2016-03-15 19:36:24 -04:00
Adam HollettandTetsuro ea16fe9b24 Adds active state to sidebar links 2016-03-15 19:36:24 -04:00
Tetsuro f0883806c4 Style updates + truthy/falsy content udpate 2016-03-15 19:36:24 -04:00
Adam HollettandTetsuro 1aa4d93160 Adds map filter 2016-03-15 19:36:24 -04:00
Tetsuro f5391d0621 Links now have underlines for better visibility 2016-03-15 19:36:23 -04:00
Adam HollettandTetsuro 4d1378b4b3 Uncomplicate the sidebar 2016-03-15 19:36:23 -04:00
Adam Hollett 2790e8c339 Redcarpetify index; edit default filter 2015-12-14 11:08:34 -05:00
Adam Hollett 1579545be2 New content for nearly all filters 2015-12-13 22:39:26 -05:00
Adam Hollett f49b990e94 Simpler code snippets + magic CSS 2015-12-10 20:29:28 -05:00
Adam Hollett 66b98ca025 Don't use a border on inline code tags 2015-12-08 23:37:22 -05:00
Adam Hollett 92f9636ebe Fix front matter 2015-12-04 21:37:27 -05:00
Adam Hollett 74eb3418b6 Tiny fixes 2015-12-04 21:36:13 -05:00
Tetsuro 6ef9c176dd Fixed conflicts 2015-12-04 21:35:11 -05:00
Tetsuro 6bf836b719 Styled input/output codeblocks, going through all docs to make sure codeblocks are consistent 2015-12-04 21:34:18 -05:00
Adam Hollett 1ce90ed355 Refactor config and sidebar 2015-12-04 21:16:07 -05:00
Adam Hollett 90825277d3 Small fixes 2015-12-04 20:52:47 -05:00
Tetsuro 95dc90e503 Begin work on input/output standardization 2015-12-04 20:31:06 -05:00
Adam Hollett def9795c8f Merge remote-tracking branch 'origin/docs-update' into docs-update 2015-12-04 20:05:49 -05:00
Adam Hollett 1e769d9263 Cleanup and config reorg 2015-12-04 20:05:47 -05:00
Tetsuro c51257692d Update Gruntfile to work with Adam's changes 2015-12-04 20:02:56 -05:00
Tetsuro a33a127f81 Fix conflicts 2015-12-04 19:53:59 -05:00
Tetsuro 1504e5ad89 Updating CSS for sidebar 2015-12-04 19:51:46 -05:00
Adam Hollett a55d638690 Why use collections at all? Fixing control flow too 2015-12-04 19:50:03 -05:00
Tetsuro 88e774cb40 CSS adjustments to headers 2015-12-04 19:21:57 -05:00
Adam Hollett 8e3dcf49ba Moving to single collection 2015-12-04 18:56:07 -05:00
Tetsuro f0c7a601c3 Remove h2 classes to make the styles work in other pages 2015-10-16 11:46:03 -04:00
Tetsuro ce1d460d8a Change config file to use pygments, not rouge 2015-10-16 11:19:03 -04:00
Tetsuro fc80f30981 Merge branch 'docs-update' of github.com:Shopify/liquid into docs-update 2015-10-16 11:16:38 -04:00
Tetsuro ffe3ee3754 WIP 2015-10-16 11:16:34 -04:00
Adam Hollett 0147832a64 Polishing up boolean logic and types 2015-10-16 10:53:23 -04:00
Adam Hollett f2a1925033 Updates to types and truthiness 2015-10-15 17:01:53 -04:00
Adam Hollett d63213d463 Minor updates, switch Pigments to Rouge 2015-10-15 17:01:53 -04:00
Tetsuro 68e39b624a Adjusting layout so that every Basics and Tags' content output their own pages 2015-10-15 16:29:00 -04:00
Adam Hollett ea54777dd2 Intro page and small handles update 2015-10-15 13:43:30 -04:00
Tetsuro 34def78926 Added remainder of missing Filters 2015-10-15 13:16:36 -04:00
Tetsuro c744422d0e Added a bunch of missing filters, made some of the copy more consistent 2015-10-15 11:45:13 -04:00
Tetsuro 67b80e35f9 Adding tags, removing custom Liquid 2015-08-28 15:45:28 -04:00
Tetsuro 57ccbd4898 Re-organizing basics content, starting to remove Shopify-specific content 2015-08-28 12:28:11 -04:00
Tetsuro d4c1429ea7 Added missing dependency for Autoprefixer in package.json 2015-08-27 00:21:28 -04:00
Tetsuro ada2a87d8c Started adding Basics section 2015-08-26 19:07:55 -04:00
Tetsuro 6028ddb189 Changing sidebar to deeplinks
Fixed up navigation, collection contents are now output on one page. CSS cleanup
2015-08-26 19:07:51 -04:00
Tetsuro d9b2d50045 WIP: Adding missing filters 2015-07-25 17:48:18 -04:00
Tetsuro 7f550c8bce Left-aligned table headers 2015-07-25 17:09:54 -04:00
Tetsuro a3ceafbcd0 Removed Objects section 2015-07-25 16:50:24 -04:00
Tetsuro d9a08801c5 Added breadcrumb to post titles. Starting to fix alignment for table headers 2015-07-25 15:39:04 -04:00
Tetsuro 0ca571f58c Sidebar work: added collapsing categories
Big refactoring of SCSS

active state for sidebar links

update gitignore for node_modules folder

removing node_modules from index

Sticky Liquid link in sidebar

Cleaned up index page

Adding fixed width to buttons

Refactoring

Converted sidebar to use Collections

Refactor sidebar CSS
2015-07-25 13:49:14 -04:00
Tetsuro 82249c99b3 Set up Grunt
Fixing up Grunt file to watch for directories

Exclude node modules folder from Jekyll built site
2015-07-25 13:48:45 -04:00
tetchi ae3d14bef5 Merge pull request #537 from parkr/docs-update
WIP: Spike out the documentation
2015-03-27 23:01:16 -04:00
Parker Moore ec38c46ff6 Exclude README and CNAME from Jekyll built site. 2015-03-27 13:06:22 -07:00
Parker Moore 13532f7768 Change posts permalink to exclude categories. 2015-03-27 13:05:55 -07:00
Parker Moore 0359e54999 Use Ruby 2.1 in Travis. 2015-03-27 13:04:08 -07:00
Parker Moore b899c61bbc Add an edit link so users can contribute easily. 2015-03-27 13:02:02 -07:00
Garen Torikian 1aeb8a3305 A lot more updates 2015-03-20 16:51:51 -07:00
Garen Torikian 99db066e2a Add docs for the strip commands 2015-03-20 16:23:38 -07:00
Parker Moore 2937f1988b Merge pull request #1 from parkr/updates-to-doc-update
Continue updating documentation
2015-03-18 13:39:05 -07:00
Garen Torikian d65511e328 Add some more docs 2015-03-15 22:10:36 -07:00
Garen Torikian 601e49dc55 pretty tables 2015-03-15 18:23:17 -07:00
Garen Torikian 2232f9157c Add documentation for date 2015-03-15 18:20:32 -07:00
Garen Torikian c09cb6e05a Add scrollbar to the left sidebar 2015-03-15 18:20:25 -07:00
Parker Moore fe0809e2fa Stub out the other filters 2015-03-13 16:39:32 -07:00
Parker Moore f21311b946 Document downcase 2015-03-13 16:32:16 -07:00
Parker Moore adff96b4cf Document divided_by & make liquid raw 2015-03-13 16:29:00 -07:00
Parker Moore 43c2aab84b Add docs for default 2015-03-13 16:20:39 -07:00
Parker Moore dc7cd7704f How do I Liquid? 2015-03-13 16:20:30 -07:00
Parker Moore 57f83933fd Document ceil 2015-03-13 16:11:12 -07:00
Parker Moore 48b5f59b8e Put all the filters on one page 2015-03-13 16:11:04 -07:00
Parker Moore 1b5ff4c86f use a table to show input/output 2015-03-13 16:06:31 -07:00
Parker Moore 963cd58ab6 Add travis build script 2015-03-13 15:40:10 -07:00
Parker Moore f302fce89e Add capitalize docs 2015-03-13 15:39:58 -07:00
Parker Moore 5342f6f55a Add 'append' documentation 2015-03-13 15:29:02 -07:00
Parker Moore 5f1624d6f5 Initial munge of the site. 2015-03-13 15:28:30 -07:00
Tetsuro 447dd346c1 further work on index.md 2015-01-27 16:50:51 -05:00
Tetsuro 26f12734f8 adding readme 2015-01-27 15:30:23 -05:00
Tetsuro 67865368aa Homepage work + Readme 2015-01-27 15:29:13 -05:00
Tetsuro 1fad0e2638 Further work on the homepage 2015-01-27 15:06:23 -05:00
Tetsuro fe4253f872 Realized I don't need flexbox. Position fixed sidebar
Refactoring

Cleaned up some styles
2015-01-27 14:00:57 -05:00
Tetsuro a8dc349f01 starting sidebar coding 2015-01-26 20:41:04 -05:00
Tetsuro 79ec1506de cleaning up stylesheets 2015-01-26 20:32:10 -05:00
Tetsuro e661b5a275 big gutting job 2015-01-26 20:19:33 -05:00
Tetsuro 955a1fae98 Forgot to add files... 2015-01-26 17:53:33 -05:00
Tetsuro ecda8ff57b initial commit, setting up Jekyll 2015-01-26 17:52:23 -05:00
John Duff ba56db81e1 Doesn't look like you can have multiple CNAMEs listed 2011-09-07 12:20:02 -04:00
John Duff 8de5aa2fb4 Add liquidmarkup.com to CNAME as well 2011-09-07 12:08:41 -04:00
John Duff 64b297a180 Move liquidmarkup.org to github pages 2011-09-07 11:44:28 -04:00
Emanuele Vicentini 0239efd826 github generated gh-pages branch 2011-09-07 08:15:23 -07:00
237 changed files with 279 additions and 26827 deletions
-22
View File
@@ -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 }}
-52
View File
@@ -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
View File
@@ -1,9 +1 @@
*~
*.gem
*.swp
pkg
*.rbc
.rvmrc
.bundle
.byebug_history
Gemfile.lock
_site
-28
View File
@@ -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'
-180
View File
@@ -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
View File
@@ -1 +0,0 @@
3.4.1
+1
View File
@@ -0,0 +1 @@
liquidmarkup.org
-36
View File
@@ -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)
-31
View File
@@ -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
View File
@@ -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]
-20
View File
@@ -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.
-70
View File
@@ -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 -->
-156
View File
@@ -1,156 +0,0 @@
[![Build status](https://github.com/Shopify/liquid/actions/workflows/liquid.yml/badge.svg)](https://github.com/Shopify/liquid/actions/workflows/liquid.yml)
[![Inline docs](http://inch-ci.org/github/Shopify/liquid.svg?branch=master)](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.
-137
View File
@@ -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
-50
View File
@@ -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
-30
View File
@@ -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
-14
View File
@@ -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
-6
View File
@@ -1,6 +0,0 @@
<p>Hello world!</p>
<p>It is {{date}}</p>
<p>Check out the <a href="/products">Products</a> screen </p>
-49
View File
@@ -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>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

+277
View File
@@ -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&uuml;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">&lt;ul</span> <span class="na">id=</span><span class="s">&quot;products&quot;</span><span class="nt">&gt;</span>
{% for product in products %}
<span class="nt">&lt;li&gt;</span>
<span class="nt">&lt;h2&gt;</span>{{ product.title }}<span class="nt">&lt;/h2&gt;</span>
Only {{ product.price | format_as_money }}
<span class="nt">&lt;p&gt;</span>{{ product.description | prettyprint | truncate: 200 }}<span class="nt">&lt;/p&gt;</span>
<span class="nt">&lt;/li&gt;</span>
{% endfor %}
<span class="nt">&lt;/ul&gt;</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">&#39;products&#39;</span> <span class="o">=&gt;</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>&nbsp;&nbsp;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">
&copy; 2011
</div>
</div>
</body>
</html>
-92
View File
@@ -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'
-98
View File
@@ -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
-273
View File
@@ -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
-507
View File
@@ -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
-88
View File
@@ -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
-49
View File
@@ -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
-82
View File
@@ -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
-81
View File
@@ -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
-349
View File
@@ -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
-145
View File
@@ -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
-155
View File
@@ -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
-230
View File
@@ -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
-418
View File
@@ -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
-252
View File
@@ -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 = { '&' => '&amp;', '<' => '&lt;', '>' => '&gt;', '"' => '&quot;', "'" => '&#39;' }.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
-130
View File
@@ -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
-28
View File
@@ -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
-47
View File
@@ -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
-45
View File
@@ -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
-17
View File
@@ -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
-339
View File
@@ -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
-34
View File
@@ -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
-119
View File
@@ -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
-19
View File
@@ -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
-176
View File
@@ -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
-37
View File
@@ -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
-183
View File
@@ -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
-8
View File
@@ -1,8 +0,0 @@
# frozen_string_literal: true
module Liquid
module Const
EMPTY_HASH = {}.freeze
EMPTY_ARRAY = [].freeze
end
end
-295
View File
@@ -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
-22
View File
@@ -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
-65
View File
@@ -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
-84
View File
@@ -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
-159
View File
@@ -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
-59
View File
@@ -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
-124
View File
@@ -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
-76
View File
@@ -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
-75
View File
@@ -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
-89
View File
@@ -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
-41
View File
@@ -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
-18
View File
@@ -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
-179
View File
@@ -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
-32
View File
@@ -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"
-77
View File
@@ -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
-42
View File
@@ -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
-102
View File
@@ -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
-45
View File
@@ -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
-33
View File
@@ -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
-139
View File
@@ -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
-35
View File
@@ -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
-57
View File
@@ -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
-51
View File
@@ -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
-62
View File
@@ -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
-62
View File
@@ -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
-121
View File
@@ -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
-75
View File
@@ -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
-22
View File
@@ -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
-13
View File
@@ -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
-49
View File
@@ -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
-75
View File
@@ -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
-29
View File
@@ -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
-42
View File
@@ -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
-126
View File
@@ -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
-88
View File
@@ -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
-20
View File
@@ -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
-87
View File
@@ -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
-38
View File
@@ -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
-77
View File
@@ -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
-39
View File
@@ -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