Compare commits

...

924 Commits

Author SHA1 Message Date
Justin Li
f2f467bdbc v4.0.3 2019-03-12 12:43:48 -04:00
Justin Li
ff99d92c18 Merge pull request #1072 from Shopify/fix-interrupts
Fix interrupts through includes
2019-03-12 12:26:12 -04:00
Justin Li
39fecd06db Fix interrupts through includes 2019-03-12 12:18:22 -04:00
Justin Li
8013df8ca2 v4.0.2 2019-03-08 15:43:46 -05:00
Clayton Smith
14cd011cb5 Merge pull request #1070 from Shopify/url-decode-validation
Validate the character encoding in url_decode.
2019-03-08 11:09:40 -05:00
Clayton Smith
e2d9907df2 Validate the character encoding in url_decode. 2019-03-07 14:01:10 -05:00
Justin Li
23d669f5e6 Merge pull request #1032 from printercu/patch-1
Single regexp for strip_html
2019-02-22 13:04:04 -05:00
Justin Li
ed73794f82 Preserve existing strip_html behaviour for weird inputs 2019-02-22 13:00:36 -05:00
Ashwin Maroli
f59f6dea83 Fix simple RuboCop offenses and update TODO file (#1062)
* Fix Layout/EmptyLineAfterMagicComment offense

* Fix Layout/ExtraSpacing offense

* Fix Layout/ClosingParenthesisIndentation offenses

* Fix Style/MutableConstant offense

* Fix Style/UnneededInterpolation offenses

* Fix Style/RedundantParentheses offenses

* Update TODO config for RuboCop

* Add executable bit to test/test_helper.rb

ref: https://travis-ci.org/Shopify/liquid/jobs/488169512#L578
2019-02-22 12:32:56 -05:00
Garland Zhang
7a81fb821a Merge pull request #1059 from Shopify/map_error_checking
Apply error-checking to sort, sort_natural, where, uniq, map, compact filter(s)
2019-02-22 10:42:16 -05:00
Garland Zhang
cec27ea326 Extract raise error line and some filters with begin/rescue blocks 2019-02-21 17:00:20 -05:00
Justin Li
14999e8f7c Merge pull request #1053 from er1/update-changelog-v4.0.1
Updated changelog for v4.0.1 for (#1038)
2019-02-10 10:37:31 -05:00
Eric Chan
b41fc10d8e Updated changelog for v4.0.1 2018-12-03 23:54:00 -05:00
David Cornu
2b3c81cfd0 Merge pull request #1046 from Shopify/make-builds-green
Make builds green
2018-10-24 10:46:01 -04:00
David Cornu
2a2376bfd9 Run :test before :rubocop in the default Rake task 2018-10-19 15:06:36 -04:00
David Cornu
ca9e75db53 Reduce perceived complexity for #sort and #sort_natural 2018-10-19 14:57:33 -04:00
David Cornu
407c8abf30 Use TrailingCommaInLiteral
TrailingCommaInArrayLiteral and TrailingCommaInHashLiteral were introduced in v0.53.0 and we're running v0.49.0.

https://github.com/rubocop-hq/rubocop/blob/master/CHANGELOG.md#0530-2018-03-05
2018-10-19 14:52:16 -04:00
Justin Li
43f181e211 Merge pull request #1044 from Shopify/enable-cla-bot
Enable CLA bot
2018-10-19 09:34:30 -04:00
Tim Layton
7c613e87cb Enable CLA bot 2018-10-18 23:10:56 -07:00
Stephen Paul Weber (Work)
fe4034ccf9 Merge pull request #1025 from Shopify/traverse-ast
Liquid::ParseTreeVisitor
2018-10-18 09:42:56 -04:00
Stephen Paul Weber
52ee303a36 s/block.call/yield 2018-10-18 09:41:53 -04:00
Stephen Paul Weber
8217a8d86c Add test for the full array structure 2018-10-18 09:39:05 -04:00
Stephen Paul Weber
7d13d88258 s/Traversal/ParseTreeVisitor 2018-10-18 09:38:33 -04:00
Stephen Paul Weber
ff727016ef s/callback_for/add_callback_for 2018-10-18 09:37:48 -04:00
Stephen Paul Weber
c11fc656cf Colocate Traversal classes with classes they traverse
This puts all knowledge of the traversal in the same file, and removes
the need for a CASES registry.
2018-10-18 09:37:48 -04:00
Stephen Paul Weber
d789ec4175 Liquid::Traversal
This enables traversal over whole document tree.
2018-10-15 10:11:58 -04:00
Samuel Doiron
fd09f049b0 Merge pull request #1026 from Shopify/where-filter
Add `where` filter to standard filters
2018-10-11 17:45:30 -04:00
Samuel
842986a972 Add where filter to standard filters
Users of Liquid will often wish to filter an array to only those items that match a certain criteria. For example, showing "pinned" messages at the top of a list.

Example usage:

`{{ comments | where: "pinned" | first }}`

or

`{{ products | where: "category", "kitchen" }}`

* Add where filter to standard filters
* Add tests for new where functionality
2018-10-11 16:52:32 -04:00
Florian Weingarten
4661700a97 bump to v4.0.1 2018-10-09 11:13:19 +02:00
Justin Li
cd5a6dd225 Merge pull request #930 from er1/fix-sort-natural-on-nil
Fix sort and sort_natural on sorting with non-string and nil values
2018-10-04 22:32:37 -04:00
printercu
89c1ba2b0e Fix rubocop warning 2018-09-27 17:24:01 +03:00
printercu
479d8fb4a4 Single regexp for strip_html 2018-09-27 17:13:35 +03:00
Justin Li
53b8babf52 Merge pull request #1027 from Shopify/rubocop-fix
Update deprecated rubocop name
2018-09-13 17:16:36 -04:00
Justin Li
76b4920d3e Update deprecated rubocop name 2018-09-13 17:15:32 -04:00
Justin Li
8dcc319128 Merge pull request #1024 from koic/suppress_warning_bigdecimal_new
Suppress warning: `BigDecimal.new` is deprecated
2018-09-09 08:28:24 -04:00
Koichi ITO
0b36461d80 Suppress warning: BigDecimal.new is deprecated
## Summary

`BigDecimal.new` is deprecated since BigDecimal 1.3.3 for Ruby 2.5.

This PR suppresses the following warnings.

```console
% ruby -v
ruby 2.6.0dev (2018-09-06 trunk 64648) [x86_64-darwin17]
% RUBYOPT=-w bundle exec rake
(snip)
/Users/koic/src/github.com/Shopify/liquid/lib/liquid/utils.rb:49:
warning: BigDecimal.new is deprecated; use Kernel.BigDecimal method
instead.
/Users/koic/src/github.com/Shopify/liquid/lib/liquid/utils.rb:53:
warning: BigDecimal.new is deprecated; use Kernel.BigDecimal method
instead.
```

## Other Information

The following is a change of BigDecimal 1.3.3 for Ruby 2.5 related to this PR.

- 533737338d
- 16738ad0ac
2018-09-09 21:10:20 +09:00
Justin Li
70e75719de Merge pull request #1010 from Shopify/circle-ci-remove-38409b
Goodbye CircleCI 👋
2018-05-15 10:53:51 -04:00
shopify-admins
b037b19688 Removing CircleCI 1.0 [ci skip] 2018-05-15 10:35:38 -04:00
Florian Weingarten
d0f77f6cf4 Merge pull request #1006 from Benhgift/master
add installation instruction
2018-04-26 18:20:02 +01:00
Ben Gift
0be260bc97 add installation instruction 2018-04-26 08:12:47 -07:00
Dylan Thacker-Smith
5f0b64cebc Merge pull request #1005 from christopheraue/render_refactor
Refactored and optimized rendering
2018-04-19 16:44:57 -04:00
Christopher Aue
c086017bc9 refactored and optimized rendering
Measures:
1) A while loop is faster than iterating with #each.
2) Check string, variable and block tokens first. They are far more
   frequent than interrupt tokens. In their case, checking for an
   interrupt can be avoided.
3) String tokens just map to themselves and don't need the special
   treatment of BlockBody#render_node (except the resource limit
   check).

Benchmark
=========

$ bundle exec rake benchmark:run

Before
------

Run 1)
              parse:     41.630  (± 0.0%) i/s -    420.000  in  10.089309s
             render:     75.962  (± 3.9%) i/s -    763.000  in  10.066823s
     parse & render:     25.497  (± 0.0%) i/s -    256.000  in  10.040862s

Run 2)
              parse:     42.130  (± 0.0%) i/s -    424.000  in  10.064738s
             render:     77.003  (± 1.3%) i/s -    777.000  in  10.093524s
     parse & render:     25.739  (± 0.0%) i/s -    258.000  in  10.024581s

Run 3)
              parse:     41.976  (± 2.4%) i/s -    420.000  in  10.021406s
             render:     76.184  (± 1.3%) i/s -    763.000  in  10.018104s
     parse & render:     25.641  (± 0.0%) i/s -    258.000  in  10.062549s

After
-----

Run 1)
              parse:     42.283  (± 0.0%) i/s -    424.000  in  10.028306s
             render:     83.158  (± 2.4%) i/s -    832.000  in  10.009201s
     parse & render:     26.417  (± 0.0%) i/s -    266.000  in  10.069718s

Run 2)
              parse:     41.159  (± 4.9%) i/s -    412.000  in  10.031297s
             render:     81.591  (± 3.7%) i/s -    816.000  in  10.018225s
     parse & render:     25.924  (± 3.9%) i/s -    260.000  in  10.035653s

Run 3)
              parse:     42.418  (± 2.4%) i/s -    424.000  in  10.003100s
             render:     84.183  (± 2.4%) i/s -    847.000  in  10.069781s
     parse & render:     26.726  (± 0.0%) i/s -    268.000  in  10.029857s
