synvert-core 1.23.1 → 1.25.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 862649a82e2cd9d14454b65b9c3e146c2c6adff96a03441624cca7d83f42a8f5
4
- data.tar.gz: 167e5ee0a3ae617888af58dd40ee582d3427012a5e0daea881f13ffd51a07b8a
3
+ metadata.gz: 69a5a31926536922320b7184cae70096bba437729c6c4dbe6c0bb2ab98d56e43
4
+ data.tar.gz: afeed86dae83348eccb85859dcc907250ff1e5361d69b7a71839cb88c1fc3e79
5
5
  SHA512:
6
- metadata.gz: dbf9a15e4e795edf0bedcb690b163b088ca00236545881744ef8ac004c68059cf90654d3804626e29e8b3ce4a9e3ccb3e9e7cf6d82ea05c98c941f6e59c77b8c
7
- data.tar.gz: 916953fc304f4a2054b3722197ec0d194a958b2f502d5ea379411b1de970fc9691aaa3caf68c3458f99281a92d7c316bb084fe3deaba7af0fd03e0df7cbd31c9
6
+ metadata.gz: 50d06d1c664c06fac3e7bc163226c3e98305bd26d802919ce73003ad4e8d5e1cf441d2559b32aeada9266d48cb7d3365ef2d39fa37a934bd2e0fe2ad489693ee
7
+ data.tar.gz: 34915fcbb4252d66c9b25318b96e1f4bf6f4a8881f8d85229bc7f6f5cbf80ff50aed3f774cddcbab10495ef865daff0c6d40fd6608382e9e97d7a42b16ca6fc3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.25.0 (2023-04-17)
4
+
5
+ * Update `wrap` dsl
6
+ * Update `node_mutation` to 1.15.1
7
+ * Fix `erb` encoded source
8
+
9
+ ## 1.24.0 (2023-04-16)
10
+
11
+ * Add `Slim` engine
12
+ * Abstract `Engine/Elegant` module
13
+ * Rewrite `Haml` and `Slim` engine by StringScanner
14
+
3
15
  ## 1.23.1 (2023-04-06)
4
16
 
5
17
  * Fix array index name in `GotoScope`
@@ -7,7 +19,7 @@
7
19
 
8
20
  ## 1.23.0 (2023-04-05)
9
21
 
10
- * Add haml engine
22
+ * Add `Haml` engine
11
23
  * Add `Engine.register` and `Engine.encode` and `Engine.generate_transform_proc`
12
24
  * Set NodeMutation `transform_proc`
13
25
  * Update `node_mutation` to 1.14.0
data/Gemfile.lock CHANGED
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- synvert-core (1.23.1)
4
+ synvert-core (1.25.0)
5
5
  activesupport (< 7.0.0)
6
- node_mutation (>= 1.14.0)
6
+ node_mutation (>= 1.15.1)
7
7
  node_query (>= 1.12.1)
8
8
  parallel
9
9
  parser
@@ -48,7 +48,7 @@ GEM
48
48
  method_source (1.0.0)
49
49
  minitest (5.18.0)
50
50
  nenv (0.3.0)
51
- node_mutation (1.14.0)
51
+ node_mutation (1.15.1)
52
52
  node_query (1.12.1)
53
53
  notiffany (0.1.3)
54
54
  nenv (~> 0.1)
