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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb5a51861ed784ebc8e0fbd2e6e6232ee05a3f3bafbc4f150e5cc92236590254
4
- data.tar.gz: b2a82b40b6740387fde8490e5054cc1a0ab5d39c34e2ce7a694421cf5e0852ed
3
+ metadata.gz: 6baa3c7c3ef6b602529e2af4a36200029291ed89c115f1c1f3d750a8e815cc03
4
+ data.tar.gz: aa9c876fb0aff915f895ec3d209d25ac06270e3809eadba8f5e71b37b9cb29db
5
5
  SHA512:
6
- metadata.gz: ca94cf8211ff117ca68db85a48d876a28f857398996b327a88a6488931d29bbdda34bc6d1b893af9196ecc25735a83875533f099cecbc15b5c9ffd248e34d0df
7
- data.tar.gz: 9ecf7d03fffb87b1d7ade9242bc0cc4e75a8ff04b31c57b3c05cb2caba5811c1cb567de362e0c95c5852b2bebc3794df6291adb773b8b4a13da0c7320df03b3f
6
+ metadata.gz: 444adc5d9b9a5223b82a921a888558a2f23dc40344a12b383137581b910ba4e33ea13873a2e5ce6698dc130ae77775cf9aa5b3e0e23f70947418301831df96b9
7
+ data.tar.gz: bcf7ecebc28788a2cf7464e3a7b67f32b2bfb86ec3d6365ca58674914d852181d27f3a051b596665807acd67291c2026d227b98d33603260e3122417e04d6a76
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- code-ruby (3.0.11)
4
+ code-ruby (3.0.13)
5
5
  activesupport
6
6
  base64
7
7
  bigdecimal
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.11
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
- "(#{format_code_inline(statement[:group], indent: indent)})"
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
- else
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].to_s
499
- left = format_nested_statement(operation[:left], indent: indent)
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)
@@ -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
@@ -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 || inline_url) { :source }\n } if (link || 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)"
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 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\"))"
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: code-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.11
4
+ version: 3.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dorian Marié