markdown_exec 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +106 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +7 -1
- data/README.md +25 -27
- data/Rakefile +18 -6
- data/assets/approve_code.png +0 -0
- data/assets/example_blocks.png +0 -0
- data/assets/output_of_execution.png +0 -0
- data/assets/select_a_block.png +0 -0
- data/assets/select_a_file.png +0 -0
- data/fixtures/bash1.md +12 -0
- data/fixtures/bash2.md +15 -0
- data/fixtures/exclude1.md +6 -0
- data/fixtures/exclude2.md +9 -0
- data/fixtures/exec1.md +8 -0
- data/fixtures/heading1.md +19 -0
- data/fixtures/sample1.md +9 -0
- data/fixtures/title1.md +6 -0
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +410 -258
- metadata +19 -10
- data/assets/approve.png +0 -0
- data/assets/blocks.png +0 -0
- data/assets/executed.png +0 -0
- data/assets/select.png +0 -0
- data/assets/select_file.png +0 -0
    
        data/lib/markdown_exec.rb
    CHANGED
    
    | @@ -3,20 +3,42 @@ | |
| 3 3 |  | 
| 4 4 | 
             
            # encoding=utf-8
         | 
| 5 5 |  | 
| 6 | 
            -
            $pdebug = !(ENV['MARKDOWN_EXEC_DEBUG'] || '').empty?
         | 
| 7 | 
            -
             | 
| 8 6 | 
             
            require 'open3'
         | 
| 9 7 | 
             
            require 'optparse'
         | 
| 10 8 | 
             
            require 'tty-prompt'
         | 
| 11 9 | 
             
            require 'yaml'
         | 
| 12 10 |  | 
| 11 | 
            +
            ##
         | 
| 12 | 
            +
            # default if nil
         | 
| 13 | 
            +
            # false if empty or '0'
         | 
| 14 | 
            +
            # else true
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            def env_bool(name, default: false)
         | 
| 17 | 
            +
              return default if (val = ENV[name]).nil?
         | 
| 18 | 
            +
              return false if val.empty? || val == '0'
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              true
         | 
| 21 | 
            +
            end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            def env_int(name, default: 0)
         | 
| 24 | 
            +
              return default if (val = ENV[name]).nil?
         | 
| 25 | 
            +
              return default if val.empty?
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              val.to_i
         | 
| 28 | 
            +
            end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            def env_str(name, default: '')
         | 
| 31 | 
            +
              ENV[name] || default
         | 
| 32 | 
            +
            end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            $pdebug = env_bool 'MDE_DEBUG'
         | 
| 35 | 
            +
             | 
| 13 36 | 
             
            require_relative 'markdown_exec/version'
         | 
| 14 37 |  | 
| 15 38 | 
             
            $stderr.sync = true
         | 
| 16 39 | 
             
            $stdout.sync = true
         | 
| 17 40 |  | 
| 18 41 | 
             
            BLOCK_SIZE = 1024
         | 
| 19 | 
            -
            SELECT_PAGE_HEIGHT = 12
         | 
| 20 42 |  | 
| 21 43 | 
             
            class Object # rubocop:disable Style/Documentation
         | 
| 22 44 | 
             
              def present?
         | 
| @@ -31,6 +53,30 @@ class String # rubocop:disable Style/Documentation | |
| 31 53 | 
             
              end
         | 
| 32 54 | 
             
            end
         | 
| 33 55 |  | 
| 56 | 
            +
            public
         | 
| 57 | 
            +
             | 
| 58 | 
            +
            # debug output
         | 
| 59 | 
            +
            #
         | 
| 60 | 
            +
            def tap_inspect(format: nil, name: 'return')
         | 
| 61 | 
            +
              return self unless $pdebug
         | 
| 62 | 
            +
             | 
| 63 | 
            +
              fn = case format
         | 
| 64 | 
            +
                   when :json
         | 
| 65 | 
            +
                     :to_json
         | 
| 66 | 
            +
                   when :string
         | 
| 67 | 
            +
                     :to_s
         | 
| 68 | 
            +
                   when :yaml
         | 
| 69 | 
            +
                     :to_yaml
         | 
| 70 | 
            +
                   else
         | 
| 71 | 
            +
                     :inspect
         | 
| 72 | 
            +
                   end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
              puts "-> #{caller[0].scan(/in `?(\S+)'$/)[0][0]}()" \
         | 
| 75 | 
            +
                   " #{name}: #{method(fn).call}"
         | 
| 76 | 
            +
             | 
| 77 | 
            +
              self
         | 
| 78 | 
            +
            end
         | 
| 79 | 
            +
             | 
| 34 80 | 
             
            module MarkdownExec
         | 
| 35 81 | 
             
              class Error < StandardError; end
         | 
| 36 82 |  | 
| @@ -41,6 +87,55 @@ module MarkdownExec | |
| 41 87 |  | 
| 42 88 | 
             
                def initialize(options = {})
         | 
| 43 89 | 
             
                  @options = options
         | 
| 90 | 
            +
                  @prompt = TTY::Prompt.new(interrupt: :exit)
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                ##
         | 
| 94 | 
            +
                # options necessary to start, parse input, defaults for cli options
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                def base_options
         | 
| 97 | 
            +
                  {
         | 
| 98 | 
            +
                    # commands
         | 
| 99 | 
            +
                    list_blocks: false, # command
         | 
| 100 | 
            +
                    list_docs: false, # command
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                    # command options
         | 
| 103 | 
            +
                    filename: env_str('MDE_FILENAME', default: nil), # option Filename to open
         | 
| 104 | 
            +
                    output_execution_summary: env_bool('MDE_OUTPUT_EXECUTION_SUMMARY', default: false), # option
         | 
| 105 | 
            +
                    output_script: env_bool('MDE_OUTPUT_SCRIPT', default: false), # option
         | 
| 106 | 
            +
                    output_stdout: env_bool('MDE_OUTPUT_STDOUT', default: true), # option
         | 
| 107 | 
            +
                    path: env_str('MDE_PATH', default: nil), # option Folder to search for files
         | 
| 108 | 
            +
                    save_executed_script: env_bool('MDE_SAVE_EXECUTED_SCRIPT', default: false), # option
         | 
| 109 | 
            +
                    saved_script_folder: env_str('MDE_SAVED_SCRIPT_FOLDER', default: 'logs'), # option
         | 
| 110 | 
            +
                    user_must_approve: env_bool('MDE_USER_MUST_APPROVE', default: true), # option Pause for user to approve script
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                    # configuration options
         | 
| 113 | 
            +
                    block_name_excluded_match: env_str('MDE_BLOCK_NAME_EXCLUDED_MATCH', default: '^\(.+\)$'),
         | 
| 114 | 
            +
                    block_name_match: env_str('MDE_BLOCK_NAME_MATCH', default: ':(?<title>\S+)( |$)'),
         | 
| 115 | 
            +
                    block_required_scan: env_str('MDE_BLOCK_REQUIRED_SCAN', default: '\+\S+'),
         | 
| 116 | 
            +
                    fenced_start_and_end_match: env_str('MDE_FENCED_START_AND_END_MATCH', default: '^`{3,}'),
         | 
| 117 | 
            +
                    fenced_start_ex_match: env_str('MDE_FENCED_START_EX_MATCH', default: '^`{3,}(?<shell>[^`\s]*) *(?<name>.*)$'),
         | 
| 118 | 
            +
                    heading1_match: env_str('MDE_HEADING1_MATCH', default: '^# *(?<name>[^#]*?) *$'),
         | 
| 119 | 
            +
                    heading2_match: env_str('MDE_HEADING2_MATCH', default: '^## *(?<name>[^#]*?) *$'),
         | 
| 120 | 
            +
                    heading3_match: env_str('MDE_HEADING3_MATCH', default: '^### *(?<name>.+?) *$'),
         | 
| 121 | 
            +
                    md_filename_glob: env_str('MDE_MD_FILENAME_GLOB', default: '*.[Mm][Dd]'),
         | 
| 122 | 
            +
                    md_filename_match: env_str('MDE_MD_FILENAME_MATCH', default: '.+\\.md'),
         | 
| 123 | 
            +
                    mdheadings: true, # use headings (levels 1,2,3) in block lable
         | 
| 124 | 
            +
                    select_page_height: env_int('MDE_SELECT_PAGE_HEIGHT', default: 12)
         | 
| 125 | 
            +
                  }
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                def default_options
         | 
