unparser 0.4.9 → 0.5.4

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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -4
  3. data/bin/unparser +1 -1
  4. data/lib/unparser.rb +115 -61
  5. data/lib/unparser/ast.rb +0 -1
  6. data/lib/unparser/ast/local_variable_scope.rb +6 -76
  7. data/lib/unparser/buffer.rb +19 -16
  8. data/lib/unparser/cli.rb +26 -39
  9. data/lib/unparser/color.rb +0 -3
  10. data/lib/unparser/comments.rb +0 -26
  11. data/lib/unparser/constants.rb +4 -53
  12. data/lib/unparser/diff.rb +0 -17
  13. data/lib/unparser/dsl.rb +0 -32
  14. data/lib/unparser/emitter.rb +23 -425
  15. data/lib/unparser/emitter/alias.rb +2 -8
  16. data/lib/unparser/emitter/args.rb +45 -0
  17. data/lib/unparser/emitter/argument.rb +8 -166
  18. data/lib/unparser/emitter/array.rb +27 -0
  19. data/lib/unparser/emitter/array_pattern.rb +29 -0
  20. data/lib/unparser/emitter/assignment.rb +36 -127
  21. data/lib/unparser/emitter/begin.rb +9 -84
  22. data/lib/unparser/emitter/binary.rb +7 -20
  23. data/lib/unparser/emitter/block.rb +57 -41
  24. data/lib/unparser/emitter/case.rb +6 -48
  25. data/lib/unparser/emitter/case_guard.rb +27 -0
  26. data/lib/unparser/emitter/case_match.rb +40 -0
  27. data/lib/unparser/emitter/cbase.rb +1 -3
  28. data/lib/unparser/emitter/class.rb +6 -26
  29. data/lib/unparser/emitter/const_pattern.rb +24 -0
  30. data/lib/unparser/emitter/def.rb +7 -51
  31. data/lib/unparser/emitter/defined.rb +2 -12
  32. data/lib/unparser/emitter/dstr.rb +22 -0
  33. data/lib/unparser/emitter/dsym.rb +41 -0
  34. data/lib/unparser/emitter/flipflop.rb +11 -10
  35. data/lib/unparser/emitter/float.rb +29 -0
  36. data/lib/unparser/emitter/flow_modifier.rb +11 -55
  37. data/lib/unparser/emitter/for.rb +5 -19
  38. data/lib/unparser/emitter/hash.rb +74 -0
  39. data/lib/unparser/emitter/hash_pattern.rb +67 -0
  40. data/lib/unparser/emitter/hookexe.rb +5 -11
  41. data/lib/unparser/emitter/if.rb +15 -71
  42. data/lib/unparser/emitter/in_match.rb +21 -0
  43. data/lib/unparser/emitter/in_pattern.rb +34 -0
  44. data/lib/unparser/emitter/index.rb +21 -88
  45. data/lib/unparser/emitter/kwbegin.rb +31 -0
  46. data/lib/unparser/emitter/lambda.rb +0 -8
  47. data/lib/unparser/emitter/masgn.rb +20 -0
  48. data/lib/unparser/emitter/match.rb +3 -17
  49. data/lib/unparser/emitter/match_alt.rb +23 -0
  50. data/lib/unparser/emitter/match_as.rb +21 -0
  51. data/lib/unparser/emitter/match_rest.rb +26 -0
  52. data/lib/unparser/emitter/match_var.rb +19 -0
  53. data/lib/unparser/emitter/mlhs.rb +40 -0
  54. data/lib/unparser/emitter/module.rb +3 -9
  55. data/lib/unparser/emitter/op_assign.rb +12 -27
  56. data/lib/unparser/emitter/pin.rb +19 -0
  57. data/lib/unparser/emitter/primitive.rb +93 -0
  58. data/lib/unparser/emitter/range.rb +35 -0
  59. data/lib/unparser/emitter/regexp.rb +35 -0
  60. data/lib/unparser/emitter/repetition.rb +17 -57
  61. data/lib/unparser/emitter/rescue.rb +1 -97
  62. data/lib/unparser/emitter/root.rb +17 -1
  63. data/lib/unparser/emitter/send.rb +10 -219
  64. data/lib/unparser/emitter/simple.rb +33 -0
  65. data/lib/unparser/emitter/splat.rb +2 -18
  66. data/lib/unparser/emitter/super.rb +1 -29
  67. data/lib/unparser/emitter/undef.rb +1 -9
  68. data/lib/unparser/emitter/variable.rb +1 -31
  69. data/lib/unparser/emitter/xstr.rb +72 -0
  70. data/lib/unparser/emitter/yield.rb +1 -9
  71. data/lib/unparser/generation.rb +250 -0
  72. data/lib/unparser/node_details.rb +21 -0
  73. data/lib/unparser/node_details/send.rb +62 -0
  74. data/lib/unparser/node_helpers.rb +46 -6
  75. data/lib/unparser/validation.rb +58 -35
  76. data/lib/unparser/writer.rb +15 -0
  77. data/lib/unparser/writer/binary.rb +99 -0
  78. data/lib/unparser/writer/dynamic_string.rb +233 -0
  79. data/lib/unparser/writer/resbody.rb +40 -0
  80. data/lib/unparser/writer/rescue.rb +39 -0
  81. data/lib/unparser/writer/send.rb +124 -0
  82. data/lib/unparser/{emitter → writer}/send/attribute_assignment.rb +11 -26
  83. data/lib/unparser/writer/send/binary.rb +27 -0
  84. data/lib/unparser/writer/send/conditional.rb +25 -0
  85. data/lib/unparser/writer/send/regular.rb +33 -0
  86. data/lib/unparser/{emitter → writer}/send/unary.rb +10 -17
  87. metadata +66 -34
  88. data/lib/unparser/emitter/empty.rb +0 -23
  89. data/lib/unparser/emitter/ensure.rb +0 -37
  90. data/lib/unparser/emitter/literal.rb +0 -10
  91. data/lib/unparser/emitter/literal/array.rb +0 -29
  92. data/lib/unparser/emitter/literal/dynamic.rb +0 -53
  93. data/lib/unparser/emitter/literal/dynamic_body.rb +0 -132
  94. data/lib/unparser/emitter/literal/execute_string.rb +0 -38
  95. data/lib/unparser/emitter/literal/hash.rb +0 -156
  96. data/lib/unparser/emitter/literal/primitive.rb +0 -145
  97. data/lib/unparser/emitter/literal/range.rb +0 -36
  98. data/lib/unparser/emitter/literal/regexp.rb +0 -114
  99. data/lib/unparser/emitter/literal/singleton.rb +0 -26
  100. data/lib/unparser/emitter/meta.rb +0 -16
  101. data/lib/unparser/emitter/redo.rb +0 -25
  102. data/lib/unparser/emitter/resbody.rb +0 -76
  103. data/lib/unparser/emitter/retry.rb +0 -25
  104. data/lib/unparser/emitter/send/binary.rb +0 -57
  105. data/lib/unparser/emitter/send/conditional.rb +0 -40
  106. data/lib/unparser/emitter/send/regular.rb +0 -40
  107. data/lib/unparser/preprocessor.rb +0 -159
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unparser
4
+ module NodeDetails
5
+ include Constants, NodeHelpers
6
+
7
+ def self.included(descendant)
8
+ descendant.class_eval do
9
+ include Adamantium::Flat, Concord.new(:node)
10
+
11
+ extend DSL
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def children
18
+ node.children
19
+ end
20
+ end # NodeDetails
21
+ end # Unparser
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unparser
4
+ module NodeDetails
5
+ class Send
6
+ include NodeDetails
7
+
8
+ ASSIGN_SUFFIX = '='.freeze
9
+ NON_ASSIGN_RANGE = (0..-2).freeze
10
+
11
+ private_constant(*constants(false))
12
+
13
+ children :receiver, :selector
14
+
15
+ public :receiver, :selector
16
+
17
+ def selector_binary_operator?
18
+ BINARY_OPERATORS.include?(selector)
19
+ end
20
+
21
+ def binary_syntax_allowed?
22
+ selector_binary_operator? && arguments.one? && !n_splat?(arguments.first)
23
+ end
24
+
25
+ def selector_unary_operator?
26
+ UNARY_OPERATORS.include?(selector)
27
+ end
28
+
29
+ def assignment_operator?
30
+ assignment? && !selector_binary_operator? && !selector_unary_operator?
31
+ end
32
+
33
+ def arguments?
34
+ arguments.any?
35
+ end
36
+
37
+ def non_assignment_selector
38
+ if assignment?
39
+ string_selector[NON_ASSIGN_RANGE]
40
+ else
41
+ string_selector
42
+ end
43
+ end
44
+
45
+ def assignment?
46
+ string_selector[-1].eql?(ASSIGN_SUFFIX)
47
+ end
48
+ memoize :assignment?
49
+
50
+ def arguments
51
+ children[2..-1]
52
+ end
53
+ memoize :arguments
54
+
55
+ def string_selector
56
+ selector.to_s
57
+ end
58
+ memoize :string_selector
59
+
60
+ end # Send
61
+ end # NodeDetails
62
+ end # Unparser
@@ -11,9 +11,6 @@ module Unparser
11
11
  # @return [Parser::AST::Node]