2018-04-19 12:10:15 +02:00
Dylan Thacker-Smith
4369fe6c85 Improve the unexpected end delimiter message for block tags. (#1003) 2018-04-05 11:18:13 -04:00
Justin Li
c118e6b435 Merge pull request #992 from ashmaroli/each-without-index
Replace unnecessary Array#each_with_index with Array#each
2018-03-16 14:28:05 -04:00
Ashwin Maroli
0fbaf873d9 replace unnecessary #each_with_index with #each 2018-03-16 14:31:43 +05:30
Justin Li
5980ddbfae Merge pull request #988 from ashmaroli/regex-to-constant
Assign regexps to constants
2018-03-14 16:49:17 -04:00
Ashwin Maroli
193fc0fb7a revert to earlier regex for matching floats 2018-03-14 07:02:04 +05:30
Ashwin Maroli
e4da4d49d2 assign regex to a constant 2018-03-13 23:36:56 +05:30
Justin Li
a0bec1f873 Merge pull request #981 from nicolasleger/patch-1
[CI] Test against Ruby 2.5 version
2018-03-05 11:23:18 -05:00
Nicolas Leger
4aa3261518 [CI] Test against Ruby 2.5 version 2018-02-12 00:23:06 +01:00
Dylan Thacker-Smith
04d552fabb Gemfile: Use https rather than git protocol to fetch liquid-c 2018-02-01 07:08:19 -05:00
Dylan Thacker-Smith
5106466a2d Add a regression test for a liquid-c trim mode bug (#972) 2018-01-25 10:55:01 -05:00
Justin Li
5d6c1ed7c6 Merge pull request #963 from lostapathy/patch-1
have travis test against ruby 2.4
2017-12-15 16:53:26 -05:00
Joe Francis
a594653a0c have travis test against ruby 2.4 2017-12-15 14:27:17 -06:00
Thibaut Courouble
0c802aba17 Merge pull request #958 from Shopify/minmax
Rename min/max filters for clarity
2017-12-06 11:41:12 -05:00
Thibaut Courouble
147d7ae24d Rename min/max filters for clarity 2017-12-06 09:48:30 -05:00
Thibaut Courouble
282d42f98d Fix min/max filters 2017-12-06 08:58:05 -05:00
Justin Li
e6ba6ee87b Revert "Use replacement string for replace filters literally (#924)"
This reverts commit 27c91203ab.
2017-12-04 15:07:59 -05:00
Nithin Bekal
2ad7a37d44 Merge pull request #954 from Shopify/max-min-filters
Add max and min filters
2017-11-30 14:18:43 -05:00
Nithin Bekal
4bdaaf069f Add max/min filters 2017-11-30 13:56:37 -05:00
Justin Li
85b1e91aed Merge pull request #952 from Shopify/bump-rubocop
Bump rubocop
2017-11-22 12:44:36 -05:00
Justin Li
a7c5e247c8 Bump rubocop 2017-11-22 11:59:06 -05:00
Dylan Thacker-Smith
6c117fd7dd refactor: Reduce maximum block nesting in Liquid::BlockBody#parse (#944) 2017-10-19 10:12:40 -04:00
Maxime Bedard
7d2d90d715 Merge pull request #932 from Shopify/avoid-default-values-hash
Avoid hash with default values due to inconsistent marshaling
2017-10-17 16:02:45 -04:00
Maxime Bedard
f761d21215 Use {} notation 2017-09-20 09:48:23 -04:00
Maxime Bedard
a796c17f8b Avoid hash with default values due to inconsistent marshalling 2017-09-19 16:23:14 -04:00
Eric Chan
deb10ebc7a Sorting support for data with undefined values 2017-09-14 02:00:43 -04:00
Eric Chan
cfe1844de9 Added test coverage for sort_natural 2017-09-13 22:17:59 -04:00
Eric Chan
59950bff87 Fix sort_natural on sorting with non-string values 2017-09-13 01:37:40 -04:00
Dylan Thacker-Smith
27c91203ab Use replacement string for replace filters literally (#924) 2017-08-28 11:51:20 -04:00
Justin Li
44eaa4b9d8 Merge pull request #920 from Shopify/symbol_to_liquid
Support rendering symbols as strings
2017-08-18 12:10:53 -04:00
Pascal Betz
a979b3ec95 Do not raise when variable is defined but nil when using strict_variables 2017-08-18 12:09:57 -04:00
Justin Li
bf3e759da3 Support rendering symbols as strings 2017-08-17 23:10:57 -04:00
Rene
59162f7a0e added attr_readers for collection and variable names in for tag (#909) 2017-07-06 09:41:48 -04:00
Thierry Joyal
c582b86f16 Merge pull request #898 from Shopify/cgi-powered-standard-filters-to-handle-non-string-inputs
CGI powered standard filters to handle non string inputs
2017-05-26 18:05:42 +00:00
Thierry Joyal
e340803d12 CGI powered standard filters to handle non string inputs 2017-05-25 15:53:41 +00:00
Dylan Thacker-Smith
48a6d86ac2 Use stackprof to test to lack of object allocations (#896) 2017-05-12 09:20:51 -04:00
Dylan Thacker-Smith
3bb29d5456 Replace assert_equal nil, with a assert_nil (#895) 2017-05-11 14:05:03 -04:00
Dylan Thacker-Smith
9c72ccb82f Limit how much blocks can be nested during parsing (#894) 2017-05-11 09:37:53 -04:00
Dylan Thacker-Smith
62d4625468 Use a loop to strictly parse binary comparisons to avoid recursion (#892)
Using recursion allows a malicious template to cause a SystemStackError
2017-05-10 10:41:52 -04:00
Dylan Thacker-Smith
8928454e29 Use a loop to evaluate binary comparisions to avoid recursion (#891)
Using recursion allows a malicious template to cause a SystemStackError
2017-05-10 10:41:24 -04:00
Florian Weingarten
1370a102c9 Merge pull request #789 from evulse/contains-strict-fix
Allow variables to start with contains in strict parser
2017-03-24 09:50:31 -04:00
Mike Angell
c9bac9befe Merge branch 'master' into contains-strict-fix 2017-03-24 11:09:09 +10:00
Mike
210a0616f3 Update History to include fix 2017-03-24 10:35:56 +10:00
Lasse Skindstad Ebert
5149cde5c3 Fix include tag used with strict_variables (#829)
Fixes https://github.com/Shopify/liquid/issues/828
2017-03-22 16:00:31 -04:00
Florian Weingarten
22f2cec5de Merge pull request #864 from chenxianyu2015/fix-strainer-add_filter-method
fix  #861: duplicate inclusion condition logic error of Liquid::Strainer.add_filter method
2017-02-23 14:06:20 -05:00
chenxianyu
4318240ae0 test: modify Strainer.add_filter duplicate inclusion test case 2017-02-22 10:33:22 +08:00
chenxianyu
aa79c33dda fix: Strainer.add_filter method 2017-02-13 15:50:19 +08:00
Justin Li
b1ef28566e Merge pull request #846 from mrmanc/master
Clarifies spelling of for’s reversed flag to address #843
2017-02-10 19:26:38 -05:00
Justin Li
41bcc48222 Merge pull request #854 from jaredbeck/patch-1
Docs: Help people upgrade to 4, re: liquid_methods
2017-02-10 19:25:04 -05:00
Dylan Thacker-Smith
27d5106dc9 Merge pull request #860 from Shopify/handle-string-node-render-exc
Avoid calling line_number on String node when rescuing a render error.
2017-02-10 14:13:11 -05:00
Dylan Thacker-Smith
7334073be2 Avoid duck typing to detect whether to call render on a node. 2017-02-10 13:49:26 -05:00
Dylan Thacker-Smith
5dcefd7d77 Avoid calling line_number on String node when rescuing a render error. 2017-02-07 15:34:10 -05:00
Richard Monette
25c7b05916 Merge pull request #857 from Shopify/handle-join-on-fixnum
handle join on fixnum
2017-02-01 14:25:40 -05:00
Richard Monette
d17f86ba4d handle join on fixnum 2017-02-01 12:47:35 -05:00
Jerry Liu
384e4313ff Merge pull request #851 from Shopify/benchmark-render
Allow benchmarks to benchmark render by itself
2017-01-31 17:18:56 -05:00
Jerry Liu
23f2af8ff5 fix travis build 2017-01-31 17:04:36 -05:00
Jerry Liu
a93eac0268 Introduce new benchmarking methods to liquid to use on rubybench 2017-01-27 10:56:16 -05:00
Florian Weingarten
2cc7493cb0 Merge pull request #855 from Shopify/bundler-benchmark-group
Create a benchmark group in Gemfile
2017-01-20 16:41:11 -05:00
Jerry Liu
85463e1753 add benchmark-ips to benchmark group in Gemfile 2017-01-20 16:04:42 -05:00
Jared Beck
52ff9b0e84 Docs: Help people upgrade to 4, re: liquid_methods
The discussion in #568 helped me.

[ci skip]
2017-01-19 14:23:39 -05:00
Dylan Thacker-Smith
0c58328a40 test: Equality comparison of two hashes (#850) 2017-01-16 15:56:38 -05:00
Dylan Thacker-Smith
2bb3552033 Fix internal liquid error when comparing hash with incompatible type (#849) 2017-01-16 13:13:17 -05:00
Mark Crossfield
8b751ddf46 Removes a non ascii character from comment to appease Rubocop 2017-01-09 10:16:35 +00:00
Mark Crossfield
e5cbdb2b27 Clarifies spelling of for’s reversed flag to address #843
It should now be harder to read the docs and miss the extra letter required for reversed compared to reverse, which causes a fairly generic syntax warning when trying to reverse sort a collection in a for loop.
2017-01-08 12:44:12 +00:00
Justin Li
ffb0ace303 Update changelog for 4.0.0 2016-12-16 13:11:22 -05:00
Florian Weingarten
ad00998ef8 bump to v4 2016-12-14 11:58:42 -05:00
Dylan Thacker-Smith
869dbc7ebf feature: Allow a default exception renderer to be specified (#837)
This could be used to preserve the old default of rendering
non-Liquid::Error messages or for providing default behaviour like error
reporting which could be missed if the exception renderer needed to be
specified on each render.
2016-12-12 10:29:09 -05:00
Dylan Thacker-Smith
fae3a2de7b Add version constraint to rake to fix CI (#836) 2016-12-09 14:01:15 -05:00
Dylan Thacker-Smith
f27bd619b9 change: Render an opaque internal error by default for non-Liquid::Error (#835)
These errors may contain sensitive information, so is safer to
render a more vague message by default.

This is done by replacing non-Liquid::Error exceptions with a
Liquid::InternalError exception with the non-Liquid::Error accessible on
through the cause method. This also allows the template name and line
number to be attached to the template errors.

The exception_handler render option has been changed to exception_renderer
since now it should raise an exception to re-raise on a liquid rendering
error or return a string to be rendered where the error occurred.
2016-12-07 17:34:29 -05:00
Dylan Thacker-Smith
a9b84b7806 test: Use ruby 2.1 in Circle CI 2016-12-05 15:36:42 -05:00
Dylan Thacker-Smith
6cc2c567c5 Merge pull request #832 from Shopify/drop-ruby-2.0
Drop support for ruby 2.0
2016-12-05 13:56:15 -05:00
Dylan Thacker-Smith
812e3c51b9 test: Add ruby 2.3.3 to CI
Travis doesn't have a ruby 2.3 alias, so the latest 2.3.x version is
specified instead.
2016-12-05 13:53:02 -05:00
Dylan Thacker-Smith
9dd0801f5c Drop support for ruby 2.0
It is no longer maintained upstream
2016-12-05 13:51:49 -05:00
Dylan Thacker-Smith
b146b49f46 fix: Clear the strainer cache when a global filter is added (#826) 2016-11-24 10:32:11 -05:00
Richard Monette
86944fe7b7 Merge pull request #809 from Shopify/introduce-unhandled-liquid-exception
introduce unhandled liquid exception
2016-10-31 10:20:06 -04:00
Richard Monette
a549d289d7 introduce unhandled liquid exception
check arity
2016-10-28 09:40:44 -04:00
Richard Monette
b2feeacbce Merge pull request #812 from Shopify/allow-split-to-accept-numeric
allow split to accept numeric
2016-10-26 10:59:44 -04:00
Richard Monette
143ba39a08 allow split to accept numeric 2016-10-26 10:43:04 -04:00
Richard Monette
43e59796f6 Merge pull request #805 from Shopify/dont-explode-when-sorting-nil-property
dont explode when sorting nil property
2016-10-05 10:18:16 -04:00
Richard Monette
bb3624b799 dont explode when sorting nil property 2016-10-04 13:22:29 -04:00
Konstantin Tennhard
64fca66ef5 Merge pull request #797 from Shopify/truncatewords-resiliency
Standard filter truncate / truncatewords: force truncate_string to string
2016-09-13 10:43:55 -04:00
Florian Weingarten
e9d7486758 4.0.0.rc3 2016-09-13 06:33:20 -04:00
Philibert Dugas
2bb98c1431 Merge pull request #798 from PhilibertDugas/bugfix-#697
Fixing #697 with better exception
2016-09-12 13:53:22 -04:00
Konstantin Tennhard
95d5c24bfc Standard filter truncate: truncate_string string coercion
The argument `truncate_string` is now coerced into a string to avoid
`NoMethodError`s. This is mostly for added resiliency. It is doubtful
that someone would actually intent to use a number as truncate string,
but accidentally supplying one is entirely possible.
2016-09-12 12:13:12 -04:00
Philibert Dugas
b7ee1a2176 Fixing #697 with better exception
When including a template which is not defined, the exception raised is
*undefined method `split` for nil:NilClass*

This occurs for a scenario like the following:
`{% include nil %}`
or
`{% include undefined-var %}`

Making the code raise an argument error to allow better understanding of
the include error
2016-09-12 09:31:59 -04:00
Florian Weingarten
0eca61a977 Merge pull request #799 from kainjow/patch-1
Update liquid-c
2016-09-12 08:12:14 -04:00
Kevin Wojniak
9bfd04da2d Update liquid-c 2016-09-10 09:23:15 -07:00
Konstantin Tennhard
302185a7fc Standard filter truncatewords: force truncate_string to string
Currently, `truncatewords` raises a TypeError when the argument
`truncate_string` is an interger. This PR forces string coercion for any
value provided for this argument. Thus,

```ruby
assert_equal 'one two1', @filters.truncatewords("one two three", 2, 1)
```

holds true. Another option would be to raise a `Liquid::ArgumentError`.

What is preferred?
2016-09-09 16:50:50 -04:00
Michael Angell
6ed6e7e12f Allow :id to start with the word contains 2016-08-20 20:32:46 +10:00
Mike Angell
f41ed78378 Merge pull request #1 from Shopify/master
Pull inline with upstream
2016-08-17 21:30:08 +10:00
Florian Weingarten
50c85afc35 Merge pull request #786 from Shopify/bump-liquid-c
Bump LiquidC for whitespace changes
2016-08-11 13:38:42 -04:00
Florian Weingarten
5876dff326 Bump LiquidC for whitespace changes 2016-08-11 13:21:39 -04:00
Florian Weingarten
f25185631d Merge pull request #773 from evulse/whitespace-trim
Add whitespace control character and associated tests
2016-08-11 13:20:12 -04:00
Michael Angell
283f1bad18 Use .last instead of pop push method for updating last node in nodelist 2016-07-08 20:49:30 +10:00
Michael Angell
e1d40c7d89 Add whitespace control character and associated tests 2016-06-28 09:15:45 +10:00
Justin Li
19c6eb426a Merge pull request #769 from zacstewart/patch-1
Fix doc formatting of code examples in file_system
2016-06-15 17:10:11 -04:00
Zac Stewart
f87b06095d Fix doc formatting of code examples in file_system
These code examples are being rendered as paragraph text in the docs.
2016-06-15 15:34:14 -04:00
Gaurav Chande
b81d54e789 Merge pull request #761 from Shopify/range-to_liquid
Support Range Type
2016-06-02 16:48:30 -04:00
Gaurav Chande
00f53b16e8 Prevent Range usage in templates from blowing up 2016-06-02 16:38:44 -04:00
Gaurav Chande
e4cf55b112 Merge pull request #748 from Shopify/expose-tags
Make Template.tags loop-able
2016-04-25 11:59:37 -04:00
Gaurav Chande
5bb211d933 Ensure no tag leakage since registry is global 2016-04-25 11:50:46 -04:00
Gaurav Chande
6adc431a19 Make tag registry enumerable 2016-04-25 11:38:42 -04:00
Justin Li
23d2beed41 Merge pull request #744 from Shopify/raw-syntax-method
Make markup validation a method on Liquid::Raw
2016-04-13 17:08:02 -04:00
Drew Martin
a80ecb7678 make markup validation a method on Liquid::Raw 2016-04-13 14:52:30 -04:00
Florian Weingarten
361c695264 Merge pull request #736 from Shopify/abs-filter
Abs filter
2016-04-05 09:13:56 -04:00
Florian Weingarten
f93243cc1a abs filter 2016-04-04 09:32:31 -04:00
Florian Weingarten
1e533a52e7 Merge pull request #735 from Shopify/fix-to-number-for-negative-float-strings
Fix to_number filter for negative float strings
2016-03-31 15:52:51 -04:00
Dylan Thacker-Smith
3ea84f095f Merge pull request #734 from Shopify/concat-liquid-error
Raise a Liquid::Error when a non-array is passed into the concat filter.
2016-03-31 15:47:43 -04:00
Dylan Thacker-Smith
4239c899a4 Raise a Liquid::Error when a non-array is passed into the concat filter. 2016-03-31 15:47:06 -04:00
Florian Weingarten
1597f8859f Fix to_number filter for negative float strings 2016-03-31 09:18:55 -04:00
Florian Weingarten
b3dda384c9 Merge pull request #733 from Shopify/fix-some-ruby-warnings
Fix a bunch of Ruby warnings
2016-03-30 17:09:00 -04:00
Florian Weingarten
6828670bfe Merge pull request #732 from Shopify/v400rc2
Release v4.0.0rc2
2016-03-30 17:02:34 -04:00
Florian Weingarten
d2f16d92d6 Fix a bunch of Ruby warnings 2016-03-30 20:42:30 +00:00
Justin Li
d233acb483 Update history to reflect merge of #731 2016-03-30 16:36:57 -04:00
Florian Weingarten
8920e2a2a2 Release v4.0.0rc2 2016-03-30 20:13:21 +00:00
Justin Li
bfee507005 Merge pull request #731 from ismasan/duck_typed_maths_filters
Duck typed maths filters
2016-03-30 16:09:16 -04:00
Ismael Celis
929c89789f Test that all maths filters work with duck-typed #to_number 2016-03-30 13:35:09 -03:00
Ismael Celis
d03c4ae8e8 Allow Utils.to_number to work with anything that responds to #to_number 2016-03-30 01:57:21 -03:00
Justin Li
021bafd260 Merge pull request #725 from jeroenvisser101/performance-start-with-vs-regex
Use start_with? instead of Regex
2016-03-21 10:34:28 -04:00
Jeroen Visser
04c393ab07 Use start_with? instead of Regex
Performance is increased by doing this:

  require 'benchmark'
  require 'tempfile'

  n = 50000
  test = File.expand_path(Tempfile.new('foo'))
  Benchmark.bm(20) do |x|
    x.report("Regex:") do
      n.times { test =~ /\A#{test}/ }
    end
    x.report("String#start_with?:") do
      n.times { test =~ test.start_with?(test) }
    end
  end

Benchmark result:
                             user     system      total        real
  Regex:                 0.440000   0.010000   0.450000 (  0.447357)
  String#start_with?:    0.000000   0.000000   0.000000 (  0.006313)
2016-03-21 14:23:35 +01:00
Gaurav Chande
9a7778e52c Merge pull request #707 from Shopify/drop-without-context
@context not always present on a Drop
2016-03-01 18:07:16 -05:00
Gaurav Chande
dde00253f9 context is not always present on a Drop 2016-03-01 21:22:11 +00:00
Gaurav Chande
18d1644980 Merge pull request #705 from Shopify/register-filter-warn
Strainer#add_filter Raises on Private Override
2016-02-24 15:31:41 -05:00
Gaurav Chande
c424d47274 Add changelog entry for Strainer method override change 2016-02-24 20:23:57 +00:00
Gaurav Chande
8e6b9d503d Make Strainer also raise when registered method is overriden as protected 2016-02-24 20:23:49 +00:00
Gaurav Chande
8be38d1795 Strainer#add_filter should raise when registered method is overriden as private 2016-02-24 20:03:17 +00:00
Justin Li
3146d5c3f2 Grammatic and other fixes to CONTRIBUTING.md 2016-02-02 23:45:37 -05:00
Justin Li
0cc8b68a97 Make logic in Context#lookup_and_evaluate more understandable 2016-02-02 23:22:46 -05:00
Justin Li
5a50c12953 Update history to reflect merge of #691
[ci skip]
2016-02-02 23:14:41 -05:00
Justin Li
a6fa4c5c38 Merge pull request #691 from urbandictionary/missing_variables_and_filters
Merge pull request 691
2016-02-02 23:13:44 -05:00
Ivan Kuznetsov
dadd9b4dd2 Add strict_variables/strict_filters render options to check for undefined variables and filters 2016-02-03 10:49:33 +07:00
Justin Li
6434b8d2bb Merge pull request #696 from Shopify/no-send
Remove possibility for arbitrary sends
2016-02-02 11:01:46 -05:00
Justin Li
2d891ddd8f Merge pull request #695 from Shopify/assign-score
Take nested values into account for assign score
2016-02-01 13:14:40 -05:00
Justin Li
60b508b151 Use #each to avoid extra allocations 2016-02-01 13:01:25 -05:00
Justin Li
3891f14a1a Take nested values into account for assign score 2016-02-01 13:01:25 -05:00
Justin Li
198f0aa366 Add test for nested assign score bookkeeping 2016-02-01 13:01:23 -05:00
Justin Li
f2e6adf566 Remove arbitrary send vector 2016-02-01 11:38:40 -05:00
Justin Li
08de6ed2c5 Merge pull request #687 from pathawks/default
Performance improvement: `default` filter
2016-01-24 11:34:05 -05:00
Pat Hawks
7e322f5cf8 Performance improvement: default filter 2016-01-23 23:18:51 -08:00
Justin Li
bf86a5a069 Merge pull request #688 from Shopify/gmp
Install libgmp3-dev in travis
2016-01-23 21:46:37 -05:00
Justin Li
0141444814 Install libgmp3-dev in travis 2016-01-23 21:41:14 -05:00
Justin Li
6d30226768 Update changelog for 4.0.0rc1 2016-01-08 15:08:06 -05:00
Florian Weingarten
63e8bac1a4 meh 2016-01-08 20:00:45 +00:00
Florian Weingarten
8449849ed5 Merge pull request #682 from Shopify/4-pre-beta1
4.0.0.rc1
2016-01-08 20:59:59 +01:00
Florian Weingarten
4bc198a0db 4.0.0.rc1 2016-01-08 19:59:38 +00:00
Florian Weingarten
3921dbe919 Merge pull request #683 from Shopify/dropify-tablerowloop
Liquid::TablerowloopDrop
2016-01-08 20:41:54 +01:00
Florian Weingarten
79e2d1d8b4 Liquid::TablerowloopDrop 2016-01-08 18:46:23 +00:00
Florian Weingarten
b7c4041db8 Merge pull request #681 from Shopify/save-some-loop-allocations
Reuse 'forloop' hash to save memory allocations
2016-01-06 22:47:39 +01:00
Florian Weingarten
e113c891ec Convert forloop hash to drop 2016-01-06 21:30:32 +00:00
Guillaume Malette
a32ad449c0 Merge pull request #672 from Shopify/fix-proc-mapping
Test mapping over procs
2016-01-06 15:59:53 -05:00
Florian Weingarten
1662ba6679 Reuse 'forloop' hash to save memory allocations 2016-01-06 20:30:25 +00:00
Dylan Thacker-Smith
99b5e86f0a Merge pull request #680 from jcheatham/master
Ensure truncate is operating on a string
2015-12-23 18:41:39 -05:00
Jonathan Cheatham
b892a73463 Ensure truncate is operating on a string 2015-12-22 19:40:35 -08:00
Guillaume Malette
0b55d09cea Fix mapping over proc attributes 2015-11-20 13:04:42 -05:00
Dylan Thacker-Smith
5f8086572b Merge pull request #667 from Shopify/remove-empty-string-check
Remove nil and empty string check in invoke_drop.
2015-11-10 10:43:11 -05:00
Dylan Thacker-Smith
bdb9a4a47f Remove nil and empty string check in invoke_drop. 2015-11-09 15:03:36 -05:00
Dylan Thacker-Smith
c38eec0293 Merge pull request #665 from tanelj/escape_filter_nil_fix
Fixed issue where "nil" value for "escape" filter breaks rendering
2015-11-06 10:54:48 -05:00
Tanel Jakobsoo
8d5a907dc8 Fixed issue where "nil" value for "escape" filter breaks rendering
Closes #664
2015-11-06 16:32:02 +02:00
Florian Weingarten
74cc41ce74 Merge pull request #662 from nickpearson/keep-argument-error-backtrace
Keep original stack trace in Liquid::ArgumentError
2015-10-29 15:24:54 +01:00
Thierry Joyal
a120cc587a Merge pull request #661 from Shopify/rename-before-method-as-dynamic-method
Rename before_method as liquid_method_missing
2015-10-29 09:49:15 -04:00
Nick Pearson
c582023321 Keep original stack trace in Liquid::ArgumentError 2015-10-29 08:15:37 -05:00
Thierry Joyal
ac041c4ad1 Rename before_method as liquid_method_missing 2015-10-28 17:28:19 +00:00
Justin Li
31d7682f4e Update history to reflect merge of #658
[ci skip]
2015-10-21 12:50:12 -04:00
Justin Li
5f1acbc086 Merge pull request #658 from Shopify/url_decode-filter
Merge pull request 658
2015-10-21 12:49:14 -04:00
Justin Li
8612716129 Remove rescue in unescape filter 2015-10-21 02:01:21 -04:00
Larry Archer
e6392d1cc1 Tests for new url_decode filter 2015-10-21 01:58:22 -04:00
Larry Archer
04381418d3 Add url_decode filter to accompany url_encode 2015-10-21 01:58:22 -04:00
Justin Li
89ccdabe9a Merge pull request #655 from dijonkitchen/patch-1
Rename MIT-LICENSE to LICENSE
2015-10-14 12:08:37 -04:00
Jonathan Chen
c0fc6777b0 Rename MIT-LICENSE to LICENSE
Standard name format
2015-10-14 12:06:08 -04:00
Justin Li
cd03346239 Update history to reflect merge of #652
[ci skip]
2015-09-29 21:06:21 -04:00
Justin Li
b4f19da127 Merge pull request #652 from mcary/empty-array-sort
Merge pull request 652
2015-09-29 21:05:10 -04:00
Marcel M. Cary
4100f8d641 Fix "sort" filter on empty array to return empty array
When sorting an empty array with the "sort" filter, it returns nil
instead of [].  This confuses subsequent filters in the chain that
expect an array.  For example, when followed by the "map" filter, it
produces an array containing one nil element: [nil].

I could special-case the nil return value, but that would be more
cumbersome than making sure "sort" always returns an array.

Add a case to the "sort" method to return [] if the array is empty,
before performing any checks on ary.first that assume a non-empty array.

There is still a danger of returning nil if the first item in the array
is nil and it is non-empty, but I'm not sure how better to handle that
case.

Apply a similar fix to sort_natural, uniq, and compact filters.
2015-09-29 10:24:31 -07:00
Dylan Thacker-Smith
d8bda2c892 Merge pull request #653 from Shopify/fix-rubocop-offenses
Fix offenses from the new version of rubocop.
2015-09-25 19:48:09 -04:00
Dylan Thacker-Smith
4f81c0a658 Lock rubocop version to avoid CI failures from new releases. 2015-09-25 19:42:35 -04:00
Dylan Thacker-Smith
704937bc00 Fix offenses from the new version of rubocop. 2015-09-25 19:34:44 -04:00
Justin Li
27c6b8074a Update history to reflect merge of #610
[ci skip]
2015-08-03 20:51:41 -04:00
Justin Li
affae5ebef Merge pull request #610 from boobooninja/gf3
Merge pull request 610
2015-08-03 20:50:14 -04:00
Florian Weingarten
fc1c0d0d83 Merge pull request #632 from knu/fix_date_error
Properly rescue ::ArgumentError in the date filter
2015-07-24 10:50:52 -04:00
Akinori MUSHA
a215b70de9 Properly rescue ::ArgumentError in the date filter 2015-07-24 13:35:06 +09:00
Justin Li
1f70928f8a Update history to reflect merge of #631
[ci skip]
2015-07-23 17:07:40 -04:00
Justin Li
7713f6709d Update history for 3.0.5 2015-07-23 17:06:12 -04:00
Justin Li
239cf0e5f5 Update history for 2.6.3 2015-07-23 17:05:58 -04:00
Dylan Thacker-Smith
fa187665b3 Merge pull request #631 from Shopify/fix-tz-test-failure
Fix a timezone test failure.
2015-07-23 16:34:48 -04:00
Dylan Thacker-Smith
cd0c5e954c Fix a timezone test failure. 2015-07-23 16:19:59 -04:00
Florian Weingarten
490b457738 Merge pull request #626 from Shopify/fix_bracket_thing
Fix bracket thing
2015-07-17 17:19:06 +02:00
Florian Weingarten
4d6dec9b5a Fix chained access to multi-dimensional hash 2015-07-17 10:10:00 -04:00
Loren Hale
0b11b573d9 add global_filter
add a global filter using a proc
only add one proc and not an array
add tests to make sure the global_filter is applied after native filters
2015-07-12 16:46:43 +08:00
Justin Li
b42d35ff36 Merge pull request #620 from Shopify/accept-invalid-range-args
Add param to accept invalid input in to_integer
2015-07-09 13:24:28 -04:00
Justin Li
b4e133e26f Fix regression in range lookup 2015-07-09 13:21:46 -04:00
Justin Li
1f9bd1d809 Add param to accept invalid input in to_integer 2015-07-09 13:18:06 -04:00
Justin Li
e88be60818 Merge pull request #618 from Shopify/move-reraise-for-line-number
Move the syntax error rescue for adding error line numbers.
2015-07-09 11:42:41 -04:00
Dylan Thacker-Smith
14416b3c49 Move the syntax error rescue for adding error line numbers. 2015-07-09 11:25:05 -04:00
Dylan Thacker-Smith
bde14a650d Merge pull request #617 from Shopify/rename-options-iv
Rename options instance variable in Variable and Tag.
2015-07-08 20:50:20 -04:00
Dylan Thacker-Smith
c535af021a Rename options instance variable in Variable and Tag. 2015-07-08 19:59:44 -04:00
Dylan Thacker-Smith
9c9345869b Merge pull request #614 from Shopify/remove-token-class
Implement line numbers without the Liquid::Token class.
2015-07-08 19:48:55 -04:00
Dylan Thacker-Smith
73834a7e52 Use reject rather than dup and delete. 2015-07-08 19:27:24 -04:00
Dylan Thacker-Smith
c45310170b Use parse_context or options instead of @options. 2015-07-08 19:21:59 -04:00
Dylan Thacker-Smith
920e1df643 Rescue and re-raise syntax errors in Template#parse to add line numbers.
This can be done now that the parse context has the line number
information, so it doesn't need to be added on closer to the original
exception.  This has the advantage of not having to rescue and re-raise the
exception multiple times, and simplifies liquid-c which would otherwise
have to rescue the exception in BlockBody#parse.
2015-07-08 19:21:59 -04:00
Dylan Thacker-Smith
cebf75b8d7 Implement line numbers without the Liquid::Token class. 2015-07-08 19:21:59 -04:00
Justin Li
afda01adbb Merge pull request #616 from Shopify/handle-non-int-range-args
Handle non-int range lookup arguments
2015-07-08 17:47:27 -04:00
Justin Li
959cd6d2a2 Temporarily disable rubinius in CI
It takes much longer than the others and is currently broken
2015-07-08 17:47:05 -04:00
Justin Li
4c1b89e20e Add regression test for ranges on non-integer types 2015-07-08 17:41:18 -04:00
Justin Li
83b6dd0268 Use to_integer for range lookup arguments 2015-07-08 17:37:07 -04:00
Justin Li
6fb402e60d Move to_integer, to_date, and to_number to Liquid::Utils 2015-07-08 17:33:05 -04:00
Dylan Thacker-Smith
338287df5e Merge pull request #613 from Shopify/taint-context-warning
Add taint warnings to the context rather than the template.
2015-07-07 16:23:10 -04:00
Dylan Thacker-Smith
c4c398174b Use early returns rather than large if in Variable#taint_check 2015-07-07 15:56:03 -04:00
Dylan Thacker-Smith
80b6ac3bc7 Add taint warnings to the context rather than the template. 2015-07-07 15:53:02 -04:00
Dylan Thacker-Smith
15974d9168 Merge pull request #612 from Shopify/fix-block-body-naming
Use node to refer to objects from the nodelist rather than token.
2015-07-07 15:49:58 -04:00
Dylan Thacker-Smith
f22ab4358b Merge pull request #611 from Shopify/no-escape-rescue
Remove standard exception rescue in escape filter.
2015-07-07 15:49:43 -04:00
Justin Li
9cf0d264e1 Require RuboCop v0.32.0 or later 2015-07-06 15:58:36 -04:00
Justin Li
575e3cae7a Remove class length metric cop 2015-07-06 15:52:11 -04:00
Dylan Thacker-Smith
fad3b8275c Use node to refer to objects from the nodelist rather than token. 2015-07-04 20:57:35 -04:00
Dylan Thacker-Smith
5a071cb7f2 Remove standard exception rescue in escape filter. 2015-07-04 13:48:25 -04:00
Justin Li
8cb2364179 Merge pull request #608 from Shopify/tag-tag_name
Add Liquid::Tag#tag_name
2015-07-02 16:28:37 -04:00
Gaurav Chande
3c23cfc167 Add Liquid::Tag#tag_name 2015-07-02 20:18:09 +00:00
Justin Li
8a8de46c6a Merge pull request #603 from Shopify/format-history
Format changelog attribution to include one name only
2015-06-23 07:40:05 -07:00
Justin Li
58c7f226cc Format changelog attribution to include one name only 2015-06-19 11:45:37 -04:00
Justin Li
adfcd0ab13 Update history to reflect merge of #600
[ci skip]
2015-06-19 11:38:59 -04:00
Justin Li
30ef7d14b0 Merge pull request #600 from carsonreinke/filter-compact
Merge pull request 600
2015-06-19 11:38:14 -04:00
Florian Weingarten
4920ec50e4 update changelog 2015-06-19 07:41:39 -04:00
David Cornu
e395229283 Merge pull request #601 from Shopify/safe-to-integer
Use to_integer instead of to_i on arguments
2015-06-16 11:31:20 -04:00
David Cornu
9470fba0c8 Exclude lib/liquid/standardfilters.rb from ModuleLength 2015-06-16 15:19:06 +00:00
David Cornu
ac180e8402 Use to_integer instead of to_i on arguments 2015-06-16 15:08:29 +00:00
Carson Reinke
7c5d54aced Ignore Rubocop Metrics/ModuleLength for now 2015-06-15 15:07:25 -04:00
Carson Reinke
5fbb312a67 "Trailing whitespace detected." 2015-06-15 14:27:48 -04:00
Carson Reinke
8385099960 Added "compact" filter 2015-06-15 14:14:28 -04:00
Florian Weingarten
504b6fb3c7 Merge pull request #596 from Shopify/liquid_c_tests
Run tests with latest liquid/c gem
2015-06-08 22:52:57 +02:00
Florian Weingarten
01420e8014 fix gem platforms 2015-06-08 18:38:40 +00:00
Florian Weingarten
dde35a2907 shut up rubocop 2015-06-08 18:38:40 +00:00
Florian Weingarten
e2323332cd Run tests with latest liquid/c gem 2015-06-08 18:38:35 +00:00
Florian Weingarten
7b4398d0c4 Merge pull request #595 from Shopify/uniq_on_strings
Fix uniq filter with string input
2015-06-05 16:27:05 +02:00
Florian Weingarten
1e23036b2d Fix uniq filter with string input 2015-06-04 22:55:03 -04:00
Florian Weingarten
13716fa68b Merge pull request #594 from boobooninja/rake_console
add rake console
2015-06-05 04:21:01 +02:00
Loren Hale
232e8bb4cd add rake console
add Rake console task to load irb with liquid
2015-06-05 10:17:55 +08:00
Dylan Thacker-Smith
6968def5dd Merge pull request #574 from Shopify/template-name-in-errors
Include template name with line numbers in render errors.
2015-06-04 15:28:12 -04:00
Dylan Thacker-Smith
ad3748af21 Include template name with line numbers in render errors. 2015-06-04 13:44:01 -04:00
Florian Weingarten
c82e04f4e6 Merge pull request #593 from Shopify/fix_predicate_name
Rename 'has_key?' and 'has_interrupt?'
2015-06-04 19:40:14 +02:00
Florian Weingarten
5919626da4 Rename 'has_key?' and 'has_interrupt?' 2015-06-04 13:14:46 -04:00
Florian Weingarten
82269e2509 fix a few more rubocop offenses 2015-06-04 13:09:58 -04:00
Florian Weingarten
b347fac3c0 Merge pull request #592 from Shopify/method_literal
blank and empty as variable names
2015-06-04 19:09:48 +02:00
Florian Weingarten
e761a6864e clean up some rubocop stuff 2015-06-04 12:56:29 -04:00
Florian Weingarten
4c22cef341 blank and empty as variable names 2015-06-04 12:30:50 -04:00
Florian Weingarten
c319240174 run rubocop on CI 2015-06-04 11:57:25 -04:00
Florian Weingarten
6ace095207 Avoid parallel assignments 2015-06-04 11:56:47 -04:00
Florian Weingarten
e36f366c33 gitignore .bundle 2015-06-04 11:56:00 -04:00
Florian Weingarten
02729e89c0 make rubocop happy 2015-06-04 11:56:00 -04:00
Gaurav Chande
6b0f6401d0 Merge pull request #590 from Shopify/allow-template-tags
Local Tags
2015-06-04 11:19:24 -04:00
Gaurav Chande
fc8e6c8d3a Change Tokenizer test to fetch tokens instead of exposing ivar 2015-06-04 15:10:01 +00:00
Gaurav Chande
79d7dd06df Extract tag fetching into a method (which can be overriden then) 2015-06-04 04:39:54 +00:00
Gaurav Chande
3a907a4db7 Move DEFAULT_OPTIONS related logic to Document 2015-06-04 04:39:54 +00:00
Gaurav Chande
8b98f92c7f Extract tokenize logic from Template to a RubyTokenizer 2015-06-04 04:39:30 +00:00
Dylan Thacker-Smith
b79c0c611c Merge pull request #586 from Shopify/string-contains-non-string
Avoid an exception from checking if a string contains a non-string.
2015-06-03 10:58:38 -04:00
Dylan Thacker-Smith
8a2947865b Avoid an exception from checking if a string contains a non-string. 2015-06-03 02:21:51 -04:00
Dylan Thacker-Smith
ea29f8b4b8 Merge pull request #583 from Shopify/slice-nil-offset
Raise a Liquid::ArgumentError in slice filter for invalid integers.
2015-06-03 01:43:56 -04:00
Dylan Thacker-Smith
c84f4520cc Keep input out of error message and add test for slice Integer parsing. 2015-06-03 01:35:01 -04:00
Dylan Thacker-Smith
3dd6433e2f Merge pull request #584 from Shopify/replace-non-string
Convert arguments to replace filters to strings to avoid exceptions.
2015-06-02 16:41:15 -04:00
Dylan Thacker-Smith
ab7109a335 Raise a Liquid::ArgumentError in slice filter for invalid integers. 2015-06-02 16:05:08 -04:00
Dylan Thacker-Smith
94fe050952 Convert arguments to replace filters to strings to avoid exceptions. 2015-06-02 15:59:29 -04:00
Justin Li
9b98c436c4 Merge pull request #582 from Shopify/require-empty-raw-tag
Ensure raw tag has no arguments
2015-06-02 15:58:15 -04:00
Justin Li
889019f53a Keep old test as well 2015-06-02 15:21:51 -04:00
Justin Li
c290375aec Remove unnecessary regex options 2015-06-02 15:17:36 -04:00
Justin Li
719a98a25e Ensure raw tag has no arguments 2015-06-02 14:32:39 -04:00
Justin Li
86d8b552da Merge pull request #581 from Shopify/require-closed-raw-tag
Raise SyntaxError if raw tag is unclosed
2015-06-02 11:38:45 -04:00
Justin Li
b1ee9129e7 Raise SyntaxError if raw tag is unclosed 2015-06-02 10:56:51 -04:00
Justin Li
be2e41e4d5 Merge pull request #579 from Shopify/ast-match
Ensure For@reversed is a boolean
2015-05-28 16:45:09 -04:00
Justin Li
20ca2b9632 Update history to reflect merge of #570
[ci skip]
2015-05-28 16:43:22 -04:00
Justin Li
6c058823ad Merge pull request #570 from Shopify/fix-strict-conditions
Fix condition parse order in strict mode
2015-05-28 16:33:54 -04:00
Dylan Thacker-Smith
27245c9eab Merge pull request #577 from Shopify/table-row-blank-string-collection
Fix exception from using an empty string for the table row collection.
2015-05-28 16:20:42 -04:00
Justin Li
a639a13380 Use cleaner recursive solution 2015-05-28 16:16:30 -04:00
Justin Li
05a0fe56c8 Ensure For@reversed is a boolean 2015-05-28 16:09:26 -04:00
Dylan Thacker-Smith
c1eb694057 Remove the redundant iterable check in the for tag.
Just do it in slice_collection for consistency with the tablerow tag.
2015-05-28 16:04:50 -04:00
Dylan Thacker-Smith
f53b31c867 Merge pull request #578 from Shopify/filter-error-handling
Handle some more standard filter errors.
2015-05-28 15:00:57 -04:00
Dylan Thacker-Smith
363388e92f Handle some more standard filter errors. 2015-05-28 14:18:53 -04:00
Dylan Thacker-Smith
873eddbb85 Split a line and use String#empty? for readability 2015-05-28 12:55:04 -04:00
Dylan Thacker-Smith
e790b60f60 Fix exception from using an empty string for the table row collection. 2015-05-28 12:11:39 -04:00
Dylan Thacker-Smith
3264d60425 Merge pull request #576 from Shopify/flexible-exception-handler
Allow the exception handler to convert exceptions to hide error messges
2015-05-28 11:38:44 -04:00
Dylan Thacker-Smith
8ff1b8e01f Test set_line_number_from_token after exception is converted. 2015-05-28 09:22:02 -04:00
Dylan Thacker-Smith
8d5e71f856 Allow the exception handler to convert exceptions to hide error messages. 2015-05-27 18:59:51 -04:00
Dylan Thacker-Smith
89c6e605f8 Merge pull request #575 from Shopify/zero-division-error
Raise Liquid::ZeroDivisionError instead of ZeroDivisionError.
2015-05-26 10:43:23 -04:00
Dylan Thacker-Smith
6265c36ec9 Raise Liquid::ZeroDivisionError instead of ZeroDivisionError. 2015-05-25 15:40:17 -04:00
Dylan Thacker-Smith
8af99ff918 Merge pull request #573 from Shopify/optional-error-rendering
Make liquid error rendering optional.
2015-05-25 12:11:10 -04:00
Dylan Thacker-Smith
36200ff704 Make liquid error rendering optional.
Although the author of the liquid template wants to see these errors, they
probably don't want the visitor to see the liquid errors.  Probably the
best fallback when rendering the page for visitors is to render the empty
string for tags with errors.
2015-05-25 11:24:53 -04:00
Justin Li
a9c7df931f Strict parse conditions in reverse order 2015-05-19 11:51:01 -04:00
Justin Li
070639daba Push to for_stack at the beginning of For#render 2015-05-15 23:13:15 -04:00
Justin Li
dad98cfc89 Merge pull request #562 from Shopify/use-find_variable-for-parentloop
Use custom stack for forloop references
2015-05-15 21:48:57 -04:00
Florian Weingarten
1d3c0b3dab Merge pull request #568 from Shopify/remove_liquid_methods
Remove support for `liquid_methods` Module extension
2015-05-14 22:19:02 +02:00
Justin Li
648a4888af Pop the for_stack register in an ensure 2015-05-14 15:02:20 -04:00
Justin Li
b4e5017c79 Add truth table test for multiple if conditions 2015-05-14 14:11:03 -04:00
Justin Li
f1bc9f27df Include message in assert_template_result 2015-05-14 14:10:45 -04:00
Florian Weingarten
f4724f0db3 Remove support for liquid_methods Module extension 2015-05-14 14:44:19 +00:00
Florian Weingarten
df74955ac4 Merge pull request #564 from Shopify/rubocop
Rubocop
2015-05-14 16:41:32 +02:00
Florian Weingarten
3372ca8136 Rubocop 2015-05-14 14:37:18 +00:00
Jean Boussier
8cf524e91c Merge pull request #565 from Shopify/file-dirname
Modernize code base with __dir__ and require_relative
2015-05-13 15:45:22 -04:00
Jean Boussier
5e38626309 Force circle in ruby 2.0 2015-05-13 15:40:34 -04:00
Jean Boussier
b31df0fb3d Mordernize code base with __dir__ and require_relative 2015-05-13 15:33:00 -04:00
Florian Weingarten
9e815ec594 Merge pull request #563 from Shopify/webscale_exceptions
Prefer Class.new() where possible
2015-05-13 06:06:35 +02:00
Florian Weingarten
93b29b67ef Prefer Class.new() where possible 2015-05-13 02:47:43 +00:00
Justin Li
863e8968f0 Use extra stack for forloop references 2015-05-12 17:04:34 -04:00
Justin Li
4c9d2009f9 Add find_own_variable method to look up internal context variables 2015-05-12 16:49:39 -04:00
Justin Li
239cfa5a44 Use find_variable for parentloop 2015-05-12 16:11:32 -04:00
Justin Li
8a8996387b Update history to reflect merge of #554
[ci skip]
2015-05-12 13:20:06 -04:00
Justin Li
9310640bdd Merge pull request #554 from arthanzel/529-sort_natural
Merge pull request 554
2015-05-12 13:19:24 -04:00
Justin Li
4c3381a523 Update history to reflect merge of #559
[ci skip]
2015-05-12 10:59:58 -04:00
Justin Li
261aa2e726 Merge pull request #559 from Shopify/fix-include-var
Merge pull request 559
2015-05-12 10:50:13 -04:00
Justin Li
247c51ac70 Call Context#find_variable directly 2015-05-11 18:22:15 -04:00
Justin Li
37dbec3610 Remove unnecessary parse 2015-05-11 18:10:38 -04:00
Justin Li
ff253a04c6 Lazily evaluate template name for context variable injection 2015-05-11 18:01:24 -04:00
Justin Li
25ef0df671 Add tests for #461 2015-05-11 17:59:05 -04:00
Martin Hanzel
32460c255b Removed a few superfluous comments 2015-05-08 11:48:33 -04:00
Justin Li
724d625f47 Update history to reflect merge of #555 [ci skip] 2015-05-07 14:03:38 -04:00
Justin Li
f658dcee8b Merge pull request #555 from boobooninja/date_filter
Merge pull request 555
2015-05-07 13:59:22 -04:00
Loren Hale
fa6cd6287e date filter gracefully accepts empty string 2015-05-07 17:04:21 +08:00
Justin Li
76c24db039 Remove ruby-head from allowed failures 2015-05-05 12:49:04 -04:00
Martin Hanzel
068791d698 Added method parens 2015-05-05 11:49:14 -04:00
Martin Hanzel
3a082ddbbd Changed sort_natural filter to use casecmp. Strings only. 2015-05-04 11:55:14 -04:00
Martin Hanzel
03b3446119 Resolves #529. Resolves #404. Added natural sorting filter and tests. 2015-05-03 20:55:28 -04:00
James Reid-Smith
251ce7483c Merge pull request #441 from Shopify/remove_context_from_read_template_file
Removed context from read_template_file, fixed tests to match new arity
2015-04-27 12:13:36 -04:00
James Reid-Smith
4592afcc8b Updated History.md and removed a couple remaining methods using the old signature 2015-04-27 15:45:44 +00:00
James Reid-Smith
448766b0c4 Removed context from read_template_file, fixed tests to match new arity 2015-04-27 15:27:03 +00:00
Justin Li
6390652c3f Update changelog with backported patches 2015-04-24 16:09:37 -04:00
Justin Li
f266aee2e5 Slightly more concise issue# reference in changelog 2015-04-21 23:40:42 -04:00
Justin Li
df0649a031 Update changelog 2015-04-21 23:36:54 -04:00
Justin Li
78a5972487 Merge pull request #541 from Shopify/history-sync
Sync History.md for Liquid 4
2015-04-21 23:34:29 -04:00
Justin Li
298ae3357c Merge pull request #551 from Shopify/expose-variable-name
Merge pull request 551
2015-04-21 23:33:13 -04:00
Justin Li
f1f3f57647 Remove command_lookups reader 2015-04-21 00:25:51 -04:00
Justin Li
e5dd63e1fc Expose name, lookups, and command flags from VariableLookup 2015-04-20 17:36:04 -04:00
Justin Li
881f86d698 Merge pull request #550 from Shopify/minitest-fail-workaround
Disable minitest expectation interface due to reckless modification of Object
2015-04-20 10:22:19 -04:00
Justin Li
a1b209d212 Disable minitest expectation interface due to reckless modification of Object 2015-04-20 10:15:19 -04:00
Thierry Joyal
8e5926669b Merge pull request #545 from Shopify/explode-invokable_methods-on-drop
Explode invokable_methods method on Liquid::Drop
2015-04-02 09:02:48 -07:00
Thierry Joyal
8736b602ea Explode invokable_methods method on Liquid::Drop 2015-04-02 13:16:07 +00:00
Justin Li
b8365af07d Add changes for 4.0.0 2015-03-25 14:53:43 -04:00
Justin Li
53842a471e Create history section for 4.0.0 2015-03-25 14:40:19 -04:00
Justin Li
86a82d3039 Merge pull request #540 from Shopify/array-concat
Add array concat filter
2015-03-25 01:42:22 -04:00
Justin Li
2b78e74b4e Add test for concat filter with non-array input 2015-03-25 01:34:47 -04:00
divecch
db396dd739 adding concat filter to append arrays 2015-03-25 01:31:22 -04:00
Justin Li
3213db54d6 Merge pull request #520 from Shopify/forloop-parentloop
Add forloop.parentloop as a reference to the parent loop
2015-03-25 01:22:35 -04:00
Justin Li
97a3f145a1 Merge pull request #499 from kreynolds/to_date_downcase_regression
Fix case sensitivity regression in date standard filter
2015-03-25 01:22:04 -04:00
Florian Weingarten
2fbe813770 Merge pull request #539 from Dorian/patch-1
Update module_ex.rb code documentation and code style
2015-03-24 15:21:22 +01:00
Dorian Marié
23a23c6419 Update module_ex.rb code documentation and code style
Didn't look good on rubydoc.info: http://i.imgur.com/469N92P.png
2015-03-24 14:09:08 +01:00
Dylan Thacker-Smith
63eb1aac69 Merge pull request #519 from Shopify/remove-filter-method-blacklist
Allow filters to redefine Object methods to make them invokable.
2015-02-04 18:07:51 -05:00
Justin Li
205bd19d3f Add forloop.parentloop as a reference to the parent loop 2015-02-04 12:43:09 -05:00
Dylan Thacker-Smith
950f062041 Allow filters to redefine Object methods to make them invokable. 2015-02-03 13:51:33 -05:00
Tobias Lütke
3476a556dd Merge pull request #512 from Shopify/fix_tobi_name
Fix Tobi last name on gemspec
2015-01-23 21:24:04 -05:00
Arthur Neves
d2ef9cef10 master is 4.0.0 2015-01-23 10:49:07 -05:00
Arthur Neves
0021c93fef Add ruby 2.2 to travis
and allow failure on ruby head
2015-01-23 10:42:26 -05:00
Arthur Neves
dcf7064460 Fix Tobi last name on gemspec 2015-01-23 10:21:40 -05:00
Florian Weingarten
bebd3570ee Merge pull request #506 from Shopify/fix_capture_with_hyphen
Use VariableSignature as Syntax for Capture tag to allow hyphens in variable names
2015-01-10 23:27:00 -05:00
Florian Weingarten
7cfee1616a Use VariableSignature as Syntax for Capture tag to allow hyphens in variable names 2015-01-09 14:15:26 +00:00
Arthur Nogueira Neves
4b0a7c5d1d Merge pull request #504 from alfredxing/duplicate-keys
Remove duplicate `index0` key in TableRow tag
2014-12-30 13:15:10 -05:00
Alfred Xing
5df1a262ad Remove duplicate key in hash 2014-12-25 12:12:42 -08:00
Kelley Reynolds
84fddba2e1 Remove regex for downcase and is_a?(String) 2014-12-18 13:01:23 -05:00
Kelley Reynolds
8b0774b519 Fix case sensitivity regression in date standard filter 2014-12-16 10:37:05 -05:00
Justin Li
e2f8b28f56 Merge pull request #492 from Shopify/resource-counting-perf
Resource counting perf
2014-12-11 16:05:41 -05:00
Justin Li
3080f95a4f Make render_length tests stricter 2014-12-11 10:41:47 -05:00
Justin Li
cc57908c03 Add test for render_length persisting between block bodies 2014-12-11 10:38:47 -05:00
Justin Li
4df4f218cf Use same template instance 2014-12-09 17:25:15 -05:00
Justin Li
c2f71ee86b Reset resource consumption before each render 2014-12-09 17:23:07 -05:00
Justin Li
9f7e601110 Convert render output to strings in BlockBody 2014-12-05 15:17:09 -05:00
Justin Li
3755031c18 Merge pull request #485 from Shopify/lazy-load-profiler-hooks
Defer loading profiler hooks
2014-12-05 15:10:16 -05:00
Justin Li
b628477af1 Disambiguate checking if Liquid::Profiler is defined 2014-12-04 17:51:54 -05:00
Justin Li
dd455a6361 Force user to require the profiler themselves 2014-12-04 17:48:26 -05:00
Justin Li
8c70682d6b Don't automatically load hooks 2014-12-04 17:39:41 -05:00
Justin Li
742b3c69bb Remove commented code 2014-12-04 16:30:37 -05:00
Justin Li
1593b784a7 Simplify interface for setting template resource limits 2014-12-04 16:18:21 -05:00
Justin Li
db00ec8b32 Move resource limit tracking to its own class 2014-12-04 16:18:09 -05:00
Justin Li
3ca40b5dea Merge pull request #491 from Shopify/drop-ruby-1-9
Drop Ruby 1.9 from CI, add Ruby head
2014-12-03 12:52:10 -05:00
Justin Li
378775992f Drop Ruby 1.9 from CI, add Ruby head 2014-12-02 14:33:51 -05:00
Florian Weingarten
319400ea23 Merge pull request #489 from alex-ross/patch-1
Fixes syntax error in documentation for unless tag
2014-11-19 14:02:58 +01:00
Alexander Ross
289a03f9d7 Fixes syntax error in documentation for unless tag 2014-11-19 10:49:58 +01:00
Justin Li
a0710f4c70 Merge pull request #486 from Shopify/fix-exponential-warnings
Fix #warnings taking exponential time to compute
2014-11-12 17:22:16 -05:00
Justin Li
737be1a0c1 Use Timeout#timeout for warnings tests 2014-11-12 17:03:48 -05:00
Justin Li
1673098126 Handle potential case where warnings returns nil 2014-11-12 16:46:10 -05:00
Justin Li
422bafd66a Fix #warnings taking exponential time to compute 2014-11-12 16:12:00 -05:00
Justin Li
c0aab820ed Lazily load profiler hooks 2014-11-12 00:05:01 -05:00
Florian Weingarten
3321cffe08 Merge pull request #482 from joshk/patch-1
Use the new beta build env on Travis
2014-11-07 03:06:52 +01:00
Josh Kalderimis
f2772518b0 Use the new beta build env on Travis
job start in seconds, instead of 20-120 seconds
2014-11-07 14:54:21 +13:00
Justin Li
76ef675eb2 Merge pull request #481 from Shopify/fix-nil-blank
Coerce regex @blank output to a boolean
2014-11-06 13:03:15 -05:00
Justin Li
e5fd4d929f Coerce regex @blank output to a boolean 2014-11-05 20:44:06 -05:00
Justin Li
2e42c7be1f Merge pull request #480 from Shopify/number_variables
Add quirks test for variables with number prefixes
2014-11-05 12:05:21 -05:00
Justin Li
95b031ee04 Add quirks test for extra dots in ranges 2014-11-05 11:41:12 -05:00
Justin Li
4d97a714a9 Add quirks test for variables with number prefixes 2014-11-05 10:56:58 -05:00
Justin Li
aa182f64b4 Merge pull request #479 from Shopify/tweaks-for-c
Tweaks for C
2014-11-04 14:02:14 -05:00
Justin Li
4e870302b1 Add env var for saving stackprof graphviz output 2014-11-04 18:38:14 +00:00
Justin Li
098c89b5f5 Convenience methods for raising terminator syntax errors 2014-11-04 18:38:08 +00:00
Justin Li
70c45f8cd8 Use SVG badge URLs
[ci skip]
2014-11-03 17:41:42 -05:00
Justin Li
12d526a05c Merge pull request #458 from Shopify/block-body
Create a BlockBody class to decouple block body parsing from tags.
2014-11-03 17:34:39 -05:00
Dylan Thacker-Smith
2fd8ad08c0 Remove unused local variable that was accidentally added. 2014-11-03 17:07:42 -05:00
Dylan Thacker-Smith
15e1d46125 Avoid storing options instance variable in BlockBody.
There is no need to pass parse options to the BlockBody initializer, since
it does all the parsing in the parse method, unlike tags which parse the
tag markup in the initializer.
2014-11-03 17:07:42 -05:00
Dylan Thacker-Smith
73fcd42403 Create a BlockBody class to decouple block body parsing from tags. 2014-11-03 17:07:42 -05:00
Justin Li
263e90e772 Merge pull request #478 from Shopify/numbers-in-identifiers
Use a single token for identifiers
2014-10-30 21:59:26 -04:00
Justin Li
81770f094d Remove unnecessary + 2014-10-29 13:39:43 -04:00
Justin Li
dd5ee81089 Disallow number and dash identifier prefixes 2014-10-29 12:08:00 -04:00
Justin Li
a07e382617 Use a single token for identifiers 2014-10-29 11:28:41 -04:00
Justin Li
4dc682313f Merge pull request #476 from Shopify/missing-variable-name-error
Disallow filters with no variable in strict mode
2014-10-27 13:56:11 -04:00
Justin Li
5616ddf00e Remove obsolete comment 2014-10-27 13:44:14 -04:00
Justin Li
fcb23a4cd2 Disallow filters with no variable in strict mode 2014-10-27 13:34:27 -04:00
Justin Li
a8f60ff6b1 Merge pull request #472 from Shopify/fix-leaky-test
Fix test leaking error_mode, fix equality check for VariableLookup
2014-10-23 10:12:41 -04:00
Justin Li
a206c8301d Fix test leaking error_mode, fix equality check for VariableLookup 2014-10-22 15:40:41 -04:00
Justin Li
ee0de01480 Merge pull request #469 from Shopify/falsy-variable-fix
Fix case where a variable name is falsy
2014-10-21 15:06:34 -04:00
Justin Li
887b05e6ed Clarify test name 2014-10-21 14:06:30 -04:00
Justin Li
5d68e8803f Ensure nil works as a variable name 2014-10-21 14:03:10 -04:00
Justin Li
dedd1d3dc0 Fix case where a variable name is falsy 2014-10-21 12:09:26 -04:00
Dylan Thacker-Smith
d9ae36ec40 Merge pull request #466 from Shopify/remove-expression-cache
Remove expression cache
2014-10-20 13:57:17 -04:00
Dylan Thacker-Smith
b9ac3fef8f Remove the quotes from the partial string in the profiler timing objects. 2014-10-18 16:26:18 -04:00
Dylan Thacker-Smith
f5faa4858c Remove parsed expression cache. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
bc5e444d04 Use Expression.parse and Context#evaluate in the Include class. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
3a4b63f37e Use Expression.parse and Context#evaluate in the TableRow class. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
a1a128db19 Refactor Condition so that it takes a parsed expression. 2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
d502b9282a Use Expression.parse and Context#evaluate in the For class. 2014-10-18 15:03:36 -04:00
Dylan Thacker-Smith
fee8e41466 Use Expression.parse and Context#evaluate in the Cycle class. 2014-10-18 14:27:58 -04:00
Dylan Thacker-Smith
37260f17ff Use Expression.parse and Context#evaluate in the Condition class. 2014-10-18 14:27:58 -04:00
Florian Weingarten
2da9d49478 Merge pull request #465 from Shopify/avoid_multi_assigns
Avoid parallel assignments
2014-10-18 16:19:02 +02:00
Florian Weingarten
7196a2d58e Avoid parallel assignments 2014-10-18 13:58:32 +00:00
Justin Li
a056f6521c Merge pull request #463 from Shopify/stricter-identifiers
Separate ? and - into special tokens
2014-10-17 13:45:48 -04:00
Justin Li
de16db9b72 Don't allow - to end a variable name 2014-10-17 13:38:07 -04:00
Justin Li
b4ea483c4e Separate ? and - into special tokens 2014-10-17 13:30:54 -04:00
Justin Li
7843bcca8d Merge pull request #443 from Shopify/completely-parse-variables
Parse expressions in Liquid::Variable#parse.
2014-10-17 13:12:46 -04:00
Florian Weingarten
76ea5596ff Merge pull request #462 from Shopify/flat_map
nodelist flat_map over map.flatten
2014-10-17 18:32:00 +02:00
Florian Weingarten
f9318e8c93 flat_map 2014-10-17 16:11:12 +00:00
Florian Weingarten
71253ec6f9 Merge pull request #459 from Shopify/pop_vs_shift
Use pop over shift to avoid reverse
2014-10-15 21:42:53 +02:00
Florian Weingarten
0fa075b879 Use pop over shift to avoid reverse 2014-10-15 19:26:39 +00:00
Dylan Thacker-Smith
6d080afd22 Merge pull request #446 from Shopify/remove-end-tag
Remove unused Block#end_tag method.
2014-10-14 03:03:31 -04:00
Dylan Thacker-Smith
a67e2a0a00 Remove unused Block#end_tag method.
Although the method is called, it is defined with an empty body and not
overridden to do anything else.
2014-10-14 02:58:11 -04:00
Dylan Thacker-Smith
f387508666 Parse expressions in Liquid::Variable#parse. 2014-10-08 21:06:59 -04:00
Florian Weingarten
632b1fb702 Merge pull request #455 from Shopify/parse_error_line_numbers
Line numbers for all parse errors
2014-10-04 17:53:30 +02:00
Dylan Thacker-Smith
d84870d7a5 Test line number of errors in nested blocks. 2014-10-03 16:25:12 -05:00
Florian Weingarten
584b492e70 Line numbers for all parse errors 2014-10-03 21:00:31 +00:00
Dylan Thacker-Smith
b79c9cb9bf Merge pull request #453 from Shopify/no-modify-default-resource-limit
Avoid modifying the default resources limits hash.
2014-10-01 19:02:09 -05:00
Dylan Thacker-Smith
cf5ccede50 Avoid modifying the default resources limits hash. 2014-10-01 18:51:06 -05:00
Evan Huus
23622a9739 Merge pull request #440 from Shopify/drop-tainting
Variable tainting
2014-09-22 13:43:35 -04:00
Florian Weingarten
7ba5a6ab75 Merge pull request #450 from Shopify/additional_test_for_includes
Regression test for including assignments
2014-09-18 21:05:37 +02:00
Florian Weingarten
be3d261e11 Regression test for including assignments 2014-09-18 10:37:44 +00:00
Evan Huus
eeb061ef44 Address code review comments
- clean up comment wording
- fix potentially leaky tests
2014-09-16 17:23:26 +00:00
Evan Huus
67b2c320a1 Add tainting tests 2014-09-16 17:23:26 +00:00
Evan Huus
1d151885be Auto-untaint variables passed through 'escape' 2014-09-16 17:23:26 +00:00
Evan Huus
e836024dd9 Check and handle when a tainted variable is used 2014-09-16 17:23:26 +00:00
Dylan Thacker-Smith
638455ed92 Merge pull request #448 from Shopify/remove-unused-filter-not-found-error
Remove Liquid::FilterNotFoundError since it is never raised.
2014-09-15 17:43:33 -04:00
Dylan Thacker-Smith
b2a74883e9 Remove Liquid::FilterNotFoundError since it is never raised. 2014-09-15 17:42:07 -04:00
Dylan Thacker-Smith
6875e5e16f Merge pull request #449 from Shopify/fix-flaky-total-render-time-test
Fix flaky test which assumes total_render_time can't be 0.
2014-09-15 17:41:41 -04:00
Dylan Thacker-Smith
a5717a3f8d Fix flaky test which assumes total_render_time can't be 0.
jruby has millisecond precision for Time.now, so total_render_time can be 0
due to this lack of precision.
2014-09-15 17:26:55 -04:00
Dylan Thacker-Smith
804fcfebd1 Merge pull request #444 from Shopify/remove-block-children
Avoid keeping track of two lists of nodes during parsing.
2014-09-15 09:56:08 -04:00
Dylan Thacker-Smith
b37ee5684a Merge pull request #445 from Shopify/prefer-super-over-render-all
Use super rather than render_all in single block render classes.
2014-09-15 09:53:54 -04:00
Dylan Thacker-Smith
0573b63b4c Use super rather than render_all in single block render classes. 2014-09-12 16:58:07 -04:00
Dylan Thacker-Smith
29c21d7867 Avoid keeping track of two lists of nodes during parsing. 2014-09-12 16:43:00 -04:00
Dylan Thacker-Smith
478eb893a9 Merge pull request #439 from Shopify/default-resource-limits
Make it easy to set default resource limits.
2014-09-11 14:27:52 -04:00
Dylan Thacker-Smith
eae29f8c48 Make it easy to set default resource limits. 2014-09-11 13:54:30 -04:00
Florian Weingarten
4004cb63a5 Merge pull request #419 from Shopify/liquid_error_line_numbers
Optional line numbers for liquid errors
2014-09-08 23:15:53 +02:00
Jason Hiltz-Laforge
aafdf4adb0 Fix JRuby builds 2014-09-08 20:41:22 +00:00
Florian Weingarten
debac5dd0b Revert "move line number check"
This reverts commit 939365c234.

Conflicts:
	lib/liquid/template.rb
2014-09-06 10:21:17 -04:00
Florian Weingarten
ce06ed4bb1 merge conflicts 2014-09-05 14:16:20 +00:00
Florian Weingarten
939365c234 move line number check 2014-09-05 14:12:30 +00:00
Florian Weingarten
c60fd0715d remove unnecessary nil 2014-09-05 14:12:30 +00:00
Florian Weingarten
c83e1c7b6d prefix for Liquid::Error instances 2014-09-05 14:12:30 +00:00
Florian Weingarten
aabbd8f1a1 remove unnecessary method 2014-09-05 14:12:30 +00:00
Florian Weingarten
60d8a213a5 Clean up Liquid::Error#render 2014-09-05 14:12:30 +00:00
Florian Weingarten
17cc8fdbb3 put line number in parentheses 2014-09-05 14:12:30 +00:00
Tristan Hume
27c1019385 Add line numbers to warnings 2014-09-05 14:12:30 +00:00
Tristan Hume
3a0ee6ae91 Remove parser switching duplication 2014-09-05 14:12:29 +00:00
Florian Weingarten
5eff375094 Optional line numbers for liquid errors 2014-09-05 14:12:29 +00:00
Tristan Hume
2df643ba18 Merge pull request #425 from Shopify/pass-options-include
Pass options through on include
2014-08-26 13:40:06 -04:00
Tristan Hume
68af2d6e2a Pass options to include tags 2014-08-26 10:50:25 -04:00
Arthur Nogueira Neves
dfb6c20493 Merge pull request #423 from bogdan/contains-with-integer
Fixed condition constains operator with wrong data type
2014-08-18 14:58:08 -04:00
Bogdan Gusiev
4e9d414fde Fixed condition constains operator with wrong data type
"contains" operator on wrong data type should not cause NoMethodError.
2014-08-18 17:32:29 +03:00
Florian Weingarten
c0ec0652ae Merge pull request #421 from djreimer/url-encode-filter
Add url_encode standard filter
2014-08-15 20:05:39 +02:00
Derrick Reimer
f8c3cea09b Add url_encode filter to history 2014-08-15 11:03:06 -07:00
Derrick Reimer
0b847e553c Add url_encode standard filter 2014-08-15 08:45:40 -07:00
Florian Weingarten
c2663258be Merge pull request #364 from collectiveidea/instrument-rendering-with-hooks
Profiling the rendering of a liquid template
2014-08-13 23:04:29 +02:00
Tristan Hume
d4654d0062 Merge pull request #417 from Shopify/simplify-regex
Simplify Variable Parsing Regexes
2014-08-13 12:07:35 -04:00
Tristan Hume
ffd4f9d959 Simplify secondary filter regex 2014-08-13 09:36:02 -04:00
Tristan Hume
292161865d Simplify filter parse regex 2014-08-13 09:28:01 -04:00
Florian Weingarten
35808390ee Merge pull request #414 from Shopify/to_liquid_context
Call to_liquid in Context invoke
2014-08-12 22:05:52 +02:00
Florian Weingarten
1678c07548 Call to_liquid in Context invoke 2014-08-12 19:54:12 +00:00
Jason Roelofs
173a58d36a Profile liquid rendering
Add a simple profiling system to liquid rendering. Each
liquid tag ({{ }} and {% %}) is processed through this profiling,
keeping track of the partial name (in the case of {% include %}), line
number, and the time it took to render the tag. In the case of {%
include %}, the profiler keeps track of the name of the partial and
properly links back tag rendering to the partial and line number for
easy lookup and dive down. With this, it's now possible to track down
exactly how long each tag takes to render.

These hooks get installed and uninstalled on an as-need basis so by
default there is no impact on the overall liquid execution speed.
2014-08-12 15:37:21 -04:00
Tristan Hume
f31e309770 Merge pull request #416 from Shopify/filter-quirks
Make Filter Quirks Tests Actual Integration Tests
2014-08-12 10:08:05 -04:00
Tristan Hume
ffe1036e15 Make tests actual integration tests 2014-08-12 09:27:46 -04:00
Dylan Thacker-Smith
d3b113d2e1 Merge pull request #391 from Shopify/extract-context-parse
Separate expression parsing and rendering from Context#[]
2014-08-11 14:17:54 -07:00
Dylan Thacker-Smith
2aa9bbbac2 Separate expression parsing and rendering from Context#resolve. 2014-08-11 14:15:58 -07:00
Tristan Hume
d5e57a8ea4 Merge pull request #412 from Shopify/assign-strict
Pass through options on assign tag
2014-08-11 15:37:49 -04:00
Florian Weingarten
5c0e0be639 Merge pull request #402 from Shopify/benchmark-ips
benchmark/ips
2014-08-11 21:22:43 +02:00
Florian Weingarten
a74d40f1e5 benchmark/ips 2014-08-11 19:22:06 +00:00
Tristan Hume
79d4ec1a48 Merge pull request #413 from Shopify/filter-quirks
Add quirks test for unanchored filter args
2014-08-11 13:06:51 -04:00
Tristan Hume
4db22be8ba Add tests for assign tag fix 2014-08-11 13:06:01 -04:00
Tristan Hume
dc58a4d648 Add quirks test for unanchored filter args 2014-08-11 11:58:36 -04:00
Tristan Hume
2809ec780a Pass through options on assign tag 2014-08-11 10:38:36 -04:00
Jean Boussier
2d98392bf5 Merge pull request #411 from Shopify/to-s-before-split
Cast input to string before spliting
2014-08-08 00:13:35 -04:00
Jean Boussier
df6b442816 Cast input to string before spliting 2014-08-07 14:01:44 -04:00
Florian Weingarten
4b22fc8d1b Merge pull request #407 from Shopify/slice_arrays
Slice filter for arrays
2014-08-05 20:00:45 +02:00
Florian Weingarten
fb6f9c1c13 Slice filter for arrays. 2014-08-05 17:59:31 +00:00
Florian Weingarten
66ae7f3ec0 Merge pull request #406 from Shopify/slice_filter
slice filter
2014-08-05 17:14:01 +02:00
Florian Weingarten
0bea31d2ef Use Integer() instead of to_i 2014-08-05 15:13:15 +00:00
Florian Weingarten
e5b0487fef Merge pull request #312 from Shopify/uniq_filter
uniq filter
2014-08-05 16:22:48 +02:00
Florian Weingarten
9117722740 Use symbols in respond_to? 2014-08-05 14:22:11 +00:00
Florian Weingarten
baea0a6bf7 slice filter 2014-08-04 16:47:08 +00:00
Tom Burns
17347d43de Merge pull request #400 from Shopify/lazy_stack
lazily create stacks
2014-07-30 11:43:31 -04:00
Tom Burns
794ca9f604 make the conditions around stack creation easier to read 2014-07-30 15:42:24 +00:00
Tom Burns
15f6cabf83 avoid a hash comparison 2014-07-30 15:12:22 +00:00
Tom Burns
e53d102a2c use 'unless' instead of 'if !' for simple conditional 2014-07-30 14:59:56 +00:00
Florian Weingarten
33e7b8e373 uniq filter 2014-07-29 13:09:34 +00:00
Florian Weingarten
9b8e3d437e Merge pull request #401 from Shopify/ktdreyer-minitest
Minitest 5 (continuation of #358)
2014-07-29 15:05:23 +02:00
Florian Weingarten
a2f0f2547d with_global_filter test helper 2014-07-28 19:28:22 +00:00
Ken Dreyer
57d5426eed tests: reset Strainer's filters after modification
Three tests in the test suite use the Liquid::Template.register_filter
function to register custom filters with Liquid::Strainer. The problem
is that these register_filter calls leave the Liquid::Strainer object in
an altered state.

As an example, the FiltersTest's test_local_filter relies on the default
behavior of Liquid::Strainer operator, and the test was failing if
register_function had been called earlier. The same thing was happening
with FiltersInTemplate's test_local_global.

The problem was present when the Filters test classes were loaded inside
a single ruby process that also loaded HashOrderingTest. One example is
"rake test", which runs "require" on every test file. Another basic
example is the following command:

  ruby -Itest -e "require 'integration/hash_ordering_test';
  require 'integration/filter_test'"

Update the tests to always reset Liquid::Strainer's filters back to the
default list of filters.

With this change, FiltersTest and FiltersInTemplate now pass.
2014-07-28 16:36:43 +00:00
Ken Dreyer
3e3a415457 tests: fix whitespace in hash_ordering_test
Indent two spaces, not one.
2014-07-28 16:36:43 +00:00
Ken Dreyer
deba039d6d tests: reset "contains" op during IfElseTagTest
Two tests in IfElseTagTest each set a custom operator function for the
"contains" comparison operator.

The problem is that IfElseTagTest was clobbering the original operator
in Liquid and leaving it in an altered state.

As an example, ConditionUnitTest's test_contains_works_on_arrays relies
on the specific behavior of the "contains" operator, and its
test_contains_works_on_arrays was failing.

The problem was present when both test classes were require'd inside a
single ruby process. One example is "rake test", which runs "require" on
every test file. Another basic example is the following command:

  ruby -Itest -e "require 'integration/tags/if_else_tag_test.rb';
  require 'unit/condition_unit_test.rb'"

This would cause test_contains_works_on_arrays to fail.

Update IfElseTagTest to avoid clobbering the "contains" operator.

With this change, ConditionUnitTest's test_contains_works_on_arrays now
passes.
2014-07-28 16:36:43 +00:00
Ken Dreyer
ee4295c889 tests: switch to minitest
Ruby 1.9+ uses Minitest as the backend for Test::Unit. As of Minitest 5,
the shim has broken some compatibility with Test::Unit::TestCase in some
scenarios.

Adjusts the test suite to support Minitest 5's syntax.

Minitest versions 4 and below do not support the newer Minitest::Test
class that arrived in version 5. For that case, use the
MiniTest::Unit::TestCase class as a fallback

Conflicts:
	test/integration/tags/for_tag_test.rb
	test/test_helper.rb
2014-07-28 16:36:38 +00:00
Tom Burns
f5e67a12f9 remove added newline in liquid.rb 2014-07-28 14:24:29 +00:00
Tom Burns
6b56bdd74f remove variables used for counting empty stacks 2014-07-28 14:23:16 +00:00
Tom Burns
ba6e3e3da6 lazily create stacks 2014-07-28 14:12:11 +00:00
Jason Hiltz-Laforge
a8e63ff03d Merge pull request #398 from Shopify/fix_order_of_constructor_initialize
Reorder constructor to avoid referencing uninitialized variable when environment contains a self-referencing proc
2014-07-24 15:04:36 -04:00
Jason Hiltz-Laforge
052ef9fcb8 Reorder constructor to avoid referencing uninitialized variable when environment contains a self-referencing proc 2014-07-24 18:58:23 +00:00
Arthur Neves
d07b12dc7d Update History log
Bring latest History from 2-6-stable and 2-5-stable
2014-07-24 11:01:19 -04:00
Arthur Nogueira Neves
32e4f2d3b1 Merge pull request #240 from Shopify/remove_flatten
remove .flatten on standard filters
2014-07-24 10:54:28 -04:00
Arthur Nogueira Neves
2cb1483d54 Merge pull request #397 from Shopify/bogdan-excetion-handling-for-humans
Excetion handling for humans (2)
2014-07-24 10:51:02 -04:00
Florian Weingarten
6c6350f18b Exception handling for humans
Ability to pass exception_handler as a block to #render
and provide whatever behavior you want on handling exceptions

https://github.com/Shopify/liquid/pull/254
2014-07-24 14:44:02 +00:00
Florian Weingarten
eae24373e6 remove unnecessary flatten filter 2014-07-24 02:56:57 +00:00
Jason Hiltz-Laforge
034a47a6cf Merge pull request #395 from Shopify/fix_block_delimiter
Forgot an error message case
2014-07-23 22:35:13 -04:00
Jason Hiltz-Laforge
51c1165f26 Forgot an error message case 2014-07-24 02:27:26 +00:00
Florian Weingarten
0b45ffeada add more legacy tests 2014-07-24 00:33:39 +00:00
Arthur Neves
b7b243a13d Fix regression on map 2014-07-23 17:16:21 -04:00
Arthur Neves
18e8ce1eb0 add flatten filter 2014-07-23 17:16:20 -04:00
Florian Weingarten
994f309465 Fix broken standardfilter test 2014-07-23 17:15:39 -04:00
Arthur Neves
02d42a1475 Array is a Enumerable 2014-07-23 17:14:27 -04:00
Arthur Neves
d099878385 add a input iterator to standard filter 2014-07-23 17:14:27 -04:00
Arthur Neves
6a061cbe81 remove .flatten on standard filters 2014-07-23 17:14:26 -04:00
Arthur Nogueira Neves
c864a75903 Merge pull request #341 from curebit/comparation_argument_error
Raise Liquid::ArugmentError when condition has wrong usage
2014-07-23 17:03:31 -04:00
Jason Hiltz-Laforge
d6fdf86acd Merge pull request #393 from Shopify/fix_block_delimiter
Fixing regression from block delimiter enhancement
2014-07-23 16:24:24 -04:00
Jason Hiltz-Laforge
55597b8398 Fixing regression from block delimiter enhancement 2014-07-23 19:18:02 +00:00
Florian Weingarten
c75522026b Merge pull request #389 from Shopify/remove_unnecessary_blank_stuf
Remove unnecessary blank? code
2014-07-23 15:07:26 +02:00
Florian Weingarten
1e0e9f1f31 Remove unnecessary blank? code 2014-07-22 21:19:12 +00:00
Jason Hiltz-Laforge
5fc1929b73 Merge pull request #384 from Shopify/optimize_block_parsing
Optimize block parsing -- don't recreate delimiter, use strings instead of regex
2014-07-22 12:59:05 -04:00
Jason Hiltz-Laforge
746a800475 Merge pull request #386 from Shopify/optimize_variable_parsing
Reduce temporary objects during variable/filter parsing
2014-07-22 11:22:57 -04:00
Jason Hiltz-Laforge
85dc7ef610 Merge pull request #385 from Shopify/optimize_scope_variable_resolution
Remove block in favour of for loop to reduce temporary object allocation during variable context resolution
2014-07-22 11:22:33 -04:00
Jason Hiltz-Laforge
bc3b066ba8 Remove block in favour of for loop to reduce temporary object allocation during variable context resolution 2014-07-22 14:54:50 +00:00
Jason Hiltz-Laforge
3c2de7737d Optimize block parsing -- don't recreate delimiter, use strings instead of regex 2014-07-22 02:43:20 +00:00
Jason Hiltz-Laforge
adb7d2bbb8 Reduce temporary objects during variable/filter parsing 2014-07-21 21:13:59 +00:00
Jason Hiltz-Laforge
0e56cf99ab Merge pull request #383 from Shopify/optimize_variable_lookup
Cache parsed markup parts to avoid repeated calls during template render
2014-07-21 12:29:58 -04:00
Jason Hiltz-Laforge
0df3f1c372 Cache parsed markup parts to avoid repeated calls during template render 2014-07-21 15:55:06 +00:00
Jason Hiltz-Laforge
44b9ad604f Merge pull request #381 from Shopify/add_object_profiling
Add object profiling in addition to cpu profiling
2014-07-16 16:41:31 -04:00
Jason Hiltz-Laforge
535d549978 Merge pull request #380 from Shopify/optimize_interrupt_handling
Optimize checking for interrupts by replacing any? with NOT empty?
2014-07-16 11:36:32 -04:00
Jason Hiltz-Laforge
32349033a9 Add object profiling in addition to cpu profiling 2014-07-16 15:32:04 +00:00
Jason Hiltz-Laforge
fd8c30070a Adding tests, spy dependency 2014-07-16 15:05:45 +00:00
Jason Hiltz-Laforge
4cfc05e32a Optimize checking for interrupts by replacing any? with NOT empty? 2014-07-15 18:31:40 +00:00
Florian Weingarten
c4bc6cf3db Merge pull request #378 from Shopify/revert_352
Revert: Add error messages for missing variables when :strict
2014-07-08 17:31:19 +02:00
Florian Weingarten
0ac3ec7834 Revert "Merge pull request #352 from gaiottino/master"
This reverts commit 553b0926ae, reversing
changes made to 628ab3dc6a.
2014-07-08 14:48:19 +00:00
Florian Weingarten
8909c9f27a add regression tests for #377 2014-07-08 14:47:39 +00:00
David Cornu
51c708c8f8 Merge pull request #376 from Shopify/tag-class-reloading
Avoid holding on to stale tag classes
2014-07-03 12:01:50 -04:00
David Cornu
f57383af37 Allow tag classes to be reloaded when using Liquid.cache_classes is false
Because Liquid keeps a reference to tag classes, Rails class reloading may
cause problems with custom tags. This commit introduces a setting that
allows these classes to be resolved when required.
2014-07-02 20:03:18 +00:00
Jean Boussier
d007c50856 Merge pull request #374 from Shopify/remove-duplicate-ruby-version
Give some ❤️ to travis.yml
2014-07-02 11:04:38 -04:00
Tobias Lütke
101f125a69 Merge pull request #375 from Shopify/relative-link
Fixed relative link
2014-06-30 17:19:37 -04:00
Tony Zou
5110ca906c Fixed relative link 2014-06-30 16:43:37 -04:00
Jean Boussier
ac0f63eda9 rbx-19mode is not supported anymore 2014-06-28 16:21:08 -04:00
Jean Boussier
1372274fca Do not intall stackprof under jruby nor rubinius 2014-06-28 16:09:14 -04:00
Jean Boussier
69951be173 Canonicalize travis ruby versions 2014-06-28 16:02:37 -04:00
Arthur Nogueira Neves
c9863836cd Merge pull request #371 from Shopify/unfreeze-version-string
Unfreeze version string
2014-06-24 09:59:46 -05:00
Jean Boussier
14b8d824d7 Unfreeze version string
It breaks gem installation on ruby 1.9.x (older rubygem?)
2014-06-23 10:47:10 -04:00
Florian Weingarten
114a37d9ba add additional tests for https://github.com/jekyll/jekyll/pull/2505 2014-06-23 09:28:24 -04:00
Arthur Nogueira Neves
30bd9ad957 Merge pull request #368 from Shopify/add-round-ceil-and-floor
[Liquid] Add round, ceil and floor standard filters
2014-06-16 10:32:17 -05:00
Christian Blais
2239921804 [Liquid] Add round, ceil and floor standard filters 2014-06-16 11:15:53 -04:00
Florian Weingarten
1ea178e7a8 Merge pull request #363 from rrrene/patch-2
Update docs badge in README
2014-06-05 10:32:47 -04:00
René Föhring
5650c7eea1 Update docs badge in README
Update the URL of the docs badge to include it from inch-ci.org instead of inch-pages.github.io (the former being the successor of the Inch Pages project).

[ci skip]
2014-06-05 12:25:26 +02:00
Arthur Neves
553b0926ae Merge pull request #352 from gaiottino/master
Add error messages for missing variables when :strict

Conflicts:
	History.md
2014-05-06 10:16:45 -04:00
Daniel Gaiottino
2bac6267f9 Add error messages for missing variables when :strict 2014-05-06 16:12:46 +02:00
Florian Weingarten
628ab3dc6a add test for numerical sort 2014-05-04 19:50:38 -04:00
Florian Weingarten
2eb552c65d Merge pull request #354 from Dillon-Benson/patch-3
use attr_writer instead of error_mode= method
2014-05-02 18:15:33 -04:00
Dillon Benson
6e40746ce4 use attr_writer instead of error_mode= method 2014-05-02 17:18:56 -04:00
Thierry Joyal
75068e8fa4 Merge pull request #349 from Shopify/render_bang_allow_not_safe_contexts
render! will properly force rethrow of errors if context is passed as an argument
2014-05-01 12:52:06 -04:00
Thierry Joyal
ad1152853a render! will properly force rethrow of errors if context is passed as an argument 2014-05-01 16:44:00 +00:00
David Cornu
73098ac5bc Merge pull request #351 from Shopify/date-cleanup
Move date coercion to #to_date
2014-04-30 20:47:39 -04:00
David Cornu
8bc3792c0e Move date coercion to #to_date 2014-04-30 22:32:36 +00:00
Florian Weingarten
3ef29c624c Merge pull request #347 from Dillon-Benson/patch-1
remove template return
2014-04-30 08:23:36 -04:00
Dillon Benson
a85fb38769 remove template return 2014-04-30 03:00:16 -04:00
Florian Weingarten
6a1c3cff1a update history.md 2014-04-29 15:26:22 -04:00
Florian Weingarten
bde32018dd Merge pull request #346 from Shopify/false_rendering
Fix broken rendering of variables which are equal to false (closes #345)
2014-04-29 15:21:30 -04:00
Florian Weingarten
2a12f253bf Fix broken rendering of variables which are equal to false (closes #345) 2014-04-29 14:33:30 -04:00
Bogdan Gusiev
fa14fd02e7 Raise Liquid::ArugmentError when condition has wrong usage
Condition now raises ::ArgumentError when built wrongly.
This patch make it raise Liquid::ArgumentError instead
to indicate a liquid markup error instead of ruby error.
2014-04-21 16:42:37 +03:00
Arthur Nogueira Neves
f15d24509d Merge pull request #338 from here/docs-filter-date-strftime
Update standardfilters.rb add docs to date filter
2014-04-12 20:14:44 -04:00
mikey dubs
09e4378cfb Update standardfilters.rb add docs to date filter
Added '%s' - Number of seconds since 1970-01-01 00:00:00 UTC to included list of flags
Added link to Ruby docs on Time#strftime() to allow easier discovery of unlisted filter options.
2014-04-12 15:21:06 -07:00
Dylan Thacker-Smith
af0e26fb16 Merge pull request #337 from Shopify/remove-rails-dev-dependancy
Remove active_support from the development dependancies.
2014-04-11 15:08:34 -04:00
Dylan Thacker-Smith
f5502e8152 Remove active_support from the development dependancies. 2014-04-11 14:26:40 -04:00
Arthur Nogueira Neves
c098235baa Merge pull request #335 from rrrene/patch-1
Add docs badge to README
2014-04-05 11:46:32 -04:00
René Föhring
0e2bf768ba Add docs badge to README 2014-04-05 12:24:56 +02:00
Dylan Thacker-Smith
4c477c2087 Merge pull request #333 from Shopify/remove-liquid-view
Remove ActionView template handler
2014-03-31 10:16:37 -04:00
Dylan Thacker-Smith
cd7fc050b1 Remove ActionView template handler
Fixes #21
2014-03-29 15:59:42 -04:00
Dylan Thacker-Smith
8291c5e72c Merge pull request #329 from Shopify/seperate-integration-tests
Separate unit and integration tests.
2014-03-26 16:04:39 -04:00
Dylan Thacker-Smith
7e45155aa9 Seperate unit and integration tests.
This makes it easier to re-use the integration tests in a seperate gem that
optimizes parts of liquid with a native implementation.
2014-03-26 15:47:07 -04:00
Florian Weingarten
3dbb35d823 Merge branch 'freeze_all_the_things'
Conflicts:
	History.md
	lib/liquid/tags/assign.rb
	lib/liquid/tags/capture.rb
	lib/liquid/tags/decrement.rb
	lib/liquid/tags/if.rb
2014-03-24 12:39:34 -04:00
Florian Weingarten
44f29a87a8 Update history 2014-03-24 12:35:47 -04:00
Dylan Thacker-Smith
f0afbc27e0 Add regression test for raw tags with open variable tags. 2014-03-24 10:01:03 -04:00
Dylan Thacker-Smith
fdf03076e0 Revert "Merge pull request #325 from Shopify/remove-variable-incomplete-end"
That pull request broke raw tags with open variable tags. E.g.

{% raw %}
{{
{% endraw %}
{{ 1 }}

This reverts commit fbaabf3b59, reversing
changes made to af24d2c2ab.
2014-03-24 09:59:07 -04:00
Dylan Thacker-Smith
d1bfda15e3 Add profile:strict rake task. 2014-03-21 21:54:53 -04:00
Dylan Thacker-Smith
d8d9984a7b Remove some unused regexes. 2014-03-21 15:50:14 -04:00
Dylan Thacker-Smith
fbaabf3b59 Merge pull request #325 from Shopify/remove-variable-incomplete-end
Allow quoted single curly braces in variables.
2014-03-21 13:48:47 -04:00
Dylan Thacker-Smith
7e0ef867d2 Make tag/variable termination error clearer. 2014-03-21 02:04:01 -04:00
Dylan Thacker-Smith
3682414cc4 Allow quoted single curly braces in variables. 2014-03-21 02:04:01 -04:00
Dylan Thacker-Smith
af24d2c2ab Add missing PR reference and author to a History.md entry. 2014-03-21 02:03:31 -04:00
Dylan Thacker-Smith
4ee43bc5d2 Merge pull request #324 from Shopify/multiline-tags-and-vars
Allow newlines in tags and variables.
2014-03-21 00:26:14 -04:00
Dylan Thacker-Smith
1320a69fca Merge pull request #323 from Shopify/render_bang_in_tests
Use render! in tests to make debugging test failures easier.
2014-03-20 18:33:40 -04:00
Dylan Thacker-Smith
3b14e27f55 Allow newlines in tags and variables. 2014-03-20 17:27:03 -04:00
Dylan Thacker-Smith
face33a137 Merge pull request #321 from Shopify/move-tag-parse-out-of-initialize
Refactor to create tags with a parse class method instead of new.
2014-03-20 16:15:58 -04:00
Dylan Thacker-Smith
d4ecaff8b8 Refactor to create tags with a parse class method instead of new.
By moving parse out of the initializer, we can call super at the start of
the initializers for subclasses, and avoid the nasty allocate hack.
2014-03-20 16:10:10 -04:00
Dylan Thacker-Smith
a5990042ff Use render! in tests to make debugging test failures easier. 2014-03-20 12:04:17 -04:00
Florian Weingarten
e190bbba9e move change to top 2014-03-19 18:15:31 -04:00
Dylan Thacker-Smith
4b5e41d04e Merge pull request #322 from Shopify/use-render-bang-in-benchmarks
Use render! in benchmarks to avoid making it faster by breaking things.
2014-03-19 18:06:15 -04:00
Dylan Thacker-Smith
4b69f6ae83 Use render! in benchmarks to avoid making it faster by breaking things. 2014-03-19 18:01:33 -04:00
Dylan Thacker-Smith
b9feb415f6 Merge pull request #320 from Shopify/move-table-row-tag
Move definition for TableRow to the tags folder.
2014-03-18 17:16:32 -04:00
Dylan Thacker-Smith
92781ec43b Move definition for TableRow to the tags folder. 2014-03-18 17:13:39 -04:00
Dylan Thacker-Smith
ff5c1f83f7 Merge pull request #318 from Shopify/stackprof
Use stackprof for profiling.
2014-03-14 10:27:46 -04:00
Dylan Thacker-Smith
e2b337af2f Merge pull request #317 from Shopify/string-anchors
Use start and end of string rather than line matching in regexes.
2014-03-14 10:17:33 -04:00
Dylan Thacker-Smith
f373b1003d Use stackprof for profiling. 2014-03-14 10:03:50 -04:00
Dylan Thacker-Smith
503d924274 Use start and end of string rather than line matching in regexes. 2014-03-13 17:56:42 -04:00
Dylan Thacker-Smith
3efa8e8762 Merge pull request #310 from Shopify/faster-increment-used-resources
Speed up Context#increment_used_resources
2014-02-25 00:18:27 -05:00
Dylan Thacker-Smith
3c06d837b5 Speed up Context#increment_used_resources 2014-02-24 23:56:39 -05:00
Arthur Nogueira Neves
d3fc30ef85 Merge pull request #309 from bogdan/argument_error_for_filters
Raise liquid argument error instead of ruby argument
2014-02-21 15:13:11 -05:00
Bogdan Gusiev
f23e69d565 Raise liquid argument error instead of ruby argument
Wrong number of arguments for filter invocation now raises
Liuqid::ArgumentError but not ::ArgumentError
2014-02-21 22:12:11 +02:00
Adam Doeler
fa179e811d Merge pull request #306 from Shopify/default_to_s_for_drops
Liquid::Drop should not return a string representation of standard ruby objects
2014-02-13 13:45:01 -05:00
Adam Doeler
18907fc570 Updates History.md 2014-02-10 10:24:37 -05:00
Adam Doeler
5f8a028a56 Liquid::Drop should not return a string representation of standard ruby objects 2014-02-07 14:48:02 -05:00
Florian Weingarten
0a7de51e2b Revert some freezes on non-literals 2014-01-27 14:56:07 -05:00
Florian Weingarten
765751b9cb Merge pull request #303 from Shopify/strip_filter
Add strip, lstrip, rstrip filters
2014-01-24 08:09:19 -08:00
Florian Weingarten
d2827bfa76 Add strip, lstrip, rstrip filters 2014-01-24 11:04:43 -05:00
Florian Weingarten
70d92b84ab Rename test 2014-01-24 10:55:45 -05:00
Florian Weingarten
808fa244ca Merge pull request #250 from wildfireapp/correct-if-nodelists
Correct if-statement nodelist.
2014-01-13 12:47:51 -08:00
Nicholas Jones
5570f697fd Update history 2014-01-13 12:46:43 -08:00
Nicholas Jones
8f9f12e542 Merge remote-tracking branch 'upstream/master' into correct-if-nodelists
Conflicts:
	test/liquid/tags/for_tag_test.rb
	test/liquid/tags/if_else_tag_test.rb
2014-01-13 12:43:43 -08:00
Florian Weingarten
17dae40707 Fix History.md ordering 2014-01-13 15:38:18 -05:00
Nicholas Jones
06e2f2577f Add else blocks to for and case nodelists 2014-01-13 11:53:25 -08:00
Florian Weingarten
ee7edacacc Merge pull request #298 from Shopify/respond_to_resource_counting_bug
Fix resource counting bug with respond_to?(:length)
2014-01-08 10:37:48 -08:00
Florian Weingarten
62a86a25ae update history 2014-01-08 13:37:24 -05:00
Florian Weingarten
c6e0c1e490 Fix resource counting bug with respond_to?(:length) 2014-01-08 13:00:53 -05:00
Florian Weingarten
43ac8d560b Freeze all the things 2014-01-07 12:35:16 -05:00
Arthur Nogueira Neves
0388376925 Merge pull request #296 from Shopify/ruby2.1.0
Ruby2.1.0
2014-01-07 08:55:03 -08:00
Arthur Neves
57c8583dc3 Add 2.1.0 to travis 2014-01-07 11:37:24 -05:00
Arthur Neves
a13f237d3c Remove some 192 tests 2014-01-07 11:37:01 -05:00
Arthur Nogueira Neves
9ed2fa425b Merge pull request #295 from Shopify/remove_ruby_debug
remove ruby-debug
2014-01-07 08:34:19 -08:00
Arthur Neves
208c6c8800 remove ruby-debug 2014-01-07 11:31:46 -05:00
Florian Weingarten
9ec2b9da2d Rename tests because of name clashes (same method name used twice) 2014-01-07 11:20:32 -05:00
Florian Weingarten
be7bef4d0b Merge pull request #284 from agladkyi/custom-patterns-for-template-filenames
Custom patterns for template filenames
2013-12-16 11:28:19 -08:00
Andrei Gladkyi
0ae42bbc32 Added separate test for custom patterns specifying
+ updated History.md
2013-12-16 17:48:43 +02:00
Gaurav Chande
0ec69890ab Merge pull request #287 from Shopify/fix-escape-once
Fix escape_once filter
2013-12-02 08:57:24 -08:00
Gaurav Chande
5e8f2f8bd0 Fix escape_once filter 2013-12-01 20:37:47 -05:00
Andrei Gladkyi
0edb252489 Option to specify custom pattern for template filenames 2013-11-30 17:55:53 +02:00
Florian Weingarten
5ce36f79e9 Add Tom's slice loading change to History.md 2013-11-25 11:12:33 -05:00
Florian Weingarten
2d1f15281b Merge pull request #282 from Shopify/load_slice
allow drops to optimize loading a slice of elements
2013-11-25 08:12:06 -08:00
Florian Weingarten
4647e6d86b Remove unnecessary comment, add joost's change to History.md 2013-11-25 10:52:46 -05:00
Florian Weingarten
f5620d4670 Merge branch 'master' of github.com:joost/liquid into joost-master 2013-11-25 10:51:48 -05:00
Florian Weingarten
f1a5f6899b Add raggi's change to History, remove Ruby 1.8 code from test 2013-11-25 10:48:03 -05:00
Florian Weingarten
de497eaed2 Merge branch 'class_cache' of github.com:wildfireapp/liquid into wildfireapp-class_cache 2013-11-25 10:46:18 -05:00
Tom Burns
30e5f06313 don't make original slice_collection_using_each private 2013-11-25 10:37:10 -05:00
Arthur Neves
d465d5e20c Add ruby 2.1 on travis 2013-11-25 10:35:24 -05:00
Arthur Neves
7989e834f3 Allow rbx failure and not 2.0.0 2013-11-25 10:33:41 -05:00
Arthur Neves
c264459931 Update history log
Conflicts:
	History.md
2013-11-25 10:28:12 -05:00
Tom Burns
e667352629 move slice_collection optimization to utils 2013-11-24 14:00:23 -05:00
Tom Burns
2c26a880f0 add another test showing equivalent functionality 2013-11-24 12:32:32 -05:00
Tom Burns
cf49b06ccc allow drops to optimize loading a slice of elements 2013-11-24 12:29:15 -05:00
Florian Weingarten
f015d804ea Update history 2013-11-11 09:03:39 -05:00
Arthur Nogueira Neves
78c42bce44 Update README.md
Show travis badge from master only.
2013-11-04 18:37:53 -05:00
Florian Weingarten
445f19d454 Merge pull request #276 from Shopify/remove_some_1.8_code
Remove some legacy Ruby 1.8 compatibility code
2013-11-01 05:50:29 -07:00
Florian Weingarten
a599a26f1a Remove some legacy Ruby 1.8 compatibility code 2013-10-31 15:35:12 -04:00
Dylan Thacker-Smith
4e14a651a7 Merge pull request #274 from Shopify/restrict-send-on-conditions
security: Prevent arbitrary method invocation on conditions in if tag.
2013-10-28 10:01:47 -07:00
Dylan Thacker-Smith
cc982e92d0 security: Prevent arbitrary method invocation on conditions in if tag. 2013-10-28 12:20:27 -04:00
Bouke van der Bijl
71a386f723 Merge pull request #273 from Shopify/make-if-less-dangerous
Make if less dangerous
2013-10-28 06:39:05 -07:00
Bouke van der Bijl
2f50a0c422 Add history message 2013-10-28 14:10:13 +01:00
Bouke van der Bijl
a5cd661dd9 Use public_send on condition creation
This makes sure you can't call Kernel methods like `throw`
2013-10-28 13:57:28 +01:00
Bouke van der Bijl
511ee7fbe1 Remove to_sym from condition creation
This prevents a DoS http://www.tricksonrails.com/2010/06/avoid-memory-leaks-in-ruby-rails-code-and-protect-against-denial-of-service/
2013-10-28 13:57:28 +01:00
Joost Hietbrink
5eddfe87d0 Support for passing variables to snippets in subdirs
Now you can use "include 'some/snippet' with variable".
2013-10-16 11:55:12 +02:00
Florian Weingarten
9b910a4e6d Update README.md
Remove old wiki link
2013-10-15 13:14:39 -04:00
Arthur Neves
7e1a0be752 Put travis badge on top 2013-10-11 12:08:19 -04:00
Arthur Neves
c9ea578b64 Drop 1.8 support on travis CI and add 2.0.0 2013-10-11 12:03:01 -04:00
Florian Weingarten
549777ae53 Merge pull request #267 from djreimer/default-filter
Add default filter to standard filters
2013-10-11 07:59:21 -07:00
Derrick Reimer
6710ef60bc Add default filter to history 2013-10-10 09:17:49 -07:00
Arthur Nogueira Neves
5fdab083b0 Merge pull request #268 from Shopify/better_rake
Improve Rakefile
2013-10-10 07:55:08 -07:00
Arthur Neves
0644da02bb Improve Rakefile 2013-10-10 10:44:16 -04:00
Derrick Reimer
5db1695694 Add default filter to standard filters 2013-10-09 16:07:32 -07:00
Florian Weingarten
a25ed17e2b Merge pull request #266 from Shopify/fix_map_on_hashes
Fix map filter on Hash inputs
2013-10-09 14:18:22 -07:00
Arthur Neves
fa3155fdcc Bump version to 3.0.0 2013-10-09 17:12:03 -04:00
Arthur Neves
534338f923 Update history
Add 3.0.0 history
Add changes related to 2.5.x
2013-10-09 17:11:19 -04:00
Florian Weingarten
96b30a89a9 Fix map filter on Hash inputs 2013-10-08 08:18:03 -04:00
Florian Weingarten
81d3733f57 Regression test for change of blank? default behaviour (2efe809e11) 2013-09-23 09:38:45 -04:00
Florian Weingarten
2efe809e11 Make blank? default to false for all tags to maintain backwards compatible 2013-09-23 08:43:26 -04:00
Simon Hørup Eskildsen
322ecca145 Merge pull request #261 from Shopify/fix-i18n-regex-warning
Fix i18n regex warning in Ruby 1.8
2013-09-16 11:36:47 -07:00
Simon Eskildsen
6ce0b9d705 Fix i18n regex warning in Ruby 1.8 2013-09-16 14:35:33 -04:00
Florian Weingarten
736998df64 Merge pull request #257 from Shopify/fix_comment_tags_second_try
Fix unknown tags in comment tags, second try
2013-09-11 13:33:26 -07:00
Florian Weingarten
5b172a4c05 Fix unknown tags in comment tags, second try 2013-09-11 12:31:54 -04:00
Florian Weingarten
bd20595f1a Add regression test for comment tag 2013-09-11 12:14:27 -04:00
Florian Weingarten
f938756a58 Revert "Merge pull request #256 from Shopify/unknown_tags_in_comments"
This reverts commit 1ae8c0e90a, reversing
changes made to 01d352bc51.
2013-09-11 12:13:55 -04:00
Florian Weingarten
1ae8c0e90a Merge pull request #256 from Shopify/unknown_tags_in_comments
Fix handling of unknown tags in comments
2013-09-11 07:59:44 -07:00
Florian Weingarten
45795f8766 Fix handling of unknown tags in comments 2013-09-11 10:40:33 -04:00
Florian Weingarten
01d352bc51 Move stuff in test around 2013-09-11 14:40:11 +02:00
Ishibashi Hideto
70513fccaf remove include Liquid from the class CustomInclude and substitute QuotedFragment with Liquid::QuotedFragment 2013-09-11 03:25:06 +09:00
Ishibashi Hideto
a5285d3d09 test for the Jekyll's issue: [Liquid doesn't render my partial · Issue #1519 · mojombo/jekyll](https://github.com/mojombo/jekyll/issues/1519) 2013-09-10 22:58:56 +09:00
James Tucker
fbfd5712df Merge pull request #3 from dntj/master
Add a test for corrected if-nodelist
2013-09-07 18:30:55 -07:00
Nicholas Jones
90593d3f18 Add a test for corrected if-nodelist 2013-09-07 11:15:49 -07:00
Tristan Hume
beded415cd Merge pull request #253 from trishume/fix-range-parsing
Fix bad range parsing.
2013-09-07 05:17:30 -07:00
James Tucker
13c826933c Update against failed cherry-pick 2013-09-07 01:42:41 +00:00
Tristan Hume
7c5b3e0c3b Fix bad range parsing. 2013-09-04 18:13:31 -04:00
Nick Jones
ca5bc5d75b Correct if-statement nodelist.
The nodelist returned by all tags is a list of containing nodes, except for the if tag.  This correct that inconsistency
2013-08-31 19:03:50 +00:00
James Tucker
d4679cd550 Strainer test now works on 1.8 2013-08-31 18:57:09 +00:00
James Tucker
9b2d5b7dd3 Add a class cache to avoid runtime extend calls
* Strainer has a class cache that creates Strainer subclasses for each filter
   set that is used on .create calls.
 * Context now creates a list of filters and passes this to Strainer.create to
   utilize the class cache in almost all use cases.
 * If add_filter was called after a render, then the method cache may still be
   invalidated.

Conflicts:

	lib/liquid/strainer.rb
2013-08-31 18:56:35 +00:00
Tristan Hume
e8b41c8856 Fix error 2013-08-30 16:06:48 -04:00
Tristan Hume
4c22bacbba Merge branch 'master' of https://github.com/Shopify/liquid 2013-08-30 15:56:13 -04:00
Tristan Hume
09a5b57ebe Fix variable closing error message 2013-08-30 15:55:43 -04:00
Florian Weingarten
8059da4938 Update History.md 2013-08-30 15:28:33 -04:00
Tristan Hume
af50f71224 Guard against state that shouldn't happen but does 2013-08-30 15:26:26 -04:00
Simon Hørup Eskildsen
136b6763e6 Merge pull request #241 from Shopify/i18n-error
Add I18n syntax error translation
2013-08-30 09:55:05 -07:00
Simon Eskildsen
ad184fbfc9 Remove superplus translations 2013-08-30 12:31:58 -04:00
Simon Eskildsen
380828f807 Rename outdated test 2013-08-30 12:31:57 -04:00
Simon Eskildsen
fc8c45ebe6 Fix use of 1.9 hash syntax 2013-08-30 12:31:57 -04:00
Simon Eskildsen
072c12dc47 Localize errors in Liquid 2013-08-30 12:31:57 -04:00
Simon Eskildsen
29cdabc30e Move I18n to options 2013-08-30 12:31:57 -04:00
Simon Eskildsen
df5980f23f Change interpolation syntax to %{key} 2013-08-30 12:31:57 -04:00
Simon Eskildsen
5ee4f960e8 Move localization option to register 2013-08-30 12:31:57 -04:00
Simon Eskildsen
0343f6dc94 Add escaping of symbols 2013-08-30 12:31:57 -04:00
Simon Eskildsen
40fba9ee6c Add locale to context registers 2013-08-30 12:31:57 -04:00
Simon Eskildsen
0a2f21386d Add fixture helper 2013-08-30 12:31:57 -04:00
Simon Eskildsen
e7bcf04d1d Remove delegate require from localization 2013-08-30 12:31:57 -04:00
Simon Eskildsen
0dac6fe88a Change to absolute path in localization test 2013-08-30 12:31:57 -04:00
Simon Eskildsen
f37a984fd7 Add sketch of I18n error translation 2013-08-30 12:31:57 -04:00
Tristan Hume
0e41c2c6e9 Merge pull request #235 from Shopify/recursive-parsing
Add a Real Parser. Closes #229 and closes #225.
2013-08-30 07:12:38 -07:00
Tristan Hume
7b52dfcb95 Clean up lexer logic 2013-08-27 16:36:22 -04:00
Tristan Hume
1fa029ab67 Simplify lexer logic. 2013-08-27 11:35:03 -04:00
Tristan Hume
26eb9a0817 Merge pull request #244 from Shopify/proper-parse-warnings
Add Better Parse Warnings To recursive-parser Branch
2013-08-27 06:53:34 -07:00
Tristan Hume
e305edc3b8 Remove extra comment 2013-08-27 09:53:06 -04:00
Tristan Hume
c94b5e87c9 Use attr_reader for warnings. 2013-08-22 16:16:28 -04:00
Tristan Hume
dd3196b22e Consistency in warnings. 2013-08-22 16:15:12 -04:00
Tristan Hume
86ba2f4174 Fix error message 1.8 compatibility 2013-08-22 13:23:44 -04:00
Tristan Hume
5bdfb62bf2 Remove old warning method 2013-08-22 12:57:16 -04:00
Tristan Hume
77db92de54 Better testing of warn mode. 2013-08-22 12:55:54 -04:00
Tristan Hume
b0cba5298a Fix warnings and make tags a proper syntax tree. 2013-08-22 12:44:23 -04:00
Tristan Hume
93fcd5687c Broken warnings implementation. 2013-08-22 12:12:35 -04:00
Tristan Hume
14a17520de Merge branch 'master' into recursive-parsing 2013-08-22 10:39:08 -04:00
Tristan Hume
0beb4a4793 Add handy context to strict parser error messages. 2013-08-19 15:45:05 -04:00
Tristan Hume
324d26d405 Consistent lack of periods in syntax errors. 2013-08-19 15:20:39 -04:00
Tristan Hume
047900d0dd Proper warning support 2013-08-19 15:14:26 -04:00
Florian Weingarten
f6f89fd0aa Merge pull request #242 from Shopify/overwrite_drop_inspect
Overwrite drop inspect
2013-08-19 09:48:16 -07:00
Florian Weingarten
a57d576708 Overwrite drop inspect 2013-08-19 12:08:27 -04:00
Tristan Hume
eb68a751ac Hopefully fix CI by improving multi-suite runner. 2013-08-16 15:15:15 -04:00
Florian Weingarten
355199dac4 Update History.md 2013-08-14 17:00:26 -04:00
Florian Weingarten
c8f38ad9d0 Merge pull request #239 from Shopify/sort_filter_on_enumerables
Sort filter on Enumerables
2013-08-14 13:59:44 -07:00
Florian Weingarten
ed4b61bfd3 Fix broken map test and add sort test 2013-08-08 11:53:52 -04:00
Florian Weingarten
8f978ecd1a Make sort filter work on Enumerable drops 2013-08-08 11:47:26 -04:00
Florian Weingarten
98c184f2fb Update History.md 2013-08-06 10:24:37 -04:00
Florian Weingarten
615e48fe29 Merge pull request #238 from Shopify/fix_clashing_method_names_in_enumerable_drops
Fix clashing method names in enumerable drops
2013-08-06 07:23:56 -07:00
Tristan Hume
6cde98319f More little fixes and changed default benchmark 2013-08-02 15:21:15 -04:00
Tristan Hume
15b53b77d6 Make stuff nicer 2013-08-02 15:17:17 -04:00
Tristan Hume
48f50eea3b Remove unused lex_specials method 2013-08-02 15:12:11 -04:00
Tristan Hume
ace12e29da Hopefully fix CI on Rubinius 2013-08-02 13:37:56 -04:00
Florian Weingarten
f98949117d Fix .include? method on Enumerable drops, used by "contains" conditions 2013-08-02 10:23:10 -04:00
Florian Weingarten
7fdb789eac Ruby 1.8.x compatibility 2013-08-01 13:56:01 -04:00
Florian Weingarten
c92efd3ab9 Update some Drop tests 2013-08-01 13:46:55 -04:00
Florian Weingarten
ff570c3ddc Fix clashing method names in enumerable drops 2013-08-01 13:17:02 -04:00
Tristan Hume
824231284c Run test suite with both parsers 2013-08-01 12:49:36 -04:00
Florian Weingarten
ee2902761c Update History.md 2013-08-01 09:18:29 -04:00
Tristan Hume
f6eacbf875 Add prayer for forgiveness. 2013-07-31 09:49:31 -04:00
Tristan Hume
c5afdc529a Shuffle logic around. 2013-07-30 16:17:03 -04:00
Tristan Hume
84f0c1bef8 Initial options passing 2013-07-30 14:44:41 -04:00
Tristan Hume
1458396733 Fix benchmark 2013-07-30 14:20:16 -04:00
Tristan Hume
346e92aaa6 Describe error modes in Readme 2013-07-29 16:40:22 -04:00
Tristan Hume
3b3961be39 Use lax mode by default so nothing breaks 2013-07-29 16:28:20 -04:00
Tristan Hume
8ca00982b6 Fixed ranges and added for loop parser 2013-07-29 14:11:47 -04:00
Tristan Hume
525e1ff195 Add range support 2013-07-29 13:25:48 -04:00
Tristan Hume
8f4b398c7a Abstract parser switching into tag 2013-07-29 13:00:35 -04:00
Tristan Hume
d5d41a8202 Make previous commit work 2013-07-29 12:43:05 -04:00
Tristan Hume
c8bd0b91b3 Catch easy cases 2013-07-29 12:07:18 -04:00
Tristan Hume
bc76c0daaf Collapse float and int into 'number' 2013-07-29 11:04:46 -04:00
Tristan Hume
be4a04ed85 Merged array_tokens into recursive-parsing 2013-07-29 10:38:52 -04:00
Tristan Hume
8dcf44e99d Faster token creation, hopefully. 2013-07-29 10:23:04 -04:00
Tristan Hume
a892e69a88 Hopefully fix CI build 2013-07-26 15:55:10 -04:00
Harry Brundage
bf53e517f5 Inline Parser#next_token to avoid method dispatch 2013-07-26 15:38:52 -04:00
Harry Brundage
bacacf2fd0 Remove the Token class from the lexer in favour of less smart but faster arrays 2013-07-26 15:14:01 -04:00
Tristan Hume
1b43bf5686 Add parser tests 2013-07-26 13:09:36 -04:00
Tristan Hume
83e71ace99 Add lexer tests and fixes 2013-07-26 12:50:27 -04:00
Tristan Hume
4dc9cc0ea1 Add back tests for lax parsing 2013-07-26 11:55:50 -04:00
Tristan Hume
87b8ee7341 Add error mode switching 2013-07-26 11:45:13 -04:00
Florian Weingarten
07f7d63bea Use kind_of? instead of class.include? and rearrange stuff 2013-07-26 11:34:00 -04:00
Florian Weingarten
1af28a6eb8 Merge pull request #233 from Shopify/make_map_work_on_enumerables
Make 'map' filter work on Enumerable drops
2013-07-26 08:30:16 -07:00
Florian Weingarten
65dfd57bb5 Make 'map' filter work on Enumerable drops 2013-07-26 10:35:28 -04:00
Tristan Hume
8b1dff9d98 Allow ! in identifiers like Ruby 2013-07-26 10:33:30 -04:00
Tristan Hume
8896b55fa5 Parsing for if statements 2013-07-26 10:31:26 -04:00
Tristan Hume
c0b9d53548 Revert "Test a different lexer architechture"
This reverts commit 24ddaf1a9c.
2013-07-26 09:52:04 -04:00
Tristan Hume
24ddaf1a9c Test a different lexer architechture 2013-07-26 09:51:58 -04:00
Tristan Hume
673826630c Unfinished if statement parser. 2013-07-26 09:32:08 -04:00
Florian Weingarten
554675d1f8 Update README.md 2013-07-26 14:46:55 +02:00
Florian Weingarten
11e1379570 Merge pull request #234 from Shopify/fix_mapping_procs
Fix mapping over procs
2013-07-26 05:45:56 -07:00
Florian Weingarten
3e13ed4ba1 Fix mapping over procs 2013-07-25 22:11:44 -04:00
Florian Weingarten
b004acf856 Merge pull request #232 from Shopify/to_liquid_stuff
Always call 'to_liquid' on stuff in map filter and allow to_liquid to be...
2013-07-25 14:10:52 -07:00
Florian Weingarten
182d3fefb6 Always call 'to_liquid' on staff in map filter and allow to_liquid to be called on drops 2013-07-25 17:10:19 -04:00
Tristan Hume
17d818b453 Fix profiler 2013-07-25 15:04:04 -04:00
Tristan Hume
0453d7e299 Fix benchmarks to use only valid liquid. 2013-07-25 11:51:51 -04:00
Tristan Hume
4da7b36139 New variable parser! 2013-07-25 11:38:57 -04:00
Florian Weingarten
c7336e0cc1 Add license to gemspec, closes #231 2013-07-24 19:10:12 -04:00
Tristan Hume
f43e973e67 Basic expression parsing 2013-07-24 16:36:14 -04:00
Florian Weingarten
bbc405a24c Merge pull request #230 from Shopify/use_invoke_drop_in_map
Use invoke_drop in map filter
2013-07-24 12:47:56 -07:00
Florian Weingarten
f9027d54ab Use invoke_drop in map filter 2013-07-24 15:41:50 -04:00
Tristan Hume
84be895db2 Fancy StringScanner based lexer 2013-07-24 15:39:48 -04:00
Tristan Hume
b20a594f25 Better lexer 2013-07-24 15:19:14 -04:00
Tristan Hume
76272a1afa Bring back the lexer 2013-07-24 14:40:29 -04:00
Tristan Hume
61a6deb43b Descriptive comment for lexer 2013-07-24 12:00:51 -04:00
Tristan Hume
ee14775f83 Replace hand-coded lexer with faster hacky lexer. 2013-07-24 11:41:47 -04:00
Tristan Hume
2332d86156 Slow lexer and parser scaffold. 2013-07-24 11:35:00 -04:00
Florian Weingarten
fbfda1a189 Little cosmetic change 2013-07-05 11:30:50 -04:00
Florian Weingarten
f0ecd02199 Fix some blank tests 2013-07-03 02:47:22 +02:00
Florian Weingarten
4a103a9dde Merge pull request #218 from Shopify/dont_render_blank_blocks
Dont render blank blocks
2013-07-02 15:15:54 -07:00
Florian Weingarten
0f38fe3596 Add blank test for case tags 2013-07-02 18:08:20 -04:00
Florian Weingarten
cd3f976288 Merge branch 'master' into dont_render_blank_blocks
Conflicts:
	lib/liquid/tag.rb
2013-07-02 14:00:30 -04:00
Florian Weingarten
b53601100f Make sure include tags are never blank 2013-07-02 13:57:27 -04:00
Florian Weingarten
5c5e7de31e Merge pull request #221 from d-Pixie/master
Changes not empty? to any?
2013-07-01 07:55:26 -07:00
Jonas Schubert Erlandsson
f91233450f Changes not empty? to any? 2013-07-01 16:12:58 +02:00
Florian Weingarten
429e492984 Merge pull request #220 from coding46/master
Fix some typos in comments
2013-06-27 13:20:24 -07:00
G. Bodenschatz
d36a1c518b Fix some typos in comments 2013-06-27 22:16:23 +02:00
Florian Weingarten
40a37c3fb6 Merge branch 'master' into dont_render_blank_blocks
Conflicts:
	lib/liquid/tags/cycle.rb
	lib/liquid/tags/increment.rb
2013-06-27 10:03:18 -04:00
Florian Weingarten
37309678de Merge pull request #219 from Shopify/indent_and_trailing_ws_cleanup
Convert legacy tab indentation to spaces and remove trailing whitespace ...
2013-06-27 06:50:28 -07:00
Florian Weingarten
cfb60c2f1b Update History.md 2013-06-27 14:30:37 +02:00
Florian Weingarten
668ee5e1c4 Clean up whitespace collapsing 2013-06-27 14:06:05 +02:00
Florian Weingarten
b4fbcea114 Cycle tags are never blank 2013-06-27 14:00:38 +02:00
Florian Weingarten
c16697746b Clean up whitespace collapsing a bit 2013-06-27 13:57:26 +02:00
Florian Weingarten
f01d0dbea6 More tests for whitespace collapsing 2013-06-27 13:39:04 +02:00
Florian Weingarten
10c151e3aa Some tests for whitespace collapsing 2013-06-26 22:30:33 -04:00
Florian Weingarten
d6e13faa43 Don't render blank blocks 2013-06-26 22:21:47 -04:00
Florian Weingarten
0f5441b09e Convert legacy tab indentation to spaces and remove trailing whitespace from all lines 2013-06-26 19:53:09 -04:00
Florian Weingarten
7a3746ad77 Update CONTRIBUTING.md
Add @arthurnn for code review
2013-06-26 05:03:05 +03:00
Florian Weingarten
24511556d3 Update README.md 2013-06-20 09:53:21 -03:00
Florian Weingarten
5621556b3a Update History.md 2013-06-18 12:36:50 -03:00
Florian Weingarten
fd230bef14 Merge pull request #106 from gnowoel/fix_example_servlet
fix example servlet
2013-06-18 08:35:42 -07:00
Florian Weingarten
e9f3a8e4d3 Update History.md 2013-06-18 05:18:43 +03:00
Florian Weingarten
8ca4868bff Merge branch 'master' of https://github.com/ndwebgroup/liquid into ndwebgroup-master 2013-06-18 04:13:49 +02:00
Florian Weingarten
f92da6948d Merge branch 'strip-html-fix' of https://github.com/jamesallardice/liquid into jamesallardice-strip-html-fix
Conflicts:
	lib/liquid/standardfilters.rb
2013-06-18 04:11:11 +02:00
Florian Weingarten
a1b156f0d4 strip_html multi-line comment test 2013-06-18 04:06:35 +02:00
Florian Weingarten
eca520025c Merge branch 'strip-html' of https://github.com/joliss/liquid into joliss-strip-html
Conflicts:
	lib/liquid/standardfilters.rb
2013-06-18 04:04:49 +02:00
Florian Weingarten
9c9a7ce8d3 Update History.md 2013-06-18 05:01:23 +03:00
Florian Weingarten
b7837ce218 Merge pull request #210 from Shopify/truncate_utf8_test
UTF8 truncate test
2013-06-17 09:45:10 -07:00
Florian Weingarten
b81469d183 Make truncate work for Ruby 1.8 2013-06-17 12:05:02 -04:00
Florian Weingarten
f488058789 UTF8 truncate test 2013-06-17 11:48:03 -04:00
Florian Weingarten
f5d75718e9 Update History.md 2013-06-16 18:31:19 +03:00
Florian Weingarten
c280936afa Merge pull request #209 from Shopify/fix_broken_raw_parsing
Fix broken 'raw' tag parsing (issue #204)
2013-06-15 04:58:51 -07:00
Florian Weingarten
e47d1af03a Fix broken 'raw' tag parsing (issue #204) 2013-06-14 12:47:27 -04:00
Florian Weingarten
81f6c79c53 Update README.md 2013-06-14 10:04:24 -03:00
Florian Weingarten
3174411407 Merge pull request #208 from Shopify/contributing
CONTRIBUTING.md
2013-06-14 05:58:37 -07:00
Florian Weingarten
f7c4b0cdec Update History.md 2013-06-14 08:57:27 -04:00
Florian Weingarten
3d1f582318 Merge pull request #201 from arthurnn/adding_version_file
create version.rb file, and bump version
2013-06-14 05:45:48 -07:00
Florian Weingarten
27db1def63 CONTRIBUTING.md 2013-06-14 02:41:08 +02:00
Florian Weingarten
5cfa13d7a4 Merge pull request #205 from phoet/stricter_handling_of_variables_in_for
rejects variables like a/b in for loops, closes #150
2013-06-13 15:26:00 -07:00
Peter Schröder
cb12497859 add test for allowing whitespace between tokens 2013-06-13 18:21:49 -04:00
Florian Weingarten
4c2e2f8a24 Merge pull request #203 from phoet/strip_carriage_return
handle carriage return in strip_newline, closes #126
2013-06-13 12:54:17 -07:00
Peter Schröder
a2df5a421d rejects variables like a/b in for loops, closes #150 2013-06-11 17:22:33 -04:00
Dylan Thacker-Smith
437e9201de Merge pull request #174 from yardstick/drop-context
Allow a Liquid::Drop to be passed into Template#render
2013-06-10 07:46:06 -07:00
Peter Schröder
fd263bba0f handle carriage return in strip_newline, closes #126 2013-06-09 10:24:35 -04:00
Arthur Neves
0d9353591b bump master version to 2.6.0
Bumping master version to 2.6.0
Adding 2.6.0 changes to history
2013-06-06 14:17:12 -04:00
Arthur Neves
1849c24f2c Adding version file
Follow the standard structure of having a version.rb file
2013-06-06 14:10:45 -04:00
Daniel Huckstep
f8288546f8 Missed in the rebase conflicts 2013-06-06 10:20:34 -06:00
Daniel Huckstep
076ae903c0 Make sure the context gets set 2013-06-06 10:19:20 -06:00
Daniel Huckstep
ba5e65f685 Better test, resuse Hash block 2013-06-06 10:18:47 -06:00
Daniel Huckstep
b699c93bae Allow a Liquid::Drop to be passed into Template#render 2013-06-06 10:17:01 -06:00
Florian Weingarten
85c1503378 Merge pull request #200 from arthurnn/155_float_precision
use BigDecimal on filters to have better precision
2013-06-05 14:07:20 -07:00
Arthur Neves
ab760649ee use BigDecimal on filters to have better precision 2013-06-05 16:09:05 -04:00
Florian Weingarten
f4fb2f159c Merge pull request #157 from stomar/avoid_warnings
Avoid warnings in Ruby 1.9.3
2013-06-05 09:10:41 -07:00
Florian Weingarten
a5cd494717 Merge pull request #176 from unreal/master
Add reverse filter
2013-06-05 09:04:53 -07:00
Florian Weingarten
f8915f3cd2 Merge pull request #199 from phoet/add_documentation_for_include
add documentation to include, fixes #163
2013-06-05 09:00:11 -07:00
Peter Schröder
e92540a9bf add documentation to include, fixes #163 2013-06-05 11:53:01 -04:00
Dylan Thacker-Smith
482115d784 Merge pull request #173 from jsw0528/master
fix `can't convert Fixnum into String` for `replace`
2013-06-04 13:09:42 -07:00
Florian Weingarten
94ff457744 Merge pull request #198 from Shopify/limit_resource_usage
Resource usage limits
2013-05-31 08:24:29 -07:00
Florian Weingarten
1e8c081b42 Create new resource_limits hash on Template initialization 2013-05-31 09:41:59 -04:00
Florian Weingarten
2b17e24b16 Mutate resource_limits hash to flag that the limit was reached (for outside observation) 2013-05-31 09:34:23 -04:00
Florian Weingarten
9075b428b1 Resource limits: Don't raise Error but render error message (but abort after first error) 2013-05-31 09:25:25 -04:00
Florian Weingarten
8760b5e8c4 Add optional resource usage limitations to number of rendering calls, length of rendering output and/or number of variable/capture assignments 2013-05-30 17:04:26 -04:00
Tom Burns
50b2ebee56 Merge pull request #189 from Shopify/cache_partials
Cache tokenized partial templates
2013-05-29 07:51:12 -07:00
Dylan Thacker-Smith
23203c0122 Fix some old templates that abused colon as an argument separator.
This is a fallback for keyword argument parsing since this feature broke
old templates that accidentally used a colon as a filter argument
separator.
2013-05-21 17:47:46 -04:00
Tom Burns
27fe76c0dd Merge pull request #192 from Shopify/revert_utf8
Revert "Merge pull request #185 from ISSIntel/liquid-utf8"
2013-05-21 14:46:17 -07:00
Tom Burns
8913a5615a Revert "Merge pull request #185 from ISSIntel/liquid-utf8"
This reverts commit c5dfcd29b0, reversing
changes made to f7d1e1d0c1.
2013-05-20 19:53:13 -04:00
Dylan Thacker-Smith
690b3ff27f Merge pull request #135 from astathopoulos/preserve_filters_ordering
Use array instead of Hash to keep the registered filters
2013-05-17 05:25:07 -07:00
Tasos Stathopoulos
8c1bbfec57 Use array instead of Hash to keep the registered filters
1.8.7 compatibility fix

In Ruby 1.8.7, Hash does not preserve insertion ordering as Array does.
This could cause a problem when registering filters which depend on others and
the registration order is important.

So, the @@filters variable was changed to array where the order of the filters is
the same as the insertion order.
2013-05-17 14:12:57 +03:00
Jay Strybis
a556ae6c26 Add reverse filter 2013-03-07 16:29:39 -06:00
Marcus Stollsteimer
1cac09831d Completely remove unused variable 2013-03-05 22:19:38 +01:00
wǒ_is神仙
17dd85868d add tests for replace filter 2013-02-21 10:52:46 +08:00
wǒ_is神仙
6e967f7f3a fix can't convert Fixnum into String 2013-02-06 12:55:35 +08:00
Marcus Stollsteimer
b48ad7da3a Remove trailing whitespace 2012-11-18 10:29:22 +01:00
Marcus Stollsteimer
afc3944a4a Fix assignment with no effect outside of iterator 2012-11-18 10:21:03 +01:00
Marcus Stollsteimer
c79abf1f87 Avoid warnings for assigned but unused variable 2012-11-18 10:20:07 +01:00
Marcus Stollsteimer
90b40ffb4b Avoid warnings for shadowed outer local variable 2012-11-18 10:19:05 +01:00
Marcus Stollsteimer
fea9c54768 Avoid warning for grouped expression 2012-11-18 10:08:00 +01:00
Jeremy Friesen
740cd6e762 Merge branch 'master' of git://github.com/Shopify/liquid
* 'master' of git://github.com/Shopify/liquid:
  * Seperated 'Howto' into 'How to'. * Added periods to the second list as the first item has them. I guess I'm anally retentive like that. :)
  Fix conditions using negative number comparisons
2012-04-19 10:59:44 -04:00
Jeremy Friesen
5d0004a87e Added tests to verify that {{ 'now' | date :'%Y' }} and {{ 'today' | date :'%Y' }} work.
In the case of Ruby 1.9.3, 'now' is no longer parsed.  For safe
measures, I've added 'today' as well.
2012-04-19 10:48:55 -04:00
jamesallardice
12112dee35 Make strip_html filter strip contents of style tags 2012-04-11 16:23:21 +01:00
Leo Wong
5c4938f443 fix example servlet 2012-03-08 01:29:50 +08:00
Jo Liss
4a2bbafeb4 Make strip_html strip tags spread across lines 2012-02-16 15:42:10 +01:00
188 changed files with 11342 additions and 5714 deletions

2
.github/probots.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
enabled:
- cla

3
.gitignore vendored
View File

@@ -4,3 +4,6 @@
pkg
*.rbc
.rvmrc
.ruby-version
Gemfile.lock
.bundle

127
.rubocop.yml Normal file
View File

@@ -0,0 +1,127 @@
inherit_from: ./.rubocop_todo.yml
AllCops:
Exclude:
- 'performance/shopify/*'
- 'pkg/**'
Metrics/BlockNesting:
Max: 3
Metrics/ModuleLength:
Enabled: false
Metrics/ClassLength:
Enabled: false
Lint/AssignmentInCondition:
Enabled: false
Lint/AmbiguousOperator:
Enabled: false
Lint/AmbiguousRegexpLiteral:
Enabled: false
Lint/ParenthesesAsGroupedExpression:
Enabled: false
Lint/UnusedBlockArgument:
Enabled: false
Lint/EndAlignment:
EnforcedStyleAlignWith: variable
Lint/UnusedMethodArgument:
Enabled: false
Style/SingleLineBlockParams:
Enabled: false
Style/DoubleNegation:
Enabled: false
Style/StringLiteralsInInterpolation:
Enabled: false
Style/AndOr:
Enabled: false
Style/SignalException:
Enabled: false
Style/StringLiterals:
Enabled: false
Style/BracesAroundHashParameters:
Enabled: false
Style/NumericLiterals:
Enabled: false
Layout/SpaceInsideBrackets:
Enabled: false
Layout/SpaceBeforeBlockBraces:
Enabled: false
Style/Documentation:
Enabled: false
Style/ClassAndModuleChildren:
Enabled: false
Style/TrailingCommaInLiteral:
Enabled: false
Layout/IndentHash:
EnforcedStyle: consistent
Style/FormatString:
Enabled: false
Layout/AlignParameters:
EnforcedStyle: with_fixed_indentation
Layout/MultilineOperationIndentation:
EnforcedStyle: indented
Style/IfUnlessModifier:
Enabled: false
Style/RaiseArgs:
Enabled: false
Style/PreferredHashMethods:
Enabled: false
Style/RegexpLiteral:
Enabled: false
Style/SymbolLiteral:
Enabled: false
Performance/Count:
Enabled: false
Style/ConstantName:
Enabled: false
Layout/CaseIndentation:
Enabled: false
Style/ClassVars:
Enabled: false
Style/PerlBackrefs:
Enabled: false
Style/TrivialAccessors:
AllowPredicates: true
Style/WordArray:
Enabled: false
Style/MethodName:
Exclude:
- 'example/server/liquid_servlet.rb'

194
.rubocop_todo.yml Normal file
View File

@@ -0,0 +1,194 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2019-02-03 21:12:39 +0530 using RuboCop version 0.49.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: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent
Layout/IndentHeredoc:
Exclude:
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/trim_mode_test.rb'
# Offense count: 6
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: symmetrical, new_line, same_line
Layout/MultilineMethodCallBraceLayout:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/unit/strainer_unit_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: runtime_error, standard_error
Lint/InheritException:
Exclude:
- 'lib/liquid/interrupts.rb'
# Offense count: 51
Metrics/AbcSize:
Max: 56
# Offense count: 11
Metrics/CyclomaticComplexity:
Max: 12
# Offense count: 639
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 294
# Offense count: 108
# Configuration parameters: CountComments.
Metrics/MethodLength:
Max: 37
# Offense count: 7
Metrics/PerceivedComplexity:
Max: 11
# Offense count: 10
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: prefer_alias, prefer_alias_method
Style/Alias:
Exclude:
- 'lib/liquid/drop.rb'
- 'lib/liquid/i18n.rb'
- 'lib/liquid/profiler/hooks.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/variable.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Exclude:
- 'lib/liquid/errors.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/EmptyCaseCondition:
Exclude:
- 'lib/liquid/block_body.rb'
- 'lib/liquid/lexer.rb'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: compact, expanded
Style/EmptyMethod:
Exclude:
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/comment.rb'
- 'lib/liquid/tags/include.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 2
# Configuration parameters: SupportedStyles.
# SupportedStyles: annotated, template
Style/FormatStringToken:
EnforcedStyle: template
# Offense count: 14
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/tags/assign.rb'
- 'lib/liquid/tags/capture.rb'
- 'lib/liquid/tags/case.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb'
- 'test/unit/tokenizer_unit_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
# SupportedStyles: skip_modifier_ifs, always
Style/Next:
Exclude:
- 'lib/liquid/tags/for.rb'
# Offense count: 4
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Exclude:
- 'spec/**/*'
- 'lib/liquid/context.rb'
- 'lib/liquid/forloop_drop.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tablerowloop_drop.rb'
# Offense count: 14
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Exclude:
- 'lib/liquid/tags/if.rb'
- 'liquid.gemspec'
- 'test/integration/assign_test.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
Style/RedundantSelf:
Exclude:
- 'lib/liquid/strainer.rb'
# Offense count: 9
# Cop supports --auto-correct.
# Configuration parameters: AllowAsExpressionSeparator.
Style/Semicolon:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: MinSize, SupportedStyles.
# SupportedStyles: percent, brackets
Style/SymbolArray:
EnforcedStyle: brackets
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment.
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
Style/TernaryParentheses:
Exclude:
- 'lib/liquid/context.rb'
- 'lib/liquid/utils.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/UnneededPercentQ:
Exclude:
- 'test/integration/error_handling_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/WhileUntilModifier:
Exclude:
- 'lib/liquid/tags/case.rb'

View File

@@ -1,13 +1,32 @@
rvm:
- 1.8.7
- 1.9.3
- ree
- jruby-18mode
- jruby-19mode
- rbx-18mode
- rbx-19mode
language: ruby
script: "rake test"
rvm:
- 2.1
- 2.2
- 2.3
- 2.4
- 2.5
- ruby-head
- jruby-head
# - rbx-2
sudo: false
addons:
apt:
packages:
- libgmp3-dev
matrix:
allow_failures:
- rvm: ruby-head
- rvm: jruby-head
install:
- gem install rainbow -v 2.2.1
- bundle install
script: bundle exec rake
notifications:
disable: true

25
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,25 @@
# How to contribute
## Things we will merge
* Bugfixes
* Performance improvements
* Features that are 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
## Workflow
* Fork the Liquid repository
* Create a new branch in your fork
* 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

20
Gemfile Normal file
View File

@@ -0,0 +1,20 @@
source 'https://rubygems.org'
git_source(:github) do |repo_name|
"https://github.com/#{repo_name}.git"
end
gemspec
gem 'stackprof', platforms: :mri
group :benchmark, :test do
gem 'benchmark-ips'
end
group :test do
gem 'rubocop', '~> 0.49.0'
platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '9168659de45d6d576fce30c735f857e597fa26f6'
end
end

View File

@@ -1,4 +1,216 @@
# Liquid Version History
# Liquid Change Log
## 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
@@ -8,6 +220,7 @@
* Fix filter parser for args without space separators
* Add support for filter keyword arguments
## 2.4.0 / 2012-08-03
* Performance improvements

View File

@@ -17,4 +17,4 @@ 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.
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,5 +1,14 @@
[![Build Status](https://api.travis-ci.org/Shopify/liquid.svg?branch=master)](http://travis-ci.org/Shopify/liquid)
[![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](http://docs.shopify.com/themes/liquid-basics)
* [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:
@@ -33,6 +42,8 @@ Liquid is a template engine which was written with very specific requirements:
## 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.
@@ -41,4 +52,57 @@ For standard use you can just pass it the content of a file and call render with
@template.render('name' => 'tobi') # => "hi tobi"
```
[![Build Status](https://secure.travis-ci.org/Shopify/liquid.png)](http://travis-ci.org/Shopify/liquid)
### 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::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
Liquid::Template.error_mode = :warn # Adds errors to template.errors but continues as normal
Liquid::Template.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
```

View File

@@ -1,53 +1,95 @@
#!/usr/bin/env ruby
require 'rubygems'
require 'rake'
require 'rake/testtask'
require 'rubygems/package_task'
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
require "liquid/version"
task :default => 'test'
task default: [:test, :rubocop]
Rake::TestTask.new(:test) do |t|
desc 'run test suite with default parser'
Rake::TestTask.new(:base_test) do |t|
t.libs << '.' << 'lib' << 'test'
t.test_files = FileList['test/liquid/**/*_test.rb']
t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
t.verbose = false
end
gemspec = eval(File.read('liquid.gemspec'))
Gem::PackageTask.new(gemspec) do |pkg|
pkg.gem_spec = gemspec
desc 'run test suite with warn error mode'
task :warn_test do
ENV['LIQUID_PARSER_MODE'] = 'warn'
Rake::Task['base_test'].invoke
end
desc "Build the gem and release it to rubygems.org"
task :release => :gem do
sh "gem push pkg/liquid-#{gemspec.version}.gem"
task :rubocop do
require 'rubocop/rake_task'
RuboCop::RakeTask.new
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'
ENV['LIQUID-C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['base_test'].reenable
Rake::Task['base_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"
desc "Run the liquid benchmark with lax parsing"
task :run do
ruby "./performance/benchmark.rb"
ruby "./performance/benchmark.rb lax"
end
desc "Run the liquid benchmark with strict parsing"
task :strict do
ruby "./performance/benchmark.rb strict"
end
end
namespace :profile do
desc "Run the liquid profile/performance coverage"
task :run do
ruby "./performance/profile.rb"
end
desc "Run KCacheGrind"
task :grind => :run do
system "qcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt"
desc "Run the liquid profile/performance coverage with strict parsing"
task :strict do
ruby "./performance/profile.rb strict"
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

View File

@@ -2,40 +2,43 @@ module ProductsFilter
def price(integer)
sprintf("$%.2d USD", integer / 100.0)
end
def prettyprint(text)
text.gsub( /\*(.*)\*/, '<b>\1</b>' )
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, 'section' => 'Snowboards', 'cool_products' => true}
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
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
end

View File

@@ -1,5 +1,4 @@
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
def do_GET(req, res)
handle(:get, req, res)
end
@@ -7,22 +6,23 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
def do_POST(req, res)
handle(:post, req, res)
end
private
def handle(type, req, res)
@request, @response = req, res
@request.path_info =~ /(\w+)$/
@action = $1 || 'index'
@assigns = send(@action) if respond_to?(@action)
@request = req
@response = res
@request.path_info =~ /(\w+)\z/
@action = $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])
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
end
def read_template(filename = @action)
File.read( File.dirname(__FILE__) + "/templates/#{filename}.liquid" )
File.read("#{__dir__}/templates/#{filename}.liquid")
end
end
end

View File

@@ -1,14 +1,12 @@
require 'webrick'
require 'rexml/document'
DIR = File.expand_path(File.dirname(__FILE__))
require DIR + '/../../lib/liquid'
require DIR + '/liquid_servlet'
require DIR + '/example_servlet'
require_relative '../../lib/liquid'
require_relative 'liquid_servlet'
require_relative 'example_servlet'
# Setup webrick
server = WEBrick::HTTPServer.new( :Port => ARGV[1] || 3000 )
server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000)
server.mount('/', Servlet)
trap("INT"){ server.shutdown }
server.start

View File

@@ -3,4 +3,4 @@
<p>It is {{date}}</p>
<p>Check out the <a href="http://localhost:3000/products">Products</a> screen </p>
<p>Check out the <a href="/products">Products</a> screen </p>

View File

@@ -1,49 +1,49 @@
<?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">
<!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>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Language" content="en-us" />
<h1>{{ description | split: '~' | first }}</h1>
<title>products</title>
<h2>{{ description | split: '~' | last }}</h2>
<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>
<h2>There are currently {{products | count}} products in the {{section}} catalog</h2>
<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 :)
Cool products :)
{% else %}
Uncool products :(
Uncool products :(
{% endif %}
<ul id="products">
{% for product in products %}
{% for product in all_products %}
<li>
<h2>{{product.name}}</h2>
Only {{product.price | price }}
{{product.description | prettyprint | paragraph }}
{{ 'it rocks!' | paragraph }}
</li>
</li>
{% endfor %}
</ul>
</body>
</body>
</html>

View File

@@ -1,8 +0,0 @@
require 'liquid'
require 'extras/liquid_view'
if defined? ActionView::Template and ActionView::Template.respond_to? :register_template_handler
ActionView::Template
else
ActionView::Base
end.register_template_handler(:liquid, LiquidView)

View File

@@ -1,51 +0,0 @@
# LiquidView is a action view extension class. You can register it with rails
# and use liquid as an template system for .liquid files
#
# Example
#
# ActionView::Base::register_template_handler :liquid, LiquidView
class LiquidView
PROTECTED_ASSIGNS = %w( template_root response _session template_class action_name request_origin session template
_response url _request _cookies variables_added _flash params _headers request cookies
ignore_missing_templates flash _params logger before_filter_chain_aborted headers )
PROTECTED_INSTANCE_VARIABLES = %w( @_request @controller @_first_render @_memoized__pick_template @view_paths
@helpers @assigns_added @template @_render_stack @template_format @assigns )
def self.call(template)
"LiquidView.new(self).render(template, local_assigns)"
end
def initialize(view)
@view = view
end
def render(template, local_assigns = nil)
@view.controller.headers["Content-Type"] ||= 'text/html; charset=utf-8'
# Rails 2.2 Template has source, but not locals
if template.respond_to?(:source) && !template.respond_to?(:locals)
assigns = (@view.instance_variables - PROTECTED_INSTANCE_VARIABLES).inject({}) do |hash, ivar|
hash[ivar[1..-1]] = @view.instance_variable_get(ivar)
hash
end
else
assigns = @view.assigns.reject{ |k,v| PROTECTED_ASSIGNS.include?(k) }
end
source = template.respond_to?(:source) ? template.source : template
local_assigns = (template.respond_to?(:locals) ? template.locals : local_assigns) || {}
if content_for_layout = @view.instance_variable_get("@content_for_layout")
assigns['content_for_layout'] = content_for_layout
end
assigns.merge!(local_assigns.stringify_keys)
liquid = Liquid::Template.parse(source)
liquid.render(assigns, :filters => [@view.controller.master_helper_module], :registers => {:action_view => @view, :controller => @view.controller})
end
def compilable?
false
end
end

View File

@@ -20,50 +20,61 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module Liquid
WordRegex = RUBY_VERSION < "1.9" ? '\w' : '[[:word:]]'
FilterSeparator = /\|/
ArgumentSeparator = ','
FilterArgumentSeparator = ':'
VariableAttributeSeparator = '.'
ArgumentSeparator = ','.freeze
FilterArgumentSeparator = ':'.freeze
VariableAttributeSeparator = '.'.freeze
WhitespaceControl = '-'.freeze
TagStart = /\{\%/
TagEnd = /\%\}/
VariableSignature = /\(?[#{WordRegex}\-\.\[\]]\)?/o
VariableSegment = /[#{WordRegex}\-]/o
VariableSignature = /\(?[\w\-\.\[\]]\)?/
VariableSegment = /[\w\-]/
VariableStart = /\{\{/
VariableEnd = /\}\}/
VariableIncompleteEnd = /\}\}?/
QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
StrictQuotedFragment = /"[^"]+"|'[^']+'|[^\s|:,]+/
FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/o
OtherFilterArgument = /#{ArgumentSeparator}(?:#{StrictQuotedFragment})/o
SpacelessFilter = /^(?:'[^']+'|"[^"]+"|[^'"])*#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/o
Expression = /(?:#{QuotedFragment}(?:#{SpacelessFilter})*)/o
TagAttributes = /(#{WordRegex}+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /\{\{|\{\%/
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/o
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
singleton_class.send(:attr_accessor, :cache_classes)
self.cache_classes = true
end
require "liquid/version"
require 'liquid/parse_tree_visitor'
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'
require 'liquid/expression'
require 'liquid/context'
require 'liquid/parser_switching'
require 'liquid/tag'
require 'liquid/block'
require 'liquid/block_body'
require 'liquid/document'
require 'liquid/variable'
require 'liquid/variable_lookup'
require 'liquid/range_lookup'
require 'liquid/file_system'
require 'liquid/resource_limits'
require 'liquid/template'
require 'liquid/htmltags'
require 'liquid/standardfilters'
require 'liquid/condition'
require 'liquid/module_ex'
require 'liquid/utils'
require 'liquid/tokenizer'
require 'liquid/parse_context'
# Load all the tags of the standard library
#
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }

View File

@@ -1,115 +1,77 @@
module Liquid
class Block < Tag
IsTag = /^#{TagStart}/o
IsVariable = /^#{VariableStart}/o
FullToken = /^#{TagStart}\s*(#{WordRegex}+)\s*(.*)?#{TagEnd}$/o
ContentOfVariable = /^#{VariableStart}(.*)#{VariableEnd}$/o
MAX_DEPTH = 100
def initialize(tag_name, markup, options)
super
@blank = true
end
def parse(tokens)
@nodelist ||= []
@nodelist.clear
while token = tokens.shift
case token
when IsTag
if token =~ FullToken
# if we found the proper block delimitor just end parsing here and let the outer block
# proceed
if block_delimiter == $1
end_tag
return
end
# fetch the tag from registered blocks
if tag = Template.tags[$1]
@nodelist << tag.new($1, $2, tokens)
else
# this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag($1, $2, tokens)
end
else
raise SyntaxError, "Tag '#{token}' was not properly terminated with regexp: #{TagEnd.inspect} "
end
when IsVariable
@nodelist << create_variable(token)
when ''
# pass
else
@nodelist << token
end
@body = BlockBody.new
while parse_body(@body, tokens)
end
# Make sure that its ok to end parsing in the current block.
# Effectively this method will throw and exception unless the current block is
# of type Document
assert_missing_delimitation!
end
def end_tag
def render(context)
@body.render(context)
end
def unknown_tag(tag, params, tokens)
case tag
when 'else'
raise SyntaxError, "#{block_name} tag does not expect else tag"
when 'end'
raise SyntaxError, "'end' is not a valid delimiter for #{block_name} tags. use #{block_delimiter}"
def blank?
@blank
end
def nodelist
@body.nodelist
end
def unknown_tag(tag, _params, _tokens)
if tag == 'else'.freeze
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
block_name: block_name))
elsif tag.start_with?('end'.freeze)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
tag: tag,
block_name: block_name,
block_delimiter: block_delimiter))
else
raise SyntaxError, "Unknown tag '#{tag}'"
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
end
end
def block_delimiter
"end#{block_name}"
end
def block_name
@tag_name
end
def create_variable(token)
token.scan(ContentOfVariable) do |content|
return Variable.new(content.first)
end
raise SyntaxError.new("Variable '#{token}' was not properly terminated with regexp: #{VariableEnd.inspect} ")
end
def render(context)
render_all(@nodelist, context)
def block_delimiter
@block_delimiter ||= "end#{block_name}"
end
protected
def assert_missing_delimitation!
raise SyntaxError.new("#{block_name} tag was never closed")
end
def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep".freeze
end
parse_context.depth += 1
begin
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank?
def render_all(list, context)
output = []
list.each do |token|
# Break out if we have any unhanded interrupts.
break if context.has_interrupt?
begin
# 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 %}
if token.is_a? Continue or token.is_a? Break
context.push_interrupt(token.interrupt)
break
return false if end_tag_name == block_delimiter
unless end_tag_name
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end
output << (token.respond_to?(:render) ? token.render(context) : token)
rescue ::StandardError => e
output << (context.handle_error(e))
# 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
output.join
true
end
end
end

143
lib/liquid/block_body.rb Normal file
View File

@@ -0,0 +1,143 @@
module Liquid
class BlockBody
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%".freeze
VARSTART = "{{".freeze
attr_reader :nodelist
def initialize
@nodelist = []
@blank = true
end
def parse(tokenizer, parse_context)
parse_context.line_number = tokenizer.line_number
while token = tokenizer.shift
next if token.empty?
case
when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context)
unless token =~ FullToken
raise_missing_tag_terminator(token, parse_context)
end
tag_name = $1
markup = $2
# fetch the tag from registered blocks
unless tag = registered_tags[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 =~ 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
previous_token.rstrip!
end
end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
end
def blank?
@blank
end
def render(context)
output = []
context.resource_limits.render_score += @nodelist.length
idx = 0
while node = @nodelist[idx]
case node
when String
check_resources(context, node)
output << node
when Variable
render_node_to_output(node, output, context)
when Block
render_node_to_output(node, output, context, node.blank?)
break if context.interrupt? # might have happened in a for-block
when Continue, Break
# 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 %}
context.push_interrupt(node.interrupt)
break
else # Other non-Block tags
render_node_to_output(node, output, context)
break if context.interrupt? # might have happened through an include
end
idx += 1
end
output.join
end
private
def render_node_to_output(node, output, context, skip_output = false)
node_output = node.render(context)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
check_resources(context, node_output)
output << node_output unless skip_output
rescue MemoryError => e
raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
output << nil
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
def check_resources(context, node_output)
context.resource_limits.render_length += node_output.length
return unless context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
end
def create_variable(token, parse_context)
token.scan(ContentOfVariable) do |content|
markup = content.first
return Variable.new(markup, parse_context)
end
raise_missing_variable_terminator(token, parse_context)
end
def raise_missing_tag_terminator(token, parse_context)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
end
def raise_missing_variable_terminator(token, parse_context)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
end
def registered_tags
Template.tags
end
end
end

View File

@@ -3,53 +3,70 @@ module Liquid
#
# Example:
#
# c = Condition.new('1', '==', '1')
# c = Condition.new(1, '==', 1)
# c.evaluate #=> true
#
class Condition #:nodoc:
@@operators = {
'==' => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
'!=' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<>' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<' => :<,
'>' => :>,
'>=' => :>=,
'<=' => :<=,
'contains' => lambda { |cond, left, right| left && right ? left.include?(right) : false }
'=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<'.freeze => :<,
'>'.freeze => :>,
'>='.freeze => :>=,
'<='.freeze => :<=,
'contains'.freeze => 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
end
}
def self.operators
@@operators
end
attr_reader :attachment
attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil)
@left, @operator, @right = left, operator, right
@left = left
@operator = operator
@right = right
@child_relation = nil
@child_condition = nil
end
def evaluate(context = Context.new)
result = interpret_condition(left, right, operator, context)
condition = self
result = nil
loop do
result = interpret_condition(condition.left, condition.right, condition.operator, context)
case @child_relation
when :or
result || @child_condition.evaluate(context)
when :and
result && @child_condition.evaluate(context)
else
result
case condition.child_relation
when :or
break if result
when :and
break unless result
else
break
end
condition = condition.child_condition
end
result
end
def or(condition)
@child_relation, @child_condition = :or, condition
@child_relation = :or
@child_condition = condition
end
def and(condition)
@child_relation, @child_condition = :and, condition
@child_relation = :and
@child_condition = condition
end
def attach(attachment)
@@ -61,23 +78,27 @@ module Liquid
end
def inspect
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
end
protected
attr_reader :child_relation
private
def equal_variables(left, right)
if left.is_a?(Symbol)
if right.respond_to?(left)
return right.send(left.to_s)
if left.is_a?(Liquid::Expression::MethodLiteral)
if right.respond_to?(left.method_name)
return right.send(left.method_name)
else
return nil
end
end
if right.is_a?(Symbol)
if left.respond_to?(right)
return left.send(right.to_s)
if right.is_a?(Liquid::Expression::MethodLiteral)
if left.respond_to?(right.method_name)
return left.send(right.method_name)
else
return nil
end
@@ -90,31 +111,41 @@ module Liquid
# 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[left] if op == nil
return context.evaluate(left) if op.nil?
left, right = context[left], context[right]
left = context.evaluate(left)
right = context.evaluate(right)
operation = self.class.operators[op] || raise(ArgumentError.new("Unknown operator #{op}"))
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
if operation.respond_to?(:call)
operation.call(self, left, right)
elsif left.respond_to?(operation) and right.respond_to?(operation)
left.send(operation, right)
else
nil
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.new(e.message)
end
end
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)
def evaluate(_context)
true
end
end
end

View File

@@ -1,5 +1,4 @@
module Liquid
# Context keeps the variable stack and resolves variables, as well as keywords
#
# context['variable'] = 'testing'
@@ -13,21 +12,37 @@ module Liquid
#
# context['bob'] #=> nil class Context
class Context
attr_reader :scopes, :errors, :registers, :environments
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false)
@environments = [environments].flatten
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@rethrow_errors = rethrow_errors
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@partial = false
@strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
squash_instance_assigns_with_environments
@this_stack_used = false
self.exception_renderer = Template.default_exception_renderer
if rethrow_errors
self.exception_renderer = ->(e) { raise }
end
@interrupts = []
@filters = []
@global_filter = nil
end
def warnings
@warnings ||= []
end
def strainer
@strainer ||= Strainer.create(self)
@strainer ||= Strainer.create(self, @filters)
end
# Adds filters to this context.
@@ -36,16 +51,16 @@ module Liquid
# for that
def add_filters(filters)
filters = [filters].flatten.compact
@filters += filters
@strainer = nil
end
filters.each do |f|
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
Strainer.add_known_filter(f)
strainer.extend(f)
end
def apply_global_filter(obj)
global_filter.nil? ? obj : global_filter.call(obj)
end
# are there any not handled interrupts?
def has_interrupt?
def interrupt?
!@interrupts.empty?
end
@@ -59,26 +74,22 @@ module Liquid
@interrupts.pop
end
def handle_error(e)
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)
raise if @rethrow_errors
case e
when SyntaxError
"Liquid syntax error: #{e.message}"
else
"Liquid error: #{e.message}"
end
exception_renderer.call(e).to_s
end
def invoke(method, *args)
strainer.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={})
def push(new_scope = {})
@scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep" if @scopes.length > 100
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH
end
# Merge a hash of variables in the current local scope
@@ -100,11 +111,19 @@ module Liquid
# end
#
# context['var] #=> nil
def stack(new_scope={})
push(new_scope)
def stack(new_scope = nil)
old_stack_used = @this_stack_used
if new_scope
push(new_scope)
@this_stack_used = true
else
@this_stack_used = false
end
yield
ensure
pop
pop if @this_stack_used
@this_stack_used = old_stack_used
end
def clear_instance_assigns
@@ -113,147 +132,95 @@ module Liquid
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
def []=(key, value)
unless @this_stack_used
@this_stack_used = true
push({})
end
@scopes[0][key] = value
end
def [](key)
resolve(key)
# 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))
end
def has_key?(key)
resolve(key) != nil
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) }
scope = @scopes[index] if index
variable = nil
if scope.nil?
@environments.each do |e|
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
# When lookup returned a value OR there is no value but the lookup also did not raise
# then it is the value we are looking for.
if !variable.nil? || @strict_variables && raise_on_not_found
scope = e
break
end
end
end
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
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
private
LITERALS = {
nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
'true' => true,
'false' => false,
'blank' => :blank?,
'empty' => :empty?
}
# 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 resolve(key)
if LITERALS.key?(key)
LITERALS[key]
else
case key
when /^'(.*)'$/ # Single quoted strings
$1
when /^"(.*)"$/ # Double quoted strings
$1
when /^(-?\d+)$/ # Integer and floats
$1.to_i
when /^\((\S+)\.\.(\S+)\)$/ # Ranges
(resolve($1).to_i..resolve($2).to_i)
when /^(-?\d[\d\.]+)$/ # Floats
$1.to_f
else
variable(key)
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
# Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key)
scope = @scopes.find { |s| s.has_key?(key) }
if scope.nil?
@environments.each do |e|
if variable = lookup_and_evaluate(e, key)
scope = e
break
end
end
end
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key)
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
return variable
end
# Resolves namespaced queries gracefully.
#
# Example
# @context['hash'] = {"name" => 'tobi'}
# assert_equal 'tobi', @context['hash.name']
# assert_equal 'tobi', @context['hash["name"]']
def variable(markup)
parts = markup.scan(VariableParser)
square_bracketed = /^\[(.*)\]$/
first_part = parts.shift
if first_part =~ square_bracketed
first_part = resolve($1)
end
if object = find_variable(first_part)
parts.each do |part|
part = resolve($1) if part_resolved = (part =~ square_bracketed)
# If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it
if object.respond_to?(:[]) and
((object.respond_to?(:has_key?) and object.has_key?(part)) or
(object.respond_to?(:fetch) and part.is_a?(Integer)))
# if its a proc we will replace the entry with the proc
res = lookup_and_evaluate(object, part)
object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls
# as commands and call them on the current object
elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
object = object.send(part.intern).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil
else
return nil
end
# If we are dealing with a drop here we have to
object.context = self if object.respond_to?(:context=)
end
end
object
end # variable
def lookup_and_evaluate(obj, key)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
else
value
end
end # lookup_and_evaluate
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@environments.each do |env|
if env.has_key?(k)
scopes.last[k] = lookup_and_evaluate(env, k)
break
end
end
end
end # squash_instance_assigns_with_environments
end # squash_instance_assigns_with_environments
end # Context
end # Liquid

View File

@@ -1,17 +1,27 @@
module Liquid
class Document < Block
# we don't need markup to open this block
def initialize(tokens)
parse(tokens)
class Document < BlockBody
def self.parse(tokens, parse_context)
doc = new
doc.parse(tokens, parse_context)
doc
end
# There isn't a real delimter
def block_delimiter
[]
def parse(tokens, parse_context)
super do |end_tag_name, end_tag_params|
unknown_tag(end_tag_name, parse_context) if end_tag_name
end
rescue SyntaxError => e
e.line_number ||= parse_context.line_number
raise
end
# Document blocks don't need to be terminated since they are not actually opened
def assert_missing_delimitation!
def unknown_tag(tag, parse_context)
case tag
when 'else'.freeze, 'end'.freeze
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
else
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
end
end
end
end

View File

@@ -1,7 +1,6 @@
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.
@@ -19,43 +18,61 @@ module Liquid
# 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 before_method(name) method which is a
# catch all.
# 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
EMPTY_STRING = ''.freeze
# Catch all for the method
def before_method(method)
nil
def liquid_method_missing(method)
return nil unless @context && @context.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end
# called by liquid to invoke a drop
def invoke_drop(method_or_key)
if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
if self.class.invokable?(method_or_key)
send(method_or_key)
else
before_method(method_or_key)
liquid_method_missing(method_or_key)
end
end
def has_key?(name)
def key?(_name)
true
end
def inspect
self.class.to_s
end
def to_liquid
self
end
alias :[] :invoke_drop
def to_s
self.class.name
end
private
alias_method :[], :invoke_drop
# Check for method existence without invoking respond_to?, which creates symbols
def self.invokable?(method_name)
@invokable_methods ||= Set.new((public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s))
@invokable_methods.include?(method_name.to_s)
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, :include?]
end
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
Set.new(whitelist.map(&:to_s))
end
end
end
end

View File

@@ -1,11 +1,56 @@
module Liquid
class Error < ::StandardError; end
class ArgumentError < Error; end
class ContextError < Error; end
class FilterNotFound < Error; end
class FileSystemError < Error; end
class StandardError < Error; end
class SyntaxError < Error; end
class StackLevelError < Error; end
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 = ""
if is_a?(SyntaxError)
str << "Liquid syntax error"
else
str << "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)
TaintedError = 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)
InternalError = Class.new(Error)
end

49
lib/liquid/expression.rb Normal file
View File

@@ -0,0 +1,49 @@
module Liquid
class Expression
class MethodLiteral
attr_reader :method_name, :to_s
def initialize(method_name, to_s)
@method_name = method_name
@to_s = to_s
end
def to_liquid
to_s
end
end
LITERALS = {
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
'true'.freeze => true,
'false'.freeze => false,
'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
}.freeze
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
INTEGERS_REGEX = /\A(-?\d+)\z/
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
def self.parse(markup)
if LITERALS.key?(markup)
LITERALS[markup]
else
case markup
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
$1
when INTEGERS_REGEX
$1.to_i
when RANGES_REGEX
RangeLookup.parse($1, $2)
when FLOATS_REGEX
$1.to_f
else
VariableLookup.parse(markup)
end
end
end
end
end

View File

@@ -7,44 +7,56 @@ class String # :nodoc:
end
end
class Array # :nodoc:
class Symbol # :nodoc:
def to_liquid
to_s
end
end
class Array # :nodoc:
def to_liquid
self
end
end
class Hash # :nodoc:
class Hash # :nodoc:
def to_liquid
self
end
end
class Numeric # :nodoc:
class Numeric # :nodoc:
def to_liquid
self
end
end
class Time # :nodoc:
class Range # :nodoc:
def to_liquid
self
end
end
class DateTime < Date # :nodoc:
class Time # :nodoc:
def to_liquid
self
end
end
class Date # :nodoc:
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:
def to_liquid # :nodoc:
self
end
end

View File

@@ -1,24 +1,24 @@
module Liquid
# A Liquid file system is way to let your templates retrieve other templates for use with the include tag.
# 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
# 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)
# 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, context)
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.
#
@@ -26,37 +26,48 @@ module Liquid
#
# 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"
# 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)
def initialize(root, pattern = "_%s.liquid".freeze)
@root = root
@pattern = pattern
end
def read_template_file(template_path, context)
def read_template_file(template_path)
full_path = full_path(template_path)
raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_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 template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/
full_path = if template_path.include?('/')
File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid")
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
full_path = if template_path.include?('/'.freeze)
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
else
File.join(root, "_#{template_path}.liquid")
File.join(root, @pattern % template_path)
end
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/
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
end

View File

@@ -0,0 +1,42 @@
module Liquid
class ForloopDrop < Drop
def initialize(name, length, parentloop)
@name = name
@length = length
@parentloop = parentloop
@index = 0
end
attr_reader :name, :length, :parentloop
def index
@index + 1
end
def index0
@index
end
def rindex
@length - @index
end
def rindex0
@length - @index - 1
end
def first
@index == 0
end
def last
@index == @length - 1
end
protected
def increment!
@index += 1
end
end
end

View File

@@ -1,74 +0,0 @@
module Liquid
class TableRow < Block
Syntax = /(#{WordRegex}+)\s+in\s+(#{QuotedFragment}+)/o
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@variable_name = $1
@collection_name = $2
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = value
end
else
raise SyntaxError.new("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3")
end
super
end
def render(context)
collection = context[@collection_name] or return ''
from = @attributes['offset'] ? context[@attributes['offset']].to_i : 0
to = @attributes['limit'] ? from + context[@attributes['limit']].to_i : nil
collection = Utils.slice_collection_using_each(collection, from, to)
length = collection.length
cols = context[@attributes['cols']].to_i
row = 1
col = 0
result = "<tr class=\"row1\">\n"
context.stack do
collection.each_with_index do |item, index|
context[@variable_name] = item
context['tablerowloop'] = {
'length' => length,
'index' => index + 1,
'index0' => index,
'col' => col + 1,
'col0' => col,
'index0' => index,
'rindex' => length - index,
'rindex0' => length - index - 1,
'first' => (index == 0),
'last' => (index == length - 1),
'col_first' => (col == 0),
'col_last' => (col == cols - 1)
}
col += 1
result << "<td class=\"col#{col}\">" << render_all(@nodelist, context) << '</td>'
if col == cols and not (index == length - 1)
col = 0
row += 1
result << "</tr>\n<tr class=\"row#{row}\">"
end
end
end
result << "</tr>\n"
result
end
end
Template.register_tag('tablerow', TableRow)
end

39
lib/liquid/i18n.rb Normal file
View File

@@ -0,0 +1,39 @@
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[$1.to_sym]).to_s
end
end
def deep_fetch_translation(name)
name.split('.'.freeze).reduce(locale) do |level, cur|
level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
end
end
end
end

View File

@@ -1,11 +1,10 @@
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"
def initialize(message = nil)
@message = message || "interrupt".freeze
end
end

55
lib/liquid/lexer.rb Normal file
View File

@@ -0,0 +1,55 @@
require "strscan"
module Liquid
class Lexer
SPECIALS = {
'|'.freeze => :pipe,
'.'.freeze => :dot,
':'.freeze => :colon,
','.freeze => :comma,
'['.freeze => :open_square,
']'.freeze => :close_square,
'('.freeze => :open_round,
')'.freeze => :close_round,
'?'.freeze => :question,
'-'.freeze => :dash
}.freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
WHITESPACE_OR_NOTHING = /\s*/
def initialize(input)
@ss = StringScanner.new(input)
end
def tokenize
@output = []
until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos?
tok = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
when t = @ss.scan(IDENTIFIER) then [:id, t]
when t = @ss.scan(DOTDOT) then [:dotdot, t]
else
c = @ss.getch
if s = SPECIALS[c]
[s, c]
else
raise SyntaxError, "Unexpected character #{c}"
end
end
@output << tok
end
@output << [:end_of_string]
end
end
end

26
lib/liquid/locales/en.yml Normal file
View File

@@ -0,0 +1,26 @@
---
errors:
syntax:
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{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 ...]"
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]"
unknown_tag: "Unknown tag '%{tag}'"
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
unexpected_else: "%{block_name} tag does not expect 'else' tag"
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
tag_never_closed: "'%{block_name}' tag was never closed"
meta_syntax_error: "Liquid syntax error: #{e.message}"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
argument:
include: "Argument error in tag 'include' - Illegal template name"

View File

@@ -1,62 +0,0 @@
# Copyright 2007 by Domizio Demichelis
# This library is free software. It may be used, redistributed and/or modified
# under the same terms as Ruby itself
#
# This extension is usesd in order to expose the object of the implementing class
# to liquid as it were a Drop. It also limits the liquid-callable methods of the instance
# to the allowed method passed with the liquid_methods call
# Example:
#
# class SomeClass
# liquid_methods :an_allowed_method
#
# def an_allowed_method
# 'this comes from an allowed method'
# end
# def unallowed_method
# 'this will never be an output'
# end
# end
#
# if you want to extend the drop to other methods you can defines more methods
# in the class <YourClass>::LiquidDropClass
#
# class SomeClass::LiquidDropClass
# def another_allowed_method
# 'and this from another allowed method'
# end
# end
# end
#
# usage:
# @something = SomeClass.new
#
# template:
# {{something.an_allowed_method}}{{something.unallowed_method}} {{something.another_allowed_method}}
#
# output:
# 'this comes from an allowed method and this from another allowed method'
#
# You can also chain associations, by adding the liquid_method call in the
# association models.
#
class Module
def liquid_methods(*allowed_methods)
drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end"
define_method :to_liquid do
drop_class.new(self)
end
drop_class.class_eval do
def initialize(object)
@object = object
end
allowed_methods.each do |sym|
define_method sym do
@object.send sym
end
end
end
end
end

View File

@@ -0,0 +1,38 @@
module Liquid
class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth
attr_reader :partial, :warnings, :error_mode
def initialize(options = {})
@template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new
@warnings = []
self.depth = 0
self.partial = false
end
def [](option_key)
@options[option_key]
end
def partial=(value)
@partial = value
@options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode
value
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

View File

@@ -0,0 +1,42 @@
# 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) : []
end
end
end

90
lib/liquid/parser.rb Normal file
View File

@@ -0,0 +1,90 @@
module Liquid
class Parser
def initialize(input)
l = Lexer.new(input)
@tokens = l.tokenize
@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]
if token[0] == :id
variable_signature
elsif [:string, :number].include? token[0]
consume
elsif token.first == :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 << ' '.freeze
end
str << expression
str
end
def variable_signature
str = consume(:id)
while look(:open_square)
str << consume
str << expression
str << consume(:close_square)
end
if look(:dot)
str << consume
str << variable_signature
end
str
end
end
end

View File

@@ -0,0 +1,31 @@
module Liquid
module ParserSwitching
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
return strict_parse_with_error_context(markup)
rescue SyntaxError => e
parse_context.warnings << e
return 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

158
lib/liquid/profiler.rb Normal file
View File

@@ -0,0 +1,158 @@
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.
# # If top level, this will be "<root>".
# 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, :partial, :line_number, :children
def initialize(node, partial)
@code = node.respond_to?(:raw) ? node.raw : node
@partial = partial
@line_number = node.respond_to?(:line_number) ? node.line_number : nil
@children = []
end
def self.start(node, partial)
new(node, partial).tap(&:start)
end
def start
@start_time = Time.now
end
def finish
@end_time = Time.now
end
def render_time
@end_time - @start_time
end
end
def self.profile_node_render(node)
if Profiler.current_profile && node.respond_to?(:render)
Profiler.current_profile.start_node(node)
output = yield
Profiler.current_profile.end_node(node)
output
else
yield
end
end
def self.profile_children(template_name)
if Profiler.current_profile
Profiler.current_profile.push_partial(template_name)
output = yield
Profiler.current_profile.pop_partial
output
else
yield
end
end
def self.current_profile
Thread.current[:liquid_profiler]
end
def initialize
@partial_stack = ["<root>"]
@root_timing = Timing.new("", current_partial)
@timing_stack = [@root_timing]
@render_start_at = Time.now
@render_end_at = @render_start_at
end
def start
Thread.current[:liquid_profiler] = self
@render_start_at = Time.now
end
def stop
Thread.current[:liquid_profiler] = nil
@render_end_at = Time.now
end
def total_render_time
@render_end_at - @render_start_at
end
def each(&block)
@root_timing.children.each(&block)
end
def [](idx)
@root_timing.children[idx]
end
def length
@root_timing.children.length
end
def start_node(node)
@timing_stack.push(Timing.start(node, current_partial))
end
def end_node(_node)
timing = @timing_stack.pop
timing.finish
@timing_stack.last.children << timing
end
def current_partial
@partial_stack.last
end
def push_partial(partial_name)
@partial_stack.push(partial_name)
end
def pop_partial
@partial_stack.pop
end
end
end

View File

@@ -0,0 +1,23 @@
module Liquid
class BlockBody
def render_node_with_profiling(node, output, context, skip_output = false)
Profiler.profile_node_render(node) do
render_node_without_profiling(node, output, context, skip_output)
end
end
alias_method :render_node_without_profiling, :render_node_to_output
alias_method :render_node_to_output, :render_node_with_profiling
end
class Include < Tag
def render_with_profiling(context)
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
render_without_profiling(context)
end
end
alias_method :render_without_profiling, :render
alias_method :render, :render_with_profiling
end
end

View File

@@ -0,0 +1,37 @@
module Liquid
class RangeLookup
def self.parse(start_markup, end_markup)
start_obj = Expression.parse(start_markup)
end_obj = Expression.parse(end_markup)
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj)
else
start_obj.to_i..end_obj.to_i
end
end
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
end
end

View File

@@ -0,0 +1,23 @@
module Liquid
class ResourceLimits
attr_accessor :render_length, :render_score, :assign_score,
:render_length_limit, :render_score_limit, :assign_score_limit
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 reached?
(@render_length_limit && @render_length > @render_length_limit) ||
(@render_score_limit && @render_score > @render_score_limit) ||
(@assign_score_limit && @assign_score > @assign_score_limit)
end
def reset
@render_length = @render_score = @assign_score = 0
end
end
end

View File

@@ -1,21 +1,34 @@
require 'cgi'
require 'bigdecimal'
module Liquid
module StandardFilters
HTML_ESCAPE = {
'&'.freeze => '&amp;'.freeze,
'>'.freeze => '&gt;'.freeze,
'<'.freeze => '&lt;'.freeze,
'"'.freeze => '&quot;'.freeze,
"'".freeze => '&#39;'.freeze
}.freeze
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
STRIP_HTML_BLOCKS = Regexp.union(
/<script.*?<\/script>/m,
/<!--.*?-->/m,
/<style.*?<\/style>/m
)
STRIP_HTML_TAGS = /<.*?>/m
# Return the size of an array or of an string
def size(input)
input.respond_to?(:size) ? input.size : 0
end
# convert a input string to DOWNCASE
# convert an input string to DOWNCASE
def downcase(input)
input.to_s.downcase
end
# convert a input string to UPCASE
# convert an input string to UPCASE
def upcase(input)
input.to_s.upcase
end
@@ -26,31 +39,56 @@ module Liquid
end
def escape(input)
CGI.escapeHTML(input) rescue input
CGI.escapeHTML(input.to_s).untaint unless input.nil?
end
def escape_once(input)
ActionView::Helpers::TagHelper.escape_once(input)
rescue NameError
input
end
alias_method :h, :escape
# Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...")
if input.nil? then return end
l = length.to_i - truncate_string.length
l = 0 if l < 0
input.length > length.to_i ? input[0...l] + truncate_string : input
def escape_once(input)
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
end
def truncatewords(input, words = 15, truncate_string = "...")
if input.nil? then return end
wordlist = input.to_s.split
l = words.to_i - 1
def url_encode(input)
CGI.escape(input.to_s) unless input.nil?
end
def url_decode(input)
return if input.nil?
result = CGI.unescape(input.to_s)
raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
result
end
def slice(input, offset, length = nil)
offset = Utils.to_integer(offset)
length = length ? Utils.to_integer(length) : 1
if input.is_a?(Array)
input.slice(offset, length) || []
else
input.to_s.slice(offset, length) || ''
end
end
# Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...".freeze)
return if input.nil?
input_str = input.to_s
length = Utils.to_integer(length)
truncate_string_str = truncate_string.to_s
l = length - truncate_string_str.length
l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
end
def truncatewords(input, words = 15, truncate_string = "...".freeze)
return if input.nil?
wordlist = input.to_s.split
words = Utils.to_integer(words)
l = words - 1
l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input
end
# Split input string into an array of substrings separated by given pattern.
@@ -59,65 +97,176 @@ module Liquid
# <div class="summary">{{ post | split '//' | first }}</div>
#
def split(input, pattern)
input.split(pattern)
input.to_s.split(pattern.to_s)
end
def strip(input)
input.to_s.strip
end
def lstrip(input)
input.to_s.lstrip
end
def rstrip(input)
input.to_s.rstrip
end
def strip_html(input)
input.to_s.gsub(/<script.*?<\/script>/, '').gsub(/<!--.*?-->/, '').gsub(/<.*?>/, '')
empty = ''.freeze
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
result.gsub!(STRIP_HTML_TAGS, empty)
result
end
# Remove all newlines from the string
def strip_newlines(input)
input.to_s.gsub(/\n/, '')
input.to_s.gsub(/\r?\n/, ''.freeze)
end
# Join elements of the array with certain character between them
def join(input, glue = ' ')
[input].flatten.join(glue)
def join(input, glue = ' '.freeze)
InputIterator.new(input).join(glue)
end
# Sort elements of the array
# provide optional property with which to sort an array of hashes or drops
def sort(input, property = nil)
ary = [input].flatten
ary = InputIterator.new(input)
return [] if ary.empty?
if property.nil?
ary.sort
elsif ary.first.respond_to?('[]') and !ary.first[property].nil?
ary.sort {|a,b| a[property] <=> b[property] }
elsif ary.first.respond_to?(property)
ary.sort {|a,b| a.send(property) <=> b.send(property) }
ary.sort do |a, b|
nil_safe_compare(a, b)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
end
end
# Sort elements of an array ignoring case if strings
# provide optional property with which to sort an array of hashes or drops
def sort_natural(input, property = nil)
ary = InputIterator.new(input)
return [] if ary.empty?
if property.nil?
ary.sort do |a, b|
nil_safe_casecmp(a, b)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
end
end
# Filter the elements of an array to those with a certain property value.
# By default the target is any truthy value.
def where(input, property, target_value = nil)
ary = InputIterator.new(input)
if ary.empty?
[]
elsif ary.first.respond_to?(:[]) && target_value.nil?
begin
ary.select { |item| item[property] }
rescue TypeError
raise_property_error(property)
end
elsif ary.first.respond_to?(:[])
begin
ary.select { |item| item[property] == target_value }
rescue TypeError
raise_property_error(property)
end
end
end
# Remove duplicate elements from an array
# provide optional property with which to determine uniqueness
def uniq(input, property = nil)
ary = InputIterator.new(input)
if property.nil?
ary.uniq
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
begin
ary.uniq { |a| a[property] }
rescue TypeError
raise_property_error(property)
end
end
end
# Reverse the elements of an array
def reverse(input)
ary = InputIterator.new(input)
ary.reverse
end
# map/collect on a given property
def map(input, property)
ary = [input].flatten
if ary.first.respond_to?('[]') and !ary.first[property].nil?
ary.map {|e| e[property] }
elsif ary.first.respond_to?(property)
ary.map {|e| e.send(property) }
InputIterator.new(input).map do |e|
e = e.call if e.is_a?(Proc)
if property == "to_liquid".freeze
e
elsif e.respond_to?(:[])
r = e[property]
r.is_a?(Proc) ? r.call : r
end
end
rescue TypeError
raise_property_error(property)
end
# Remove nils within an array
# provide optional property with which to check for nil
def compact(input, property = nil)
ary = InputIterator.new(input)
if property.nil?
ary.compact
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
begin
ary.reject { |a| a[property].nil? }
rescue TypeError
raise_property_error(property)
end
end
end
# Replace occurrences of a string with another
def replace(input, string, replacement = '')
input.to_s.gsub(string, replacement)
def replace(input, string, replacement = ''.freeze)
input.to_s.gsub(string.to_s, replacement.to_s)
end
# Replace the first occurrences of a string with another
def replace_first(input, string, replacement = '')
input.to_s.sub(string, replacement)
def replace_first(input, string, replacement = ''.freeze)
input.to_s.sub(string.to_s, replacement.to_s)
end
# remove a substring
def remove(input, string)
input.to_s.gsub(string, '')
input.to_s.gsub(string.to_s, ''.freeze)
end
# remove the first occurrences of a substring
def remove_first(input, string)
input.to_s.sub(string, '')
input.to_s.sub(string.to_s, ''.freeze)
end
# add one string to another
@@ -125,6 +274,13 @@ module Liquid
input.to_s + string.to_s
end
def concat(input, array)
unless array.respond_to?(:to_ary)
raise ArgumentError.new("concat filter requires an array argument")
end
InputIterator.new(input).concat(array)
end
# prepend a string to another
def prepend(input, string)
string.to_s + input.to_s
@@ -132,10 +288,10 @@ module Liquid
# Add <br /> tags in front of all newlines in input string
def newline_to_br(input)
input.to_s.gsub(/\n/, "<br />\n")
input.to_s.gsub(/\n/, "<br />\n".freeze)
end
# Reformat a date
# Reformat a date using Ruby's core Time#strftime( string ) -> string
#
# %a - The abbreviated weekday name (``Sun'')
# %A - The full weekday name (``Sunday'')
@@ -149,6 +305,7 @@ module Liquid
# %m - Month of the year (01..12)
# %M - Minute of the hour (00..59)
# %p - Meridian indicator (``AM'' or ``PM'')
# %s - Number of seconds since 1970-01-01 00:00:00 UTC.
# %S - Second of the minute (00..60)
# %U - Week number of the current year,
# starting with the first Sunday as the first
@@ -163,25 +320,14 @@ module Liquid
# %Y - Year with century
# %Z - Time zone name
# %% - Literal ``%'' character
#
# See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
def date(input, format)
return input if format.to_s.empty?
if format.to_s.empty?
return input.to_s
end
return input unless date = Utils.to_date(input)
if ((input.is_a?(String) && !/^\d+$/.match(input.to_s).nil?) || input.is_a?(Integer)) && input.to_i > 0
input = Time.at(input.to_i)
end
date = input.is_a?(String) ? Time.parse(input) : input
if date.respond_to?(:strftime)
date.strftime(format.to_s)
else
input
end
rescue => e
input
date.strftime(format.to_s)
end
# Get the first element of the passed in array
@@ -202,43 +348,158 @@ module Liquid
array.last if array.respond_to?(:last)
end
# absolute value
def abs(input)
result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result
end
# addition
def plus(input, operand)
to_number(input) + to_number(operand)
apply_operation(input, operand, :+)
end
# subtraction
def minus(input, operand)
to_number(input) - to_number(operand)
apply_operation(input, operand, :-)
end
# multiplication
def times(input, operand)
to_number(input) * to_number(operand)
apply_operation(input, operand, :*)
end
# division
def divided_by(input, operand)
to_number(input) / to_number(operand)
apply_operation(input, operand, :/)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end
def modulo(input, operand)
to_number(input) % to_number(operand)
apply_operation(input, operand, :%)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end
def round(input, n = 0)
result = Utils.to_number(input).round(Utils.to_number(n))
result = result.to_f if result.is_a?(BigDecimal)
result = result.to_i if n == 0
result
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
def ceil(input)
Utils.to_number(input).ceil.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
def floor(input)
Utils.to_number(input).floor.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
def at_least(input, n)
min_value = Utils.to_number(n)
result = Utils.to_number(input)
result = min_value if min_value > result
result.is_a?(BigDecimal) ? result.to_f : result
end
def at_most(input, n)
max_value = Utils.to_number(n)
result = Utils.to_number(input)
result = max_value if max_value < result
result.is_a?(BigDecimal) ? result.to_f : result
end
def default(input, default_value = ''.freeze)
if !input || input.respond_to?(:empty?) && input.empty?
default_value
else
input
end
end
private
def to_number(obj)
case obj
when Numeric
obj
when String
(obj.strip =~ /^\d+\.\d+$/) ? obj.to_f : obj.to_i
def raise_property_error(property)
raise Liquid::ArgumentError.new("cannot select the property '#{property}'")
end
def apply_operation(input, operand, operation)
result = Utils.to_number(input).send(operation, Utils.to_number(operand))
result.is_a?(BigDecimal) ? result.to_f : result
end
def nil_safe_compare(a, b)
if !a.nil? && !b.nil?
a <=> b
else
a.nil? ? 1 : -1
end
end
def nil_safe_casecmp(a, b)
if !a.nil? && !b.nil?
a.to_s.casecmp(b.to_s)
else
a.nil? ? 1 : -1
end
end
class InputIterator
include Enumerable
def initialize(input)
@input = if input.is_a?(Array)
input.flatten
elsif input.is_a?(Hash)
[input]
elsif input.is_a?(Enumerable)
input
else
0
Array(input)
end
end
def join(glue)
to_a.join(glue.to_s)
end
def concat(args)
to_a.concat(args)
end
def reverse
reverse_each.to_a
end
def uniq(&block)
to_a.uniq(&block)
end
def compact
to_a.compact
end
def empty?
@input.each { return false }
true
end
def each
@input.each do |e|
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
end
end
end
end
Template.register_filter(StandardFilters)

View File

@@ -1,53 +1,66 @@
require 'set'
module Liquid
# Strainer is the parent 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 Strainer.global_filter,
# Context#add_filters or Template.register_filter
class Strainer #:nodoc:
@@filters = {}
@@known_filters = Set.new
@@known_methods = Set.new
@@global_strainer = Class.new(Strainer) do
@filter_methods = Set.new
end
@@strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(@@global_strainer) do
@filter_methods = @@global_strainer.filter_methods.dup
filters.each { |f| add_filter(f) }
end
end
def initialize(context)
@context = context
end
def self.global_filter(filter)
raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
add_known_filter(filter)
@@filters[filter.name] = filter
class << self
attr_reader :filter_methods
end
def self.add_known_filter(filter)
unless @@known_filters.include?(filter)
@@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s))
new_methods = filter.instance_methods.map(&:to_s)
new_methods.reject!{ |m| @@method_blacklist.include?(m) }
@@known_methods.merge(new_methods)
@@known_filters.add(filter)
def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless self.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(', ')}"
else
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
end
end
def self.create(context)
strainer = Strainer.new(context)
@@filters.each { |k,m| strainer.extend(m) }
strainer
def self.global_filter(filter)
@@strainer_class_cache.clear
@@global_strainer.add_filter(filter)
end
def self.invokable?(method)
@filter_methods.include?(method.to_s)
end
def self.create(context, filters = [])
@@strainer_class_cache[filters].new(context)
end
def invoke(method, *args)
if invokable?(method)
if self.class.invokable?(method)
send(method, *args)
elsif @context && @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
end
end
def invokable?(method)
@@known_methods.include?(method.to_s) && respond_to?(method)
rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message, e.backtrace
end
end
end

View File

@@ -0,0 +1,62 @@
module Liquid
class TablerowloopDrop < Drop
def initialize(length, cols)
@length = length
@row = 1
@col = 1
@cols = cols
@index = 0
end
attr_reader :length, :col, :row
def index
@index + 1
end
def index0
@index
end
def col0
@col - 1
end
def rindex
@length - @index
end
def rindex0
@length - @index - 1
end
def first
@index == 0
end
def last
@index == @length - 1
end
def col_first
@col == 1
end
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

View File

@@ -1,26 +1,43 @@
module Liquid
class Tag
attr_reader :nodelist, :tag_name, :line_number, :parse_context
alias_method :options, :parse_context
include ParserSwitching
attr_accessor :nodelist
class << self
def parse(tag_name, markup, tokenizer, options)
tag = new(tag_name, markup, options)
tag.parse(tokenizer)
tag
end
def initialize(tag_name, markup, tokens)
@tag_name = tag_name
@markup = markup
parse(tokens)
private :new
end
def parse(tokens)
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)
''
def render(_context)
''.freeze
end
end # Tag
end # Tag
def blank?
false
end
end
end

View File

@@ -1,5 +1,4 @@
module Liquid
# Assign sets a variable in your template.
#
# {% assign foo = 'monkey' %}
@@ -9,25 +8,52 @@ module Liquid
# {{ foo }}
#
class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/o
def initialize(tag_name, markup, tokens)
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
attr_reader :to, :from
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@to = $1
@from = Variable.new($2)
@from = Variable.new($2, options)
else
raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]")
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
end
super
end
def render(context)
context.scopes.last[@to] = @from.render(context)
''
end
end
Template.register_tag('assign', Assign)
val = @from.render(context)
context.scopes.last[@to] = val
context.resource_limits.assign_score += assign_score_of(val)
''.freeze
end
def blank?
true
end
private
def assign_score_of(val)
if val.instance_of?(String)
val.length
elsif val.instance_of?(Array) || val.instance_of?(Hash)
sum = 1
# Uses #each to avoid extra allocations.
val.each { |child| sum += assign_score_of(child) }
sum
else
1
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.from]
end
end
end
Template.register_tag('assign'.freeze, Assign)
end

View File

@@ -1,5 +1,4 @@
module Liquid
# Break tag to be used to break out of a for loop.
#
# == Basic Usage:
@@ -8,14 +7,12 @@ module Liquid
# {% break %}
# {% endif %}
# {% endfor %}
#
class Break < Tag
#
class Break < Tag
def interrupt
BreakInterrupt.new
end
end
Template.register_tag('break', Break)
Template.register_tag('break'.freeze, Break)
end

View File

@@ -1,5 +1,4 @@
module Liquid
# Capture stores the result of a block into a variable without rendering it inplace.
#
# {% capture heading %}
@@ -12,24 +11,28 @@ module Liquid
# in a sidebar or footer.
#
class Capture < Block
Syntax = /(#{WordRegex}+)/o
Syntax = /(#{VariableSignature}+)/o
def initialize(tag_name, markup, tokens)
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@to = $1
else
raise SyntaxError.new("Syntax Error in 'capture' - Valid syntax: capture [var]")
raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
end
super
end
def render(context)
output = super
context.scopes.last[@to] = output
''
context.resource_limits.assign_score += output.length
''.freeze
end
def blank?
true
end
end
Template.register_tag('capture', Capture)
Template.register_tag('capture'.freeze, Capture)
end

View File

@@ -1,79 +1,94 @@
module Liquid
class Case < Block
Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
def initialize(tag_name, markup, tokens)
@blocks = []
if markup =~ Syntax
@left = $1
else
raise SyntaxError.new("Syntax Error in tag 'case' - Valid syntax: case [condition]")
end
attr_reader :blocks, :left
def initialize(tag_name, markup, options)
super
@blocks = []
if markup =~ Syntax
@left = Expression.parse($1)
else
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
end
end
def parse(tokens)
body = BlockBody.new
while parse_body(body, tokens)
body = @blocks.last.attachment
end
end
def nodelist
@blocks.map(&:attachment)
end
def unknown_tag(tag, markup, tokens)
@nodelist = []
case tag
when 'when'
when 'when'.freeze
record_when_condition(markup)
when 'else'
when 'else'.freeze
record_else_condition(markup)
else
super
end
end
def render(context)
context.stack do
def render(context)
context.stack do
execute_else_block = true
output = ''
@blocks.each do |block|
if block.else?
return render_all(block.attachment, context) if execute_else_block
if block.else?
return block.attachment.render(context) if execute_else_block
elsif block.evaluate(context)
execute_else_block = false
output << render_all(block.attachment, context)
end
execute_else_block = false
output << block.attachment.render(context)
end
end
output
end
end
end
private
def record_when_condition(markup)
def record_when_condition(markup)
body = BlockBody.new
while markup
# Create a new nodelist and assign it to the new block
if not markup =~ WhenSyntax
raise SyntaxError.new("Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %} ")
end
unless markup =~ WhenSyntax
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
end
markup = $2
markup = $2
block = Condition.new(@left, '==', $1)
block.attach(@nodelist)
@blocks.push(block)
block = Condition.new(@left, '=='.freeze, Expression.parse($1))
block.attach(body)
@blocks << block
end
end
def record_else_condition(markup)
if not markup.strip.empty?
raise SyntaxError.new("Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) ")
def record_else_condition(markup)
unless markup.strip.empty?
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
end
block = ElseCondition.new
block.attach(@nodelist)
block = ElseCondition.new
block.attach(BlockBody.new)
@blocks << block
end
end
Template.register_tag('case', Case)
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.left] + @node.blocks
end
end
end
Template.register_tag('case'.freeze, Case)
end

View File

@@ -1,9 +1,16 @@
module Liquid
class Comment < Block
def render(context)
''
def render(_context)
''.freeze
end
def unknown_tag(_tag, _markup, _tokens)
end
def blank?
true
end
end
Template.register_tag('comment', Comment)
Template.register_tag('comment'.freeze, Comment)
end

View File

@@ -1,5 +1,4 @@
module Liquid
# Continue tag to be used to break out of a for loop.
#
# == Basic Usage:
@@ -8,14 +7,12 @@ module Liquid
# {% continue %}
# {% endif %}
# {% endfor %}
#
#
class Continue < Tag
def interrupt
ContinueInterrupt.new
end
end
Template.register_tag('continue', Continue)
Template.register_tag('continue'.freeze, Continue)
end

View File

@@ -1,5 +1,4 @@
module Liquid
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
#
# {% for item in items %}
@@ -13,47 +12,54 @@ module Liquid
# <div class="green"> Item five</div>
#
class Cycle < Tag
SimpleSyntax = /^#{QuotedFragment}+/o
NamedSyntax = /^(#{QuotedFragment})\s*\:\s*(.*)/o
def initialize(tag_name, markup, tokens)
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($2)
@name = $1
@variables = variables_from_string($2)
@name = Expression.parse($1)
when SimpleSyntax
@variables = variables_from_string(markup)
@name = "'#{@variables.to_s}'"
@name = @variables.to_s
else
raise SyntaxError.new("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]")
end
super
end
def render(context)
context.registers[:cycle] ||= Hash.new(0)
context.stack do
key = context[@name]
iteration = context.registers[:cycle][key]
result = context[@variables[iteration]]
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
result
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
end
end
def render(context)
context.registers[:cycle] ||= {}
context.stack do
key = context.evaluate(@name)
iteration = context.registers[:cycle][key].to_i
result = context.evaluate(@variables[iteration])
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
result
end
end
private
def variables_from_string(markup)
markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o
$1 ? $1 : nil
end.compact
var =~ /\s*(#{QuotedFragment})\s*/o
$1 ? Expression.parse($1) : nil
end.compact
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
Array(@node.variables)
end
end
end
Template.register_tag('cycle', Cycle)
end

View File

@@ -1,5 +1,4 @@
module Liquid
# decrement is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across
# multiple instantiations of the template.
@@ -19,21 +18,18 @@ module Liquid
# Hello: -3
#
class Decrement < Tag
def initialize(tag_name, markup, tokens)
def initialize(tag_name, markup, options)
super
@variable = markup.strip
end
super
end
def render(context)
value = context.environments.first[@variable] ||= 0
value = value - 1
value -= 1
context.environments.first[@variable] = value
value.to_s
end
private
end
Template.register_tag('decrement', Decrement)
Template.register_tag('decrement'.freeze, Decrement)
end

View File

@@ -1,6 +1,5 @@
module Liquid
# "For" iterates over an array or collection.
# "For" iterates over an array or collection.
# Several useful variables are available to you within the loop.
#
# == Basic usage:
@@ -22,16 +21,16 @@ module Liquid
#
# {% for item in collection limit:5 offset:10 %}
# {{ item.name }}
# {% end %}
# {% end %}
#
# To reverse the for loop simply use {% for item in collection reversed %}
# To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
#
# == Available variables:
#
# forloop.name:: 'item-collection'
# forloop.length:: Length of the loop
# forloop.index:: The current item's position in the collection;
# forloop.index starts at 1.
# forloop.index starts at 1.
# This is helpful for non-programmers who start believe
# the first item in an array is 1, not 0.
# forloop.index0:: The current item's position in the collection
@@ -42,101 +41,163 @@ module Liquid
# where 0 is the last item.
# forloop.first:: Returns true if the item is the first item.
# forloop.last:: Returns true if the item is the last item.
# forloop.parentloop:: Provides access to the parent loop, if present.
#
class For < Block
Syntax = /(#{WordRegex}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/ou
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@variable_name = $1
@collection_name = $2
@name = "#{$1}-#{$2}"
@reversed = $3
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = value
end
else
raise SyntaxError.new("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]")
end
class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
@nodelist = @for_block = []
attr_reader :collection_name, :variable_name, :limit, :from
def initialize(tag_name, markup, options)
super
@from = @limit = nil
parse_with_selected_parser(markup)
@for_block = BlockBody.new
@else_block = nil
end
def parse(tokens)
return unless parse_body(@for_block, tokens)
parse_body(@else_block, tokens)
end
def nodelist
@else_block ? [@for_block, @else_block] : [@for_block]
end
def unknown_tag(tag, markup, tokens)
return super unless tag == 'else'
@nodelist = @else_block = []
return super unless tag == 'else'.freeze
@else_block = BlockBody.new
end
def render(context)
context.registers[:for] ||= Hash.new(0)
collection = context[@collection_name]
collection = collection.to_a if collection.is_a?(Range)
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
return render_else(context) unless iterable?(collection)
from = if @attributes['offset'] == 'continue'
context.registers[:for][@name].to_i
def render(context)
segment = collection_segment(context)
if segment.empty?
render_else(context)
else
context[@attributes['offset']].to_i
render_segment(context, segment)
end
limit = context[@attributes['limit']]
to = limit ? limit.to_i + from : nil
end
protected
segment = Utils.slice_collection_using_each(collection, from, to)
return render_else(context) if segment.empty?
segment.reverse! if @reversed
result = ''
length = segment.length
# Store our progress through the collection for the continue flag
context.registers[:for][@name] = from + segment.length
context.stack do
segment.each_with_index do |item, index|
context[@variable_name] = item
context['forloop'] = {
'name' => @name,
'length' => length,
'index' => index + 1,
'index0' => index,
'rindex' => length - index,
'rindex0' => length - index - 1,
'first' => (index == 0),
'last' => (index == length - 1) }
result << render_all(@for_block, context)
# Handle any interrupts if they exist.
if context.has_interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
def lax_parse(markup)
if markup =~ Syntax
@variable_name = $1
collection_name = $2
@reversed = !!$3
@name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name)
markup.scan(TagAttributes) do |key, value|
set_attribute(key, value)
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
end
result
end
end
def strict_parse(markup)
p = Parser.new(markup)
@variable_name = p.consume(:id)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
collection_name = p.expression
@name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name)
@reversed = p.id?('reversed'.freeze)
while p.look(:id) && p.look(:colon, 1)
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
end
p.consume
set_attribute(attribute, p.expression)
end
p.consume(:end_of_string)
end
private
def render_else(context)
return @else_block ? [render_all(@else_block, context)] : ''
def collection_segment(context)
offsets = context.registers[:for] ||= {}
from = if @from == :continue
offsets[@name].to_i
else
context.evaluate(@from).to_i
end
def iterable?(collection)
collection.respond_to?(:each) || Utils.non_blank_string?(collection)
collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range)
limit = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed
offsets[@name] = from + segment.length
segment
end
def render_segment(context, segment)
for_stack = context.registers[:for_stack] ||= []
length = segment.length
result = ''
context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
for_stack.push(loop_vars)
begin
context['forloop'.freeze] = loop_vars
segment.each do |item|
context[@variable_name] = item
result << @for_block.render(context)
loop_vars.send(:increment!)
# Handle any interrupts if they exist.
if context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
end
ensure
for_stack.pop
end
end
result
end
def set_attribute(key, expr)
case key
when 'offset'.freeze
@from = if expr == 'continue'.freeze
:continue
else
Expression.parse(expr)
end
when 'limit'.freeze
@limit = Expression.parse(expr)
end
end
def render_else(context)
@else_block ? @else_block.render(context) : ''.freeze
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
(super + [@node.limit, @node.from, @node.collection_name]).compact
end
end
end
Template.register_tag('for', For)
Template.register_tag('for'.freeze, For)
end

View File

@@ -1,5 +1,4 @@
module Liquid
# If is the conditional block
#
# {% if user.admin %}
@@ -10,22 +9,30 @@ module Liquid
#
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
#
#
class If < Block
SyntaxHelp = "Syntax Error in tag 'if' - Valid syntax: if [expression]"
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
BOOLEAN_OPERATORS = %w(and or).freeze
def initialize(tag_name, markup, tokens)
@blocks = []
push_block('if', markup)
attr_reader :blocks
def initialize(tag_name, markup, options)
super
@blocks = []
push_block('if'.freeze, markup)
end
def nodelist
@blocks.map(&:attachment)
end
def parse(tokens)
while parse_body(@blocks.last.attachment, tokens)
end
end
def unknown_tag(tag, markup, tokens)
if ['elsif', 'else'].include?(tag)
if ['elsif'.freeze, 'else'.freeze].include?(tag)
push_block(tag, markup)
else
super
@@ -36,44 +43,80 @@ module Liquid
context.stack do
@blocks.each do |block|
if block.evaluate(context)
return render_all(block.attachment, context)
return block.attachment.render(context)
end
end
''
''.freeze
end
end
private
def push_block(tag, markup)
block = if tag == 'else'
ElseCondition.new
else
expressions = markup.scan(ExpressionsAndOperators).reverse
raise(SyntaxError, SyntaxHelp) unless expressions.shift =~ Syntax
condition = Condition.new($1, $2, $3)
while not expressions.empty?
operator = (expressions.shift).to_s.strip
raise(SyntaxError, SyntaxHelp) unless expressions.shift.to_s =~ Syntax
new_condition = Condition.new($1, $2, $3)
new_condition.send(operator.to_sym, condition)
condition = new_condition
end
condition
end
@blocks.push(block)
@nodelist = block.attach(Array.new)
def push_block(tag, markup)
block = if tag == 'else'.freeze
ElseCondition.new
else
parse_with_selected_parser(markup)
end
@blocks.push(block)
block.attach(BlockBody.new)
end
def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators)
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
until expressions.empty?
operator = expressions.pop.to_s.strip
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition)
condition = new_condition
end
condition
end
def strict_parse(markup)
p = Parser.new(markup)
condition = parse_binary_comparisons(p)
p.consume(:end_of_string)
condition
end
def parse_binary_comparisons(p)
condition = parse_comparison(p)
first_condition = condition
while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
child_condition = parse_comparison(p)
condition.send(op, child_condition)
condition = child_condition
end
first_condition
end
def parse_comparison(p)
a = Expression.parse(p.expression)
if op = p.consume?(:comparison)
b = Expression.parse(p.expression)
Condition.new(a, op, b)
else
Condition.new(a)
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.blocks
end
end
end
Template.register_tag('if', If)
Template.register_tag('if'.freeze, If)
end

View File

@@ -1,20 +1,18 @@
module Liquid
class Ifchanged < Block
def render(context)
context.stack do
output = render_all(@nodelist, context)
context.stack do
output = super
if output != context.registers[:ifchanged]
context.registers[:ifchanged] = output
output
else
''
end
''.freeze
end
end
end
end
Template.register_tag('ifchanged', Ifchanged)
end
end
Template.register_tag('ifchanged'.freeze, Ifchanged)
end

View File

@@ -1,78 +1,124 @@
module Liquid
# Include allows templates to relate with other templates
#
# Simply include another template:
#
# {% include 'product' %}
#
# Include a template with a local variable:
#
# {% include 'product' with products[0] %}
#
# Include a template for a collection:
#
# {% include 'product' for products %}
#
class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
def initialize(tag_name, markup, tokens)
attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@template_name = $1
@variable_name = $3
@attributes = {}
template_name = $1
variable_name = $3
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = Expression.parse(template_name)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = value
@attributes[key] = Expression.parse(value)
end
else
raise SyntaxError.new("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]")
raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
end
end
def parse(_tokens)
end
def render(context)
template_name = context.evaluate(@template_name_expr)
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
partial = load_cached_partial(template_name, context)
context_variable_name = template_name.split('/'.freeze).last
variable = if @variable_name_expr
context.evaluate(@variable_name_expr)
else
context.find_variable(template_name, raise_on_not_found: false)
end
super
end
def parse(tokens)
end
def render(context)
partial = load_cached_partial(context)
variable = context[@variable_name || @template_name[1..-2]]
context.stack do
@attributes.each do |key, value|
context[key] = context[value]
end
old_template_name = context.template_name
old_partial = context.partial
begin
context.template_name = template_name
context.partial = true
context.stack do
@attributes.each do |key, value|
context[key] = context.evaluate(value)
end
if variable.is_a?(Array)
variable.collect do |variable|
context[@template_name[1..-2]] = variable
if variable.is_a?(Array)
variable.collect do |var|
context[context_variable_name] = var
partial.render(context)
end
else
context[context_variable_name] = variable
partial.render(context)
end
else
context[@template_name[1..-2]] = variable
partial.render(context)
end
ensure
context.template_name = old_template_name
context.partial = old_partial
end
end
private
def load_cached_partial(context)
cached_partials = context.registers[:cached_partials] || {}
template_name = context[@template_name]
if cached = cached_partials[template_name]
return cached
end
source = read_template_from_file_system(context)
partial = Liquid::Template.parse(source)
cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials
partial
end
alias_method :parse_context, :options
private :parse_context
def read_template_from_file_system(context)
file_system = context.registers[:file_system] || Liquid::Template.file_system
# make read_template_file call backwards-compatible.
case file_system.method(:read_template_file).arity
when 1
file_system.read_template_file(context[@template_name])
when 2
file_system.read_template_file(context[@template_name], context)
else
raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
end
def load_cached_partial(template_name, context)
cached_partials = context.registers[:cached_partials] || {}
if cached = cached_partials[template_name]
return cached
end
source = read_template_from_file_system(context)
begin
parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context)
ensure
parse_context.partial = false
end
cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials
partial
end
def read_template_from_file_system(context)
file_system = context.registers[:file_system] || Liquid::Template.file_system
file_system.read_template_file(context.evaluate(@template_name_expr))
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
@node.variable_name_expr
] + @node.attributes.values
end
end
end
Template.register_tag('include', Include)
Template.register_tag('include'.freeze, Include)
end

View File

@@ -1,12 +1,11 @@
module Liquid
# increment is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across
# multiple instantiations of the template.
# (To achieve the survival, the application must keep the context)
#
# if the variable does not exist, it is created with value 0.
#
# Hello: {% increment variable %}
#
# gives you:
@@ -16,20 +15,17 @@ module Liquid
# Hello: 2
#
class Increment < Tag
def initialize(tag_name, markup, tokens)
def initialize(tag_name, markup, options)
super
@variable = markup.strip
end
super
end
def render(context)
value = context.environments.first[@variable] ||= 0
context.environments.first[@variable] = value + 1
value.to_s
end
private
end
Template.register_tag('increment', Increment)
Template.register_tag('increment'.freeze, Increment)
end

View File

@@ -1,21 +1,47 @@
module Liquid
class Raw < Block
def parse(tokens)
@nodelist ||= []
@nodelist.clear
Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
def initialize(tag_name, markup, parse_context)
super
ensure_valid_markup(tag_name, markup, parse_context)
end
def parse(tokens)
@body = ''
while token = tokens.shift
if token =~ FullToken
if block_delimiter == $1
end_tag
return
end
if token =~ FullTokenPossiblyInvalid
@body << $1 if $1 != "".freeze
return if block_delimiter == $2
end
@nodelist << token if not token.empty?
@body << token unless token.empty?
end
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end
def render(_context)
@body
end
def nodelist
[@body]
end
def blank?
@body.empty?
end
protected
def ensure_valid_markup(tag_name, markup, parse_context)
unless markup =~ Syntax
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
end
end
end
Template.register_tag('raw', Raw)
Template.register_tag('raw'.freeze, Raw)
end

View File

@@ -0,0 +1,62 @@
module Liquid
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
attr_reader :variable_name, :collection_name, :attributes
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@variable_name = $1
@collection_name = Expression.parse($2)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value)
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
end
end
def render(context)
collection = context.evaluate(@collection_name) or return ''.freeze
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
collection = Utils.slice_collection(collection, from, to)
length = collection.length
cols = context.evaluate(@attributes['cols'.freeze]).to_i
result = "<tr class=\"row1\">\n"
context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'.freeze] = tablerowloop
collection.each do |item|
context[@variable_name] = item
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
if tablerowloop.col_last && !tablerowloop.last
result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
end
tablerowloop.send(:increment!)
end
end
result << "</tr>\n"
result
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
super + @node.attributes.values + [@node.collection_name]
end
end
end
Template.register_tag('tablerow'.freeze, TableRow)
end

View File

@@ -1,33 +1,30 @@
require File.dirname(__FILE__) + '/if'
require_relative 'if'
module Liquid
# Unless is a conditional just like 'if' but works on the inverse logic.
#
# {% unless x < 0 %} x is greater than zero {% end %}
# {% unless x < 0 %} x is greater than zero {% endunless %}
#
class Unless < If
def render(context)
context.stack do
# First condition is interpreted backwards ( if not )
block = @blocks.first
unless block.evaluate(context)
return render_all(block.attachment, context)
first_block = @blocks.first
unless first_block.evaluate(context)
return first_block.attachment.render(context)
end
# After the first condition unless works just like if
@blocks[1..-1].each do |block|
if block.evaluate(context)
return render_all(block.attachment, context)
return block.attachment.render(context)
end
end
''
''.freeze
end
end
end
Template.register_tag('unless', Unless)
end
Template.register_tag('unless'.freeze, Unless)
end

View File

@@ -1,5 +1,4 @@
module Liquid
# Templates are central to liquid.
# Interpretating templates is a two step process. First you compile the
# source code you got. During compile time some extensive error checking is performed.
@@ -15,9 +14,66 @@ module Liquid
#
class Template
attr_accessor :root
attr_reader :resource_limits, :warnings
@@file_system = BlankFileSystem.new
class TagRegistry
include Enumerable
def initialize
@tags = {}
@cache = {}
end
def [](tag_name)
return nil unless @tags.key?(tag_name)
return @cache[tag_name] if Liquid.cache_classes
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
end
def []=(tag_name, klass)
@tags[tag_name] = klass.name
@cache[tag_name] = klass
end
def delete(tag_name)
@tags.delete(tag_name)
@cache.delete(tag_name)
end
def each(&block)
@tags.each(&block)
end
private
def lookup_class(name)
name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
end
end
attr_reader :profiler
class << self
# Sets how strict the parser should be.
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
# :warn is the default and will give deprecation warnings when invalid syntax is used.
# :strict will enforce correct syntax.
attr_writer :error_mode
# Sets how strict the taint checker should be.
# :lax is the default, and ignores the taint flag completely
# :warn adds a warning, but does not interrupt the rendering
# :error raises an error when tainted output is used
attr_writer :taint_mode
attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception|
exception
end
def file_system
@@file_system
end
@@ -31,7 +87,15 @@ module Liquid
end
def tags
@tags ||= {}
@tags ||= TagRegistry.new
end
def error_mode
@error_mode ||= :lax
end
def taint_mode
@taint_mode ||= :lax
end
# Pass a module with filter methods which should be available
@@ -40,22 +104,33 @@ module Liquid
Strainer.global_filter(mod)
end
def default_resource_limits
@default_resource_limits ||= {}
end
# creates a new <tt>Template</tt> object from liquid source code
def parse(source)
# To enable profiling, pass in <tt>profile: true</tt> as an option.
# See Liquid::Profiler for more information
def parse(source, options = {})
template = Template.new
template.parse(source)
template
template.parse(source, options)
end
end
# creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
def initialize
@rethrow_errors = false
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
end
# Parse source code.
# Returns self for easy chaining
def parse(source)
@root = Document.new(tokenize(source))
def parse(source, options = {})
@options = options
@profiling = options[:profile]
@line_numbers = options[:line_numbers] || @profiling
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
@root = Document.parse(tokenize(source), parse_context)
@warnings = parse_context.warnings
self
end
@@ -80,6 +155,9 @@ module Liquid
# if you use the same filters over and over again consider registering them globally
# with <tt>Template.register_filter</tt>
#
# if profiling was enabled in <tt>Template#parse</tt> then the resulting profiling information
# will be available via <tt>Template#profiler</tt>
#
# Following options can be passed:
#
# * <tt>filters</tt> : array with local filters
@@ -87,64 +165,90 @@ module Liquid
# filters and tags and might be useful to integrate liquid more with its host application
#
def render(*args)
return '' if @root.nil?
return ''.freeze if @root.nil?
context = case args.first
when Liquid::Context
args.shift
c = args.shift
if @rethrow_errors
c.exception_renderer = ->(e) { raise }
end
c
when Liquid::Drop
drop = args.shift
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
when Hash
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors)
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
when nil
Context.new(assigns, instance_assigns, registers, @rethrow_errors)
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
else
raise ArgumentError, "Expect Hash or Liquid::Context as parameter"
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end
case args.last
when Hash
options = args.pop
if options[:registers].is_a?(Hash)
self.registers.merge!(options[:registers])
end
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
if options[:filters]
context.add_filters(options[:filters])
end
when Module
context.add_filters(args.pop)
when Array
apply_options_to_context(context, options)
when Module, Array
context.add_filters(args.pop)
end
# Retrying a render resets resource usage
context.resource_limits.reset
begin
# render the nodelist.
# for performance reasons we get a array back here. join will make a string out of it
result = @root.render(context)
# for performance reasons we get an array back here. join will make a string out of it.
result = with_profiling(context) do
@root.render(context)
end
result.respond_to?(:join) ? result.join : result
rescue Liquid::MemoryError => e
context.handle_error(e)
ensure
@errors = context.errors
end
end
def render!(*args)
@rethrow_errors = true; render(*args)
@rethrow_errors = true
render(*args)
end
private
# Uses the <tt>Liquid::TemplateParser</tt> regexp to tokenize the passed source
def tokenize(source)
source = source.source if source.respond_to?(:source)
return [] if source.to_s.empty?
tokens = source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] and tokens[0].empty?
tokens
Tokenizer.new(source, @line_numbers)
end
def with_profiling(context)
if @profiling && !context.partial
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
@profiler = Profiler.new
@profiler.start
begin
yield
ensure
@profiler.stop
end
else
yield
end
end
def apply_options_to_context(context, options)
context.add_filters(options[:filters]) if options[:filters]
context.global_filter = options[:global_filter] if options[:global_filter]
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
context.strict_variables = options[:strict_variables] if options[:strict_variables]
context.strict_filters = options[:strict_filters] if options[:strict_filters]
end
end
end

