synvert-core 1.23.0 → 1.24.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6590e4796366364169c7e2cc65779406ce8a8f88221ffcd7f0ad05df4487f78b
4
- data.tar.gz: b1792cc8d6744fbef809d43ba428f0d5592702c6abee4653fe8160a87261acd4
3
+ metadata.gz: b80b28538674cde89c7dc0098b53d736abb355874d4736654b48234c38c76e2b
4
+ data.tar.gz: 463e9b2616f07146ce551e904912dc2cbfb71e010ffa11eda8ff134c0ea1a5b3
5
5
  SHA512:
6
- metadata.gz: ec759f01184a5730b99742f28310b72df7e5c844fcafa535238dcfacaca8582e9e13b695219ec76f3b8a45194d15aec2caed1e714848cad33263de53a6a56be2
7
- data.tar.gz: eef33bbc2623a9ff16e908722ef358cbec51188911e07e1c9c8e44bcb4029c4e73cc1c0da54581d557c3d1edde0f8ed224fff75dffca5b2fcd347e1eed3f83f8
6
+ metadata.gz: 2f25773c6887fe6b676dc1abd92df89b82dddab81e26841d3e1b566b1ab4f84666f7fde4563e864508814b49f7a54313014a88e412946b908e58976641a1223f
7
+ data.tar.gz: 13cadd41c39b742837a0b562626b1de9355e63f52f953133139f60c47f6341001f2442bca9276078bfdadea909ef5068db688444ed8e55ceaf9fc2511e7321d4
data/CHANGELOG.md CHANGED
@@ -1,8 +1,19 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.24.0 (2023-04-16)
4
+
5
+ * Add `Slim` engine
6
+ * Abstract `Engine/Elegant` module
7
+ * Rewrite `Haml` and `Slim` engine by StringScanner
8
+
9
+ ## 1.23.1 (2023-04-06)
10
+
11
+ * Fix array index name in `GotoScope`
12
+ * Update `node_query` to 1.12.1
13
+
3
14
  ## 1.23.0 (2023-04-05)
4
15
 
5
- * Add haml engine
16
+ * Add `Haml` engine
6
17
  * Add `Engine.register` and `Engine.encode` and `Engine.generate_transform_proc`
7
18
  * Set NodeMutation `transform_proc`
8
19
  * Update `node_mutation` to 1.14.0
data/Gemfile.lock CHANGED
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- synvert-core (1.23.0)
4
+ synvert-core (1.24.0)
5
5
  activesupport (< 7.0.0)
6
6
  node_mutation (>= 1.14.0)
7
- node_query (>= 1.12.0)
7
+ node_query (>= 1.12.1)
8
8
  parallel
9
9
  parser
10
10
  parser_node_ext (>= 1.0.0)
@@ -49,7 +49,7 @@ GEM
49
49
  minitest (5.18.0)
50
50
  nenv (0.3.0)
51
51
  node_mutation (1.14.0)
52
- node_query (1.12.0)
52
+ node_query (1.12.1)
53
53
  notiffany (0.1.3)
54
54
  nenv (~> 0.1)
55
55
  shellany (~> 0.0)
