code-ruby 3.0.10 → 3.0.12
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 +134 -14
- data/spec/code/format_spec.rb +75 -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: eef80c2ebc4ef6d318ee0af151704caca0f0a370c698dc727814836d6e5146db
|
|
4
|
+
data.tar.gz: 6ac8ae69e473aa937eb4794466e710ec40444c53f5d1a12149368e03de0bc4c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b99011ea5c71b86b7709949e2d4096f4708b2a4c985e1f30e704bac060c58c710922797c4c4ade47f91b31ad37a47153f66fd8e204783786d55da38234e0d79
|
|
7
|
+
data.tar.gz: 367d671e1fbb4aaacd278bd3e186a03a7304f7041f5381e502a54e44afe754048ed603eb83e6e21f2c8415916078b633fc352984ae6c5b4ab27f96c330394b19
|
data/Gemfile.lock
CHANGED
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.0.
|
|
1
|
+
3.0.12
|
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)
|
|
@@ -431,19 +438,28 @@ class Code
|
|
|
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
|
-
if
|
|
438
|
-
right.include?("\n")
|
|
444
|
+
if right.include?("\n")
|
|
439
445
|
first_line, *rest = right.lines(chomp: true)
|
|
440
|
-
[
|
|
446
|
+
if multiline_operand_statement?(other[:statement]) ||
|
|
447
|
+
!%w[and or].include?(operator)
|
|
448
|
+
[ "#{expression} #{operator} #{first_line.lstrip}", *rest ].join("\n")
|
|
449
|
+
else
|
|
450
|
+
[
|
|
451
|
+
"#{expression}\n#{INDENT * (indent + 1)}#{operator} #{first_line.lstrip}",
|
|
452
|
+
*rest
|
|
453
|
+
].join("\n")
|
|
454
|
+
end
|
|
441
455
|
elsif expression.include?("\n") || candidate.length > MAX_LINE_LENGTH
|
|
442
456
|
right_lines =
|
|
443
457
|
if right.include?("\n")
|
|
444
458
|
right.lines(chomp: true).map(&:lstrip)
|
|
445
|
-
|
|
459
|
+
elsif %w[and or].include?(operator)
|
|
446
460
|
right.split(" #{operator} ")
|
|
461
|
+
else
|
|
462
|
+
[right]
|
|
447
463
|
end
|
|
448
464
|
continuation_lines =
|
|
449
465
|
right_lines.map do |line|
|
|
@@ -462,6 +478,34 @@ class Code
|
|
|
462
478
|
expression
|
|
463
479
|
end
|
|
464
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
|
+
|
|
465
509
|
def extract_string_concatenation_parts(statement)
|
|
466
510
|
return nil unless statement.is_a?(Hash)
|
|
467
511
|
|
|
@@ -489,11 +533,64 @@ class Code
|
|
|
489
533
|
|
|
490
534
|
def format_right_operation(operation, indent:)
|
|
491
535
|
operator = operation[:operator].to_s
|
|
492
|
-
left =
|
|
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
|
|
493
542
|
right = format_nested_statement(operation[:right], indent: indent)
|
|
543
|
+
|
|
544
|
+
if right.include?("\n")
|
|
545
|
+
first_line, *rest = right.lines(chomp: true)
|
|
546
|
+
first_line = first_line.lstrip
|
|
547
|
+
return "#{left} #{operator} #{first_line}" if rest.empty?
|
|
548
|
+
|
|
549
|
+
return [
|
|
550
|
+
"#{left} #{operator} #{first_line}",
|
|
551
|
+
*rest
|
|
552
|
+
].join("\n")
|
|
553
|
+
end
|
|
554
|
+
|
|
494
555
|
"#{left} #{operator} #{right}"
|
|
495
556
|
end
|
|
496
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
|
+
|
|
497
594
|
def compact_operator?(operator)
|
|
498
595
|
%w[. :: &. .. ...].include?(operator)
|
|
499
596
|
end
|
|
@@ -501,9 +598,11 @@ class Code
|
|
|
501
598
|
def format_ternary(ternary, indent:)
|
|
502
599
|
left = format_nested_statement(ternary[:left], indent: indent)
|
|
503
600
|
middle = format_nested_statement(ternary[:middle], indent: indent)
|
|
601
|
+
middle = group_multiline_expression(middle, indent: indent)
|
|
504
602
|
return "#{left} ? #{middle}" unless ternary.key?(:right)
|
|
505
603
|
|
|
506
604
|
right = format_nested_statement(ternary[:right], indent: indent)
|
|
605
|
+
right = group_multiline_expression(right, indent: indent)
|
|
507
606
|
"#{left} ? #{middle} : #{right}"
|
|
508
607
|
end
|
|
509
608
|
|
|
@@ -598,9 +697,22 @@ class Code
|
|
|
598
697
|
values.join(", ").length > MAX_INLINE_COLLECTION_LENGTH
|
|
599
698
|
end
|
|
600
699
|
|
|
601
|
-
def
|
|
602
|
-
statement.is_a?(Hash)
|
|
603
|
-
|
|
700
|
+
def multiline_operand_statement?(statement)
|
|
701
|
+
return false unless statement.is_a?(Hash)
|
|
702
|
+
|
|
703
|
+
return true if statement.key?(:dictionnary) || statement.key?(:list)
|
|
704
|
+
return true if statement.key?(:call)
|
|
705
|
+
if statement.key?(:left_operation)
|
|
706
|
+
operation = statement[:left_operation]
|
|
707
|
+
others = Array(operation[:others])
|
|
708
|
+
|
|
709
|
+
return false if others.empty?
|
|
710
|
+
return false unless others.all? { |other| compact_operator?(other[:operator]) }
|
|
711
|
+
|
|
712
|
+
return multiline_operand_statement?(operation[:first])
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
false
|
|
604
716
|
end
|
|
605
717
|
|
|
606
718
|
def multiline_call_arguments?(raw_arguments, arguments)
|
|
@@ -623,7 +735,7 @@ class Code
|
|
|
623
735
|
|
|
624
736
|
def indent_lines(value, indent)
|
|
625
737
|
prefix = INDENT * indent
|
|
626
|
-
value.split("\n").map { |line| "#{prefix}#{line}" }.join("\n")
|
|
738
|
+
value.split("\n").map { |line| line.empty? ? "" : "#{prefix}#{line}" }.join("\n")
|
|
627
739
|
end
|
|
628
740
|
|
|
629
741
|
def statement_separator(inline:, indent: nil)
|
|
@@ -692,7 +804,7 @@ class Code
|
|
|
692
804
|
search_limit = [MAX_LINE_LENGTH, line.length - token.length].min
|
|
693
805
|
index = line.rindex(token, search_limit)
|
|
694
806
|
while index
|
|
695
|
-
break if index.positive? &&
|
|
807
|
+
break if index.positive? && outside_string_and_grouping?(line, index)
|
|
696
808
|
|
|
697
809
|
index = line.rindex(token, index - 1)
|
|
698
810
|
end
|
|
@@ -701,9 +813,10 @@ class Code
|
|
|
701
813
|
[index, token]
|
|
702
814
|
end
|
|
703
815
|
|
|
704
|
-
def
|
|
816
|
+
def outside_string_and_grouping?(line, index)
|
|
705
817
|
quote_count = 0
|
|
706
818
|
escaped = false
|
|
819
|
+
grouping_depth = 0
|
|
707
820
|
line[0...index].each_char do |char|
|
|
708
821
|
if escaped
|
|
709
822
|
escaped = false
|
|
@@ -714,10 +827,17 @@ class Code
|
|
|
714
827
|
escaped = true
|
|
715
828
|
elsif char == '"'
|
|
716
829
|
quote_count += 1
|
|
830
|
+
elsif quote_count.even?
|
|
831
|
+
case char
|
|
832
|
+
when "(", "[", "{"
|
|
833
|
+
grouping_depth += 1
|
|
834
|
+
when ")", "]", "}"
|
|
835
|
+
grouping_depth -= 1 if grouping_depth.positive?
|
|
836
|
+
end
|
|
717
837
|
end
|
|
718
838
|
end
|
|
719
839
|
|
|
720
|
-
quote_count.even?
|
|
840
|
+
quote_count.even? && grouping_depth.zero?
|
|
721
841
|
end
|
|
722
842
|
end
|
|
723
843
|
end
|
data/spec/code/format_spec.rb
CHANGED
|
@@ -51,11 +51,41 @@ RSpec.describe Code::Format do
|
|
|
51
51
|
[
|
|
52
52
|
"blocks << { title: \"hello world\", description: \"lorem ipsum dolor es sit\", position: 1 }",
|
|
53
53
|
"blocks << {\n title: \"hello world\",\n description: \"lorem ipsum dolor es sit\",\n position: 1\n}"
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
"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)"
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
"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 (\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
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"items.each { |item, index| proxied_image_url = if image_url proxy_url(image_url) else nothing end }",
|
|
65
|
+
"items.each { |item, index|\n proxied_image_url = if image_url\n proxy_url(image_url)\n else\n nothing\n end\n}"
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"lines << \"vacances scolaires france {zone} (prochains {months_ahead} mois) :\".downcase",
|
|
69
|
+
"lines << (\n \"vacances scolaires france {zone} (prochains \"\n + \"{months_ahead} mois) :\"\n).downcase"
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"src = \"https://proxy.dorianmarie.com?\" + \"{{ url: src, disposition: :inline }.to_query}\" if src",
|
|
73
|
+
"src = (\n \"https://proxy.dorianmarie.com?\"\n + \"{{ url: src, disposition: :inline }.to_query}\"\n) if src"
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
"inline_image = image ? \"https://proxy.dorianmarie.com?\" + \"{inline_params.to_query}\" : nothing",
|
|
77
|
+
"inline_image = image ? (\n \"https://proxy.dorianmarie.com?\"\n + \"{inline_params.to_query}\"\n) : nothing"
|
|
54
78
|
]
|
|
55
79
|
].each do |input, expected|
|
|
56
80
|
it "formats #{input.inspect}" do
|
|
57
81
|
expect(described_class.format(Code.parse(input))).to eq(expected)
|
|
58
82
|
end
|
|
83
|
+
|
|
84
|
+
it "formats #{input.inspect} idempotently" do
|
|
85
|
+
formatted = described_class.format(Code.parse(input))
|
|
86
|
+
|
|
87
|
+
expect(described_class.format(Code.parse(formatted))).to eq(formatted)
|
|
88
|
+
end
|
|
59
89
|
end
|
|
60
90
|
|
|
61
91
|
it "round-trips parse and evaluation semantics for formatted code" do
|
|
@@ -65,5 +95,50 @@ RSpec.describe Code::Format do
|
|
|
65
95
|
expect(Code.parse(formatted)).to be_present
|
|
66
96
|
expect(Code.evaluate(formatted)).to eq(Code.evaluate(input))
|
|
67
97
|
end
|
|
98
|
+
|
|
99
|
+
it "keeps grouped multiline receivers stable" do
|
|
100
|
+
input = <<~CODE.chomp
|
|
101
|
+
lines << (
|
|
102
|
+
"vacances scolaires france {zone} (prochains "
|
|
103
|
+
+ "{months_ahead} mois) :"
|
|
104
|
+
).downcase
|
|
105
|
+
CODE
|
|
106
|
+
|
|
107
|
+
expect(described_class.format(Code.parse(input))).to eq(input)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "does not split operators inside string interpolations when wrapping" do
|
|
111
|
+
input =
|
|
112
|
+
%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"))
|
|
113
|
+
|
|
114
|
+
formatted = described_class.format(Code.parse(input))
|
|
115
|
+
|
|
116
|
+
expect(formatted).to include(
|
|
117
|
+
'instructions.map { |instruction, index| "{index + 1}. {instruction}" }'
|
|
118
|
+
)
|
|
119
|
+
expect(formatted).not_to include(
|
|
120
|
+
"\"{index\n + 1}. {instruction}\""
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "does not emit whitespace-only blank lines" do
|
|
125
|
+
input = <<~CODE.chomp
|
|
126
|
+
body = Html.join(elements.map { |element| title = element.at_css(".x") value = element.at_css(".y") title }, Html.br)
|
|
127
|
+
CODE
|
|
128
|
+
|
|
129
|
+
formatted = described_class.format(Code.parse(input))
|
|
130
|
+
|
|
131
|
+
expect(formatted.lines).not_to include(match(/\A[ \t]+\n\z/))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "keeps lines within 80 characters" do
|
|
135
|
+
input = <<~CODE.chomp
|
|
136
|
+
src = "https://proxy.dorianmarie.com?" + "{{ url: src, disposition: :inline }.to_query}" if src
|
|
137
|
+
CODE
|
|
138
|
+
|
|
139
|
+
formatted = described_class.format(Code.parse(input))
|
|
140
|
+
|
|
141
|
+
expect(formatted.lines.map(&:chomp).map(&:length).max).to be <= 80
|
|
142
|
+
end
|
|
68
143
|
end
|
|
69
144
|
end
|