markdown_exec 2.3.0 → 2.4.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -2
  3. data/CHANGELOG.md +19 -0
  4. data/Gemfile.lock +1 -1
  5. data/Rakefile +32 -8
  6. data/bats/bats.bats +33 -0
  7. data/bats/block-types.bats +56 -0
  8. data/bats/cli.bats +74 -0
  9. data/bats/fail.bats +11 -0
  10. data/bats/history.bats +34 -0
  11. data/bats/markup.bats +66 -0
  12. data/bats/mde.bats +29 -0
  13. data/bats/options.bats +92 -0
  14. data/bats/test_helper.bash +152 -0
  15. data/bin/tab_completion.sh +44 -20
  16. data/docs/dev/block-type-opts.md +10 -0
  17. data/docs/dev/block-type-port.md +24 -0
  18. data/docs/dev/block-type-vars.md +7 -0
  19. data/docs/dev/pass-through-arguments.md +8 -0
  20. data/docs/dev/specs-import.md +9 -0
  21. data/docs/dev/specs.md +83 -0
  22. data/docs/dev/text-decoration.md +7 -0
  23. data/examples/bash-blocks.md +4 -4
  24. data/examples/block-names.md +2 -2
  25. data/examples/import0.md +23 -0
  26. data/examples/import1.md +13 -0
  27. data/examples/link-blocks-vars.md +3 -3
  28. data/examples/opts-blocks-require.md +6 -6
  29. data/examples/table-markup.md +31 -0
  30. data/examples/text-markup.md +58 -0
  31. data/examples/vars-blocks.md +2 -2
  32. data/examples/wrap.md +87 -9
  33. data/lib/ansi_formatter.rb +12 -6
  34. data/lib/ansi_string.rb +153 -0
  35. data/lib/argument_processor.rb +160 -0
  36. data/lib/cached_nested_file_reader.rb +4 -2
  37. data/lib/ce_get_cost_and_usage.rb +4 -3
  38. data/lib/cli.rb +1 -1
  39. data/lib/colorize.rb +39 -11
  40. data/lib/constants.rb +17 -0
  41. data/lib/directory_searcher.rb +4 -2
  42. data/lib/doh.rb +190 -0
  43. data/lib/env.rb +1 -1
  44. data/lib/exceptions.rb +9 -6
  45. data/lib/fcb.rb +0 -199
  46. data/lib/filter.rb +18 -5
  47. data/lib/find_files.rb +8 -3
  48. data/lib/format_table.rb +406 -0
  49. data/lib/hash_delegator.rb +888 -603
  50. data/lib/hierarchy_string.rb +113 -25
  51. data/lib/input_sequencer.rb +16 -10
  52. data/lib/instance_method_wrapper.rb +2 -1
  53. data/lib/layered_hash.rb +143 -0
  54. data/lib/link_history.rb +22 -8
  55. data/lib/markdown_exec/version.rb +1 -1
  56. data/lib/markdown_exec.rb +413 -165
  57. data/lib/mdoc.rb +27 -34
  58. data/lib/menu.src.yml +825 -710
  59. data/lib/menu.yml +799 -703
  60. data/lib/namer.rb +6 -12
  61. data/lib/object_present.rb +1 -1
  62. data/lib/option_value.rb +7 -3
  63. data/lib/poly.rb +33 -14
  64. data/lib/resize_terminal.rb +60 -52
  65. data/lib/saved_assets.rb +45 -34
  66. data/lib/saved_files_matcher.rb +6 -3
  67. data/lib/streams_out.rb +7 -1
  68. data/lib/table_extractor.rb +166 -0
  69. data/lib/tap.rb +5 -6
  70. data/lib/text_analyzer.rb +144 -8
  71. metadata +26 -3
  72. data/lib/std_out_err_logger.rb +0 -119
data/lib/markdown_exec.rb CHANGED
@@ -115,12 +115,14 @@ module MarkdownExec
115
115
 
116
116
  # A class that generates a histogram bar in terminal using xterm-256 color codes.
117
117
  class Histogram
118
- # Generates and prints a histogram bar for a given value within a specified range and width, with an option for inverse display.
118
+ # Generates and prints a histogram bar for a given value within a
119
+ # specified range and width, with an option for inverse display.
119
120
  # @param integer_value [Integer] the value to represent in the histogram
120
121
  # @param min [Integer] the minimum value of the range
121
122
  # @param max [Integer] the maximum value of the range
122
123
  # @param width [Integer] the total width of the histogram in characters
123
- # @param inverse [Boolean] whether the histogram is displayed in inverse order (right to left)
124
+ # @param inverse [Boolean] whether the histogram is
125
+ # displayed in inverse order (right to left)
124
126
  def self.display(integer_value, min, max, width, inverse: false)
