markdown_exec 2.5.0 → 2.6.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/Gemfile.lock +2 -2
  4. data/Rakefile +3 -3
  5. data/bats/block-types.bats +13 -7
  6. data/bats/import.bats +6 -0
  7. data/bats/markup.bats +6 -15
  8. data/bats/options-collapse.bats +26 -0
  9. data/bats/options.bats +1 -1
  10. data/bats/table.bats +8 -0
  11. data/bats/test_helper.bash +74 -49
  12. data/bats/variable-expansion.bats +46 -0
  13. data/bin/tab_completion.sh +1 -1
  14. data/docs/dev/bats-document-configuration.md +8 -1
  15. data/docs/dev/block-type-bash.md +1 -1
  16. data/docs/dev/block-type-opts.md +1 -5
  17. data/docs/dev/block-type-vars.md +4 -0
  18. data/docs/dev/import-missing.md +2 -0
  19. data/docs/dev/menu-cli.md +1 -1
  20. data/docs/dev/options-collapse.md +47 -0
  21. data/docs/dev/requiring-blocks.md +3 -0
  22. data/docs/dev/specs.md +2 -1
  23. data/docs/dev/table-crash.md +39 -0
  24. data/docs/dev/table-indent.md +26 -0
  25. data/docs/dev/text-decoration.md +2 -5
  26. data/docs/dev/variable-expansion.md +2 -4
  27. data/examples/bash-blocks.md +1 -1
  28. data/examples/block-names.md +1 -1
  29. data/examples/block-types.md +1 -1
  30. data/examples/data-files.md +1 -1
  31. data/examples/document_options.md +2 -2
  32. data/examples/indent.md +1 -1
  33. data/examples/interrupt.md +1 -1
  34. data/examples/link-blocks-vars.md +1 -1
  35. data/examples/linked.md +1 -1
  36. data/examples/linked1.md +1 -1
  37. data/examples/nickname.md +1 -1
  38. data/examples/opts-blocks-require.md +1 -1
  39. data/examples/opts-blocks.md +1 -1
  40. data/examples/opts_output_execution.md +1 -1
  41. data/examples/pass-through-arguments.md +1 -1
  42. data/examples/pause-after-execution.md +1 -1
  43. data/examples/port-blocks.md +1 -1
  44. data/examples/save.md +1 -1
  45. data/examples/text-markup.md +1 -1
  46. data/examples/variable-expansion.md +6 -2
  47. data/examples/vars-blocks.md +1 -1
  48. data/examples/wrap.md +1 -1
  49. data/lib/block_types.rb +4 -0
  50. data/lib/cached_nested_file_reader.rb +7 -4
  51. data/lib/collapser.rb +302 -0
  52. data/lib/constants.rb +10 -0
  53. data/lib/evaluate_shell_expressions.rb +0 -3
  54. data/lib/fcb.rb +13 -17
  55. data/lib/format_table.rb +11 -7
  56. data/lib/hash_delegator.rb +461 -272
  57. data/lib/hierarchy_string.rb +5 -1
  58. data/lib/markdown_exec/version.rb +1 -1
  59. data/lib/markdown_exec.rb +16 -32
  60. data/lib/mdoc.rb +100 -35
  61. data/lib/menu.src.yml +124 -17
  62. data/lib/menu.yml +102 -16
  63. data/lib/ww.rb +75 -22
  64. metadata +12 -9
  65. data/lib/append_to_bash_history.rb +0 -303
  66. data/lib/ce_get_cost_and_usage.rb +0 -23
  67. data/lib/doh.rb +0 -190
  68. data/lib/layered_hash.rb +0 -143
  69. data/lib/poly.rb +0 -171
@@ -20,7 +20,6 @@ require 'yaml'
20
20
  require_relative 'ansi_string'
21
21
  require_relative 'array'
22
22
  require_relative 'array_util'
23
- # require_relative 'block_label'
24
23
  require_relative 'block_types'
25
24
  require_relative 'cached_nested_file_reader'
26
25
  require_relative 'constants'
@@ -165,6 +164,7 @@ module HashDelegatorSelf
165
164
 
166
165
  def initialize_fcb_names(fcb)
167
166
  fcb.oname = fcb.dname = fcb.title || ''
167
+ fcb.s2title = fcb.oname
168
168
  end
169
169
 
170
170
  def join_code_lines(lines)
@@ -267,6 +267,7 @@ module HashDelegatorSelf
267
267
  def tables_into_columns!(blocks_menu, delegate_object)
268
268
  return unless delegate_object[:tables_into_columns]
269
269
 
270
+ screenwidth = delegate_object[:console_width]
270
271
  lines = blocks_menu.map(&:oname)
271
272
  text_tables = TableExtractor.extract_tables(
272
273
  lines,
@@ -279,7 +280,7 @@ module HashDelegatorSelf
279
280
 
280
281
  range = table[:start_index]..(table[:start_index] + table[:rows] - 1)
281
282
  lines = blocks_menu[range].map(&:dname)
282
- formatted = MarkdownTableFormatter.format_table(
283
+ table__hs = MarkdownTableFormatter.format_table__hs(
283
284
  column_count: table[:columns],
284
285
  decorate: {
285
286
  border: delegate_object[:table_border_color],
@@ -290,8 +291,7 @@ module HashDelegatorSelf
290
291
  lines: lines
291
292
  )
292
293
 
293
- unless formatted.count == range.size
294
- # warn [__LINE__, range, lines, formatted].inspect
294
+ unless table__hs.count == range.size
295
295
  raise 'Invalid result from MarkdownTableFormatter.format_table()'
296
296
  end
297
297
 
@@ -300,11 +300,28 @@ module HashDelegatorSelf
300
300
 
301
301
  # replace text in each block
302
302
  range.each.with_index do |block_ind, ind|
303
- ### format oname to dname
304
- blocks_menu[block_ind].dname = indent + formatted[ind]
303
+ fcb = blocks_menu[block_ind]
304
+ fcb.s3formatted_table_row = fcb.padded = table__hs[ind] ####
305
+ fcb.padded_width = table__hs[ind].padded_width
306
+ if fcb.center
307
+ cw = (screenwidth - table__hs[ind].padded_width) / 2
308
+ if cw.positive?
309
+ indent = ' ' * cw
310
+ fcb.s3indent = fcb.indent = indent
311
+ end
312
+ else
313
+ fcb.s3indent = fcb.indent
314
+ end
315
+ fcb.s3indent ||= ''
316
+ fcb.dname = fcb.indented_decorated = fcb.s3indent + fcb.s3formatted_table_row.decorate
305
317
  end
306
318
  end
307
319
  end
320
+ # s0indent: indent,
321
+ # s0printable: line_obj[:text],
322
+ # s1decorated: decorated,
323
+ # s2title = fcb.oname
324
+ # s3formatted_table_row = fcb.padded = table__hs[ind]####
308
325
 
309
326
  # Creates a TTY prompt with custom settings. Specifically,
310
327
  # it disables the default 'cross' symbol and
@@ -345,10 +362,10 @@ module HashDelegatorSelf
345
362
  # @param [String] line The line to be processed.
346
363
  # @param [Array<Symbol>] selected_types A list of message types to check.
347
364
  # @param [Proc] block The block to be called with the line data.
348
- def yield_line_if_selected(line, selected_types, &block)
365
+ def yield_line_if_selected(line, selected_types, id: '', &block)
349
366
  return unless block && block_type_selected?(selected_types, :line)
350
367
 
351
- block.call(:line, MarkdownExec::FCB.new(body: [line]))
368
+ block.call(:line, MarkdownExec::FCB.new(body: [line], id: id))
352
369
  end
353
370
  end
354
371
 
@@ -532,7 +549,7 @@ module MarkdownExec
532
549
  end
533
550
 
534
551
  class HashDelegatorParent
535
- attr_accessor :most_recent_loaded_filename, :pass_args, :run_state,
552
+ attr_accessor :pass_args, :run_state,
536
553
  :p_all_arguments, :p_options_parsed, :p_params, :p_rest
537
554
 
538
555
  extend HashDelegatorSelf
@@ -543,7 +560,8 @@ module MarkdownExec
543
560
  @delegate_object = delegate_object
544
561
  @prompt = HashDelegator.tty_prompt_without_disabled_symbol
545
562
 
546
- @most_recent_loaded_filename = nil
563
+ @opts_most_recent_filename = nil
564
+ @vars_most_recent_filename = nil
547
565
  @pass_args = []
548
566
  @run_state = OpenStruct.new(
549
567
  link_history: [],
@@ -559,17 +577,9 @@ module MarkdownExec
559
577
  @p_options_parsed = []
560
578
  @p_params = {}
561
579
  @p_rest = []
562
- end
563
580
 
564
- # private
565
-
566
- # def [](key)
567
- # @delegate_object[key]
568
- # end
569
-
570
- # def []=(key, value)
571
- # @delegate_object[key] = value
572
- # end
581
+ @compressed_ids = nil
582
+ end
573
583
 
574
584
  ##
575
585
  # Returns the absolute path of the given file path.
@@ -591,11 +601,25 @@ module MarkdownExec
591
601
  end
592
602
  end
593
603
 
604
+ def add_back_option(id: '', menu_blocks:)
605
+ append_chrome_block(id: id, menu_blocks: menu_blocks,
606
+ menu_state: MenuState::BACK)
607
+ end
608
+
609
+ def add_exit_option(id: '', menu_blocks:)
610
+ append_chrome_block(id: id, menu_blocks: menu_blocks,
611
+ menu_state: MenuState::EXIT)
612
+ end
613
+
614
+ def add_inherited_lines(menu_blocks:, link_state:)
615
+ append_inherited_lines(menu_blocks: menu_blocks, link_state: link_state)
616
+ end
617
+
594
618
  # Modifies the provided menu blocks array by adding 'Back' and 'Exit' options,
595
619
  # along with initial and final dividers, based on the delegate object's configuration.
596
620
  #
597
621
  # @param menu_blocks [Array] The array of menu block elements to be modified.
598
- def add_menu_chrome_blocks!(menu_blocks:, link_state:)
622
+ def add_menu_chrome_blocks!(id: '', menu_blocks:, link_state:)
599
623
  return unless @delegate_object[:menu_link_format].present?
600
624
 
601
625
  if @delegate_object[:menu_with_inherited_lines]
@@ -604,89 +628,18 @@ module MarkdownExec
604
628
  end
605
629
 
606
630
  # back before exit
607
- add_back_option(menu_blocks: menu_blocks) if should_add_back_option?
631
+ add_back_option(id: "#{id}.back",
632
+ menu_blocks: menu_blocks) if should_add_back_option?
608
633
 
609
634
  # exit after other options
610
635
  if @delegate_object[:menu_with_exit]
611
- add_exit_option(menu_blocks: menu_blocks)
636
+ add_exit_option(id: "#{id}.exit", menu_blocks: menu_blocks)
612
637
  end
613
638
 
614
- append_divider(menu_blocks: menu_blocks, position: :initial)
615
- append_divider(menu_blocks: menu_blocks, position: :final)
616
- end
617
-
618
- def variable_expansions!(
619
- echo_command_form: 'echo "$%s"',
620
- link_state:,
621
- menu_blocks:,
622
- regexp: Regexp.new(@delegate_object[:variable_expression_regexp])
623
- )
624
- # !!v link_state.inherited_lines_block
625
- # collect variables in menu_blocks
626
- #
627
- variables_count = Hash.new(0)
628
- menu_blocks.each do |fcb|
629
- next if fcb.type == BlockType::SHELL
630
-
631
- fcb.oname.scan(regexp) do |(expression, variable)|
632
- expression.match(regexp)
633
- variables_count[$LAST_MATCH_INFO[:variable]] += 1
634
- end
635
- end
636
- # !!v variables_count
637
-
638
- # commands to echo variables
639
- #
640
- commands = {}
641
- variables_count.each do |variable, count|
642
- command = format(echo_command_form, variable)
643
- commands[variable] = command
644
- end
645
- # !!v commands
646
-
647
- # replacement dictionary from evaluated commands
648
- #
649
- replacement_dictionary = evaluate_shell_expressions(
650
- link_state.inherited_lines_block, commands,
651
- key_format: "${%s}" # no need to escape variable name for regexp
652
- ) # !!t
653
- return if replacement_dictionary.nil?
654
-
655
- # update blocks
656
- #
657
- Regexp.union(replacement_dictionary.keys).tap do |pattern|
658
- menu_blocks.each do |fcb|
659
- next if fcb.type == BlockType::SHELL
660
-
661
- fcb.variable_expansion!(pattern, replacement_dictionary)
662
- end
663
- end
664
- end
665
-
666
- private
667
-
668
- def replace_keys_in_lines(replacement_dictionary, lines)
669
- # Create a regex pattern that matches any key in the replacement dictionary
670
- pattern = Regexp.union(replacement_dictionary.keys.map { |key|
671
- "%<#{key}>"
672
- })
673
-
674
- # Iterate over each line and apply gsub with the replacement hash
675
- lines.map do |line|
676
- line.gsub(pattern) { |match| replacement_dictionary[match] }
677
- end
678
- end
679
-
680
- def add_back_option(menu_blocks:)
681
- append_chrome_block(menu_blocks: menu_blocks, menu_state: MenuState::BACK)
682
- end
683
-
684
- def add_exit_option(menu_blocks:)
685
- append_chrome_block(menu_blocks: menu_blocks, menu_state: MenuState::EXIT)
686
- end
687
-
688
- def add_inherited_lines(menu_blocks:, link_state:)
689
- append_inherited_lines(menu_blocks: menu_blocks, link_state: link_state)
639
+ append_divider(id: "#{id}.init", menu_blocks: menu_blocks,
640
+ position: :initial)
641
+ append_divider(id: "#{id}.final", menu_blocks: menu_blocks,
642
+ position: :final)
690
643
  end
691
644
 
692
645
  public
@@ -695,7 +648,7 @@ module MarkdownExec
695
648
  #
696
649
  # @param all_blocks [Array] The current blocks in the menu
697
650
  # @param type [Symbol] The type of chrome block to add (:back or :exit)
698
- def append_chrome_block(menu_blocks:, menu_state:)
651
+ def append_chrome_block(menu_blocks:, menu_state:, id: '')
699
652
  case menu_state
700
653
  when MenuState::BACK
701
654
  history_state_partition
@@ -733,6 +686,8 @@ module MarkdownExec
733
686
  dname: HashDelegator.new(@delegate_object).string_send_color(
734
687
  formatted_name, :menu_chrome_color
735
688
  ),
689
+ id: id,
690
+ type: BlockType::CHROME,
736
691
  nickname: formatted_name,
737
692
  oname: formatted_name
738
693
  )
@@ -751,10 +706,10 @@ module MarkdownExec
751
706
  #
752
707
  # @param menu_blocks [Array] The array of menu block elements.
753
708
  # @param position [Symbol] The position to insert the divider (:initial or :final).
754
- def append_divider(menu_blocks:, position:)
709
+ def append_divider(id: '', menu_blocks:, position:)
755
710
  return unless divider_formatting_present?(position)
756
711
 
757
- divider = create_divider(position)
712
+ divider = create_divider(position, id: id)
758
713
  position == :initial ? menu_blocks.unshift(divider) : menu_blocks.push(divider)
759
714
  end
760
715
 
@@ -772,7 +727,7 @@ module MarkdownExec
772
727
  { line: line })
773
728
  FCB.new(
774
729
  chrome: true,
775
- disabled: '',
730
+ disabled: TtyMenu::DISABLE,
776
731
  dname: HashDelegator.new(@delegate_object).string_send_color(
777
732
  formatted, :menu_inherited_lines_color
778
733
  ),
@@ -840,8 +795,10 @@ module MarkdownExec
840
795
  register_console_attributes(@delegate_object)
841
796
  @decor_patterns_from_delegate_object_for_block_create = collect_line_decor_patterns(@delegate_object)
842
797
 
798
+ count = 0
843
799
  blocks = []
844
800
  iter_blocks_from_nested_files do |btype, fcb|
801
+ count += 1
845
802
  case btype
846
803
  when :blocks
847
804
  if @delegate_object[:bash]
@@ -849,6 +806,7 @@ module MarkdownExec
849
806
  block_calls_scan: @delegate_object[:block_calls_scan],
850
807
  block_name_match: @delegate_object[:block_name_match],
851
808
  block_name_nick_match: @delegate_object[:block_name_nick_match],
809
+ id: "*#{count}",
852
810
  ) do |oname, color|
853
811
  apply_block_type_color_option(oname, color)
854
812
  end
@@ -858,7 +816,8 @@ module MarkdownExec
858
816
  %i[blocks line]
859
817
  when :line
860
818
  unless @delegate_object[:no_chrome]
861
- create_and_add_chrome_blocks(blocks, fcb)
819
+ create_and_add_chrome_blocks(blocks, fcb, id: "*#{count}",
820
+ init_ids: init_ids)
862
821
  end
863
822
  end
864
823
  end
@@ -894,6 +853,13 @@ module MarkdownExec
894
853
 
895
854
  # private
896
855
 
856
+ def build_replacement_dictionary(commands, link_state)
857
+ evaluate_shell_expressions(
858
+ link_state.inherited_lines_block, commands,
859
+ key_format: "${%s}" # no need to escape variable name for regexp
860
+ ) # !!t
861
+ end
862
+
897
863
  def calc_logged_stdout_filename(block_name:)
898
864
  return unless @delegate_object[:saved_stdout_folder]
899
865
 
@@ -937,13 +903,39 @@ module MarkdownExec
937
903
  true
938
904
  end
939
905
 
906
+ def chrome_block_criteria
907
+ [
908
+ { center: :table_center, format: :menu_note_format,
909
+ match: :menu_table_rows_match, type: BlockType::TEXT },
910
+ { case_conversion: :upcase, center: :heading1_center,
911
+ collapse: :heading1_collapse, collapsible: :heading1_collapsible,
912
+ color: :menu_heading1_color, format: :menu_heading1_format, level: 1,
913
+ match: :heading1_match, type: BlockType::HEADING, wrap: true },
914
+ { center: :heading2_center,
915
+ collapse: :heading2_collapse, collapsible: :heading2_collapsible,
916
+ color: :menu_heading2_color, format: :menu_heading2_format, level: 2,
917
+ match: :heading2_match, type: BlockType::HEADING, wrap: true },
918
+ { case_conversion: :downcase, center: :heading3_center,
919
+ collapse: :heading3_collapse, collapsible: :heading3_collapsible,
920
+ color: :menu_heading3_color, format: :menu_heading3_format, level: 3,
921
+ match: :heading3_match, type: BlockType::HEADING, wrap: true },
922
+ { center: :divider4_center,
923
+ collapse: :divider4_collapse, collapsible: :divider4_collapsible,
924
+ color: :menu_divider_color, format: :menu_divider_format, level: 4,
925
+ match: :divider_match, type: BlockType::DIVIDER },
926
+ { color: :menu_note_color, format: :menu_note_format,
927
+ match: :menu_note_match, type: BlockType::TEXT, wrap: true },
928
+ { color: :menu_task_color, format: :menu_task_format,
929
+ match: :menu_task_match, type: BlockType::TEXT, wrap: true }
930
+ ]
931
+ end
932
+
933
+ # sets ENV
940
934
  def code_from_vars_block_to_set_environment_variables(selected)
941
935
  code_lines = []
942
936
  YAML.load(selected.body.join("\n"))&.each do |key, value|
943
937
  ENV[key] = value.to_s
944
-
945
- require 'shellwords'
946
- code_lines.push "#{key}=\"#{Shellwords.escape(value)}\""
938
+ code_lines.push "#{key}=#{Shellwords.escape(value)}"
947
939
 
948
940
  next unless @delegate_object[:menu_vars_set_format].present?
949
941
 
@@ -971,7 +963,11 @@ module MarkdownExec
971
963
  end
972
964
  end
973
965
 
974
- def command_execute(command, shell:, args: [])
966
+ def command_execute(
967
+ command,
968
+ erls:,
969
+ shell:, args: []
970
+ )
975
971
  @run_state.files = StreamsOut.new
976
972
  @run_state.options = @delegate_object
977
973
  @run_state.started_at = Time.now.utc
@@ -983,6 +979,7 @@ module MarkdownExec
983
979
  @run_state.in_own_window = true
984
980
  command_execute_in_own_window(
985
981
  args: args,
982
+ erls: erls,
986
983
  script: @delegate_object[:execute_command_format]
987
984
  )
988
985
 
@@ -990,6 +987,7 @@ module MarkdownExec
990
987
  @run_state.in_own_window = false
991
988
  command_execute_in_process(
992
989
  args: args, command: command,
990
+ erls: erls,
993
991
  filename: @delegate_object[:filename], shell: shell
994
992
  )
995
993
  end
@@ -1007,18 +1005,27 @@ module MarkdownExec
1007
1005
  @fout.fout "Error ENOENT: #{err.inspect}"
1008
1006
  end
1009
1007
 
1010
- def command_execute_in_own_window(args:, script:)
1008
+ def command_execute_in_own_window(
1009
+ args:,
1010
+ erls:,
1011
+ script:
1012
+ )
1011
1013
  system(
1012
1014
  format(
1013
1015
  script,
1014
1016
  command_execute_in_own_window_format_arguments(
1017
+ erls: erls,
1015
1018
  rest: args ? args.join(' ') : ''
1016
1019
  )
1017
1020
  )
1018
1021
  )
1019
1022
  end
1020
1023
 
1021
- def command_execute_in_own_window_format_arguments(home: Dir.pwd, rest: '')
1024
+ def command_execute_in_own_window_format_arguments(
1025
+ home: Dir.pwd,
1026
+ erls:,
1027
+ rest: ''
1028
+ )
1022
1029
  {
1023
1030
  batch_index: @run_state.batch_index,
1024
1031
  batch_random: @run_state.batch_random,
@@ -1030,6 +1037,7 @@ module MarkdownExec
1030
1037
  @delegate_object[:logged_stdout_filespec]
1031
1038
  ),
1032
1039
  output_filespec: @delegate_object[:logged_stdout_filespec],
1040
+ play_command: erls[:play_bin],
1033
1041
  rest: rest,
1034
1042
  script_filename: @run_state.saved_filespec,
1035
1043
  script_filespec: File.join(home, @run_state.saved_filespec),
@@ -1039,7 +1047,11 @@ module MarkdownExec
1039
1047
  }
1040
1048
  end
1041
1049
 
1042
- def command_execute_in_process(args:, command:, filename:, shell:)
1050
+ def command_execute_in_process(
1051
+ args:, command:,
1052
+ erls:,
1053
+ filename:, shell:
1054
+ )
1043
1055
  execute_command_with_streams(
1044
1056
  [shell, '-c', command,
1045
1057
  @delegate_object[:filename], *args]
@@ -1061,6 +1073,19 @@ module MarkdownExec
1061
1073
  def compile_execute_and_trigger_reuse(
1062
1074
  mdoc:, selected:, block_source:, link_state:
1063
1075
  )
1076
+ # play_bin matches the name in mde.applescript, called by .mde.macos.yml
1077
+ bim = @delegate_object[:block_interactive_match]
1078
+ play_bin = if bim.present? && selected.start_line =~ Regexp.new(bim)
1079
+ @delegate_object[:play_bin_interactive]
1080
+ else
1081
+ bbm = @delegate_object[:block_batch_match]
1082
+ if bbm.present? && selected.start_line =~ Regexp.new(bbm)
1083
+ @delegate_object[:play_bin_batch]
1084
+ else
1085
+ @delegate_object[:document_play_bin]
1086
+ end
1087
+ end
1088
+
1064
1089
  required_lines = execute_block_type_port_code_lines(
1065
1090
  mdoc: mdoc, selected: selected,
1066
1091
  link_state: link_state, block_source: block_source
@@ -1080,8 +1105,10 @@ module MarkdownExec
1080
1105
  end
1081
1106
 
1082
1107
  if allow_execution
1083
- execute_required_lines(required_lines: required_lines,
1084
- selected: selected,
1108
+ execute_required_lines(blockname: selected.pub_name,
1109
+ erls: { play_bin: play_bin,
1110
+ shell: selected.shell },
1111
+ required_lines: required_lines,
1085
1112
  shell: selected.shell)
1086
1113
  end
1087
1114
 
@@ -1117,6 +1144,26 @@ module MarkdownExec
1117
1144
  HashDelegator.count_matches_in_lines(lines, regex) / 2
1118
1145
  end
1119
1146
 
1147
+ def count_named_group_occurrences(
1148
+ blocks, pattern, exclude_types: [BlockType::SHELL]
1149
+ )
1150
+ # Initialize a counter for named group occurrences
1151
+ occurrence_count = Hash.new(0)
1152
+
1153
+ blocks.each do |block|
1154
+ # Skip processing for shell-type blocks
1155
+ next if exclude_types.include?(block.type)
1156
+
1157
+ # Scan each block name for matches of the pattern
1158
+ block.oname.scan(pattern) do |(_, variable_name)|
1159
+ pattern.match($LAST_MATCH_INFO.to_s) # Reapply match for named groups
1160
+ occurrence_count[$LAST_MATCH_INFO[:variable]] += 1
1161
+ end
1162
+ end
1163
+
1164
+ occurrence_count
1165
+ end
1166
+
1120
1167
  ##
1121
1168
  # Creates and adds a formatted block to the blocks array
1122
1169
  # based on the provided match and format options.
@@ -1129,13 +1176,17 @@ module MarkdownExec
1129
1176
  # to the block's display name.
1130
1177
  # return number of lines added
1131
1178
  def create_and_add_chrome_block(blocks:, match_data:,
1179
+ collapse: nil,
1132
1180
  format_option:, color_method:,
1133
1181
  case_conversion: nil,
1134
1182
  center: nil,
1135
1183
  decor_patterns: [],
1184
+ disabled: true,
1185
+ id: '',
1186
+ level: 0,
1187
+ type: '',
1136
1188
  wrap: nil)
1137
1189
  line_cap = NamedCaptureExtractor::extract_named_group2(match_data)
1138
-
1139
1190
  # replace tabs in indent
1140
1191
  line_cap[:indent] ||= ''
1141
1192
  line_cap[:indent] = line_cap[:indent].dup if line_cap[:indent].frozen?
@@ -1145,23 +1196,19 @@ module MarkdownExec
1145
1196
  line_cap[:text] = line_cap[:text].dup if line_cap[:text].frozen?
1146
1197
  line_cap[:text].gsub!("\t", ' ')
1147
1198
  # missing capture
1199
+ line_cap[:collapse] ||= ''
1148
1200
  line_cap[:line] ||= ''
1149
1201
 
1150
1202
  accepted_width = @delegate_object[:console_width] - 2
1151
- line_caps = if wrap
1152
- if line_cap[:text].length > accepted_width
1153
- wrapper = StringWrapper.new(
1154
- width: accepted_width - line_cap[:indent].length
1155
- )
1156
- wrapper.wrap(line_cap[:text]).map do |line|
1157
- line_cap.dup.merge(text: line)
1158
- end
1159
- else
1160
- [line_cap]
1161
- end
1162
- else
1163
- [line_cap]
1164
- end
1203
+
1204
+ line_caps = [line_cap]
1205
+ if wrap && line_cap[:text].length > accepted_width
1206
+ wrapper = StringWrapper.new(width: accepted_width - line_cap[:indent].length)
1207
+ line_caps = wrapper.wrap(line_cap[:text]).map do |wrapped_line|
1208
+ line_cap.dup.merge(text: wrapped_line)
1209
+ end
1210
+ end
1211
+
1165
1212
  if center
1166
1213
  line_caps.each do |line_obj|
1167
1214
  line_obj[:indent] =
@@ -1173,7 +1220,7 @@ module MarkdownExec
1173
1220
  end
1174
1221
  end
1175
1222
 
1176
- line_caps.each do |line_obj|
1223
+ line_caps.each_with_index do |line_obj, index|
1177
1224
  next if line_obj[:text].nil?
1178
1225
 
1179
1226
  case case_conversion
@@ -1196,10 +1243,23 @@ module MarkdownExec
1196
1243
 
1197
1244
  line_obj[:line] = line_obj[:indent] + line_obj[:text]
1198
1245
  blocks.push FCB.new(
1246
+ center: center,
1199
1247
  chrome: true,
1200
- disabled: '',
1248
+ collapse: collapse.nil? ? (line_obj[:collapse] == COLLAPSIBLE_TOKEN_COLLAPSE) : collapse,
1249
+ token: line_obj[:collapse],
1250
+ disabled: disabled ? TtyMenu::DISABLE : nil,
1251
+ ####
1252
+ # id: "#{@delegate_object[:filename]}:#{index}",
1253
+ id: "#{id}.#{index}",
1254
+ level: level,
1255
+ s0indent: indent,
1256
+ s0printable: line_obj[:text],
1257
+ s1decorated: decorated,
1201
1258
  dname: line_obj[:indent] + decorated,
1202
- oname: line_obj[:text]
1259
+ indent: line_obj[:indent],
1260
+ oname: line_obj[:text],
1261
+ text: line_obj[:text],
1262
+ type: type
1203
1263
  )
1204
1264
  end
1205
1265
  line_caps.count
@@ -1213,20 +1273,8 @@ module MarkdownExec
1213
1273
  # @param opts [Hash] Options containing configuration for line processing.
1214
1274
  # @param use_chrome [Boolean] Indicates if the chrome styling should
1215
1275
  # be applied.
1216
- def create_and_add_chrome_blocks(blocks, fcb)
1217
- # rubocop:disable Layout/LineLength
1218
- match_criteria = [
1219
- { format: :menu_note_format, match: :menu_table_rows_match },
1220
- { color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match, wrap: true, center: :heading1_center, case_conversion: :upcase },
1221
- { color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match, wrap: true, center: :heading2_center },
1222
- { color: :menu_heading3_color, format: :menu_heading3_format, match: :heading3_match, wrap: true, center: :heading3_center, case_conversion: :downcase },
1223
- { color: :menu_divider_color, format: :menu_divider_format, match: :menu_divider_match },
1224
- { color: :menu_note_color, format: :menu_note_format, match: :menu_note_match, wrap: true },
1225
- { color: :menu_task_color, format: :menu_task_format, match: :menu_task_match, wrap: true }
1226
- ]
1227
- # rubocop:enable Layout/LineLength
1228
- # rubocop:enable Style/UnlessElse
1229
- match_criteria.each do |criteria|
1276
+ def create_and_add_chrome_blocks(blocks, fcb, id: '', init_ids: false)
1277
+ chrome_block_criteria.each_with_index do |criteria, index|
1230
1278
  unless @delegate_object[criteria[:match]].present? &&
1231
1279
  (mbody = fcb.body[0].match @delegate_object[criteria[:match]])
1232
1280
  next
@@ -1237,20 +1285,34 @@ module MarkdownExec
1237
1285
  case_conversion: criteria[:case_conversion],
1238
1286
  center: criteria[:center] &&
1239
1287
  @delegate_object[criteria[:center]],
1288
+
1289
+ collapse: case fcb.collapse_token
1290
+ when COLLAPSIBLE_TOKEN_COLLAPSE
1291
+ true
1292
+ when COLLAPSIBLE_TOKEN_EXPAND
1293
+ false
1294
+ else
1295
+ false####
1296
+ end,
1297
+
1240
1298
  color_method: criteria[:color] &&
1241
1299
  @delegate_object[criteria[:color]].to_sym,
1242
1300
  decor_patterns:
1243
1301
  @decor_patterns_from_delegate_object_for_block_create,
1302
+ disabled: !(criteria[:collapsible] && @delegate_object[criteria[:collapsible]]),
1303
+ id: "#{id}.#{index}",
1244
1304
  format_option: criteria[:format] &&
1245
1305
  @delegate_object[criteria[:format]],
1306
+ level: criteria[:level],
1246
1307
  match_data: mbody,
1308
+ type: criteria[:type],
1247
1309
  wrap: criteria[:wrap]
1248
1310
  )
1249
1311
  break
1250
1312
  end
1251
1313
  end
1252
1314
 
1253
- def create_divider(position)
1315
+ def create_divider(position, id: '')
1254
1316
  divider_key = if position == :initial
1255
1317
  :menu_initial_divider
1256
1318
  else
@@ -1261,8 +1323,9 @@ module MarkdownExec
1261
1323
 
1262
1324
  FCB.new(
1263
1325
  chrome: true,
1264
- disabled: '',
1326
+ disabled: TtyMenu::DISABLE,
1265
1327
  dname: string_send_color(oname, :menu_divider_color),
1328
+ id: id,
1266
1329
  oname: oname
1267
1330
  )
1268
1331
  end
@@ -1344,7 +1407,9 @@ module MarkdownExec
1344
1407
  end
1345
1408
 
1346
1409
  def dml_menu_append_chrome_item(
1347
- name, count, type, menu_state: MenuState::LOAD,
1410
+ name, count, type,
1411
+ id: '',
1412
+ menu_state: MenuState::LOAD,
1348
1413
  always_create: true, always_enable: true
1349
1414
  )
1350
1415
  raise unless name.present?
@@ -1355,7 +1420,8 @@ module MarkdownExec
1355
1420
  # create menu item when it is needed (count > 0)
1356
1421
  #
1357
1422
  if item.nil? && (always_create || count.positive?)
1358
- item = append_chrome_block(menu_blocks: @dml_menu_blocks,
1423
+ item = append_chrome_block(id: id,
1424
+ menu_blocks: @dml_menu_blocks,
1359
1425
  menu_state: menu_state)
1360
1426
  end
1361
1427
 
@@ -1367,7 +1433,7 @@ module MarkdownExec
1367
1433
  if always_enable || count.positive?
1368
1434
  item.delete(:disabled)
1369
1435
  else
1370
- item[:disabled] = ''
1436
+ item[:disabled] = TtyMenu::DISABLE
1371
1437
  end
1372
1438
  end
1373
1439
 
@@ -1521,18 +1587,15 @@ module MarkdownExec
1521
1587
  def execute_block_by_type_for_lfls(
1522
1588
  selected:, mdoc:, block_source:, link_state: LinkState.new
1523
1589
  )
1524
- # !!v selected
1525
1590
  # order should not be important other than else clause
1526
1591
  if selected.type == BlockType::EDIT
1527
1592
  debounce_reset
1528
- # !!v link_state.inherited_lines_block
1529
1593
  vux_edit_inherited
1530
1594
  return :break if pause_user_exit
1531
1595
 
1532
1596
  next_state_append_code(selected, link_state, [])
1533
1597
 
1534
1598
  elsif selected.type == BlockType::HISTORY
1535
- # !!b
1536
1599
  debounce_reset
1537
1600
  return :break if execute_block_type_history_ux(
1538
1601
  selected: selected,
@@ -1544,10 +1607,10 @@ module MarkdownExec
1544
1607
  elsif selected.type == BlockType::LINK
1545
1608
  debounce_reset
1546
1609
  execute_block_type_link_with_state(link_block_body: selected.body,
1547
- mdoc: mdoc,
1548
- selected: selected,
1549
- link_state: link_state,
1550
- block_source: block_source)
1610
+ mdoc: mdoc,
1611
+ selected: selected,
1612
+ link_state: link_state,
1613
+ block_source: block_source)
1551
1614
 
1552
1615
  elsif selected.type == BlockType::LOAD
1553
1616
  debounce_reset
@@ -1611,6 +1674,15 @@ module MarkdownExec
1611
1674
  next_state_append_code(selected, link_state,
1612
1675
  code_from_vars_block_to_set_environment_variables(selected))
1613
1676
 
1677
+ elsif COLLAPSIBLE_TYPES.include?(selected.type)
1678
+ debounce_reset
1679
+ if @compressed_ids.keys.include?(selected.id)
1680
+ @compressed_ids.delete(selected.id)
1681
+ else
1682
+ @compressed_ids.merge!(selected.id => selected.level)
1683
+ end
1684
+ LoadFileLinkState.new(LoadFile::REUSE, link_state)
1685
+
1614
1686
  elsif debounce_allows
1615
1687
  compile_execute_and_trigger_reuse(mdoc: mdoc,
1616
1688
  selected: selected,
@@ -1695,9 +1767,7 @@ module MarkdownExec
1695
1767
  link_block_body: [], mdoc: nil, selected: FCB.new,
1696
1768
  link_state: LinkState.new, block_source: {}
1697
1769
  )
1698
- # !!p link_block_body selected
1699
1770
  link_block_data = HashDelegator.parse_yaml_data_from_body(link_block_body)
1700
- # !!v link_block_data
1701
1771
  ## collect blocks specified by block
1702
1772
  #
1703
1773
  if mdoc
@@ -1791,16 +1861,15 @@ module MarkdownExec
1791
1861
  def execute_block_type_load_code_lines(
1792
1862
  selected,
1793
1863
  directory: @delegate_object[:document_configurations_directory],
1864
+ exit_prompt: @delegate_object[:prompt_filespec_back],
1794
1865
  filename_pattern: @delegate_object[:vars_block_filename_pattern],
1795
1866
  glob: @delegate_object[:document_configurations_glob],
1796
1867
  view: @delegate_object[:vars_block_filename_view]
1797
1868
  )
1798
- # !!p selected
1799
1869
  block_data = HashDelegator.parse_yaml_data_from_body(selected.body)
1800
- # !!v block_data
1801
1870
  if selected_option = select_option_with_metadata(
1802
1871
  prompt_title,
1803
- Dir.glob(
1872
+ [exit_prompt] + Dir.glob(
1804
1873
  File.join(
1805
1874
  Dir.pwd,
1806
1875
  block_data['directory'] || directory,
@@ -1808,19 +1877,21 @@ module MarkdownExec
1808
1877
  )
1809
1878
  ).sort.map do |file|
1810
1879
  { name: format(
1811
- block_data['view'] || view,
1812
- NamedCaptureExtractor::extract_named_group2(
1813
- file.match(
1814
- Regexp.new(block_data['filename_pattern'] ||
1815
- filename_pattern)
1816
- )
1880
+ block_data['view'] || view,
1881
+ NamedCaptureExtractor::extract_named_group2(
1882
+ file.match(
1883
+ Regexp.new(block_data['filename_pattern'] ||
1884
+ filename_pattern)
1817
1885
  )
1818
- ),
1886
+ )
1887
+ ),
1819
1888
  oname: file }
1820
1889
  end,
1821
1890
  simple_menu_options
1822
1891
  )
1823
- File.readlines(selected_option.oname, chomp: true)
1892
+ if selected_option.dname != exit_prompt
1893
+ File.readlines(selected_option.oname, chomp: true)
1894
+ end
1824
1895
  else
1825
1896
  warn "No matching files found" ###
1826
1897
  end
@@ -1835,7 +1906,7 @@ module MarkdownExec
1835
1906
  # @param selected [Hash] The selected block.
1836
1907
  # @return [Array<String>] Required code blocks as an array of lines.
1837
1908
  def execute_block_type_port_code_lines(mdoc:, selected:, block_source:,
1838
- link_state: LinkState.new)
1909
+ link_state: LinkState.new)
1839
1910
  required = mdoc.collect_recursively_required_code(
1840
1911
  anyname: selected.pub_name,
1841
1912
  label_format_above: @delegate_object[:shell_code_label_format_above],
@@ -1881,24 +1952,19 @@ module MarkdownExec
1881
1952
  end
1882
1953
 
1883
1954
  def execute_block_type_save(code_lines:, selected:)
1884
- # !!p code_lines, selected
1885
1955
  block_data = HashDelegator.parse_yaml_data_from_body(selected.body)
1886
- # !!v block_data
1887
1956
  directory_glob = if block_data['directory']
1888
- # !!b
1889
1957
  File.join(
1890
1958
  block_data['directory'],
1891
1959
  block_data['glob'] ||
1892
1960
  @delegate_object[:document_saved_lines_glob].split('/').last
1893
1961
  )
1894
1962
  else
1895
- # !!b
1896
1963
  @delegate_object[:document_saved_lines_glob]
1897
1964
  end
1898
- # !!v directory_glob
1899
1965
 
1900
1966
  save_filespec_from_expression(directory_glob).tap do |save_filespec|
1901
- if save_filespec
1967
+ if save_filespec && save != exit_prompt
1902
1968
  begin
1903
1969
  File.write(save_filespec,
1904
1970
  HashDelegator.join_code_lines(code_lines))
@@ -2049,20 +2115,63 @@ module MarkdownExec
2049
2115
  # @param required_lines [Array<String>] The lines of code to be executed.
2050
2116
  # @param selected [FCB] The selected functional code block object.
2051
2117
  def execute_required_lines(
2052
- required_lines: [], selected: FCB.new, shell:
2118
+ blockname: '',
2119
+ erls: {},
2120
+ required_lines: [], shell:
2053
2121
  )
2054
2122
  if @delegate_object[:save_executed_script]
2055
- write_command_file(required_lines: required_lines,
2056
- selected: selected,
2123
+ write_command_file(blockname: blockname,
2124
+ required_lines: required_lines,
2057
2125
  shell: shell)
2058
2126
  end
2059
2127
  if @dml_block_state
2060
2128
  calc_logged_stdout_filename(block_name: @dml_block_state.block.oname)
2061
2129
  end
2062
- format_and_execute_command(code_lines: required_lines, shell: shell)
2130
+ format_and_execute_command(
2131
+ code_lines: required_lines,
2132
+ erls: erls,
2133
+ shell: shell
2134
+ )
2063
2135
  post_execution_process
2064
2136
  end
2065
2137
 
2138
+ def expand_blocks_with_replacements(
2139
+ menu_blocks, replacements, exclude_types: [BlockType::SHELL]
2140
+ )
2141
+ # update blocks
2142
+ #
2143
+ Regexp.union(replacements.keys).tap do |pattern|
2144
+ menu_blocks.each do |block|
2145
+ next if exclude_types.include?(block.type)
2146
+
2147
+ block.expand_variables_in_attributes!(pattern, replacements)
2148
+ end
2149
+ end
2150
+ end
2151
+
2152
+ def expand_variable_references!(
2153
+ # echo_format: 'echo "$%s"',
2154
+ echo_format: 'echo $%s',
2155
+ link_state:,
2156
+ blocks:,
2157
+ pattern: Regexp.new(@delegate_object[:variable_expression_regexp])
2158
+ )
2159
+ # Count occurrences of named groups in each block
2160
+ variable_counts = count_named_group_occurrences(blocks, pattern)
2161
+
2162
+ # Generate echo commands for each variable based on its count
2163
+ echo_commands = generate_echo_commands(variable_counts, echo_format)
2164
+
2165
+ # Build a dictionary to replace variables with the corresponding commands
2166
+ replacements = build_replacement_dictionary(echo_commands, link_state)
2167
+
2168
+ # Exit early if no replacements are needed
2169
+ return if replacements.nil?
2170
+
2171
+ # Expand each block with replacements from the dictionary
2172
+ expand_blocks_with_replacements(blocks, replacements)
2173
+ end
2174
+
2066
2175
  # Retrieves a specific data symbol from the delegate object,
2067
2176
  # converts it to a string, and applies a color style
2068
2177
  # based on the specified color symbol.
@@ -2096,11 +2205,20 @@ module MarkdownExec
2096
2205
  { size: file_size, lines: line_count }
2097
2206
  end
2098
2207
 
2099
- def format_and_execute_command(code_lines:, shell:)
2208
+ def format_and_execute_command(
2209
+ code_lines:,
2210
+ erls:,
2211
+ shell:
2212
+ )
2100
2213
  formatted_command = code_lines.flatten.join("\n")
2101
2214
  @fout.fout fetch_color(data_sym: :script_execution_head,
2102
2215
  color_sym: :script_execution_frame_color)
2103
- command_execute(formatted_command, args: @pass_args, shell: shell)
2216
+ command_execute(
2217
+ formatted_command,
2218
+ args: @pass_args,
2219
+ erls: erls,
2220
+ shell: shell
2221
+ )
2104
2222
  @fout.fout fetch_color(data_sym: :script_execution_tail,
2105
2223
  color_sym: :script_execution_frame_color)
2106
2224
  end
@@ -2162,6 +2280,17 @@ module MarkdownExec
2162
2280
  color_sym: :execution_report_preview_frame_color)
2163
2281
  end
2164
2282
 
2283
+ def generate_echo_commands(variable_counts, echo_format)
2284
+ # commands to echo variables
2285
+ #
2286
+ commands = {}
2287
+ variable_counts.each do |variable, count|
2288
+ command = format(echo_format, variable)
2289
+ commands[variable] = command
2290
+ end
2291
+ commands
2292
+ end
2293
+
2165
2294
  def generate_temp_filename(ext = '.sh')
2166
2295
  filename = begin
2167
2296
  Dir::Tmpname.make_tmpname(['x', ext], nil)
@@ -2219,12 +2348,9 @@ module MarkdownExec
2219
2348
  order: :chronological,
2220
2349
  path: ''
2221
2350
  )
2222
- # !!v filename, 'path', path
2223
- # !!v File.join(home, path, filename)
2224
2351
  files = Dir.glob(
2225
2352
  File.join(home, path, filename)
2226
2353
  )
2227
- # !!v files
2228
2354
  sorted_files = case order
2229
2355
  when :alphabetical
2230
2356
  files.sort
@@ -2267,16 +2393,18 @@ module MarkdownExec
2267
2393
  cfile.readlines(
2268
2394
  @delegate_object[:filename],
2269
2395
  import_paths: @delegate_object[:import_paths]&.split(':')
2270
- ).each do |nested_line|
2396
+ ).each_with_index do |nested_line, index|
2271
2397
  if nested_line
2272
- update_line_and_block_state(nested_line, state, selected_types,
2273
- &block)
2398
+ update_line_and_block_state(
2399
+ nested_line, state, selected_types,
2400
+ id: "#{@delegate_object[:filename]}:#{index}",
2401
+ &block
2402
+ )
2274
2403
  end
2275
2404
  end
2276
2405
  end
2277
2406
 
2278
2407
  def iter_source_blocks(source, &block)
2279
- # !!v source
2280
2408
  case source
2281
2409
  when 1
2282
2410
  blocks_from_nested_files.each(&block)
@@ -2400,18 +2528,14 @@ module MarkdownExec
2400
2528
  end
2401
2529
 
2402
2530
  def list_blocks
2403
- # !!b
2404
2531
  message = @delegate_object[:list_blocks_message]
2405
2532
  block_eval = @delegate_object[:list_blocks_eval]
2406
- # !!v message block_eval
2407
2533
 
2408
2534
  list = []
2409
2535
  iter_source_blocks(@delegate_object[:list_blocks_type]) do |block|
2410
- # !!v block
2411
2536
  list << (block_eval.present? ? eval(block_eval) : block.send(message))
2412
2537
  end
2413
2538
  list.compact!
2414
- # !!v list
2415
2539
 
2416
2540
  @fout.fout_list(list)
2417
2541
  end
@@ -2421,10 +2545,10 @@ module MarkdownExec
2421
2545
  # Executes a specified block once per filename.
2422
2546
  # @param all_blocks [Array] Array of all block elements.
2423
2547
  # @return [Boolean, nil] True if values were modified, nil otherwise.
2424
- def load_auto_opts_block(all_blocks, mdoc:)
2548
+ def load_auto_opts_block(all_blocks, id: '', mdoc:)
2425
2549
  block_name = @delegate_object[:document_load_opts_block_name]
2426
2550
  unless block_name.present? &&
2427
- @most_recent_loaded_filename != @delegate_object[:filename]
2551
+ @opts_most_recent_filename != @delegate_object[:filename]
2428
2552
  return
2429
2553
  end
2430
2554
 
@@ -2437,13 +2561,26 @@ module MarkdownExec
2437
2561
  )
2438
2562
  update_menu_base(options_state.options)
2439
2563
 
2440
- @most_recent_loaded_filename = @delegate_object[:filename]
2564
+ @opts_most_recent_filename = @delegate_object[:filename]
2441
2565
  true
2442
2566
  end
2443
2567
 
2568
+ def load_auto_vars_block(all_blocks,
2569
+ block_name: @delegate_object[:document_load_vars_block_name])
2570
+ unless block_name.present? &&
2571
+ @vars_most_recent_filename != @delegate_object[:filename]
2572
+ return
2573
+ end
2574
+
2575
+ block = HashDelegator.block_find(all_blocks, :oname, block_name)
2576
+ return unless block
2577
+
2578
+ @vars_most_recent_filename = @delegate_object[:filename]
2579
+ code_from_vars_block_to_set_environment_variables(block)
2580
+ end
2581
+
2444
2582
  def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [],
2445
2583
  default: nil)
2446
- # !!b
2447
2584
  if @delegate_object[:block_name].present?
2448
2585
  block = all_blocks.find do |item|
2449
2586
  item.pub_name == @delegate_object[:block_name]
@@ -2505,7 +2642,6 @@ module MarkdownExec
2505
2642
  link_state:)