12
12
  #
13
13
  # @api private
14
- #
15
- # ignore :reek:UncommunicativeMethodName
16
- # ignore :reek:UtilityFunction
17
14
  def s(type, *children)
18
15
  Parser::AST::Node.new(type, children)
19
16
  end
@@ -26,12 +23,55 @@ module Unparser
26
23
  # @param [Array] children
27
24
  #
28
25
  # @api private
29
- #
30
- # ignore :reek:UncommunicativeMethodName
31
- # ignore :reek:UtilityFunction
32
26
  def n(type, children = [])
33
27
  Parser::AST::Node.new(type, children)
34
28
  end
35
29
 
30
+ def n?(type, node)
31
+ node.type.equal?(type)
32
+ end
33
+
34
+ %i[
35
+ arg
36
+ args
37
+ array
38
+ array_pattern
39
+ empty_else
40
+ begin
41
+ block
42
+ cbase
43
+ const
44
+ dstr
45
+ ensure
46
+ hash
47
+ hash_pattern
48
+ if
49
+ in_pattern
50
+ int
51
+ kwsplat
52
+ lambda
53
+ match_rest
54
+ pair
55
+ rescue
56
+ send
57
+ shadowarg
58
+ splat
59
+ str
60
+ sym
61
+ ].each do |type|
62
+ name = "n_#{type}?"
63
+ define_method(name) do |node|
64
+ n?(type, node)
65
+ end
66
+ private(name)
67
+ end
68
+
69
+ def unwrap_single_begin(node)
70
+ if n_begin?(node) && node.children.one?
71
+ node.children.first
72
+ else
73
+ node
74
+ end
75
+ end
36
76
  end # NodeHelpers