125
127
  return if max <= min # Ensure the range is valid
126
128
 
@@ -135,7 +137,7 @@ module MarkdownExec
135
137
 
136
138
  # # Generate the histogram bar using xterm-256 colors (color code 42 is green)
137
139
  # filled_bar = "\e[48;5;42m" + ' ' * filled_length + "\e[0m"
138
- filled_bar = ('¤' * filled_length).fg_rgbh_AF_AF_00
140
+ filled_bar = AnsiString.new('¤' * filled_length).fg_rgbh_AF_AF_00
139
141
  empty_bar = ' ' * (width - filled_length)
140
142
 
141
143
  # Determine the order of filled and empty parts based on the inverse flag
@@ -150,26 +152,34 @@ module MarkdownExec
150
152
  end
151
153
 
152
154
  def build_menu(file_names, directory_names, found_in_block_names,
153
- files_in_directories, vbn)
155
+ file_name_choices, choices_from_block_names)
154
156
  choices = []
155
157
 
156
158
  # Adding section title and data for file names
157
- choices << { disabled: '',
158
- name: "in #{file_names[:section_title]}".send(@chrome_color) }
159
+ choices << {
160
+ disabled: '',
161
+ name: AnsiString.new("in #{file_names[:section_title]}")
162
+ .send(@chrome_color)
163
+ }
159
164
  choices += file_names[:data].map { |str| FileInMenu.for_menu(str) }
160
165
 
161
166
  # Conditionally add directory names if data is present
162
- unless directory_names[:data].count.zero?
163
- choices << { disabled: '',
164
- name: "in #{directory_names[:section_title]}".send(@chrome_color) }
165
- choices += files_in_directories
167
+ if directory_names[:data].any?
168
+ choices << {
169
+ disabled: '',
170
+ name: AnsiString.new("in #{directory_names[:section_title]}")
171
+ .send(@chrome_color)
172
+ }
173
+ choices += file_name_choices
166
174
  end
167
175
 
168
176
  # Adding found in block names
169
- choices << { disabled: '',
170
- name: "in #{found_in_block_names[:section_title]}".send(@chrome_color) }
171
-
172
- choices += vbn
177
+ choices << {
178
+ disabled: '',
179
+ name: AnsiString.new("in #{found_in_block_names[:section_title]}")
180
+ .send(@chrome_color)
181
+ }
182
+ choices += choices_from_block_names
173
183
 
174
184
  choices
175
185
  end
@@ -181,18 +191,22 @@ module MarkdownExec
181
191
  {
182
192
  section_title: 'directory names',
183
193
  data: matched_directories,
184
- formatted_text: [{ content: AnsiFormatter.new(search_options).format_and_highlight_array(
185
- matched_directories, highlight: [highlight_value]
186
- ) }]
194
+ formatted_text: [{ content:
195
+ AnsiFormatter.new(search_options).format_and_highlight_array(
196
+ matched_directories, highlight: [highlight_value]
197
+ ) }]
187
198
  }
188
199
  end
189
200
 
190
201
  def found_in_block_names(search_options, highlight_value,
191
202
  formspec: '=%<index>4.d: %<line>s')
192
- matched_contents = (find_file_contents do |line|
193
- read_block_name(line, search_options[:fenced_start_and_end_regex],
194
- search_options[:block_name_match], search_options[:block_name_nick_match])
195
- end).map.with_index do |(file, contents), index|
203
+ matched_contents = (
204
+ find_file_contents do |line|
205
+ read_block_name(line,
206
+ search_options[:fenced_start_and_end_regex],
207
+ search_options[:block_name_match],
208
+ search_options[:block_name_nick_match])
209
+ end).map.with_index do |(file, contents), index|
196
210
  # [file, contents.map { |detail| format(formspec, detail.index, detail.line) }, index]
