liquid2 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/.rubocop.yml +46 -0
  4. data/.ruby-version +1 -0
  5. data/.vscode/settings.json +32 -0
  6. data/CHANGELOG.md +5 -0
  7. data/LICENSE.txt +21 -0
  8. data/LICENSE_SHOPIFY.txt +20 -0
  9. data/README.md +219 -0
  10. data/Rakefile +23 -0
  11. data/Steepfile +26 -0
  12. data/lib/liquid2/context.rb +297 -0
  13. data/lib/liquid2/environment.rb +287 -0
  14. data/lib/liquid2/errors.rb +79 -0
  15. data/lib/liquid2/expression.rb +20 -0
  16. data/lib/liquid2/expressions/arguments.rb +25 -0
  17. data/lib/liquid2/expressions/array.rb +20 -0
  18. data/lib/liquid2/expressions/blank.rb +41 -0
  19. data/lib/liquid2/expressions/boolean.rb +20 -0
  20. data/lib/liquid2/expressions/filtered.rb +136 -0
  21. data/lib/liquid2/expressions/identifier.rb +43 -0
  22. data/lib/liquid2/expressions/lambda.rb +53 -0
  23. data/lib/liquid2/expressions/logical.rb +71 -0
  24. data/lib/liquid2/expressions/loop.rb +79 -0
  25. data/lib/liquid2/expressions/path.rb +33 -0
  26. data/lib/liquid2/expressions/range.rb +28 -0
  27. data/lib/liquid2/expressions/relational.rb +119 -0
  28. data/lib/liquid2/expressions/template_string.rb +20 -0
  29. data/lib/liquid2/filter.rb +95 -0
  30. data/lib/liquid2/filters/array.rb +202 -0
  31. data/lib/liquid2/filters/date.rb +20 -0
  32. data/lib/liquid2/filters/default.rb +16 -0
  33. data/lib/liquid2/filters/json.rb +15 -0
  34. data/lib/liquid2/filters/math.rb +87 -0
  35. data/lib/liquid2/filters/size.rb +11 -0
  36. data/lib/liquid2/filters/slice.rb +17 -0
  37. data/lib/liquid2/filters/sort.rb +96 -0
  38. data/lib/liquid2/filters/string.rb +204 -0
  39. data/lib/liquid2/loader.rb +59 -0
  40. data/lib/liquid2/loaders/file_system_loader.rb +76 -0
  41. data/lib/liquid2/loaders/mixins.rb +52 -0
  42. data/lib/liquid2/node.rb +113 -0
  43. data/lib/liquid2/nodes/comment.rb +18 -0
  44. data/lib/liquid2/nodes/output.rb +24 -0
  45. data/lib/liquid2/nodes/tags/assign.rb +35 -0
  46. data/lib/liquid2/nodes/tags/block_comment.rb +26 -0
  47. data/lib/liquid2/nodes/tags/capture.rb +40 -0
  48. data/lib/liquid2/nodes/tags/case.rb +111 -0
  49. data/lib/liquid2/nodes/tags/cycle.rb +63 -0
  50. data/lib/liquid2/nodes/tags/decrement.rb +29 -0
  51. data/lib/liquid2/nodes/tags/doc.rb +24 -0
  52. data/lib/liquid2/nodes/tags/echo.rb +31 -0
  53. data/lib/liquid2/nodes/tags/extends.rb +3 -0
  54. data/lib/liquid2/nodes/tags/for.rb +155 -0
  55. data/lib/liquid2/nodes/tags/if.rb +84 -0
  56. data/lib/liquid2/nodes/tags/include.rb +123 -0
  57. data/lib/liquid2/nodes/tags/increment.rb +29 -0
  58. data/lib/liquid2/nodes/tags/inline_comment.rb +28 -0
  59. data/lib/liquid2/nodes/tags/liquid.rb +29 -0
  60. data/lib/liquid2/nodes/tags/macro.rb +3 -0
  61. data/lib/liquid2/nodes/tags/raw.rb +30 -0
  62. data/lib/liquid2/nodes/tags/render.rb +137 -0
  63. data/lib/liquid2/nodes/tags/tablerow.rb +143 -0
  64. data/lib/liquid2/nodes/tags/translate.rb +3 -0
  65. data/lib/liquid2/nodes/tags/unless.rb +23 -0
  66. data/lib/liquid2/nodes/tags/with.rb +3 -0
  67. data/lib/liquid2/parser.rb +917 -0
  68. data/lib/liquid2/scanner.rb +595 -0
  69. data/lib/liquid2/static_analysis.rb +301 -0
  70. data/lib/liquid2/tag.rb +22 -0
  71. data/lib/liquid2/template.rb +182 -0
  72. data/lib/liquid2/undefined.rb +131 -0
  73. data/lib/liquid2/utils/cache.rb +80 -0
  74. data/lib/liquid2/utils/chain_hash.rb +40 -0
  75. data/lib/liquid2/utils/unescape.rb +119 -0
  76. data/lib/liquid2/version.rb +5 -0
  77. data/lib/liquid2.rb +90 -0
  78. data/performance/benchmark.rb +73 -0
  79. data/performance/memory_profile.rb +62 -0
  80. data/performance/profile.rb +71 -0
  81. data/sig/liquid2.rbs +2348 -0
  82. data.tar.gz.sig +0 -0
  83. metadata +164 -0
  84. metadata.gz.sig +0 -0
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../tag"
4
+
5
+ module Liquid2
6
+ # The standard _liquid_ tag.
7
+ class LiquidTag < Tag
8
+ # @param parser [Parser]
9
+ # @return [LiquidTag]
10
+ def self.parse(token, parser)
11
+ block = parser.parse_line_statements
12
+ parser.carry_whitespace_control
13
+ parser.eat(:token_tag_end)
14
+ new(token, block)
15
+ end
16
+
17
+ def initialize(token, block)
18
+ super(token)
19
+ @block = block
20
+ @blank = block.blank
21
+ end
22
+
23
+ def render(context, buffer)
24
+ @block.render(context, buffer)
25
+ end
26
+
27
+ def children(_static_context, include_partials: true) = [@block]
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../tag"
4
+
5
+ module Liquid2
6
+ # The standard _raw_ tag.
7
+ class RawTag < Tag
8
+ # @param parser [Parser]
9
+ # @return [RawTag]
10
+ def self.parse(token, parser)
11
+ parser.carry_whitespace_control
12
+ parser.eat(:token_tag_end)
13
+ # TODO: apply whitespace control to raw text
14
+ raw = parser.eat(:token_raw)
15
+ parser.eat_empty_tag("endraw")
16
+ new(token, raw[1] || raise)
17
+ end
18
+
19
+ # @param text [String]
20
+ def initialize(token, text)
21
+ super(token)
22
+ @text = text
23
+ @blank = false
24
+ end
25
+
26
+ def render(_context, buffer)
27
+ buffer << @text
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../tag"
4
+ require_relative "for"
5
+
6
+ module Liquid2
7
+ DISABLED_TAGS = Set["include"]
8
+
9
+ # The standard _render_ tag.
10
+ class RenderTag < Tag
11
+ # @param parser [Parser]
12
+ # @return [RenderTag]
13
+ def self.parse(token, parser)
14
+ name = parser.parse_string
15
+ raise LiquidTypeError, "expected a string literal" unless name.is_a?(String)
16
+
17
+ repeat = false
18
+ var = nil # : Expression?
19
+ as = nil # : Identifier?
20
+
21
+ if parser.current_kind == :token_for && !%i[token_comma
22
+ token_colon].include?(parser.peek_kind)
23
+ parser.next
24
+ repeat = true
25
+ var = parser.parse_primary
26
+ if parser.current_kind == :token_as
27
+ parser.next
28
+ as = parser.parse_identifier
29
+ end
30
+ elsif parser.current_kind == :token_with && !%i[token_comma
31
+ token_colon].include?(parser.peek_kind)
32
+ parser.next
33
+ var = parser.parse_primary
34
+ if parser.current_kind == :token_as
35
+ parser.next
36
+ as = parser.parse_identifier
37
+ end
38
+ end
39
+
40
+ parser.next if parser.current_kind == :token_comma
41
+ args = parser.parse_keyword_arguments
42
+ parser.carry_whitespace_control
43
+ parser.eat(:token_tag_end)
44
+ new(token, name, repeat, var, as, args)
45
+ end
46
+
47
+ # @param name [String]
48
+ # @param repeat [bool]
49
+ # @param var [Expression?]
50
+ # @param as [Identifier?]
51
+ # @param args [Array<KeywordArgument> | nil]
52
+ def initialize(token, name, repeat, var, as, args)
53
+ super(token)
54
+ @name = name
55
+ @repeat = repeat
56
+ @var = var
57
+ @as = as&.name
58
+ @args = args
59
+ @blank = false
60
+ end
61
+
62
+ def render(context, buffer)
63
+ template = context.env.get_template(@name, context: context, tag: :render)
64
+ namespace = @args.to_h { |arg| [arg.name, context.evaluate(arg.value)] }
65
+
66
+ ctx = context.copy(namespace,
67
+ template: template,
68
+ disabled_tags: DISABLED_TAGS,
69
+ carry_loop_iterations: true)
70
+
71
+ if @var
72
+ val = context.evaluate(@var || raise)
73
+ key = @as || template.name.split(".").first
74
+
75
+ if @repeat && val.respond_to?(:[]) && val.respond_to?(:size)
76
+ ctx.raise_for_loop_limit(length: val.size)
77
+
78
+ forloop = ForLoop.new(
79
+ key, val.size, context.env.undefined("parentloop")
80
+ )
81
+
82
+ namespace["forloop"] = forloop
83
+
84
+ index = 0
85
+ while index < val.length
86
+ namespace[key] = val[index]
87
+ index += 1
88
+ forloop.next
89
+ template.render_with_context(ctx, buffer, partial: true, block_scope: true)
90
+ end
91
+ else
92
+ namespace[key] = val
93
+ template.render_with_context(ctx, buffer, partial: true, block_scope: true)
94
+ end
95
+ else
96
+ template.render_with_context(ctx, buffer, partial: true, block_scope: true)
97
+ end
98
+ rescue LiquidTemplateNotFoundError => e
99
+ e.token = @name
100
+ e.template_name = context.template.full_name
101
+ raise e
102
+ end
103
+
104
+ def children(static_context, include_partials: true)
105
+ return [] unless include_partials
106
+
107
+ name = static_context.evaluate(@name)
108
+ template = static_context.env.get_template(name.to_s, context: static_context, tag: :include)
109
+ template.ast
110
+ rescue LiquidTemplateNotFoundError => e
111
+ e.token = @token
112
+ e.template_name = static_context.template.full_name
113
+ raise e
114
+ end
115
+
116
+ def expressions
117
+ exprs = [@name]
118
+ exprs << @var if @var
119
+ exprs.concat(@args.map(&:value))
120
+ exprs
121
+ end
122
+
123
+ def partial_scope
124
+ scope = @args.map { |arg| Identifier.new([:token_word, arg.name, arg.token.last]) }
125
+
126
+ if @var
127
+ if @as
128
+ scope << @as # steep:ignore
129
+ elsif @name.is_a?(String)
130
+ scope << Identifier.new([:token_word, @name.split(".").first, @token.last])
131
+ end
132
+ end
133
+
134
+ Partial.new(@name, :isolated, scope)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../tag"
4
+
5
+ module Liquid2
6
+ # The standard _tablerow_ tag.
7
+ class TableRowTag < Tag
8
+ END_BLOCK = Set["endtablerow"]
9
+
10
+ def self.parse(token, parser)
11
+ expression = parser.parse_loop_expression
12
+ parser.carry_whitespace_control
13
+ parser.eat(:token_tag_end)
14
+ block = parser.parse_block(END_BLOCK)
15
+ parser.eat_empty_tag("endtablerow")
16
+ new(token, expression, block)
17
+ end
18
+
19
+ def initialize(token, expression, block)
20
+ super(token)
21
+ @expression = expression
22
+ @block = block
23
+ @blank = false
24
+ end
25
+
26
+ def render(context, buffer)
27
+ array = @expression.evaluate(context)
28
+ name = @expression.identifier.name
29
+
30
+ context.raise_for_loop_limit(length: array.length)
31
+
32
+ cols = if @expression.cols
33
+ Liquid2.to_liquid_int(context.evaluate(@expression.cols))
34
+ else
35
+ array.length
36
+ end
37
+
38
+ drop = TableRow.new(@expression.name, array.length, cols)
39
+ namespace = { "tablerowloop" => drop }
40
+
41
+ buffer << "<tr class=\"row1\">\n"
42
+
43
+ context.extend(namespace) do
44
+ index = 0
45
+ while index < array.length
46
+ namespace[name] = array[index]
47
+ index += 1
48
+ drop.next
49
+
50
+ buffer << "<td class=\"col#{drop.col}\">"
51
+ @block.render(context, buffer)
52
+ buffer << "</td>"
53
+
54
+ buffer << "</tr>\n<tr class=\"row#{drop.row + 1}\">" if drop.col_last && !drop.last
55
+
56
+ case context.interrupts.pop
57
+ when :continue
58
+ next
59
+ when :break
60
+ break
61
+ end
62
+ end
63
+
64
+ buffer << "</tr>\n"
65
+ end
66
+ end
67
+
68
+ def children(_static_context, include_partials: true) = [@block]
69
+ def expressions = [@expression]
70
+
71
+ def block_scope
72
+ [@expression.identifier,
73
+ Identifier.new([:token_word, "tablerowloop", @expression.token.last])]
74
+ end
75
+ end
76
+
77
+ # `tablerow` loop helper variables.
78
+ class TableRow
79
+ attr_reader :name, :length, :col, :row
80
+
81
+ KEYS = Set[
82
+ "name",
83
+ "length",
84
+ "index",
85
+ "index0",
86
+ "rindex",
87
+ "rindex0",
88
+ "first",
89
+ "last",
90
+ "col",
91
+ "col0",
92
+ "col_first",
93
+ "col_last",
94
+ "row"
95
+ ]
96
+
97
+ def initialize(name, length, cols)
98
+ @name = name
99
+ @length = length
100
+ @cols = cols
101
+ @index = -1
102
+ @row = 1
103
+ @col = 0
104
+ end
105
+
106
+ def key?(key)
107
+ KEYS.member?(key)
108
+ end
109
+
110
+ def [](key)
111
+ send(key) if KEYS.member?(key)
112
+ end
113
+
114
+ def fetch(key, default = :undefined)
115
+ if KEYS.member?(key)
116
+ send(key)
117
+ else
118
+ default
119
+ end
120
+ end
121
+
122
+ def next
123
+ @index += 1
124
+
125
+ if @col == @cols
126
+ @col = 1
127
+ @row += 1
128
+ else
129
+ @col += 1
130
+ end
131
+ end
132
+
133
+ def index = @index + 1
134
+ def index0 = @index
135
+ def rindex = @length - @index
136
+ def rindex0 = @length - @index - 1
137
+ def first = @index.zero?
138
+ def last = @index == @length - 1
139
+ def col0 = @col - 1
140
+ def col_first = @col == 1
141
+ def col_last = @col == @cols
142
+ end
143
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "if"
4
+
5
+ module Liquid2
6
+ # The standard _unless_ tag.
7
+ class UnlessTag < IfTag
8
+ END_TAG = "endunless"
9
+ END_BLOCK = Set["else", "elsif", "endunless"].freeze
10
+
11
+ def render(context, buffer)
12
+ return @block.render(context, buffer) unless @expression.evaluate(context)
13
+
14
+ @alternatives.each do |alt|
15
+ return alt.block.render(context, buffer) if alt.expression.evaluate(context)
16
+ end
17
+
18
+ return (@default || raise).render(context, buffer) if @default
19
+
20
+ 0
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO