phlex 2.3.2 → 2.4.0.beta1

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.
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require "refract"
5
+
6
+ module Phlex::Compiler
7
+ Error = Class.new(StandardError)
8
+
9
+ def self.compile(component)
10
+ path, line = Object.const_source_location(component.name)
11
+ return unless File.exist?(path)
12
+ source = File.read(path)
13
+ tree = Prism.parse(source).value
14
+ refract = Refract::Converter.new.visit(tree)
15
+
16
+ Compilation.new(component, path, line, source, refract).compile
17
+ end
18
+ end
data/lib/phlex/html.rb CHANGED
@@ -55,10 +55,10 @@ class Phlex::HTML < Phlex::SGML
55
55
  raise Phlex::ArgumentError.new("Expected the tag name to be a Symbol.")
56
56
  end
57
57
 
58
- if (tag = StandardElements.__registered_elements__[name]) || ((tag = name.name.tr("_", "-")).include?("-") && tag.match?(/\A[a-z0-9-]+\z/))
58
+ if (tag = StandardElements.__registered_elements__[name]) || (tag = name.name.tr("_", "-")).include?("-")
59
59
  if attributes.length > 0 # with attributes
60
60
  if block_given # with content block
61
- buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"
61
+ buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes)) << ">"
62
62
  if tag == "svg"
63
63
  render Phlex::SVG.new(&)
64
64
  else
@@ -66,7 +66,7 @@ class Phlex::HTML < Phlex::SGML
66
66
  end
67
67
  buffer << "</#{tag}>"
68
68
  else # without content
69
- buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << "></#{tag}>"
69
+ buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes)) << "></#{tag}>"
70
70
  end
71
71
  else # without attributes
72
72
  if block_given # with content block
@@ -87,7 +87,7 @@ class Phlex::HTML < Phlex::SGML
87
87
  end
88
88
 
89
89
  if attributes.length > 0 # with attributes
90
- buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"
90
+ buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes)) << ">"
91
91
  else # without attributes
92
92
  buffer << "<#{tag}>"