data/README.md CHANGED
@@ -94,7 +94,7 @@ Actions:
94
94
  * [replace](https://xinminlabs.github.io/synvert-core-ruby/Synvert/Core/Rewriter/Instance.html#replace-instance_method) - replace the code of specified child nodes
95
95
  * [delete](https://xinminlabs.github.io/synvert-core-ruby/Synvert/Core/Rewriter/Instance.html#delete-instance_method) - delete the code in specified child nodes
96
96
  * [remove](https://xinminlabs.github.io/synvert-core-ruby/Synvert/Core/Rewriter/Instance.html#remove-instance_method) - remove the whole code of current node
97
- * [wrap](https://xinminlabs.github.io/synvert-core-ruby/Synvert/Core/Rewriter/Instance.html#wrap-instance_method) - wrap the current node with code
97
+ * [wrap](https://xinminlabs.github.io/synvert-core-ruby/Synvert/Core/Rewriter/Instance.html#wrap-instance_method) - wrap the current node with prefix and suffix code
98
98
  * [replace_with](https://xinminlabs.github.io/synvert-core-ruby/Synvert/Core/Rewriter/Instance.html#replace_with-instance_method) - replace the whole code of current node
99
99
  * [warn](https://xinminlabs.github.io/synvert-core-ruby/Synvert/Core/Rewriter/Instance.html#warn-instance_method) - warn message
100
100
  * [replace_erb_stmt_with_expr](https://xinminlabs.github.io/synvert-core-ruby/Synvert/Core/Rewriter/Instance.html#replace_erb_stmt_with_expr-instance_method) - replace erb stmt code to expr code
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synvert::Core
4
+ module Engine
5
+ # Engine::Elegant provides some helper methods for engines
6
+ # who read block code without end.
7
+ # For those engines, it will try to insert `end\n` when encode,
8
+ # while delete `end\n` when generate_transform_proc.
9
+ module Elegant
10
+ END_LINE = "end\n"
11
+ WHITESPACE = ' '
12
+ DO_BLOCK_REGEX = / do(\s|\z|\n)/
13
+
14
+ IF_KEYWORDS = %w[if unless begin case for while until]
15
+ ELSE_KEYWORDS = %w[else elsif when in rescue ensure]
16
+
17
+ # Generate transform proc, it's used to adjust start and end position of actions.
18
+ # Due to the fact that we insert `end\n` when encode the source code, we need to adjust
19
+ # start and end position of actions to match the original source code.
20
+ # e.g. if end\n exists in position 10, action start position is 20 and end position is 30,
21
+ # then action start position should be 16 and end position should be 26.
22
+ #
23
+ # @param encoded_source [String] encoded source.
24
+ # @return [Proc] transform proc.
25
+ def generate_transform_proc(encoded_source)
26
+ proc do |actions|
27
+ start = 0
28
+ indices = []
29
+ loop do
30
+ index = encoded_source[start..-1].index(END_LINE)
31
+ break unless index
32
+
33
+ indices << (start + index)
34
+ start += index + END_LINE.length
35
+ end
36
+ indices.each do |index|
37
+ actions.each do |action|
38
+ action.start -= END_LINE.length if action.start > index
39
+ action.end -= END_LINE.length if action.end > index
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ # Check if the current leading_spaces_count is less than or equal to the last leading_spaces_count in leading_spaces_counts.
46
+ # If so, pop the last leading_spaces_count and return true.
47
+ def insert_end?(leading_spaces_counts, current_leading_spaces_count, equal = true)
48
+ operation = equal ? :<= : :<
49
+ if !leading_spaces_counts.empty? && current_leading_spaces_count.send(operation, leading_spaces_counts.last)
50
+ leading_spaces_counts.pop
51
+ true
52
+ else
53
+ false
54
+ end
55
+ end
56
+
57
+ def scan_ruby_statement(scanner, new_code, leading_spaces_counts, leading_spaces_count)
58
+ new_code << scanner.scan(/\s*/)
59
+ keyword = scanner.scan(/\w+/)
60
+ rest = scanner.scan(/.*?(\z|\n)/)
61
+ if insert_end?(leading_spaces_counts, leading_spaces_count, !ELSE_KEYWORDS.include?(keyword))
62
+ new_code << END_LINE
63
+ end
64
+ if IF_KEYWORDS.include?(keyword) || rest =~ DO_BLOCK_REGEX
65
+ leading_spaces_counts << leading_spaces_count
66
+ end
67
+ new_code << keyword
68
+ new_code << rest
69
+
70
+ while rest.rstrip.end_with?(',')
71
+ rest = scanner.scan(/.*?(\z|\n)/)
72
+ new_code << rest
73
+ end
74
+ end
75
+
76
+ def scan_ruby_expression(scanner, new_code, leading_spaces_counts, leading_spaces_count)
77
+ if insert_end?(leading_spaces_counts, leading_spaces_count)
78
+ new_code << END_LINE
79
+ end
80
+ rest = scanner.scan(/.*?(\z|\n)/)
81
+ if rest =~ DO_BLOCK_REGEX
82
+ leading_spaces_counts << leading_spaces_count
83
+ end
84
+ new_code << rest
85
+
86
+ while rest.rstrip.end_with?(',')
87
+ rest = scanner.scan(/.*?(\z|\n)/)
88
+ new_code << rest
89
+ end
90
+ end
91
+
92
+ def scan_ruby_interpolation_and_plain_text(scanner, new_code, leading_spaces_counts, leading_spaces_count)
93
+ if insert_end?(leading_spaces_counts, leading_spaces_count)
94
+ new_code << END_LINE
95
+ end
96
+ while scanner.scan(/(.*?)(\\*)#\{/) # it matches interpolation " #{current_user.login}"
97
+ new_code << (WHITESPACE * scanner.matched.size)
98
+ unless scanner.matched[-3] == '\\'
99
+ count = 1
100
+ while scanner.scan(/.*?([\{\}])/)
101
+ if scanner.matched[-1] == '}'
102
+ count -= 1
103
+ if count == 0
104
+ new_code << (scanner.matched[0..-2] + ';')
105
+ break
106
+ else
107
+ new_code << scanner.matched
108
+ end
109
+ else
110
+ count += 1
111
+ new_code << scanner.matched
112
+ end
113
+ end
114
+ end
115
+ end
116
+ if scanner.scan(/.*?\z/)
117
+ new_code << (WHITESPACE * scanner.matched.size)
118
+ end
119
+ if scanner.scan(/.*?\n/)
120
+ new_code << ((WHITESPACE * (scanner.matched.size - 1)) + "\n")
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -9,9 +9,9 @@ module Synvert::Core
9
9
  # @param source [String] erb code.
10
10
  # @return [String] encoded ruby code.
11
11
  def encode(source)
12
- source.gsub(/%>.*?<%=?/m) { |str| replace_all_code_but_white_space_characters(str) }
12
+ source.gsub(/%>.*?<%=?/m) { |str| ';' + replace_all_code_but_white_space_characters(str[1..-1]) }
13
13
  .sub(/^.*?<%=?/m) { |str| replace_all_code_but_white_space_characters(str) }
14
- .sub(/%>.*?$/m) { |str| replace_all_code_but_white_space_characters(str) }
14
+ .sub(/%>.*?$/m) { |str| ';' + replace_all_code_but_white_space_characters(str[1..-1]) }
15
15
  end
16
16
 
17
17
  # Generate an empty proc.
@@ -4,7 +4,7 @@ module Synvert::Core
4
4
  module Engine
5
5
  class Haml
6
6
  class << self
7
- END_LINE = "end\n"
7
+ include Elegant
8
8
 
9
9
  # Encode haml string, leave only ruby code, replace other haml code with whitespace.
10
10
  # And insert `end\n` for each if, unless, begin, case to make it a valid ruby code.
@@ -12,70 +12,55 @@ module Synvert::Core
12
12
  # @param source [String] haml code.
13
13
  # @return [String] encoded ruby code.
14
14
  def encode(source)
15
- tab_sizes = []
16
- lines =
17
- source.lines.map do |line|
18
- if line =~ /\A(\s*)(- ?)(.*)/ # match " - if currenet_user"
19
- code = (' ' * ($1.size + $2.size)) + $3
20
- new_line =
21
- $3.start_with?('else', 'elsif', 'when') ? code : check_and_insert_end(code, tab_sizes, $1.size)
22
- tab_sizes.push($1.size) if $3.start_with?('if', 'unless', 'begin', 'case') || $3.include?(' do ')
23
-
24
- new_line
15
+ leading_spaces_counts = []
16
+ new_code = []
17
+ scanner = StringScanner.new(source)
18
+ loop do
19
+ new_code << scanner.scan(/\s*/)
20
+ leading_spaces_count = scanner.matched.size
21
+ if scanner.scan('-') # it matches ruby statement " - current_user"
22
+ new_code << WHITESPACE
23
+ scan_ruby_statement(scanner, new_code, leading_spaces_counts, leading_spaces_count)
24
+ elsif scanner.scan('=') # it matches ruby expression " = current_user.login"
25
+ new_code << WHITESPACE
26
+ scan_ruby_expression(scanner, new_code, leading_spaces_counts, leading_spaces_count)
27
+ elsif scanner.scan(/[%#\.][a-zA-Z0-9\-_%#\.]+/) # it matches element, id and class " %span.user"
28
+ new_code << (WHITESPACE * scanner.matched.size)
29
+ scan_matching_wrapper(scanner, new_code, '{', '}')
30
+ if scanner.scan('=')
31
+ new_code << WHITESPACE
32
+ scan_ruby_expression(scanner, new_code, leading_spaces_counts, leading_spaces_count)
25
33
  else
26
- if line =~ /\A(\s*)([%#\.].*)?=(.*)/ # match " %span= current_user.login"
27
- code = (' ' * ($1.size + ($2 || '').size + 1)) + $3
28
- new_line = check_and_insert_end(code, tab_sizes, $1.size)
29
- tab_sizes.push($1.size) if line.include?(' do ')
30
- new_line
31
- elsif line =~ /\A(\s*)(.*)/ # match any other line
32
- check_and_insert_end(' ' * line.size, tab_sizes, $1.size)
33
- end
34
+ scan_ruby_interpolation_and_plain_text(scanner, new_code, leading_spaces_counts, leading_spaces_count)
34
35
  end
36
+ else
37
+ scan_ruby_interpolation_and_plain_text(scanner, new_code, leading_spaces_counts, leading_spaces_count)
35
38
  end
36
- lines.push(END_LINE) unless tab_sizes.empty?
37
- lines.join("\n")
38
- end
39
39
 
40
- # Generate transform proc, it's used to adjust start and end position of actions.
41
- # Due to the fact that we insert `end\n` when encode the source code, we need to adjust
42
- # start and end position of actions to match the original source code.
43
- # e.g. if end\n exists in position 10, action start position is 20 and end position is 30,
44
- # then action start position should be 16 and end position should be 26.
45
- #
46
- # @param encoded_source [String] encoded source.
47
- # @return [Proc] transform proc.
48
- def generate_transform_proc(encoded_source)
49
- proc do |actions|
50
- start = 0
51
- indices = []
52
- loop do
53
- index = encoded_source[start..-1].index(END_LINE)
54
- break unless index
40
+ break if scanner.eos?
41
+ end
55
42
 
56
- indices << (start + index)
57
- start += index + END_LINE.length
58
- end
59
- indices.each do |index|
60
- actions.each do |action|
61
- action.start -= END_LINE.length if action.start > index
62
- action.end -= END_LINE.length if action.end > index
63
- end
64
- end
43
+ while leading_spaces_counts.pop
44
+ new_code << END_LINE
65
45
  end
46
+ new_code.join
66
47
  end
67
48
 
68
49
  private
69
50
 
70
- # Check if the current tab_size is less than or equal to the last tab_size in tab_sizes.
71
- # If so, pop the last tab_size and insert "end\n" before code.
72
- # otherwise, return code.
73
- def check_and_insert_end(code, tab_sizes, current_tab_size)
74
- if !tab_sizes.empty? && current_tab_size <= tab_sizes[-1]
75
- tab_sizes.pop
76
- "end\n" + code
77
- else
78
- code
51
+ def scan_matching_wrapper(scanner, new_code, start, ending)
52
+ if scanner.scan(start) # it matches attributes " %span{:class => 'user'}"
53
+ new_code << start
54
+ count = 1
55
+ while scanner.scan(/.*?[#{Regexp.quote(start)}#{Regexp.quote(ending)}]/m)
56
+ new_code << scanner.matched
57
+ if scanner.matched[-1] == ending
58
+ count -= 1
59
+ break if count == 0
60
+ else
61
+ count += 1
62
+ end
63
+ end
79
64
  end
80
65
  end
81
66
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synvert::Core
4
+ module Engine
5
+ class Slim
6
+ class << self
7
+ include Elegant
8
+
9
+ ATTRIBUTES_PAIR = { '{' => '}', '[' => ']', '(' => ')' }
10
+
11
+ # Encode haml string, leave only ruby code, replace other haml code with whitespace.
12
+ # And insert `end\n` for each if, unless, begin, case to make it a valid ruby code.
13
+ #
14
+ # @param source [String] haml code.
15
+ # @return [String] encoded ruby code.
16
+ def encode(source)
17
+ leading_spaces_counts = []
18
+ new_code = []
19
+ scanner = StringScanner.new(source)
20
+ loop do
21
+ new_code << scanner.scan(/\s*/)
22
+ leading_spaces_count = scanner.matched.size
23
+ if scanner.scan('-') # it matches ruby statement " - current_user"
24
+ new_code << WHITESPACE
25
+ scan_ruby_statement(scanner, new_code, leading_spaces_counts, leading_spaces_count)
26
+ elsif scanner.scan(/==?/) # it matches ruby expression " = current_user.login"
27
+ new_code << (WHITESPACE * scanner.matched.size)
28
+ scan_ruby_expression(scanner, new_code, leading_spaces_counts, leading_spaces_count)
29
+ elsif scanner.scan(/[a-z#\.][a-zA-Z0-9\-_%#\.]*/) # it matches element, id and class " span.user"
30
+ new_code << (WHITESPACE * scanner.matched.size)
31
+ ATTRIBUTES_PAIR.each do |start, ending| # it matches attributes in brackets " span[ class='user' ]"
32
+ scan_matching_wrapper(scanner, new_code, start, ending)
33
+ end
34
+ scan_attributes_between_whitespace(scanner, new_code)
35
+ if scanner.scan(/ ?==?/) # it matches ruby expression " span= current_user.login"
36
+ new_code << (WHITESPACE * scanner.matched.size)
37
+ scan_ruby_expression(scanner, new_code, leading_spaces_counts, leading_spaces_count)
38
+ else
39
+ scan_ruby_interpolation_and_plain_text(scanner, new_code, leading_spaces_counts, leading_spaces_count)
40
+ end
41
+ else
42
+ scan_ruby_interpolation_and_plain_text(scanner, new_code, leading_spaces_counts, leading_spaces_count)
43
+ end
44
+
45
+ break if scanner.eos?
46
+ end
47
+
48
+ while leading_spaces_counts.pop
49
+ new_code << END_LINE
50
+ end
51
+ new_code.join
52
+ end
53
+
54
+ private
55
+
56
+ def scan_matching_wrapper(scanner, new_code, start, ending)
57
+ if scanner.scan(start) # it matches attributes " %span[ class='user' ]"
58
+ new_code << WHITESPACE
59
+ count = 1
60
+ while scanner.scan(/.*?[#{Regexp.quote(start)}#{Regexp.quote(ending)}]/m)
61
+ matched = scanner.matched.gsub(/(\A| ).*?=/) { |key| WHITESPACE * key.size }
62
+ if scanner.matched[-1] == ending
63
+ new_code << (matched[0..-2] + ';')
64
+ count -= 1
65
+ break if count == 0
66
+ else
67
+ new_code << matched
68
+ count += 1
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def scan_attributes_between_whitespace(scanner, new_code)
75
+ while scanner.scan(/ ?[\w\-_]+==?/) # it matches attributes split by space " span class='user'"
76
+ new_code << (WHITESPACE * scanner.matched.size)
77
+ stack = []
78
+ while scanner.scan(/(.*?['"\(\)\[\]\{\}]|.+?\b)/)
79
+ matched = scanner.matched
80
+ new_code << matched
81
+ if ['(', '[', '{'].include?(matched[-1])
82
+ stack << matched[-1]
83
+ elsif [')', ']', '}'].include?(matched[-1])
84
+ stack.pop
85
+ elsif %w[' "].include?(matched[-1])
86
+ stack.last == matched[-1] ? stack.pop : stack << matched[-1]
87
+ end
88
+ break if stack.empty?
89
+ end
90
+ if scanner.scan(' ')
91
+ new_code << ';'
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -3,8 +3,10 @@
3
3
  module Synvert::Core
4
4
  # Engine defines how to encode / decode other files (like erb).
5
5
  module Engine
6
+ autoload :Elegant, 'synvert/core/engine/elegant'
6
7
  autoload :Erb, 'synvert/core/engine/erb'
7
8
  autoload :Haml, 'synvert/core/engine/haml'
9
+ autoload :Slim, 'synvert/core/engine/slim'
8
10
 
9
11
  # Register an engine
10
12
  # @param [String] extension
@@ -36,4 +38,5 @@ module Synvert::Core
36
38
 
37
39
  Engine.register('.erb', Engine::Erb)
38
40
  Engine.register('.haml', Engine::Haml)
41
+ Engine.register('.slim', Engine::Slim)
39
42
  end
@@ -362,7 +362,7 @@ module Synvert::Core
362
362
  @current_mutation.delete(@current_node, *selectors, and_comma: and_comma)
363
363
  end
364
364
 
365
- # It wraps current node with code.
365
+ # It wraps current node with prefix and suffix code.
366
366
  # @example
367
367
  # # class Foobar
368
368
  # # end
@@ -372,11 +372,13 @@ module Synvert::Core
372
372
  # # end
373
373
  # # end
374
374
  # within_node type: 'class' do
375
- # wrap with: 'module Synvert'
375
+ # wrap with: 'module Synvert', and: 'end', newline: true
376
376
  # end
377
- # @param with [String] code need to be wrapped with.
378
- def wrap(with:)
379
- @current_mutation.wrap(@current_node, with: with)
377
+ # @param prefix [String] prefix code need to be wrapped with.
378
+ # @param suffix [String] suffix code need to be wrapped with.
379
+ # @param newline [Boolean] if wrap code in newline, default is false
380
+ def wrap(prefix:, suffix:, newline: false)
381
+ @current_mutation.wrap(@current_node, prefix: prefix, suffix: suffix, newline: newline)
380
382
  end
381
383
 
382
384
  # No operation.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Synvert
4
4
  module Core
5
- VERSION = '1.23.1'
5
+ VERSION = '1.25.0'
6
6
  end
7
7
  end
data/lib/synvert/core.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'synvert/core/version'
4
4
  require 'bundler'
5
+ require 'strscan'
5
6
  require 'parser'
6
7
  require 'parser/current'
7
8
  require 'parser_node_ext'
@@ -29,8 +30,9 @@ module Synvert
29
30
  ALL_RUBY_FILES = %w[**/*.rb]
30
31
  ALL_ERB_FILES = %w[**/*.erb]
31
32
  ALL_HAML_FILES = %w[**/*.haml]
33
+ ALL_SLIM_FILES = %w[**/*.slim]
32
34
  ALL_RAKE_FILES = %w[**/*.rake]
33
- ALL_FILES = ALL_RUBY_FILES + ALL_ERB_FILES + ALL_HAML_FILES + ALL_RAKE_FILES
35
+ ALL_FILES = ALL_RUBY_FILES + ALL_ERB_FILES + ALL_HAML_FILES + ALL_SLIM_FILES + ALL_RAKE_FILES
34
36
 
35
37
  RAILS_APP_FILES = %w[app/**/*.rb engines/*/app/**/*.rb]
36
38
  RAILS_CONTROLLER_FILES = %w[app/controllers/**/*.rb engines/*/app/controllers/**/*.rb]
@@ -47,7 +49,7 @@ module Synvert
47
49
  engines/*/config/routes.rb
48
50
  engines/*/config/routes/**/*.rb
49
51
  ]
50
- RAILS_VIEW_FILES = ALL_ERB_FILES + ALL_HAML_FILES
52
+ RAILS_VIEW_FILES = ALL_ERB_FILES + ALL_HAML_FILES + ALL_SLIM_FILES
51
53
 
52
54
  RAILS_CONTROLLER_TEST_FILES = %w[
53
55
  test/functional/**/*.rb
data/spec/spec_helper.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
4
 
5
+ require 'pp'
5
6
  require 'synvert/core'
6
7
  require 'fakefs/spec_helpers'
7
8
 
@@ -20,6 +20,8 @@ module Synvert::Core
20
20
  bar = 'foo'
21
21
  %>
22
22
 
23
+ <%= user.first_name %> <%= user.last_name %>
24
+
23
25
  <% if User.current &&
24
26
  User.current.admin %>
25
27
  <%= rounded_content("page") do %>
@@ -40,6 +42,8 @@ module Synvert::Core
40
42
  expect(encoded_source).to be_include 'post = Post.find(:first)'
41
43
  expect(encoded_source).to be_include "link_to_function 'test', \"confirm('test');\""
42
44
  expect(encoded_source).to be_include 'end'
45
+ expect(encoded_source).to be_include 'user.first_name ;'
46
+ expect(encoded_source).to be_include 'user.last_name ;'
43
47
  expect(encoded_source).not_to be_include 'style'
44
48
  expect(encoded_source).not_to be_include 'div'
45
49
  end
@@ -17,11 +17,11 @@ module Synvert::Core
17
17
  = "Hello!"
18
18
  - if current_admin?
19
19
  %strong= "Admin"
20
+ #error= error_message
21
+ .form-actions
22
+ = form_for @user do |f|
20
23
  - elsif current_user?
21
24
  %span= "User"
22
- #error= error_message
23
- .form-actions
24
- = form_for @user do |f|
25
25
  Test
26
26
  EOS
27
27
  encoded_source = Engine::Haml.encode(source)
@@ -43,6 +43,164 @@ module Synvert::Core
43
43
  expect(encoded_source).not_to be_include 'Test'
44
44
  expect(encoded_source).not_to be_include '.form-actions'
45
45
  end
46
+
47
+ it 'encodes plain text' do
48
+ source = <<~EOS
49
+ %gee
50
+ %whiz
51
+ Wow this is cool!
52
+ EOS
53
+ encoded_source = Engine::Haml.encode(source)
54
+ expect(encoded_source).not_to be_include '%gee'
55
+ expect(encoded_source).not_to be_include '%whiz'
56
+ expect(encoded_source).not_to be_include 'Wow this is cool!'
57
+ end
58
+
59
+ it 'encodes escaping' do
60
+ source = <<~EOS
61
+ %title
62
+ = @title
63
+ \\= @title
64
+ EOS
65
+ encoded_source = Engine::Haml.encode(source)
66
+ expect(encoded_source.scan('@title').length).to eq 1
67
+ end
68
+
69
+ it 'encodes attributes' do
70
+ source = <<~EOS
71
+ %html{:xmlns => "http://www.w3.org/1999/xhtml", "xml:lang" => "en", :lang => "en"}
72
+ EOS
73
+ encoded_source = Engine::Haml.encode(source)
74
+ expect(encoded_source).to be_include '{:xmlns => "http://www.w3.org/1999/xhtml", "xml:lang" => "en", :lang => "en"}'
75
+ end
76
+
77
+ it 'encodes multiple lines attributes' do
78
+ source = <<~EOS
79
+ %script{
80
+ "type": "text/javascript",
81
+ "src": "javascripts/script_9",
82
+ "data": {
83
+ "controller": "reporter",
84
+ },
85
+ }
86
+ EOS
87
+ encoded_source = Engine::Haml.encode(source)
88
+ expect(encoded_source).to be_include '"type": "text/javascript",'
89
+ expect(encoded_source).to be_include '"src": "javascripts/script_9",'
90
+ expect(encoded_source).to be_include '"controller": "reporter",'
91
+ end
92
+
93
+ it 'encodes prefixed attributes' do
94
+ source = <<~EOS
95
+ %a{:href=>"/posts", :data => {:author_id => 123, :category => 7}} Posts By Author
96
+ EOS
97
+ encoded_source = Engine::Haml.encode(source)
98
+ expect(encoded_source).to be_include '{:href=>"/posts", :data => {:author_id => 123, :category => 7}}'
99
+ end
100
+
101
+ it 'encodes class and ID' do
102
+ source = <<~EOS
103
+ %div#things
104
+ %span#rice Chicken Fried
105
+ %p.beans{ :food => 'true' } The magical fruit
106
+ %h1.class.otherclass#id La La La
107
+ EOS
108
+ encoded_source = Engine::Haml.encode(source)
109
+ expect(encoded_source).not_to be_include '%div'
110
+ expect(encoded_source).not_to be_include '#things'
111
+ expect(encoded_source).not_to be_include '.beans'
112
+ expect(encoded_source).not_to be_include '.class'
113
+ expect(encoded_source).not_to be_include '#id'
114
+ expect(encoded_source).to be_include "{ :food => 'true' }"
115
+ end
116
+
117
+ it 'encodes haml comments' do
118
+ source = <<~EOS
119
+ %p foo
120
+ -# This is a comment
121
+ %p bar
122
+ EOS
123
+ encoded_source = Engine::Haml.encode(source)
124
+ expect(encoded_source).not_to be_include 'foo'
125
+ expect(encoded_source).not_to be_include 'bar'
126
+ expect(encoded_source).to be_include '# This is a comment'
127
+ end
128
+
129
+ it 'encodes ruby evaluation' do
130
+ source = <<~EOS
131
+ %p
132
+ = ['hi', 'there', 'reader!'].join " "
133
+ = "yo"
134
+ EOS
135
+ encoded_source = Engine::Haml.encode(source)
136
+ expect(encoded_source).to be_include "['hi', 'there', 'reader!'].join \" \""
137
+ expect(encoded_source).to be_include '"yo"'
138
+ end
139
+
140
+ it 'encodes ruby evaluation in the same line' do
141
+ source = <<~EOS
142
+ %p= "hello"
143
+ EOS
144
+ encoded_source = Engine::Haml.encode(source)
145
+ expect(encoded_source).not_to be_include '%p'
146
+ expect(encoded_source).to be_include '"hello"'
147
+ end
148
+
149
+ it 'encodes multiple lines ruby evaluation' do
150
+ source = <<~EOS
151
+ = link_to_remote "Add to cart",
152
+ :url => { :action => "add", :id => product.id },
153
+ :update => { :success => "cart", :failure => "error" }
154
+ EOS
155
+ encoded_source = Engine::Haml.encode(source)
156
+ expect(encoded_source).to be_include 'link_to_remote "Add to cart",'
157
+ expect(encoded_source).to be_include ':url => { :action => "add", :id => product.id },'
158
+ expect(encoded_source).to be_include ':update => { :success => "cart", :failure => "error" }'
159
+ end
160
+
161
+ it 'encodes running ruby' do
162
+ source = <<~EOS
163
+ - foo = "hello"
164
+ - foo << " there"
165
+ - foo << " you!"
166
+ EOS
167
+ encoded_source = Engine::Haml.encode(source)
168
+ expect(encoded_source).to be_include 'foo = "hello"'
169
+ expect(encoded_source).to be_include 'foo << " there"'
170
+ expect(encoded_source).to be_include 'foo << " you!"'
171
+ end
172
+
173
+ it 'encodes multiple line running ruby' do
174
+ source = <<~EOS
175
+ - links = {:home => "/",
176
+ :docs => "/docs",
177
+ :about => "/about"}
178
+ EOS
179
+ encoded_source = Engine::Haml.encode(source)
180
+ expect(encoded_source).to be_include 'links = {:home => "/",'
181
+ expect(encoded_source).to be_include ':docs => "/docs",'
182
+ expect(encoded_source).to be_include ':about => "/about"}'
183
+ end
184
+
185
+ it 'encodes ruby block' do
186
+ source = <<~EOS
187
+ - (42...47).each do |i|
188
+ %p= i
189
+ %p See, I can count!
190
+ EOS
191
+ encoded_source = Engine::Haml.encode(source)
192
+ expect(encoded_source).to be_include '(42...47).each do |i|'
193
+ expect(encoded_source).to be_include 'end'
194
+ expect(encoded_source).not_to be_include 'See, I can count!'
195
+ end
196
+
197
+ it 'encodes ruby interpolation' do
198
+ source = 'Look at #{h word} lack of backslash: \#{foo}'
199
+ encoded_source = Engine::Haml.encode(source)
200
+ expect(encoded_source).to be_include 'h word;'
201
+ expect(encoded_source).not_to be_include 'foo'
202
+ expect(encoded_source).not_to be_include 'lack of backslash:'
203
+ end
46
204
  end
47
205
 
48
206
  describe '#generate_transform_proc' do
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ module Synvert::Core
6
+ describe Engine::Slim do
7
+ describe '#encode' do
8
+ it 'encodes source' do
9
+ source = <<~EOS
10
+ doctype html
11
+ html
12
+ head
13
+ title Slim Examples
14
+ meta name="keywords" content="template language"
15
+ meta name="author" content=author
16
+ javascript:
17
+ alert('Slim supports embedded javascript!')
18
+
19
+ body
20
+ h1 Markup examples
21
+
22
+ #content
23
+ p This example shows you what a basic Slim file looks like.
24
+
25
+ == yield
26
+
27
+ - unless items.empty?
28
+ table
29
+ - items.each do |item|
30
+ tr
31
+ td.name = item.name
32
+ td.price = item.price
33
+ - else
34
+ p
35
+ | No items found. Please add some inventory.
36
+ Thank you!
37
+
38
+ div id="footer"
39
+ = render 'footer'
40
+ | Copyright © \#{year} \#{author}
41
+ EOS
42
+ encoded_source = Engine::Slim.encode(source)
43
+ expect(encoded_source).to be_include 'yield'
44
+ expect(encoded_source).to be_include 'unless items.empty?'
45
+ expect(encoded_source).to be_include 'items.each do |item|'
46
+ expect(encoded_source).to be_include 'item.name'
47
+ expect(encoded_source).to be_include 'item.price'
48
+ expect(encoded_source).to be_include "render 'footer'"
49
+ expect(encoded_source).to be_include 'year'
50
+ expect(encoded_source).to be_include 'author'
51
+ expect(encoded_source.scan("end\n").length).to eq 2
52
+ expect(encoded_source).not_to be_include 'html'
53
+ expect(encoded_source).not_to be_include 'meta'
54
+ expect(encoded_source).not_to be_include 'javascript:'
55
+ expect(encoded_source).not_to be_include 'Markup examples'
56
+ expect(encoded_source).not_to be_include 'td.name'
57
+ expect(encoded_source).not_to be_include 'td.price'
58
+ expect(encoded_source).not_to be_include 'Copyright'
59
+ end
60
+
61
+ it 'encodes control code' do
62
+ source = <<~EOS
63
+ body
64
+ - if articles.empty?
65
+ | No inventory
66
+ EOS
67
+ encoded_source = Engine::Slim.encode(source)
68
+ expect(encoded_source).to be_include 'if articles.empty?'
69
+ expect(encoded_source).to be_include 'end'
70
+ expect(encoded_source).not_to be_include 'No inventory'
71
+ end
72
+
73
+ it 'encodes output' do
74
+ source = <<~EOS
75
+ = javascript_include_tag \
76
+ "jquery",
77
+ "application"
78
+ EOS
79
+ encoded_source = Engine::Slim.encode(source)
80
+ expect(encoded_source).to be_include 'javascript_include_tag'
81
+ expect(encoded_source).to be_include '"jquery",'
82
+ expect(encoded_source).to be_include '"application"'
83
+ end
84
+
85
+ it 'encodes output without html escaping' do
86
+ source = <<~EOS
87
+ == yield
88
+ EOS
89
+ encoded_source = Engine::Slim.encode(source)
90
+ expect(encoded_source).to be_include 'yield'
91
+ end
92
+
93
+ it 'encodes tags' do
94
+ source = <<~EOS
95
+ ul
96
+ li.first: a href="/a" A link
97
+ li: a href="/b" B link
98
+ EOS
99
+ encoded_source = Engine::Slim.encode(source)
100
+ expect(encoded_source).not_to be_include 'ul'
101
+ expect(encoded_source).not_to be_include 'li'
102
+ expect(encoded_source).not_to be_include 'first'
103
+ expect(encoded_source).not_to be_include 'link'
104
+ end
105
+
106
+ it 'encodes text content' do
107
+ source = <<~EOS
108
+ body
109
+ h1 id="headline" Welcome to my site.
110
+ EOS
111
+ encoded_source = Engine::Slim.encode(source)
112
+ expect(encoded_source).not_to be_include 'h1'
113
+ expect(encoded_source).not_to be_include 'id'
114
+ expect(encoded_source).not_to be_include 'Welcome'
115
+ end
116
+
117
+ it 'encodes dynamic content' do
118
+ source = <<~EOS
119
+ body
120
+ h1 id="headline" = page_headline
121
+ EOS
122
+ encoded_source = Engine::Slim.encode(source)
123
+ expect(encoded_source).to be_include 'page_headline'
124
+ expect(encoded_source).not_to be_include 'h1'
125
+ end
126
+
127
+ it 'encodes attributes wrapper' do
128
+ source = <<~EOS
129
+ body
130
+ h1(id="logo") = page_logo
131
+ h2[id="tagline" class="small tagline"] = page_tagline
132
+ EOS
133
+ encoded_source = Engine::Slim.encode(source)
134
+ expect(encoded_source).to be_include 'page_logo'
135
+ expect(encoded_source).to be_include 'page_tagline'
136
+ expect(encoded_source).not_to be_include 'h1'
137
+ expect(encoded_source).not_to be_include 'h2'
138
+ expect(encoded_source).not_to be_include 'id'
139
+ expect(encoded_source).not_to be_include 'class'
140
+ end
141
+
142
+ it 'encodes multiple lines attributes wrapper' do
143
+ source = <<~EOS
144
+ h2[id="tagline"
145
+ class="small tagline"] = page_tagline
146
+ EOS
147
+ encoded_source = Engine::Slim.encode(source)
148
+ expect(encoded_source).to be_include 'page_tagline'
149
+ expect(encoded_source).not_to be_include 'h2'
150
+ expect(encoded_source).not_to be_include 'id'
151
+ expect(encoded_source).not_to be_include 'class'
152
+ end
153
+
154
+ it 'encodes ruby attributes' do
155
+ source = <<~EOS
156
+ body
157
+ table
158
+ - for user in users
159
+ td id="user_\#{user.id}" class=user.role
160
+ a href=user_action(user, :edit) Edit \#{user.first_name}
161
+ a href=(path_to_user user) = user.last_name
162
+ EOS
163
+ encoded_source = Engine::Slim.encode(source)
164
+ expect(encoded_source).to be_include 'for user in users'
165
+ expect(encoded_source).to be_include 'user.id'
166
+ expect(encoded_source).to be_include 'user_action(user, :edit)'
167
+ expect(encoded_source).to be_include 'user.first_name'
168
+ expect(encoded_source).to be_include '(path_to_user user)'
169
+ expect(encoded_source).to be_include 'user.last_name'
170
+ expect(encoded_source).to be_include 'end'
171
+ expect(encoded_source).not_to be_include 'href'
172
+ end
173
+
174
+ it 'encodes ruby attributes with ==' do
175
+ source = 'a href==action_path(:start)'
176
+ encoded_source = Engine::Slim.encode(source)
177
+ expect(encoded_source).to be_include 'action_path(:start)'
178
+ expect(encoded_source).not_to be_include 'href'
179
+ expect(encoded_source).not_to be_include '='
180
+ end
181
+
182
+ it 'encodes text interpolation' do
183
+ source = <<~EOS
184
+ body
185
+ h1 Welcome \#{current_user.name} to the show.
186
+ EOS
187
+ encoded_source = Engine::Slim.encode(source)
188
+ expect(encoded_source).to be_include 'current_user.name;'
189
+ expect(encoded_source).not_to be_include 'Welcome'
190
+ end
191
+ end
192
+
193
+ describe '#generate_transform_proc' do
194
+ it 'generates transform proc' do
195
+ encoded_source = <<~EOS
196
+ now = DateTime.now
197
+ if now > DateTime.parse("December 31, 2006")
198
+ else
199
+ end
200
+ if current_admin?
201
+ elsif current_user?
202
+ end
203
+ DateTime.now - now
204
+ EOS
205
+ proc = Engine::Slim.generate_transform_proc(encoded_source)
206
+ actions = [
207
+ NodeMutation::Struct::Action.new(50, 55, ''),
208
+ # first end position is 69
209
+ NodeMutation::Struct::Action.new(100, 105, ''),
210
+ # second end position is 111
211
+ NodeMutation::Struct::Action.new(120, 125, '')
212
+ ]
213
+ proc.call(actions)
214
+ expect(actions.first.start).to eq 50
215
+ expect(actions.first.end).to eq 55
216
+ expect(actions.second.start).to eq 96
217
+ expect(actions.second.end).to eq 101
218
+ expect(actions.third.start).to eq 112
219
+ expect(actions.third.end).to eq 117
220
+ end
221
+ end
222
+ end
223
+ end
@@ -233,14 +233,16 @@ module Synvert::Core
233
233
  instance.delete :dot, :message, and_comma: true
234
234
  end
235
235
 
236
- it 'parses wrap with' do
236
+ it 'parses wrap' do
237
237
  instance.instance_variable_set(:@current_mutation, double)
238
238
  instance.current_node = double
239
239
  expect(instance.instance_variable_get(:@current_mutation)).to receive(:wrap).with(
240
240
  instance.current_node,
241
- with: 'module Foobar'
241
+ prefix: 'module Foobar',
242
+ suffix: 'end',
243
+ newline: true
242
244
  )
243
- instance.wrap with: 'module Foobar'
245
+ instance.wrap prefix: 'module Foobar', suffix: 'end', newline: true
244
246
  end
245
247
 
246
248
  it 'parses noop' do
@@ -385,6 +387,26 @@ module Synvert::Core
385
387
  expect(File).to receive(:write).with('./app/views/posts/_form.html.haml', output)
386
388
  instance.process
387
389
  end
390
+
391
+ it 'updates slim file' do
392
+ instance =
393
+ Rewriter::Instance.new rewriter, 'app/views/posts/_form.html.slim' do
394
+ with_node node_type: 'ivar' do
395
+ replace_with 'post'
396
+ end
397
+ end
398
+ input = <<~EOS
399
+ = form_for @post do |f|
400
+ = form_for @post do |f|
401
+ EOS
402
+ output = <<~EOS
403
+ = form_for post do |f|
404
+ = form_for post do |f|
405
+ EOS
406
+ allow(File).to receive(:read).with('./app/views/posts/_form.html.slim', encoding: 'UTF-8').and_return(input)
407
+ expect(File).to receive(:write).with('./app/views/posts/_form.html.slim', output)
408
+ instance.process
409
+ end
388
410
  end
389
411
 
390
412
  describe '#test' do
@@ -477,6 +499,34 @@ module Synvert::Core
477
499
  ),
478
500
  ]
479
501
  end
502
+
503
+ it 'updates slim file' do
504
+ instance =
505
+ Rewriter::Instance.new rewriter, 'app/views/posts/_form.html.slim' do
506
+ with_node node_type: 'ivar' do
507
+ replace_with 'post'
508
+ end
509
+ end
510
+ input = <<~EOS
511
+ = form_for @post do |f|
512
+ = form_for @post do |f|
513
+ EOS
514
+ output = <<~EOS
515
+ = form_for post do |f|
516
+ = form_for post do |f|
517
+ EOS
518
+ allow(File).to receive(:read).with('./app/views/posts/_form.html.slim', encoding: 'UTF-8').and_return(input)
519
+ result = instance.test
520
+ expect(result.file_path).to eq 'app/views/posts/_form.html.slim'
521
+ expect(result.actions).to eq [
522
+ NodeMutation::Struct::Action.new("= form_for ".length, "= form_for @post".length, 'post'),
523
+ NodeMutation::Struct::Action.new(
524
+ "= form_for @post do |f|\n= form_for ".length,
525
+ "= form_for @post do |f|\n= form_for @post".length,
526
+ 'post'
527
+ ),
528
+ ]
529
+ end
480
530
  end
481
531
 
482
532
  describe '#process_with_node' do
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.add_runtime_dependency "activesupport", "< 7.0.0"
23
23
  spec.add_runtime_dependency "node_query", ">= 1.12.1"
24
- spec.add_runtime_dependency "node_mutation", ">= 1.14.0"
24
+ spec.add_runtime_dependency "node_mutation", ">= 1.15.1"
25
25
  spec.add_runtime_dependency "parser"
26
26
  spec.add_runtime_dependency "parser_node_ext", ">= 1.0.0"
27
27
  spec.add_runtime_dependency "parallel"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: synvert-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.23.1
4
+ version: 1.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Huang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-06 00:00:00.000000000 Z
11
+ date: 2023-04-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 1.14.0
47
+ version: 1.15.1
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 1.14.0
54
+ version: 1.15.1
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: parser
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -115,8 +115,10 @@ files:
115
115
  - lib/synvert/core.rb
116
116
  - lib/synvert/core/configuration.rb
117
117
  - lib/synvert/core/engine.rb
118
+ - lib/synvert/core/engine/elegant.rb
118
119
  - lib/synvert/core/engine/erb.rb
119
120
  - lib/synvert/core/engine/haml.rb
121
+ - lib/synvert/core/engine/slim.rb
120
122
  - lib/synvert/core/node_ext.rb
121
123
  - lib/synvert/core/rewriter.rb
122
124
  - lib/synvert/core/rewriter/action/replace_erb_stmt_with_expr_action.rb
@@ -140,6 +142,7 @@ files:
140
142
  - spec/support/parser_helper.rb
141
143
  - spec/synvert/core/engine/erb_spec.rb
142
144
  - spec/synvert/core/engine/haml_spec.rb
145
+ - spec/synvert/core/engine/slim_spec.rb
143
146
  - spec/synvert/core/node_ext_spec.rb
144
147
  - spec/synvert/core/rewriter/action/replace_erb_stmt_with_expr_action_spec.rb
145
148
  - spec/synvert/core/rewriter/condition/if_exist_condition_spec.rb
@@ -185,6 +188,7 @@ test_files:
185
188
  - spec/support/parser_helper.rb
186
189
  - spec/synvert/core/engine/erb_spec.rb
187
190
  - spec/synvert/core/engine/haml_spec.rb
191
+ - spec/synvert/core/engine/slim_spec.rb
188
192
  - spec/synvert/core/node_ext_spec.rb
189
193
  - spec/synvert/core/rewriter/action/replace_erb_stmt_with_expr_action_spec.rb
190
194
  - spec/synvert/core/rewriter/condition/if_exist_condition_spec.rb