sql_beautifier 0.6.0 → 0.7.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +2 -2
  4. data/lib/sql_beautifier/base.rb +9 -0
  5. data/lib/sql_beautifier/clauses/base.rb +2 -2
  6. data/lib/sql_beautifier/clauses/condition_clause.rb +1 -1
  7. data/lib/sql_beautifier/clauses/from.rb +15 -69
  8. data/lib/sql_beautifier/clauses/order_by.rb +12 -1
  9. data/lib/sql_beautifier/clauses/select.rb +28 -15
  10. data/lib/sql_beautifier/comment.rb +23 -0
  11. data/lib/sql_beautifier/{comment_stripper.rb → comment_parser.rb} +67 -24
  12. data/lib/sql_beautifier/condition.rb +162 -0
  13. data/lib/sql_beautifier/configuration.rb +4 -15
  14. data/lib/sql_beautifier/create_table_as.rb +127 -0
  15. data/lib/sql_beautifier/cte_definition.rb +41 -0
  16. data/lib/sql_beautifier/cte_query.rb +129 -0
  17. data/lib/sql_beautifier/expression.rb +54 -0
  18. data/lib/sql_beautifier/formatter.rb +13 -80
  19. data/lib/sql_beautifier/join.rb +69 -0
  20. data/lib/sql_beautifier/normalizer.rb +33 -59
  21. data/lib/sql_beautifier/query.rb +185 -0
  22. data/lib/sql_beautifier/scanner.rb +420 -0
  23. data/lib/sql_beautifier/sort_expression.rb +39 -0
  24. data/lib/sql_beautifier/statement_assembler.rb +4 -4
  25. data/lib/sql_beautifier/statement_splitter.rb +35 -143
  26. data/lib/sql_beautifier/table_reference.rb +52 -0
  27. data/lib/sql_beautifier/table_registry.rb +50 -124
  28. data/lib/sql_beautifier/tokenizer.rb +47 -278
  29. data/lib/sql_beautifier/types.rb +9 -0
  30. data/lib/sql_beautifier/version.rb +1 -1
  31. data/lib/sql_beautifier.rb +14 -6
  32. metadata +43 -7
  33. data/lib/sql_beautifier/comment_restorer.rb +0 -62
  34. data/lib/sql_beautifier/condition_formatter.rb +0 -127
  35. data/lib/sql_beautifier/create_table_as_formatter.rb +0 -177
  36. data/lib/sql_beautifier/cte_formatter.rb +0 -192
  37. data/lib/sql_beautifier/subquery_formatter.rb +0 -113
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SqlBeautifier
4
- class Configuration
4
+ class Configuration < Base
5
5
  DEFAULTS = {
6
6
  keyword_case: :lower,
7
7
  keyword_column_width: 8,
@@ -14,20 +14,9 @@ module SqlBeautifier
14
14
  removable_comment_types: :none,
15
15
  }.freeze
16
16
 
17
- COMMENT_TYPES = %i[inline separate_line blocks].freeze
18
-
19
- attr_accessor :keyword_case
20
- attr_accessor :keyword_column_width
21
- attr_accessor :indent_spaces
22
- attr_accessor :clause_spacing_mode
23
- attr_accessor :table_name_format
24
- attr_accessor :inline_group_threshold
25
- attr_accessor :alias_strategy
26
- attr_accessor :trailing_semicolon
27
- attr_accessor :removable_comment_types
28
-
29
- def initialize
30
- reset!
17
+ DEFAULTS.each_key do |key|
18
+ option key, default: -> { DEFAULTS[key] }
19
+ attr_writer key
31
20
  end
32
21
 
