code-ruby 3.0.11 → 3.0.13
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/Gemfile.lock +1 -1
- data/VERSION +1 -1
- data/lib/code/format.rb +99 -7
- data/lib/code/object/string.rb +13 -0
- data/spec/code/format_spec.rb +67 -2
- data/spec/code/object/string_spec.rb +8 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6baa3c7c3ef6b602529e2af4a36200029291ed89c115f1c1f3d750a8e815cc03
|
|
4
|
+
data.tar.gz: aa9c876fb0aff915f895ec3d209d25ac06270e3809eadba8f5e71b37b9cb29db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 444adc5d9b9a5223b82a921a888558a2f23dc40344a12b383137581b910ba4e33ea13873a2e5ce6698dc130ae77775cf9aa5b3e0e23f70947418301831df96b9
|
|
7
|
+
data.tar.gz: bcf7ecebc28788a2cf7464e3a7b67f32b2bfb86ec3d6365ca58674914d852181d27f3a051b596665807acd67291c2026d227b98d33603260e3122417e04d6a76
|
data/Gemfile.lock
CHANGED
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.0.
|
|
1
|
+
3.0.13
|
data/lib/code/format.rb
CHANGED
|
@@ -57,13 +57,20 @@ class Code
|
|
|
57
57
|
"begin\n#{body}\n#{INDENT * indent}end"
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
def format_group(group, indent:)
|
|
61
|
+
formatted = format_code_inline(group, indent: indent)
|
|
62
|
+
return "(#{formatted})" unless formatted.include?("\n")
|
|
63
|
+
|
|
64
|
+
"(\n#{indent_lines(normalize_group_indentation(formatted, indent: indent), indent + 1)}\n#{INDENT * indent})"
|
|
65
|
+
end
|
|
66
|
+
|
|
60
67
|
def format_statement(statement, indent:)
|
|
61
68
|
if statement.is_a?(Hash) && statement.key?(:nothing)
|
|
62
69
|
statement[:nothing].presence || "nothing"
|
|
63
70
|
elsif statement.is_a?(Hash) && statement.key?(:boolean)
|
|
64
71
|
statement[:boolean]
|
|
65
72
|
elsif statement.is_a?(Hash) && statement.key?(:group)
|
|
66
|
-
|
|
73
|
+
format_group(statement[:group], indent: indent)
|
|
67
74
|
elsif statement.is_a?(Hash) && statement.key?(:call)
|
|
68
75
|
format_call(statement[:call], indent: indent)
|
|
69
76
|
elsif statement.is_a?(Hash) && statement.key?(:number)
|
|
@@ -427,11 +434,11 @@ class Code
|
|
|
427
434
|
|
|
428
435
|
Array(operation[:others]).each do |other|
|
|
429
436
|
right = format_nested_statement(other[:statement], indent: indent)
|
|
430
|
-
operator = other[:operator]
|
|
437
|
+
operator = format_operator(other[:operator])
|
|
431
438
|
|
|
432
439
|
expression =
|
|
433
440
|
if compact_operator?(operator)
|
|
434
|
-
"#{expression}#{operator}#{right}"
|
|
441
|
+
"#{format_compact_receiver(expression, indent: indent)}#{operator}#{right}"
|
|
435
442
|
else
|
|
436
443
|
candidate = "#{expression} #{operator} #{right}"
|
|
437
444
|
if right.include?("\n")
|
|
@@ -449,8 +456,10 @@ class Code
|
|
|
449
456
|
right_lines =
|
|
450
457
|
if right.include?("\n")
|
|
451
458
|
right.lines(chomp: true).map(&:lstrip)
|
|
452
|
-
|
|
459
|
+
elsif %w[and or].include?(operator)
|
|
453
460
|
right.split(" #{operator} ")
|
|
461
|
+
else
|
|
462
|
+
[right]
|
|
454
463
|
end
|
|
455
464
|
continuation_lines =
|
|
456
465
|
right_lines.map do |line|
|
|
@@ -469,6 +478,34 @@ class Code
|
|
|
469
478
|
expression
|
|
470
479
|
end
|
|
471
480
|
|
|
481
|
+
def format_compact_receiver(expression, indent:)
|
|
482
|
+
return expression unless compact_receiver_needs_parentheses?(expression)
|
|
483
|
+
|
|
484
|
+
"(\n#{indent_lines(expression, indent + 1)}\n#{INDENT * indent})"
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def compact_receiver_needs_parentheses?(expression)
|
|
488
|
+
return false unless expression.include?("\n")
|
|
489
|
+
return false if expression.lstrip.start_with?("(")
|
|
490
|
+
|
|
491
|
+
continuation_lines =
|
|
492
|
+
expression
|
|
493
|
+
.lines(chomp: true)[1..]
|
|
494
|
+
.to_a
|
|
495
|
+
.reject { |line| line.strip.empty? || line.strip.match?(/\A[\)\]\}]+\z/) }
|
|
496
|
+
|
|
497
|
+
return false if continuation_lines.empty?
|
|
498
|
+
|
|
499
|
+
base_indent =
|
|
500
|
+
continuation_lines.map { |line| line[/\A */].to_s.length }.min
|
|
501
|
+
|
|
502
|
+
continuation_lines.any? do |line|
|
|
503
|
+
next false unless line[/\A */].to_s.length == base_indent
|
|
504
|
+
|
|
505
|
+
line.lstrip.match?(/\A(\+|-|\*|\/|%|<<|>>|\||\^|&|and\b|or\b)/)
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
472
509
|
def extract_string_concatenation_parts(statement)
|
|
473
510
|
return nil unless statement.is_a?(Hash)
|
|
474
511
|
|
|
@@ -495,9 +532,15 @@ class Code
|
|
|
495
532
|
end
|
|
496
533
|
|
|
497
534
|
def format_right_operation(operation, indent:)
|
|
498
|
-
operator = operation[:operator]
|
|
499
|
-
left =
|
|
535
|
+
operator = format_operator(operation[:operator])
|
|
536
|
+
left =
|
|
537
|
+
if %w[if unless].include?(operator)
|
|
538
|
+
format_modifier_left(operation[:left], indent: indent)
|
|
539
|
+
else
|
|
540
|
+
format_nested_statement(operation[:left], indent: indent)
|
|
541
|
+
end
|
|
500
542
|
right = format_nested_statement(operation[:right], indent: indent)
|
|
543
|
+
|
|
501
544
|
if right.include?("\n")
|
|
502
545
|
first_line, *rest = right.lines(chomp: true)
|
|
503
546
|
first_line = first_line.lstrip
|
|
@@ -512,16 +555,65 @@ class Code
|
|
|
512
555
|
"#{left} #{operator} #{right}"
|
|
513
556
|
end
|
|
514
557
|
|
|
558
|
+
def format_modifier_left(statement, indent:)
|
|
559
|
+
if statement.is_a?(Hash) && statement.key?(:right_operation)
|
|
560
|
+
nested = statement[:right_operation]
|
|
561
|
+
nested_operator = nested[:operator].to_s
|
|
562
|
+
if nested_operator == "="
|
|
563
|
+
left = format_nested_statement(nested[:left], indent: indent)
|
|
564
|
+
right = format_nested_statement(nested[:right], indent: indent)
|
|
565
|
+
return "#{left} #{nested_operator} #{group_multiline_expression(right, indent: indent)}"
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
format_nested_statement(statement, indent: indent)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def group_multiline_expression(expression, indent:)
|
|
573
|
+
return expression unless expression.include?("\n")
|
|
574
|
+
return expression if expression.lstrip.start_with?("(")
|
|
575
|
+
|
|
576
|
+
normalized = normalize_group_indentation(expression, indent: indent)
|
|
577
|
+
"(\n#{indent_lines(normalized, indent + 1)}\n#{INDENT * indent})"
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def normalize_group_indentation(expression, indent:)
|
|
581
|
+
prefix = INDENT * indent
|
|
582
|
+
|
|
583
|
+
expression
|
|
584
|
+
.lines(chomp: true)
|
|
585
|
+
.map
|
|
586
|
+
.with_index do |line, index|
|
|
587
|
+
next line if index.zero? || line.empty? || prefix.empty?
|
|
588
|
+
|
|
589
|
+
line.delete_prefix(prefix)
|
|
590
|
+
end
|
|
591
|
+
.join("\n")
|
|
592
|
+
end
|
|
593
|
+
|
|
515
594
|
def compact_operator?(operator)
|
|
516
595
|
%w[. :: &. .. ...].include?(operator)
|
|
517
596
|
end
|
|
518
597
|
|
|
598
|
+
def format_operator(operator)
|
|
599
|
+
case operator.to_s
|
|
600
|
+
when "||"
|
|
601
|
+
"or"
|
|
602
|
+
when "&&"
|
|
603
|
+
"and"
|
|
604
|
+
else
|
|
605
|
+
operator.to_s
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
|
|
519
609
|
def format_ternary(ternary, indent:)
|
|
520
610
|
left = format_nested_statement(ternary[:left], indent: indent)
|
|
521
611
|
middle = format_nested_statement(ternary[:middle], indent: indent)
|
|
612
|
+
middle = group_multiline_expression(middle, indent: indent)
|
|
522
613
|
return "#{left} ? #{middle}" unless ternary.key?(:right)
|
|
523
614
|
|
|
524
615
|
right = format_nested_statement(ternary[:right], indent: indent)
|
|
616
|
+
right = group_multiline_expression(right, indent: indent)
|
|
525
617
|
"#{left} ? #{middle} : #{right}"
|
|
526
618
|
end
|
|
527
619
|
|
|
@@ -654,7 +746,7 @@ class Code
|
|
|
654
746
|
|
|
655
747
|
def indent_lines(value, indent)
|
|
656
748
|
prefix = INDENT * indent
|
|
657
|
-
value.split("\n").map { |line| "#{prefix}#{line}" }.join("\n")
|
|
749
|
+
value.split("\n").map { |line| line.empty? ? "" : "#{prefix}#{line}" }.join("\n")
|
|
658
750
|
end
|
|
659
751
|
|
|
660
752
|
def statement_separator(inline:, indent: nil)
|
data/lib/code/object/string.rb
CHANGED
|
@@ -157,6 +157,10 @@ class Code
|
|
|
157
157
|
|
|
158
158
|
def code_strip
|
|
159
159
|
String.new(raw.strip)
|
|
160
|
+
rescue ArgumentError, Encoding::CompatibilityError => e
|
|
161
|
+
raise unless e.message.include?("invalid byte sequence")
|
|
162
|
+
|
|
163
|
+
String.new(sanitized_utf8_raw.strip)
|
|
160
164
|
end
|
|
161
165
|
|
|
162
166
|
def code_split(value)
|
|
@@ -172,6 +176,15 @@ class Code
|
|
|
172
176
|
def present?
|
|
173
177
|
raw.present?
|
|
174
178
|
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def sanitized_utf8_raw
|
|
183
|
+
raw
|
|
184
|
+
.dup
|
|
185
|
+
.force_encoding(::Encoding::UTF_8)
|
|
186
|
+
.encode(::Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
187
|
+
end
|
|
175
188
|
end
|
|
176
189
|
end
|
|
177
190
|
end
|
data/spec/code/format_spec.rb
CHANGED
|
@@ -13,6 +13,8 @@ RSpec.describe Code::Format do
|
|
|
13
13
|
%w[100000 100_000],
|
|
14
14
|
%w[1000000 1_000_000],
|
|
15
15
|
%w[1.0000000001 1.000_000_000_1],
|
|
16
|
+
["true || false", "true or false"],
|
|
17
|
+
["true && false", "true and false"],
|
|
16
18
|
["{a:1}", "{ a: 1 }"],
|
|
17
19
|
["[1,2,3]", "[1, 2, 3]"],
|
|
18
20
|
["[1, 2, 3].select { |n| n.even? }", "[1, 2, 3].select { |n| n.even? }"],
|
|
@@ -54,20 +56,38 @@ RSpec.describe Code::Format do
|
|
|
54
56
|
],
|
|
55
57
|
[
|
|
56
58
|
"sections << Html.join([Html.p { Html.b { \"{index + 1}. {title}\" } }, Html.p { query } if query.presence, Html.p { Html.a(href: link || inline_url) { :source } } if (link || inline_url), Html.p { Html.a(href: inline_url) { Html.img(src: inline_url, alt: title) } }, Html.p { Html.a(href: attachment_url) { \"télécharger\" } }].compact)",
|
|
57
|
-
"sections << Html.join(\n [\n Html.p { Html.b { \"{index + 1}. {title}\" } },\n Html.p { query } if query.presence,\n Html.p {\n Html.a(href: link
|
|
59
|
+
"sections << Html.join(\n [\n Html.p { Html.b { \"{index + 1}. {title}\" } },\n Html.p { query } if query.presence,\n Html.p {\n Html.a(href: link or inline_url) { :source }\n } if (link or inline_url),\n Html.p {\n Html.a(href: inline_url) { Html.img(src: inline_url, alt: title) }\n },\n Html.p {\n Html.a(href: attachment_url) { \"télécharger\" }\n }\n ].compact\n)"
|
|
58
60
|
],
|
|
59
61
|
[
|
|
60
62
|
"safe = post.present? and !post[:over_18] and post[:post_hint] == :image and post[:url].to_string.strip.presence and (post[:url].to_string.strip.ends_with?(\".jpg\") or post[:url].to_string.strip.ends_with?(\".jpeg\") or post[:url].to_string.strip.ends_with?(\".png\") or post[:url].to_string.strip.include?(\"i.redd.it\"))",
|
|
61
|
-
"safe = post.present?\n and !post[:over_18]\n and post[:post_hint] == :image\n and post[:url].to_string.strip.presence\n and (post[:url].to_string.strip.ends_with?(\".jpg\")\n
|
|
63
|
+
"safe = post.present?\n and !post[:over_18]\n and post[:post_hint] == :image\n and post[:url].to_string.strip.presence\n and (\n post[:url].to_string.strip.ends_with?(\".jpg\")\n or post[:url].to_string.strip.ends_with?(\".jpeg\")\n or post[:url].to_string.strip.ends_with?(\".png\")\n or post[:url].to_string.strip.include?(\"i.redd.it\")\n)"
|
|
62
64
|
],
|
|
63
65
|
[
|
|
64
66
|
"items.each { |item, index| proxied_image_url = if image_url proxy_url(image_url) else nothing end }",
|
|
65
67
|
"items.each { |item, index|\n proxied_image_url = if image_url\n proxy_url(image_url)\n else\n nothing\n end\n}"
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
"lines << \"vacances scolaires france {zone} (prochains {months_ahead} mois) :\".downcase",
|
|
71
|
+
"lines << (\n \"vacances scolaires france {zone} (prochains \"\n + \"{months_ahead} mois) :\"\n).downcase"
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
"src = \"https://proxy.dorianmarie.com?\" + \"{{ url: src, disposition: :inline }.to_query}\" if src",
|
|
75
|
+
"src = (\n \"https://proxy.dorianmarie.com?\"\n + \"{{ url: src, disposition: :inline }.to_query}\"\n) if src"
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
"inline_image = image ? \"https://proxy.dorianmarie.com?\" + \"{inline_params.to_query}\" : nothing",
|
|
79
|
+
"inline_image = image ? (\n \"https://proxy.dorianmarie.com?\"\n + \"{inline_params.to_query}\"\n) : nothing"
|
|
66
80
|
]
|
|
67
81
|
].each do |input, expected|
|
|
68
82
|
it "formats #{input.inspect}" do
|
|
69
83
|
expect(described_class.format(Code.parse(input))).to eq(expected)
|
|
70
84
|
end
|
|
85
|
+
|
|
86
|
+
it "formats #{input.inspect} idempotently" do
|
|
87
|
+
formatted = described_class.format(Code.parse(input))
|
|
88
|
+
|
|
89
|
+
expect(described_class.format(Code.parse(formatted))).to eq(formatted)
|
|
90
|
+
end
|
|
71
91
|
end
|
|
72
92
|
|
|
73
93
|
it "round-trips parse and evaluation semantics for formatted code" do
|
|
@@ -77,5 +97,50 @@ RSpec.describe Code::Format do
|
|
|
77
97
|
expect(Code.parse(formatted)).to be_present
|
|
78
98
|
expect(Code.evaluate(formatted)).to eq(Code.evaluate(input))
|
|
79
99
|
end
|
|
100
|
+
|
|
101
|
+
it "keeps grouped multiline receivers stable" do
|
|
102
|
+
input = <<~CODE.chomp
|
|
103
|
+
lines << (
|
|
104
|
+
"vacances scolaires france {zone} (prochains "
|
|
105
|
+
+ "{months_ahead} mois) :"
|
|
106
|
+
).downcase
|
|
107
|
+
CODE
|
|
108
|
+
|
|
109
|
+
expect(described_class.format(Code.parse(input))).to eq(input)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "does not split operators inside string interpolations when wrapping" do
|
|
113
|
+
input =
|
|
114
|
+
%q(body_text = "{title}\n\n{description}\n\ningrédients :\n" + ingredients.map { |ingredient| "- {ingredient}" }.join("\n") + "\n\ninstructions :\n" + instructions.map { |instruction, index| "{index + 1}. {instruction}" }.join("\n"))
|
|
115
|
+
|
|
116
|
+
formatted = described_class.format(Code.parse(input))
|
|
117
|
+
|
|
118
|
+
expect(formatted).to include(
|
|
119
|
+
'instructions.map { |instruction, index| "{index + 1}. {instruction}" }'
|
|
120
|
+
)
|
|
121
|
+
expect(formatted).not_to include(
|
|
122
|
+
"\"{index\n + 1}. {instruction}\""
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "does not emit whitespace-only blank lines" do
|
|
127
|
+
input = <<~CODE.chomp
|
|
128
|
+
body = Html.join(elements.map { |element| title = element.at_css(".x") value = element.at_css(".y") title }, Html.br)
|
|
129
|
+
CODE
|
|
130
|
+
|
|
131
|
+
formatted = described_class.format(Code.parse(input))
|
|
132
|
+
|
|
133
|
+
expect(formatted.lines).not_to include(match(/\A[ \t]+\n\z/))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "keeps lines within 80 characters" do
|
|
137
|
+
input = <<~CODE.chomp
|
|
138
|
+
src = "https://proxy.dorianmarie.com?" + "{{ url: src, disposition: :inline }.to_query}" if src
|
|
139
|
+
CODE
|
|
140
|
+
|
|
141
|
+
formatted = described_class.format(Code.parse(input))
|
|
142
|
+
|
|
143
|
+
expect(formatted.lines.map(&:chomp).map(&:length).max).to be <= 80
|
|
144
|
+
end
|
|
80
145
|
end
|
|
81
146
|
end
|
|
@@ -15,4 +15,12 @@ RSpec.describe Code::Object::String do
|
|
|
15
15
|
expect(Code.evaluate(input).to_s).to eq(expected)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
describe "#code_strip" do
|
|
20
|
+
it "replaces invalid utf-8 bytes instead of raising" do
|
|
21
|
+
string = described_class.new("\xC3 ".b.force_encoding(Encoding::UTF_8))
|
|
22
|
+
|
|
23
|
+
expect(string.code_strip.to_s).to eq("\uFFFD")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
18
26
|
end
|