| 129 | 
            +
                  {
         | 
| 130 | 
            +
                    bash: true, # bash block parsing in get_block_summary()
         | 
| 131 | 
            +
                    exclude_expect_blocks: true,
         | 
| 132 | 
            +
                    exclude_matching_block_names: true, # exclude hidden blocks
         | 
| 133 | 
            +
                    output_saved_script_filename: false,
         | 
| 134 | 
            +
                    prompt_select_block: 'Choose a block:', # in select_and_approve_block()
         | 
| 135 | 
            +
                    prompt_select_md: 'Choose a file:', # in select_md_file()
         | 
| 136 | 
            +
                    saved_script_filename: nil, # calculated
         | 
| 137 | 
            +
                    struct: true # allow get_block_summary()
         | 
| 138 | 
            +
                  }
         | 
| 44 139 | 
             
                end
         | 
| 45 140 |  | 
| 46 141 | 
             
                # Returns true if all files are EOF
         | 
| @@ -49,16 +144,130 @@ module MarkdownExec | |
| 49 144 | 
             
                  files.find { |f| !f.eof }.nil?
         | 
| 50 145 | 
             
                end
         | 
| 51 146 |  | 
| 147 | 
            +
                def approve_block(opts, blocks_in_file)
         | 
| 148 | 
            +
                  required_blocks = list_recursively_required_blocks(blocks_in_file, opts[:block_name])
         | 
| 149 | 
            +
                  display_command(opts, required_blocks) if opts[:output_script] || opts[:user_must_approve]
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                  allow = true
         | 
| 152 | 
            +
                  allow = @prompt.yes? 'Process?' if opts[:user_must_approve]
         | 
| 153 | 
            +
                  opts[:ir_approve] = allow
         | 
| 154 | 
            +
                  selected = get_block_by_name blocks_in_file, opts[:block_name]
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                  if opts[:ir_approve]
         | 
| 157 | 
            +
                    write_command_file(opts, required_blocks) if opts[:save_executed_script]
         | 
| 158 | 
            +
                    command_execute opts, required_blocks.flatten.join("\n")
         | 
| 159 | 
            +
                  end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                  selected[:name]
         | 
| 162 | 
            +
                end
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                def code(table, block)
         | 
| 165 | 
            +
                  all = [block[:name]] + recursively_required(table, block[:reqs])
         | 
| 166 | 
            +
                  all.reverse.map do |req|
         | 
| 167 | 
            +
                    get_block_by_name(table, req).fetch(:body, '')
         | 
| 168 | 
            +
                  end
         | 
| 169 | 
            +
                     .flatten(1)
         | 
| 170 | 
            +
                     .tap_inspect
         | 
| 171 | 
            +
                end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                def command_execute(opts, cmd2)
         | 
| 174 | 
            +
                  @execute_options = opts
         | 
| 175 | 
            +
                  @execute_started_at = Time.now.utc
         | 
| 176 | 
            +
                  Open3.popen3(cmd2) do |stdin, stdout, stderr|
         | 
| 177 | 
            +
                    stdin.close_write
         | 
| 178 | 
            +
                    begin
         | 
| 179 | 
            +
                      files = [stdout, stderr]
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                      until all_at_eof(files)
         | 
| 182 | 
            +
                        ready = IO.select(files)
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                        next unless ready
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                        # readable = ready[0]
         | 
| 187 | 
            +
                        # # writable = ready[1]
         | 
| 188 | 
            +
                        # # exceptions = ready[2]
         | 
| 189 | 
            +
                        @execute_files = Hash.new([])
         | 
| 190 | 
            +
                        ready.each.with_index do |readable, ind|
         | 
| 191 | 
            +
                          readable.each do |f|
         | 
| 192 | 
            +
                            block = f.read_nonblock(BLOCK_SIZE)
         | 
| 193 | 
            +
                            @execute_files[ind] = @execute_files[ind] + [block]
         | 
| 194 | 
            +
                            print block if opts[:output_stdout]
         | 
| 195 | 
            +
                          rescue EOFError #=> e
         | 
| 196 | 
            +
                            # do nothing at EOF
         | 
| 197 | 
            +
                          end
         | 
| 198 | 
            +
                        end
         | 
| 199 | 
            +
                      end
         | 
| 200 | 
            +
                    rescue IOError => e
         | 
| 201 | 
            +
                      fout "IOError: #{e}"
         | 
| 202 | 
            +
                    end
         | 
| 203 | 
            +
                    @execute_completed_at = Time.now.utc
         | 
| 204 | 
            +
                  end
         | 
| 205 | 
            +
                rescue Errno::ENOENT => e
         | 
| 206 | 
            +
                  @execute_aborted_at = Time.now.utc
         | 
| 207 | 
            +
                  @execute_error_message = e.message
         | 
| 208 | 
            +
                  @execute_error = e
         | 
| 209 | 
            +
                  fout "Error ENOENT: #{e.inspect}"
         | 
| 210 | 
            +
                end
         | 
| 211 | 
            +
             | 
| 52 212 | 
             
                def count_blocks_in_filename
         | 
| 213 | 
            +
                  fenced_start_and_end_match = Regexp.new @options[:fenced_start_and_end_match]
         | 
| 53 214 | 
             
                  cnt = 0
         | 
| 54 | 
            -
                  File.readlines(options[:filename]).each do |line|
         | 