2506
2643
  if block_name_from_cli &&
2507
2644
  @cli_block_name == @menu_base_options[:menu_persist_block_name]
2508
- # !!b 'pause cli control, allow user to select block'
2509
2645
  block_name_from_cli = false
2510
2646
  now_using_cli = false
2511
2647
  @menu_base_options[:block_name] =
@@ -2530,24 +2666,48 @@ module MarkdownExec
2530
2666
  ## Handles the file loading and returns the blocks
2531
2667
  # in the file and MDoc instance
2532
2668
  #
2533
- def mdoc_menu_and_blocks_from_nested_files(link_state)
2669
+ def mdoc_menu_and_blocks_from_nested_files(link_state, id: '')
2670
+ # read blocks, load document opts block, and re-process blocks
2671
+ #
2534
2672
  all_blocks, mdoc = mdoc_and_blocks_from_nested_files
2673
+ if load_auto_opts_block(all_blocks, id: id, mdoc: mdoc)
2674
+ all_blocks, mdoc = mdoc_and_blocks_from_nested_files
2675
+ end
2535
2676
 
2536
- # recreate menu with new options
2677
+ # load document vars block
2537
2678
  #
2538
- if load_auto_opts_block(all_blocks, mdoc: mdoc)
2539
- all_blocks, mdoc = mdoc_and_blocks_from_nested_files
2679
+ if code_lines = load_auto_vars_block(all_blocks)
2680
+ new_code = HashDelegator.code_merge(link_state.inherited_lines,
2681
+ code_lines)
2682
+ next_state_set_code(
2683
+ nil,
2684
+ link_state,
2685
+ new_code
2686
+ )
2687
+ link_state.inherited_lines = new_code
2540
2688
  end