197
211
  [file, contents.map do |detail|
198
212
  format(formspec, { index: detail.index, line: detail.line })
@@ -201,13 +215,14 @@ module MarkdownExec
201
215
  {
202
216
  section_title: 'block names',
203
217
  data: matched_contents.map(&:first),
204
- formatted_text: matched_contents.map do |(file, details, index)|
205
- { header: format('- %3.d: %s', index + 1, file),
206
- content: AnsiFormatter.new(search_options).format_and_highlight_array(
207
- details,
208
- highlight: [highlight_value]
209
- ) }
210
- end,
218
+ formatted_text:
219
+ matched_contents.map do |(file, details, index)|
220
+ { header: format('- %3.d: %s', index + 1, file),
221
+ content: AnsiFormatter.new(search_options).format_and_highlight_array(
222
+ details,
223
+ highlight: [highlight_value]
224
+ ) }
225
+ end,
211
226
  matched_contents: matched_contents
212
227
  }
213
228
  end
@@ -217,9 +232,12 @@ module MarkdownExec
217
232
  {
218
233
  section_title: 'file names',
219
234
  data: matched_files,
220
- formatted_text: [{ content: AnsiFormatter.new(search_options).format_and_highlight_array(
221
- matched_files, highlight: [highlight_value]
222
- ).join("\n") }]
235
+ formatted_text:
236
+ [{ content:
237
+ AnsiFormatter.new(search_options).format_and_highlight_array(
238
+ matched_files,
239
+ highlight: [highlight_value]
240
+ ).join("\n") }]
223
241
  }
224
242
  end
225
243
 
@@ -268,20 +286,6 @@ module MarkdownExec
268
286
  )
269
287
  end
270
288
 
271
- # :reek:UtilityFunction
272
- def list_recent_output(saved_stdout_folder, saved_stdout_glob,
273
- list_count)
274
- SavedFilesMatcher.most_recent_list(saved_stdout_folder,
275
- saved_stdout_glob, list_count)
276
- end
277
-
278
- # :reek:UtilityFunction
279
- def list_recent_scripts(saved_script_folder, saved_script_glob,
280
- list_count)
281
- SavedFilesMatcher.most_recent_list(saved_script_folder,
282
- saved_script_glob, list_count)
283
- end
284
-
285
289
  def warn_format(name, message, opts = {})
286
290
  Exceptions.warn_format(
287
291
  "CachedNestedFileReader.#{name} -- #{message}",
@@ -329,22 +333,52 @@ module MarkdownExec
329
333
  }
330
334
  end
331
335
 
336
+ def choices_from_block_names(value, found_in_block_names)
337
+ found_in_block_names[:matched_contents].map do |matched_contents|
338
+ filename, details, = matched_contents
339
+ nexo = AnsiFormatter.new(@options).format_and_highlight_array(
340
+ details,
341
+ highlight: [value]
342
+ )
343
+ [FileInMenu.for_menu(filename)] +
344
+ nexo.map do |str|
345
+ { disabled: '', name: (' ' * 20) + str }
346
+ end
347
+ end.flatten
348
+ end
349
+
350
+ def choices_from_file_names(directory_names)
351
+ directory_names[:data].map do |dn|
352
+ find_files('*', [dn], exclude_dirs: true)
353
+ end.flatten(1).map { |str| FileInMenu.for_menu(str) }
354
+ end
355
+
332
356
  public
333
357
 
334
358
  ## Determines the correct filename to use for searching files
335
359
  #
