phlex 2.0.3 → 2.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.
- checksums.yaml +4 -4
- data/lib/phlex/csv.rb +185 -54
- data/lib/phlex/html.rb +1 -1
- data/lib/phlex/kit.rb +2 -10
- data/lib/phlex/sgml/elements.rb +60 -39
- data/lib/phlex/sgml/state.rb +10 -1
- data/lib/phlex/sgml.rb +8 -33
- data/lib/phlex/svg.rb +15 -1
- data/lib/phlex/version.rb +1 -1
- data/lib/phlex.rb +2 -0
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8db189359fc8003771075a598fbeed0c2205ca52604fd829d77afe3c0b3a467
|
|
4
|
+
data.tar.gz: e5446344e9a83c982fdfa94d4d31abd4b7511858704cc2e9f9d285ad9740b9c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db1c7e4b591440cea077fb99285552f443301564b293f2a0cf0098750d514af3ac48684d820398ac588570ba71f6f1c1284b72c3b32d6f54f48d20c457a476eb
|
|
7
|
+
data.tar.gz: fb9ff7728339e8edbcae2b9031c41f77ae2d28e68963b8c27368575d44a20935ff6393730d180579bb29d9a3786e43c1c6388223d0115b17c6d35b31245b64ff
|
data/lib/phlex/csv.rb
CHANGED
|
@@ -1,63 +1,122 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Phlex::CSV
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
FORMULA_PREFIXES_MAP = Array.new(128).tap do |map|
|
|
5
|
+
"=+-@\t\r".each_byte do |byte|
|
|
6
|
+
map[byte] = true
|
|
7
|
+
end
|
|
8
|
+
end.freeze
|
|
9
|
+
|
|
10
|
+
UNDEFINED = Object.new
|
|
6
11
|
|
|
7
12
|
def initialize(collection)
|
|
8
13
|
@collection = collection
|
|
14
|
+
@_row_buffer = []
|
|
9
15
|
@_headers = []
|
|
10
|
-
@_current_row = []
|
|
11
|
-
@_current_column_index = 0
|
|
12
|
-
@_first = true
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
attr_reader :collection
|
|
16
19
|
|
|
17
|
-
def call(buffer = +"", context: nil)
|
|
18
|
-
|
|
19
|
-
raise <<~MESSAGE
|
|
20
|
-
You need to define escape_csv_injection? in #{self.class.name}, returning either `true` or `false`.
|
|
20
|
+
def call(buffer = +"", context: nil, delimiter: self.delimiter)
|
|
21
|
+
ensure_escape_csv_injection_configured!
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
strip_whitespace = trim_whitespace?
|
|
24
|
+
escape_csv_injection = escape_csv_injection?
|
|
25
|
+
row_buffer = @_row_buffer
|
|
26
|
+
headers = @_headers
|
|
27
|
+
has_yielder = respond_to?(:yielder, true)
|
|
28
|
+
first_row = true
|
|
29
|
+
render_headers = render_headers?
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
This is useful when using CSVs for byte-for-byte data exchange between secure systems.
|
|
31
|
+
if delimiter.length != 1
|
|
32
|
+
raise Phlex::ArgumentError.new("Delimiter must be a single character")
|
|
33
|
+
end
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
if strip_whitespace
|
|
36
|
+
escape_regex = /[\n"#{delimiter}]/
|
|
37
|
+
else
|
|
38
|
+
escape_regex = /^\s|\s$|[\n"#{delimiter}]/
|
|
39
|
+
end
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
if has_yielder
|
|
42
|
+
warn <<~MESSAGE
|
|
43
|
+
Custom yielders are deprecated in Phlex::CSV.
|
|
35
44
|
|
|
36
|
-
|
|
45
|
+
Please replace your yielder with an `around_row` method.
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
You should be able to just rename your yielder method
|
|
48
|
+
and change `yield` to `super`.
|
|
39
49
|
MESSAGE
|
|
40
50
|
end
|
|
41
51
|
|
|
42
52
|
each_item do |record|
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
if has_yielder
|
|
54
|
+
yielder(record) { |*a, **k| row = row_template(*a, **k) }
|
|
55
|
+
else
|
|
56
|
+
around_row(record)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
row = row_buffer
|
|
60
|
+
|
|
61
|
+
if first_row
|
|
62
|
+
first_row = false
|
|
63
|
+
|
|
64
|
+
i = 0
|
|
65
|
+
number_of_columns = row.length
|
|
66
|
+
first_col = true
|
|
67
|
+
|
|
68
|
+
while i < number_of_columns
|
|
69
|
+
header, = row[i]
|
|
70
|
+
headers[i] = header
|
|
45
71
|
|
|
46
|
-
|
|
47
|
-
|
|
72
|
+
if render_headers
|
|
73
|
+
if first_col
|
|
74
|
+
first_col = false
|
|
75
|
+
else
|
|
76
|
+
buffer << delimiter
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
__escape__(buffer, header, escape_csv_injection:, strip_whitespace:, escape_regex:)
|
|
80
|
+
end
|
|
81
|
+
i += 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
buffer << "\n" if render_headers
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
i = 0
|
|
88
|
+
number_of_columns = row.length
|
|
89
|
+
first_col = true
|
|
90
|
+
|
|
91
|
+
while i < number_of_columns
|
|
92
|
+
header, value = row[i]
|
|
93
|
+
|
|
94
|
+
unless headers[i] == header
|
|
95
|
+
raise Phlex::RuntimeError.new("Header mismatch at index #{i}: expected #{headers[i]}, got #{header}.")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if first_col
|
|
99
|
+
first_col = false
|
|
100
|
+
else
|
|
101
|
+
buffer << delimiter
|
|
48
102
|
end
|
|
49
103
|
|
|
50
|
-
buffer
|
|
51
|
-
|
|
52
|
-
@_current_row.clear
|
|
104
|
+
__escape__(buffer, value, escape_csv_injection:, strip_whitespace:, escape_regex:)
|
|
105
|
+
i += 1
|
|
53
106
|
end
|
|
54
107
|
|
|
55
|
-
|
|
108
|
+
buffer << "\n"
|
|
109
|
+
|
|
110
|
+
row_buffer.clear
|
|
56
111
|
end
|
|
57
112
|
|
|
58
113
|
buffer
|
|
59
114
|
end
|
|
60
115
|
|
|
116
|
+
def around_row(item)
|
|
117
|
+
row_template(item)
|
|
118
|
+
end
|
|
119
|
+
|
|
61
120
|
def filename
|
|
62
121
|
nil
|
|
63
122
|
end
|
|
@@ -66,27 +125,20 @@ class Phlex::CSV
|
|
|
66
125
|
"text/csv"
|
|
67
126
|
end
|
|
68
127
|
|
|
128
|
+
def delimiter
|
|
129
|
+
","
|
|
130
|
+
end
|
|
131
|
+
|
|
69
132
|
private
|
|
70
133
|
|
|
71
134
|
def column(header = nil, value)
|
|
72
|
-
|
|
73
|
-
@_headers << __escape__(header)
|
|
74
|
-
elsif header != @_headers[@_current_column_index]
|
|
75
|
-
raise "Inconsistent header."
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
@_current_row << __escape__(value)
|
|
79
|
-
@_current_column_index += 1
|
|
135
|
+
@_row_buffer << [header, value]
|
|
80
136
|
end
|
|
81
137
|
|
|
82
138
|
def each_item(&)
|
|
83
139
|
collection.each(&)
|
|
84
140
|
end
|
|
85
141
|
|
|
86
|
-
def yielder(record)
|
|
87
|
-
yield(record)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
142
|
# Override and set to `false` to disable rendering headers.
|
|
91
143
|
def render_headers?
|
|
92
144
|
true
|
|
@@ -99,22 +151,101 @@ class Phlex::CSV
|
|
|
99
151
|
|
|
100
152
|
# Override and set to `false` to disable CSV injection escapes or `true` to enable.
|
|
101
153
|
def escape_csv_injection?
|
|
102
|
-
|
|
154
|
+
UNDEFINED
|
|
103
155
|
end
|
|
104
156
|
|
|
105
|
-
def __escape__(value)
|
|
106
|
-
value =
|
|
107
|
-
|
|
108
|
-
last_char = value[-1]
|
|
109
|
-
|
|
110
|
-
if escape_csv_injection? && FORMULA_PREFIXES.include?(first_char)
|
|
111
|
-
# Prefix a single quote to prevent Excel, Google Docs, etc. from interpreting the value as a formula.
|
|
112
|
-
# See https://owasp.org/www-community/attacks/CSV_Injection
|
|
113
|
-
%("'#{value.gsub('"', '""')}")
|
|
114
|
-
elsif (!trim_whitespace? && (SPACE_CHARACTERS.include?(first_char) || SPACE_CHARACTERS.include?(last_char))) || value.include?('"') || value.include?(",") || value.include?("\n")
|
|
115
|
-
%("#{value.gsub('"', '""')}")
|
|
116
|
-
else
|
|
157
|
+
def __escape__(buffer, value, escape_csv_injection:, strip_whitespace:, escape_regex:)
|
|
158
|
+
value = case value
|
|
159
|
+
when String
|
|
117
160
|
value
|
|
161
|
+
when Symbol
|
|
162
|
+
value.name
|
|
163
|
+
else
|
|
164
|
+
value.to_s
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
if strip_whitespace
|
|
168
|
+
value = value.strip
|
|
169
|
+
|
|
170
|
+
if escape_csv_injection
|
|
171
|
+
if FORMULA_PREFIXES_MAP[value.getbyte(0)]
|
|
172
|
+
value.gsub!('"', '""')
|
|
173
|
+
buffer << '"\'' << value << '"'
|
|
174
|
+
elsif value.match?(escape_regex)
|
|
175
|
+
value.gsub!('"', '""')
|
|
176
|
+
buffer << '"' << value << '"'
|
|
177
|
+
else
|
|
178
|
+
buffer << value
|
|
179
|
+
end
|
|
180
|
+
else # not escaping CSV injection
|
|
181
|
+
buffer << value
|
|
182
|
+
end
|
|
183
|
+
else # not stripping whitespace
|
|
184
|
+
if escape_csv_injection
|
|
185
|
+
first_byte = value.getbyte(0)
|
|
186
|
+
|
|
187
|
+
if FORMULA_PREFIXES_MAP[first_byte]
|
|
188
|
+
buffer << '"\'' << value.gsub('"', '""') << '"'
|
|
189
|
+
elsif value.match?(escape_regex)
|
|
190
|
+
buffer << '"' << value.gsub('"', '""') << '"'
|
|
191
|
+
else
|
|
192
|
+
buffer << value
|
|
193
|
+
end
|
|
194
|
+
else # not escaping CSV injection
|
|
195
|
+
if value.match?(escape_regex)
|
|
196
|
+
buffer << '"' << value.gsub('"', '""') << '"'
|
|
197
|
+
else
|
|
198
|
+
buffer << value
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Handle legacy `view_template` method
|
|
205
|
+
def respond_to_missing?(method_name, include_private)
|
|
206
|
+
(method_name == :row_template && respond_to?(:view_template)) || super
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Handle legacy `view_template` method
|
|
210
|
+
def method_missing(method_name, ...)
|
|
211
|
+
if method_name == :row_template && respond_to?(:view_template)
|
|
212
|
+
warn "Deprecated: Use `row_template` instead of `view_template` in Phlex CSVs."
|
|
213
|
+
self.class.alias_method :row_template, :view_template
|
|
214
|
+
view_template(...)
|
|
215
|
+
else
|
|
216
|
+
super
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def ensure_escape_csv_injection_configured!
|
|
221
|
+
if escape_csv_injection? == UNDEFINED
|
|
222
|
+
raise <<~MESSAGE
|
|
223
|
+
You need to define `escape_csv_injection?` in #{self.class.name}.
|
|
224
|
+
|
|
225
|
+
CSV injection is a security vulnerability where malicious spreadsheet
|
|
226
|
+
formulae are used to execute code or exfiltrate data when a CSV is opened
|
|
227
|
+
in a spreadsheet program such as Microsoft Excel or Google Sheets.
|
|
228
|
+
|
|
229
|
+
For more information, see https://owasp.org/www-community/attacks/CSV_Injection
|
|
230
|
+
|
|
231
|
+
If you’re sure this CSV will never be opened in a spreadsheet program,
|
|
232
|
+
you can *disable* CSV injection escapes:
|
|
233
|
+
|
|
234
|
+
def escape_csv_injection? = false
|
|
235
|
+
|
|
236
|
+
This is useful when using CSVs for byte-for-byte data exchange between secure systems.
|
|
237
|
+
|
|
238
|
+
Alternatively, you can *enable* CSV injection escapes at the cost of data integrity:
|
|
239
|
+
|
|
240
|
+
def escape_csv_injection? = true
|
|
241
|
+
|
|
242
|
+
Enabling the CSV injection escapes will prefix with a single quote `'` any
|
|
243
|
+
values that start with: `=`, `+`, `-`, `@`, `\\t`, `\\r`
|
|
244
|
+
|
|
245
|
+
Unfortunately, there is no one-size-fits-all solution to CSV injection.
|
|
246
|
+
|
|
247
|
+
You need to decide based on your specific use case.
|
|
248
|
+
MESSAGE
|
|
118
249
|
end
|
|
119
250
|
end
|
|
120
251
|
end
|
data/lib/phlex/html.rb
CHANGED
|
@@ -58,7 +58,7 @@ class Phlex::HTML < Phlex::SGML
|
|
|
58
58
|
raise Phlex::ArgumentError.new("Expected the tag name to be a Symbol.")
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
if (tag = StandardElements.__registered_elements__[name]) || (
|
|
61
|
+
if (tag = StandardElements.__registered_elements__[name]) || (tag = name.name.tr("_", "-")).include?("-")
|
|
62
62
|
if attributes.length > 0 # with attributes
|
|
63
63
|
if block_given # with content block
|
|
64
64
|
buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"
|
data/lib/phlex/kit.rb
CHANGED
|
@@ -15,11 +15,7 @@ module Phlex::Kit
|
|
|
15
15
|
def respond_to_missing?(name, include_private = false)
|
|
16
16
|
mod = self.class
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
true
|
|
20
|
-
else
|
|
21
|
-
super
|
|
22
|
-
end
|
|
18
|
+
(name[0] == name[0].upcase && mod.constants.include?(name) && mod.const_get(name) && methods.include?(name)) || super
|
|
23
19
|
end
|
|
24
20
|
end
|
|
25
21
|
|
|
@@ -45,11 +41,7 @@ module Phlex::Kit
|
|
|
45
41
|
end
|
|
46
42
|
|
|
47
43
|
def respond_to_missing?(name, include_private = false)
|
|
48
|
-
|
|
49
|
-
true
|
|
50
|
-
else
|
|
51
|
-
super
|
|
52
|
-
end
|
|
44
|
+
(name[0] == name[0].upcase && constants.include?(name) && const_get(name) && methods.include?(name)) || super
|
|
53
45
|
end
|
|
54
46
|
|
|
55
47
|
def const_added(name)
|
data/lib/phlex/sgml/elements.rb
CHANGED
|
@@ -21,55 +21,69 @@ module Phlex::SGML::Elements
|
|
|
21
21
|
|
|
22
22
|
if attributes.length > 0 # with attributes
|
|
23
23
|
if block_given # with content block
|
|
24
|
-
buffer << "<#{tag}"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
24
|
+
buffer << "<#{tag}"
|
|
25
|
+
begin
|
|
26
|
+
buffer << (Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes))
|
|
27
|
+
ensure
|
|
28
|
+
buffer << ">"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
original_length = buffer.bytesize
|
|
33
|
+
content = yield(self)
|
|
34
|
+
if original_length == buffer.bytesize
|
|
35
|
+
case content
|
|
36
|
+
when ::Phlex::SGML::SafeObject
|
|
37
|
+
buffer << content.to_s
|
|
38
|
+
when String
|
|
39
|
+
buffer << ::Phlex::Escape.html_escape(content)
|
|
40
|
+
when Symbol
|
|
41
|
+
buffer << ::Phlex::Escape.html_escape(content.name)
|
|
42
|
+
when nil
|
|
43
|
+
nil
|
|
44
|
+
else
|
|
45
|
+
if (formatted_object = format_object(content))
|
|
46
|
+
buffer << ::Phlex::Escape.html_escape(formatted_object)
|
|
47
|
+
end
|
|
41
48
|
end
|
|
42
49
|
end
|
|
50
|
+
ensure
|
|
51
|
+
buffer << "</#{tag}>"
|
|
43
52
|
end
|
|
44
|
-
|
|
45
|
-
buffer << "</#{tag}>"
|
|
46
53
|
else # without content
|
|
47
|
-
buffer << "<#{tag}"
|
|
54
|
+
buffer << "<#{tag}"
|
|
55
|
+
begin
|
|
56
|
+
buffer << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes))
|
|
57
|
+
ensure
|
|
58
|
+
buffer << "></#{tag}>"
|
|
59
|
+
end
|
|
48
60
|
end
|
|
49
61
|
else # without attributes
|
|
50
62
|
if block_given # with content block
|
|
51
63
|
buffer << "<#{tag}>"
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
nil
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
begin
|
|
66
|
+
original_length = buffer.bytesize
|
|
67
|
+
content = yield(self)
|
|
68
|
+
if original_length == buffer.bytesize
|
|
69
|
+
case content
|
|
70
|
+
when ::Phlex::SGML::SafeObject
|
|
71
|
+
buffer << content.to_s
|
|
72
|
+
when String
|
|
73
|
+
buffer << ::Phlex::Escape.html_escape(content)
|
|
74
|
+
when Symbol
|
|
75
|
+
buffer << ::Phlex::Escape.html_escape(content.name)
|
|
76
|
+
when nil
|
|
77
|
+
nil
|
|
78
|
+
else
|
|
79
|
+
if (formatted_object = format_object(content))
|
|
80
|
+
buffer << ::Phlex::Escape.html_escape(formatted_object)
|
|
81
|
+
end
|
|
68
82
|
end
|
|
69
83
|
end
|
|
84
|
+
ensure
|
|
85
|
+
buffer << "</#{tag}>"
|
|
70
86
|
end
|
|
71
|
-
|
|
72
|
-
buffer << "</#{tag}>"
|
|
73
87
|
else # without content
|
|
74
88
|
buffer << "<#{tag}></#{tag}>"
|
|
75
89
|
end
|
|
@@ -95,10 +109,17 @@ module Phlex::SGML::Elements
|
|
|
95
109
|
|
|
96
110
|
return unless state.should_render?
|
|
97
111
|
|
|
112
|
+
buffer = state.buffer
|
|
113
|
+
|
|
98
114
|
if attributes.length > 0 # with attributes
|
|
99
|
-
|
|
115
|
+
buffer << "<#{tag}"
|
|
116
|
+
begin
|
|
117
|
+
buffer << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes))
|
|
118
|
+
ensure
|
|
119
|
+
buffer << ">"
|
|
120
|
+
end
|
|
100
121
|
else # without attributes
|
|
101
|
-
|
|
122
|
+
buffer << "<#{tag}>"
|
|
102
123
|
end
|
|
103
124
|
|
|
104
125
|
nil
|
data/lib/phlex/sgml/state.rb
CHANGED
|
@@ -12,10 +12,19 @@ class Phlex::SGML::State
|
|
|
12
12
|
@output_buffer = output_buffer
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
attr_accessor :
|
|
15
|
+
attr_accessor :capturing, :user_context
|
|
16
16
|
|
|
17
17
|
attr_reader :fragments, :fragment_depth, :output_buffer
|
|
18
18
|
|
|
19
|
+
def buffer
|
|
20
|
+
case @buffer
|
|
21
|
+
when Proc
|
|
22
|
+
@buffer.call
|
|
23
|
+
else
|
|
24
|
+
@buffer
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
19
28
|
def around_render(component)
|
|
20
29
|
stack = @stack
|
|
21
30
|
|
data/lib/phlex/sgml.rb
CHANGED
|
@@ -3,13 +3,7 @@
|
|
|
3
3
|
# **Standard Generalized Markup Language** for behaviour common to {HTML} and {SVG}.
|
|
4
4
|
class Phlex::SGML
|
|
5
5
|
UNSAFE_ATTRIBUTES = Set.new(%w[srcdoc sandbox http-equiv]).freeze
|
|
6
|
-
REF_ATTRIBUTES = Set.new(%w[href src action formaction lowsrc dynsrc background ping
|
|
7
|
-
NAMED_CHARACTER_REFERENCES = {
|
|
8
|
-
"colon" => ":",
|
|
9
|
-
"tab" => "\t",
|
|
10
|
-
"newline" => "\n",
|
|
11
|
-
}.freeze
|
|
12
|
-
UNSAFE_ATTRIBUTE_NAME_CHARS = %r([<>&"'/=\s\x00])
|
|
6
|
+
REF_ATTRIBUTES = Set.new(%w[href src action formaction lowsrc dynsrc background ping]).freeze
|
|
13
7
|
|
|
14
8
|
autoload :Elements, "phlex/sgml/elements"
|
|
15
9
|
autoload :SafeObject, "phlex/sgml/safe_object"
|
|
@@ -294,6 +288,10 @@ class Phlex::SGML
|
|
|
294
288
|
end
|
|
295
289
|
end
|
|
296
290
|
|
|
291
|
+
def json_escape(string)
|
|
292
|
+
ERB::Util.json_escape(string)
|
|
293
|
+
end
|
|
294
|
+
|
|
297
295
|
private
|
|
298
296
|
|
|
299
297
|
# Override this method to use a different deployment key.
|
|
@@ -483,9 +481,7 @@ class Phlex::SGML
|
|
|
483
481
|
if value != true && REF_ATTRIBUTES.include?(normalized_name)
|
|
484
482
|
case value
|
|
485
483
|
when String
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if decoded_value.downcase.delete("^a-z:").start_with?("javascript:")
|
|
484
|
+
if value.downcase.delete("^a-z:").start_with?("javascript:")
|
|
489
485
|
# We just ignore these because they were likely not specified by the developer.
|
|
490
486
|
next
|
|
491
487
|
end
|
|
@@ -503,7 +499,7 @@ class Phlex::SGML
|
|
|
503
499
|
end
|
|
504
500
|
end
|
|
505
501
|
|
|
506
|
-
if name.match?(
|
|
502
|
+
if name.match?(/[<>&"']/)
|
|
507
503
|
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
|
508
504
|
end
|
|
509
505
|
|
|
@@ -534,7 +530,7 @@ class Phlex::SGML
|
|
|
534
530
|
else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols")
|
|
535
531
|
end
|
|
536
532
|
|
|
537
|
-
if name.match?(
|
|
533
|
+
if name.match?(/[<>&"']/)
|
|
538
534
|
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
|
539
535
|
end
|
|
540
536
|
|
|
@@ -610,27 +606,6 @@ class Phlex::SGML
|
|
|
610
606
|
buffer.gsub('"', """)
|
|
611
607
|
end
|
|
612
608
|
|
|
613
|
-
private def decode_html_character_references(value)
|
|
614
|
-
value
|
|
615
|
-
.gsub(/&#x([0-9a-f]+);?/i) {
|
|
616
|
-
begin
|
|
617
|
-
[$1.to_i(16)].pack("U*")
|
|
618
|
-
rescue
|
|
619
|
-
""
|
|
620
|
-
end
|
|
621
|
-
}
|
|
622
|
-
.gsub(/&#(\d+);?/) {
|
|
623
|
-
begin
|
|
624
|
-
[$1.to_i].pack("U*")
|
|
625
|
-
rescue
|
|
626
|
-
""
|
|
627
|
-
end
|
|
628
|
-
}
|
|
629
|
-
.gsub(/&([a-z][a-z0-9]+);?/i) {
|
|
630
|
-
NAMED_CHARACTER_REFERENCES[$1.downcase] || ""
|
|
631
|
-
}
|
|
632
|
-
end
|
|
633
|
-
|
|
634
609
|
# Result is **unsafe**, so it should be escaped!
|
|
635
610
|
def __styles__(styles)
|
|
636
611
|
case styles
|
data/lib/phlex/svg.rb
CHANGED
|
@@ -15,6 +15,20 @@ class Phlex::SVG < Phlex::SGML
|
|
|
15
15
|
nil
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def cdata(content = nil, &block)
|
|
19
|
+
state = @_state
|
|
20
|
+
return unless state.should_render?
|
|
21
|
+
|
|
22
|
+
if !block && String === content
|
|
23
|
+
state.buffer << "<![CDATA[" << content.gsub("]]>", "]]>]]<![CDATA[") << "]]>"
|
|
24
|
+
elsif block && nil == content
|
|
25
|
+
state.buffer << "<![CDATA[" << capture(&block).gsub("]]>", "]]>]]<![CDATA[") << "]]>"
|
|
26
|
+
else
|
|
27
|
+
|
|
28
|
+
raise Phlex::ArgumentError.new("Expected a String or block.")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
18
32
|
def tag(name, **attributes, &)
|
|
19
33
|
state = @_state
|
|
20
34
|
block_given = block_given?
|
|
@@ -29,7 +43,7 @@ class Phlex::SVG < Phlex::SGML
|
|
|
29
43
|
raise Phlex::ArgumentError.new("Expected the tag name to be a Symbol.")
|
|
30
44
|
end
|
|
31
45
|
|
|
32
|
-
if (tag = StandardElements.__registered_elements__[name]) || (
|
|
46
|
+
if (tag = StandardElements.__registered_elements__[name]) || (tag = name.name.tr("_", "-")).include?("-")
|
|
33
47
|
if attributes.length > 0 # with attributes
|
|
34
48
|
if block_given # with content block
|
|
35
49
|
buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"
|
data/lib/phlex/version.rb
CHANGED
data/lib/phlex.rb
CHANGED
metadata
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phlex
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joel Drapper
|
|
8
8
|
- Will Cosgrove
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2025-03-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description: Build HTML
|
|
13
|
+
description: Build HTML, SVG and CSV views with Ruby classes.
|
|
14
14
|
email:
|
|
15
15
|
- joel@drapper.me
|
|
16
16
|
executables: []
|
|
@@ -65,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
65
65
|
- !ruby/object:Gem::Version
|
|
66
66
|
version: '0'
|
|
67
67
|
requirements: []
|
|
68
|
-
rubygems_version: 3.6.
|
|
68
|
+
rubygems_version: 3.6.2
|
|
69
69
|
specification_version: 4
|
|
70
70
|
summary: Object-oriented views in Ruby.
|
|
71
71
|
test_files: []
|