37
77
  end # Unparser
@@ -2,8 +2,6 @@
2
2
 
3
3
  module Unparser
4
4
  # Validation of unparser results
5
- #
6
- # ignore :reek:TooManyMethods
7
5
  class Validation
8
6
  include Adamantium::Flat, Anima.new(
9
7
  :generated_node,
@@ -25,7 +23,7 @@ module Unparser
25
23
  original_node,
26
24
  generated_source,
27
25
  generated_node
28
- ].all?(&:right?) && generated_node.from_right.eql?(original_node.from_right)
26
+ ].all?(&:right?) && generated_node.from_right.==(original_node.from_right)
29
27
  end
30
28
 
31
29
  # Return error report
@@ -55,16 +53,14 @@ module Unparser
55
53
  def self.from_string(original_source)
56
54
  original_node = Unparser
57
55
  .parse_either(original_source)
58
- .fmap(&Preprocessor.method(:run))
59
56
 
60
57
  generated_source = original_node
61
58
  .lmap(&method(:const_unit))
62
- .bind(&method(:unparse_either))
59
+ .bind(&Unparser.method(:unparse_either))
63
60
 
64
61
  generated_node = generated_source
65
62
  .lmap(&method(:const_unit))
66
63
  .bind(&Unparser.method(:parse_either))
67
- .fmap(&Preprocessor.method(:run))
68
64
 
69
65
  new(
70
66
  identification: '(string)',
@@ -75,6 +71,27 @@ module Unparser
75
71
  )
76
72
  end
77
73
 