| 55 | 
            -
                    cnt += 1 if line.match( | 
| 215 | 
            +
                  File.readlines(@options[:filename]).each do |line|
         | 
| 216 | 
            +
                    cnt += 1 if line.match(fenced_start_and_end_match)
         | 
| 56 217 | 
             
                  end
         | 
| 57 218 | 
             
                  cnt / 2
         | 
| 58 219 | 
             
                end
         | 
| 59 220 |  | 
| 221 | 
            +
                def display_command(_opts, required_blocks)
         | 
| 222 | 
            +
                  required_blocks.each { |cb| fout cb }
         | 
| 223 | 
            +
                end
         | 
| 224 | 
            +
             | 
| 225 | 
            +
                def exec_block(options, block_name = '')
         | 
| 226 | 
            +
                  options = default_options.merge options
         | 
| 227 | 
            +
                  update_options options, over: false
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                  # document and block reports
         | 
| 230 | 
            +
                  #
         | 
| 231 | 
            +
                  files = list_files_per_options(options)
         | 
| 232 | 
            +
                  if @options[:list_docs]
         | 
| 233 | 
            +
                    fout_list files
         | 
| 234 | 
            +
                    return
         | 
| 235 | 
            +
                  end
         | 
| 236 | 
            +
             | 
| 237 | 
            +
                  if @options[:list_blocks]
         | 
| 238 | 
            +
                    fout_list (files.map do |file|
         | 
| 239 | 
            +
                                 make_block_labels(filename: file, struct: true)
         | 
| 240 | 
            +
                               end).flatten(1)
         | 
| 241 | 
            +
                    return
         | 
| 242 | 
            +
                  end
         | 
| 243 | 
            +
             | 
| 244 | 
            +
                  # process
         | 
| 245 | 
            +
                  #
         | 
| 246 | 
            +
                  select_and_approve_block(
         | 
| 247 | 
            +
                    bash: true,
         | 
| 248 | 
            +
                    block_name: block_name,
         | 
| 249 | 
            +
                    filename: select_md_file(files),
         | 
| 250 | 
            +
                    struct: true
         | 
| 251 | 
            +
                  )
         | 
| 252 | 
            +
             | 
| 253 | 
            +
                  fout "saved_filespec: #{@execute_script_filespec}" if @options[:output_saved_script_filename]
         | 
| 254 | 
            +
             | 
| 255 | 
            +
                  output_execution_summary if @options[:output_execution_summary]
         | 
| 256 | 
            +
                end
         | 
| 257 | 
            +
             | 
| 258 | 
            +
                # standard output; not for debug
         | 
| 259 | 
            +
                #
         | 
| 60 260 | 
             
                def fout(str)
         | 
| 61 | 
            -
                  puts str | 
| 261 | 
            +
                  puts str
         | 
| 262 | 
            +
                end
         | 
| 263 | 
            +
             | 
| 264 | 
            +
                def fout_list(str)
         | 
| 265 | 
            +
                  puts str
         | 
| 266 | 
            +
                end
         | 
| 267 | 
            +
             | 
| 268 | 
            +
                def fout_section(name, data)
         | 
| 269 | 
            +
                  puts "# #{name}"
         | 
| 270 | 
            +
                  puts data.to_yaml
         | 
| 62 271 | 
             
                end
         | 
| 63 272 |  | 
| 64 273 | 
             
                def get_block_by_name(table, name, default = {})
         | 
| @@ -70,11 +279,11 @@ module MarkdownExec | |
| 70 279 |  | 
| 71 280 | 
             
                  return [summarize_block(headings, block_title).merge({ body: current })] unless opts[:bash]
         | 
| 72 281 |  | 
| 73 | 
            -
                  bm = block_title.match( | 
| 74 | 
            -
                  reqs = block_title.scan( | 
| 282 | 
            +
                  bm = block_title.match(Regexp.new(opts[:block_name_match]))
         | 
| 283 | 
            +
                  reqs = block_title.scan(Regexp.new(opts[:block_required_scan])).map { |s| s[1..] }
         | 
| 75 284 |  | 
| 76 285 | 
             
                  if bm && bm[1]
         | 
| 77 | 
            -
                    [summarize_block(headings, bm[ | 
| 286 | 
            +
                    [summarize_block(headings, bm[:title]).merge({ body: current, reqs: reqs })]
         | 
| 78 287 | 
             
                  else
         | 
| 79 288 | 
             
                    [summarize_block(headings, block_title).merge({ body: current, reqs: reqs })]
         | 
| 80 289 | 
             
                  end
         | 
| @@ -88,136 +297,120 @@ module MarkdownExec | |
| 88 297 | 
             
                    exit 1
         | 
| 89 298 | 
             
                  end
         | 
| 90 299 |  | 
| 300 | 
            +
                  fenced_start_and_end_match = Regexp.new opts[:fenced_start_and_end_match]
         | 
| 301 | 
            +
                  fenced_start_ex = Regexp.new opts[:fenced_start_ex_match]
         | 
| 302 | 
            +
                  block_title = ''
         | 
| 91 303 | 
             
                  blocks = []
         | 
| 92 304 | 
             
                  current = nil
         | 
| 93 | 
            -
                  in_block = false
         | 
| 94 | 
            -
                  block_title = ''
         | 
| 95 | 
            -
             | 
| 96 305 | 
             
                  headings = []
         | 
| 306 | 
            +
                  in_block = false
         | 
| 97 307 | 
             
                  File.readlines(opts[:filename]).each do |line|
         | 
| 98 308 | 
             
                    continue unless line
         | 
| 99 309 |  | 
| 100 310 | 
             
                    if opts[:mdheadings]
         | 
| 101 | 
            -
                      if (lm = line.match( | 
| 102 | 
            -
                        headings = [headings[0], headings[1], lm[ | 
| 103 | 
            -
                      elsif (lm = line.match( | 
| 104 | 
            -
                        headings = [headings[0], lm[ | 
| 105 | 
            -
                      elsif (lm = line.match( | 
| 106 | 
            -
                        headings = [lm[ | 
| 311 | 
            +
                      if (lm = line.match(Regexp.new(opts[:heading3_match])))
         | 
| 312 | 
            +
                        headings = [headings[0], headings[1], lm[:name]]
         | 
| 313 | 
            +
                      elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
         | 
| 314 | 
            +
                        headings = [headings[0], lm[:name]]
         | 
| 315 | 
            +
                      elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
         | 
| 316 | 
            +
                        headings = [lm[:name]]
         | 
| 107 317 | 
             
                      end
         | 
| 108 318 | 
             
                    end
         | 
| 109 319 |  | 
| 110 | 
            -
                    if line.match( | 
| 320 | 
            +
                    if line.match(fenced_start_and_end_match)
         | 
| 111 321 | 
             
                      if in_block
         | 
| 112 322 | 
             
                        if current
         | 
| 113 | 
            -
             | 
| 114 323 | 
             
                          block_title = current.join(' ').gsub(/  +/, ' ')[0..64] if block_title.nil? || block_title.empty?
         | 
| 115 | 
            -
             | 
| 116 324 | 
             
                          blocks += get_block_summary opts, headings, block_title, current
         | 
| 117 325 | 
             
                          current = nil
         | 
| 118 326 | 
             
                        end
         | 
| 119 327 | 
             
                        in_block = false
         | 
| 120 328 | 
             
                        block_title = ''
         | 
| 121 329 | 
             
                      else
         | 
| 122 | 
            -
                         | 
| 330 | 
            +
                        # new block
         | 
| 123 331 | 
             
                        #
         | 
| 124 | 
            -
             | 
| 125 | 
            -
                        lm = line.match(/^`{3,}([^`\s]*) *(.*)$/)
         | 
| 332 | 
            +
                        lm = line.match(fenced_start_ex)
         | 
| 126 333 | 
             
                        do1 = false
         | 
| 127 334 | 
             
                        if opts[:bash_only]
         | 
| 128 | 
            -
                          do1 = true if lm && (lm[ | 
| 335 | 
            +
                          do1 = true if lm && (lm[:shell] == 'bash')
         | 
| 129 336 | 
             
                        else
         | 
| 130 337 | 
             
                          do1 = true
         | 
| 131 | 
            -
                          do1 = !(lm && (lm[ | 
| 132 | 
            -
             | 
| 133 | 
            -
                          #             if do1 && opts[:exclude_matching_block_names]
         | 
| 134 | 
            -
                          # puts " MW a4"
         | 
| 135 | 
            -
                          # puts " MW a4 #{(lm[2].match %r{^:\(.+\)$})}"
         | 
| 136 | 
            -
                          #               do1 = !(lm && (lm[2].match %r{^:\(.+\)$}))
         | 
| 137 | 
            -
                          #             end
         | 
| 338 | 
            +
                          do1 = !(lm && (lm[:shell] == 'expect')) if opts[:exclude_expect_blocks]
         | 
| 138 339 | 
             
                        end
         | 
| 139 340 |  | 
| 140 341 | 
             
                        in_block = true
         | 
| 141 | 
            -
                        if do1 && (!opts[:title_match] || (lm && lm[ | 
| 342 | 
            +
                        if do1 && (!opts[:title_match] || (lm && lm[:name] && lm[:name].match(opts[:title_match])))
         | 
| 142 343 | 
             
                          current = []
         | 
| 143 | 
            -
                          block_title = (lm && lm[ | 
| 344 | 
            +
                          block_title = (lm && lm[:name])
         | 
| 144 345 | 
             
                        end
         | 
| 145 346 | 
             
                      end
         | 
| 146 347 | 
             
                    elsif current
         | 
| 147 348 | 
             
                      current += [line.chomp]
         | 
| 148 349 | 
             
                    end
         | 
| 149 350 | 
             
                  end
         | 
| 150 | 
            -
                  blocks. | 
| 151 | 
            -
                  # blocks.map do |block|
         | 
| 152 | 
            -
                  #   next if opts[:exclude_matching_block_names] && block[:name].match(%r{^\(.+\)$})
         | 
| 153 | 
            -
                  #   block
         | 
| 154 | 
            -
                  # end.compact.tap { |ret| puts "list_blocks_in_file() ret: #{ret.inspect}" if $pdebug }
         | 
| 351 | 
            +
                  blocks.tap_inspect
         | 
| 155 352 | 
             
                end
         | 
| 156 353 |  | 
| 157 354 | 
             
                def list_files_per_options(options)
         | 
| 158 355 | 
             
                  default_filename = 'README.md'
         | 
| 159 356 | 
             
                  default_folder = '.'
         | 
| 160 357 | 
             
                  if options[:filename]&.present?
         | 
| 161 | 
            -
                    list_files_specified(options[:filename], options[: | 
| 358 | 
            +
                    list_files_specified(options[:filename], options[:path], default_filename, default_folder)
         | 
| 162 359 | 
             
                  else
         | 
| 163 | 
            -
                    list_files_specified(nil, options[: | 
| 164 | 
            -
                  end
         | 
| 360 | 
            +
                    list_files_specified(nil, options[:path], default_filename, default_folder)
         | 
| 361 | 
            +
                  end.tap_inspect
         | 
| 165 362 | 
             
                end
         | 
| 166 363 |  | 
| 167 364 | 
             
                def list_files_specified(specified_filename, specified_folder, default_filename, default_folder, filetree = nil)
         | 
| 168 | 
            -
                  fn = if specified_filename&.present?
         | 
| 169 | 
            -
             | 
| 170 | 
            -
             | 
| 171 | 
            -
             | 
| 172 | 
            -
             | 
| 173 | 
            -
             | 
| 174 | 
            -
             | 
| 175 | 
            -
             | 
| 176 | 
            -
             | 
| 177 | 
            -
             | 
| 178 | 
            -
             | 
| 179 | 
            -
             | 
| 180 | 
            -
             | 
| 181 | 
            -
             | 
| 182 | 
            -
             | 
| 365 | 
            +
                  fn = File.join(if specified_filename&.present?
         | 
| 366 | 
            +
                                   if specified_folder&.present?
         | 
| 367 | 
            +
                                     [specified_folder, specified_filename]
         | 
| 368 | 
            +
                                   elsif specified_filename.start_with? '/'
         | 
| 369 | 
            +
                                     [specified_filename]
         | 
| 370 | 
            +
                                   else
         | 
| 371 | 
            +
                                     [default_folder, specified_filename]
         | 
| 372 | 
            +
                                   end
         | 
| 373 | 
            +
                                 elsif specified_folder&.present?
         | 
| 374 | 
            +
                                   if filetree
         | 
| 375 | 
            +
                                     [specified_folder, @options[:md_filename_match]]
         | 
| 376 | 
            +
                                   else
         | 
| 377 | 
            +
                                     [specified_folder, @options[:md_filename_glob]]
         | 
| 378 | 
            +
                                   end
         | 
| 379 | 
            +
                                 else
         | 
| 380 | 
            +
                                   [default_folder, default_filename]
         | 
| 381 | 
            +
                                 end)
         | 
| 183 382 | 
             
                  if filetree
         | 
| 184 383 | 
             
                    filetree.select { |filename| filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$}) }
         | 
| 185 384 | 
             
                  else
         | 
| 186 385 | 
             
                    Dir.glob(fn)
         | 
| 187 | 
            -
                  end. | 
| 386 | 
            +
                  end.tap_inspect
         | 
| 188 387 | 
             
                end
         | 
| 189 388 |  | 
| 190 | 
            -
                def  | 
| 191 | 
            -
                  Dir.glob(File.join(options[: | 
| 389 | 
            +
                def list_markdown_files_in_path
         | 
| 390 | 
            +
                  Dir.glob(File.join(@options[:path], @options[:md_filename_glob])).tap_inspect
         | 
| 192 391 | 
             
                end
         | 
| 193 392 |  | 
| 194 393 | 
             
                def list_named_blocks_in_file(call_options = {}, &options_block)
         | 
| 195 394 | 
             
                  opts = optsmerge call_options, options_block
         | 
| 395 | 
            +
                  block_name_excluded_match = Regexp.new opts[:block_name_excluded_match]
         | 
| 196 396 | 
             
                  list_blocks_in_file(opts).map do |block|
         | 
| 197 | 
            -
                    next if opts[:exclude_matching_block_names] && block[:name].match( | 
| 397 | 
            +
                    next if opts[:exclude_matching_block_names] && block[:name].match(block_name_excluded_match)
         | 
| 198 398 |  | 
| 199 399 | 
             
                    block
         | 
| 200 | 
            -
                  end.compact. | 
| 201 | 
            -
                end
         | 
| 202 | 
            -
             | 
| 203 | 
            -
                def code(table, block)
         | 
| 204 | 
            -
                  all = [block[:name]] + recursively_required(table, block[:reqs])
         | 
| 205 | 
            -
                  all.reverse.map do |req|
         | 
| 206 | 
            -
                    get_block_by_name(table, req).fetch(:body, '')
         | 
| 207 | 
            -
                  end
         | 
| 208 | 
            -
                     .flatten(1)
         | 
| 209 | 
            -
                     .tap { |ret| puts "code() ret: #{ret.inspect}" if $pdebug }
         | 
| 400 | 
            +
                  end.compact.tap_inspect
         | 
| 210 401 | 
             
                end
         | 
| 211 402 |  | 
| 212 403 | 
             
                def list_recursively_required_blocks(table, name)
         | 
| 213 404 | 
             
                  name_block = get_block_by_name(table, name)
         | 
| 405 | 
            +
                  raise "Named code block `#{name}` not found." if name_block.nil? || name_block.keys.empty?
         | 
| 406 | 
            +
             | 
| 214 407 | 
             
                  all = [name_block[:name]] + recursively_required(table, name_block[:reqs])
         | 
| 215 408 |  | 
| 216 409 | 
             
                  # in order of appearance in document
         | 
| 217 410 | 
             
                  table.select { |block| all.include? block[:name] }
         | 
| 218 411 | 
             
                       .map { |block| block.fetch(:body, '') }
         | 
| 219 412 | 
             
                       .flatten(1)
         | 
| 220 | 
            -
                       . | 
| 413 | 
            +
                       .tap_inspect
         | 
| 221 414 | 
             
                end
         | 
| 222 415 |  | 
| 223 416 | 
             
                def make_block_label(block, call_options = {})
         | 
| @@ -236,34 +429,47 @@ module MarkdownExec | |
| 236 429 | 
             
                    # next if opts[:exclude_matching_block_names] && block[:name].match(%r{^:\(.+\)$})
         | 
| 237 430 |  | 
| 238 431 | 
             
                    make_block_label block, opts
         | 
| 239 | 
            -
                  end.compact. | 
| 432 | 
            +
                  end.compact.tap_inspect
         | 
| 240 433 | 
             
                end
         | 
| 241 434 |  | 
| 242 435 | 
             
                def option_exclude_blocks(opts, blocks)
         | 
| 436 | 
            +
                  block_name_excluded_match = Regexp.new opts[:block_name_excluded_match]
         | 
| 243 437 | 
             
                  if opts[:exclude_matching_block_names]
         | 
| 244 | 
            -
                    blocks.reject { |block| block[:name].match( | 
| 438 | 
            +
                    blocks.reject { |block| block[:name].match(block_name_excluded_match) }
         | 
| 245 439 | 
             
                  else
         | 
| 246 440 | 
             
                    blocks
         | 
| 247 441 | 
             
                  end
         | 
| 248 442 | 
             
                end
         | 
| 249 443 |  | 
| 250 444 | 
             
                def optsmerge(call_options = {}, options_block = nil)
         | 
| 251 | 
            -
                  class_call_options = options.merge(call_options || {})
         | 
| 445 | 
            +
                  class_call_options = @options.merge(call_options || {})
         | 
| 252 446 | 
             
                  if options_block
         | 
| 253 447 | 
             
                    options_block.call class_call_options
         | 
| 254 448 | 
             
                  else
         | 
| 255 449 | 
             
                    class_call_options
         | 
| 256 | 
            -
                  end. | 
| 450 | 
            +
                  end.tap_inspect
         | 
| 451 | 
            +
                end
         | 
| 452 | 
            +
             | 
| 453 | 
            +
                def output_execution_summary
         | 
| 454 | 
            +
                  fout_section 'summary', {
         | 
| 455 | 
            +
                    execute_aborted_at: @execute_aborted_at,
         | 
| 456 | 
            +
                    execute_completed_at: @execute_completed_at,
         | 
| 457 | 
            +
                    execute_error: @execute_error,
         | 
| 458 | 
            +
                    execute_error_message: @execute_error_message,
         | 
| 459 | 
            +
                    execute_files: @execute_files,
         | 
| 460 | 
            +
                    execute_options: @execute_options,
         | 
| 461 | 
            +
                    execute_started_at: @execute_started_at,
         | 
| 462 | 
            +
                    execute_script_filespec: @execute_script_filespec
         | 
| 463 | 
            +
                  }
         | 
| 257 464 | 
             
                end
         | 
| 258 465 |  | 
| 259 466 | 
             
                def read_configuration_file!(options, configuration_path)
         | 
| 260 | 
            -
                   | 
| 261 | 
            -
             | 
| 262 | 
            -
             | 
| 263 | 
            -
             | 
| 264 | 
            -
                     | 
| 265 | 
            -
                   | 
| 266 | 
            -
                  options
         | 
| 467 | 
            +
                  return unless File.exist?(configuration_path)
         | 
| 468 | 
            +
             | 
| 469 | 
            +
                  # rubocop:disable Security/YAMLLoad
         | 
| 470 | 
            +
                  options.merge!((YAML.load(File.open(configuration_path)) || {})
         | 
| 471 | 
            +
                    .transform_keys(&:to_sym))
         | 
| 472 | 
            +
                  # rubocop:enable Security/YAMLLoad
         | 
| 267 473 | 
             
                end
         | 
| 268 474 |  | 
| 269 475 | 
             
                def recursively_required(table, reqs)
         | 
| @@ -278,231 +484,177 @@ module MarkdownExec | |
| 278 484 | 
             
                    end
         | 
| 279 485 | 
             
                             .compact
         | 
| 280 486 | 
             
                             .flatten(1)
         | 
| 281 | 
            -
                             . | 
| 487 | 
            +
                             .tap_inspect(name: 'rem')
         | 
| 282 488 | 
             
                  end
         | 
| 283 | 
            -
                  all. | 
| 489 | 
            +
                  all.tap_inspect
         | 
| 284 490 | 
             
                end
         | 
| 285 491 |  | 
| 286 492 | 
             
                def run
         | 
| 287 493 | 
             
                  ## default configuration
         | 
| 288 494 | 
             
                  #
         | 
| 289 | 
            -
                  options =  | 
| 290 | 
            -
                    mdheadings: true,
         | 
| 291 | 
            -
                    list_blocks: false,
         | 
| 292 | 
            -
                    list_docs: false
         | 
| 293 | 
            -
                  }
         | 
| 495 | 
            +
                  @options = base_options
         | 
| 294 496 |  | 
| 295 497 | 
             
                  ## post-parse options configuration
         | 
| 296 498 | 
             
                  #
         | 
| 297 499 | 
             
                  options_finalize = ->(_options) {}
         | 
| 298 500 |  | 
| 501 | 
            +
                  proc_self = ->(value) { value }
         | 
| 502 | 
            +
                  proc_to_i = ->(value) { value.to_i != 0 }
         | 
| 503 | 
            +
                  proc_true = ->(_) { true }
         | 
| 504 | 
            +
             | 
| 299 505 | 
             
                  # read local configuration file
         | 
| 300 506 | 
             
                  #
         | 
| 301 | 
            -
                  read_configuration_file! options, ".#{MarkdownExec::APP_NAME.downcase}.yml"
         | 
| 507 | 
            +
                  read_configuration_file! @options, ".#{MarkdownExec::APP_NAME.downcase}.yml"
         | 
| 302 508 |  | 
| 303 509 | 
             
                  option_parser = OptionParser.new do |opts|
         | 
| 304 510 | 
             
                    executable_name = File.basename($PROGRAM_NAME)
         | 
| 305 511 | 
             
                    opts.banner = [
         | 
| 306 | 
            -
                      "#{MarkdownExec::APP_NAME}  | 
| 307 | 
            -
                      " | 
| 512 | 
            +
                      "#{MarkdownExec::APP_NAME}" \
         | 
| 513 | 
            +
                      " - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
         | 
| 514 | 
            +
                      "Usage: #{executable_name} [path] [filename] [options]"
         | 
| 308 515 | 
             
                    ].join("\n")
         | 
| 309 516 |  | 
| 310 | 
            -
                     | 
| 311 | 
            -
             | 
| 312 | 
            -
             | 
| 313 | 
            -
                       | 
| 314 | 
            -
             | 
| 315 | 
            -
             | 
| 316 | 
            -
             | 
| 317 | 
            -
                     | 
| 318 | 
            -
             | 
| 319 | 
            -
             | 
| 320 | 
            -
             | 
| 321 | 
            -
             | 
| 322 | 
            -
             | 
| 323 | 
            -
             | 
| 324 | 
            -
             | 
| 325 | 
            -
             | 
| 326 | 
            -
             | 
| 327 | 
            -
             | 
| 328 | 
            -
             | 
| 329 | 
            -
             | 
| 330 | 
            -
             | 
| 331 | 
            -
             | 
| 332 | 
            -
             | 
| 333 | 
            -
             | 
| 334 | 
            -
             | 
| 335 | 
            -
             | 
| 336 | 
            -
             | 
| 337 | 
            -
             | 
| 338 | 
            -
             | 
| 339 | 
            -
             | 
| 340 | 
            -
             | 
| 341 | 
            -
                     | 
| 342 | 
            -
                       | 
| 343 | 
            -
             | 
| 344 | 
            -
             | 
| 345 | 
            -
             | 
| 346 | 
            -
             | 
| 347 | 
            -
             | 
| 348 | 
            -
             | 
| 349 | 
            -
             | 
| 350 | 
            -
                     | 
| 351 | 
            -
             | 
| 352 | 
            -
             | 
| 517 | 
            +
                    summary_head = [
         | 
| 518 | 
            +
                      ['config', nil, nil, 'PATH', 'Read configuration file',
         | 
| 519 | 
            +
                       nil, ->(value) { read_configuration_file! options, value }],
         | 
| 520 | 
            +
                      ['debug', 'd', 'MDE_DEBUG', 'BOOL', 'Debug output',
         | 
| 521 | 
            +
                       nil, ->(value) { $pdebug = value.to_i != 0 }]
         | 
| 522 | 
            +
                    ]
         | 
| 523 | 
            +
             | 
| 524 | 
            +
                    summary_body = [
         | 
| 525 | 
            +
                      ['filename', 'f', 'MDE_FILENAME', 'RELATIVE', 'Name of document',
         | 
| 526 | 
            +
                       :filename, proc_self],
         | 
| 527 | 
            +
                      ['list-blocks', nil, nil, nil, 'List blocks',
         | 
| 528 | 
            +
                       :list_blocks, proc_true],
         | 
| 529 | 
            +
                      ['list-docs', nil, nil, nil, 'List docs in current folder',
         | 
| 530 | 
            +
                       :list_docs, proc_true],
         | 
| 531 | 
            +
                      ['output-execution-summary', nil, 'MDE_OUTPUT_EXECUTION_SUMMARY', 'BOOL', 'Display summary for execution',
         | 
| 532 | 
            +
                       :output_execution_summary, proc_to_i],
         | 
| 533 | 
            +
                      ['output-script', nil, 'MDE_OUTPUT_SCRIPT', 'BOOL', 'Display script',
         | 
| 534 | 
            +
                       :output_script, proc_to_i],
         | 
| 535 | 
            +
                      ['output-stdout', nil, 'MDE_OUTPUT_STDOUT', 'BOOL', 'Display standard output from execution',
         | 
| 536 | 
            +
                       :output_stdout, proc_to_i],
         | 
| 537 | 
            +
                      ['path', 'p', 'MDE_PATH', 'PATH', 'Path to documents',
         | 
| 538 | 
            +
                       :path, proc_self],
         | 
| 539 | 
            +
                      ['save-executed-script', nil, 'MDE_SAVE_EXECUTED_SCRIPT', 'BOOL', 'Save executed script',
         | 
| 540 | 
            +
                       :save_executed_script, proc_to_i],
         | 
| 541 | 
            +
                      ['saved-script-folder', nil, 'MDE_SAVED_SCRIPT_FOLDER', 'SPEC', 'Saved script folder',
         | 
| 542 | 
            +
                       :saved_script_folder, proc_self],
         | 
| 543 | 
            +
                      ['user-must-approve', nil, 'MDE_USER_MUST_APPROVE', 'BOOL', 'Pause to approve execution',
         | 
| 544 | 
            +
                       :user_must_approve, proc_to_i]
         | 
| 545 | 
            +
                    ]
         | 
| 546 | 
            +
             | 
| 547 | 
            +
                    # rubocop:disable Style/Semicolon
         | 
| 548 | 
            +
                    summary_tail = [
         | 
| 549 | 
            +
                      [nil, '0', nil, nil, 'Show configuration',
         | 
| 550 | 
            +
                       nil, ->(_) { options_finalize.call options; fout options.to_yaml }],
         | 
| 551 | 
            +
                      ['help', 'h', nil, nil, 'App help',
         | 
| 552 | 
            +
                       nil, ->(_) { fout option_parser.help; exit }],
         | 
| 553 | 
            +
                      ['version', 'v', nil, nil, 'App version',
         | 
| 554 | 
            +
                       nil, ->(_) { fout MarkdownExec::VERSION; exit }],
         | 
| 555 | 
            +
                      ['exit', 'x', nil, nil, 'Exit app',
         | 
| 556 | 
            +
                       nil, ->(_) { exit }]
         | 
| 557 | 
            +
                    ]
         | 
| 558 | 
            +
                    # rubocop:enable Style/Semicolon
         | 
| 559 | 
            +
             | 
| 560 | 
            +
                    (summary_head + summary_body + summary_tail)
         | 
| 561 | 
            +
                      .map do |long_name, short_name, env_var, arg_name, description, opt_name, proc1| # rubocop:disable Metrics/ParameterLists
         | 
| 562 | 
            +
                      opts.on(*[long_name.present? ? "--#{long_name}#{arg_name.present? ? (' ' + arg_name) : ''}" : nil,
         | 
| 563 | 
            +
                                short_name.present? ? "-#{short_name}" : nil,
         | 
| 564 | 
            +
                                [description,
         | 
| 565 | 
            +
                                 env_var.present? ? "env: #{env_var}" : nil].compact.join(' - '),
         | 
| 566 | 
            +
                                lambda { |value|
         | 
| 567 | 
            +
                                  ret = proc1.call(value)
         | 
| 568 | 
            +
                                  options[opt_name] = ret if opt_name
         | 
| 569 | 
            +
                                  ret
         | 
| 570 | 
            +
                                }].compact)
         | 
| 353 571 | 
             
                    end
         | 
| 354 572 | 
             
                  end
         | 
| 355 573 | 
             
                  option_parser.load # filename defaults to basename of the program without suffix in a directory ~/.options
         | 
| 356 574 | 
             
                  option_parser.environment # env defaults to the basename of the program.
         | 
| 357 575 | 
             
                  rest = option_parser.parse! # (into: options)
         | 
| 358 | 
            -
                  options_finalize.call options
         | 
| 359 576 |  | 
| 360 | 
            -
                   | 
| 361 | 
            -
                    if Dir.exist?(rest[0])
         | 
| 362 | 
            -
                      options[:folder] = rest[0]
         | 
| 363 | 
            -
                    elsif File.exist?(rest[0])
         | 
| 364 | 
            -
                      options[:filename] = rest[0]
         | 
| 365 | 
            -
                    end
         | 
| 366 | 
            -
                  end
         | 
| 367 | 
            -
             | 
| 368 | 
            -
                  ## process
         | 
| 577 | 
            +
                  ## finalize configuration
         | 
| 369 578 | 
             
                  #
         | 
| 370 | 
            -
                  options | 
| 371 | 
            -
                    {
         | 
| 372 | 
            -
                      approve: true,
         | 
| 373 | 
            -
                      bash: true,
         | 
| 374 | 
            -
                      display: true,
         | 
| 375 | 
            -
                      exclude_expect_blocks: true,
         | 
| 376 | 
            -
                      exclude_matching_block_names: true,
         | 
| 377 | 
            -
                      execute: true,
         | 
| 378 | 
            -
                      prompt: 'Execute',
         | 
| 379 | 
            -
                      struct: true
         | 
| 380 | 
            -
                    }
         | 
| 381 | 
            -
                  )
         | 
| 382 | 
            -
                  mp = MarkParse.new options
         | 
| 579 | 
            +
                  options_finalize.call options
         | 
| 383 580 |  | 
| 384 | 
            -
                  ##  | 
| 581 | 
            +
                  ## position 0: file or folder (optional)
         | 
| 385 582 | 
             
                  #
         | 
| 386 | 
            -
                  if  | 
| 387 | 
            -
                     | 
| 388 | 
            -
             | 
| 389 | 
            -
             | 
| 390 | 
            -
             | 
| 391 | 
            -
             | 
| 392 | 
            -
             | 
| 393 | 
            -
                            mp.make_block_labels(filename: file, struct: true)
         | 
| 394 | 
            -
                          end).flatten(1)
         | 
| 395 | 
            -
                    return
         | 
| 396 | 
            -
                  end
         | 
| 397 | 
            -
             | 
| 398 | 
            -
                  mp.select_block(
         | 
| 399 | 
            -
                    bash: true,
         | 
| 400 | 
            -
                    filename: select_md_file(list_files_per_options(options)),
         | 
| 401 | 
            -
                    struct: true
         | 
| 402 | 
            -
                  )
         | 
| 403 | 
            -
                end
         | 
| 404 | 
            -
             | 
| 405 | 
            -
                def select_block(call_options = {}, &options_block)
         | 
| 406 | 
            -
                  opts = optsmerge call_options, options_block
         | 
| 407 | 
            -
             | 
| 408 | 
            -
                  blocks = list_blocks_in_file(opts.merge(struct: true))
         | 
| 409 | 
            -
             | 
| 410 | 
            -
                  prompt = TTY::Prompt.new(interrupt: :exit)
         | 
| 411 | 
            -
                  pt = "#{opts.fetch(:prompt, nil) || 'Pick one'}:"
         | 
| 412 | 
            -
             | 
| 413 | 
            -
                  # blocks.map do |block|
         | 
| 414 | 
            -
                  #   next if opts[:exclude_matching_block_names] && block[:name].match(%r{^\(.+\)$})
         | 
| 415 | 
            -
                  #   block
         | 
| 416 | 
            -
                  # end.compact.tap { |ret| puts "list_blocks_in_file() ret: #{ret.inspect}" if $pdebug }
         | 
| 417 | 
            -
             | 
| 418 | 
            -
                  blocks.each { |block| block.merge! label: make_block_label(block, opts) }
         | 
| 419 | 
            -
                  # block_labels = blocks.map { |block| block[:label] }
         | 
| 420 | 
            -
                  block_labels = option_exclude_blocks(opts, blocks).map { |block| block[:label] }
         | 
| 421 | 
            -
             | 
| 422 | 
            -
                  if opts[:preview_options]
         | 
| 423 | 
            -
                    select_per_page = 3
         | 
| 424 | 
            -
                    block_labels.each do |bn|
         | 
| 425 | 
            -
                      fout " - #{bn}"
         | 
| 583 | 
            +
                  if (pos = rest.fetch(0, nil))&.present?
         | 
| 584 | 
            +
                    if Dir.exist?(pos)
         | 
| 585 | 
            +
                      options[:path] = pos
         | 
| 586 | 
            +
                    elsif File.exist?(pos)
         | 
| 587 | 
            +
                      options[:filename] = pos
         | 
| 588 | 
            +
                    else
         | 
| 589 | 
            +
                      raise "Invalid parameter: #{pos}"
         | 
| 426 590 | 
             
                    end
         | 
| 427 | 
            -
                  else
         | 
| 428 | 
            -
                    select_per_page = SELECT_PAGE_HEIGHT
         | 
| 429 591 | 
             
                  end
         | 
| 430 592 |  | 
| 431 | 
            -
                   | 
| 432 | 
            -
             | 
| 433 | 
            -
                  sel = prompt.select(pt, block_labels, per_page: select_per_page)
         | 
| 434 | 
            -
             | 
| 435 | 
            -
                  label_block = blocks.select { |block| block[:label] == sel }.fetch(0, nil)
         | 
| 436 | 
            -
                  sel = label_block[:name]
         | 
| 437 | 
            -
             | 
| 438 | 
            -
                  cbs = list_recursively_required_blocks(blocks, sel)
         | 
| 439 | 
            -
             | 
| 440 | 
            -
                  ## display code blocks for approval
         | 
| 593 | 
            +
                  ## position 1: block name (optional)
         | 
| 441 594 | 
             
                  #
         | 
| 442 | 
            -
                   | 
| 595 | 
            +
                  block_name = rest.fetch(1, nil)
         | 
| 443 596 |  | 
| 444 | 
            -
                   | 
| 445 | 
            -
             | 
| 446 | 
            -
             | 
| 447 | 
            -
                  selected = get_block_by_name blocks, sel
         | 
| 448 | 
            -
                  if allow && opts[:execute]
         | 
| 449 | 
            -
             | 
| 450 | 
            -
                    cmd2 = cbs.flatten.join("\n")
         | 
| 451 | 
            -
             | 
| 452 | 
            -
                    Open3.popen3(cmd2) do |stdin, stdout, stderr|
         | 
| 453 | 
            -
                      stdin.close_write
         | 
| 454 | 
            -
                      begin
         | 
| 455 | 
            -
                        files = [stdout, stderr]
         | 
| 597 | 
            +
                  exec_block options, block_name
         | 
| 598 | 
            +
                end
         | 
| 456 599 |  | 
| 457 | 
            -
             | 
| 458 | 
            -
             | 
| 600 | 
            +
                def select_and_approve_block(call_options = {}, &options_block)
         | 
| 601 | 
            +
                  opts = optsmerge call_options, options_block
         | 
| 602 | 
            +
                  blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
         | 
| 459 603 |  | 
| 460 | 
            -
             | 
| 604 | 
            +
                  unless opts[:block_name].present?
         | 
| 605 | 
            +
                    pt = (opts[:prompt_select_block]).to_s
         | 
| 606 | 
            +
                    blocks_in_file.each { |block| block.merge! label: make_block_label(block, opts) }
         | 
| 607 | 
            +
                    block_labels = option_exclude_blocks(opts, blocks_in_file).map { |block| block[:label] }
         | 
| 461 608 |  | 
| 462 | 
            -
             | 
| 463 | 
            -
                          # writable = ready[1]
         | 
| 464 | 
            -
                          # exceptions = ready[2]
         | 
| 609 | 
            +
                    return nil if block_labels.count.zero?
         | 
| 465 610 |  | 
| 466 | 
            -
             | 
| 467 | 
            -
             | 
| 468 | 
            -
             | 
| 469 | 
            -
                            # do nothing at EOF
         | 
| 470 | 
            -
                          end
         | 
| 471 | 
            -
                        end
         | 
| 472 | 
            -
                      rescue IOError => e
         | 
| 473 | 
            -
                        fout "IOError: #{e}"
         | 
| 474 | 
            -
                      end
         | 
| 475 | 
            -
                    end
         | 
| 611 | 
            +
                    sel = @prompt.select(pt, block_labels, per_page: opts[:select_page_height])
         | 
| 612 | 
            +
                    label_block = blocks_in_file.select { |block| block[:label] == sel }.fetch(0, nil)
         | 
| 613 | 
            +
                    opts[:block_name] = label_block[:name]
         | 
| 476 614 | 
             
                  end
         | 
| 477 615 |  | 
| 478 | 
            -
                   | 
| 616 | 
            +
                  approve_block opts, blocks_in_file
         | 
| 479 617 | 
             
                end
         | 
| 480 618 |  | 
| 481 619 | 
             
                def select_md_file(files_ = nil)
         | 
| 482 620 | 
             
                  opts = options
         | 
| 483 | 
            -
                  files = files_ ||  | 
| 621 | 
            +
                  files = files_ || list_markdown_files_in_path
         | 
| 484 622 | 
             
                  if files.count == 1
         | 
| 485 | 
            -
                     | 
| 623 | 
            +
                    files[0]
         | 
| 486 624 | 
             
                  elsif files.count >= 2
         | 
| 487 | 
            -
             | 
| 488 | 
            -
                    if opts[:preview_options]
         | 
| 489 | 
            -
                      select_per_page = 3
         | 
| 490 | 
            -
                      files.each do |file|
         | 
| 491 | 
            -
                        fout " - #{file}"
         | 
| 492 | 
            -
                      end
         | 
| 493 | 
            -
                    else
         | 
| 494 | 
            -
                      select_per_page = SELECT_PAGE_HEIGHT
         | 
| 495 | 
            -
                    end
         | 
| 496 | 
            -
             | 
| 497 | 
            -
                    prompt = TTY::Prompt.new
         | 
| 498 | 
            -
                    sel = prompt.select("#{opts.fetch(:prompt, 'Pick one')}:", files, per_page: select_per_page)
         | 
| 625 | 
            +
                    @prompt.select(opts[:prompt_select_md].to_s, files, per_page: opts[:select_page_height])
         | 
| 499 626 | 
             
                  end
         | 
| 500 | 
            -
             | 
| 501 | 
            -
                  sel
         | 
| 502 627 | 
             
                end
         | 
| 503 628 |  | 
| 504 629 | 
             
                def summarize_block(headings, title)
         | 
| 505 630 | 
             
                  { headings: headings, name: title, title: title }
         | 
| 506 631 | 
             
                end
         | 
| 632 | 
            +
             | 
| 633 | 
            +
                def update_options(opts = {}, over: true)
         | 
| 634 | 
            +
                  if over
         | 
| 635 | 
            +
                    @options = @options.merge opts
         | 
| 636 | 
            +
                  else
         | 
| 637 | 
            +
                    @options.merge! opts
         | 
| 638 | 
            +
                  end
         | 
| 639 | 
            +
                  @options
         | 
| 640 | 
            +
                end
         | 
| 641 | 
            +
             | 
| 642 | 
            +
                def write_command_file(opts, required_blocks)
         | 
| 643 | 
            +
                  return unless opts[:saved_script_filename].present?
         | 
| 644 | 
            +
             | 
| 645 | 
            +
                  fne = File.basename(opts[:filename], '.*').gsub(/[^a-z0-9]/i, '-') # scan(/[a-z0-9]/i).join
         | 
| 646 | 
            +
                  bne = opts[:block_name].gsub(/[^a-z0-9]/i, '-') # scan(/[a-z0-9]/i).join
         | 
| 647 | 
            +
                  opts[:saved_script_filename] = "mde_#{Time.now.utc.strftime '%F-%H-%M-%S'}_#{fne}_#{bne}.sh"
         | 
| 648 | 
            +
             | 
| 649 | 
            +
                  @options[:saved_filespec] = File.join opts[:saved_script_folder], opts[:saved_script_filename]
         | 
| 650 | 
            +
                  @execute_script_filespec = @options[:saved_filespec]
         | 
| 651 | 
            +
                  dirname = File.dirname(@options[:saved_filespec])
         | 
| 652 | 
            +
                  Dir.mkdir dirname unless File.exist?(dirname)
         | 
| 653 | 
            +
                  File.write(@options[:saved_filespec], "#!/usr/bin/env bash\n" \
         | 
| 654 | 
            +
                                                        "# file_name: #{opts[:filename]}\n" \
         | 
| 655 | 
            +
                                                        "# block_name: #{opts[:block_name]}\n" \
         | 
| 656 | 
            +
                                                        "# time: #{Time.now.utc}\n" \
         | 
| 657 | 
            +
                                                        "#{required_blocks.flatten.join("\n")}\n")
         | 
| 658 | 
            +
                end
         | 
| 507 659 | 
             
              end
         | 
| 508 660 | 
             
            end
         |