31
lib/liquid/tokenizer.rb Normal file
View File

@@ -0,0 +1,31 @@
module Liquid
class Tokenizer
attr_reader :line_number
def initialize(source, line_numbers = false)
@source = source
@line_number = line_numbers ? 1 : nil
@tokens = tokenize
end
def shift
token = @tokens.shift
@line_number += token.count("\n") if @line_number && token
token
end
private
def tokenize
@source = @source.source if @source.respond_to?(:source)
return [] if @source.to_s.empty?
tokens = @source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] && tokens[0].empty?
tokens
end
end
end

View File

@@ -1,15 +1,24 @@
module Liquid
module Utils
def self.slice_collection(collection, from, to)
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
collection.load_slice(from, to)
else
slice_collection_using_each(collection, from, to)
end
end
def self.slice_collection_using_each(collection, from, to)
segments = []
index = 0
yielded = 0
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
return [collection] if non_blank_string?(collection)
if collection.is_a?(String)
return collection.empty? ? [] : [collection]
end
return [] unless collection.respond_to?(:each)
collection.each do |item|
if to && to <= index
break
end
@@ -24,8 +33,51 @@ module Liquid
segments
end
def self.non_blank_string?(collection)
collection.is_a?(String) && collection != ''
def self.to_integer(num)
return num if num.is_a?(Integer)
num = num.to_s
begin
Integer(num)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid integer"
end
end
def self.to_number(obj)
case obj
when Float
BigDecimal(obj.to_s)
when Numeric
obj
when String
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal(obj) : obj.to_i
else
if obj.respond_to?(:to_number)
obj.to_number
else
0
end
end
end
def self.to_date(obj)
return obj if obj.respond_to?(:strftime)
if obj.is_a?(String)
return nil if obj.empty?
obj = obj.downcase
end
case obj
when 'now'.freeze, 'today'.freeze
Time.now
when /\A\d+\z/, Integer
Time.at(obj.to_i)
when String
Time.parse(obj)
end
rescue ::ArgumentError
nil
end
end
end