33
22
  def reset!
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ class CreateTableAs < Base
5
+ MODIFIERS = %w[temp temporary unlogged local].freeze
6
+ WITH_DATA_SUFFIX_REGEX = %r{\s+(with\s+(?:no\s+)?data)\s*\z}i
7
+
8
+ option :modifier, default: -> {}
9
+ option :if_not_exists, type: Types::Bool
10
+ option :table_name
11
+ option :body_sql
12
+ option :suffix, default: -> {}
13
+ option :depth, default: -> { 0 }
14
+
15
+ def self.parse(normalized_sql, depth: 0)
16
+ scanner = Scanner.new(normalized_sql)
17
+ return nil unless scanner.keyword_at?("create")
18
+
19
+ scanner.skip_past_keyword!("create")
20
+ modifier = detect_modifier(scanner)
21
+ scanner.skip_past_keyword!(modifier) if modifier
22
+
23
+ return nil unless scanner.keyword_at?("table")
24
+
25
+ scanner.skip_past_keyword!("table")
26
+
27
+ if_not_exists = detect_if_not_exists?(scanner)
28
+ skip_past_if_not_exists!(scanner) if if_not_exists
29
+
30
+ table_name = scanner.read_identifier!
31
+ return nil unless table_name
32
+
33
+ scanner.skip_whitespace!
34
+ return nil unless scanner.keyword_at?("as")
35
+
36
+ scanner.skip_past_keyword!("as")
37
+
38
+ body_sql, suffix = extract_body(normalized_sql, scanner.position)
39
+ return nil unless body_sql
40
+
41
+ new(modifier: modifier, if_not_exists: if_not_exists, table_name: table_name, body_sql: body_sql, suffix: suffix, depth: depth)
42
+ end
43
+
44
+ def self.detect_modifier(scanner)
45
+ MODIFIERS.detect { |modifier| scanner.keyword_at?(modifier) }
46
+ end
47
+
48
+ def self.detect_if_not_exists?(scanner)
49
+ return false unless scanner.keyword_at?("if")
50
+
51
+ probe = Scanner.new(scanner.source, position: scanner.position)
52
+ probe.skip_past_keyword!("if")
53
+ return false unless probe.keyword_at?("not")
54
+
55
+ probe.skip_past_keyword!("not")
56
+ probe.keyword_at?("exists")
57
+ end
58
+
59
+ def self.skip_past_if_not_exists!(scanner)
60
+ scanner.skip_past_keyword!("if")
61
+ scanner.skip_past_keyword!("not")
62
+ scanner.skip_past_keyword!("exists")
63
+ end
64
+
65
+ def self.extract_body(sql, position)
66
+ scanner = Scanner.new(sql, position: position)
67
+ scanner.skip_whitespace!
68
+ return nil if scanner.finished?
69
+
70
+ if scanner.current_char == Constants::OPEN_PARENTHESIS
71
+ closing = scanner.find_matching_parenthesis(scanner.position)
72
+ return nil unless closing
73
+
74
+ body = sql[(scanner.position + 1)...closing].strip
75
+ suffix = sql[(closing + 1)..].strip.presence
76
+ [body, suffix]
77
+ else
78
+ extract_unparenthesized_body(sql[scanner.position..].strip)
79
+ end
80
+ end
81
+
82
+ def self.extract_unparenthesized_body(raw_body)
83
+ return nil unless raw_body.present?
84
+
85
+ match = raw_body.match(WITH_DATA_SUFFIX_REGEX)
86
+
87
+ if match
88
+ body = raw_body[0...match.begin(0)].strip
89
+ return nil unless body.present?
90
+
91
+ [body, match[1]]
92
+ else
93
+ [raw_body, nil]
94
+ end
95
+ end
96
+
97
+ def render
98
+ indent_spaces = SqlBeautifier.config_for(:indent_spaces) || 4
99
+ formatted = Formatter.new(@body_sql, depth: 0).call
100
+ return "#{preamble}\n" unless formatted
101
+
102
+ indentation = Util.whitespace(indent_spaces)
103
+ indented_lines = formatted.chomp.lines.map do |line|
104
+ line.strip.empty? ? "\n" : "#{indentation}#{line}"
105
+ end.join
106
+
107
+ formatted_suffix = @suffix ? " #{format_suffix}" : ""
108
+ "#{preamble} (\n#{indented_lines}\n)#{formatted_suffix}\n"
109
+ end
110
+
111
+ private
112
+
113
+ def preamble
114
+ parts = [Util.format_keyword("create")]
115
+ parts << Util.format_keyword(@modifier) if @modifier
116
+ parts << Util.format_keyword("table")
117
+ parts << "#{Util.format_keyword('if')} #{Util.format_keyword('not')} #{Util.format_keyword('exists')}" if @if_not_exists
118
+ parts << Util.format_table_name(@table_name)
119
+ parts << Util.format_keyword("as")
120
+ parts.join(" ")
121
+ end
122
+
123
+ def format_suffix
124
+ @suffix.strip.split(Constants::WHITESPACE_REGEX).map { |word| Util.format_keyword(word) }.join(" ")
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ class CteDefinition < Base
5
+ option :name
6
+ option :body_sql
7
+ option :column_list, default: -> {}
8
+ option :materialization, default: -> {}
9
+
10
+ def render_header
11
+ header = +@name.to_s
12
+ header << " (#{@column_list})" if @column_list
13
+ header << " #{Util.format_keyword('as')}"
14
+ header << " #{format_materialization}" if @materialization
15
+ header << " "
16
+ header
17
+ end
18
+
19
+ def render_body(base_indent)
20
+ indent_spaces = SqlBeautifier.config_for(:indent_spaces) || 4
21
+ body_indent = base_indent + indent_spaces
22
+ formatted = Formatter.new(@body_sql, depth: 0).call
23
+ return "(#{@body_sql})" unless formatted
24
+
25
+ indentation = Util.whitespace(body_indent)
26
+ indented_lines = formatted.chomp.lines.map do |line|
27
+ line.strip.empty? ? "\n" : "#{indentation}#{line}"
28
+ end.join
29
+
30
+ "(\n#{indented_lines}\n#{Util.whitespace(base_indent)})"
31
+ end
32
+
33
+ private
34
+
35
+ def format_materialization
36
+ return Util.format_keyword("materialized") if @materialization == "materialized"
37
+
38
+ [Util.format_keyword("not"), Util.format_keyword("materialized")].join(" ")
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ class CteQuery < Base
5
+ option :recursive, type: Types::Bool
6
+ option :definitions
7
+ option :main_query_sql
8
+ option :depth, default: -> { 0 }
9
+
10
+ def self.parse(normalized_sql, depth: 0)
11
+ scanner = Scanner.new(normalized_sql)
12
+ return nil unless scanner.keyword_at?("with")
13
+
14
+ scanner.skip_past_keyword!("with")
15
+
16
+ recursive = scanner.keyword_at?("recursive")
17
+ scanner.skip_past_keyword!("recursive") if recursive
18
+
19
+ definitions = []
20
+
21
+ loop do
22
+ definition, new_position = parse_raw_definition(normalized_sql, scanner.position)
23
+ break unless definition
24
+
25
+ definitions << CteDefinition.new(**definition)
26
+ scanner.advance!(new_position - scanner.position)
27
+ scanner.skip_whitespace!
28
+
29
+ break unless scanner.position < normalized_sql.length && scanner.current_char == Constants::COMMA
30
+
31
+ scanner.advance!
32
+ scanner.skip_whitespace!
33
+ end
34
+
35
+ main_query_sql = normalized_sql[scanner.position..].strip
36
+ return nil unless definitions.any? && main_query_sql.present?
37
+
38
+ new(recursive: recursive, definitions: definitions, main_query_sql: main_query_sql, depth: depth)
39
+ end
40
+
41
+ def self.parse_raw_definition(sql, position)
42
+ scanner = Scanner.new(sql, position: position)
43
+
44
+ name = scanner.read_identifier!
45
+ return nil unless name
46
+
47
+ scanner.skip_whitespace!
48
+
49
+ column_list = parse_column_list(sql, scanner.position)
50
+ scanner.advance!(column_list[:next_position] - scanner.position) if column_list
51
+
52
+ return nil unless scanner.keyword_at?("as")
53
+
54
+ scanner.skip_past_keyword!("as")
55
+ materialization, materialization_end_position = parse_materialization(sql, scanner.position)
56
+ scanner.advance!(materialization_end_position - scanner.position)
57
+
58
+ return nil unless scanner.position < sql.length && scanner.current_char == Constants::OPEN_PARENTHESIS
59
+
60
+ closing = scanner.find_matching_parenthesis(scanner.position)
61
+ return nil unless closing
62
+
63
+ body_sql = sql[(scanner.position + 1)...closing].strip
64
+ result = { name: name, body_sql: body_sql }
65
+ result[:column_list] = column_list[:text] if column_list
66
+ result[:materialization] = materialization if materialization
67
+
68
+ [result, closing + 1]
69
+ end
70
+
71
+ def self.parse_column_list(sql, position)
72
+ return nil unless position < sql.length && sql[position] == Constants::OPEN_PARENTHESIS
73
+
74
+ scanner = Scanner.new(sql)
75
+ closing = scanner.find_matching_parenthesis(position)
76
+ return nil unless closing
77
+
78
+ after_paren_scanner = Scanner.new(sql, position: closing + 1)
79
+ after_paren_scanner.skip_whitespace!
80
+ return nil unless after_paren_scanner.keyword_at?("as")
81
+
82
+ { text: sql[(position + 1)...closing].strip, next_position: after_paren_scanner.position }
83
+ end
84
+
85
+ def self.parse_materialization(sql, position)
86
+ scanner = Scanner.new(sql, position: position)
87
+ scanner.skip_whitespace!
88
+
89
+ if scanner.keyword_at?("materialized")
90
+ scanner.skip_past_keyword!("materialized")
91
+ return ["materialized", scanner.position]
92
+ end
93
+
94
+ return [nil, scanner.position] unless scanner.keyword_at?("not")
95
+
96
+ scanner.skip_past_keyword!("not")
97
+
98
+ return [nil, position] unless scanner.keyword_at?("materialized")
99
+
100
+ scanner.skip_past_keyword!("materialized")
101
+ ["not materialized", scanner.position]
102
+ end
103
+
104
+ def render
105
+ keyword_width = SqlBeautifier.config_for(:keyword_column_width)
106
+ continuation_indent = Util.continuation_padding
107
+
108
+ output = +""
109
+
110
+ @definitions.each_with_index do |definition, index|
111
+ if index.zero?
112
+ output << Util.keyword_padding("with")
113
+ output << "#{Util.format_keyword('recursive')} " if @recursive
114
+ else
115
+ output << continuation_indent
116
+ end
117
+
118
+ output << definition.render_header
119
+ output << definition.render_body(keyword_width)
120
+ output << (index < @definitions.length - 1 ? ",\n" : "\n\n")
121
+ end
122
+
123
+ formatted_main = Formatter.new(@main_query_sql, depth: @depth).call
124
+ output << formatted_main if formatted_main
125
+
126
+ output
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ class Expression < Base
5
+ option :definition
6
+ option :alias_name, default: -> {}
7
+
8
+ def self.parse(text)
9
+ stripped = text.strip
10
+ alias_position = find_top_level_as(stripped)
11
+
12
+ if alias_position
13
+ definition = stripped[0...alias_position].strip
14
+ alias_name = stripped[(alias_position + 3)..].strip
15
+ new(definition: definition, alias_name: alias_name)
16
+ else
17
+ new(definition: stripped)
18
+ end
19
+ end
20
+
21
+ def self.find_top_level_as(text)
22
+ scanner = Scanner.new(text)
23
+
24
+ until scanner.finished?
25
+ consumed = scanner.scan_quoted_or_sentinel!
26
+ next if consumed
27
+
28
+ if scanner.current_char == Constants::OPEN_PARENTHESIS
29
+ scanner.increment_depth!
30
+ scanner.advance!
31
+ next
32
+ end
33
+
34
+ if scanner.current_char == Constants::CLOSE_PARENTHESIS
35
+ scanner.decrement_depth!
36
+ scanner.advance!
37
+ next
38
+ end
39
+
40
+ return scanner.position if scanner.top_level? && scanner.keyword_at?("as")
41
+
42
+ scanner.advance!
43
+ end
44
+
45
+ nil
46
+ end
47
+
48
+ def render
49
+ return @definition unless @alias_name
50
+
51
+ "#{@definition} as #{@alias_name}"
52
+ end
53
+ end
54
+ end
@@ -2,8 +2,11 @@
2
2
 