2541
2689
 
2542
- menu_blocks = mdoc.fcbs_per_options(@delegate_object)
2690
+ # filter by name, collapsed
2691
+ #
2692
+ menu_blocks, @compressed_ids = mdoc.fcbs_per_options(
2693
+ @delegate_object.merge!(compressed_ids: @compressed_ids)
2694
+ )
2695
+
2696
+ # text substitution in menu
2697
+ #
2698
+ expand_variable_references!(blocks: menu_blocks, link_state: link_state)
2699
+ # expand_command_substition!(blocks: menu_blocks, link_state: link_state)
2543
2700
 
2544
- variable_expansions!(menu_blocks: menu_blocks, link_state: link_state)
2545
- add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
2701
+ # chrome for menu
2702
+ #
2703
+ add_menu_chrome_blocks!(id: id, menu_blocks: menu_blocks,
2704
+ link_state: link_state)
2546
2705
 
2547
2706
  ### compress empty lines
2548
2707
  HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
2549
- HashDelegator.tables_into_columns!(menu_blocks, @delegate_object)
2550
- [all_blocks, menu_blocks, mdoc] # !!r
2708
+ HashDelegator.tables_into_columns!(menu_blocks, @delegate_object) ####
2709
+
2710
+ [all_blocks, menu_blocks, mdoc]
2551
2711
  end