View File

@@ -1,5 +1,4 @@
module Liquid
# Holds variables. Variables are only loaded "just in time"
# and are not evaluated as part of the render stage
#
@@ -11,46 +10,138 @@ module Liquid
# {{ user | link }}
#
class Variable
FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
attr_accessor :filters, :name
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
JustTagAttributes = /\A#{TagAttributes}\z/o
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
def initialize(markup)
attr_accessor :filters, :name, :line_number
attr_reader :parse_context
alias_method :options, :parse_context
include ParserSwitching
def initialize(markup, parse_context)
@markup = markup
@name = nil
@parse_context = parse_context
@line_number = parse_context.line_number
parse_with_selected_parser(markup)
end
def raw
@markup
end
def markup_context(markup)
"in \"{{#{markup}}}\""
end
def lax_parse(markup)
@filters = []
if match = markup.match(/\s*(#{QuotedFragment})(.*)/o)
@name = match[1]
if match[2].match(/#{FilterSeparator}\s*(.*)/o)
filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f|
if matches = f.match(/\s*(#{WordRegex}+)(?:\s*#{FilterArgumentSeparator}(.*))?/)
filtername = matches[1]
filterargs = matches[2].to_s.scan(/(?:\A|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
@filters << [filtername, filterargs]
end
end
return unless markup =~ MarkupWithQuotedFragment
name_markup = $1
filter_markup = $2
@name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex
filters = $1.scan(FilterParser)
filters.each do |f|
next unless f =~ /\w+/
filtername = Regexp.last_match(0)
filterargs = f.scan(FilterArgsRegex).flatten
@filters << parse_filter_expressions(filtername, filterargs)
end
end
end
def strict_parse(markup)
@filters = []
p = Parser.new(markup)
@name = Expression.parse(p.expression)
while p.consume?(:pipe)
filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
@filters << parse_filter_expressions(filtername, filterargs)
end
p.consume(:end_of_string)
end
def parse_filterargs(p)
# first argument
filterargs = [p.argument]
# followed by comma separated others
filterargs << p.argument while p.consume?(:comma)
filterargs
end
def render(context)
return '' if @name.nil?
@filters.inject(context[@name]) do |output, filter|
filterargs = []
keyword_args = {}
filter[1].to_a.each do |a|
if matches = a.match(/\A#{TagAttributes}\z/o)
keyword_args[matches[1]] = context[matches[2]]
else
filterargs << context[a]
end
obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
context.invoke(filter_name, output, *filter_args)
end
obj = context.apply_global_filter(obj)
taint_check(context, obj)
obj
end
private
def parse_filter_expressions(filter_name, unparsed_args)
filter_args = []
keyword_args = {}
unparsed_args.each do |a|
if matches = a.match(JustTagAttributes)
keyword_args[matches[1]] = Expression.parse(matches[2])
else
filter_args << Expression.parse(a)
end
filterargs << keyword_args unless keyword_args.empty?
begin
output = context.invoke(filter[0], output, *filterargs)
rescue FilterNotFound
raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found."
end
result = [filter_name, filter_args]
result << keyword_args unless keyword_args.empty?
result
end
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
if filter_kwargs
parsed_kwargs = {}
filter_kwargs.each do |key, expr|
parsed_kwargs[key] = context.evaluate(expr)
end
parsed_args << parsed_kwargs
end
parsed_args
end
def taint_check(context, obj)
return unless obj.tainted?
return if Template.taint_mode == :lax
@markup =~ QuotedFragment
name = Regexp.last_match(0)
error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
error.line_number = line_number
error.template_name = context.template_name
case Template.taint_mode
when :warn
context.warnings << error
when :error
raise error
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.name] + @node.filters.flatten
end
end
end

View File

@@ -0,0 +1,88 @@
module Liquid
class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze].freeze
attr_reader :name, :lookups
def self.parse(markup)
new(markup)
end
def initialize(markup)
lookups = markup.scan(VariableParser)
name = lookups.shift
if name =~ SQUARE_BRACKETED
name = Expression.parse($1)
end
@name = name
@lookups = lookups
@command_flags = 0
@lookups.each_index do |i|
lookup = lookups[i]
if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse($1)
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i
end
end
end
def evaluate(context)
name = context.evaluate(@name)
object = context.find_variable(name)
@lookups.each_index do |i|
key = context.evaluate(@lookups[i])
# If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it
if object.respond_to?(:[]) &&
((object.respond_to?(:key?) && object.key?(key)) ||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
# if its a proc we will replace the entry with the proc
res = context.lookup_and_evaluate(object, key)
object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls
# as commands and call them on the current object
elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil or
# raise an exception if `strict_variables` option is set to true
else
return nil unless context.strict_variables
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
# If we are dealing with a drop here we have to
object.context = context if object.respond_to?(:context=)
end
object
end
def ==(other)
self.class == other.class && state == other.state
end
protected
def state
[@name, @lookups, @command_flags]
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.lookups
end
end
end
end

5
lib/liquid/version.rb Normal file
View File

@@ -0,0 +1,5 @@
# encoding: utf-8
module Liquid
VERSION = "4.0.3".freeze
end

View File

@@ -1,21 +1,31 @@
# encoding: utf-8
lib = File.expand_path('../lib/', __FILE__)
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
require "liquid/version"
Gem::Specification.new do |s|
s.name = "liquid"
s.version = "2.5.0"
s.version = Liquid::VERSION
s.platform = Gem::Platform::RUBY
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
s.authors = ["Tobias Luetke"]
s.authors = ["Tobias Lütke"]
s.email = ["tobi@leetsoft.com"]
s.homepage = "http://www.liquidmarkup.org"
#s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.1.0"
s.required_rubygems_version = ">= 1.3.7"
s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md)
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
s.extra_rdoc_files = ["History.md", "README.md"]
s.extra_rdoc_files = ["History.md", "README.md"]
s.require_path = "lib"
s.add_development_dependency 'rake', '~> 11.3'
s.add_development_dependency 'minitest'
end

View File

@@ -1,11 +1,18 @@
require 'rubygems'
require 'benchmark'
require File.dirname(__FILE__) + '/theme_runner'
require 'benchmark/ips'
require_relative 'theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.bmbm do |x|
x.report("parse:") { 100.times { profiler.compile } }
x.report("parse & run:") { 100.times { profiler.run } }
end
Benchmark.ips do |x|
x.time = 10
x.warmup = 5
puts
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
puts
x.report("parse:") { profiler.compile }
x.report("render:") { profiler.render }
x.report("parse & render:") { profiler.run }
end

View File

@@ -1,19 +1,24 @@
require 'rubygems'
require 'ruby-prof' rescue fail("install ruby-prof extension/gem")
require File.dirname(__FILE__) + '/theme_runner'
require 'stackprof'
require_relative 'theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
profiler.run
puts 'Running profiler...'
[:cpu, :object].each do |profile_type|
puts "Profiling in #{profile_type} mode..."
results = StackProf.run(mode: profile_type) do
200.times do
profiler.run
end
end
results = profiler.run
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME']
File.open(graph_filename, 'w') do |f|
StackProf::Report.new(results).print_graphviz(nil, f)
end
end
puts 'Success'
puts
[RubyProf::FlatPrinter, RubyProf::GraphPrinter, RubyProf::GraphHtmlPrinter, RubyProf::CallTreePrinter].each do |klass|
filename = (ENV['TMP'] || '/tmp') + (klass.name.include?('Html') ? "/liquid.#{klass.name.downcase}.html" : "/callgrind.liquid.#{klass.name.downcase}.txt")
filename.gsub!(/:+/, '_')
File.open(filename, "w+") { |fp| klass.new(results).print(fp, :print_file => true) }
$stderr.puts "wrote #{klass.name} output to #{filename}"
StackProf::Report.new(results).print_text(false, 20)
File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME']
end

View File

@@ -1,21 +1,21 @@
class CommentForm < Liquid::Block
Syntax = /(#{Liquid::VariableSignature}+)/
class CommentForm < Liquid::Block
Syntax = /(#{Liquid::VariableSignature}+)/
def initialize(tag_name, markup, options)
super
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@variable_name = $1
@attributes = {}
else
raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]")
end
super
end
def render(context)
def render(context)
article = context[@variable_name]
context.stack do
context.stack do
context['form'] = {
'posted_successfully?' => context.registers[:posted_successfully],
'errors' => context['comment.errors'],
@@ -25,9 +25,9 @@ class CommentForm < Liquid::Block
}
wrap_in_form(article, render_all(@nodelist, context))
end
end
def wrap_in_form(article, input)
%Q{<form id="article-#{article.id}-comment-form" class="comment-form" method="post" action="">\n#{input}\n</form>}
end
def wrap_in_form(article, input)
%(<form id="article-#{article.id}-comment-form" class="comment-form" method="post" action="">\n#{input}\n</form>)
end
end

View File

@@ -1,45 +1,45 @@
require 'yaml'
module Database
# Load the standard vision toolkit database and re-arrage it to be simply exportable
# Load the standard vision toolkit database and re-arrage it to be simply exportable
# to liquid as assigns. All this is based on Shopify
def self.tables
@tables ||= begin
db = YAML.load_file(File.dirname(__FILE__) + '/vision.database.yml')
@tables ||= begin
db = YAML.load_file("#{__dir__}/vision.database.yml")
# From vision source
# From vision source
db['products'].each do |product|
collections = db['collections'].find_all do |collection|
collection['products'].any? { |p| p['id'].to_i == product['id'].to_i }
end
product['collections'] = collections
end
product['collections'] = collections
end
# key the tables by handles, as this is how liquid expects it.
# key the tables by handles, as this is how liquid expects it.
db = db.inject({}) do |assigns, (key, values)|
assigns[key] = values.inject({}) { |h, v| h[v['handle']] = v; h; }
assigns
end
# Some standard direct accessors so that the specialized templates
end
# Some standard direct accessors so that the specialized templates
# render correctly
db['collection'] = db['collections'].values.first
db['product'] = db['products'].values.first
db['blog'] = db['blogs'].values.first
db['article'] = db['blog']['articles'].first
db['cart'] = {
db['cart'] = {
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] },
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] },
'items' => db['line_items'].values
}
db
end
end
end
end
if __FILE__ == $0
if __FILE__ == $PROGRAM_NAME
p Database.tables['collections']['frontpage'].keys
#p Database.tables['blog']['articles']
end
# p Database.tables['blog']['articles']
end

View File

@@ -1,7 +1,7 @@
require 'json'
module JsonFilter
def json(object)
object.reject {|k,v| k == "collections" }.to_json
JSON.dump(object.reject { |k, v| k == "collections" })
end
end
end

View File

@@ -1,13 +1,13 @@
$:.unshift File.dirname(__FILE__) + '/../../lib'
require File.dirname(__FILE__) + '/../../lib/liquid'
$:.unshift __dir__ + '/../../lib'
require_relative '../../lib/liquid'
require File.dirname(__FILE__) + '/comment_form'
require File.dirname(__FILE__) + '/paginate'
require File.dirname(__FILE__) + '/json_filter'
require File.dirname(__FILE__) + '/money_filter'
require File.dirname(__FILE__) + '/shop_filter'
require File.dirname(__FILE__) + '/tag_filter'
require File.dirname(__FILE__) + '/weight_filter'
require_relative 'comment_form'
require_relative 'paginate'
require_relative 'json_filter'
require_relative 'money_filter'
require_relative 'shop_filter'
require_relative 'tag_filter'
require_relative 'weight_filter'
Liquid::Template.register_tag 'paginate', Paginate
Liquid::Template.register_tag 'form', CommentForm

View File

@@ -1,18 +1,17 @@
module MoneyFilter
def money_with_currency(money)
return '' if money.nil?
sprintf("$ %.2f USD", money/100.0)
sprintf("$ %.2f USD", money / 100.0)
end
def money(money)
return '' if money.nil?
sprintf("$ %.2f", money/100.0)
sprintf("$ %.2f", money / 100.0)
end
private
private
def currency
ShopDrop.new.currency
end
end
end
end

View File

@@ -1,9 +1,9 @@
class Paginate < Liquid::Block
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
def initialize(tag_name, markup, tokens)
@nodelist = []
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@collection_name = $1
@page_size = if $2
@@ -11,22 +11,20 @@ class Paginate < Liquid::Block
else
20
end
@attributes = { 'window_size' => 3 }
markup.scan(Liquid::TagAttributes) do |key, value|
@attributes[key] = value
end
end
else
raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number")
end
super
end
def render(context)
@context = context
context.stack do
context.stack do
current_page = context['current_page'].to_i
pagination = {
@@ -34,33 +32,32 @@ class Paginate < Liquid::Block
'current_page' => 5,
'current_offset' => @page_size * 5
}
context['paginate'] = pagination
collection_size = context[@collection_name].size
context['paginate'] = pagination
collection_size = context[@collection_name].size
raise ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection_size.nil?
page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
pagination['items'] = collection_size
pagination['pages'] = page_count -1
pagination['previous'] = link('&laquo; Previous', current_page-1 ) unless 1 >= current_page
pagination['next'] = link('Next &raquo;', current_page+1 ) unless page_count <= current_page+1
pagination['pages'] = page_count - 1
pagination['previous'] = link('&laquo; Previous', current_page - 1) unless 1 >= current_page
pagination['next'] = link('Next &raquo;', current_page + 1) unless page_count <= current_page + 1
pagination['parts'] = []
hellip_break = false
if page_count > 2
1.upto(page_count-1) do |page|
1.upto(page_count - 1) do |page|
if current_page == page
pagination['parts'] << no_link(page)
elsif page == 1
elsif page == 1
pagination['parts'] << link(page, page)
elsif page == page_count -1
elsif page == page_count - 1
pagination['parts'] << link(page, page)
elsif page <= current_page - @attributes['window_size'] or page >= current_page + @attributes['window_size']
elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size']
next if hellip_break
pagination['parts'] << no_link('&hellip;')
hellip_break = true
@@ -68,26 +65,26 @@ class Paginate < Liquid::Block
else
pagination['parts'] << link(page, page)
end
hellip_break = false
end
end
end
render_all(@nodelist, context)
super
end
end
private
def no_link(title)
{ 'title' => title, 'is_link' => false}
{ 'title' => title, 'is_link' => false }
end
def link(title, page)
{ 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true}
{ 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true }
end
def current_url
"/collections/frontpage"
end
end
end

View File

@@ -1,5 +1,4 @@
module ShopFilter
def asset_url(input)
"/files/1/[shop_id]/[shop_id]/assets/#{input}"
end
@@ -7,27 +6,27 @@ module ShopFilter
def global_asset_url(input)
"/global/#{input}"
end
def shopify_asset_url(input)
"/shopify/#{input}"
end
def script_tag(url)
%(<script src="#{url}" type="text/javascript"></script>)
end
def stylesheet_tag(url, media="all")
def stylesheet_tag(url, media = "all")
%(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />)
end
def link_to(link, url, title="")
%|<a href="#{url}" title="#{title}">#{link}</a>|
def link_to(link, url, title = "")
%(<a href="#{url}" title="#{title}">#{link}</a>)
end
def img_tag(url, alt="")
%|<img src="#{url}" alt="#{alt}" />|
end
def img_tag(url, alt = "")
%(<img src="#{url}" alt="#{alt}" />)
end
def link_to_vendor(vendor)
if vendor
link_to vendor, url_for_vendor(vendor), vendor
@@ -35,7 +34,7 @@ module ShopFilter
'Unknown Vendor'
end
end
def link_to_type(type)
if type
link_to type, url_for_type(type), type
@@ -43,56 +42,65 @@ module ShopFilter
'Unknown Vendor'
end
end
def url_for_vendor(vendor_title)
"/collections/#{vendor_title.to_handle}"
"/collections/#{to_handle(vendor_title)}"
end
def url_for_type(type_title)
"/collections/#{type_title.to_handle}"
"/collections/#{to_handle(type_title)}"
end
def product_img_url(url, style = 'small')
unless url =~ /^products\/([\w\-\_]+)\.(\w{2,4})/
unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/
raise ArgumentError, 'filter "size" can only be called on product images'
end
case style
when 'original'
return '/files/shops/random_number/' + url
return '/files/shops/random_number/' + url
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
"/files/shops/random_number/products/#{$1}_#{style}.#{$2}"
"/files/shops/random_number/products/#{$1}_#{style}.#{$2}"
else
raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon '
raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon '
end
end
def default_pagination(paginate)
html = []
html = []
html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous']
for part in paginate['parts']
if part['is_link']
html << %(<span class="page">#{link_to(part['title'], part['url'])}</span>)
html << %(<span class="page">#{link_to(part['title'], part['url'])}</span>)
elsif part['title'].to_i == paginate['current_page'].to_i
html << %(<span class="page current">#{part['title']}</span>)
html << %(<span class="page current">#{part['title']}</span>)
else
html << %(<span class="deco">#{part['title']}</span>)
html << %(<span class="deco">#{part['title']}</span>)
end
end
html << %(<span class="next">#{link_to(paginate['next']['title'], paginate['next']['url'])}</span>) if paginate['next']
html.join(' ')
end
# Accepts a number, and two words - one for singular, one for plural
# Returns the singular word if input equals 1, otherwise plural
def pluralize(input, singular, plural)
input == 1 ? singular : plural
end
private
def to_handle(str)
result = str.dup
result.downcase!
result.delete!("'\"()[]")
result.gsub!(/\W+/, '-')
result.gsub!(/-+\z/, '') if result[-1] == '-'
result.gsub!(/\A-+/, '') if result[0] == '-'
result
end
end

View File

@@ -1,10 +1,9 @@
module TagFilter
def link_to_tag(label, tag)
def link_to_tag(label, tag)
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>"
end
def highlight_active_tag(tag, css_class='active')
def highlight_active_tag(tag, css_class = 'active')
if @context['current_tags'].include?(tag)
"<span class=\"#{css_class}\">#{tag}</span>"
else
@@ -12,14 +11,13 @@ module TagFilter
end
end
def link_to_add_tag(label, tag)
def link_to_add_tag(label, tag)
tags = (@context['current_tags'] + [tag]).uniq
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
end
def link_to_remove_tag(label, tag)
def link_to_remove_tag(label, tag)
tags = (@context['current_tags'] - [tag]).uniq
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
end
end

View File

@@ -2,7 +2,7 @@
# Variants
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
product_variants:
product_variants:
- &product-1-var-1
id: 1
title: 151cm / Normal
@@ -13,7 +13,7 @@ product_variants:
inventory_quantity: 5
option1: 151cm
option2: Normal
option3:
option3:
- &product-1-var-2
id: 2
title: 155cm / Normal
@@ -24,7 +24,7 @@ product_variants:
inventory_quantity: 2
option1: 155cm
option2: Normal
option3:
option3:
- &product-2-var-1
id: 3
title: 162cm
@@ -34,19 +34,19 @@ product_variants:
available: true
inventory_quantity: 3
option1: 162cm
option2:
option3:
option2:
option3:
- &product-3-var-1
id: 4
title: 159cm
price: 19900
weight: 1000
compare_at_price:
compare_at_price:
available: true
inventory_quantity: 4
option1: 159cm
option2:
option3:
option2:
option3:
- &product-4-var-1
id: 5
title: 159cm
@@ -56,8 +56,8 @@ product_variants:
available: true
inventory_quantity: 6
option1: 159cm
option2:
option3:
option2:
option3:
- &product-1-var-3
id: 6
title: 158cm / Wide
@@ -74,12 +74,12 @@ product_variants:
title: 162cm
price: 19900
weight: 1000
compare_at_price:
compare_at_price:
available: false
inventory_quantity: 0
option1: 162cm
option2:
option3:
option2:
option3:
- &product-3-var-3
id: 8
title: 165cm
@@ -89,8 +89,8 @@ product_variants:
available: true
inventory_quantity: 4
option1: 165cm
option2:
option3:
option2:
option3:
- &product-5-var-1
id: 9
title: black / 42
@@ -233,7 +233,7 @@ product_variants:
inventory_quantity: 0
option1: brown
option2: medium
option3:
option3:
- &product-8-var-3
id: 22
title: brown / large
@@ -315,7 +315,7 @@ product_variants:
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Products
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
products:
- &product-1
id: 1
@@ -327,27 +327,27 @@ products:
price_max: 31900
price_min: 23900
price_varies: true
available: true
tags:
available: true
tags:
- season2005
- pro
- intermediate
- wooden
- wooden
- freestyle
options:
options:
- Length
- Style
- Style
compare_at_price: 49900
compare_at_price_max: 50900
compare_at_price_min: 49900
compare_at_price_varies: true
url: /products/arbor-draft
featured_image: products/arbor_draft.jpg
images:
images:
- products/arbor_draft.jpg
description:
description:
The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a new level. The board's freaky Tiki design pays homage to culture that inspired snowboarding. It's designed to spin with ease, land smoothly, lock hook-free onto rails, and take the abuse of a pavement pounding or twelve. The Draft will pop off kickers with authority and carve solidly across the pipe. The Draft features targeted Koa wood die cuts inlayed into the deck that enhance the flex pattern. Now bow down to riding's ancestors.
variants:
variants:
- *product-1-var-1
- *product-1-var-2
- *product-1-var-3
@@ -361,14 +361,14 @@ products:
price_max: 29900
price_min: 29900
price_varies: false
available: true
tags:
available: true
tags:
- season2005
- pro
- wooden
- wooden
- freestyle
options:
- Length
- Length
compare_at_price: 52900
compare_at_price_max: 52900
compare_at_price_min: 52900
@@ -379,29 +379,29 @@ products:
- products/element58.jpg
description:
The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience. The Element is exceedingly lively, freely initiates, and holds a tight edge at speed. Its structural real-wood topsheet is made with book-matched Koa.
variants:
variants:
- *product-2-var-1
- &product-3
id: 3
title: Comic ~ Pastel
title: Comic ~ Pastel
handle: comic-pastel
type: Snowboards
vendor: Technine
price: 19900
price_max: 22900
price_min: 19900
tags:
tags:
- season2006
- beginner
- intermediate
- freestyle
- freestyle
- purple
options:
- Length
price_varies: true
available: true
compare_at_price:
available: true
compare_at_price:
compare_at_price_max: 0
compare_at_price_min: 0
compare_at_price_varies: false
@@ -413,7 +413,7 @@ products:
- products/technine_detail.jpg
description:
2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all.
variants:
variants:
- *product-3-var-1
- *product-3-var-2
- *product-3-var-3
@@ -428,12 +428,12 @@ products:
price_max: 19900
price_min: 19900
price_varies: false
available: true
tags:
available: true
tags:
- season2006
- beginner
- intermediate
- freestyle
- freestyle
- orange
options:
- Length
@@ -448,9 +448,9 @@ products:
- products/technine4.jpg
description:
2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all.
variants:
variants:
- *product-4-var-1
- &product-5
id: 5
title: Burton Boots
@@ -461,11 +461,11 @@ products:
price_max: 11900
price_min: 11900
price_varies: false
available: true
tags:
available: true
tags:
- season2006
- beginner
- intermediate
- intermediate
- boots
options:
- Color
@@ -480,13 +480,13 @@ products:
- products/burton.jpg
description:
The Burton boots are particularly well on snowboards. The very best thing about them is that the according picture is cubic. This makes testing in a Vision testing environment very easy.
variants:
variants:
- *product-5-var-1
- *product-5-var-2
- *product-5-var-3
- *product-5-var-4
- *product-5-var-5
- *product-5-var-6
- *product-5-var-6
- &product-6
id: 6
@@ -498,17 +498,17 @@ products:
price_max: 2179500
price_min: 2179500
price_varies: false
available: true
tags:
available: true
tags:
- ducati
- superbike
- bike
- bike
- street
- racing
- performance
- performance
options:
- Color
compare_at_price:
compare_at_price:
compare_at_price_max: 0
compare_at_price_min: 0
compare_at_price_varies: false
@@ -520,7 +520,7 @@ products:
<h3>S PERFORMANCE</h3>
<p>Producing 170hp (125kW) and with a dry weight of just 169kg (372.6lb), the new 1198 S now incorporates more World Superbike technology than ever before by taking the 1198 motor and adding top-of-the-range suspension, lightweight chassis components and a true racing-style traction control system designed for road use.</p>
<p>The high performance, fully adjustable 43mm Öhlins forks, which sport low friction titanium nitride-treated fork sliders, respond effortlessly to every imperfection in the tarmac. Beyond their advanced engineering solutions, one of the most important characteristics of Öhlins forks is their ability to communicate the condition and quality of the tyre-to-road contact patch, a feature that puts every rider in superior control. The suspension set-up at the rear is complemented with a fully adjustable Öhlins rear shock equipped with a ride enhancing top-out spring and mounted to a single-sided swingarm for outstanding drive and traction. The front-to-rear Öhlins package is completed with a control-enhancing adjustable steering damper.</p>
variants:
variants:
- *product-6-var-1
- &product-7
@@ -533,17 +533,17 @@ products:
price_max: 1900
price_min: 1900
price_varies: false
available: true
tags:
available: true
tags:
- shopify
- shirt
- apparel
- apparel
- tshirt
- clothing
options:
- Color
- Size
compare_at_price:
compare_at_price:
compare_at_price_max: 0
compare_at_price_min: 0
compare_at_price_varies: false
@@ -554,11 +554,11 @@ products:
description:
<p>High Quality Shopify Shirt. Wear your e-commerce solution with pride and attract attention anywhere you go.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
variants:
variants:
- *product-7-var-1
- *product-7-var-2
- *product-7-var-3
- *product-7-var-4
- *product-7-var-4
- &product-8
id: 8
@@ -570,11 +570,11 @@ products:
price_max: 5900
price_min: 5900
price_varies: false
available: true
tags:
available: true
tags:
- sweater
- hooded
- apparel
- apparel
- clothing
options:
- Color
@@ -591,11 +591,11 @@ products:
description:
<p>Extra comfortable zip up sweater. Durable quality, ideal for any outdoor activities.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
variants:
variants:
- *product-8-var-1
- *product-8-var-2
- *product-8-var-3
- *product-8-var-4
- *product-8-var-4
- *product-8-var-5
- *product-8-var-6
@@ -609,11 +609,11 @@ products:
price_max: 552500
price_min: 499995
price_varies: true
available: true
tags:
available: true
tags:
- camera
- slr
- nikon
- nikon
- professional
options:
- Bundle
@@ -626,21 +626,21 @@ products:
images:
- products/d3.jpg
- products/d3_2.jpg
- products/d3_3.jpg
- products/d3_3.jpg
description:
<p>Flagship pro D-SLR with a 12.1-MP FX-format CMOS sensor, blazing 9 fps shooting at full FX resolution and low-noise performance up to 6400 ISO.</p>
<p><strong>Nikon's original 12.1-megapixel FX-format (23.9 x 36mm) CMOS sensor:</strong> Couple Nikon's exclusive digital image processing system with the 12.1-megapixel FX-format and you'll get breathtakingly rich images while also reducing noise to unprecedented levels with even higher ISOs.</p>
<p><strong>Continuous shooting at up to 9 frames per second:</strong> At full FX resolution and up to 11fps in the DX crop mode, the D3 offers uncompromised shooting speeds for fast-action and sports photography.</p>
variants:
variants:
- *product-9-var-1
- *product-9-var-2
- *product-9-var-3
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Line Items
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
line_items:
- &line_item-1
id: 1
@@ -661,7 +661,7 @@ line_items:
quantity: 2
variant: *product-4-var-1
product: *product-4
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Link Lists
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
@@ -715,7 +715,7 @@ links:
title: Catalog
url: /collections/all
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Link Lists
@@ -727,7 +727,7 @@ link_lists:
title: 'Main Menu'
handle: 'main-menu'
links:
- *link-12
- *link-12
- *link-5
- *link-7
- *link-8
@@ -738,7 +738,7 @@ link_lists:
links:
- *link-5
- *link-6
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Collections
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
@@ -759,7 +759,7 @@ collections:
title: Arbor
handle: arbor
url: /collections/arbor
products:
products:
- *product-1
- *product-2
@@ -770,26 +770,26 @@ collections:
url: /collections/snowboards
description:
<p>This is a description for my <strong>Snowboards</strong> collection.</p>
products:
products:
- *product-1
- *product-2
- *product-3
- *product-4
- &collection-4
id: 4
title: Items On Sale
handle: sale
url: /collections/sale
products:
products:
- *product-1
- &collection-5
id: 5
title: Paginated Sale
handle: 'paginated-sale'
url: '/collections/paginated-sale'
products:
products:
- *product-1
- *product-2
- *product-3
@@ -801,7 +801,7 @@ collections:
title: All products
handle: 'all'
url: '/collections/all'
products:
products:
- *product-7
- *product-8
- *product-9
@@ -812,7 +812,7 @@ collections:
- *product-4
- *product-5
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Pages and Blogs
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
@@ -828,12 +828,12 @@ pages:
<p>Our retail store is located at <em>Rue d'Avignon 32, Avignon (Provence)</em>.</p>
<p><strong>Opening Hours:</strong><br />Monday through Friday: 9am - 6pm<br />Saturday: 10am - 3pm<br />Sunday: closed</p>"
created_at: 2005-04-04 12:00
- &page-3
id: 2
title: About Us
handle: about-us
url: /pages/about-us
url: /pages/about-us
author: Tobi
content:
"<p>Our company was founded in 1894 and we are since operating out of Avignon from the beautiful Provence.</p>
@@ -848,12 +848,12 @@ pages:
author: Tobi
content: "<ul><li>Your order is safe with us. Our checkout uses industry standard security to protect your information.</li><li>Your order will be billed immediately upon checkout.</li><li><b>ALL SALES ARE FINAL:</b> Defective or damaged product will be exchanged</li><li>All orders are processed expediently: usually in under 24 hours.</li></ul>"
created_at: 2005-04-04 12:00
- &page-5
id: 4
title: Shipping and Handling
handle: shipping
url: /pages/shipping
url: /pages/shipping
author: Tobi
content: <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
created_at: 2005-04-04 12:00
@@ -862,7 +862,7 @@ pages:
id: 5
title: Frontpage
handle: frontpage
url: /pages/frontpage
url: /pages/frontpage
author: Tobi
content: <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
created_at: 2005-04-04 12:00
@@ -872,11 +872,11 @@ blogs:
handle: news
title: News
url: /blogs/news
articles:
articles:
- id: 3
title: 'Welcome to the new Foo Shop'
author: Daniel
content: <p><strong>Welcome to your Shopify store! The jaded Pixel crew is really glad you decided to take Shopify for a spin.</strong></p><p>To help you get you started with Shopify, here are a couple of tips regarding what you see on this page.</p><p>The text you see here is an article. To edit this article, create new articles or create new pages you can go to the <a href="/admin/pages">Blogs &amp; Pages</a> tab of the administration menu.</p><p>The Shopify t-shirt above is a product and selling products is what Shopify is all about. To edit this product, or create new products you can go to the <a href="/admin/products">Products Tab</a> in of the administration menu.</p><p>While you're looking around be sure to check out the <a href="/admin/collections">Collections</a> and <a href="/admin/links">Navigations</a> tabs and soon you will be well on your way to populating your site.</p><p>And of course don't forget to browse the <a href="admin/design/appearance/themes">theme gallery</a> to pick a new look for your shop!</p><p><strong>Shopify is in beta</strong><br />If you would like to make comments or suggestions please visit us in the <a href="http://forums.shopify.com/community">Shopify Forums</a> or drop us an <a href="mailto:feedback@shopify.com">email</a>.</p>
content: <p><strong>Welcome to your Shopify store! The jaded Pixel crew is really glad you decided to take Shopify for a spin.</strong></p><p>To help you get you started with Shopify, here are a couple of tips regarding what you see on this page.</p><p>The text you see here is an article. To edit this article, create new articles or create new pages you can go to the <a href="/admin/pages">Blogs &amp; Pages</a> tab of the administration menu.</p><p>The Shopify t-shirt above is a product and selling products is what Shopify is all about. To edit this product, or create new products you can go to the <a href="/admin/products">Products Tab</a> in of the administration menu.</p><p>While you're looking around be sure to check out the <a href="/admin/collections">Collections</a> and <a href="/admin/links">Navigations</a> tabs and soon you will be well on your way to populating your site.</p><p>And of course don't forget to browse the <a href="admin/design/appearance/themes">theme gallery</a> to pick a new look for your shop!</p><p><strong>Shopify is in beta</strong><br />If you would like to make comments or suggestions please visit us in the <a href="http://forums.shopify.com/community">Shopify Forums</a> or drop us an <a href="mailto:feedback@shopify.com">email</a>.</p>
created_at: 2005-04-04 16:00
- id: 4
title: 'Breaking News: Restock on all sales products'
@@ -884,7 +884,7 @@ blogs:
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-04 12:00
articles_count: 2
- id: 2
handle: bigcheese-blog
title: Bigcheese blog
@@ -896,7 +896,7 @@ blogs:
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-04 16:00
comments:
-
-
id: 1
author: John Smith
email: john@smith.com
@@ -905,7 +905,7 @@ blogs:
created_at: 2009-01-01 12:00
updated_at: 2009-02-01 12:00
url: ""
-
-
id: 2
author: John Jones
email: john@jones.com
@@ -921,22 +921,22 @@ blogs:
created_at: 2005-04-06 12:00
comments:
articles_count: 2
comments_enabled?: true
comments_enabled?: true
comment_post_url: ""
comments_count: 2
moderated?: true
moderated?: true
- id: 3
handle: paginated-blog
title: Paginated blog
url: /blogs/paginated-blog
articles:
articles:
- id: 6
title: 'One thing you probably did not know yet...'
author: Justin
content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
created_at: 2005-04-04 16:00
- id: 7
title: Fascinating
author: Tobi

View File

@@ -1,11 +1,9 @@
module WeightFilter
def weight(grams)
sprintf("%.2f", grams / 1000)
end
def weight_with_unit(grams)
"#{weight(grams)} kg"
end
end
end
end

View File

@@ -4,7 +4,7 @@
<div class="article-body textile">
{{ article.content }}
</div>
</div>
</div>
@@ -12,27 +12,27 @@
{% if blog.comments_enabled? %}
<div id="comments">
<h3>Comments</h3>
<!-- List all comments -->
<ul id="comment-list">
{% for comment in article.comments %}
<li>
<div class="comment-details">
<span class="comment-author">{{ comment.author }}</span> said on <span class="comment-date">{{ comment.created_at | date: "%B %d, %Y" }}</span>:
<div class="comment-details">
<span class="comment-author">{{ comment.author }}</span> said on <span class="comment-date">{{ comment.created_at | date: "%B %d, %Y" }}</span>:
</div>
<div class="comment">
{{ comment.content }}
</div>
</li>
</div>
</li>
{% endfor %}
</ul>
<!-- Comment Form -->
<div id="comment-form">
{% form article %}
<h3>Leave a comment</h3>
<!-- Check if a comment has been submitted in the last request, and if yes display an appropriate message -->
{% if form.posted_successfully? %}
{% if blog.moderated? %}
@@ -44,11 +44,11 @@
<div class="notice">Successfully posted your comment.</div>
{% endif %}
{% endif %}
{% if form.errors %}
<div class="notice error">Not all the fields have been filled out correctly!</div>
{% endif %}
<dl>
<dt class="{% if form.errors contains 'author' %}error{% endif %}"><label for="comment_author">Your name</label></dt>
<dd><input type="text" id="comment_author" name="comment[author]" size="40" value="{{form.author}}" class="{% if form.errors contains 'author' %}input-error{% endif %}" /></dd>
@@ -59,16 +59,16 @@
<dt class="{% if form.errors contains 'body' %}error{% endif %}"><label for="comment_body">Your comment</label></dt>
<dd><textarea id="comment_body" name="comment[body]" cols="40" rows="5" class="{% if form.errors contains 'body' %}input-error{% endif %}">{{form.body}}</textarea></dd>
</dl>
{% if blog.moderated? %}
<p class="hint">comments have to be approved before showing up</p>
{% endif %}
{% endif %}
<input type="submit" value="Post comment" id="comment-submit" />
{% endform %}
</div>
<!-- END Comment Form -->
</div>
{% endif %}
<!-- END Comments -->
<!-- END Comments -->

View File

@@ -1,33 +1,33 @@
<div id="page">
<h2>{{page.title}}</h2>
{% paginate blog.articles by 20 %}
{% for article in blog.articles %}
<div class="article">
<div class="headline">
<h3 class="title">
<a href="{{article.url}}">{{ article.title }}</a>
</h3>
<h4 class="date">Posted on {{ article.created_at | date: "%B %d, '%y" }} by {{ article.author }}.</h4>
</div>
<div class="article-body textile">
{{ article.content | strip_html | truncate: 250 }}
</div>
{% if blog.comments_enabled? %}
<p style="text-align: right"><a href="{{article.url}}#comments">{{ article.comments_count }} comments</a></p>
{% endif %}
</div>
{% endfor %}
<div id="pagination">
{{ paginate | default_pagination }}
</div>
{% endpaginate %}
</div>
<div id="page">
<h2>{{page.title}}</h2>
{% paginate blog.articles by 20 %}
{% for article in blog.articles %}
<div class="article">
<div class="headline">
<h3 class="title">
<a href="{{article.url}}">{{ article.title }}</a>
</h3>
<h4 class="date">Posted on {{ article.created_at | date: "%B %d, '%y" }} by {{ article.author }}.</h4>
</div>
<div class="article-body textile">
{{ article.content | strip_html | truncate: 250 }}
</div>
{% if blog.comments_enabled? %}
<p style="text-align: right"><a href="{{article.url}}#comments">{{ article.comments_count }} comments</a></p>
{% endif %}
</div>
{% endfor %}
<div id="pagination">
{{ paginate | default_pagination }}
</div>
{% endpaginate %}
</div>

View File

@@ -1,66 +1,66 @@
<script type="text/javascript">
function remove_item(id) {
document.getElementById('updates_'+id).value = 0;
document.getElementById('cartform').submit();
}
</script>
<div>
{% if cart.item_count == 0 %}
<h4>Your shopping cart is looking rather empty...</h4>
{% else %}
<form action="/cart" method="post" id="cartform">
<div id="cart">
<h3>You have {{ cart.item_count }} {{ cart.item_count | pluralize: 'product', 'products' }} in here!</h3>
<ul id="line-items">
{% for item in cart.items %}
<li id="item-{{item.id}}" class="clearfix">
<div class="thumb">
<div class="prodimage">
<a href="{{item.product.url}}" title="View {{item.title}} Page"><img src="{{item.product.featured_image | product_img_url: 'thumb' }}" alt="{{item.title | escape }}" /></a>
</div></div>
<h3 style="padding-right: 150px">
<a href="{{item.product.url}}" title="View {{item.title | escape }} Page">
{{ item.title }}
{% if item.variant.available == true %}
({{item.variant.title}})
{% endif %}
</a>
</h3>
<small class="itemcost">Costs {{ item.price | money }} each, <span class="money">{{item.line_price | money }}</span> total.</small>
<p class="right">
<label for="updates">How many? </label>
<input type="text" size="4" name="updates[{{item.variant.id}}]" id="updates_{{item.variant.id}}" value="{{item.quantity}}" onfocus="this.select();"/><br />
<a href="#" onclick="remove_item({{item.variant.id}}); return false;" class="remove"><img style="padding:15px 0 0 0;margin:0;" src="{{ 'delete.gif' | asset_url }}" /></a>
</p>
</li>
{% endfor %}
<li id="total">
<input type="image" id="update-cart" name="update" value="Update My Cart" src="{{ 'update.gif' | asset_url }}" />
Subtotal:
<span class="money">{{ cart.total_price | money_with_currency }}</span>
</li>
</ul>
</div>
<div class="info">
<input type="image" value="Checkout!" name="checkout" src="{{ 'checkout.gif' | asset_url }}" />
</div>
{% if additional_checkout_buttons %}
<div class="additional-checkout-buttons">
<p>- or -</p>
{{ content_for_additional_checkout_buttons }}
</div>
{% endif %}
</form>
{% endif %}
</div>
<script type="text/javascript">
function remove_item(id) {
document.getElementById('updates_'+id).value = 0;
document.getElementById('cartform').submit();
}
</script>
<div>
{% if cart.item_count == 0 %}
<h4>Your shopping cart is looking rather empty...</h4>
{% else %}
<form action="/cart" method="post" id="cartform">
<div id="cart">
<h3>You have {{ cart.item_count }} {{ cart.item_count | pluralize: 'product', 'products' }} in here!</h3>
<ul id="line-items">
{% for item in cart.items %}
<li id="item-{{item.id}}" class="clearfix">
<div class="thumb">
<div class="prodimage">
<a href="{{item.product.url}}" title="View {{item.title}} Page"><img src="{{item.product.featured_image | product_img_url: 'thumb' }}" alt="{{item.title | escape }}" /></a>
</div></div>
<h3 style="padding-right: 150px">
<a href="{{item.product.url}}" title="View {{item.title | escape }} Page">
{{ item.title }}
{% if item.variant.available == true %}
({{item.variant.title}})
{% endif %}
</a>
</h3>
<small class="itemcost">Costs {{ item.price | money }} each, <span class="money">{{item.line_price | money }}</span> total.</small>
<p class="right">
<label for="updates">How many? </label>
<input type="text" size="4" name="updates[{{item.variant.id}}]" id="updates_{{item.variant.id}}" value="{{item.quantity}}" onfocus="this.select();"/><br />
<a href="#" onclick="remove_item({{item.variant.id}}); return false;" class="remove"><img style="padding:15px 0 0 0;margin:0;" src="{{ 'delete.gif' | asset_url }}" /></a>
</p>
</li>
{% endfor %}
<li id="total">
<input type="image" id="update-cart" name="update" value="Update My Cart" src="{{ 'update.gif' | asset_url }}" />
Subtotal:
<span class="money">{{ cart.total_price | money_with_currency }}</span>
</li>
</ul>
</div>
<div class="info">
<input type="image" value="Checkout!" name="checkout" src="{{ 'checkout.gif' | asset_url }}" />
</div>
{% if additional_checkout_buttons %}
<div class="additional-checkout-buttons">
<p>- or -</p>
{{ content_for_additional_checkout_buttons }}
</div>
{% endif %}
</form>
{% endif %}
</div>

View File

@@ -1,22 +1,22 @@
{% paginate collection.products by 20 %}
<ul id="product-collection">
{% for product in collection.products %}
<li class="singleproduct clearfix">
<div class="small">
<div class="prodimage"><a href="{{product.url}}"><img src="{{ product.featured_image | product_img_url: 'small' }}" /></a></div>
</div>
<div class="description">
<h3><a href="{{product.url}}">{{product.title}}</a></h3>
<p>{{ product.description | strip_html | truncatewords: 35 }}</p>
<p class="money">{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}</p>
</div>
</li>
{% endfor %}
</ul>
<div id="pagination">
{{ paginate | default_pagination }}
</div>
{% endpaginate %}
{% paginate collection.products by 20 %}
<ul id="product-collection">
{% for product in collection.products %}
<li class="singleproduct clearfix">
<div class="small">
<div class="prodimage"><a href="{{product.url}}"><img src="{{ product.featured_image | product_img_url: 'small' }}" /></a></div>
</div>
<div class="description">
<h3><a href="{{product.url}}">{{product.title}}</a></h3>
<p>{{ product.description | strip_html | truncatewords: 35 }}</p>
<p class="money">{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}</p>
</div>
</li>
{% endfor %}
</ul>
<div id="pagination">
{{ paginate | default_pagination }}
</div>
{% endpaginate %}

View File

@@ -1,47 +1,47 @@
<div id="frontproducts"><div id="frontproducts-top"><div id="frontproducts-bottom">
<h2 style="display: none;">Featured Items</h2>
{% for product in collections.frontpage.products limit:1 offset:0 %}
<div class="productmain">
<a href="{{ product.url }}"><img src="{{ product.featured_image | product_img_url: 'small' }}" alt="{{ product.title | escape }}" /></a>
<h3><a href="{{ product.url }}">{{ product.title }}</a></h3>
<div class="description">{{ product.description | strip_html | truncatewords: 18 }}</div>
<p class="money">{{ product.price_min | money }}</p>
</div>
{% endfor %}
{% for product in collections.frontpage.products offset:1 %}
<div class="product">
<a href="{{ product.url }}"><img src="{{ product.featured_image | product_img_url: 'thumb' }}" alt="{{ product.title | escape }}" /></a>
<h3><a href="{{ product.url }}">{{ product.title }}</a></h3>
<p class="money">{{ product.price_min | money }}</p>
</div>
{% endfor %}
</div></div></div>
<div id="mainarticle">
{% assign article = pages.frontpage %}
{% if article.content != "" %}
<h2>{{ article.title }}</h2>
<div class="article-body textile">
{{ article.content }}
</div>
{% else %}
<div class="article-body textile">
In <em>Admin &gt; Blogs &amp; Pages</em>, create a page with the handle <strong><code>frontpage</code></strong> and it will show up here.<br />
{{ "Learn more about handles" | link_to "http://wiki.shopify.com/Handle" }}
</div>
{% endif %}
</div>
<br style="clear: both;" />
<div id="articles">
{% for article in blogs.news.articles offset:1 %}
<div class="article">
<h2>{{ article.title }}</h2>
<div class="article-body textile">
{{ article.content }}
</div>
</div>
{% endfor %}
</div>
<div id="frontproducts"><div id="frontproducts-top"><div id="frontproducts-bottom">
<h2 style="display: none;">Featured Items</h2>
{% for product in collections.frontpage.products limit:1 offset:0 %}
<div class="productmain">
<a href="{{ product.url }}"><img src="{{ product.featured_image | product_img_url: 'small' }}" alt="{{ product.title | escape }}" /></a>
<h3><a href="{{ product.url }}">{{ product.title }}</a></h3>
<div class="description">{{ product.description | strip_html | truncatewords: 18 }}</div>
<p class="money">{{ product.price_min | money }}</p>
</div>
{% endfor %}
{% for product in collections.frontpage.products offset:1 %}
<div class="product">
<a href="{{ product.url }}"><img src="{{ product.featured_image | product_img_url: 'thumb' }}" alt="{{ product.title | escape }}" /></a>
<h3><a href="{{ product.url }}">{{ product.title }}</a></h3>
<p class="money">{{ product.price_min | money }}</p>
</div>
{% endfor %}
</div></div></div>
<div id="mainarticle">
{% assign article = pages.frontpage %}
{% if article.content != "" %}
<h2>{{ article.title }}</h2>
<div class="article-body textile">
{{ article.content }}
</div>
{% else %}
<div class="article-body textile">
In <em>Admin &gt; Blogs &amp; Pages</em>, create a page with the handle <strong><code>frontpage</code></strong> and it will show up here.<br />
{{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }}
</div>
{% endif %}
</div>
<br style="clear: both;" />
<div id="articles">
{% for article in blogs.news.articles offset:1 %}
<div class="article">
<h2>{{ article.title }}</h2>
<div class="article-body textile">
{{ article.content }}
</div>
</div>
{% endfor %}
</div>

View File

@@ -5,4 +5,4 @@
{{page.content}}
</div>
</div>
</div>

View File

@@ -1,68 +1,68 @@
<div id="productpage">
<div id="productimages"><div id="productimages-top"><div id="productimages-bottom">
{% for image in product.images %}
{% if forloop.first %}
<a href="{{ image | product_img_url: 'large' }}" class="productimage" rel="lightbox">
<img src="{{ image | product_img_url: 'medium'}}" alt="{{product.title | escape }}" />
</a>
{% else %}
<a href="{{ image | product_img_url: 'large' }}" class="productimage-small" rel="lightbox">
<img src="{{ image | product_img_url: 'small'}}" alt="{{product.title | escape }}" />
</a>
{% endif %}
{% endfor %}
</div></div></div>
<h2>{{ product.title }}</h2>
<ul id="details" class="hlist">
<li>Vendor: {{ product.vendor | link_to_vendor }}</li>
<li>Type: {{ product.type | link_to_type }}</li>
</ul>
<small>{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}</small>
<div id="variant-add">
<form action="/cart/add" method="post">
<select id="variant-select" name="id" class="product-info-options">
{% for variant in product.variants %}
<option value="{{ variant.id }}">{{ variant.title }} - {{ variant.price | money }}</option>
{% endfor %}
</select>
<div id="price-field" class="price"></div>
<div style="text-align:center;"><input type="image" name="add" value="Add to Cart" id="add" src="{{ 'addtocart.gif' | asset_url }}" /></div>
</form>
</div>
<div class="description textile">
{{ product.description }}
</div>
</div>
<script type="text/javascript">
<!--
// prototype callback for multi variants dropdown selector
var selectCallback = function(variant, selector) {
if (variant && variant.available == true) {
// selected a valid variant
$('add').removeClassName('disabled'); // remove unavailable class from add-to-cart button
$('add').disabled = false; // reenable add-to-cart button
$('price-field').innerHTML = Shopify.formatMoney(variant.price, "{{shop.money_with_currency_format}}"); // update price field
} else {
// variant doesn't exist
$('add').addClassName('disabled'); // set add-to-cart button to unavailable class
$('add').disabled = true; // disable add-to-cart button
$('price-field').innerHTML = (variant) ? "Sold Out" : "Unavailable"; // update price-field message
}
};
// initialize multi selector for product
Event.observe(document, 'dom:loaded', function() {
new Shopify.OptionSelectors("variant-select", { product: {{ product | json }}, onVariantSelected: selectCallback });
});
-->
</script>
<div id="productpage">
<div id="productimages"><div id="productimages-top"><div id="productimages-bottom">
{% for image in product.images %}
{% if forloop.first %}
<a href="{{ image | product_img_url: 'large' }}" class="productimage" rel="lightbox">
<img src="{{ image | product_img_url: 'medium'}}" alt="{{product.title | escape }}" />
</a>
{% else %}
<a href="{{ image | product_img_url: 'large' }}" class="productimage-small" rel="lightbox">
<img src="{{ image | product_img_url: 'small'}}" alt="{{product.title | escape }}" />
</a>
{% endif %}
{% endfor %}
</div></div></div>
<h2>{{ product.title }}</h2>
<ul id="details" class="hlist">
<li>Vendor: {{ product.vendor | link_to_vendor }}</li>
<li>Type: {{ product.type | link_to_type }}</li>
</ul>
<small>{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}</small>
<div id="variant-add">
<form action="/cart/add" method="post">
<select id="variant-select" name="id" class="product-info-options">
{% for variant in product.variants %}
<option value="{{ variant.id }}">{{ variant.title }} - {{ variant.price | money }}</option>
{% endfor %}
</select>
<div id="price-field" class="price"></div>
<div style="text-align:center;"><input type="image" name="add" value="Add to Cart" id="add" src="{{ 'addtocart.gif' | asset_url }}" /></div>
</form>
</div>
<div class="description textile">
{{ product.description }}
</div>
</div>
<script type="text/javascript">
<!--
// prototype callback for multi variants dropdown selector
var selectCallback = function(variant, selector) {
if (variant && variant.available == true) {
// selected a valid variant
$('add').removeClassName('disabled'); // remove unavailable class from add-to-cart button
$('add').disabled = false; // reenable add-to-cart button
$('price-field').innerHTML = Shopify.formatMoney(variant.price, "{{shop.money_with_currency_format}}"); // update price field
} else {
// variant doesn't exist
$('add').addClassName('disabled'); // set add-to-cart button to unavailable class
$('add').disabled = true; // disable add-to-cart button
$('price-field').innerHTML = (variant) ? "Sold Out" : "Unavailable"; // update price-field message
}
};
// initialize multi selector for product
Event.observe(document, 'dom:loaded', function() {
new Shopify.OptionSelectors("variant-select", { product: {{ product | json }}, onVariantSelected: selectCallback });
});
-->
</script>

View File

@@ -1,105 +1,105 @@
<!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" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>{{shop.name}} - {{page_title}}</title>
{{ 'textile.css' | global_asset_url | stylesheet_tag }}
{{ 'lightbox/v204/lightbox.css' | global_asset_url | stylesheet_tag }}
{{ 'prototype/1.6/prototype.js' | global_asset_url | script_tag }}
{{ 'scriptaculous/1.8.2/scriptaculous.js' | global_asset_url | script_tag }}
{{ 'lightbox/v204/lightbox.js' | global_asset_url | script_tag }}
{{ 'option_selection.js' | shopify_asset_url | script_tag }}
{{ 'layout.css' | asset_url | stylesheet_tag }}
{{ 'shop.js' | asset_url | script_tag }}
{{ content_for_header }}
</head>
<body id="page-{{template}}">
<p class="hide"><a href="#rightsiders">Skip to navigation.</a></p>
<!-- mini cart -->
{% if cart.item_count > 0 %}
<div id="minicart" style="display:none;"><div id="minicart-inner">
<div id="minicart-items">
<h2>There {{ cart.item_count | pluralize: 'is', 'are' }} {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }} in <a href="/cart" title="View your cart">your cart</a>!</h2><h4 style="font-size: 16px; margin: 0 0 10px 0; padding: 0;">Your subtotal is {{ cart.total_price | money }}.</h4>
{% for item in cart.items %}
<div class="thumb">
<div class="prodimage"><a href="{{item.product.url}}" onMouseover="tooltip('{{ item.quantity }} x {{ item.title }} ({{ item.variant.title }})', 200)"; onMouseout="hidetooltip()"><img src="{{ item.product.featured_image | product_img_url: 'thumb' }}" /></a></div>
</div>
{% endfor %}
</div>
<br style="clear:both;" />
</div></div>
{% endif %}
<div id="container">
<div id="header">
<!-- Begin Header -->
<h1 id="logo"><a href="/" title="Go Home">{{shop.name}}</a></h1>
<div id="cartlinks">
{% if cart.item_count > 0 %}
<h2 id="cartcount"><a href="/cart" onMouseover="tooltip('There {{ cart.item_count | pluralize: 'is', 'are' }} {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }} in your cart!', 200)"; onMouseout="hidetooltip()">{{ cart.item_count }} {{ cart.item_count | pluralize: 'thing', 'things' }}!</a></h2>
<a href="/cart" id="minicartswitch" onclick="superSwitch(this, 'minicart', 'Close Mini Cart'); return false;" id="cartswitch">View Mini Cart ({{ cart.total_price | money }})</a>
{% endif %}
</div>
<!-- End Header -->
</div>
<hr />
<div id="main">
<div id="content">
<div id="innercontent">
{{ content_for_layout }}
</div>
</div>
<hr />
<div id="rightsiders">
<ul class="rightlinks">
{% for link in linklists.main-menu.links %}
<li>{{ link.title | link_to: link.url }}</li>
{% endfor %}
</ul>
{% if tags %}
<ul class="rightlinks">
{% for tag in collection.tags %}
<li><span class="add-link">{{ '+' | link_to_add_tag: tag }}</span>{{ tag | highlight_active_tag | link_to_tag: tag }}</li>
{% endfor %}
</ul>
{% endif %}
<ul class="rightlinks">
{% for link in linklists.footer.links %}
<li>{{ link.title | link_to: link.url }}</li>
{% endfor %}
</ul>
</div>
<hr /><br style="clear:both;" />
<div id="footer">
<div class="footerinner">
All prices are in {{ shop.currency }}.
Powered by <a href="http://www.shopify.com" title="Shopify, Hosted E-Commerce">Shopify</a>.
</div>
</div>
</div>
</div>
<div id="tooltip"></div>
<img id="pointer" src="{{ 'arrow2.gif' | asset_url }}" />
</body>
</html>
<!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" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>{{shop.name}} - {{page_title}}</title>
{{ 'textile.css' | global_asset_url | stylesheet_tag }}
{{ 'lightbox/v204/lightbox.css' | global_asset_url | stylesheet_tag }}
{{ 'prototype/1.6/prototype.js' | global_asset_url | script_tag }}
{{ 'scriptaculous/1.8.2/scriptaculous.js' | global_asset_url | script_tag }}
{{ 'lightbox/v204/lightbox.js' | global_asset_url | script_tag }}
{{ 'option_selection.js' | shopify_asset_url | script_tag }}
{{ 'layout.css' | asset_url | stylesheet_tag }}
{{ 'shop.js' | asset_url | script_tag }}
{{ content_for_header }}
</head>
<body id="page-{{template}}">
<p class="hide"><a href="#rightsiders">Skip to navigation.</a></p>
<!-- mini cart -->
{% if cart.item_count > 0 %}
<div id="minicart" style="display:none;"><div id="minicart-inner">
<div id="minicart-items">
<h2>There {{ cart.item_count | pluralize: 'is', 'are' }} {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }} in <a href="/cart" title="View your cart">your cart</a>!</h2><h4 style="font-size: 16px; margin: 0 0 10px 0; padding: 0;">Your subtotal is {{ cart.total_price | money }}.</h4>
{% for item in cart.items %}
<div class="thumb">
<div class="prodimage"><a href="{{item.product.url}}" onMouseover="tooltip('{{ item.quantity }} x {{ item.title }} ({{ item.variant.title }})', 200)"; onMouseout="hidetooltip()"><img src="{{ item.product.featured_image | product_img_url: 'thumb' }}" /></a></div>
</div>
{% endfor %}
</div>
<br style="clear:both;" />
</div></div>
{% endif %}
<div id="container">
<div id="header">
<!-- Begin Header -->
<h1 id="logo"><a href="/" title="Go Home">{{shop.name}}</a></h1>
<div id="cartlinks">
{% if cart.item_count > 0 %}
<h2 id="cartcount"><a href="/cart" onMouseover="tooltip('There {{ cart.item_count | pluralize: 'is', 'are' }} {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }} in your cart!', 200)"; onMouseout="hidetooltip()">{{ cart.item_count }} {{ cart.item_count | pluralize: 'thing', 'things' }}!</a></h2>
<a href="/cart" id="minicartswitch" onclick="superSwitch(this, 'minicart', 'Close Mini Cart'); return false;" id="cartswitch">View Mini Cart ({{ cart.total_price | money }})</a>
{% endif %}
</div>
<!-- End Header -->
</div>
<hr />
<div id="main">
<div id="content">
<div id="innercontent">
{{ content_for_layout }}
</div>
</div>
<hr />
<div id="rightsiders">
<ul class="rightlinks">
{% for link in linklists.main-menu.links %}
<li>{{ link.title | link_to: link.url }}</li>
{% endfor %}
</ul>
{% if tags %}
<ul class="rightlinks">
{% for tag in collection.tags %}
<li><span class="add-link">{{ '+' | link_to_add_tag: tag }}</span>{{ tag | highlight_active_tag | link_to_tag: tag }}</li>
{% endfor %}
</ul>
{% endif %}
<ul class="rightlinks">
{% for link in linklists.footer.links %}
<li>{{ link.title | link_to: link.url }}</li>
{% endfor %}
</ul>
</div>
<hr /><br style="clear:both;" />
<div id="footer">
<div class="footerinner">
All prices are in {{ shop.currency }}.
Powered by <a href="http://www.shopify.com" title="Shopify, Hosted E-Commerce">Shopify</a>.
</div>
</div>
</div>
</div>
<div id="tooltip"></div>
<img id="pointer" src="{{ 'arrow2.gif' | asset_url }}" />
</body>
</html>

View File

@@ -4,7 +4,7 @@
<div class="article-body textile">
{{ article.content }}
</div>
</div>
</div>
@@ -12,7 +12,7 @@
{% if blog.comments_enabled? %}
<div id="comments">
<h3>Comments</h3>
<!-- List all comments -->
<ul id="comment-list">
{% for comment in article.comments %}
@@ -20,19 +20,19 @@
<div class="comment">
{{ comment.content }}
</div>
<div class="comment-details">
Posted by {{ comment.author }} on {{ comment.created_at | date: "%B %d, %Y" }}
<div class="comment-details">
Posted by {{ comment.author }} on {{ comment.created_at | date: "%B %d, %Y" }}
</div>
</li>
</li>
{% endfor %}
</ul>
<!-- Comment Form -->
<div id="comment-form">
{% form article %}
<h3>Leave a comment</h3>
<!-- Check if a comment has been submitted in the last request, and if yes display an appropriate message -->
{% if form.posted_successfully? %}
{% if blog.moderated? %}
@@ -44,11 +44,11 @@
<div class="notice">Successfully posted your comment.</div>
{% endif %}
{% endif %}
{% if form.errors %}
<div class="notice error">Not all the fields have been filled out correctly!</div>
{% endif %}
<dl>
<dt class="{% if form.errors contains 'author' %}error{% endif %}"><label for="comment_author">Your name</label></dt>
<dd><input type="text" id="comment_author" name="comment[author]" size="40" value="{{form.author}}" class="{% if form.errors contains 'author' %}input-error{% endif %}" /></dd>
@@ -59,16 +59,16 @@
<dt class="{% if form.errors contains 'body' %}error{% endif %}"><label for="comment_body">Your comment</label></dt>
<dd><textarea id="comment_body" name="comment[body]" cols="40" rows="5" class="{% if form.errors contains 'body' %}input-error{% endif %}">{{form.body}}</textarea></dd>
</dl>
{% if blog.moderated? %}
<p class="hint">comments have to be approved before showing up</p>
{% endif %}
{% endif %}
<input type="submit" value="Post comment" id="comment-submit" />
{% endform %}
</div>
<!-- END Comment Form -->
</div>
{% endif %}
<!-- END Comments -->
<!-- END Comments -->

View File

@@ -1,13 +1,13 @@
<div id="blog-page">
<h2 class="heading-shaded">{{page.title}}</h2>
{% for article in blog.articles %}
<h4>
{{ article.created_at | date: '%d %b' }}
<a href="{{article.url}}">{{ article.title }}</a>
</h4>
{{ article.content }}
{% if blog.comments_enabled? %}
<p><a href="{{article.url}}#comments">{{ article.comments_count }} comments</a></p>
{% endif %}
{% endfor %}
</div>
<div id="blog-page">
<h2 class="heading-shaded">{{page.title}}</h2>
{% for article in blog.articles %}
<h4>
{{ article.created_at | date: '%d %b' }}
<a href="{{article.url}}">{{ article.title }}</a>
</h4>
{{ article.content }}
{% if blog.comments_enabled? %}
<p><a href="{{article.url}}#comments">{{ article.comments_count }} comments</a></p>
{% endif %}
{% endfor %}
</div>

View File

@@ -7,9 +7,9 @@
<div id="cart-page">
{% if cart.item_count == 0 %}
{% if cart.item_count == 0 %}
<p>Your shopping cart is empty...</p>
<p><a href="/"><img src="{{ 'continue_shopping_icon.gif' | asset_url }}" alt="Continue shopping"/></a><p>
<p><a href="/"><img src="{{ 'continue_shopping_icon.gif' | asset_url }}" alt="Continue shopping"/></a><p>
{% else %}
<form action="/cart" method="post" id="cart">
@@ -26,21 +26,21 @@
{% for item in cart.items %}
<tr class="{% cycle 'odd', 'even' %}">
<td class="short">{{ item.product.featured_image | product_img_url: 'thumb' | img_tag }}</td>
<td><a href="{{item.product.url}}">{{ item.title }}</a></td>
<td><a href="{{item.product.url}}">{{ item.title }}</a></td>
<td class="short"><input type="text" class="quantity" name="updates[{{item.variant.id}}]" id="updates_{{item.variant.id}}" value="{{item.quantity}}" onfocus="this.select();"/></td>
<td class="cart-price">{{ item.price | money }}</td>
<td class="cart-price">{{item.line_price | money }}</td>
<td class="short"><a href="#" onclick="remove_item({{item.variant.id}}); return false;" class="remove"><img src="{{ 'cancel_icon.gif' | asset_url }}" alt="Remove" /></a></td>
</tr>
{% endfor %}
</table>
<p class="updatebtn"><input type="image" value="Update Cart" name="update" src="{{ 'update_icon.gif' | asset_url }}" alt="Update" /></p>
<p class="subtotal">
<strong>Subtotal:</strong> {{cart.total_price | money_with_currency }}
</p>
</table>
<p class="updatebtn"><input type="image" value="Update Cart" name="update" src="{{ 'update_icon.gif' | asset_url }}" alt="Update" /></p>
<p class="subtotal">
<strong>Subtotal:</strong> {{cart.total_price | money_with_currency }}
</p>
<p class="checkout"><input type="image" src="{{ 'checkout_icon.gif' | asset_url }}" alt="Proceed to Checkout" value="Proceed to Checkout" name="checkout" /></p>
{% if additional_checkout_buttons %}
{% if additional_checkout_buttons %}
<div class="additional-checkout-buttons">
<p>- or -</p>
{{ content_for_additional_checkout_buttons }}
@@ -48,7 +48,7 @@
{% endif %}
</form>
{% endif %}
</div>
</div>

View File

@@ -1,29 +1,29 @@
<div id="collection-page">
{% if collection.description %}
<div id="collection-description" class="textile">{{ collection.description }}</div>
{% endif %}
{% paginate collection.products by 20 %}
<ul id="product-collection">
{% for product in collection.products %}
<li class="single-product clearfix">
<div class="small">
<div class="prod-image"><a href="{{product.url}}"><img src="{{ product.featured_image | product_img_url: 'small' }}" alt="{{ product.title | escape }}" /></a></div>
</div>
<div class="prod-list-description">
<h3><a href="{{product.url}}">{{product.title}}</a></h3>
<p>{{ product.description | strip_html | truncatewords: 35 }}</p>
<p class="prd-price">{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}</p>
</div>
</li>
{% endfor %}
</ul>
<div id="pagination">
{{ paginate | default_pagination }}
</div>
{% endpaginate %}
</div>
<div id="collection-page">
{% if collection.description %}
<div id="collection-description" class="textile">{{ collection.description }}</div>
{% endif %}
{% paginate collection.products by 20 %}
<ul id="product-collection">
{% for product in collection.products %}
<li class="single-product clearfix">
<div class="small">
<div class="prod-image"><a href="{{product.url}}"><img src="{{ product.featured_image | product_img_url: 'small' }}" alt="{{ product.title | escape }}" /></a></div>
</div>
<div class="prod-list-description">
<h3><a href="{{product.url}}">{{product.title}}</a></h3>
<p>{{ product.description | strip_html | truncatewords: 35 }}</p>
<p class="prd-price">{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}</p>
</div>
</li>
{% endfor %}
</ul>
<div id="pagination">
{{ paginate | default_pagination }}
</div>
{% endpaginate %}
</div>

View File

@@ -1,32 +1,32 @@
<div id="home-page">
<h3 class="heading-shaded">Featured products...</h3>
<div class="featured-prod-row clearfix">
{% for product in collections.frontpage.products %}
<div class="featured-prod-item">
<p>
<a href="{{product.url}}"><img src="{{ product.featured_image | product_img_url: 'small' }}" alt="{{ product.title | escape }}"/></a>
</p>
<h4><a href="{{product.url}}">{{product.title}}</a></h4>
{% if product.compare_at_price %}
{% if product.price_min != product.compare_at_price %}
<p class="prd-price">Was:<del>{{product.compare_at_price | money}}</del></p>
<p class="prd-price"><ins>Now: {{product.price_min | money}}</ins></p>
{% endif %}
{% else %}
<p class="prd-price"><ins>{{product.price_min | money}}</ins></p>
{% endif %}
</div>
{% endfor %}
</div>
<div id="articles">
{% assign article = pages.frontpage %}
{% if article.content != "" %}
<h3>{{ article.title }}</h3>
{{ article.content }}
{% else %}
In <em>Admin &gt; Blogs &amp; Pages</em>, create a page with the handle <strong><code>frontpage</code></strong> and it will show up here.<br />
{{ "Learn more about handles" | link_to "http://wiki.shopify.com/Handle" }}
{% endif %}
</div>
</div>
<div id="home-page">
<h3 class="heading-shaded">Featured products...</h3>
<div class="featured-prod-row clearfix">
{% for product in collections.frontpage.products %}
<div class="featured-prod-item">
<p>
<a href="{{product.url}}"><img src="{{ product.featured_image | product_img_url: 'small' }}" alt="{{ product.title | escape }}"/></a>
</p>
<h4><a href="{{product.url}}">{{product.title}}</a></h4>
{% if product.compare_at_price %}
{% if product.price_min != product.compare_at_price %}
<p class="prd-price">Was:<del>{{product.compare_at_price | money}}</del></p>
<p class="prd-price"><ins>Now: {{product.price_min | money}}</ins></p>
{% endif %}
{% else %}
<p class="prd-price"><ins>{{product.price_min | money}}</ins></p>
{% endif %}
</div>
{% endfor %}
</div>
<div id="articles">
{% assign article = pages.frontpage %}
{% if article.content != "" %}
<h3>{{ article.title }}</h3>
{{ article.content }}
{% else %}
In <em>Admin &gt; Blogs &amp; Pages</em>, create a page with the handle <strong><code>frontpage</code></strong> and it will show up here.<br />
{{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }}
{% endif %}
</div>
</div>

View File

@@ -1,4 +1,4 @@
<div id="single-page">
<h2 class="heading-shaded">{{page.title}}</h2>
{{ page.content }}
</div>
<div id="single-page">
<h2 class="heading-shaded">{{page.title}}</h2>
{{ page.content }}
</div>

View File

@@ -1,75 +1,75 @@
<div id="product-page">
<h2 class="heading-shaded">{{ product.title }}</h2>
<div id="product-details">
<div id="product-images">
{% for image in product.images %}
{% if forloop.first %}
<a href="{{ image | product_img_url: 'large' }}" class="product-image" rel="lightbox[ product]" title="">
<img src="{{ image | product_img_url: 'medium'}}" alt="{{product.title | escape }}" />
</a>
{% else %}
<a href="{{ image | product_img_url: 'large' }}" class="product-image-small" rel="lightbox[ product]" title="">
<img src="{{ image | product_img_url: 'small'}}" alt="{{product.title | escape }}" />
</a>
{% endif %}
{% endfor %}
</div>
<ul id="product-info">
<li>Vendor: {{ product.vendor | link_to_vendor }}</li>
<li>Type: {{ product.type | link_to_type }}</li>
</ul>
<small>{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}</small>
<div id="product-options">
{% if product.available %}
<form action="/cart/add" method="post">
<select id="product-select" name='id'>
{% for variant in product.variants %}
<option value="{{ variant.id }}">{{ variant.title }} - {{ variant.price | money }}</option>
{% endfor %}
</select>
<div id="price-field"></div>
<div class="add-to-cart"><input type="image" name="add" value="Add to Cart" id="add" src="{{ 'add-to-cart.gif' | asset_url }}" /></div>
</form>
{% else %}
<span>Sold Out!</span>
{% endif %}
</div>
<div class="product-description">
{{ product.description }}
</div>
</div>
</div>
<script type="text/javascript">
<!--
// mootools callback for multi variants dropdown selector
var selectCallback = function(variant, selector) {
if (variant && variant.available == true) {
// selected a valid variant
$('add').removeClass('disabled'); // remove unavailable class from add-to-cart button
$('add').disabled = false; // reenable add-to-cart button
$('price-field').innerHTML = Shopify.formatMoney(variant.price, "{{shop.money_with_currency_format}}"); // update price field
} else {
// variant doesn't exist
$('add').addClass('disabled'); // set add-to-cart button to unavailable class
$('add').disabled = true; // disable add-to-cart button
$('price-field').innerHTML = (variant) ? "Sold Out" : "Unavailable"; // update price-field message
}
};
// initialize multi selector for product
window.addEvent('domready', function() {
new Shopify.OptionSelectors("product-select", { product: {{ product | json }}, onVariantSelected: selectCallback });
});
-->
</script>
<div id="product-page">
<h2 class="heading-shaded">{{ product.title }}</h2>
<div id="product-details">
<div id="product-images">
{% for image in product.images %}
{% if forloop.first %}
<a href="{{ image | product_img_url: 'large' }}" class="product-image" rel="lightbox[ product]" title="">
<img src="{{ image | product_img_url: 'medium'}}" alt="{{product.title | escape }}" />
</a>
{% else %}
<a href="{{ image | product_img_url: 'large' }}" class="product-image-small" rel="lightbox[ product]" title="">
<img src="{{ image | product_img_url: 'small'}}" alt="{{product.title | escape }}" />
</a>
{% endif %}
{% endfor %}
</div>
<ul id="product-info">
<li>Vendor: {{ product.vendor | link_to_vendor }}</li>
<li>Type: {{ product.type | link_to_type }}</li>
</ul>
<small>{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}</small>
<div id="product-options">
{% if product.available %}
<form action="/cart/add" method="post">
<select id="product-select" name='id'>
{% for variant in product.variants %}
<option value="{{ variant.id }}">{{ variant.title }} - {{ variant.price | money }}</option>
{% endfor %}
</select>
<div id="price-field"></div>
<div class="add-to-cart"><input type="image" name="add" value="Add to Cart" id="add" src="{{ 'add-to-cart.gif' | asset_url }}" /></div>
</form>
{% else %}
<span>Sold Out!</span>
{% endif %}
</div>
<div class="product-description">
{{ product.description }}
</div>
</div>
</div>
<script type="text/javascript">
<!--
// mootools callback for multi variants dropdown selector
var selectCallback = function(variant, selector) {
if (variant && variant.available == true) {
// selected a valid variant
$('add').removeClass('disabled'); // remove unavailable class from add-to-cart button
$('add').disabled = false; // reenable add-to-cart button
$('price-field').innerHTML = Shopify.formatMoney(variant.price, "{{shop.money_with_currency_format}}"); // update price field
} else {
// variant doesn't exist
$('add').addClass('disabled'); // set add-to-cart button to unavailable class
$('add').disabled = true; // disable add-to-cart button
$('price-field').innerHTML = (variant) ? "Sold Out" : "Unavailable"; // update price-field message
}
};
// initialize multi selector for product
window.addEvent('domready', function() {
new Shopify.OptionSelectors("product-select", { product: {{ product | json }}, onVariantSelected: selectCallback });
});
-->
</script>

View File

@@ -1,85 +1,85 @@
<!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" lang="en">
<head>
<title>{{shop.name}} - {{page_title}}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
{{ 'main.css' | asset_url | stylesheet_tag }}
{{ 'shop.js' | asset_url | script_tag }}
{{ 'mootools.js' | asset_url | script_tag }}
{{ 'slimbox.js' | asset_url | script_tag }}
{{ 'option_selection.js' | shopify_asset_url | script_tag }}
{{ 'slimbox.css' | asset_url | stylesheet_tag }}
{{ content_for_header }}
</head>
<body id="page-{{template}}">
<p class="hide"><a href="#navigation">Skip to navigation.</a></p>
<div id="wrapper">
<div class="content clearfix">
<div id="header">
<h2><a href="/">{{shop.name}}</a></h2>
</div>
<div id="left-col">
{{ content_for_layout }}
</div>
<div id="right-col">
{% if template != 'cart' %}
<div id="cart-right-col">
<dl id="cart-right-col-info">
<dt>Shopping Cart</dt>
<dd>
{% if cart.item_count != 0 %}
<a href="/cart">{{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }}</a> in your cart
{% else %}
Your cart is empty
{% endif %}
</dd>
</dl>
</div>
{% endif %}
<div id="search">
<dl id="searchbox">
<dt>Search</dt>
<dd>
<form action="/search" method="get">
<fieldset>
<input class="search-input" type="text" onclick="this.select()" value="Search this shop..." name="q" />
</fieldset>
</form>
</dd>
</dl>
</div>
<div id="navigation">
<dl class="navbar">
<dt>Navigation</dt>
{% for link in linklists.main-menu.links %}
<dd>{{ link.title | link_to: link.url }}</dd>
{% endfor %}
</dl>
{% if tags %}
<dl class="navbar">
<dt>Tags</dt>
{% for tag in collection.tags %}
<dd>{{ tag | highlight_active_tag | link_to_tag: tag }}</dd>
{% endfor %}
</dl>
{% endif %}
</div>
</div>
</div>
<div id="content-padding"></div>
</div>
<div id="footer">
{% for link in linklists.footer.links %}
{{ link.title | link_to: link.url }} {% if forloop.rindex != 1 %} | {% endif %}
{% endfor %}
</div>
</body>
</html>
<!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" lang="en">
<head>
<title>{{shop.name}} - {{page_title}}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
{{ 'main.css' | asset_url | stylesheet_tag }}
{{ 'shop.js' | asset_url | script_tag }}
{{ 'mootools.js' | asset_url | script_tag }}
{{ 'slimbox.js' | asset_url | script_tag }}
{{ 'option_selection.js' | shopify_asset_url | script_tag }}
{{ 'slimbox.css' | asset_url | stylesheet_tag }}
{{ content_for_header }}
</head>
<body id="page-{{template}}">
<p class="hide"><a href="#navigation">Skip to navigation.</a></p>
<div id="wrapper">
<div class="content clearfix">
<div id="header">
<h2><a href="/">{{shop.name}}</a></h2>
</div>
<div id="left-col">
{{ content_for_layout }}
</div>
<div id="right-col">
{% if template != 'cart' %}
<div id="cart-right-col">
<dl id="cart-right-col-info">
<dt>Shopping Cart</dt>
<dd>
{% if cart.item_count != 0 %}
<a href="/cart">{{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }}</a> in your cart
{% else %}
Your cart is empty
{% endif %}
</dd>
</dl>
</div>
{% endif %}
<div id="search">
<dl id="searchbox">
<dt>Search</dt>
<dd>
<form action="/search" method="get">
<fieldset>
<input class="search-input" type="text" onclick="this.select()" value="Search this shop..." name="q" />
</fieldset>
</form>
</dd>
</dl>
</div>
<div id="navigation">
<dl class="navbar">
<dt>Navigation</dt>
{% for link in linklists.main-menu.links %}
<dd>{{ link.title | link_to: link.url }}</dd>
{% endfor %}
</dl>
{% if tags %}
<dl class="navbar">
<dt>Tags</dt>
{% for tag in collection.tags %}
<dd>{{ tag | highlight_active_tag | link_to_tag: tag }}</dd>
{% endfor %}
</dl>
{% endif %}
</div>
</div>
</div>
<div id="content-padding"></div>
</div>
<div id="footer">
{% for link in linklists.footer.links %}
{{ link.title | link_to: link.url }} {% if forloop.rindex != 1 %} | {% endif %}
{% endfor %}
</div>
</body>
</html>

View File

@@ -1,56 +1,56 @@
<div id="page" class="innerpage clearfix">
<div id="text-page">
<div class="entry">
<h1>Oh no!</h1>
<div class="entry-post">
Seems like you are looking for something that just isn't here. <a href="/">Try heading back to our main page</a>. Or you can checkout some of our featured products below.
</div>
</div>
</div>
<div id="page" class="innerpage clearfix">
<h1>Featured Products</h1>
<ul class="item-list clearfix">
{% for product in collections.frontpage.products %}
<li>
<form action="/cart/add" method="post">
<div class="item-list-item">
<div class="ili-top clearfix">
<div class="ili-top-content">
<h2><a href="{{product.url}}">{{product.title}}</a></h2>
<p>{{ product.description | truncatewords: 15 }}</p>
</div>
<a href="{{product.url}}" class="ili-top-image"><img src="{{ product.featured_image | product_img_url: 'small' }}" alt="{{ product.title | escape }}"/></a>
</div>
<div class="ili-bottom clearfix">
<p class="hiddenvariants" style="display: none">{% for variant in product.variants %}<span><input type="radio" name="id" value="{{variant.id}}" id="radio_{{variant.id}}" style="vertical-align: middle;" {%if forloop.first%} checked="checked" {%endif%} /><label for="radio_{{variant.id}}">{{ variant.price | money_with_currency }} - {{ variant.title }}</label></span>{% endfor %}</p>
<input type="submit" class="" value="Add to Basket" />
<p>
<a href="{{product.url}}">View Details</a>
<span>
{% if product.compare_at_price %}
{% if product.price_min != product.compare_at_price %}
{{product.compare_at_price | money}} -
{% endif %}
{% endif %}
<strong>
{{product.price_min | money}}
</strong>
</span>
</p>
</div>
</div>
</form>
</li>
{% endfor %}
<div id="text-page">
<div class="entry">
<h1>Oh no!</h1>
<div class="entry-post">
Seems like you are looking for something that just isn't here. <a href="/">Try heading back to our main page</a>. Or you can checkout some of our featured products below.
</div>
</div>
</div>
</ul>
</div>
<!-- end page -->
<h1>Featured Products</h1>
<ul class="item-list clearfix">
{% for product in collections.frontpage.products %}
<li>
<form action="/cart/add" method="post">
<div class="item-list-item">
<div class="ili-top clearfix">
<div class="ili-top-content">
<h2><a href="{{product.url}}">{{product.title}}</a></h2>
<p>{{ product.description | truncatewords: 15 }}</p>
</div>
<a href="{{product.url}}" class="ili-top-image"><img src="{{ product.featured_image | product_img_url: 'small' }}" alt="{{ product.title | escape }}"/></a>
</div>
<div class="ili-bottom clearfix">
<p class="hiddenvariants" style="display: none">{% for variant in product.variants %}<span><input type="radio" name="id" value="{{variant.id}}" id="radio_{{variant.id}}" style="vertical-align: middle;" {%if forloop.first%} checked="checked" {%endif%} /><label for="radio_{{variant.id}}">{{ variant.price | money_with_currency }} - {{ variant.title }}</label></span>{% endfor %}</p>
<input type="submit" class="" value="Add to Basket" />
<p>
<a href="{{product.url}}">View Details</a>
<span>
{% if product.compare_at_price %}
{% if product.price_min != product.compare_at_price %}
{{product.compare_at_price | money}} -
{% endif %}
{% endif %}
<strong>
{{product.price_min | money}}
</strong>
</span>
</p>
</div>
</div>
</form>
</li>
{% endfor %}
</ul>
</div>
<!-- end page -->

Some files were not shown because too many files have changed in this diff Show More