omnitest-psychic 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +5 -0
  5. data/.rubocop_todo.yml +36 -0
  6. data/.travis.yml +10 -0
  7. data/CONTRIBUTING.md +61 -0
  8. data/Gemfile +23 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +234 -0
  11. data/Rakefile +12 -0
  12. data/appveyor.yml +9 -0
  13. data/bin/psychic +4 -0
  14. data/docs/code_samples.md +92 -0
  15. data/docs/index.md +128 -0
  16. data/lib/omnitest/output_helper.rb +84 -0
  17. data/lib/omnitest/psychic.rb +191 -0
  18. data/lib/omnitest/psychic/cli.rb +171 -0
  19. data/lib/omnitest/psychic/code2doc.rb +75 -0
  20. data/lib/omnitest/psychic/code2doc/code_helper.rb +122 -0
  21. data/lib/omnitest/psychic/code2doc/code_segmenter.rb +170 -0
  22. data/lib/omnitest/psychic/code2doc/comment_styles.rb +92 -0
  23. data/lib/omnitest/psychic/command_template.rb +35 -0
  24. data/lib/omnitest/psychic/error.rb +15 -0
  25. data/lib/omnitest/psychic/execution/default_strategy.rb +82 -0
  26. data/lib/omnitest/psychic/execution/env_strategy.rb +12 -0
  27. data/lib/omnitest/psychic/execution/flag_strategy.rb +14 -0
  28. data/lib/omnitest/psychic/execution/token_strategy.rb +68 -0
  29. data/lib/omnitest/psychic/factories/go_factories.rb +14 -0
  30. data/lib/omnitest/psychic/factories/hot_read_task_factory.rb +54 -0
  31. data/lib/omnitest/psychic/factories/java_factories.rb +81 -0
  32. data/lib/omnitest/psychic/factories/powershell_factories.rb +53 -0
  33. data/lib/omnitest/psychic/factories/ruby_factories.rb +56 -0
  34. data/lib/omnitest/psychic/factories/shell_factories.rb +89 -0
  35. data/lib/omnitest/psychic/factories/travis_factories.rb +33 -0
  36. data/lib/omnitest/psychic/factory_manager.rb +46 -0
  37. data/lib/omnitest/psychic/file_finder.rb +69 -0
  38. data/lib/omnitest/psychic/hints.rb +21 -0
  39. data/lib/omnitest/psychic/magic_task_factory.rb +98 -0
  40. data/lib/omnitest/psychic/script.rb +146 -0
  41. data/lib/omnitest/psychic/script_factory.rb +66 -0
  42. data/lib/omnitest/psychic/script_factory_manager.rb +24 -0
  43. data/lib/omnitest/psychic/script_runner.rb +45 -0
  44. data/lib/omnitest/psychic/task.rb +6 -0
  45. data/lib/omnitest/psychic/task_factory_manager.rb +19 -0
  46. data/lib/omnitest/psychic/task_runner.rb +30 -0
  47. data/lib/omnitest/psychic/tokens.rb +51 -0
  48. data/lib/omnitest/psychic/version.rb +5 -0
  49. data/lib/omnitest/psychic/workflow.rb +27 -0
  50. data/lib/omnitest/shell.rb +27 -0
  51. data/lib/omnitest/shell/buff_shellout_executor.rb +41 -0
  52. data/lib/omnitest/shell/execution_result.rb +61 -0
  53. data/lib/omnitest/shell/mixlib_shellout_executor.rb +66 -0
  54. data/mkdocs.yml +6 -0
  55. data/omnitest-psychic.gemspec +36 -0
  56. data/spec/fabricators/shell_fabricator.rb +9 -0
  57. data/spec/omnitest/psychic/code2doc/code_helper_spec.rb +123 -0
  58. data/spec/omnitest/psychic/execution/default_strategy_spec.rb +17 -0
  59. data/spec/omnitest/psychic/execution/env_strategy_spec.rb +17 -0
  60. data/spec/omnitest/psychic/execution/flag_strategy_spec.rb +26 -0
  61. data/spec/omnitest/psychic/execution/token_strategy_spec.rb +26 -0
  62. data/spec/omnitest/psychic/factories/java_factories_spec.rb +35 -0
  63. data/spec/omnitest/psychic/factories/powershell_factories_spec.rb +59 -0
  64. data/spec/omnitest/psychic/factories/ruby_factories_spec.rb +91 -0
  65. data/spec/omnitest/psychic/factories/shell_factories_spec.rb +79 -0
  66. data/spec/omnitest/psychic/factories/travis_factories_spec.rb +78 -0
  67. data/spec/omnitest/psychic/hot_read_task_factory_spec.rb +51 -0
  68. data/spec/omnitest/psychic/script_factory_manager_spec.rb +57 -0
  69. data/spec/omnitest/psychic/script_spec.rb +55 -0
  70. data/spec/omnitest/psychic/shell_spec.rb +68 -0
  71. data/spec/omnitest/psychic/workflow_spec.rb +46 -0
  72. data/spec/omnitest/psychic_spec.rb +170 -0
  73. data/spec/spec_helper.rb +52 -0
  74. metadata +352 -0