2552
2712
 
2553
2713
  def menu_add_disabled_option(document_glob)
@@ -2562,7 +2722,7 @@ module MarkdownExec
2562
2722
 
2563
2723
  chrome_block = FCB.new(
2564
2724
  chrome: true,
2565
- disabled: '',
2725
+ disabled: TtyMenu::DISABLE,
2566
2726
  dname: HashDelegator.new(@delegate_object).string_send_color(
2567
2727
  document_glob, :menu_inherited_lines_color
2568
2728
  ),
@@ -2630,7 +2790,7 @@ module MarkdownExec
2630
2790
  block_names = []
2631
2791
  dependencies = {}
2632
2792
  link_history_push_and_next(
2633
- curr_block_name: selected.pub_name,
2793
+ curr_block_name: selected&.pub_name,
2634
2794
  curr_document_filename: @delegate_object[:filename],
2635
2795
  inherited_block_names:
2636
2796
  ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
@@ -2747,32 +2907,31 @@ module MarkdownExec
2747
2907
  fout_execution_report if @delegate_object[:output_execution_report]
2748
2908
  end
2749
2909
 
2750
- # Prepare the blocks menu by adding labels and other necessary details.
2751
- # Remove filtered blocks.
2910
+ # Filter blocks per block_name_include_match, block_name_wrapper_match.
2752
2911
  #
2753
2912
  # @param all_blocks [Array<Hash>] The list of blocks from the file.
2754
- # @param opts [Hash] The options hash.
2755
- # @return [Array<Hash>] The updated blocks menu.
2756
- def prepare_blocks_menu(menu_blocks)
2757
- menu_blocks.map do |fcb|
2758
- next if Filter.prepared_not_in_menu?(
2913
+ def select_blocks(menu_blocks)
2914
+ menu_blocks.select do |fcb|
2915
+ !Filter.prepared_not_in_menu?(
2759
2916
  @delegate_object,
2760
2917
  fcb,
2761
2918
  %i[block_name_include_match block_name_wrapper_match]
2762
2919
  )
2920
+ end
2921
+ end
2763
2922
 
2764
- fcb.name = fcb.dname
2765
- # fcb.label = BlockLabel.make(
2766
- # body: fcb.body,
2767
- # filename: @delegate_object[:filename],
2768
- # headings: fcb.headings,
2769
- # menu_blocks_with_docname: @delegate_object[:menu_blocks_with_docname],
2770
- # menu_blocks_with_headings: @delegate_object[:menu_blocks_with_headings],
2771
- # text: fcb.text,
2772
- # title: fcb.title
2773
- # )
2923
+ # Filter blocks per block_name_include_match, block_name_wrapper_match.
2924
+ # Set name displayed by tty-prompt.
2925
+ #
2926
+ # @param all_blocks [Array<Hash>] The list of blocks from the file.
2927
+ # @param opts [Hash] The options hash.
2928
+ # @return [Array<Hash>] The updated blocks menu.
2929
+ def blocks_as_menu_items(menu_blocks)
2930
+ select_blocks(menu_blocks).map do |fcb|
2931
+ fcb.name = fcb.indented_decorated || (fcb.indent + (fcb.s1decorated || fcb.dname))
2932
+ fcb.value = fcb.id || fcb.name
2774
2933
  fcb.to_h
2775
- end.compact
2934
+ end
2776
2935
  end
2777
2936
 
2778
2937
  def print_formatted_option(key, value)
@@ -3142,6 +3301,20 @@ module MarkdownExec
3142
3301
  { abort: true })
3143
3302
  end
3144
3303
 
3304
+ # private
3305
+
3306
+ def replace_keys_in_lines(replacement_dictionary, lines)
3307
+ # Create a regex pattern that matches any key in the replacement dictionary
3308
+ pattern = Regexp.union(replacement_dictionary.keys.map { |key|
3309
+ "%<#{key}>"
3310
+ })
3311
+
3312
+ # Iterate over each line and apply gsub with the replacement hash
3313
+ lines.map do |line|
3314
+ line.gsub(pattern) { |match| replacement_dictionary[match] }
3315
+ end
3316
+ end
3317
+
3145
3318
  def report_error(err)
3146
3319
  # Handle ENOENT error
3147
3320
  @run_state.aborted_at = Time.now.utc
@@ -3235,9 +3408,12 @@ module MarkdownExec
3235
3408
  end
3236
3409
  end
3237
3410
 
3238
- def save_to_file(required_lines:, selected:, shell:)
3411
+ def save_to_file(
3412
+ erls:,
3413
+ required_lines:, selected:, shell:
3414
+ )
3239
3415
  write_command_file(
3240
- required_lines: required_lines, selected: selected, shell: shell
3416
+ required_lines: required_lines, blockname: selected.pub_name, shell: shell
3241
3417
  )
3242
3418
  @fout.fout "File saved: #{@run_state.saved_filespec}"
3243
3419
  end
@@ -3254,7 +3430,6 @@ module MarkdownExec
3254
3430
  end
3255
3431
 
3256
3432
  def select_document_if_multiple(options, files, prompt:)
3257
- # binding.irb
3258
3433
  return files if files.class == String ###
3259
3434
  return files[0] if (count = files.count) == 1
3260
3435
 
@@ -3274,17 +3449,34 @@ module MarkdownExec
3274
3449
  # Presents a TTY prompt to select an option or exit,
3275
3450
  # returns metadata including option and selected
3276
3451
  def select_option_with_metadata(prompt_text, menu_items, opts = {})
3277
- # !!v prompt_text menu_items
3278
3452
  ## configure to environment
3279
3453
  #
3280
3454
  register_console_attributes(opts)
3281
3455
 
3282
- # crashes if all menu options are disabled
3456
+ active_color_pastel = Pastel.new
3457
+ active_color_pastel = opts[:menu_active_color_pastel_messages]
3458
+ .inject(active_color_pastel) do |p, message|
3459
+ p.send(message)
3460
+ end
3461
+
3283
3462
  begin
3463
+ props = {
3464
+ active_color: active_color_pastel.detach,
3465
+ # activate dynamic list searching on letter/number key presses
3466
+ filter: true,
3467
+ }.freeze
3468
+
3469
+ # crashes if all menu options are disabled
3470
+ # crashes if default is not an existing item
3471
+ #
3284
3472
  selection = @prompt.select(prompt_text,
3285
3473
  menu_items,
3286
- opts.merge(filter: true))
3287
- # !!v selection
3474
+ opts.merge(props))
3475
+ rescue TTY::Prompt::ConfigurationError
3476
+ # prompt fails when collapsible block name has changed; clear default
3477
+ selection = @prompt.select(prompt_text,
3478
+ menu_items,
3479
+ opts.merge(props).merge(default: nil))
3288
3480
  rescue NoMethodError
3289
3481
  # no enabled options in page
3290
3482
  return
@@ -3292,9 +3484,10 @@ module MarkdownExec
3292
3484
 
3293
3485
  selected = menu_items.find do |item|
3294
3486
  if item.instance_of?(Hash)
3295
- (item[:name] || item[:dname]) == selection
3487
+ # (item[:id] || item[:name] || item[:dname]) == selection
3488
+ [item[:id], item[:name], item[:dname]].include?(selection)
3296
3489
  elsif item.instance_of?(MarkdownExec::FCB)
3297
- item.dname == selection
3490
+ item.dname == selection || item.id == selection
3298
3491
  else
3299
3492
  item == selection
3300
3493
  end
@@ -3353,7 +3546,7 @@ module MarkdownExec
3353
3546
 
3354
3547
  out = `#{cmd}`.sub(/.*?#{marker}/m, '')
3355
3548
  File.delete filespec
3356
- out # !!r
3549
+ out
3357
3550
  end
3358
3551
 
3359
3552
  def should_add_back_option?(
@@ -3487,8 +3680,11 @@ module MarkdownExec
3487
3680
  # @return [Void] The function modifies the `state`
3488
3681
  # and `selected_types` arguments in place.
3489
3682
  ##
3490
- def update_line_and_block_state(nested_line, state, selected_types,
3491
- &block)
3683
+ def update_line_and_block_state(
3684
+ nested_line, state, selected_types,
3685
+ id:,
3686
+ &block
3687
+ )
3492
3688
  line = nested_line.to_s
3493
3689
  if line.match(@delegate_object[:fenced_start_and_end_regex])
3494
3690
  if state[:in_fenced_block]
@@ -3523,11 +3719,8 @@ module MarkdownExec
3523
3719
  @delegate_object[:menu_include_imported_notes]
3524
3720
  # add line if it is depth 0 or option allows it
3525
3721
  #
3526
- HashDelegator.yield_line_if_selected(line, selected_types, &block)
3527
-
3528
- else
3529
- # !!b 'line is not recognized for block state'
3530
-
3722
+ HashDelegator.yield_line_if_selected(line, selected_types, id: id,
3723
+ &block)
3531
3724
  end
3532
3725
  end
3533
3726
 
@@ -3545,13 +3738,11 @@ module MarkdownExec
3545
3738
  menu_blocks: @dml_menu_blocks,
3546
3739
  default: @dml_menu_default_dname
3547
3740
  )
3548
- # !!b '@run_state.source.block_name_from_cli:',@run_state.source.block_name_from_cli
3549
3741
  if !@dml_block_state
3550
3742
  # HashDelegator.error_handler('block_state missing', { abort: true })
3551
3743
  # document has no enabled items
3552
3744
  :break
3553
3745
  elsif @dml_block_state.state == MenuState::EXIT
3554
- # !!b 'load_cli_or_user_selected_block -> break'
3555
3746
  :break
3556
3747
  end
3557
3748
  end
@@ -3604,7 +3795,6 @@ module MarkdownExec
3604
3795
  was_using_cli: @dml_now_using_cli
3605
3796
  )
3606
3797
 
3607
- # !!b '!block_name_from_ui + cli_break -> break'
3608
3798
  !@dml_block_state.source.block_name_from_ui && cli_break && :break
3609
3799
  end
3610
3800
 
@@ -3810,23 +4000,21 @@ module MarkdownExec
3810
4000
  @delegate_object[:filename],
3811
4001
  block_list
3812
4002
  ).run do |msg, data|
