markdown_exec 2.2.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 +16 -4
  3. data/CHANGELOG.md +28 -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 +40 -5
  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 +41 -0
  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 +939 -611
  50. data/lib/hierarchy_string.rb +221 -0
  51. data/lib/input_sequencer.rb +19 -11
  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 +420 -165
  57. data/lib/mdoc.rb +38 -38
  58. data/lib/menu.src.yml +832 -680
  59. data/lib/menu.yml +814 -689
  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 +236 -0
  71. metadata +28 -3
  72. data/lib/std_out_err_logger.rb +0 -119
data/lib/markdown_exec.rb CHANGED
@@ -115,24 +115,29 @@ 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
 
127
129
  # Normalize the value within the range 0 to 1
128
- normalized_value = [0, [(integer_value - min).to_f / (max - min), 1].min].max
130
+ normalized_value = [
131
+ 0,
132
+ [(integer_value - min).to_f / (max - min), 1].min
133
+ ].max
129
134
 
130
135
  # Calculate how many characters should be filled
131
136
  filled_length = (normalized_value * width).round
132
137
 
133
138
  # # Generate the histogram bar using xterm-256 colors (color code 42 is green)
134
139
  # filled_bar = "\e[48;5;42m" + ' ' * filled_length + "\e[0m"
135
- filled_bar = ('¤' * filled_length).fg_rgbh_AF_AF_00
140
+ filled_bar = AnsiString.new('¤' * filled_length).fg_rgbh_AF_AF_00
136
141
  empty_bar = ' ' * (width - filled_length)
137
142
 
138
143
  # Determine the order of filled and empty parts based on the inverse flag
@@ -146,25 +151,35 @@ module MarkdownExec
146
151
  @o_color = :red
147
152
  end
148
153
 
149
- def build_menu(file_names, directory_names, found_in_block_names, files_in_directories, vbn)
154
+ def build_menu(file_names, directory_names, found_in_block_names,
155
+ file_name_choices, choices_from_block_names)
150
156
  choices = []
151
157
 
152
158
  # Adding section title and data for file names
153
- choices << { disabled: '', 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
+ }
154
164
  choices += file_names[:data].map { |str| FileInMenu.for_menu(str) }
155
165
 
156
166
  # Conditionally add directory names if data is present
157
- unless directory_names[:data].count.zero?
158
- choices << { disabled: '',
159
- name: "in #{directory_names[:section_title]}".send(@chrome_color) }
160
- 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
161
174
  end
162
175
 
163
176
  # Adding found in block names
164
- choices << { disabled: '',
165
- name: "in #{found_in_block_names[:section_title]}".send(@chrome_color) }
166
-
167
- 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
168
183
 
169
184
  choices
170
185
  end
@@ -176,18 +191,22 @@ module MarkdownExec
176
191
  {
177
192
  section_title: 'directory names',
178
193
  data: matched_directories,
179
- formatted_text: [{ content: AnsiFormatter.new(search_options).format_and_highlight_array(
180
- matched_directories, highlight: [highlight_value]
181
- ) }]
194
+ formatted_text: [{ content:
195
+ AnsiFormatter.new(search_options).format_and_highlight_array(
196
+ matched_directories, highlight: [highlight_value]
197
+ ) }]
182
198
  }
183
199
  end
184
200
 
185
201
  def found_in_block_names(search_options, highlight_value,
186
202
  formspec: '=%<index>4.d: %<line>s')