74
+ # Create validator from node
75
+ #
76
+ # @param [Parser::AST::Node] original_node
77
+ #
78
+ # @return [Validator]
79
+ def self.from_node(original_node)
80
+ generated_source = Unparser.unparse_either(original_node)
81
+
82
+ generated_node = generated_source
83
+ .lmap(&method(:const_unit))
84
+ .bind(&Unparser.public_method(:parse_either))
85
+
86
+ new(
87
+ identification: '(string)',
88
+ original_source: generated_source,
89
+ original_node: MPrelude::Either::Right.new(original_node),
90
+ generated_source: generated_source,
91
+ generated_node: generated_node
92
+ )
93
+ end
94
+
78
95
  # Create validator from file
79
96
  #
80
97
  # @param [Pathname] path
@@ -86,21 +103,10 @@ module Unparser
86
103
 
87
104
  private
88
105
 
89
- # Create a labeled report from
90
- #
91
- # @param [String] label
92
- # @param [Symbol] attribute_name
93
- #
94
- # @return [Array<String>]
95
106
  def make_report(label, attribute_name)
96
107
  ["#{label}:"].concat(public_send(attribute_name).either(method(:report_exception), ->(value) { [value] }))
97
108
  end
98
109
 
99
- # Report optional exception
100
- #
101
- # @param [Exception, nil] exception
102
- #
103
- # @return [Array<String>]
104
110
  def report_exception(exception)
105
111
  if exception
106
112
  [exception.inspect].concat(exception.backtrace.take(20))
@@ -109,9 +115,6 @@ module Unparser
109
115
  end
110
116
  end
111
117
 
112
- # Report the node diff
113
- #
114
- # @return [Array<String>]
115
118
  def node_diff_report
116
119
  diff = nil
117
120
 
@@ -127,23 +130,43 @@ module Unparser
127
130
  diff ? ['Node-Diff:', diff] : []
128
131
  end
129
132
 
130
- # Create unit represented as nil
131
- #
132
- # @param [Object] _value
133
- #
134
- # @return [nil]
135
133
  def self.const_unit(_value); end
136
134
  private_class_method :const_unit
137
135
 
138
- # Unparse capturing errors
139
- #
140
- # @param [Parser::AST::Node] node
141
- #
142
- # @return [Either<RuntimeError, String>]
143
- def self.unparse_either(node)
144
- MPrelude::Either
145
- .wrap_error(RuntimeError) { Unparser.unparse(node) }
146
- end
147
- private_class_method :unparse_either
136
+ class Literal < self
137
+ def success?
138
+ original_source.eql?(generated_source)
139
+ end
140
+
141
+ def report
142
+ message = [identification]
143
+
144
+ message.concat(make_report('Original-Source', :original_source))
145
+ message.concat(make_report('Generated-Source', :generated_source))
146
+ message.concat(make_report('Original-Node', :original_node))
147
+ message.concat(make_report('Generated-Node', :generated_node))
148
+ message.concat(node_diff_report)
149
+ message.concat(source_diff_report)
150
+
151
+ message.join("\n")
152
+ end
153
+
154
+ private
155
+
156
+ def source_diff_report
157
+ diff = nil
158
+
159
+ original_source.fmap do |original|
160
+ generated_source.fmap do |generated|
161
+ diff = Diff.new(
162
+ original.split("\n", -1),
163
+ generated.split("\n", -1)
164
+ ).colorized_diff
165
+ end
166
+ end
167
+
168
+ diff ? ['Source-Diff:', diff] : []
169
+ end
170
+ end # Literal
148
171
  end # Validation