3813
- # !!v msg data
3814
- # !!t msg
3815
4003
  case msg
3816
4004
  when :parse_document # once for each menu
3817
- vux_parse_document
3818
- vux_menu_append_history_files(formatted_choice_ostructs)
4005
+ vux_parse_document(id: 'vux_parse_document')
4006
+ vux_menu_append_history_files(formatted_choice_ostructs,
4007
+ id: "vux_menu_append_history_files",)
3819
4008
  vux_publish_document_file_name_for_external_automation
3820
4009
 
3821
4010
  when :display_menu
4011
+ # does not display
3822
4012
  vux_clear_menu_state
3823
4013
 
3824
4014
  when :end_of_cli
3825
- # !!b
3826
4015
  # yield :end_of_cli, @delegate_object
3827
4016
 
3828
4017
  if @delegate_object[:list_blocks]
3829
- # !!b
3830
4018
  list_blocks
3831
4019
  :exit
3832
4020
  end
@@ -3862,7 +4050,8 @@ module MarkdownExec
3862
4050
  end
3863
4051
  end
3864
4052
 
3865
- def vux_menu_append_history_files(formatted_choice_ostructs)
4053
+ def vux_menu_append_history_files(formatted_choice_ostructs,
4054
+ id: '')
3866
4055
  if @delegate_object[:menu_for_history]