187
- matched_contents = (find_file_contents do |line|
188
- read_block_name(line, search_options[:fenced_start_and_end_regex],
189
- search_options[:block_name_match], search_options[:block_name_nick_match])
190
- 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|
191
210
  # [file, contents.map { |detail| format(formspec, detail.index, detail.line) }, index]
192
211
  [file, contents.map do |detail|
193
212
  format(formspec, { index: detail.index, line: detail.line })
@@ -196,13 +215,14 @@ module MarkdownExec
196
215
  {
197
216
  section_title: 'block names',
198
217
  data: matched_contents.map(&:first),
199
- formatted_text: matched_contents.map do |(file, details, index)|
200
- { header: format('- %3.d: %s', index + 1, file),
201
- content: AnsiFormatter.new(search_options).format_and_highlight_array(
202
- details,
203
- highlight: [highlight_value]
204
- ) }
205
- 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,
206
226
  matched_contents: matched_contents
207
227
  }
208
228
  end
@@ -212,9 +232,12 @@ module MarkdownExec
212
232
  {
213
233
  section_title: 'file names',
214
234
  data: matched_files,
215
- formatted_text: [{ content: AnsiFormatter.new(search_options).format_and_highlight_array(
216
- matched_files, highlight: [highlight_value]
217
- ).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") }]
218
241
  }
219
242
  end
220
243
 
@@ -263,20 +286,6 @@ module MarkdownExec
263
286
  )
264
287
  end
265
288
 
266
- # :reek:UtilityFunction
267
- def list_recent_output(saved_stdout_folder, saved_stdout_glob,
268
- list_count)
269
- SavedFilesMatcher.most_recent_list(saved_stdout_folder,
270
- saved_stdout_glob, list_count)
271
- end
272
-
273
- # :reek:UtilityFunction
274
- def list_recent_scripts(saved_script_folder, saved_script_glob,
275
- list_count)
276
- SavedFilesMatcher.most_recent_list(saved_script_folder,
277
- saved_script_glob, list_count)
278
- end
279
-
280
289
  def warn_format(name, message, opts = {})
281
290
  Exceptions.warn_format(
282
291
  "CachedNestedFileReader.#{name} -- #{message}",
@@ -324,22 +333,52 @@ module MarkdownExec
324
333
  }
325
334
  end
326
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
+
327
356
  public
328
357
 
329
358
  ## Determines the correct filename to use for searching files
330
359
  #
331
- def determine_filename(specified_filename: nil, specified_folder: nil, default_filename: nil,
332
- default_folder: nil, filetree: nil)
333
- if specified_filename&.present?
334
- return specified_filename if specified_filename.start_with?('/')
335
-
336
- File.join(specified_folder || default_folder, specified_filename)
337
- elsif specified_folder&.present?
338
- File.join(specified_folder,
339
- filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
340
- else
341
- File.join(default_folder, default_filename)
342
- 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
+ )
343
382
  end
344
383
 
345
384
  private
@@ -351,82 +390,57 @@ module MarkdownExec
351
390
  # raise ArgumentError, error
352
391
  # end
353
392
 
354
- # Reports and executes block logic
355
- def execute_block_logic(files)
356
- @options[:filename] = select_document_if_multiple(files)
357
- @options.document_inpseq
358
- rescue StandardError
359
- error_handler('execute_block_logic')
360
- # rubocop:disable Style/RescueStandardError
361
- rescue
362
- pp $!, $@
363
- exit 1
364
- # rubocop:enable Style/RescueStandardError
365
- end
366
-
367
393
  ## Executes the block specified in the options
368
394
  #
369
395
  def execute_block_with_error_handling
370
396
  finalize_cli_argument_processing
371
- execute_code_block_based_on_options(@options)
397
+ execute_code_block_based_on_options(@options, @options.run_state)
372
398
  rescue FileMissingError
373
399
  warn "File missing: #{$!}"
374
- rescue StandardError
375
- error_handler('execute_block_with_error_handling')
376
400
  end
377
401
 
378
402
  # Main method to execute a block based on options and block_name
379
- def execute_code_block_based_on_options(options)
403
+ def execute_code_block_based_on_options(options, run_state)
380
404
  options = calculated_options.merge(options)
381
405
  update_options(options, over: false)
406
+ # recognize commands with an opt_name, no procname
407
+ return if execute_simple_commands(options)
382
408
 
383
- simple_commands = {
384
- doc_glob: -> { @fout.fout options[:md_filename_glob] },
385
- # list_blocks: -> { list_blocks },
386
- list_default_env: -> { @fout.fout_list list_default_env },
387
- list_default_yaml: -> { @fout.fout_list list_default_yaml },
388
- list_docs: -> { @fout.fout_list files },
389
- list_recent_output: -> {
390
- @fout.fout_list list_recent_output(
391
- @options[:saved_stdout_folder],
392
- @options[:saved_stdout_glob], @options[:list_count]
393
- )
394
- },
395
- list_recent_scripts: -> {
396
- @fout.fout_list list_recent_scripts(
397
- options[:saved_script_folder],
398
- options[:saved_script_glob], options[:list_count]
399
- )
400
- },
401
- pwd: -> { @fout.fout File.expand_path('..', __dir__) },
402
- run_last_script: -> { run_last_script },
403
- tab_completions: -> { @fout.fout tab_completions },
404
- menu_export: -> { @fout.fout menu_export }
405
- }
406
-
407
- return if execute_simple_commands(simple_commands)
408
-
409
- files = opts_prepare_file_list(options)
410
- execute_block_logic(files)
409
+ mde_vux_main_loop(opts_prepare_file_list(options))
411
410
  return unless @options[:output_saved_script_filename]
412
411
 
413
- @fout.fout "script_block_name: #{@options.run_state.script_block_name}"
414
- @fout.fout "s_save_filespec: #{@options.run_state.saved_filespec}"
415
- rescue StandardError
416
- 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}"
417
414
  end
418
415
 
419
416
  # Executes command based on the provided option keys
420
- def execute_simple_commands(simple_commands)
421
- simple_commands.each_key do |key|
422
- if @options[key]
423
- 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
424
421
  return true
425
422
  end
426
423
  end
427
424
  false
428
425
  end
429
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
+
430
444
  ## post-parse options configuration
431
445
  #
432
446
  def finalize_cli_argument_processing(rest = @rest)
@@ -444,9 +458,6 @@ module MarkdownExec
444
458
  end
445
459
  end
446
460
 
447
- ## position 1: block name (optional)
448
- #
449
- @options[:block_name] = nil
450
461
  @options[:input_cli_rest] = @rest
451
462
  rescue FileMissingError
452
463
  warn_format('finalize_cli_argument_processing',
@@ -464,10 +475,36 @@ module MarkdownExec
464
475
  :menu_chrome_color)}"
465
476
  searcher = SearchResultsReport.new(value, [find_path])
466
477
  file_names = searcher.file_names(options, value)
467
- found_in_block_names = searcher.found_in_block_names(options, value, formspec: '%<line>s')
478
+ found_in_block_names = searcher.found_in_block_names(options, value,
479
+ formspec: '%<line>s')
468
480
  directory_names = searcher.directory_names(options, value)
469
481
 
470
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)
471
508
  [found_in_block_names,
472
509
  directory_names,
473
510
  file_names].each do |data|