3
3
  module SqlBeautifier
4
4
  class Formatter
5
- def self.call(value)
6
- new(value).call
5
+ LEADING_SENTINEL_PATTERN = %r{\A#{CommentParser::SENTINEL_PATTERN}[[:space:]]*}
6
+ LEADING_SENTINEL_CAPTURE = %r{\A(#{CommentParser::SENTINEL_PATTERN}[[:space:]]*)}
7
+
8
+ def self.call(...)
9
+ new(...).call
7
10
  end
8
11
 
9
12
  def initialize(value, depth: 0)
@@ -20,33 +23,20 @@ module SqlBeautifier
20
23
  @leading_sentinels = extract_leading_sentinels!
21
24
  return unless @normalized_value.present?
22
25
 
23
- cte_result = CteFormatter.format(@normalized_value, depth: @depth)
26
+ cte_result = CteQuery.parse(@normalized_value, depth: @depth)&.render
24
27
  return prepend_sentinels(cte_result) if cte_result
25
28
 
26
- create_table_as_result = CreateTableAsFormatter.format(@normalized_value, depth: @depth)
29
+ create_table_as_result = CreateTableAs.parse(@normalized_value, depth: @depth)&.render
27
30
  return prepend_sentinels(create_table_as_result) if create_table_as_result
28
31
 
29
32
  first_clause_position = Tokenizer.first_clause_position(@normalized_value)
30
33
  return prepend_sentinels("#{@normalized_value}\n") if first_clause_position.nil? || first_clause_position.positive?
31
34
 
32
- @clauses = Tokenizer.split_into_clauses(@normalized_value)
33
- @table_registry = TableRegistry.new(@clauses[:from]) if @clauses[:from].present?
34
- @parts = []
35
-
36
- append_clause!(:select, Clauses::Select)
37
- append_from_clause!
38
- append_clause!(:where, Clauses::Where)
39
- append_clause!(:group_by, Clauses::GroupBy)
40
- append_clause!(:having, Clauses::Having)
41
- append_clause!(:order_by, Clauses::OrderBy)
42
- append_clause!(:limit, Clauses::Limit)
35
+ query = Query.parse(@normalized_value, depth: @depth)
36
+ result = query.render
37
+ return prepend_sentinels("#{@normalized_value}\n") unless result
43
38
 
44
- output = @parts.join(clause_separator)
45
- return prepend_sentinels("#{@normalized_value}\n") if output.empty?
46
-
47
- output = SubqueryFormatter.format(output, @depth)
48
- output = @table_registry.apply_aliases(output) if @table_registry
49
- prepend_sentinels("#{output}\n")
39
+ prepend_sentinels(result)
50
40
  end
51
41
 
52
42
  private
@@ -55,8 +45,8 @@ module SqlBeautifier
55
45
  leading_sentinel_text = +""
56
46
  remaining_value = @normalized_value
57
47
 
58
- while remaining_value.match?(%r{\A#{CommentStripper::SENTINEL_PATTERN}[[:space:]]*})
59
- match = remaining_value.match(%r{\A(#{CommentStripper::SENTINEL_PATTERN}[[:space:]]*)})
48
+ while remaining_value.match?(LEADING_SENTINEL_PATTERN)
49
+ match = remaining_value.match(LEADING_SENTINEL_CAPTURE)
60
50
  leading_sentinel_text << match[1]
61
51
  remaining_value = remaining_value[match[1].length..]
62
52
  end
@@ -71,62 +61,5 @@ module SqlBeautifier
71
61
 
72
62
  "#{@leading_sentinels}#{output}"
73
63
  end
74
-
75
- def append_clause!(clause_key, formatter_class)
76
- value = @clauses[clause_key]
77
- return unless value.present?
78
-
79
- @parts << formatter_class.call(value)
80
- end
81
-
82
- def append_from_clause!
83
- value = @clauses[:from]
84
- return unless value.present?
85
-
86
- @parts << Clauses::From.call(value, table_registry: @table_registry)
87
- end
88
-
89
- def clause_separator
90
- return "\n\n" if SqlBeautifier.config_for(:clause_spacing_mode) == :spacious
91
- return "\n\n" unless compact_query?
92
-
93
- "\n"
94
- end
95
-
96
- def compact_query?
97
- compact_clause_set? && single_select_column? && single_from_table? && one_or_fewer_conditions?
98
- end
99
-
100
- def compact_clause_set?
101
- clause_keys = @clauses.keys
102
- allowed_keys = %i[select from where order_by limit]
103
-
104
- clause_keys.all? { |key| allowed_keys.include?(key) } && clause_keys.include?(:select) && clause_keys.include?(:from)
105
- end
106
-
107
- def single_select_column?
108
- select_value = @clauses[:select]
109
- return false unless select_value.present?
110
-
111
- formatted_select = Clauses::Select.call(select_value)
112
- formatted_select.lines.length == 1
113
- end
114
-
115
- def single_from_table?
116
- from_value = @clauses[:from]
117
- return false unless from_value.present?
118
-
119
- join_keywords = Constants::JOIN_KEYWORDS_BY_LENGTH.any? { |keyword| Tokenizer.find_top_level_keyword(from_value, keyword) }
120
- return false if join_keywords
121
-
122
- !from_value.match?(%r{,})
123
- end
124
-
125
- def one_or_fewer_conditions?
126
- where_value = @clauses[:where]
127
- return true unless where_value.present?
128
-
129
- Tokenizer.split_top_level_conditions(where_value).length <= 1
130
- end
131
64
  end
132
65
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqlBeautifier
4
+ class Join < Base
5
+ option :keyword
6
+ option :table_reference
7
+ option :trailing_sentinels, default: -> {}
8
+ option :conditions, default: -> { [] }
9
+
10
+ def self.parse(join_text, table_registry:)
11
+ keyword, remaining_content = extract_keyword(join_text)
12
+ return unless keyword && remaining_content
13
+
14
+ on_keyword_position = Tokenizer.find_top_level_keyword(remaining_content, "on")
15
+
16
+ if on_keyword_position
17
+ table_text = remaining_content[0...on_keyword_position].strip
18
+ condition_text = remaining_content[on_keyword_position..].delete_prefix("on").strip
19
+ conditions = Tokenizer.split_top_level_conditions(condition_text)
20
+ else
21
+ table_text = remaining_content
22
+ conditions = []
23
+ end
24
+
25
+ table_name = Util.first_word(table_text)
26
+ table_ref = table_registry.reference_for(table_name)
27
+ trailing_sentinels = extract_trailing_sentinels(table_text)
28
+
29
+ new(keyword: keyword, table_reference: table_ref, trailing_sentinels: trailing_sentinels, conditions: conditions)
30
+ end
31
+
32
+ def self.extract_keyword(join_text)
33
+ trimmed = join_text.strip
34
+
35
+ Constants::JOIN_KEYWORDS_BY_LENGTH.each do |keyword|
36
+ next unless trimmed.downcase.start_with?(keyword)
37
+
38
+ remaining = trimmed[keyword.length..].strip
39
+ return [keyword, remaining]
40
+ end
41
+
42
+ [nil, nil]
43
+ end
44
+
45
+ def self.extract_trailing_sentinels(text)
46
+ text.scan(CommentParser::SENTINEL_PATTERN).map do |match|
47
+ "#{CommentParser::SENTINEL_PREFIX}#{match[0]}#{CommentParser::SENTINEL_SUFFIX}"
48
+ end.presence
49
+ end
50
+
51
+ def render(continuation_indent:, condition_indent:)
52
+ rendered_table = @table_reference.render(trailing_sentinels: @trailing_sentinels)
53
+ lines = []
54
+
55
+ if @conditions.any?
56
+ first_condition = @conditions.first[1]
57
+ lines << "#{continuation_indent}#{@keyword} #{rendered_table} on #{first_condition}"
58
+
59
+ @conditions.drop(1).each do |conjunction, condition|
60
+ lines << "#{condition_indent}#{conjunction} #{condition}"
61
+ end
62
+ else
63
+ lines << "#{continuation_indent}#{@keyword} #{rendered_table}"
64
+ end
65
+
66
+ lines.join("\n")
67
+ end
68
+ end
69
+ end