3867
4056
  history_files(
3868
4057
  @dml_link_state,
@@ -3873,7 +4062,9 @@ module MarkdownExec
3873
4062
  if files.count.positive?
3874
4063
  dml_menu_append_chrome_item(
3875
4064
  formatted_choice_ostructs[:history].oname, files.count,
3876
- 'files', menu_state: MenuState::HISTORY
4065
+ 'files',
4066
+ id: id,
4067
+ menu_state: MenuState::HISTORY
3877
4068
  )
3878
4069
  end
3879
4070
  end
@@ -3895,6 +4086,7 @@ module MarkdownExec
3895
4086
  if files.count.positive?
3896
4087
  dml_menu_append_chrome_item(
3897
4088
  formatted_choice_ostructs[:load].dname, files.count, 'files',
4089
+ id: "#{id}.load",
3898
4090
  menu_state: MenuState::LOAD
3899
4091
  )
3900
4092
  end
@@ -3902,18 +4094,21 @@ module MarkdownExec
3902
4094
  lines_count.positive?
3903
4095
  dml_menu_append_chrome_item(
3904
4096
  formatted_choice_ostructs[:edit].dname, lines_count, 'lines',
4097
+ id: "#{id}.edit",
3905
4098
  menu_state: MenuState::EDIT
3906
4099
  )
3907
4100
  end
3908
4101
  if lines_count.positive?
3909
4102
  dml_menu_append_chrome_item(
3910
4103
  formatted_choice_ostructs[:save].dname, 1, '',
4104
+ id: "#{id}.save",
3911
4105
  menu_state: MenuState::SAVE
3912
4106
  )
3913
4107
  end
3914
4108
  if lines_count.positive?
3915
4109
  dml_menu_append_chrome_item(
3916
4110
  formatted_choice_ostructs[:view].dname, 1, '',
4111
+ id: "#{id}.view",
3917
4112
  menu_state: MenuState::VIEW
3918
4113
  )
3919
4114
  end
@@ -3921,6 +4116,7 @@ module MarkdownExec
3921
4116
  if @delegate_object[:menu_with_shell]
3922
4117
  dml_menu_append_chrome_item(
3923
4118
  formatted_choice_ostructs[:shell].dname, 1, '',
4119
+ id: "#{id}.shell",
3924
4120
  menu_state: MenuState::SHELL
3925
4121
  )
3926
4122
  end
@@ -3939,7 +4135,7 @@ module MarkdownExec
3939
4135
  )