@@ -480,69 +517,155 @@ module MarkdownExec
480
517
  @fout.fout fi[:content] if fi[:content]
481
518
  end
482
519
  end
483
- return { exit: true } unless execute_chosen_found
484
-
485
- ## pick a document to open
486
- #
487
- files_in_directories = directory_names[:data].map do |dn|
488
- find_files('*', [dn], exclude_dirs: true)
489
- end.flatten(1).map { |str| FileInMenu.for_menu(str) }
490
-
491
- unless file_names[:data]&.count.positive? || files_in_directories&.count.positive? || found_in_block_names[:data]&.count.positive?
492
- return { exit: true }
493
- end
494
-
495
- vbn = found_in_block_names[:matched_contents].map do |matched_contents|
496
- filename, details, = matched_contents
497
- nexo = AnsiFormatter.new(@options).format_and_highlight_array(
498
- details,
499
- highlight: [value]
500
- )
501
- [FileInMenu.for_menu(filename)] + nexo.map do |str|
502
- { disabled: '', name: (' ' * 20) + str }
503
- end
504
- end.flatten
520
+ end
505
521
 
506
- choices = MenuBuilder.new.build_menu(file_names, directory_names, found_in_block_names,
507
- 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
+ )
508
530
 
509
531
  @options[:filename] = FileInMenu.from_menu(
510
532
  select_document_if_multiple(
511
533
  choices,
512
- 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
513
536
  )
514
537
  )
