synvert-core 1.23.0 → 1.24.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: 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