1
1
# frozen_string_literal: true
2
2
3
+ require "lru_redux"
4
+
3
5
module Liquid
4
6
class Expression
5
7
LITERALS = {
@@ -10,37 +12,106 @@ class Expression
10
12
'true' => true ,
11
13
'false' => false ,
12
14
'blank' => '' ,
13
- 'empty' => ''
15
+ 'empty' => '' ,
16
+ # in lax mode, minus sign can be a VariableLookup
17
+ # For simplicity and performace, we treat it like a literal
18
+ '-' => VariableLookup . parse ( "-" , nil ) . freeze ,
14
19
} . freeze
15
20
16
- INTEGERS_REGEX = /\A (-?\d +)\z /
17
- FLOATS_REGEX = /\A (-?\d [\d \. ]+)\z /
21
+ DOT = "." . ord
22
+ ZERO = "0" . ord
23
+ NINE = "9" . ord
24
+ DASH = "-" . ord
18
25
19
26
# Use an atomic group (?>...) to avoid pathological backtracing from
20
27
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
21
- RANGES_REGEX = /\A \( \s *(?>(\S +)\s *\. \. )\s *(\S +)\s *\) \z /
28
+ RANGES_REGEX = /\A \( \s *(?>(\S +)\s *\. \. )\s *(\S +)\s *\) \z /
29
+ INTEGER_REGEX = /\A (-?\d +)\z /
30
+ FLOAT_REGEX = /\A (-?\d +)\. \d +\z /
31
+
32
+ class << self
33
+ def parse ( markup , ss = StringScanner . new ( "" ) , cache = nil )
34
+ return unless markup
35
+
36
+ markup = markup . strip # markup can be a frozen string
22
37
23
- def self . parse ( markup )
24
- return nil unless markup
38
+ if ( markup . start_with? ( '"' ) && markup . end_with? ( '"' ) ) ||
39
+ ( markup . start_with? ( "'" ) && markup . end_with? ( "'" ) )
40
+ return markup [ 1 ..-2 ]
41
+ elsif LITERALS . key? ( markup )
42
+ return LITERALS [ markup ]
43
+ end
44
+
45
+ # Cache only exists during parsing
46
+ if cache
47
+ return cache [ markup ] if cache . key? ( markup )
25
48
26
- markup = markup . strip
27
- if ( markup . start_with? ( '"' ) && markup . end_with? ( '"' ) ) ||
28
- ( markup . start_with? ( "'" ) && markup . end_with? ( "'" ) )
29
- return markup [ 1 ..- 2 ]
49
+ cache [ markup ] = inner_parse ( markup , ss , cache ) . freeze
50
+ else
51
+ inner_parse ( markup , ss , nil ) . freeze
52
+ end
30
53
end
31
54
32
- case markup
33
- when INTEGERS_REGEX
34
- Regexp . last_match ( 1 ) . to_i
35
- when RANGES_REGEX
36
- RangeLookup . parse ( Regexp . last_match ( 1 ) , Regexp . last_match ( 2 ) )
37
- when FLOATS_REGEX
38
- Regexp . last_match ( 1 ) . to_f
39
- else
40
- if LITERALS . key? ( markup )
41
- LITERALS [ markup ]
55
+ def inner_parse ( markup , ss , cache )
56
+ if ( markup . start_with? ( "(" ) && markup . end_with? ( ")" ) ) && markup =~ RANGES_REGEX
57
+ return RangeLookup . parse (
58
+ Regexp . last_match ( 1 ) ,
59
+ Regexp . last_match ( 2 ) ,
60
+ ss ,
61
+ cache ,
62
+ )
63
+ end
64
+
65
+ if ( num = parse_number ( markup , ss ) )
66
+ num
67
+ else
68
+ VariableLookup . parse ( markup , ss , cache )
69
+ end
70
+ end
71
+
72
+ def parse_number ( markup , ss )
73
+ # check if the markup is simple integer or float
74
+ case markup
75
+ when INTEGER_REGEX
76
+ return Integer ( markup , 10 )
77
+ when FLOAT_REGEX
78
+ return markup . to_f
79
+ end
80
+
81
+ ss . string = markup
82
+ # the first byte must be a digit, a period, or a dash
83
+ byte = ss . scan_byte
84
+
85
+ return false if byte != DASH && byte != DOT && ( byte < ZERO || byte > NINE )
86
+
87
+ # The markup could be a float with multiple dots
88
+ first_dot_pos = nil
89
+ num_end_pos = nil
90
+
91
+ while ( byte = ss . scan_byte )
92
+ return false if byte != DOT && ( byte < ZERO || byte > NINE )
93
+
94
+ # we found our number and now we are just scanning the rest of the string
95
+ next if num_end_pos
96
+
97
+ if byte == DOT
98
+ if first_dot_pos . nil?
99
+ first_dot_pos = ss . pos
100
+ else
101
+ # we found another dot, so we know that the number ends here
102
+ num_end_pos = ss . pos - 1
103
+ end
104
+ end
105
+ end
106
+
107
+ num_end_pos = markup . length if ss . eos?
108
+
109
+ if num_end_pos
110
+ # number ends with a number "123.123"
111
+ markup . byteslice ( 0 , num_end_pos ) . to_f
42
112
else
43
- VariableLookup . parse ( markup )
113
+ # number ends with a dot "123."
114
+ markup . byteslice ( 0 , first_dot_pos ) . to_f
44
115
end
45
116
end
46
117
end
0 commit comments