515
- { 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
516
606
  end
517
607
 
518
608
  ## Sets up the options and returns the parsed arguments
519
609
  #
520
- def initialize_and_parse_cli_options
521
- # @options = base_options
610
+ def initialize_parse_execute_cli
522
611
  @options = HashDelegator.new(base_options)
523
-
612
+ @options.p_all_arguments = ARGV.dup # Duplicate ARGV to track original order
613
+ ### should not include after '--'
524
614
  read_configuration_file!(@options,
525
615
  ".#{MarkdownExec::APP_NAME.downcase}.yml")
526
616
 
617
+ options_parsed = []
527
618
  @option_parser = OptionParser.new do |opts|
528
619
  executable_name = File.basename($PROGRAM_NAME)
529
620
  opts.banner = [
530
621
  "#{MarkdownExec::APP_NAME}" \
531
622
  " - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
532
- "Usage: #{executable_name} [(directory | file [block_name] | search_keyword)] [options]"
623
+ "Usage: #{executable_name}" \
624
+ ' [(directory | file [block_name] | search_keyword)] [options]'
533
625
  ].join("\n")
534
626
 
535
627
  menu_iter do |item|
536
- opts_menu_option_append opts, @options, item
628
+ opts_menu_option_append(opts, @options, item, options_parsed)
537
629
  end
538
630
  end
539
631
  @option_parser.load
540
632
  @option_parser.environment
541
- @rest = rest = @option_parser.parse!(arguments_for_mde)
542
- @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
+
543
642
  @options.merge(@options.run_state.to_h)
643
+ end
544
644
 
545
- 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
546
669
  end
547
670
 
548
671
  ##
@@ -558,8 +681,9 @@ module MarkdownExec
558
681
  ->(_) { exit }
559
682
  when 'find', 'open'
560
683
  ->(value) {
561
- exit if find_value(value, execute_chosen_found: procname == 'open').fetch(:exit,
562
- false)
684
+ exit if find_value(
685
+ value, execute_chosen_found: procname == 'open'
686
+ ).fetch(:exit, false)
563
687
  }
564
688
  when 'help'
565
689
  ->(_) {
@@ -588,6 +712,8 @@ module MarkdownExec
588
712
  lambda(&:to_i)
589
713
  when 'val_as_str'
590
714
  lambda(&:to_s)
715
+ when 'val_as_sym'
716
+ lambda(&:to_sym)
591
717
  when 'version'
592
718
  lambda { |_|
593
719
  @fout.fout MarkdownExec::VERSION
@@ -598,7 +724,17 @@ module MarkdownExec
598
724
  end
599
725
  end
600
726
 
601
- # 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
602
738
 
603
739
  def list_default_env
604
740
  menu_iter do |item|
@@ -639,6 +775,35 @@ module MarkdownExec
639
775
  @options[:md_filename_glob]))
640
776
  end
641
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
+
642
807
  private
643
808
 
644
809
  ##
@@ -668,37 +833,88 @@ module MarkdownExec
668
833
  end.to_yaml
669
834
  end
670
835
 
671
- def opts_menu_option_append(opts, options, item)
836
+ def opts_menu_option_append(opts, options, item, options_parsed)
672
837
  return unless item[:long_name].present? || item[:short_name].present?
673
838
 
674
- 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
+
675
851
  # - long name
676
852
  if item[:long_name].present?
677
- "--#{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]}" : ''}"
678
856
  end,
679
857
 
680
858
  # - short name
681
- 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,
682
863
 
683
864
  # - description and default
684
- [item[:description],
685
- ("[#{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.
686
898
 
687
899
  # apply proccode, if present, to value
688
900
  # save value to options hash if option is named
689
901
  #
690
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)
691
905
  (item[:proccode] ? item[:proccode].call(value) : value).tap do |converted|
692
906
  options[item[:opt_name]] = converted if item[:opt_name]
693
907
  end
694
908
  }
695
- ].compact)
909
+ ].compact
910
+ opts.on(*switches)
696
911
  end
697
912
 
698
913
  def opts_prepare_file_list(options)
699
914
  list_files_specified(
700
915
  determine_filename(
701
- specified_filename: options[:filename]&.present? ? options[:filename] : nil,
916
+ specified_filename:
917
+ options[:filename]&.present? ? options[:filename] : nil,
702
918
  specified_folder: options[:path],
703
919
  default_filename: 'README.md',
704
920
  default_folder: '.'
@@ -717,12 +933,21 @@ module MarkdownExec
717
933
  public
718
934
 
719
935
  def run
720
- initialize_and_parse_cli_options
936
+ initialize_parse_execute_cli
721
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
722
944
  rescue StandardError
723
945
  error_handler('run')
724
- ensure
725
- yield if block_given?
946
+ # rubocop:disable Style/RescueStandardError
947
+ rescue
948
+ warn 'Exiting...' if $DEBUG
949
+ exit 1
950
+ # rubocop:enable Style/RescueStandardError
726
951
  end
727
952
 
728
953
  private
@@ -734,7 +959,7 @@ module MarkdownExec
734
959
 
735
960
  saved_name_split filename
736
961
  @options[:save_executed_script] = false
737
- @options.document_inpseq
962
+ @options.vux_main_loop
738
963
  rescue StandardError
739
964
  error_handler('run_last_script')
740
965
  end
@@ -756,8 +981,11 @@ module MarkdownExec
756
981
 
757
982
  opts = options.dup
758
983
  select_option_or_exit(
759
- HashDelegator.new(@options).string_send_color(prompt,
760
- :prompt_color_after_script_execution),
984
+ HashDelegator.new(@options)
985
+ .string_send_color(
986
+ prompt,
987
+ :prompt_color_after_script_execution
988
+ ),
761
989
  files,
762
990
  opts.merge(per_page: opts[:select_page_height])
763
991
  )
@@ -770,6 +998,33 @@ module MarkdownExec
770
998
  )&.fetch(:selected)
771
999
  end
772
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
+
773
1028
  public
774
1029
 
775
1030
  def tab_completions(data = menu_for_optparse)