@@ -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
@@ -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,102 @@
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
+ '[' => ']',
12
+ '(' => ')'
13
+ }
14
+
15
+ # Encode haml string, leave only ruby code, replace other haml code with whitespace.
16
+ # And insert `end\n` for each if, unless, begin, case to make it a valid ruby code.
17
+ #
18
+ # @param source [String] haml code.
19
+ # @return [String] encoded ruby code.
20
+ def encode(source)
21
+ leading_spaces_counts = []
22
+ new_code = []
23
+ scanner = StringScanner.new(source)
24
+ loop do
25
+ new_code << scanner.scan(/\s*/)
26
+ leading_spaces_count = scanner.matched.size
27
+ if scanner.scan('-') # it matches ruby statement " - current_user"
28
+ new_code << WHITESPACE
29
+ scan_ruby_statement(scanner, new_code, leading_spaces_counts, leading_spaces_count)
30
+ elsif scanner.scan(/==?/) # it matches ruby expression " = current_user.login"
31
+ new_code << WHITESPACE * scanner.matched.size
32
+ scan_ruby_expression(scanner, new_code, leading_spaces_counts, leading_spaces_count)
33
+ elsif scanner.scan(/[a-z#\.][a-zA-Z0-9\-_%#\.]*/) # it matches element, id and class " span.user"
34
+ new_code << WHITESPACE * scanner.matched.size
35
+ ATTRIBUTES_PAIR.each do |start, ending| # it matches attributes in brackets " span[ class='user' ]"
36
+ scan_matching_wrapper(scanner, new_code, start, ending)
37
+ end
38
+ scan_attributes_between_whitespace(scanner, new_code)
39
+ if scanner.scan(/ ?==?/) # it matches ruby expression " span= current_user.login"
40
+ new_code << WHITESPACE * scanner.matched.size
41
+ scan_ruby_expression(scanner, new_code, leading_spaces_counts, leading_spaces_count)
42
+ else
43
+ scan_ruby_interpolation_and_plain_text(scanner, new_code, leading_spaces_counts, leading_spaces_count)
44
+ end
45
+ else
46
+ scan_ruby_interpolation_and_plain_text(scanner, new_code, leading_spaces_counts, leading_spaces_count)
47
+ end
48
+
49
+ break if scanner.eos?
50
+ end
51
+
52
+ while leading_spaces_counts.pop
53
+ new_code << END_LINE
54
+ end
55
+ new_code.join
56
+ end
57
+
58
+ private
59
+
60
+ def scan_matching_wrapper(scanner, new_code, start, ending)
61
+ if scanner.scan(start) # it matches attributes " %span[ class='user' ]"
62
+ new_code << WHITESPACE
63
+ count = 1
64
+ while scanner.scan(/.*?[#{Regexp.quote(start)}#{Regexp.quote(ending)}]/m)
65
+ matched = scanner.matched.gsub(/(\A| ).*?=/) { |key| WHITESPACE * key.size }
66
+ if scanner.matched[-1] == ending
67
+ new_code << matched[0..-2] + ';'
68
+ count -= 1
69
+ break if count == 0
70
+ else
71
+ new_code << matched
72
+ count += 1
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def scan_attributes_between_whitespace(scanner, new_code)
79
+ while scanner.scan(/ ?[\w\-_]+==?/) # it matches attributes split by space " span class='user'"
80
+ new_code << WHITESPACE * scanner.matched.size
81
+ stack = []
82
+ while scanner.scan(/(.*?['"\(\)\[\]\{\}]|.+?\b)/)
83
+ matched = scanner.matched
84
+ new_code << matched
85
+ if ['(', '[', '{'].include?(matched[-1])
86
+ stack << matched[-1]
87
+ elsif [')', ']', '}'].include?(matched[-1])
88
+ stack.pop
89
+ elsif %w[' "].include?(matched[-1])
90
+ stack.last == matched[-1] ? stack.pop : stack << matched[-1]
91
+ end
92
+ break if stack.empty?
93
+ end
94
+ if scanner.scan(' ')
95
+ new_code << ';'
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ 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
@@ -20,7 +20,7 @@ module Synvert::Core
20
20
 
21
21
  child_node = current_node
22
22
  @child_node_name.to_s.split('.').each do |child_node_name|
23
- child_node = child_node_name.is_a?(Parser::AST::Node) ? child_node_name : child_node.send(child_node_name)
23
+ child_node = child_node.is_a?(Array) && child_node_name =~ /-?\d+/ ? child_node[child_node_name.to_i] : child_node.send(child_node_name)
24
24
  end
25
25
  if child_node.is_a?(Array)
26
26
  child_node.each do |child_child_node|
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Synvert
4
4
  module Core
5
- VERSION = '1.23.0'
5
+ VERSION = '1.24.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
 
@@ -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
@@ -385,6 +385,26 @@ module Synvert::Core
385
385
  expect(File).to receive(:write).with('./app/views/posts/_form.html.haml', output)
386
386
  instance.process
387
387
  end
388
+
389
+ it 'updates slim file' do
390
+ instance =
391
+ Rewriter::Instance.new rewriter, 'app/views/posts/_form.html.slim' do
392
+ with_node node_type: 'ivar' do
393
+ replace_with 'post'
394
+ end
395
+ end
396
+ input = <<~EOS
397
+ = form_for @post do |f|
398
+ = form_for @post do |f|
399
+ EOS
400
+ output = <<~EOS
401
+ = form_for post do |f|
402
+ = form_for post do |f|
403
+ EOS
404
+ allow(File).to receive(:read).with('./app/views/posts/_form.html.slim', encoding: 'UTF-8').and_return(input)
405
+ expect(File).to receive(:write).with('./app/views/posts/_form.html.slim', output)
406
+ instance.process
407
+ end
388
408
  end
389
409
 
390
410
  describe '#test' do
@@ -477,6 +497,34 @@ module Synvert::Core
477
497
  ),
478
498
  ]
479
499
  end
500
+
501
+ it 'updates slim file' do
502
+ instance =
503
+ Rewriter::Instance.new rewriter, 'app/views/posts/_form.html.slim' do
504
+ with_node node_type: 'ivar' do
505
+ replace_with 'post'
506
+ end
507
+ end
508
+ input = <<~EOS
509
+ = form_for @post do |f|
510
+ = form_for @post do |f|
511
+ EOS
512
+ output = <<~EOS
513
+ = form_for post do |f|
514
+ = form_for post do |f|
515
+ EOS
516
+ allow(File).to receive(:read).with('./app/views/posts/_form.html.slim', encoding: 'UTF-8').and_return(input)
517
+ result = instance.test
518
+ expect(result.file_path).to eq 'app/views/posts/_form.html.slim'
519
+ expect(result.actions).to eq [
520
+ NodeMutation::Struct::Action.new("= form_for ".length, "= form_for @post".length, 'post'),
521
+ NodeMutation::Struct::Action.new(
522
+ "= form_for @post do |f|\n= form_for ".length,
523
+ "= form_for @post do |f|\n= form_for @post".length,
524
+ 'post'
525
+ ),
526
+ ]
527
+ end
480
528
  end
481
529
 
482
530
  describe '#process_with_node' do
@@ -33,7 +33,21 @@ module Synvert::Core
33
33
  expect(instance.current_node.type).to eq :block
34
34
  end
35
35
 
36
- it 'calls block multiple times with blok body' do
36
+ it 'call block with child node in array' do
37
+ run = false
38
+ type_in_scope = nil
39
+ scope =
40
+ Rewriter::GotoScope.new instance, 'body.1' do
41
+ run = true
42
+ type_in_scope = node.type
43
+ end
44
+ scope.process
45
+ expect(run).to be_truthy
46
+ expect(type_in_scope).to eq :send
47
+ expect(instance.current_node.type).to eq :block
48
+ end
49
+
50
+ it 'calls block multiple times with block body' do
37
51
  count = 0
38
52
  scope =
39
53
  Rewriter::GotoScope.new instance, 'body' do
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.require_paths = ["lib"]
21
21
 
22
22
  spec.add_runtime_dependency "activesupport", "< 7.0.0"
23
- spec.add_runtime_dependency "node_query", ">= 1.12.0"
23
+ spec.add_runtime_dependency "node_query", ">= 1.12.1"
24
24
  spec.add_runtime_dependency "node_mutation", ">= 1.14.0"
25
25
  spec.add_runtime_dependency "parser"
26
26
  spec.add_runtime_dependency "parser_node_ext", ">= 1.0.0"
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.0
4
+ version: 1.24.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-05 00:00:00.000000000 Z
11
+ date: 2023-04-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 1.12.0
33
+ version: 1.12.1
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 1.12.0
40
+ version: 1.12.1
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: node_mutation
43
43
  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
@@ -176,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
176
179
  - !ruby/object:Gem::Version
177
180
  version: '0'
178
181
  requirements: []
179
- rubygems_version: 3.4.6
182
+ rubygems_version: 3.4.10
180
183
  signing_key:
181
184
  specification_version: 4
182
185
  summary: convert ruby code to better syntax.
@@ -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