3940
4136
  end
3941
4137
 
3942
- def vux_parse_document
4138
+ def vux_parse_document(id: '')
3943
4139
  @run_state.batch_index += 1
3944
4140
  @run_state.in_own_window = false
3945
4141
 
@@ -3960,10 +4156,10 @@ module MarkdownExec
3960
4156
 
3961
4157
  # update @delegate_object and @menu_base_options in auto_load
3962
4158
  #
4159
+ # @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc, @dml_link_state =
3963
4160
  @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc =
3964
- mdoc_menu_and_blocks_from_nested_files(@dml_link_state)
4161
+ mdoc_menu_and_blocks_from_nested_files(@dml_link_state, id: id)
3965
4162
  dump_delobj(@dml_blocks_in_file, @dml_menu_blocks, @dml_link_state)
3966
- # !!b 'loop', @run_state.source.block_name_from_cli, @cli_block_name
3967
4163
  end
3968
4164
 
3969
4165
  def vux_publish_block_name_for_external_automation(block_name)
@@ -3995,7 +4191,6 @@ module MarkdownExec
3995
4191
 
3996
4192
  # return :break to break from loop
3997
4193
  def vux_user_selected_block_name
3998
- # !!b
3999
4194
  if @dml_link_state.block_name.present?
4000
4195
  # @prior_block_was_link = true
4001
4196
  @dml_block_state.block = blocks_find_by_block_name(
@@ -4026,32 +4221,27 @@ module MarkdownExec
4026
4221
  end
4027
4222
 
4028
4223
  def wait_for_user_selected_block(all_blocks, menu_blocks, default)
4029
- # !!b
4030
4224
  block_state = wait_for_user_selection(all_blocks, menu_blocks, default)
4031
4225
  handle_back_or_continue(block_state)
4032
4226
  block_state
4033
4227
  end
4034
4228
 
4035
4229
  def wait_for_user_selection(_all_blocks, menu_blocks, default)
4036
- # !!b
4037
4230
  if @delegate_object[:clear_screen_for_select_block]
4038
4231
  printf("\e[1;1H\e[2J")
4039
4232
  end
4040
4233
 
4041
- # !!b
4042
4234
  prompt_title = string_send_color(
4043
4235
  @delegate_object[:prompt_select_block].to_s,
4044
4236
  :prompt_color_after_script_execution
4045
4237
  )
4046
4238
 
4047
- # !!b
4048
- menu_items = prepare_blocks_menu(menu_blocks)
4239
+ menu_items = blocks_as_menu_items(menu_blocks)
4049
4240
  if menu_items.empty?
4050
4241
  return SelectedBlockMenuState.new(nil, OpenStruct.new,
4051
4242
  MenuState::EXIT)
4052
4243
  end
4053
4244
 
4054
- # !!b
4055
4245
  # default value may not match if color is different from
4056
4246
  # originating menu (opts changed while processing)
4057
4247
  selection_opts = if default && menu_blocks.map(&:dname).include?(default)
@@ -4060,26 +4250,24 @@ module MarkdownExec
4060
4250
  @delegate_object
4061
4251
  end
4062
4252
 
4063
- # !!b
4064
4253
  selection_opts.merge!(
4065
4254
  { cycle: @delegate_object[:select_page_cycle],
4066
4255
  per_page: @delegate_object[:select_page_height] }
4067
4256
  )
4068
4257
  selected_option = select_option_with_metadata(prompt_title, menu_items,
4069
4258
  selection_opts)
4070
- # !!b
4071
4259
  determine_block_state(selected_option)
4072
4260
  end
4073
4261
 
4074
4262
  # Handles the core logic for generating the command
4075
4263
  # file's metadata and content.
4076
- def write_command_file(required_lines:, selected:, shell: nil)
4264
+ def write_command_file(required_lines:, blockname:, shell: nil)
4077
4265
  return unless @delegate_object[:save_executed_script]
4078
4266
 
4079
4267
  time_now = Time.now.utc
4080
4268
  @run_state.saved_script_filename =
4081
4269
  SavedAsset.new(
4082
- blockname: selected.pub_name,
4270
+ blockname: blockname,
4083
4271
  exts: '.sh',
4084
4272
  filename: @delegate_object[:filename],
4085
4273
  prefix: @delegate_object[:saved_script_filename_prefix],
@@ -4273,6 +4461,7 @@ module MarkdownExec
4273
4461
  c.expects(:command_execute).with(
4274
4462
  '',
4275
4463
  args: pigeon,
4464
+ erls: {},
4276
4465
  shell: ShellType::BASH
4277
4466
  )
4278
4467
 
@@ -4487,7 +4676,7 @@ module MarkdownExec
4487
4676
  mdoc: @mdoc, selected: @selected, block_source: {}
4488
4677
  )
4489
4678
 
4490
- assert_equal ['code line', 'key="value"'], result
4679
+ assert_equal ['code line', 'key=value'], result
4491
4680
  end
4492
4681
  end
4493
4682
 
@@ -4814,7 +5003,7 @@ module MarkdownExec
4814
5003
  def test_call
4815
5004
  @hd.expects(:history_files).with(nil, filename: '*', path: nil).once
4816
5005
  @hd.execute_block_type_history_ux(filename: '*', link_state: LinkState.new,
4817
- selected: FCB.new(body: []))
5006
+ selected: FCB.new(body: []))
4818
5007
  end
4819
5008
  end
4820
5009