336
- def determine_filename(specified_filename: nil, specified_folder: nil, default_filename: nil,
337
- default_folder: nil, filetree: nil)
338
- if specified_filename&.present?
339
- return specified_filename if specified_filename.start_with?('/')
340
-
341
- File.join(specified_folder || default_folder, specified_filename)
342
- elsif specified_folder&.present?
343
- File.join(specified_folder,
344
- filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
345
- else
346
- File.join(default_folder, default_filename)
347
- end
360
+ def determine_filename(
361
+ specified_filename: nil, specified_folder: nil,
362
+ default_filename: nil, default_folder: nil, filetree: nil
363
+ )
364
+ File.join(
365
+ *(if specified_filename&.present?
366
+ if specified_filename.start_with?('/')
367
+ [specified_filename]
368
+ else
369
+ [specified_folder || default_folder, specified_filename]
370
+ end
371
+ elsif specified_folder&.present?
372
+ [specified_folder,
373
+ if filetree
374
+ @options[:md_filename_match]
375
+ else
376
+ @options[:md_filename_glob]
377
+ end]
378
+ else
379
+ [default_folder, default_filename]
380
+ end)
381
+ )
348
382
  end
349
383
 
350
384
  private
@@ -356,82 +390,57 @@ module MarkdownExec
356
390
  # raise ArgumentError, error
357
391
  # end
358
392
 
359
- # Reports and executes block logic
360
- def execute_block_logic(files)
361
- @options[:filename] = select_document_if_multiple(files)
362
- @options.document_inpseq
363
- rescue StandardError
364
- error_handler('execute_block_logic')
365
- # rubocop:disable Style/RescueStandardError
366
- rescue
367
- pp $!, $@
368
- exit 1
369
- # rubocop:enable Style/RescueStandardError
370
- end
371
-
372
393
  ## Executes the block specified in the options
373
394
  #
374
395
  def execute_block_with_error_handling
375
396
  finalize_cli_argument_processing
376
- execute_code_block_based_on_options(@options)
397
+ execute_code_block_based_on_options(@options, @options.run_state)
377
398
  rescue FileMissingError
378
399
  warn "File missing: #{$!}"
379
- rescue StandardError
380
- error_handler('execute_block_with_error_handling')
381
400
  end
382
401
 
383
402
  # Main method to execute a block based on options and block_name
384
- def execute_code_block_based_on_options(options)
403
+ def execute_code_block_based_on_options(options, run_state)
385
404
  options = calculated_options.merge(options)
386
405
  update_options(options, over: false)
406
+ # recognize commands with an opt_name, no procname
407
+ return if execute_simple_commands(options)
387
408
 
388
- simple_commands = {
389
- doc_glob: -> { @fout.fout options[:md_filename_glob] },
390
- # list_blocks: -> { list_blocks },
391
- list_default_env: -> { @fout.fout_list list_default_env },
392
- list_default_yaml: -> { @fout.fout_list list_default_yaml },
393
- list_docs: -> { @fout.fout_list files },
394
- list_recent_output: -> {
395
- @fout.fout_list list_recent_output(
396
- @options[:saved_stdout_folder],
397
- @options[:saved_stdout_glob], @options[:list_count]
398
- )
399
- },
400
- list_recent_scripts: -> {
401
- @fout.fout_list list_recent_scripts(
402
- options[:saved_script_folder],
403
- options[:saved_script_glob], options[:list_count]
404
- )
405
- },
406
- pwd: -> { @fout.fout File.expand_path('..', __dir__) },
407
- run_last_script: -> { run_last_script },
408
- tab_completions: -> { @fout.fout tab_completions },
409
- menu_export: -> { @fout.fout menu_export }
410
- }
411
-
412
- return if execute_simple_commands(simple_commands)
413
-
414
- files = opts_prepare_file_list(options)
415
- execute_block_logic(files)
409
+ mde_vux_main_loop(opts_prepare_file_list(options))
416
410
  return unless @options[:output_saved_script_filename]
417
411
 
418
- @fout.fout "script_block_name: #{@options.run_state.script_block_name}"
419
- @fout.fout "s_save_filespec: #{@options.run_state.saved_filespec}"
420
- rescue StandardError
421
- error_handler('execute_code_block_based_on_options')
412
+ @fout.fout "script_block_name: #{run_state.script_block_name}"
413
+ @fout.fout "s_save_filespec: #{run_state.saved_filespec}"
422
414
  end
423
415
 
424
416
  # Executes command based on the provided option keys
425
- def execute_simple_commands(simple_commands)
426
- simple_commands.each_key do |key|
427
- if @options[key]
428
- simple_commands[key].call
417
+ def execute_simple_commands(options)
418
+ simple_commands(options).each do |key, proc|
419
+ if @options[key].is_a?(TrueClass) || @options[key].present?
420
+ proc.call
429
421
  return true
430
422
  end
431
423
  end
432
424
  false
433
425
  end
434
426
 
427
+ # Extracts all lines matching the given regular expression from a file.
428
+ #
429
+ # @param file_path [String] The path to the file to be searched.
430
+ # @param pattern [Regexp] The regular expression pattern to match.
431
+ # @return [Array<String>] An array of lines from the file that match the pattern.
432
+ def extract_lines_matching(file_path, pattern)
433
+ matching_lines = []
434
+
435
+ File.open(file_path, 'r') do |file|
436
+ file.each_line do |line|
437
+ matching_lines << line if line =~ pattern
438
+ end
439
+ end
440
+
441
+ matching_lines
442
+ end
443
+
435
444
  ## post-parse options configuration
436
445
  #
437
446
  def finalize_cli_argument_processing(rest = @rest)
@@ -449,9 +458,6 @@ module MarkdownExec
449
458
  end
450
459
  end
451
460
 
452
- ## position 1: block name (optional)
453
- #
454
- @options[:block_name] = nil
455
461
  @options[:input_cli_rest] = @rest
456
462
  rescue FileMissingError
457
463
  warn_format('finalize_cli_argument_processing',
@@ -474,6 +480,31 @@ module MarkdownExec
474
480
  directory_names = searcher.directory_names(options, value)
475
481
 
476
482
  ### search in file contents (block names, chrome, or text)
483
+ found_report(found_in_block_names, directory_names, file_names)
484
+
485
+ return { exit: true } unless execute_chosen_found
486
+
487
+ file_name_choices = choices_from_file_names(directory_names)
488
+
489
+ unless file_names[:data]&.count.positive? ||
490
+ file_name_choices&.count.positive? ||
491
+ found_in_block_names[:data]&.count.positive?
492
+ return :exit
493
+ end
494
+
495
+ ## pick a document to open
496
+ #
497
+ found_files_build_menu_user_select(
498
+ file_names, directory_names, found_in_block_names,
499
+ file_name_choices,
500
+ choices_from_block_names(value, found_in_block_names)
501
+ )
502
+ { exit: false }
503
+ end
504
+
505
+ def found_report(found_in_block_names,
506
+ directory_names,
507
+ file_names)
477
508
  [found_in_block_names,
478
509
  directory_names,
479
510
  file_names].each do |data|
@@ -486,70 +517,155 @@ module MarkdownExec
486
517
  @fout.fout fi[:content] if fi[:content]
487
518
  end
488
519
  end
489
- return { exit: true } unless execute_chosen_found
490
-
491
- ## pick a document to open
492
- #
493
- files_in_directories = directory_names[:data].map do |dn|
494
- find_files('*', [dn], exclude_dirs: true)
495
- end.flatten(1).map { |str| FileInMenu.for_menu(str) }
496
-
497
- unless file_names[:data]&.count.positive? || files_in_directories&.count.positive? || found_in_block_names[:data]&.count.positive?
498
- return { exit: true }
499
- end
500
-
501
- vbn = found_in_block_names[:matched_contents].map do |matched_contents|
502
- filename, details, = matched_contents
503
- nexo = AnsiFormatter.new(@options).format_and_highlight_array(
504
- details,
505
- highlight: [value]
506
- )
507
- [FileInMenu.for_menu(filename)] +
508
- nexo.map do |str|
509
- { disabled: '', name: (' ' * 20) + str }
510
- end
511
- end.flatten
520
+ end
512
521
 
513
- choices = MenuBuilder.new.build_menu(file_names, directory_names, found_in_block_names,
514
- files_in_directories, vbn)
522
+ def found_files_build_menu_user_select(
523
+ file_names, directory_names, found_in_block_names,
524
+ file_name_choices, choices_from_block_names
525
+ )
526
+ choices = MenuBuilder.new.build_menu(
527
+ file_names, directory_names, found_in_block_names,
528
+ file_name_choices, choices_from_block_names
529
+ )
515
530
 
516
531
  @options[:filename] = FileInMenu.from_menu(
517
532
  select_document_if_multiple(
518
533
  choices,
519
- prompt: options[:prompt_select_md].to_s + ' ¤ Age in months'.fg_rgbh_AF_AF_00
534
+ prompt: options[:prompt_select_md].to_s +
535
+ AnsiString.new(' ¤ Age in months').fg_rgbh_AF_AF_00
520
536
  )
521
537
  )
522
- { exit: false }
538
+ end
539
+
540
+ def fout_list(list)
541
+ if @options[:list_output_format] == :yaml
542
+ @fout.fout list.to_yaml.sub(/^---\n/, '')
543
+ elsif @options[:list_output_format] == :json
544
+ @fout.fout list.to_json
545
+ else # :text
546
+ list.each do |item|
547
+ @fout.fout item
548
+ end
549
+ end
550
+ end
551
+
552
+ def history(probe: true)
553
+ if probe && @options[:probe].present?
554
+ probe_regexp = Regexp.new(@options[:probe], Regexp::IGNORECASE)
555
+ end
556
+
557
+ if @options[:sift].present?
558
+ sift_regexp = Regexp.new(@options[:sift], Regexp::IGNORECASE)
559
+ end
560
+
561
+ files_table_rows = @options.read_saved_assets_for_history_table
562
+
563
+ if sift_regexp
564
+ # Filter history to file names matching a pattern
565
+ files_table_rows.select! { |item| sift_regexp.match(item[:file]) }
566
+ end
567
+
568
+ if probe_regexp
569
+ # Filter history to files with lines matching a pattern
570
+ files_table_rows.each do |item|
571
+ item[:probe] = extract_lines_matching(item[:file], probe_regexp)
572
+ end
573
+ files_table_rows.select! { |item| item[:probe].count.positive? }
574
+ end
575
+
576
+ if files_table_rows.count.zero?
577
+ warn 'Nothing revealed.'
578
+ elsif probe || probe_regexp
579
+ if @options[:dig]
580
+ # Present menu of history
581
+ @options.register_console_attributes(@options)
582
+ @options.execute_history_select(
583
+ files_table_rows,
584
+ exit_prompt: @options[:prompt_exit],
585
+ pause_refresh: true,
586
+ stream: $stderr
587
+ )
588
+ elsif @options[:mine]
589
+ # List lines matched by probe
590
+ files_table_rows.each do |item|
591
+ @fout.fout(item[:file])
592
+ fout_list(item[:probe])
593
+ end
594
+ else
595
+ fout_list(files_table_rows.map(&:file))
596
+ end
597
+ else
598
+ @options.register_console_attributes(@options)
599
+ @options.execute_history_select(
600
+ files_table_rows,
601
+ exit_prompt: @options[:prompt_exit],
602
+ pause_refresh: true,
603
+ stream: $stderr
604
+ )
605
+ end
523
606
  end
524
607
 
525
608
  ## Sets up the options and returns the parsed arguments
526
609
  #
527
- def initialize_and_parse_cli_options
528
- # @options = base_options
610
+ def initialize_parse_execute_cli
529
611
  @options = HashDelegator.new(base_options)
530
-
612
+ @options.p_all_arguments = ARGV.dup # Duplicate ARGV to track original order
613
+ ### should not include after '--'
531
614
  read_configuration_file!(@options,
532
615
  ".#{MarkdownExec::APP_NAME.downcase}.yml")
533
616
 
617
+ options_parsed = []
534
618
  @option_parser = OptionParser.new do |opts|
535
619
  executable_name = File.basename($PROGRAM_NAME)
536
620
  opts.banner = [
537
621
  "#{MarkdownExec::APP_NAME}" \
538
622
  " - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
539
- "Usage: #{executable_name} [(directory | file [block_name] | search_keyword)] [options]"
623
+ "Usage: #{executable_name}" \
624
+ ' [(directory | file [block_name] | search_keyword)] [options]'
540
625
  ].join("\n")
541
626
 
542
627
  menu_iter do |item|
543
- opts_menu_option_append opts, @options, item
628
+ opts_menu_option_append(opts, @options, item, options_parsed)
544
629
  end
545
630
  end
546
631
  @option_parser.load
547
632
  @option_parser.environment
548
- @rest = rest = @option_parser.parse!(arguments_for_mde)
549
- @options.pass_args = ARGV[rest.count + 1..]
633
+ @options.p_params = {}
634
+ @rest = @option_parser.parse!(arguments_for_mde, into: @options.p_params)
635
+ @options.p_options_parsed = options_parsed
636
+ @options.p_rest = @rest.dup
637
+
638
+ # Arguments for script follow ARGV, excluding arguments reserved for MDE, and separator.
639
+ # Parsed options have already been removed from ARGV.
640
+ @options.pass_args = ARGV[@rest.count + 1..]
641
+
550
642
  @options.merge(@options.run_state.to_h)
643
+ end
551
644
 
552
- rest
645
+ def iter_source_blocks(source, &block)
646
+ case source
647
+ when 1
648
+ HashDelegator.new(@options).blocks_from_nested_files.each(&block)
649
+ when 2
650
+ blocks_in_file, menu_blocks, mdoc =
651
+ HashDelegator.new(@options)
652
+ .mdoc_menu_and_blocks_from_nested_files(LinkState.new)
653
+ blocks_in_file.each(&block)
654
+ when 3
655
+ blocks_in_file, menu_blocks, mdoc =
656
+ HashDelegator.new(@options)
657
+ .mdoc_menu_and_blocks_from_nested_files(LinkState.new)
658
+ menu_blocks.each(&block)
659
+ else
660
+ @options.iter_blocks_from_nested_files do |btype, fcb|
661
+ case btype
662
+ when :blocks
663
+ yield fcb
664
+ when :filter
665
+ %i[blocks]
666
+ end
667
+ end
668
+ end
553
669
  end
554
670
 
555
671
  ##
@@ -565,8 +681,9 @@ module MarkdownExec
565
681
  ->(_) { exit }
566
682
  when 'find', 'open'
567
683
  ->(value) {
568
- exit if find_value(value, execute_chosen_found: procname == 'open').fetch(:exit,
569
- false)
684
+ exit if find_value(
685
+ value, execute_chosen_found: procname == 'open'
686
+ ).fetch(:exit, false)
570
687
  }
571
688
  when 'help'
572
689
  ->(_) {
@@ -595,6 +712,8 @@ module MarkdownExec
595
712
  lambda(&:to_i)
596
713
  when 'val_as_str'
597
714
  lambda(&:to_s)
715
+ when 'val_as_sym'
716
+ lambda(&:to_sym)
598
717
  when 'version'
599
718
  lambda { |_|
600
719
  @fout.fout MarkdownExec::VERSION
@@ -605,7 +724,17 @@ module MarkdownExec
605
724
  end
606
725
  end
607
726
 
608
- # def list_blocks; end
727
+ def list_blocks
728
+ message = @options[:list_blocks_message]
729
+ block_eval = @options[:list_blocks_eval]
730
+
731
+ list = []
732
+ iter_source_blocks(@options[:list_blocks_type]) do |block|
733
+ list << (block_eval.present? ? eval(block_eval) : block.send(message))
734
+ end
735
+
736
+ fout_list(list)
737
+ end
609
738
 
610
739
  def list_default_env
611
740
  menu_iter do |item|
@@ -646,6 +775,35 @@ module MarkdownExec
646
775
  @options[:md_filename_glob]))
647
776
  end
648
777
 
778
+ # :reek:UtilityFunction
779
+ def list_recent_output(saved_stdout_folder, saved_stdout_glob,
780
+ list_count)
781
+ SavedFilesMatcher.most_recent_list(saved_stdout_folder,
782
+ saved_stdout_glob, list_count)
783
+ end
784
+
785
+ # :reek:UtilityFunction
786
+ def list_recent_scripts(saved_script_folder, saved_script_glob,
787
+ list_count)
788
+ SavedFilesMatcher.most_recent_list(saved_script_folder,
789
+ saved_script_glob, list_count)
790
+ end
791
+
792
+ # Reports and executes block logic
793
+ def mde_vux_main_loop(files)
794
+ @options[:filename] = select_document_if_multiple(files)
795
+ @options.vux_main_loop do |type, data|
796
+ case type
797
+ when :command_names
798
+ simple_commands(data).keys
799
+ when :call_proc
800
+ simple_commands(data[0])[data[1]].call
801
+ else
802
+ raise
803
+ end
804
+ end
805
+ end
806
+
649
807
  private
650
808
 
651
809
  ##
@@ -675,37 +833,88 @@ module MarkdownExec
675
833
  end.to_yaml
676
834
  end
677
835
 
678
- def opts_menu_option_append(opts, options, item)
836
+ def opts_menu_option_append(opts, options, item, options_parsed)
679
837
  return unless item[:long_name].present? || item[:short_name].present?
680
838
 
681
- opts.on(*[
839
+ optname = "-#{item[:short_name]}"
840
+ switches = [
841
+ # - argument style = :NONE, :REQUIRED, :OPTIONAL
842
+ case item[:procname]&.to_s
843
+ when nil
844
+ :NONE
845
+ when *%w[val_as_bool val_as_int val_as_str val_as_sym]
846
+ :REQUIRED
847
+ else # debug, exit, find, help, how, open, path, show_config, version
848
+ nil
849
+ end,
850
+
682
851
  # - long name
683
852
  if item[:long_name].present?
684
- "--#{item[:long_name]}#{item[:arg_name].present? ? " #{item[:arg_name]}" : ''}"
853
+ optname = "--#{item[:long_name]}"
854
+ "--#{item[:long_name]}" \
855
+ "#{item[:arg_name].present? ? " #{item[:arg_name]}" : ''}"
685
856
  end,
686
857
 
687
858
  # - short name
688
- item[:short_name].present? ? "-#{item[:short_name]}" : nil,
859
+ if item[:short_name].present?
860
+ "-#{item[:short_name]}" \
861
+ "#{item[:arg_name].present? ? " #{item[:arg_name]}" : ''}"
862
+ end,
689
863
 
690
864
  # - description and default
691
- [item[:description],
692
- ("[#{value_for_cli item[:default]}]" if item[:default].present?)].compact.join(' '),
865
+ [
866
+ item[:description],
867
+ ("[#{value_for_cli item[:default]}]" if item[:default].present?)
868
+ ].compact.join(' '),
869
+
870
+ # - type coercion
871
+ case item[:procname]&.to_s
872
+ when nil
873
+ nil
874
+ when 'val_as_bool'
875
+ item[:default] ? FalseClass : TrueClass # use sets to desired value
876
+ when 'val_as_int'
877
+ Integer
878
+ else # str, sym
879
+ # String
880
+ nil
881
+ end,
882
+ # Date – Anything accepted by Date.parse
883
+ # DateTime – Anything accepted by DateTime.parse
884
+ # Time – Anything accepted by Time.httpdate or Time.parse
885
+ # URI – Anything accepted by URI.parse
886
+ # Shellwords – Anything accepted by Shellwords.shellwords
887
+ # String – Any non-empty string
888
+ # Integer – Any integer. Will convert octal. (e.g. 124, -3, 040)
889
+ # Float – Any float. (e.g. 10, 3.14, -100E+13)
890
+ # Numeric – Any integer, float, or rational (1, 3.4, 1/3)
891
+ # DecimalInteger – Like Integer, but no octal format.
892
+ # OctalInteger – Like Integer, but no decimal format.
893
+ # DecimalNumeric – Decimal integer or float.
894
+ # TrueClass – Accepts ‘+, yes, true, -, no, false’ and defaults as true
895
+ # FalseClass – Same as TrueClass, but defaults to false
896
+ # Array – Strings separated by ‘,’ (e.g. 1,2,3)
897
+ # Regexp – Regular expressions. Also includes options.
693
898
 
694
899
  # apply proccode, if present, to value
695
900
  # save value to options hash if option is named
696
901
  #
697
902
  lambda { |value|
903
+ name = item[:long_name]&.present? ? '--' + item[:long_name].to_s : '-' + item[:short_name].to_s
904
+ options_parsed << item.merge(name: name, value: value)
698
905
  (item[:proccode] ? item[:proccode].call(value) : value).tap do |converted|
699
906
  options[item[:opt_name]] = converted if item[:opt_name]
700
907
  end
701
908
  }
702
- ].compact)
909
+ ].compact
910
+ opts.on(*switches)
703
911
  end
704
912
 
705
913
  def opts_prepare_file_list(options)
706
914
  list_files_specified(
707
915
  determine_filename(
708
- specified_filename: options[:filename]&.present? ? options[:filename] : nil,
916
+ specified_filename:
917
+ options[:filename]&.present? ? options[:filename] : nil,
709
918
  specified_folder: options[:path],
710
919
  default_filename: 'README.md',
711
920
  default_folder: '.'
@@ -724,12 +933,21 @@ module MarkdownExec
724
933
  public
725
934
 
726
935
  def run
727
- initialize_and_parse_cli_options
936
+ initialize_parse_execute_cli
728
937
  execute_block_with_error_handling
938
+ rescue BlockMissing
939
+ warn 'Block missing'
940
+ exit 1
941
+ rescue AppInterrupt, TTY::Reader::InputInterrupt, BlockMissing
942
+ warn 'Exiting...' if $DEBUG
943
+ exit 1
729
944
  rescue StandardError
730
945
  error_handler('run')
731
- ensure
732
- yield if block_given?
946
+ # rubocop:disable Style/RescueStandardError
947
+ rescue
948
+ warn 'Exiting...' if $DEBUG
949
+ exit 1
950
+ # rubocop:enable Style/RescueStandardError
733
951
  end
734
952
 
735
953
  private
@@ -741,7 +959,7 @@ module MarkdownExec
741
959
 
742
960
  saved_name_split filename
743
961
  @options[:save_executed_script] = false
744
- @options.document_inpseq
962
+ @options.vux_main_loop
745
963
  rescue StandardError
746
964
  error_handler('run_last_script')
747
965
  end
@@ -763,8 +981,11 @@ module MarkdownExec
763
981
 
764
982
  opts = options.dup
765
983
  select_option_or_exit(
766
- HashDelegator.new(@options).string_send_color(prompt,
767
- :prompt_color_after_script_execution),
984
+ HashDelegator.new(@options)
985
+ .string_send_color(
986
+ prompt,
987
+ :prompt_color_after_script_execution
988
+ ),
768
989
  files,
769
990
  opts.merge(per_page: opts[:select_page_height])
770
991
  )
@@ -777,6 +998,33 @@ module MarkdownExec
777
998
  )&.fetch(:selected)
778
999
  end
779
1000
 
1001
+ def simple_commands(options)
1002
+ {
1003
+ doc_glob: -> { @fout.fout options[:md_filename_glob] },
1004
+ history: -> { history },
1005
+ list_blocks: -> { list_blocks },
1006
+ list_default_env: -> { @fout.fout_list list_default_env },
1007
+ list_default_yaml: -> { @fout.fout_list list_default_yaml },
1008
+ list_docs: -> { @fout.fout_list opts_prepare_file_list(options) },
1009
+ list_recent_output: -> {
1010
+ @fout.fout_list list_recent_output(
1011
+ @options[:saved_stdout_folder],
1012
+ @options[:saved_stdout_glob], @options[:list_count]
1013
+ )
1014
+ },
1015
+ list_recent_scripts: -> {
1016
+ @fout.fout_list list_recent_scripts(
1017
+ options[:saved_script_folder],
1018
+ options[:saved_script_glob], options[:list_count]
1019
+ )
1020
+ },
1021
+ menu_export: -> { @fout.fout menu_export },
1022
+ pwd: -> { @fout.fout File.expand_path('..', __dir__) },
1023
+ run_last_script: -> { run_last_script },
1024
+ tab_completions: -> { @fout.fout tab_completions }
1025
+ }
1026
+ end
1027
+
780
1028
  public
781
1029
 
782
1030
  def tab_completions(data = menu_for_optparse)