149
172
  end # Unparser
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unparser
4
+ module Writer
5
+ include Generation, NodeHelpers
6
+
7
+ def self.included(descendant)
8
+ descendant.class_eval do
9
+ include Anima.new(:buffer, :comments, :node, :local_variable_scope)
10
+
11
+ extend DSL
12
+ end
13
+ end
14
+ end # Writer
15
+ end # Unparser
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unparser
4
+ module Writer
5
+ class Binary
6
+ include Writer, Adamantium::Flat
7
+
8
+ children :left, :right
9
+
10
+ OPERATOR_TOKENS =
11
+ {
12
+ and: '&&',
13
+ or: '||'
14
+ }.freeze
15
+
16
+ KEYWORD_TOKENS =
17
+ {
18
+ and: 'and',
19
+ or: 'or'
20
+ }.freeze
21
+
22
+ KEYWORD_SYMBOLS =
23
+ {
24
+ and: :kAND,
25
+ or: :kOR
26
+ }.freeze
27
+
28
+ OPERATOR_SYMBOLS =
29
+ {
30
+ and: :tANDOP,
31
+ or: :tOROP
32
+ }.freeze
33
+
34
+ MAP =
35
+ {
36
+ kAND: 'and',
37
+ kOR: 'or',
38
+ tOROP: '||',
39
+ tANDOP: '&&'
40
+ }.freeze
41
+
42
+ NEED_KEYWORD = %i[return break next].freeze
43
+
44
+ private_constant(*constants(false))
45
+
46
+ def emit_operator
47
+ emit_with(OPERATOR_TOKENS)
48
+ end
49
+
50
+ def symbol_name
51
+ true
52
+ end
53
+
54
+ def dispatch
55
+ left_emitter.write_to_buffer
56
+ write(' ', MAP.fetch(effective_symbol), ' ')
57
+ visit(right)
58
+ end
59
+
60
+ private
61
+
62
+ def effective_symbol
63
+ if NEED_KEYWORD.include?(right.type) || NEED_KEYWORD.include?(left.type)
64
+ return keyword_symbol
65
+ end
66
+
67
+ unless left_emitter.symbol_name
68
+ return operator_symbol
69
+ end
70
+
71
+ keyword_symbol
72
+ end
73
+
74
+ def emit_with(map)
75
+ visit(left)
76
+ write(' ', map.fetch(node.type), ' ')
77
+ visit(right)
78
+ end
79
+
80
+ def keyword_symbol
81
+ KEYWORD_SYMBOLS.fetch(node.type)
82
+ end
83
+
84
+ def operator_symbol
85
+ OPERATOR_SYMBOLS.fetch(node.type)
86
+ end
87
+
88
+ def left_emitter
89
+ emitter(left)
90
+ end
91
+ memoize :left_emitter
92
+
93
+ def right_emitter
94
+ emitter(right)
95
+ end
96
+ memoize :right_emitter
97
+ end # Binary
98
+ end # Writer
99
+ end # Unparser
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unparser
4
+ module Writer
5
+ class DynamicString
6
+ include Writer, Adamantium::Flat
7
+
8
+ PATTERNS_2 = IceNine.deep_freeze(
9
+ [
10
+ %i[str_empty begin],
11
+ %i[begin str_nl]
12
+ ]
13
+ )
14
+
15
+ PATTERNS_3 = IceNine.deep_freeze(
16
+ [
17
+ %i[begin str_nl_eol str_nl_eol],
18
+ %i[str_nl_eol begin str_nl_eol],
19
+ %i[str_ws begin str_nl_eol]
20
+ ]
21
+ )
22
+
23
+ FLAT_INTERPOLATION = %i[ivar cvar gvar].to_set.freeze
24
+
25
+ private_constant(*constants(false))
26
+
27
+ def emit_heredoc_reminder
28
+ return unless heredoc?
29
+
30
+ emit_heredoc_body
31
+ emit_heredoc_footer
32
+ end
33
+
34
+ def dispatch
35
+ if heredoc?
36
+ emit_heredoc_header
37
+ else
38
+ emit_dstr
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def heredoc_header
45
+ need_squiggly? ? '<<~HEREDOC' : '<<-HEREDOC'
46
+ end
47
+
48
+ def heredoc?
49
+ !children.empty? && (nl_last_child? && heredoc_pattern?)
50
+ end
51
+
52
+ def emit_heredoc_header
53
+ write(heredoc_header)
54
+ end
55
+
56
+ def emit_heredoc_body
57
+ nl
58
+ if need_squiggly?
59
+ emit_squiggly_heredoc_body
60
+ else
61
+ emit_normal_heredoc_body
62
+ end
63
+ end
64
+
65
+ def emit_heredoc_footer
66
+ write('HEREDOC')
67
+ end
68
+
69
+ def classify(node)
70
+ if n_str?(node)
71
+ classify_str(node)
72
+ else
73
+ node.type
74
+ end
75
+ end
76
+
77
+ def classify_str(node)
78
+ if str_nl?(node)
79
+ :str_nl
80
+ elsif node.children.first.end_with?("\n")
81
+ :str_nl_eol
82
+ elsif str_ws?(node)
83
+ :str_ws
84
+ elsif str_empty?(node)
85
+ :str_empty
86
+ end
87
+ end
88
+
89
+ def str_nl?(node)
90
+ node.eql?(s(:str, "\n"))
91
+ end
92
+
93
+ def str_empty?(node)
94
+ node.eql?(s(:str, ''))
95
+ end
96
+
97
+ def str_ws?(node)
98
+ /\A( |\t)+\z/.match?(node.children.first)
99
+ end
100
+
101
+ def heredoc_pattern?
102
+ heredoc_pattern_2? || heredoc_pattern_3?
103
+ end
104
+
105
+ def heredoc_pattern_3?
106
+ children.each_cons(3).any? do |group|
107
+ PATTERNS_3.include?(group.map(&method(:classify)))
108
+ end
109
+ end
110
+
111
+ def heredoc_pattern_2?
112
+ children.each_cons(2).any? do |group|
113
+ PATTERNS_2.include?(group.map(&method(:classify)))
114
+ end
115
+ end
116
+
117
+ def nl_last_child?
118
+ last = children.last
119
+ n_str?(last) && last.children.first[-1].eql?("\n")
120
+ end
121
+
122
+ def need_squiggly?
123
+ children.any?(s(:str, ''))
124
+ end
125
+
126
+ def emit_squiggly_heredoc_body
127
+ buffer.indent
128
+ children.each do |child|
129
+ if n_str?(child)
130
+ write(escape_dynamic(child.children.first))
131
+ else
132
+ emit_dynamic(child)
133
+ end
134
+ end
135
+ buffer.unindent
136
+ end
137
+
138
+ def emit_normal_heredoc_body
139
+ buffer.root_indent do
140
+ children.each do |child|
141
+ if n_str?(child)
142
+ write(escape_dynamic(child.children.first))
143
+ else
144
+ emit_dynamic(child)
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ def escape_dynamic(string)
151
+ string.gsub('#', '\#')
152
+ end
153
+
154
+ def emit_dynamic(child)
155
+ if FLAT_INTERPOLATION.include?(child.type)
156
+ write('#')
157
+ visit(child)
158
+ elsif n_dstr?(child)
159
+ emit_body(child.children)
160
+ else
161
+ write('#{')
162
+ emit_dynamic_component(child.children.first)
163
+ write('}')
164
+ end
165
+ end
166
+
167
+ def emit_dynamic_component(node)
168
+ visit(node) if node
169
+ end
170
+
171
+ def emit_dstr
172
+ if children.empty?
173
+ write('%()')
174
+ else
175
+ segments.each_with_index do |children, index|
176
+ emit_segment(children, index)
177
+ end
178
+ end
179
+ end
180
+
181
+ def breakpoint?(child, current)
182
+ last_type = current.last&.type
183
+
184
+ [
185
+ n_str?(child) && last_type.equal?(:str) && current.none?(&method(:n_begin?)),
186
+ last_type.equal?(:dstr),
187
+ n_dstr?(child) && last_type
188
+ ].any?
189
+ end
190
+
191
+ def segments
192
+ segments = []
193
+
194
+ segments << current = []
195
+
196
+ children.each do |child|
197
+ if breakpoint?(child, current)
198
+ segments << current = []
199
+ end
200
+
201
+ current << child
202
+ end
203
+
204
+ segments
205
+ end
206
+
207
+ def emit_segment(children, index)
208
+ write(' ') unless index.zero?
209
+
210
+ write('"')
211
+ emit_body(children)
212
+ write('"')
213
+ end
214
+
215
+ def emit_body(children)
216
+ buffer.root_indent do
217
+ children.each_with_index do |child, index|
218
+ if n_str?(child)
219
+ string = child.children.first
220
+ if string.eql?("\n") && children.fetch(index.pred).type.equal?(:begin)
221
+ write("\n")
222
+ else
223
+ write(string.inspect[1..-2])
224
+ end
225
+ else
226
+ emit_dynamic(child)
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end # DynamicString
232
+ end # Writer
233
+ end # Unparser