@@ -0,0 +1,171 @@
1
+ require 'omnitest/core'
2
+ require 'omnitest/psychic'
3
+
4
+ # rubocop:disable Metrics/LineLength
5
+
6
+ module Omnitest
7
+ class Psychic
8
+ class BaseCLI < Omnitest::Core::CLI
9
+ include Thor::Actions
10
+
11
+ no_commands do
12
+ def psychic
13
+ @psychic ||= setup_runner
14
+ end
15
+
16
+ def setup_runner
17
+ runner_opts = { cwd: Dir.pwd, cli: shell, parameters: options.parameters }
18
+ runner_opts.merge!(Omnitest::Core::Util.symbolized_hash(options))
19
+ Omnitest::Psychic.new(runner_opts)
20
+ end
21
+ end
22
+ end
23
+
24
+ class List < BaseCLI
25
+ desc 'scripts', 'Lists known scripts'
26
+ method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
27
+ method_option :cwd, desc: 'Working directory for detecting and running commands'
28
+ def scripts
29
+ scripts = psychic.known_scripts.map do |script|
30
+ [set_color(script.name, :bold), script.source_file]
31
+ end
32
+ print_table scripts
33
+ end
34
+
35
+ desc 'tasks', 'List known tasks'
36
+ method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
37
+ method_option :cwd, desc: 'Working directory for detecting and running commands'
38
+ def tasks # rubocop:disable Metrics/AbcSize
39
+ psychic.known_tasks.map do |task|
40
+ task_id = set_color(task, :bold)
41
+ if options[:verbose]
42
+ details = psychic.task(task)
43
+ details = "\n#{details}".lines.join(' ') if details.lines.size > 1
44
+ say "#{task_id}: #{details}"
45
+ else
46
+ say task_id
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ class Show < BaseCLI
53
+ desc 'script <name>', 'Show detailed information about a script'
54
+ method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
55
+ method_option :cwd, desc: 'Working directory for detecting and running commands'
56
+ def script(script_name)
57
+ script = psychic.script(script_name)
58
+ say script.to_s(options[:verbose])
59
+ end
60
+ end
61
+
62
+ class CLI < BaseCLI
63
+ BUILT_IN_TASKS = %w(bootstrap unittest acceptancetest)
64
+
65
+ desc 'task <name>', 'Executes any task by name'
66
+ method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
67
+ method_option :cwd, desc: 'Working directory for detecting and running commands'
68
+ method_option :os, desc: "Target OS (default value is `RbConfig::CONFIG['host_os']`)"
69
+ method_option :travis, type: :boolean, desc: "Enable/disable delegation to travis-build, if it's available"
70
+ method_option :print, aliases: '-p', desc: 'Print the command (or script) instead of running it'
71
+ def task(task_alias = nil) # rubocop:disable Metrics/AbcSize
72
+ abort 'You must specify a task name, run `psychic list tasks` for a list of known tasks' unless task_alias
73
+ command = psychic.task(task_alias)
74
+ if options[:print]
75
+ say command
76
+ else
77
+ psychic.execute(command, *extra_args)
78
+ end
79
+ rescue TaskNotImplementedError => e
80
+ abort "No usable command was found for task #{task_alias}"
81
+ rescue Omnitest::Shell::ExecutionError => e
82
+ say_status :failed, task_alias, :red
83
+ say e.execution_result if e.execution_result
84
+ end
85
+
86
+ BUILT_IN_TASKS.each do |task_alias|
87
+ desc task_alias, "Executes the #{task_alias} task"
88
+ method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
89
+ method_option :cwd, desc: 'Working directory for detecting and running commands'
90
+ define_method(task_alias) do
91
+ task(task_alias)
92
+ end
93
+ end
94
+
95
+ desc 'script <name>', 'Executes a script'
96
+ method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
97
+ method_option :cwd, desc: 'Working directory for detecting and running commands'
98
+ method_option :interactive, desc: 'Prompt for parameters?', enum: %w(always missing), lazy_default: 'missing'
99
+ method_option :parameters, desc: 'YAML file containing key/value parameters. Default: psychic-parameters.yaml'
100
+ method_option :parameter_mode, desc: 'How should the parameters be passed?', enum: %w(tokens arguments env)
101
+ method_option :os, desc: "Target OS (default value is `RbConfig::CONFIG['host_os']`)"
102
+ method_option :print, aliases: '-p', desc: 'Print the command (or script) instead of running it', lazy_default: true
103
+ def script(script_name = nil) # rubocop:disable Metrics/AbcSize
104
+ abort 'You must specify a script name, run `psychic list scripts` for a list of known scripts' unless script_name
105
+ command = psychic.script(script_name, *extra_args)
106
+ if options[:print]
107
+ say command.command(*extra_args) << "\n"
108
+ else
109
+ command.execute(*extra_args)
110
+ end
111
+ rescue ScriptNotRunnable => e
112
+ abort "No usable command was found for script #{script_name}"
113
+ rescue Omnitest::Shell::ExecutionError => e
114
+ say_status :failed, script_name, :red
115
+ say e.execution_result if e.execution_result
116
+ end
117
+
118
+ desc 'workflow [*tasks]', 'Executes multiple tasks together as a single workflow'
119
+ method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
120
+ method_option :cwd, desc: 'Working directory for detecting and running commands'
121
+ method_option :os, desc: "Target OS (default value is `RbConfig::CONFIG['host_os']`)"
122
+ method_option :travis, type: :boolean, desc: "Enable/disable delegation to travis-build, if it's available"
123
+ method_option :print, aliases: '-p', desc: 'Print the command (or script) instead of running it'
124
+ method_option :name, desc: 'A display name for the workflow'
125
+ def workflow(*tasks)
126
+ abort 'Please specify at least one task' if tasks.empty?
127
+ workflow = psychic.workflow(options[:name], options) do
128
+ tasks.each do | task_alias |
129
+ begin
130
+ task task_alias
131
+ rescue TaskNotImplementedError => e
132
+ abort "No usable command was found for task #{task_alias}"
133
+ end
134
+ end
135
+ end
136
+ if options[:print]
137
+ say workflow.command
138
+ else
139
+ workflow.execute({}, {}, *extra_args)
140
+ end
141
+ rescue Omnitest::Shell::ExecutionError => e
142
+ say_status :failed, options[:name], :red
143
+ say e.execution_result if e.execution_result
144
+ end
145
+
146
+ desc 'code2doc', 'Convert script to lightweight markup'
147
+ method_option :format,
148
+ enum: %w(md rst),
149
+ default: 'md',
150
+ desc: 'Target documentation format'
151
+ method_option :destination,
152
+ aliases: '-d',
153
+ default: 'docs/',
154
+ desc: 'The target directory where documentation for generated documentation.'
155
+ def code2doc(*script_names)
156
+ script_names.each do | script_name |
157
+ script = psychic.script(script_name)
158
+ target_file = File.expand_path(script.name + ".#{options[:format]}", options[:destination])
159
+ create_file(target_file, script.code2doc(options))
160
+ end
161
+ end
162
+
163
+ desc 'list', 'List known tasks or scripts'
164
+ subcommand 'list', List
165
+ desc 'show', 'Show details about a task or script'
166
+ subcommand 'show', Show
167
+ end
168
+ end
169
+ end
170
+
171
+ # rubocop:enable Metrics/LineLength
@@ -0,0 +1,75 @@
1
+ require 'omnitest/psychic/code2doc/code_helper'
2
+
3
+ module Omnitest
4
+ class Psychic
5
+ module Code2Doc
6
+ module SnippetHelper
7
+ include CodeHelper
8
+
9
+ def file_snippet(file_name, opts = {})
10
+ file = expand_file(file_name)
11
+ snippet_opts = {
12
+ language: (opts[:language] || detect_language(file))
13
+ }.merge(opts)
14
+ content = file_content(file_name, snippet_opts)
15
+ snippetize(content, snippet_opts)
16
+ end
17
+
18
+ def exec_snippet(command, opts = {})
19
+ cwd = opts.delete(:cwd) || '.'
20
+ psychic = Omnitest::Psychic.new(cwd: cwd)
21
+ result = psychic.execute(command)
22
+ snippetize_output(result, opts)
23
+ end
24
+
25
+ def snippetize_output(result, opts)
26
+ include_command = (opts.key?(:include_command) ? opts.delete(:include_command) : true)
27
+ snippet = include_command ? "$ #{result.command}\n" : ''
28
+ snippet << result.stdout
29
+ snippetize(snippet, opts)
30
+ end
31
+
32
+ def snippetize(str, opts)
33
+ language = opts.delete(:language)
34
+ snippet_opts = {
35
+ format: (opts[:format] || :markdown)
36
+ }.merge(opts)
37
+ code_block(str, language, snippet_opts).rstrip
38
+ end
39
+
40
+ private
41
+
42
+ def file_content(file_name, opts = {})
43
+ file = expand_file(file_name)
44
+ content = file.read
45
+ after_pattern, before_pattern = [opts[:after], opts[:before]].map do | pattern |
46
+ case pattern
47
+ when nil
48
+ nil
49
+ when Regexp
50
+ pattern.source
51
+ else
52
+ Regexp.quote(pattern.to_s)
53
+ end
54
+ end
55
+ content = content.gsub(/\A.*#{after_pattern}\s*^(.*)\Z/m, '\1') if after_pattern
56
+ content = content.gsub(/\A(.*)#{before_pattern}.*\Z/m, '\1') if before_pattern
57
+ content.rstrip
58
+ end
59
+
60
+ def detect_language(file)
61
+ file = Pathname(file)
62
+ language, _comment_style = Code2Doc::CommentStyles.infer file.extname
63
+ language
64
+ rescue CommentStyles::UnknownStyleError
65
+ nil
66
+ end
67
+
68
+ def expand_file(file_name)
69
+ file = Pathname(file_name)
70
+ file.expand_path(Omnitest.basedir) unless file.absolute?
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,122 @@
1
+ require 'omnitest/psychic/code2doc/code_segmenter'
2
+ require 'rouge'
3
+
4
+ module Omnitest
5
+ class Psychic
6
+ module Code2Doc
7
+ module CodeHelper
8
+ class ReStructuredTextHelper
9
+ def self.code_block(source, language)
10
+ buffer = StringIO.new
11
+ buffer.puts ".. code-block:: #{language}"
12
+ indented_source = source.lines.map do|line|
13
+ " #{line}"
14
+ end.join("\n")
15
+ buffer.puts indented_source
16
+ buffer.string
17
+ end
18
+ end
19
+ class MarkdownHelper
20
+ def self.code_block(source, language)
21
+ buffer = StringIO.new
22
+ buffer.puts "```#{language}"
23
+ buffer.puts source
24
+ buffer.puts '```'
25
+ buffer.string
26
+ end
27
+ end
28
+
29
+ def source
30
+ File.read absolute_source_file
31
+ end
32
+
33
+ def source?
34
+ !absolute_source_file.nil?
35
+ end
36
+
37
+ def highlighted_code(formatter = 'terminal256')
38
+ language, _comment_style = Code2Doc::CommentStyles.infer source_file.extname
39
+ highlight(source, language: language, filename: absolute_source_file, formatter: formatter)
40
+ end
41
+
42
+ def code_block(source_code, language, opts = { format: :markdown })
43
+ case opts[:format].to_sym
44
+ when :rst
45
+ ReStructuredTextHelper.code_block source_code, language
46
+ when :md, :markdown
47
+ MarkdownHelper.code_block source_code, language
48
+ when :raw
49
+ source_code
50
+ else
51
+ fail ArgumentError, "Unknown format: #{opts[:format]}"
52
+ end
53
+ end
54
+
55
+ # Loses proper indentation on comments
56
+ def snippet_after(matcher)
57
+ segments = segmenter.segment(source)
58
+ buffer = StringIO.new
59
+ segment = segments.find do |s|
60
+ doc_segment_content = s.first.join
61
+ doc_segment_content.match matcher
62
+ end
63
+ buffer.print segment[1].join "\n" if segment # return code segment
64
+ buffer.string
65
+ end
66
+
67
+ def snippet_between(before_matcher, after_matcher)
68
+ segments = segmenter.segment(source)
69
+ start_segment = find_segment_index segments, before_matcher
70
+ end_segment = find_segment_index segments, after_matcher
71
+ buffer = StringIO.new
72
+ if start_segment && end_segment
73
+ segments[start_segment...end_segment].each do |segment|
74
+ buffer.puts @segmenter.comment(segment[0]) unless segment == segments[start_segment]
75
+ buffer.puts segment[1].join
76
+ end
77
+ end
78
+ buffer.string
79
+ end
80
+
81
+ def code2doc(options = { format: :markdown })
82
+ source_code = File.read(absolute_source_file)
83
+ segmenter_language = infer_language(source_file)
84
+
85
+ buffer = StringIO.new
86
+ segmenter_options = {
87
+ language: segmenter_language
88
+ }
89
+ segmenter = Omnitest::Psychic::Code2Doc::CodeSegmenter.new(segmenter_options)
90
+ segments = segmenter.segment source_code
91
+ segments.each do |comment, code|
92
+ comment = comment.join("\n")
93
+ code = code.join("\n")
94
+ code = code_block(code, segmenter_language, options) unless code.empty?
95
+ next if comment.empty? && code.empty?
96
+ code = "\n#{code}\n" if !comment.empty? && !code.empty? # Markdown needs separation
97
+ buffer.puts [comment, code].join("\n")
98
+ end
99
+ buffer.string
100
+ end
101
+
102
+ def infer_language(file)
103
+ language, comment_style = Psychic::Code2Doc::CommentStyles.infer File.extname(file)
104
+ segmenter_language = comment_style[:language] || language
105
+ end
106
+
107
+ private
108
+
109
+ def segmenter
110
+ @segmenter ||= Code2Doc::CodeSegmenter.new
111
+ end
112
+
113
+ def find_segment_index(segments, matcher)
114
+ segments.find_index do |s|
115
+ doc_segment_content = s.first.join
116
+ doc_segment_content.match matcher
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,170 @@
1
+ require 'omnitest/psychic/code2doc/comment_styles'
2
+
3
+ module Omnitest
4
+ class Psychic
5
+ module Code2Doc
6
+ # This class was extracted from the [Rocco](http://rtomayko.github.com/rocco/) project
7
+ # which was in turn based on the [Docco](http://jashkenas.github.com/docco/).
8
+ class CodeSegmenter # rubocop:disable all
9
+ # Cops are disabled because the code is from Rocco
10
+ include CommentStyles
11
+
12
+ DEFAULT_OPTIONS = {
13
+ language: 'rb',
14
+ comment_chars: '#'
15
+ }
16
+
17
+ def initialize(options = {})
18
+ @options = DEFAULT_OPTIONS.merge options
19
+ @options[:comment_chars] = generate_comment_chars if @options[:comment_chars].is_a? String
20
+ end
21
+ # Internal Parsing and Highlighting
22
+ # ---------------------------------
23
+
24
+ # Parse the raw file source_code into a list of two-tuples. Each tuple has the
25
+ # form `[docs, code]` where both elements are arrays containing the
26
+ # raw lines parsed from the input file, comment characters stripped.
27
+ def segment(source_code) # rubocop:disable all
28
+ sections, docs, code = [], [], []
29
+ lines = source_code.split("\n")
30
+
31
+ # The first line is ignored if it is a shebang line. We also ignore the
32
+ # PEP 263 encoding information in python sourcefiles, and the similar ruby
33
+ # 1.9 syntax.
34
+ lines.shift if lines[0] =~ /^\#\!/
35
+ lines.shift if lines[0] =~ /coding[:=]\s*[-\w.]+/ &&
36
+ %w(python rb).include?(@options[:language])
37
+
38
+ # To detect both block comments and single-line comments, we'll set
39
+ # up a tiny state machine, and loop through each line of the file.
40
+ # This requires an `in_comment_block` boolean, and a few regular
41
+ # expressions for line tests. We'll do the same for fake heredoc parsing.
42
+ in_comment_block = false
43
+ in_heredoc = false
44
+ single_line_comment, block_comment_start, block_comment_mid, block_comment_end =
45
+ nil, nil, nil, nil
46
+ unless @options[:comment_chars][:single].nil?
47
+ single_line_comment = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:single])}\\s?")
48
+ end
49
+ unless @options[:comment_chars][:multi].nil?
50
+ block_comment_start = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*$")
51
+ block_comment_end = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$")
52
+ block_comment_one_liner = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*(.*?)\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$") # rubocop:disable Metrics/LineLength
53
+ block_comment_start_with = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*(.*?)$")
54
+ block_comment_end_with = Regexp.new("\\s*(.*?)\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$")
55
+ if @options[:comment_chars][:multi][:middle]
56
+ block_comment_mid = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:middle])}\\s?")
57
+ end
58
+ end
59
+ unless @options[:comment_chars][:heredoc].nil?
60
+ heredoc_start = Regexp.new("#{Regexp.escape(@options[:comment_chars][:heredoc])}(\\S+)$")
61
+ end
62
+ lines.each do |line|
63
+ # If we're currently in a comment block, check whether the line matches
64
+ # the _end_ of a comment block or the _end_ of a comment block with a
65
+ # comment.
66
+ if in_comment_block
67
+ if block_comment_end && line.match(block_comment_end)
68
+ in_comment_block = false
69
+ elsif block_comment_end_with && line.match(block_comment_end_with)
70
+ in_comment_block = false
71
+ docs << line.match(block_comment_end_with).captures.first
72
+ .sub(block_comment_mid || '', '')
73
+ else
74
+ docs << line.sub(block_comment_mid || '', '')
75
+ end
76
+ # If we're currently in a heredoc, we're looking for the end of the
77
+ # heredoc, and everything it contains is code.
78
+ elsif in_heredoc
79
+ if line.match(Regexp.new("^#{Regexp.escape(in_heredoc)}$"))
80
+ in_heredoc = false
81
+ end
82
+ code << line
83
+ # Otherwise, check whether the line starts a heredoc. If so, note the end
84
+ # pattern, and the line is code. Otherwise check whether the line matches
85
+ # the beginning of a block, or a single-line comment all on it's lonesome.
86
+ # In either case, if there's code, start a new section.
87
+ else
88
+ if heredoc_start && line.match(heredoc_start)
89
+ in_heredoc = Regexp.last_match[1]
90
+ code << line
91
+ elsif block_comment_one_liner && line.match(block_comment_one_liner)
92
+ if code.any?
93
+ sections << [docs, code]
94
+ docs, code = [], []
95
+ end
96
+ docs << line.match(block_comment_one_liner).captures.first
97
+ elsif block_comment_start && line.match(block_comment_start)
98
+ in_comment_block = true
99
+ if code.any?
100
+ sections << [docs, code]
101
+ docs, code = [], []
102
+ end
103
+ elsif block_comment_start_with && line.match(block_comment_start_with)
104
+ in_comment_block = true
105
+ if code.any?
106
+ sections << [docs, code]
107
+ docs, code = [], []
108
+ end
109
+ docs << line.match(block_comment_start_with).captures.first
110
+ elsif single_line_comment && line.match(single_line_comment)
111
+ if code.any?
112
+ sections << [docs, code]
113
+ docs, code = [], []
114
+ end
115
+ docs << line.sub(single_line_comment || '', '')
116
+ else
117
+ code << line
118
+ end
119
+ end
120
+ end
121
+ sections << [docs, code] if docs.any? || code.any?
122
+ normalize_leading_spaces(sections)
123
+ end
124
+
125
+ # Normalizes documentation whitespace by checking for leading whitespace,
126
+ # removing it, and then removing the same amount of whitespace from each
127
+ # succeeding line. That is:
128
+ #
129
+ # def func():
130
+ # """
131
+ # Comment 1
132
+ # Comment 2
133
+ # """
134
+ # print "omg!"
135
+ #
136
+ # should yield a comment block of `Comment 1\nComment 2` and code of
137
+ # `def func():\n print "omg!"`
138
+ def normalize_leading_spaces(sections)
139
+ sections.map do |section|
140
+ if section.any? && section[0].any?
141
+ leading_space = section[0][0].match("^\s+")
142
+ if leading_space
143
+ section[0] =
144
+ section[0].map { |line| line.sub(/^#{leading_space.to_s}/, '') }
145
+ end
146
+ end
147
+ section
148
+ end
149
+ end
150
+
151
+ def comment(lines)
152
+ lines.map do | line |
153
+ "#{@options[:comment_chars][:single]} #{line}"
154
+ end.join "\n"
155
+ end
156
+
157
+ private
158
+
159
+ def generate_comment_chars
160
+ @_commentchar ||=
161
+ if COMMENT_STYLES[@options[:language]]
162
+ COMMENT_STYLES[@options[:language]]
163
+ else
164
+ { single: @options[:comment_chars], multi: nil, heredoc: nil }
165
+ end
166
+ end
167
+ end # rubocop:enable all
168
+ end
169
+ end
170
+ end