93
93
  end
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex::SGML::Attributes
4
+ extend self
5
+
6
+ UNSAFE_ATTRIBUTES = Set.new(%w[srcdoc sandbox http-equiv]).freeze
7
+ REF_ATTRIBUTES = Set.new(%w[href src action formaction lowsrc dynsrc background ping]).freeze
8
+
9
+ def generate_attributes(attributes, buffer = +"")
10
+ attributes.each do |k, v|
11
+ next unless v
12
+
13
+ name = case k
14
+ when String then k
15
+ when Symbol then k.name.tr("_", "-")
16
+ else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols.")
17
+ end
18
+
19
+ value = case v
20
+ when true
21
+ true
22
+ when String
23
+ v.gsub('"', "&quot;")
24
+ when Symbol
25
+ v.name.tr("_", "-").gsub('"', "&quot;")
26
+ when Integer, Float
27
+ v.to_s
28
+ when Date
29
+ v.iso8601
30
+ when Time
31
+ v.respond_to?(:iso8601) ? v.iso8601 : v.strftime("%Y-%m-%dT%H:%M:%S%:z")
32
+ when Hash
33
+ case k
34
+ when :style
35
+ generate_styles(v).gsub('"', "&quot;")
36
+ else
37
+ generate_nested_attributes(v, "#{name}-", buffer)
38
+ end
39
+ when Array
40
+ case k
41
+ when :style
42
+ generate_styles(v).gsub('"', "&quot;")
43
+ else
44
+ generate_nested_tokens(v)
45
+ end
46
+ when Set
47
+ case k
48
+ when :style
49
+ generate_styles(v).gsub('"', "&quot;")
50
+ else
51
+ generate_nested_tokens(v.to_a)
52
+ end
53
+ when Phlex::SGML::SafeObject
54
+ v.to_s.gsub('"', "&quot;")
55
+ else
56
+ raise Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
57
+ end
58
+
59
+ lower_name = name.downcase
60
+
61
+ unless Phlex::SGML::SafeObject === v
62
+ normalized_name = lower_name.delete("^a-z-")
63
+
64
+ if value != true && REF_ATTRIBUTES.include?(normalized_name)
65
+ case value
66
+ when String
67
+ if value.downcase.delete("^a-z:").start_with?("javascript:")
68
+ # We just ignore these because they were likely not specified by the developer.
69
+ next
70
+ end
71
+ else
72
+ raise Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
73
+ end
74
+ end
75
+
76
+ if normalized_name.bytesize > 2 && normalized_name.start_with?("on") && !normalized_name.include?("-")
77
+ raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
78
+ end
79
+
80
+ if UNSAFE_ATTRIBUTES.include?(normalized_name)
81
+ raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
82
+ end
83
+ end
84
+
85
+ if name.match?(/[<>&"']/)
86
+ raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
87
+ end
88
+
89
+ if lower_name.to_sym == :id && k != :id
90
+ raise Phlex::ArgumentError.new(":id attribute should only be passed as a lowercase symbol.")
91
+ end
92
+
93
+ case value
94
+ when true
95
+ buffer << " " << name
96
+ when String
97
+ buffer << " " << name << '="' << value << '"'
98
+ end
99
+ end
100
+
101
+ buffer
102
+ end
103
+
104
+ # Provides the nested-attributes case for serializing out attributes.
105
+ # This allows us to skip many of the checks the `__attributes__` method must perform.
106
+ def generate_nested_attributes(attributes, base_name, buffer = +"")
107
+ attributes.each do |k, v|
108
+ next unless v
109
+
110
+ if (root_key = (:_ == k))
111
+ name = ""
112
+ original_base_name = base_name
113
+ base_name = base_name.delete_suffix("-")
114
+ else
115
+ name = case k
116
+ when String then k
117
+ when Symbol then k.name.tr("_", "-")
118
+ else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols")
119
+ end
120
+
121
+ if name.match?(/[<>&"']/)
122
+ raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
123
+ end
124
+ end
125
+
126
+ case v
127
+ when true
128
+ buffer << " " << base_name << name
129
+ when String
130
+ buffer << " " << base_name << name << '="' << v.gsub('"', "&quot;") << '"'
131
+ when Symbol
132
+ buffer << " " << base_name << name << '="' << v.name.tr("_", "-").gsub('"', "&quot;") << '"'
133
+ when Integer, Float
134
+ buffer << " " << base_name << name << '="' << v.to_s << '"'
135
+ when Hash
136
+ generate_nested_attributes(v, "#{base_name}#{name}-", buffer)
137
+ when Array
138
+ if (value = generate_nested_tokens(v))
139
+ buffer << " " << base_name << name << '="' << value << '"'
140
+ end
141
+ when Set
142
+ if (value = generate_nested_tokens(v.to_a))
143
+ buffer << " " << base_name << name << '="' << value << '"'
144
+ end
145
+ when Phlex::SGML::SafeObject
146
+ buffer << " " << base_name << name << '="' << v.to_s.gsub('"', "&quot;") << '"'
147
+ else
148
+ raise Phlex::ArgumentError.new("Invalid attribute value #{v.inspect}.")
149
+ end
150
+
151
+ if root_key
152
+ base_name = original_base_name
153
+ end
154
+
155
+ buffer
156
+ end
157
+ end
158
+
159
+ def generate_nested_tokens(tokens, sep = " ", gsub_from = nil, gsub_to = "")
160
+ buffer = +""
161
+
162
+ i, length = 0, tokens.length
163
+
164
+ while i < length
165
+ token = tokens[i]
166
+
167
+ case token
168
+ when String
169
+ token = token.gsub(gsub_from, gsub_to) if gsub_from
170
+ if i > 0
171
+ buffer << sep << token
172
+ else
173
+ buffer << token
174
+ end
175
+ when Symbol
176
+ if i > 0
177
+ buffer << sep << token.name.tr("_", "-")
178
+ else
179
+ buffer << token.name.tr("_", "-")
180
+ end
181
+ when Integer, Float, Phlex::SGML::SafeObject
182
+ if i > 0
183
+ buffer << sep << token.to_s
184
+ else
185
+ buffer << token.to_s
186
+ end
187
+ when Array
188
+ if token.length > 0 && (value = generate_nested_tokens(token, sep, gsub_from, gsub_to))
189
+ if i > 0
190
+ buffer << sep << value
191
+ else
192
+ buffer << value
193
+ end
194
+ end
195
+ when Set
196
+ if token.length > 0 && (value = generate_nested_tokens(token.to_a, sep, gsub_from, gsub_to))
197
+ if i > 0
198
+ buffer << sep << value
199
+ else
200
+ buffer << value
201
+ end
202
+ end
203
+ when nil
204
+ # Do nothing
205
+ else
206
+ raise Phlex::ArgumentError.new("Invalid token type: #{token.class}.")
207
+ end
208
+
209
+ i += 1
210
+ end
211
+
212
+ return if buffer.empty?
213
+
214
+ buffer.gsub('"', "&quot;")
215
+ end
216
+
217
+ # The result is unsafe so should be escaped.
218
+ def generate_styles(styles)
219
+ case styles
220
+ when Array, Set
221
+ styles.filter_map do |s|
222
+ case s
223
+ when String
224
+ if s == "" || s.end_with?(";")
225
+ s
226
+ else
227
+ "#{s};"
228
+ end
229
+ when Phlex::SGML::SafeObject
230
+ value = s.to_s
231
+ value.end_with?(";") ? value : "#{value};"
232
+ when Hash
233
+ next generate_styles(s)
234
+ when nil
235
+ next nil
236
+ else
237
+ raise Phlex::ArgumentError.new("Invalid style: #{s.inspect}.")
238
+ end
239
+ end.join(" ")
240
+ when Hash
241
+ buffer = +""
242
+ i = 0
243
+ styles.each do |k, v|
244
+ prop = case k
245
+ when String
246
+ k
247
+ when Symbol
248
+ k.name.tr("_", "-")
249
+ else
250
+ raise Phlex::ArgumentError.new("Style keys should be Strings or Symbols.")
251
+ end
252
+
253
+ value = case v
254
+ when String
255
+ v
256
+ when Symbol
257
+ v.name.tr("_", "-")
258
+ when Integer, Float, Phlex::SGML::SafeObject
259
+ v.to_s
260
+ when nil
261
+ nil
262
+ else
263
+ raise Phlex::ArgumentError.new("Invalid style value: #{v.inspect}")
264
+ end
265
+
266
+ if value
267
+ if i == 0
268
+ buffer << prop << ": " << value << ";"
269
+ else
270
+ buffer << " " << prop << ": " << value << ";"
271
+ end
272
+ end
273
+
274
+ i += 1
275
+ end
276
+
277
+ buffer
278
+ end
279
+ end
280
+ end
@@ -4,16 +4,16 @@ module Phlex::SGML::Elements
4
4
  COMMA_SEPARATED_TOKENS = {
5
5
  img: <<~RUBY,
6
6
  if Array === (srcset_attribute = attributes[:srcset])
7
- attributes[:srcset] = __nested_tokens__(srcset_attribute, ", ")
7
+ attributes[:srcset] = Phlex::SGML::Attributes.generate_nested_tokens(srcset_attribute, ", ", ",", "%2C")
8
8
  end
9
9
  RUBY
10
10
  link: <<~RUBY,
11
11
  if Array === (media_attribute = attributes[:media])
12
- attributes[:media] = __nested_tokens__(media_attribute, ", ")
12
+ attributes[:media] = Phlex::SGML::Attributes.generate_nested_tokens(media_attribute, ", ", ",", "%2C")
13
13
  end
14
14
 
15
15
  if Array === (sizes_attribute = attributes[:sizes])
16
- attributes[:sizes] = __nested_tokens__(sizes_attribute, ", ")
16
+ attributes[:sizes] = Phlex::SGML::Attributes.generate_nested_tokens(sizes_attribute, ", ", ",", "%2C")
17
17
  end
18
18
 
19
19
  if Array === (imagesrcset_attribute = attributes[:imagesrcset])
@@ -21,7 +21,7 @@ module Phlex::SGML::Elements
21
21
  as_attribute = attributes[:as] || attributes["as"]
22
22
 
23
23
  if ("preload" == rel_attribute || :preload == rel_attribute) && ("image" == as_attribute || :image == as_attribute)
24
- attributes[:imagesrcset] = __nested_tokens__(imagesrcset_attribute, ", ")
24
+ attributes[:imagesrcset] = Phlex::SGML::Attributes.generate_nested_tokens(imagesrcset_attribute, ", ", ",", "%2C")
25
25
  end
26
26
  end
27
27
  RUBY
@@ -30,7 +30,7 @@ module Phlex::SGML::Elements
30
30
  type_attribute = attributes[:type] || attributes["type"]
31
31
 
32
32
  if "file" == type_attribute || :file == type_attribute
33
- attributes[:accept] = __nested_tokens__(accept_attribute, ", ")
33
+ attributes[:accept] = Phlex::SGML::Attributes.generate_nested_tokens(accept_attribute, ", ", ",", "%2C")
34
34
  end
35
35
  end
36
36
  RUBY
@@ -59,7 +59,7 @@ module Phlex::SGML::Elements
59
59
  buffer << "<#{tag}"
60
60
  begin
61
61
  #{COMMA_SEPARATED_TOKENS[method_name]}
62
- buffer << (Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes))
62
+ buffer << (Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes))
63
63
  ensure
64
64
  buffer << ">"
65
65
  end
@@ -90,7 +90,7 @@ module Phlex::SGML::Elements
90
90
  buffer << "<#{tag}"
91
91
  begin
92
92
  #{COMMA_SEPARATED_TOKENS[method_name]}
93
- buffer << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes))
93
+ buffer << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes))
94
94
  ensure
95
95
  buffer << "></#{tag}>"
96
96
  end
@@ -152,7 +152,7 @@ module Phlex::SGML::Elements
152
152
  buffer << "<#{tag}"
153
153
  begin
154
154
  #{COMMA_SEPARATED_TOKENS[method_name]}
155
- buffer << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes))
155
+ buffer << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes))
156
156
  ensure
157
157
  buffer << ">"
158
158
  end