lib/hash_delegator.rb in markdown_exec-2.4.0 vs lib/hash_delegator.rb in markdown_exec-2.5.0

- old
+ new

@@ -18,15 +18,16 @@ require 'yaml' require_relative 'ansi_string' require_relative 'array' require_relative 'array_util' -require_relative 'block_label' +# require_relative 'block_label' require_relative 'block_types' require_relative 'cached_nested_file_reader' require_relative 'constants' require_relative 'directory_searcher' +require_relative 'evaluate_shell_expressions' require_relative 'exceptions' require_relative 'fcb' require_relative 'filter' require_relative 'format_table' require_relative 'fout' @@ -40,12 +41,10 @@ require_relative 'streams_out' require_relative 'string_util' require_relative 'table_extractor' require_relative 'text_analyzer' -require_relative 'argument_processor' - $pd = false unless defined?($pd) class String # Checks if the string is not empty. # @return [Boolean] Returns true if the string is not empty, false otherwise. @@ -59,55 +58,38 @@ # The method retrieves the color method from the provided hash. If the # color key is not present in the hash, it uses a default color method. # @param string [String] The string to be colored. # @param color_methods [Hash] A hash where keys are color names # (String/Symbol) and values are color methods. - # @param color_key [String, Symbol] The key representing the desired + # @param color_key [String, Symbol] The key representing + # the desired # color method in the color_methods hash. # @param default_method [String] (optional) Default color method to # use if color_key is not found in color_methods. Defaults to 'plain'. # @return [String] The colored string. def apply_color_from_hash(string, color_methods, color_key, default_method: 'plain') color_method = color_methods.fetch(color_key, default_method).to_sym AnsiString.new(string.to_s).send(color_method) end - # # Enhanced `apply_color_from_hash` method to support dynamic color transformations - # # @param string [String] The string to be colored. - # # @param color_transformations [Hash] A hash mapping color names to lambdas that apply color transformations. - # # @param color_key [String, Symbol] The key representing the desired color transformation in the color_transformations hash. - # # @param default_transformation [Proc] Default color transformation to use if color_key is not found in color_transformations. - # # @return [String] The colored string. - # def apply_color_from_hash(string, color_transformations, color_key, default_transformation: ->(str) { str }) - # transformation = color_transformations.fetch(color_key.to_sym, default_transformation) - # transformation.call(string) - # end - # color_transformations = { - # red: ->(str) { "\e[31m#{str}\e[0m" }, # ANSI color code for red - # green: ->(str) { "\e[32m#{str}\e[0m" }, # ANSI color code for green - # # Add more color transformations as needed - # } - # string = "Hello, World!" - # colored_string = apply_color_from_hash(string, color_transformations, :red) - # puts colored_string # This will print the string in red - # Searches for the first element in a collection where the specified # message sent to an element matches a given value. # This method is particularly useful for finding a specific hash-like # object within an enumerable collection. # If no match is found, it returns a specified default value. # - # @param blocks [Enumerable] The collection of hash-like objects to search. + # @param blocks [Enumerable] The collection of hash-like + # objects to search. # @param msg [Symbol, String] The message to send to each element of - # the collection. + # the collection. # @param value [Object] The value to match against the result of the message - # sent to each element. + # sent to each element. # @param default [Object, nil] The default value to return if no match is - # found (optional). + # found (optional). # @return [Object, nil] The first matching element or the default value if - # no match is found. + # no match is found. def block_find(blocks, msg, value, default = nil) blocks.find { |item| item.send(msg) == value } || default end def code_merge(*bodies) @@ -120,18 +102,19 @@ def create_directory_for_file(file_path) FileUtils.mkdir_p(File.dirname(file_path)) end - # Creates a file at the specified path, writes the given content to it, - # and sets file permissions if required. Handles any errors encountered - # during the process. + # Creates a file at the specified path, writes the given + # content to it, and sets file permissions if required. + # Handles any errors encountered during the process. # - # @param file_path [String] The path where the file will be created. + # @param file_path [String] The path where the file will + # be created. # @param content [String] The content to write into the file. - # @param chmod_value [Integer] The file permission value to set; - # skips if zero. + # @param chmod_value [Integer] The file permission value + # to set; skips if zero. def create_file_and_write_string_with_permissions(file_path, content, chmod_value) create_directory_for_file(file_path) File.write(file_path, content) set_file_permissions(file_path, chmod_value) unless chmod_value.zero? @@ -144,56 +127,37 @@ # end # Updates the title of an FCB object from its body content if the title # is nil or empty. def default_block_title_from_body(fcb) - return unless fcb.title.nil? || fcb.title.empty? + return fcb.title unless fcb.title.nil? || fcb.title.empty? fcb.derive_title_from_body end # delete the current line if it is empty and the previous is also empty def delete_consecutive_blank_lines!(blocks_menu) - blocks_menu.process_and_conditionally_delete! do |prev_item, current_item, _next_item| + blocks_menu.process_and_conditionally_delete! do + |prev_item, current_item, _next_item| prev_item&.fetch(:chrome, nil) && !(prev_item && prev_item.oname.present?) && current_item&.fetch(:chrome, nil) && !(current_item && current_item.oname.present?) end end - # # Deletes a temporary file specified by an environment variable. - # # Checks if the file exists before attempting to delete it and clears the environment variable afterward. - # # Any errors encountered during deletion are handled gracefully. - # def delete_required_temp_file(temp_blocks_file_path) - # return if temp_blocks_file_path.nil? || temp_blocks_file_path.empty? - - # HashDelegator.remove_file_without_standard_errors(temp_blocks_file_path) - # end - def error_handler(name = '', opts = {}, error: $!) Exceptions.error_handler( "HashDelegator.#{name} -- #{error}", opts ) end - # # DebugHelper.d ["HDmm method_name: #{method_name}", "#{first_n_caller_items 1}"] - # def first_n_caller_items(n) - # call_stack = caller - # base_path = File.realpath('.') - - # # Modify the call stack to remove the base path and keep only the first n items - # call_stack.take(n + 1)[1..].map do |line| - # " . #{line.sub(/^#{Regexp.escape(base_path)}\//, '')}" - # end.join("\n") - # end - # Indents all lines in a given string with a specified indentation string. # @param body [String] A multi-line string to be indented. # @param indent [String] The string used for indentation - # (default is an empty string). + # (default is an empty string). # @return [String] A single string with each line indented as specified. def indent_all_lines(body, indent = nil) return body unless indent&.non_empty? body.lines.map { |line| indent + line.chomp }.join("\n") @@ -206,38 +170,48 @@ def join_code_lines(lines) ((lines || []) + ['']).join("\n") end def merge_lists(*args) - # Filters out nil values, flattens the arrays, and ensures an empty list is returned if no valid lists are provided + # Filters out nil values, flattens the arrays, and ensures an + # empty list is returned if no valid lists are provided. merged = args.compact.flatten merged.empty? ? [] : merged end - def next_link_state(block_name_from_cli:, was_using_cli:, block_state:, - block_name: nil) + def next_link_state( + block_name_from_cli:, was_using_cli:, block_state:, block_name: nil + ) # Set block_name based on block_name_from_cli block_name = @cli_block_name if block_name_from_cli # Determine the state of breaker based on was_using_cli and the block type - # true only when block_name is nil, block_name_from_cli is false, was_using_cli is true, and the block_state.block.shell equals BlockType::BASH. In all other scenarios, breaker is false. - breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.shell == BlockType::BASH + # true only when block_name is nil, block_name_from_cli is false, + # was_using_cli is true, and the block_state.block.shell equals + # BlockType::BASH. In all other scenarios, breaker is false. + breaker = !block_name && + !block_name_from_cli && + was_using_cli && + block_state.block.type == BlockType::SHELL # Reset block_name_from_cli if the conditions are not met block_name_from_cli ||= false [block_name, block_name_from_cli, breaker] end def parse_yaml_data_from_body(body) - body.any? ? YAML.load(body.join("\n")) : {} + body&.any? ? YAML.load(body.join("\n")) : {} rescue StandardError - error_handler('parse_yaml_data_from_body', { abort: true }) + error_handler("parse_yaml_data_from_body for body: #{body}", + { abort: true }) end - # Reads required code blocks from a temporary file specified by an environment variable. - # @return [Array<String>] Lines read from the temporary file, or an empty array if file is not found or path is empty. + # Reads required code blocks from a temporary file specified + # by an environment variable. + # @return [Array<String>] Lines read from the temporary file, or + # an empty array if file is not found or path is empty. def read_required_blocks_from_temp_file(temp_blocks_file_path) return [] if temp_blocks_file_path.to_s.empty? if File.exist?(temp_blocks_file_path) File.readlines( @@ -262,11 +236,12 @@ # error_handler('safeval') # 'Invalid expression' # return # end # # Whitelisting allowed operations - # allowed_methods = %w[+ - * / == != < > <= >= && || % & |] + # allowed_methods = %w[+ - * / == != < > <= >= && || % & + # |] # unless allowed_methods.any? { |op| str.include?(op) } # error_handler('safeval', 'Operation not allowed') # return # end @@ -291,62 +266,72 @@ # find tables in multiple lines and format horizontally def tables_into_columns!(blocks_menu, delegate_object) return unless delegate_object[:tables_into_columns] lines = blocks_menu.map(&:oname) - text_tables = TableExtractor.extract_tables(lines) + text_tables = TableExtractor.extract_tables( + lines, + regexp: delegate_object[:table_parse_regexp] + ) return unless text_tables.count.positive? - text_tables.each do |match| - range = match[:start_index]..(match[:start_index] + match[:rows] - 1) - lines = blocks_menu[range].map(&:oname) + text_tables.each do |table| + next unless table[:columns].positive? + + range = table[:start_index]..(table[:start_index] + table[:rows] - 1) + lines = blocks_menu[range].map(&:dname) formatted = MarkdownTableFormatter.format_table( - lines, - match[:columns], + column_count: table[:columns], decorate: { border: delegate_object[:table_border_color], header_row: delegate_object[:table_header_row_color], row: delegate_object[:table_row_color], separator_line: delegate_object[:table_separator_line_color] - } + }, + lines: lines ) - if formatted.count == range.size - # read indentation from first line - indent = blocks_menu[range.first].oname.split('|', 2).first - - # replace text in each block - range.each.with_index do |block_ind, ind| - ### format oname to dname - blocks_menu[block_ind].dname = indent + formatted[ind] - end - else - warn [__LINE__, range, lines, formatted].inspect + unless formatted.count == range.size + # warn [__LINE__, range, lines, formatted].inspect raise 'Invalid result from MarkdownTableFormatter.format_table()' end + + # read indentation from first line + indent = blocks_menu[range.first].oname.split('|', 2).first + + # replace text in each block + range.each.with_index do |block_ind, ind| + ### format oname to dname + blocks_menu[block_ind].dname = indent + formatted[ind] + end end end - # Creates a TTY prompt with custom settings. Specifically, it disables the default 'cross' symbol and + # Creates a TTY prompt with custom settings. Specifically, + # it disables the default 'cross' symbol and # defines a lambda function to handle interrupts. - # @return [TTY::Prompt] A new TTY::Prompt instance with specified configurations. + # @return [TTY::Prompt] A new TTY::Prompt instance + # with specified configurations. def tty_prompt_without_disabled_symbol TTY::Prompt.new( interrupt: lambda { puts # next line in case not at start raise TTY::Reader::InputInterrupt }, symbols: { cross: ' ' } ) end - # Updates the attributes of the given fcb object and conditionally yields to a block. + # Updates the attributes of the given fcb object and + # conditionally yields to a block. # It initializes fcb names and sets the default block title from fcb's body. - # If the fcb has a body and meets certain conditions, it yields to the given block. + # If the fcb has a body and meets certain conditions, + # it yields to the given block. # # @param fcb [Object] The fcb object whose attributes are to be updated. - # @param selected_types [Array<Symbol>] A list of message types to determine if yielding is applicable. + # @param selected_types [Array<Symbol>] A list of message types to + # determine if yielding is applicable. # @param block [Block] An optional block to yield to if conditions are met. def update_menu_attrib_yield_selected(fcb:, messages:, configuration: {}, &block) initialize_fcb_names(fcb) return unless fcb.body @@ -367,11 +352,12 @@ end end # This module provides methods for compacting and converting data structures. module CompactionHelpers - # Converts an array of key-value pairs into a hash, applying compaction to the values. + # Converts an array of key-value pairs into a hash, + # applying compaction to the values. # Each value is processed by `compact_hash` to remove ineligible elements. # # @param array [Array] The array of key-value pairs to be converted. # @return [Hash] A hash with keys from the array and compacted values. def compact_and_convert_array_to_hash(array) @@ -379,11 +365,12 @@ compact_hash(value) end end # Compacts a hash by removing ineligible elements. - # It filters out nil, empty arrays, empty hashes, and empty strings from its values. + # It filters out nil, empty arrays, empty hashes, + # and empty strings from its values. # It also removes entries with :random as the key. # # @param hash [Hash] The hash to be compacted. # @return [Hash] A compacted version of the input hash. def compact_hash(hash) @@ -392,12 +379,14 @@ [key, value] end.compact.to_h end - # Converts a hash into another hash with indexed keys, applying compaction to the values. - # The keys are indexed, and the values are compacted using `compact_and_convert_array_to_hash`. + # Converts a hash into another hash with indexed keys, + # applying compaction to the values. + # The keys are indexed, and the values are + # compacted using `compact_and_convert_array_to_hash`. # # @param hash [Hash] The hash to be converted and compacted. # @return [Hash] A hash with indexed keys and the compacted original values. def compact_and_index_hash(hash) compact_and_convert_array_to_hash(hash.map.with_index do |value, index| @@ -405,36 +394,42 @@ end.to_h) end private - # Determines if a value is ineligible for inclusion in a compacted hash. - # Ineligible values are nil, empty arrays, empty hashes, and empty strings. + # Determines if a value is ineligible for inclusion in a + # compacted hash. + # Ineligible values are nil, empty arrays, empty hashes, + # and empty strings. # # @param value [Object] The value to be checked. # @return [Boolean] True if the value is ineligible, false otherwise. def value_ineligible?(value) [nil, [], {}, ''].include?(value) end end module PathUtils - # Determines if a given path is absolute or substitutes a placeholder in an expression with the path. + # Determines if a given path is absolute or substitutes a + # placeholder in an expression with the path. # @param path [String] The input path to check or fill in. - # @param expression [String] The expression where a wildcard '*' is replaced by the path if it's not absolute. - # @return [String] The absolute path or the expression with the wildcard replaced by the path. + # @param expression [String] The expression where a wildcard + # '*' is replaced by the path if it's not absolute. + # @return [String] The absolute path or the expression with + # the wildcard replaced by the path. def self.resolve_path_or_substitute(path, expression) if path.start_with?('/') path else expression.gsub('*', path) end end end class BashCommentFormatter - # Formats a multi-line string into a format safe for use in Bash comments. + # Formats a multi-line string into a format safe for use + # in Bash comments. def self.format_comment(input_string) return '# ' if input_string.nil? return '# ' if input_string.empty? formatted = input_string.split("\n").map do |line| @@ -453,12 +448,14 @@ # Initializes the StringWrapper with the given options. # # @param width [Integer] the maximum width of each line # @param left_margin [Integer] the number of spaces for the left margin - # @param right_margin [Integer] the number of spaces for the right margin - # @param indent [Integer] the number of spaces to indent all but the first line + # @param right_margin [Integer] the number of spaces for + # the right margin + # @param indent [Integer] the number of + # spaces to indent all but the first line # @param fill_margin [Boolean] whether to fill the left margin with spaces def initialize( width:, fill_margin: false, first_indent: '', @@ -616,12 +613,72 @@ append_divider(menu_blocks: menu_blocks, position: :initial) append_divider(menu_blocks: menu_blocks, position: :final) end + def variable_expansions!( + echo_command_form: 'echo "$%s"', + link_state:, + menu_blocks:, + regexp: Regexp.new(@delegate_object[:variable_expression_regexp]) + ) + # !!v link_state.inherited_lines_block + # collect variables in menu_blocks + # + variables_count = Hash.new(0) + menu_blocks.each do |fcb| + next if fcb.type == BlockType::SHELL + + fcb.oname.scan(regexp) do |(expression, variable)| + expression.match(regexp) + variables_count[$LAST_MATCH_INFO[:variable]] += 1 + end + end + # !!v variables_count + + # commands to echo variables + # + commands = {} + variables_count.each do |variable, count| + command = format(echo_command_form, variable) + commands[variable] = command + end + # !!v commands + + # replacement dictionary from evaluated commands + # + replacement_dictionary = evaluate_shell_expressions( + link_state.inherited_lines_block, commands, + key_format: "${%s}" # no need to escape variable name for regexp + ) # !!t + return if replacement_dictionary.nil? + + # update blocks + # + Regexp.union(replacement_dictionary.keys).tap do |pattern| + menu_blocks.each do |fcb| + next if fcb.type == BlockType::SHELL + + fcb.variable_expansion!(pattern, replacement_dictionary) + end + end + end + private + def replace_keys_in_lines(replacement_dictionary, lines) + # Create a regex pattern that matches any key in the replacement dictionary + pattern = Regexp.union(replacement_dictionary.keys.map { |key| + "%<#{key}>" + }) + + # Iterate over each line and apply gsub with the replacement hash + lines.map do |line| + line.gsub(pattern) { |match| replacement_dictionary[match] } + end + end + def add_back_option(menu_blocks:) append_chrome_block(menu_blocks: menu_blocks, menu_state: MenuState::BACK) end def add_exit_option(menu_blocks:) @@ -737,26 +794,30 @@ # private # Applies shell color options to the given string if applicable. # # @param name [String] The name to potentially colorize. - # @param shell_color_option [Symbol, nil] The shell color option to apply. + # @param block_type_color_option [Symbol, nil] The shell color option to apply. # @return [String] The colorized or original name string. - def apply_shell_color_option(name, shell_color_option) - if shell_color_option && @delegate_object[shell_color_option].present? - string_send_color(name, shell_color_option) + def apply_block_type_color_option(name, block_type_color_option) + if block_type_color_option && @delegate_object[block_type_color_option].present? + string_send_color(name, block_type_color_option) else name end end def apply_tree_decorations(text, color_method, decor_patterns) tree = HierarchyString.new([{ text: text, color: color_method }]) - decor_patterns.each do |pc| - analyzed_hierarchy = TextAnalyzer.analyze_hierarchy(tree.substrings, pc[:pattern], - color_method, pc[:color_method]) - tree = HierarchyString.new(analyzed_hierarchy) + if color_method + decor_patterns.each do |pc| + analyzed_hierarchy = TextAnalyzer.analyze_hierarchy( + tree.substrings, pc[:pattern], + color_method, pc[:color_method] + ) + tree = HierarchyString.new(analyzed_hierarchy) + end end tree.decorate end def assign_key_value_in_bash(key, value) @@ -768,23 +829,42 @@ end end # private - # Iterates through nested files to collect various types of blocks, including dividers, tasks, and others. + # Iterates through nested files to collect various types + # of blocks, including dividers, tasks, and others. # The method categorizes blocks based on their type and processes them accordingly. # # @return [Array<FCB>] An array of FCB objects representing the blocks. def blocks_from_nested_files register_console_attributes(@delegate_object) @decor_patterns_from_delegate_object_for_block_create = collect_line_decor_patterns(@delegate_object) blocks = [] iter_blocks_from_nested_files do |btype, fcb| - process_block_based_on_type(blocks, btype, fcb) + case btype + when :blocks + if @delegate_object[:bash] + fcb.for_menu!( + block_calls_scan: @delegate_object[:block_calls_scan], + block_name_match: @delegate_object[:block_name_match], + block_name_nick_match: @delegate_object[:block_name_nick_match], + ) do |oname, color| + apply_block_type_color_option(oname, color) + end + end + blocks << fcb + when :filter # types accepted + %i[blocks line] + when :line + unless @delegate_object[:no_chrome] + create_and_add_chrome_blocks(blocks, fcb) + end + end end - # &bt blocks.count + # !!t blocks.count blocks rescue StandardError HashDelegator.error_handler('blocks_from_nested_files') end @@ -822,11 +902,15 @@ blockname: block_name, filename: @delegate_object[:filename], prefix: @delegate_object[:logged_stdout_filename_prefix], time: Time.now.utc, exts: '.out.txt', - saved_asset_format: shell_escape_asset_format(@dml_link_state) + saved_asset_format: + shell_escape_asset_format( + code_lines: @dml_link_state.inherited_lines, + shell: ShellType::BASH + ) ).generate_name @logged_stdout_filespec = @delegate_object[:logged_stdout_filespec] = File.join @delegate_object[:saved_stdout_folder], @@ -851,10 +935,27 @@ return false end true end + def code_from_vars_block_to_set_environment_variables(selected) + code_lines = [] + YAML.load(selected.body.join("\n"))&.each do |key, value| + ENV[key] = value.to_s + + require 'shellwords' + code_lines.push "#{key}=\"#{Shellwords.escape(value)}\"" + + next unless @delegate_object[:menu_vars_set_format].present? + + formatted_string = format(@delegate_object[:menu_vars_set_format], + { key: key, value: value }) + print string_send_color(formatted_string, :menu_vars_set_color) + end + code_lines + end + def collect_line_decor_patterns(delegate_object) extract_patterns = lambda do |key| return [] unless delegate_object[key].present? HashDelegator.safeval(delegate_object[key]).map do |pc| @@ -868,125 +969,106 @@ %i[line_decor_pre line_decor_main line_decor_post].flat_map do |key| extract_patterns.call(key) end end - # Collects required code lines based on the selected block and the delegate object's configuration. - # If the block type is VARS, it also sets environment variables based on the block's content. - # - # @param mdoc [YourMDocClass] An instance of the MDoc class. - # @param selected [Hash] The selected block. - # @return [Array<String>] Required code blocks as an array of lines. - def collect_required_code_lines(mdoc:, selected:, block_source:, - link_state: LinkState.new) - required = mdoc.collect_recursively_required_code( - anyname: selected.pub_name, - label_format_above: @delegate_object[:shell_code_label_format_above], - label_format_below: @delegate_object[:shell_code_label_format_below], - block_source: block_source - ) # &bt 'required' - dependencies = (link_state&.inherited_dependencies || {}).merge(required[:dependencies] || {}) - required[:unmet_dependencies] = - (required[:unmet_dependencies] || []) - (link_state&.inherited_block_names || []) - if required[:unmet_dependencies].present? - ### filter against link_state.inherited_block_names - - warn format_and_highlight_dependencies(dependencies, - highlight: required[:unmet_dependencies]) - runtime_exception(:runtime_exception_error_level, - 'unmet_dependencies, flag: runtime_exception_error_level', - required[:unmet_dependencies]) - elsif @delegate_object[:dump_dependencies] - warn format_and_highlight_dependencies(dependencies, - highlight: [@delegate_object[:block_name]]) - end - - if selected[:shell] == BlockType::OPTS - # body of blocks is returned as a list of lines to be read an YAML - HashDelegator.code_merge(required[:blocks].map(&:body).flatten(1)) - else - code_lines = selected.shell == BlockType::VARS ? set_environment_variables_for_block(selected) : [] - HashDelegator.code_merge(link_state&.inherited_lines, - required[:code] + code_lines) - end - end - - def command_execute(command, args: []) + def command_execute(command, shell:, args: []) @run_state.files = StreamsOut.new @run_state.options = @delegate_object @run_state.started_at = Time.now.utc if @delegate_object[:execute_in_own_window] && @delegate_object[:execute_command_format].present? && @run_state.saved_filespec.present? + @run_state.in_own_window = true - system( - format( - @delegate_object[:execute_command_format], - command_execute_in_own_window_format_arguments(rest: args ? args.join(' ') : '') - ) + command_execute_in_own_window( + args: args, + script: @delegate_object[:execute_command_format] ) + else @run_state.in_own_window = false - execute_command_with_streams( - [@delegate_object[:shell], '-c', command, - @delegate_object[:filename], *args] + command_execute_in_process( + args: args, command: command, + filename: @delegate_object[:filename], shell: shell ) end @run_state.completed_at = Time.now.utc - rescue Errno::ENOENT => err - # Handle ENOENT error - @run_state.aborted_at = Time.now.utc - @run_state.error_message = err.message - @run_state.error = err - @run_state.files.append_stream_line(ExecutionStreams::STD_ERR, - @run_state.error_message) - @fout.fout "Error ENOENT: #{err.inspect}" + rescue Errno::ENOENT + report_error($ERROR_INFO) rescue SignalException => err # Handle SignalException @run_state.aborted_at = Time.now.utc @run_state.error_message = 'SIGTERM' @run_state.error = err @run_state.files.append_stream_line(ExecutionStreams::STD_ERR, @run_state.error_message) @fout.fout "Error ENOENT: #{err.inspect}" end + def command_execute_in_own_window(args:, script:) + system( + format( + script, + command_execute_in_own_window_format_arguments( + rest: args ? args.join(' ') : '' + ) + ) + ) + end + def command_execute_in_own_window_format_arguments(home: Dir.pwd, rest: '') { batch_index: @run_state.batch_index, batch_random: @run_state.batch_random, block_name: @delegate_object[:block_name], document_filename: File.basename(@delegate_object[:filename]), document_filespec: @delegate_object[:filename], home: home, - output_filename: File.basename(@delegate_object[:logged_stdout_filespec]), + output_filename: File.basename( + @delegate_object[:logged_stdout_filespec] + ), output_filespec: @delegate_object[:logged_stdout_filespec], rest: rest, script_filename: @run_state.saved_filespec, script_filespec: File.join(home, @run_state.saved_filespec), started_at: @run_state.started_at.strftime( @delegate_object[:execute_command_title_time_format] ) } end - # This method is responsible for handling the execution of generic blocks in a markdown document. - # It collects the required code lines from the document and, depending on the configuration, - # may display the code for user approval before execution. It then executes the approved block. + def command_execute_in_process(args:, command:, filename:, shell:) + execute_command_with_streams( + [shell, '-c', command, + @delegate_object[:filename], *args] + ) + end + + # This method is responsible for handling the execution of + # generic blocks in a markdown document. + # It collects the required code lines from the document and, + # depending on the configuration, may display the code for + # user approval before execution. It then executes the approved block. # - # @param mdoc [Object] The markdown document object containing code blocks. - # @param selected [Hash] The selected item from the menu to be executed. - # @return [LoadFileLinkState] An object indicating whether to load the next block or reuse the current one. - def compile_execute_and_trigger_reuse(mdoc:, selected:, block_source:, - link_state:) - required_lines = collect_required_code_lines( + # @param mdoc [Object] The markdown document object + # containing code blocks. + # @param selected [Hash] The selected item from the menu + # to be executed. + # @return [LoadFileLinkState] An object indicating whether to load + # the next block or reuse the current one. + def compile_execute_and_trigger_reuse( + mdoc:, selected:, block_source:, link_state: + ) + required_lines = execute_block_type_port_code_lines( mdoc: mdoc, selected: selected, link_state: link_state, block_source: block_source ) - output_or_approval = @delegate_object[:output_script] || @delegate_object[:user_must_approve] + output_or_approval = @delegate_object[:output_script] || + @delegate_object[:user_must_approve] if output_or_approval display_required_code(required_lines: required_lines) end allow_execution = if @delegate_object[:user_must_approve] prompt_for_user_approval( @@ -997,11 +1079,12 @@ true end if allow_execution execute_required_lines(required_lines: required_lines, - selected: selected) + selected: selected, + shell: selected.shell) end link_state.block_name = nil end @@ -1017,35 +1100,43 @@ " #{required_lines.flatten.count} lines," \ " #{text.length} characters" end # Counts the number of fenced code blocks in a file. - # It reads lines from a file and counts occurrences of lines matching the fenced block regex. - # Assumes that every fenced block starts and ends with a distinct line (hence divided by 2). + # It reads lines from a file and counts occurrences of lines + # matching the fenced block regex. + # Assumes that every fenced block starts and ends with a + # distinct line (hence divided by 2). # # @return [Integer] The count of fenced code blocks in the file. def count_blocks_in_filename regex = Regexp.new(@delegate_object[:fenced_start_and_end_regex]) - lines = cfile.readlines(@delegate_object[:filename], - import_paths: @delegate_object[:import_paths]&.split(':')) + lines = cfile.readlines( + @delegate_object[:filename], + import_paths: @delegate_object[:import_paths]&.split(':') + ) HashDelegator.count_matches_in_lines(lines, regex) / 2 end ## - # Creates and adds a formatted block to the blocks array based on the provided match and format options. + # Creates and adds a formatted block to the blocks array + # based on the provided match and format options. # @param blocks [Array] The array of blocks to add the new block to. - # @param match_data [MatchData] The match data containing named captures for formatting. - # @param format_option [String] The format string to be used for the new block. - # @param color_method [Symbol] The color method to apply to the block's display name. + # @param match_data [MatchData] The match data containing named captures + # for formatting. + # @param format_option [String] The format string to be used + # for the new block. + # @param color_method [Symbol] The color method to apply + # to the block's display name. # return number of lines added def create_and_add_chrome_block(blocks:, match_data:, format_option:, color_method:, case_conversion: nil, center: nil, decor_patterns: [], wrap: nil) - line_cap = match_data.named_captures.transform_keys(&:to_sym) + line_cap = NamedCaptureExtractor::extract_named_group2(match_data) # replace tabs in indent line_cap[:indent] ||= '' line_cap[:indent] = line_cap[:indent].dup if line_cap[:indent].frozen? line_cap[:indent].gsub!("\t", ' ') @@ -1057,11 +1148,13 @@ line_cap[:line] ||= '' accepted_width = @delegate_object[:console_width] - 2 line_caps = if wrap if line_cap[:text].length > accepted_width - wrapper = StringWrapper.new(width: accepted_width - line_cap[:indent].length) + wrapper = StringWrapper.new( + width: accepted_width - line_cap[:indent].length + ) wrapper.wrap(line_cap[:text]).map do |line| line_cap.dup.merge(text: line) end else [line_cap] @@ -1069,15 +1162,16 @@ else [line_cap] end if center line_caps.each do |line_obj| - line_obj[:indent] = if line_obj[:text].length < accepted_width - ' ' * ((accepted_width - line_obj[:text].length) / 2) - else - '' - end + line_obj[:indent] = + if line_obj[:text].length < accepted_width + ' ' * ((accepted_width - line_obj[:text].length) / 2) + else + '' + end end end line_caps.each do |line_obj| next if line_obj[:text].nil? @@ -1089,14 +1183,19 @@ line_obj[:text].downcase! end # format expects :line to be text only line_obj[:line] = line_obj[:text] - oname = format(format_option, line_obj) + oname = if format_option + format(format_option, line_obj) + else + line_obj[:line] + end + decorated = apply_tree_decorations( + oname, color_method, decor_patterns + ) - decorated = apply_tree_decorations(oname, color_method, decor_patterns) - line_obj[:line] = line_obj[:indent] + line_obj[:text] blocks.push FCB.new( chrome: true, disabled: '', dname: line_obj[:indent] + decorated, @@ -1105,24 +1204,27 @@ end line_caps.count end ## - # Processes lines within the file and converts them into blocks if they match certain criteria. + # Processes lines within the file and converts them into + # blocks if they match certain criteria. # @param blocks [Array] The array to append new blocks to. # @param fcb [FCB] The file control block being processed. # @param opts [Hash] Options containing configuration for line processing. - # @param use_chrome [Boolean] Indicates if the chrome styling should be applied. + # @param use_chrome [Boolean] Indicates if the chrome styling should + # be applied. def create_and_add_chrome_blocks(blocks, fcb) # rubocop:disable Layout/LineLength match_criteria = [ - { color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match, center: true, case_conversion: :upcase, wrap: true }, - { color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match, center: true, wrap: true }, - { color: :menu_heading3_color, format: :menu_heading3_format, match: :heading3_match, center: true, case_conversion: :downcase, wrap: true }, + { format: :menu_note_format, match: :menu_table_rows_match }, + { color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match, wrap: true, center: :heading1_center, case_conversion: :upcase }, + { color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match, wrap: true, center: :heading2_center }, + { color: :menu_heading3_color, format: :menu_heading3_format, match: :heading3_match, wrap: true, center: :heading3_center, case_conversion: :downcase }, { color: :menu_divider_color, format: :menu_divider_format, match: :menu_divider_match }, - { color: :menu_note_color, format: :menu_note_format, match: :menu_note_match, wrap: true }, - { color: :menu_task_color, format: :menu_task_format, match: :menu_task_match, wrap: true } + { color: :menu_note_color, format: :menu_note_format, match: :menu_note_match, wrap: true }, + { color: :menu_task_color, format: :menu_task_format, match: :menu_task_match, wrap: true } ] # rubocop:enable Layout/LineLength # rubocop:enable Style/UnlessElse match_criteria.each do |criteria| unless @delegate_object[criteria[:match]].present? && @@ -1131,23 +1233,31 @@ end create_and_add_chrome_block( blocks: blocks, case_conversion: criteria[:case_conversion], - center: criteria[:center], - color_method: @delegate_object[criteria[:color]].to_sym, - decor_patterns: @decor_patterns_from_delegate_object_for_block_create, - format_option: @delegate_object[criteria[:format]], + center: criteria[:center] && + @delegate_object[criteria[:center]], + color_method: criteria[:color] && + @delegate_object[criteria[:color]].to_sym, + decor_patterns: + @decor_patterns_from_delegate_object_for_block_create, + format_option: criteria[:format] && + @delegate_object[criteria[:format]], match_data: mbody, wrap: criteria[:wrap] ) break end end def create_divider(position) - divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider + divider_key = if position == :initial + :menu_initial_divider + else + :menu_final_divider + end oname = format(@delegate_object[:menu_divider_format], HashDelegator.safeval(@delegate_object[divider_key])) FCB.new( chrome: true, @@ -1166,11 +1276,12 @@ # filter block if selected in menu return true if @run_state.source.block_name_from_cli # return false if @prior_execution_block == @delegate_object[:block_name] if @prior_execution_block == @delegate_object[:block_name] - return @allowed_execution_block == @prior_execution_block || prompt_approve_repeat + return @allowed_execution_block == @prior_execution_block || + prompt_approve_repeat end @prior_execution_block = @delegate_object[:block_name] @allowed_execution_block = nil true @@ -1178,16 +1289,21 @@ def debounce_reset @prior_execution_block = nil end - # Determines the state of a selected block in the menu based on the selected option. - # It categorizes the selected option into either EXIT, BACK, or CONTINUE state. + # Determines the state of a selected block in the menu based + # on the selected option. + # It categorizes the selected option into either EXIT, BACK, + # or CONTINUE state. # # @param selected_option [Hash] The selected menu option. - # @return [SelectedBlockMenuState] An object representing the state of the selected block. + # @return [SelectedBlockMenuState] An object representing + # the state of the selected block. def determine_block_state(selected_option) + return if selected_option.nil? + option_name = selected_option[:oname] if option_name == menu_chrome_formatted_option(:menu_option_exit_name) return SelectedBlockMenuState.new(nil, OpenStruct.new, MenuState::EXIT) @@ -1201,12 +1317,14 @@ SelectedBlockMenuState.new(selected_option, OpenStruct.new, MenuState::CONTINUE) end - # Displays the required lines of code with color formatting for the preview section. - # It wraps the code lines between a formatted header and tail. + # Displays the required lines of code with color formatting + # for the preview section. + # It wraps the code lines between a formatted header and + # tail. # # @param required_lines [Array<String>] The lines of code to be displayed. def display_required_code(required_lines:) output_color_formatted(:script_preview_head, :script_preview_frame_color) @@ -1214,12 +1332,17 @@ output_color_formatted(:script_preview_tail, :script_preview_frame_color) end def divider_formatting_present?(position) - divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider - @delegate_object[:menu_divider_format].present? && @delegate_object[divider_key].present? + divider_key = if position == :initial + :menu_initial_divider + else + :menu_final_divider + end + @delegate_object[:menu_divider_format].present? && + @delegate_object[divider_key].present? end def dml_menu_append_chrome_item( name, count, type, menu_state: MenuState::LOAD, always_create: true, always_enable: true @@ -1250,23 +1373,34 @@ def do_save_execution_output return unless @delegate_object[:save_execution_output] return if @run_state.in_own_window - @run_state.files.write_execution_output_to_file(@delegate_object[:logged_stdout_filespec]) + @run_state.files.write_execution_output_to_file( + @delegate_object[:logged_stdout_filespec] + ) end # remove leading "./" # replace characters: / : . * (space) with: (underscore) - def document_name_in_glob_as_file_name(document_filename, glob) + def document_name_in_glob_as_file_name( + document_filename: @dml_link_state.document_filename, + format_glob: @delegate_object[:document_saved_lines_glob], + remove_regexp: %r{^\./}, + subst_regexp: /[\/:\.\* ]/, + subst_string: '_' + ) if document_filename.nil? || document_filename.empty? return document_filename end - format(glob, - { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/, - '_') }) + format( + format_glob, + { document_filename: + document_filename.gsub(remove_regexp, '') + .gsub(subst_regexp, subst_string) } + ) end def dump_and_warn_block_state(name:, selected:) if selected.nil? Exceptions.warn_format("Block not found -- name: #{name}", @@ -1278,28 +1412,34 @@ warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n") end # Outputs warnings based on the delegate object's configuration # - # @param delegate_object [Hash] The delegate object containing configuration flags. - # @param blocks_in_file [Hash] Hash of blocks present in the file. + # @param delegate_object [Hash] The delegate object containing + # configuration flags. + # @param blocks_in_file [Hash] Hash of blocks present in + # the file. # @param menu_blocks [Hash] Hash of menu blocks. # @param link_state [LinkState] Current state of the link. def dump_delobj(blocks_in_file, menu_blocks, link_state) if @delegate_object[:dump_delegate_object] warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') end if @delegate_object[:dump_blocks_in_file] - warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file), - label: 'blocks_in_file') + warn format_and_highlight_dependencies( + compact_and_index_hash(blocks_in_file), + label: 'blocks_in_file' + ) end if @delegate_object[:dump_menu_blocks] - warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks), - label: 'menu_blocks') + warn format_and_highlight_dependencies( + compact_and_index_hash(menu_blocks), + label: 'menu_blocks' + ) end if @delegate_object[:dump_inherited_block_names] warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') @@ -1312,21 +1452,24 @@ warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines') end - # Opens text in an editor for user modification and returns the modified text. + # Opens text in an editor for user modification and + # returns the modified text. # # This method reads the provided text, opens it in the default editor, # and allows the user to modify it. If the user makes changes, the # modified text is returned. If the user exits the editor without # making changes or the editor is closed abruptly, appropriate messages # are displayed. # # @param [String] initial_text The initial text to be edited. - # @param [String] temp_name The base name for the temporary file (default: 'edit_text'). - # @return [String, nil] The modified text, or nil if no changes were made or the editor was closed abruptly. + # @param [String] temp_name The base name for the temporary file + # (default: 'edit_text'). + # @return [String, nil] The modified text, or nil if no changes + # were made or the editor was closed abruptly. def edit_text(initial_text, temp_name: 'edit_text') # Create a temporary file to store the initial text temp_file = Tempfile.new(temp_name) temp_file.write(initial_text) temp_file.rewind @@ -1364,20 +1507,136 @@ temp_file.unlink result_text end - def execute_block_for_state_and_name(selected:, mdoc:, link_state:, - block_source: {}) + # Execute a code block after approval and provide user interaction options. + # + # This method displays required code blocks, asks for user approval, and + # executes the code block if approved. It also allows users to copy the + # code to the clipboard or save it to a file. + # + # @param opts [Hash] Options hash containing configuration settings. + # @param mdoc [YourMDocClass] An instance of the MDoc class. + # + def execute_block_by_type_for_lfls( + selected:, mdoc:, block_source:, link_state: LinkState.new + ) + # !!v selected + # order should not be important other than else clause + if selected.type == BlockType::EDIT + debounce_reset + # !!v link_state.inherited_lines_block + vux_edit_inherited + return :break if pause_user_exit + + next_state_append_code(selected, link_state, []) + + elsif selected.type == BlockType::HISTORY + # !!b + debounce_reset + return :break if execute_block_type_history_ux( + selected: selected, + link_state: link_state + ) == :no_history + + LoadFileLinkState.new(LoadFile::REUSE, link_state) + + elsif selected.type == BlockType::LINK + debounce_reset + execute_block_type_link_with_state(link_block_body: selected.body, + mdoc: mdoc, + selected: selected, + link_state: link_state, + block_source: block_source) + + elsif selected.type == BlockType::LOAD + debounce_reset + code_lines = execute_block_type_load_code_lines(selected) + next_state_append_code(selected, link_state, code_lines) + + elsif selected.type == BlockType::SAVE + debounce_reset + + execute_block_type_save( + code_lines: link_state&.inherited_lines, + selected: selected + ) + + LoadFileLinkState.new(LoadFile::REUSE, link_state) + + elsif selected.type == BlockType::VIEW + debounce_reset + vux_view_inherited(stream: $stderr) + return :break if pause_user_exit + + LoadFileLinkState.new(LoadFile::REUSE, link_state) + + # from CLI + elsif selected.nickname == @delegate_object[:menu_option_exit_name][:line] + debounce_reset + LoadFileLinkState.new(LoadFile::EXIT, link_state) + + elsif @menu_user_clicked_back_link + debounce_reset + LoadFileLinkState.new( + LoadFile::LOAD, + pop_link_history_new_state + ) + + elsif selected.type == BlockType::OPTS + debounce_reset + code_lines = [] + options_state = read_show_options_and_trigger_reuse( + link_state: link_state, + mdoc: @dml_mdoc, + selected: selected + ) + update_menu_base(options_state.options) + + link_state = LinkState.new + next_state_append_code(selected, link_state, code_lines) + + elsif selected.type == BlockType::PORT + debounce_reset + required_lines = execute_block_type_port_code_lines( + mdoc: @dml_mdoc, + selected: selected, + link_state: link_state, + block_source: block_source + ) + next_state_set_code(selected, link_state, required_lines) + + elsif selected.type == BlockType::VARS + debounce_reset + next_state_append_code(selected, link_state, + code_from_vars_block_to_set_environment_variables(selected)) + + elsif debounce_allows + compile_execute_and_trigger_reuse(mdoc: mdoc, + selected: selected, + link_state: link_state, + block_source: block_source) + LoadFileLinkState.new(LoadFile::REUSE, link_state) + + else + LoadFileLinkState.new(LoadFile::REUSE, link_state) + end + end + + def execute_block_for_state_and_name( + selected:, mdoc:, link_state:, block_source: {} + ) lfls = execute_block_by_type_for_lfls( selected: selected, mdoc: mdoc, link_state: link_state, block_source: block_source ) - # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item + # if the same menu is being displayed, collect the display name + # of the selected menu item for use as the default item [lfls.link_state, lfls.load_file == LoadFile::LOAD ? nil : selected.dname, # 2024-08-22 true to quit lfls.load_file == LoadFile::EXIT] end @@ -1391,24 +1650,279 @@ selected: @dml_block_state.block, mdoc: @dml_mdoc, link_state: @dml_link_state, block_source: { document_filename: @delegate_object[:filename], - time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format]) + time_now_date: Time.now.utc.strftime( + @delegate_object[:shell_code_label_time_format] + ) } ) :break if quit end - # Executes a given command and processes its input, output, and error streams. + def execute_block_type_history_ux( + directory: @delegate_object[:document_configurations_directory], + filename: '*', + form: '%{line}', + link_state:, + regexp: "^(?<line>.*)$", + selected: + ) + block_data = HashDelegator.parse_yaml_data_from_body(selected.body) + files_table_rows = read_saved_assets_for_history_table( + filename: filename, + form: form, + path: block_data['directory'] || directory, + regexp: regexp + ) + return :no_history unless files_table_rows + + execute_history_select(files_table_rows, stream: $stderr) + end + + # Handles the processing of a link block in Markdown Execution. + # It loads YAML data from the link_block_body content, + # pushes the state to history, sets environment variables, + # and decides on the next block to load. # - # @param [Array<String>] command the command to execute along with its arguments. - # @yield [stdin, stdout, stderr, thread] if a block is provided, it yields input, output, error lines, and the execution thread. + # @param link_block_body [Array<String>] + # The body content as an array of strings. + # @param mdoc [Object] Markdown document object. + # @param selected [FCB] Selected code block. + # @return [LoadFileLinkState] Object indicating the next + # action for file loading. + def execute_block_type_link_with_state( + link_block_body: [], mdoc: nil, selected: FCB.new, + link_state: LinkState.new, block_source: {} + ) + # !!p link_block_body selected + link_block_data = HashDelegator.parse_yaml_data_from_body(link_block_body) + # !!v link_block_data + ## collect blocks specified by block + # + if mdoc + code_info = mdoc.collect_recursively_required_code( + anyname: selected.pub_name, + label_format_above: @delegate_object[:shell_code_label_format_above], + label_format_below: @delegate_object[:shell_code_label_format_below], + block_source: block_source + ) + code_lines = code_info[:code] + block_names = code_info[:block_names] + dependencies = code_info[:dependencies] + else + block_names = [] + code_lines = [] + dependencies = {} + end + + # load key and values from link block into current environment + # + if link_block_data[LinkKeys::VARS] + code_lines.push BashCommentFormatter.format_comment(selected.pub_name) + (link_block_data[LinkKeys::VARS] || []).each do |(key, value)| + ENV[key] = value.to_s + code_lines.push(assign_key_value_in_bash(key, value)) + end + end + + ## append blocks loaded + # + if (load_expr = link_block_data.fetch(LinkKeys::LOAD, '')).present? + load_filespec = load_filespec_from_expression(load_expr) + if load_filespec + begin + code_lines += File.readlines(load_filespec, + chomp: true) + rescue Errno::ENOENT + report_error($ERROR_INFO) + end + end + end + + # if an eval link block, evaluate code_lines and return its standard output + # + if link_block_data.fetch(LinkKeys::EVAL, + false) || link_block_data.fetch(LinkKeys::EXEC, + false) + code_lines = link_block_data_eval( + link_state, code_lines, selected, link_block_data, + block_source: block_source, + shell: @delegate_object[:block_type_default] + ) + end + + # config next state + # + next_document_filename = write_inherited_lines_to_file(link_state, + link_block_data) + next_block_name = link_block_data.fetch( + LinkKeys::NEXT_BLOCK, + nil + ) || link_block_data.fetch(LinkKeys::BLOCK, nil) || '' + + if link_block_data[LinkKeys::RETURN] + pop_add_current_code_to_head_and_trigger_load( + link_state, block_names, code_lines, + dependencies, selected, next_block_name: next_block_name + ) + + else + next_keep_code = link_state&.keep_code || link_block_data.fetch('keep', false) #/*LinkKeys::KEEP*/ + link_history_push_and_next( + curr_block_name: selected.pub_name, + curr_document_filename: @delegate_object[:filename], + inherited_block_names: + ((link_state&.inherited_block_names || []) + block_names).sort.uniq, + inherited_dependencies: + (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data + inherited_lines: HashDelegator.code_merge( + link_state&.inherited_lines, code_lines + ), + keep_code: link_state&.keep_code, + next_block_name: next_block_name, + next_document_filename: next_document_filename, + next_keep_code: next_keep_code, + next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD + ) + end + end + + def execute_block_type_load_code_lines( + selected, + directory: @delegate_object[:document_configurations_directory], + filename_pattern: @delegate_object[:vars_block_filename_pattern], + glob: @delegate_object[:document_configurations_glob], + view: @delegate_object[:vars_block_filename_view] + ) + # !!p selected + block_data = HashDelegator.parse_yaml_data_from_body(selected.body) + # !!v block_data + if selected_option = select_option_with_metadata( + prompt_title, + Dir.glob( + File.join( + Dir.pwd, + block_data['directory'] || directory, + block_data['glob'] || glob + ) + ).sort.map do |file| + { name: format( + block_data['view'] || view, + NamedCaptureExtractor::extract_named_group2( + file.match( + Regexp.new(block_data['filename_pattern'] || + filename_pattern) + ) + ) + ), + oname: file } + end, + simple_menu_options + ) + File.readlines(selected_option.oname, chomp: true) + else + warn "No matching files found" ### + end + end + + # Collects required code lines based on the selected block and + # the delegate object's configuration. + # If the block type is VARS, it also sets environment variables + # based on the block's content. + # + # @param mdoc [YourMDocClass] An instance of the MDoc class. + # @param selected [Hash] The selected block. + # @return [Array<String>] Required code blocks as an array of lines. + def execute_block_type_port_code_lines(mdoc:, selected:, block_source:, + link_state: LinkState.new) + required = mdoc.collect_recursively_required_code( + anyname: selected.pub_name, + label_format_above: @delegate_object[:shell_code_label_format_above], + label_format_below: @delegate_object[:shell_code_label_format_below], + block_source: block_source + ) # !!t 'required' + dependencies = ( + link_state&.inherited_dependencies || {} + ).merge(required[:dependencies] || {}) + required[:unmet_dependencies] = ( + required[:unmet_dependencies] || [] + ) - (link_state&.inherited_block_names || []) + if required[:unmet_dependencies].present? + ### filter against link_state.inherited_block_names + + warn format_and_highlight_dependencies( + dependencies, highlight: required[:unmet_dependencies] + ) + runtime_exception( + :runtime_exception_error_level, + 'unmet_dependencies, flag: runtime_exception_error_level', + required[:unmet_dependencies] + ) + elsif @delegate_object[:dump_dependencies] + warn format_and_highlight_dependencies( + dependencies, + highlight: [@delegate_object[:block_name]] + ) + end + + if selected[:type] == BlockType::OPTS + # body of blocks is returned as a list of lines to be read an YAML + HashDelegator.code_merge(required[:blocks].map(&:body).flatten(1)) + else + code_lines = if selected.type == BlockType::VARS + code_from_vars_block_to_set_environment_variables(selected) + else + [] + end + HashDelegator.code_merge(link_state&.inherited_lines, + required[:code] + code_lines) + end + end + + def execute_block_type_save(code_lines:, selected:) + # !!p code_lines, selected + block_data = HashDelegator.parse_yaml_data_from_body(selected.body) + # !!v block_data + directory_glob = if block_data['directory'] + # !!b + File.join( + block_data['directory'], + block_data['glob'] || + @delegate_object[:document_saved_lines_glob].split('/').last + ) + else + # !!b + @delegate_object[:document_saved_lines_glob] + end + # !!v directory_glob + + save_filespec_from_expression(directory_glob).tap do |save_filespec| + if save_filespec + begin + File.write(save_filespec, + HashDelegator.join_code_lines(code_lines)) + rescue Errno::ENOENT + report_error($ERROR_INFO) + end + end + end + end + + # Executes a given command and processes its + # input, output, and error streams. + # + # @param [Array<String>] command the command to + # execute along with its arguments. + # @yield [stdin, stdout, stderr, thread] if a block is provided, it + # yields input, output, error lines, and the execution thread. # @return [Integer] the exit status of the executed command (0 to 255). # # @example - # status = execute_command_with_streams(['ls', '-la']) do |stdin, stdout, stderr, thread| + # status = execute_command_with_streams(['ls', '-la']) \ + # do |stdin, stdout, stderr, thread| # puts "STDOUT: #{stdout}" if stdout # puts "STDERR: #{stderr}" if stderr # end # puts "Command exited with status: #{status}" def execute_command_with_streams(command) @@ -1458,12 +1972,12 @@ # repeat select+display until user exits pause_now = false row_attrib = :row loop do - if pause_now - break if prompt_select_continue == MenuState::EXIT + if pause_now && (prompt_select_continue == MenuState::EXIT) + break end # menu with Back and Facet options at top case (name = prompt_select_code_filename( [exit_prompt, @@ -1484,28 +1998,29 @@ "#{info[:size]} bytes" stream.puts( File.readlines(file.file, chomp: false).map.with_index do |line, ind| format(' %s. %s', - AnsiString.new(format('% 4d', ind + 1)).send(:violet), line) + AnsiString.new(format('% 4d', ind + 1)).send(:violet), + line) end ) pause_now = pause_refresh end end end - def execute_inherited_save - save_filespec = save_filespec_from_expression( - document_name_in_glob_as_file_name( - @dml_link_state.document_filename, - @delegate_object[:document_saved_lines_glob] - ) + def execute_inherited_save( + code_lines: @dml_link_state.inherited_lines + ) + return unless save_filespec = save_filespec_from_expression( + document_name_in_glob_as_file_name ) - if save_filespec && !write_file_with_directory_creation( - save_filespec, - HashDelegator.join_code_lines(@dml_link_state.inherited_lines) + + unless write_file_with_directory_creation( + content: HashDelegator.join_code_lines(code_lines), + filespec: save_filespec ) :break end end @@ -1524,106 +2039,42 @@ keep_code: keep_code } end # Executes a block of code that has been approved for execution. - # It sets the script block name, writes command files if required, and handles the execution + # It sets the script block name, writes command files if + # required, + # and handles the execution # including output formatting and summarization. # # @param required_lines [Array<String>] The lines of code to be executed. # @param selected [FCB] The selected functional code block object. - def execute_required_lines(required_lines: [], selected: FCB.new) + def execute_required_lines( + required_lines: [], selected: FCB.new, shell: + ) if @delegate_object[:save_executed_script] write_command_file(required_lines: required_lines, - selected: selected) + selected: selected, + shell: shell) end if @dml_block_state calc_logged_stdout_filename(block_name: @dml_block_state.block.oname) end - format_and_execute_command(code_lines: required_lines) + format_and_execute_command(code_lines: required_lines, shell: shell) post_execution_process end - # Execute a code block after approval and provide user interaction options. + # Retrieves a specific data symbol from the delegate object, + # converts it to a string, and applies a color style + # based on the specified color symbol. # - # This method displays required code blocks, asks for user approval, and - # executes the code block if approved. It also allows users to copy the - # code to the clipboard or save it to a file. - # - # @param opts [Hash] Options hash containing configuration settings. - # @param mdoc [YourMDocClass] An instance of the MDoc class. - # - def execute_block_by_type_for_lfls(selected:, mdoc:, block_source:, - link_state: LinkState.new) - if selected.shell == BlockType::LINK - debounce_reset - push_link_history_and_trigger_load(link_block_body: selected.body, - mdoc: mdoc, - selected: selected, - link_state: link_state, - block_source: block_source) - - # from CLI - elsif selected.nickname == @delegate_object[:menu_option_exit_name][:line] - debounce_reset - LoadFileLinkState.new(LoadFile::EXIT, link_state) - - elsif @menu_user_clicked_back_link - debounce_reset - # pop_link_history_new_state - LoadFileLinkState.new( - LoadFile::LOAD, - pop_link_history_new_state - ) - - elsif selected.shell == BlockType::OPTS - debounce_reset - code_lines = [] - options_state = read_show_options_and_trigger_reuse( - link_state: link_state, - mdoc: @dml_mdoc, - selected: selected - ) - update_menu_base(options_state.options) - - ### options_state.load_file_link_state - link_state = LinkState.new - next_state_append_code(selected, link_state, code_lines) - - elsif selected.shell == BlockType::PORT - debounce_reset - required_lines = collect_required_code_lines( - mdoc: @dml_mdoc, - selected: selected, - link_state: link_state, - block_source: block_source - ) - next_state_set_code(selected, link_state, required_lines) - - elsif selected.shell == BlockType::VARS - debounce_reset - next_state_append_code(selected, link_state, - set_environment_variables_for_block(selected)) - - elsif debounce_allows - compile_execute_and_trigger_reuse(mdoc: mdoc, - selected: selected, - link_state: link_state, - block_source: block_source) - LoadFileLinkState.new(LoadFile::REUSE, link_state) - - else - LoadFileLinkState.new(LoadFile::REUSE, link_state) - end - end - - # Retrieves a specific data symbol from the delegate object, converts it to a string, - # and applies a color style based on the specified color symbol. - # - # @param default [String] The default value if the data symbol is not found. - # @param data_sym [Symbol] The symbol key to fetch data from the delegate object. - # @param color_sym [Symbol] The symbol key to fetch the color option for styling. + # @param default [String] The default value + # if the data symbol is not found. + # @param data_sym [Symbol] The symbol key to + # fetch data from the delegate object. + # @param color_sym [Symbol] The symbol key to + # fetch the color option for styling. # @return [String] The color-styled string. def fetch_color(default: '', data_sym: :execution_report_preview_head, color_sym: :execution_report_preview_frame_color) data_string = @delegate_object.fetch(data_sym, default).to_s @@ -1643,47 +2094,45 @@ end { size: file_size, lines: line_count } end - def format_and_execute_command(code_lines:) + def format_and_execute_command(code_lines:, shell:) formatted_command = code_lines.flatten.join("\n") @fout.fout fetch_color(data_sym: :script_execution_head, color_sym: :script_execution_frame_color) - command_execute(formatted_command, args: @pass_args) + command_execute(formatted_command, args: @pass_args, shell: shell) @fout.fout fetch_color(data_sym: :script_execution_tail, color_sym: :script_execution_frame_color) end # Format expression using environment variables and run state def format_expression(expr) data = link_load_format_data - ENV.each { |key, value| data[key] = value } + ENV.each { |key, value| data[key.to_sym] = value } format(expr, data) end - # Formats multiline body content as a title string. - # indents all but first line with two spaces so it displays correctly in menu - # @param body_lines [Array<String>] The lines of body content. - # @return [String] Formatted title. - def format_multiline_body_as_title(body_lines) - body_lines.map.with_index do |line, index| - index.zero? ? line : " #{line}" - end.join("\n") + "\n" - end - - # Formats a string based on a given context and applies color styling to it. - # It retrieves format and color information from the delegate object and processes accordingly. + # Formats a string based on a given context and + # applies color styling to it. + # It retrieves format and color information from + # the delegate object and processes accordingly. # - # @param default [String] The default value if the format symbol is not found (unused in current implementation). + # @param default [String] The default value if the format symbol + # is not found (unused in current implementation). # @param context [Hash] Contextual data used for string formatting. - # @param format_sym [Symbol] Symbol key to fetch the format string from the delegate object. - # @param color_sym [Symbol] Symbol key to fetch the color option for string styling. + # @param format_sym [Symbol] Symbol key to fetch the format string + # from the delegate object. + # @param color_sym [Symbol] Symbol key to fetch the color option + # for string styling. # @return [String] The formatted and color-styled string. - def format_references_send_color(default: '', context: {}, - format_sym: :output_execution_label_format, - color_sym: :execution_report_preview_frame_color) + def format_references_send_color( + color_sym: :execution_report_preview_frame_color, + context: {}, + default: '', + format_sym: :output_execution_label_format + ) formatted_string = format(@delegate_object.fetch(format_sym, ''), context).to_s string_send_color(formatted_string, color_sym) end @@ -1721,44 +2170,16 @@ "#{SecureRandom.urlsafe_base64}#{ext}" end File.join(Dir.tmpdir, filename) end - # Processes a block to generate its summary, modifying its attributes based on various matching criteria. - # It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname. - # - # @param fcb [Object] An object representing a functional code block. - # @return [Object] The modified functional code block with updated summary attributes. - def get_block_summary(fcb) - return fcb unless @delegate_object[:bash] - - fcb.call = fcb.title.match(Regexp.new(@delegate_object[:block_calls_scan]))&.fetch(1, nil) - titlexcall = fcb.call ? fcb.title.sub("%#{fcb.call}", '') : fcb.title - bm = extract_named_captures_from_option(titlexcall, - @delegate_object[:block_name_match]) - - shell_color_option = SHELL_COLOR_OPTIONS[fcb.shell] - - if @delegate_object[:block_name_nick_match].present? && fcb.oname =~ Regexp.new(@delegate_object[:block_name_nick_match]) - fcb.nickname = $~[0] - fcb.title = fcb.oname = format_multiline_body_as_title(fcb.body) - else - fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall - end - - fcb.dname = HashDelegator.indent_all_lines( - apply_shell_color_option(fcb.oname, shell_color_option), - fcb.indent - ) - - fcb # &br - end - # Updates the delegate object's state based on the provided block state. - # It sets the block name and determines if the user clicked the back link in the menu. + # It sets the block name and determines + # if the user clicked the back link in the menu. # - # @param block_state [Object] An object representing the state of a block in the menu. + # @param block_state [Object] An object representing the + # state of a block in the menu. def handle_back_or_continue(block_state) return if block_state.nil? unless [MenuState::BACK, MenuState::CONTINUE].include?(block_state.state) return @@ -1788,21 +2209,24 @@ @process_cv.signal end end end - def history_files(link_state, order: :chronological, direction: :reverse) + def history_files( + link_state, + direction: :reverse, + filename: nil, + home: Dir.pwd, + order: :chronological, + path: '' + ) + # !!v filename, 'path', path + # !!v File.join(home, path, filename) files = Dir.glob( - File.join( - @delegate_object[:saved_script_folder], - SavedAsset.new( - filename: @delegate_object[:filename], - saved_asset_format: shell_escape_asset_format(link_state) - ).generate_name - ) + File.join(home, path, filename) ) - + # !!v files sorted_files = case order when :alphabetical files.sort when :chronological files.sort_by { |file| File.mtime(file) } @@ -1828,35 +2252,60 @@ in_fenced_block: false, headings: [] } end + public + # Iterates through blocks in a file, applying the provided block to each line. # The iteration only occurs if the file exists. # @yield [Symbol] :filter Yields to obtain selected messages for processing. def iter_blocks_from_nested_files(&block) return unless check_file_existence(@delegate_object[:filename]) state = initial_state selected_types = yield :filter - cfile.readlines(@delegate_object[:filename], - import_paths: @delegate_object[:import_paths]&.split(':')).each do |nested_line| + cfile.readlines( + @delegate_object[:filename], + import_paths: @delegate_object[:import_paths]&.split(':') + ).each do |nested_line| if nested_line update_line_and_block_state(nested_line, state, selected_types, &block) end end end + def iter_source_blocks(source, &block) + # !!v source + case source + when 1 + blocks_from_nested_files.each(&block) + when 2 + @dml_blocks_in_file.each(&block) + when 3 + @dml_menu_blocks.each(&block) + else + iter_blocks_from_nested_files do |btype, fcb| + case btype + when :blocks + yield fcb + when :filter + %i[blocks] + end + end + end + end + def link_block_data_eval(link_state, code_lines, selected, link_block_data, - block_source:) + block_source:, shell:) all_code = HashDelegator.code_merge(link_state&.inherited_lines, code_lines) output_lines = [] Tempfile.open do |file| - cmd = "#{@delegate_object[:shell]} #{file.path}" + cmd = "#{shell} #{file.path}" file.write(all_code.join("\n")) file.rewind if link_block_data.fetch(LinkKeys::EXEC, false) @run_state.files = StreamsOut.new @@ -1942,21 +2391,42 @@ batch_random: @run_state.batch_random, block_name: @delegate_object[:block_name], document_filename: File.basename(@delegate_object[:filename]), document_filespec: @delegate_object[:filename], home: Dir.pwd, - started_at: Time.now.utc.strftime(@delegate_object[:execute_command_title_time_format]) + started_at: Time.now.utc.strftime( + @delegate_object[:execute_command_title_time_format] + ) } end - # Loads auto blocks based on delegate object settings and updates if new filename is detected. + def list_blocks + # !!b + message = @delegate_object[:list_blocks_message] + block_eval = @delegate_object[:list_blocks_eval] + # !!v message block_eval + + list = [] + iter_source_blocks(@delegate_object[:list_blocks_type]) do |block| + # !!v block + list << (block_eval.present? ? eval(block_eval) : block.send(message)) + end + list.compact! + # !!v list + + @fout.fout_list(list) + end + + # Loads auto blocks based on delegate object settings and updates + # if new filename is detected. # Executes a specified block once per filename. # @param all_blocks [Array] Array of all block elements. # @return [Boolean, nil] True if values were modified, nil otherwise. def load_auto_opts_block(all_blocks, mdoc:) block_name = @delegate_object[:document_load_opts_block_name] - unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename] + unless block_name.present? && + @most_recent_loaded_filename != @delegate_object[:filename] return end block = HashDelegator.block_find(all_blocks, :oname, block_name) return unless block @@ -1971,28 +2441,32 @@ true end def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil) + # !!b if @delegate_object[:block_name].present? block = all_blocks.find do |item| item.pub_name == @delegate_object[:block_name] end source = OpenStruct.new(block_name_from_ui: false) else block_state = wait_for_user_selected_block(all_blocks, menu_blocks, default) + return if block_state.nil? + block = block_state.block source = OpenStruct.new(block_name_from_ui: true) state = block_state.state end SelectedBlockMenuState.new(block, source, state) end # format + glob + select for file in load block - # name has references to ENV vars and doc and batch vars incl. timestamp + # name has references to ENV vars and doc and batch vars + # incl. timestamp def load_filespec_from_expression(expression) # Process expression with embedded formatting expanded_expression = formatted_expression(expression) # Handle wildcards or direct file specification @@ -2027,12 +2501,13 @@ end end def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:) - if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name] - # &bsp 'pause cli control, allow user to select block' + if block_name_from_cli && + @cli_block_name == @menu_base_options[:menu_persist_block_name] + # !!b 'pause cli control, allow user to select block' block_name_from_cli = false now_using_cli = false @menu_base_options[:block_name] = @delegate_object[:block_name] = \ link_state.block_name = @@ -2050,45 +2525,48 @@ @delegate_object.merge!(nopts) end [menu_blocks, mdoc] end - ## Handles the file loading and returns the blocks in the file and MDoc instance + ## Handles the file loading and returns the blocks + # in the file and MDoc instance # def mdoc_menu_and_blocks_from_nested_files(link_state) all_blocks, mdoc = mdoc_and_blocks_from_nested_files # recreate menu with new options # - all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block( - all_blocks, mdoc: mdoc - ) + if load_auto_opts_block(all_blocks, mdoc: mdoc) + all_blocks, mdoc = mdoc_and_blocks_from_nested_files + end menu_blocks = mdoc.fcbs_per_options(@delegate_object) + + variable_expansions!(menu_blocks: menu_blocks, link_state: link_state) add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state) + ### compress empty lines HashDelegator.delete_consecutive_blank_lines!(menu_blocks) HashDelegator.tables_into_columns!(menu_blocks, @delegate_object) - [all_blocks, menu_blocks, mdoc] # &br + [all_blocks, menu_blocks, mdoc] # !!r end - def menu_add_disabled_option(name) - raise unless name.present? + def menu_add_disabled_option(document_glob) + raise unless document_glob.present? raise if @dml_menu_blocks.nil? - block = @dml_menu_blocks.find { |item| item.oname == name } + block = @dml_menu_blocks.find { |item| item.oname == document_glob } # create menu item when it is needed (count > 0) # return unless block.nil? - # append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: MenuState::LOAD) chrome_block = FCB.new( chrome: true, disabled: '', dname: HashDelegator.new(@delegate_object).string_send_color( - name, :menu_inherited_lines_color + document_glob, :menu_inherited_lines_color ), oname: formatted_name ) if insert_at_top @@ -2096,23 +2574,27 @@ else @dml_menu_blocks.push(chrome_block) end end - # Formats and optionally colors a menu option based on delegate object's configuration. - # @param option_symbol [Symbol] The symbol key for the menu option in the delegate object. - # @return [String] The formatted and possibly colored value of the menu option. + # Formats and optionally colors a menu option based on delegate + # object's configuration. + # @param option_symbol [Symbol] The symbol key for the menu option + # in the delegate object. + # @return [String] The formatted and possibly colored value + # of the menu option. def menu_chrome_colored_option(option_symbol = :menu_option_back_name) formatted_option = menu_chrome_formatted_option(option_symbol) return formatted_option unless @delegate_object[:menu_chrome_color] string_send_color(formatted_option, :menu_chrome_color) end # Formats a menu option based on the delegate object's configuration. # It safely evaluates the value of the option and optionally formats it. - # @param option_symbol [Symbol] The symbol key for the menu option in the delegate object. + # @param option_symbol [Symbol] The symbol key for the menu option in + # the delegate object. # @return [String] The formatted or original value of the menu option. def menu_chrome_formatted_option(option_symbol = :menu_option_back_name) option_value = HashDelegator.safeval(@delegate_object.fetch( option_symbol, '' )) @@ -2135,23 +2617,27 @@ # super end end def next_state_append_code(selected, link_state, code_lines) - next_state_set_code(selected, link_state, HashDelegator.code_merge( - link_state&.inherited_lines, code_lines - )) + next_state_set_code( + selected, + link_state, + HashDelegator.code_merge(link_state&.inherited_lines, code_lines) + ) end def next_state_set_code(selected, link_state, code_lines) block_names = [] dependencies = {} link_history_push_and_next( curr_block_name: selected.pub_name, curr_document_filename: @delegate_object[:filename], - inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq, - inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data + inherited_block_names: + ((link_state&.inherited_block_names || []) + block_names).sort.uniq, + inherited_dependencies: + (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data inherited_lines: HashDelegator.code_merge(code_lines), keep_code: link_state&.keep_code, next_block_name: '', next_document_filename: @delegate_object[:filename], next_keep_code: false, @@ -2181,24 +2667,28 @@ } end def output_labeled_value(label, value, level) @fout.lout format_references_send_color( - context: { name: string_send_color(label, :output_execution_label_name_color), - value: string_send_color(value.to_s, - :output_execution_label_value_color) }, + context: { + name: string_send_color(label, :output_execution_label_name_color), + value: string_send_color(value.to_s, + :output_execution_label_value_color) + }, format_sym: :output_execution_label_format ), level: level end def pause_user_exit @delegate_object[:pause_after_script_execution] && prompt_select_continue == MenuState::EXIT end - def pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines, - dependencies, selected, next_block_name: nil) + def pop_add_current_code_to_head_and_trigger_load( + link_state, block_names, code_lines, + dependencies, selected, next_block_name: nil + ) pop = @link_history.pop # updatable if pop.document_filename next_state = LinkState.new( block_name: pop.block_name, document_filename: pop.document_filename, @@ -2212,11 +2702,12 @@ @link_history.push(next_state) next_state.block_name = nil LoadFileLinkState.new(LoadFile::LOAD, next_state) else - # no history exists; must have been called independently => retain script + # no history exists; must have been called independently + # => retain script link_history_push_and_next( curr_block_name: selected.pub_name, curr_document_filename: @delegate_object[:filename], inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq, @@ -2235,11 +2726,12 @@ end # This method handles the back-link operation in the Markdown execution context. # It updates the history state for the next block. # - # @return [LinkState] An object indicating the state for the next block. + # @return [LinkState] An object indicating the state for + # the next block. def pop_link_history_new_state pop = @link_history.pop peek = @link_history.peek LinkState.new( document_filename: pop.document_filename, @@ -2254,10 +2746,11 @@ output_execution_summary fout_execution_report if @delegate_object[:output_execution_report] end # Prepare the blocks menu by adding labels and other necessary details. + # Remove filtered blocks. # # @param all_blocks [Array<Hash>] The list of blocks from the file. # @param opts [Hash] The options hash. # @return [Array<Hash>] The updated blocks menu. def prepare_blocks_menu(menu_blocks) @@ -2267,19 +2760,19 @@ fcb, %i[block_name_include_match block_name_wrapper_match] ) fcb.name = fcb.dname - fcb.label = BlockLabel.make( - body: fcb.body, - filename: @delegate_object[:filename], - headings: fcb.headings, - menu_blocks_with_docname: @delegate_object[:menu_blocks_with_docname], - menu_blocks_with_headings: @delegate_object[:menu_blocks_with_headings], - text: fcb.text, - title: fcb.title - ) + # fcb.label = BlockLabel.make( + # body: fcb.body, + # filename: @delegate_object[:filename], + # headings: fcb.headings, + # menu_blocks_with_docname: @delegate_object[:menu_blocks_with_docname], + # menu_blocks_with_headings: @delegate_object[:menu_blocks_with_headings], + # text: fcb.text, + # title: fcb.title + # ) fcb.to_h end.compact end def print_formatted_option(key, value) @@ -2288,23 +2781,10 @@ print string_send_color(formatted_str, :menu_opts_set_color) end # private - def process_block_based_on_type(blocks, btype, fcb) - case btype - when :blocks - blocks.push(get_block_summary(fcb)) - when :filter - %i[blocks line] - when :line - unless @delegate_object[:no_chrome] - create_and_add_chrome_blocks(blocks, fcb) - end - end - end - def process_string_array(arr, begin_pattern: nil, end_pattern: nil, scan1: nil, format1: nil) in_block = !begin_pattern.present? collected_lines = [] @@ -2312,11 +2792,11 @@ if in_block if end_pattern.present? && line.match?(end_pattern) in_block = false elsif scan1.present? if format1.present? - caps = extract_named_captures_from_option(line, scan1) + caps = NamedCaptureExtractor::extract_named_groups(line, scan1) if caps formatted = format(format1, caps) collected_lines << formatted end else @@ -2362,52 +2842,64 @@ gets.chomp rescue Interrupt nil end - # Prompts the user to enter a path or name to substitute into the wildcard expression. - # If interrupted by the user (e.g., pressing Ctrl-C), it returns nil. + # Prompts the user to enter a path or name to substitute + # into the wildcard expression. + # If interrupted by the user (e.g., pressing Ctrl-C), it + # returns nil. # - # @param filespec [String] the wildcard expression to be substituted - # @return [String, nil] the resolved path or substituted expression, or nil if interrupted + # @param filespec [String] the wildcard expression to be + # substituted + # @return [String, nil] the resolved path or substituted + # expression, or nil if interrupted def prompt_for_filespec_with_wildcard(filespec) puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec }) puts @delegate_object[:prompt_enter_filespec] begin - input = gets.chomp + input = $stdin.gets.chomp PathUtils.resolve_path_or_substitute(input, filespec) rescue Interrupt puts "\nOperation interrupted. Returning nil." nil end end ## - # Presents a menu to the user for approving an action and performs additional tasks based on the selection. - # The function provides options for approval, rejection, copying data to clipboard, or saving data to a file. + # Presents a menu to the user for approving an action + # and performs additional tasks based on the selection. + # The function provides options for approval, rejection, + # copying data to clipboard, or saving data to a file. # # @param opts [Hash] A hash containing various options for the menu. - # @param required_lines [Array<String>] Lines of text or code that are subject to user approval. + # @param required_lines [Array<String>] Lines of text or + # code that are subject to user approval. # - # @option opts [String] :prompt_approve_block Prompt text for the approval menu. - # @option opts [String] :prompt_yes Text for the 'Yes' choice in the menu. - # @option opts [String] :prompt_no Text for the 'No' choice in the menu. - # @option opts [String] :prompt_script_to_clipboard Text for the 'Copy to Clipboard' choice in the menu. - # @option opts [String] :prompt_save_script Text for the 'Save to File' choice in the menu. + # @option opts [String] :prompt_approve_block + # Prompt text for the approval menu. + # @option opts [String] :prompt_yes + # Text for the 'Yes' choice in the menu. + # @option opts [String] :prompt_no + # Text for the 'No' choice in the menu. + # @option opts [String] :prompt_script_to_clipboard Text + # for the 'Copy to Clipboard' choice in the menu. + # @option opts [String] :prompt_save_script Text for the + # 'Save to File' choice in the menu. # - # @return [Boolean] Returns true if the user approves (selects 'Yes'), false otherwise. + # @return [Boolean] Returns true if the user approves (selects 'Yes'), + # false otherwise. ## def prompt_for_user_approval(required_lines:, selected:) # Present a selection menu for user approval. sel = @prompt.select( string_send_color(@delegate_object[:prompt_approve_block], :prompt_color_after_script_execution), filter: true ) do |menu| - # sel = @prompt.select(@delegate_object[:prompt_approve_block], filter: true) do |menu| menu.default MenuOptions::YES menu.choice @delegate_object[:prompt_yes], MenuOptions::YES menu.choice @delegate_object[:prompt_no], MenuOptions::NO menu.choice @delegate_object[:prompt_script_to_clipboard], MenuOptions::SCRIPT_TO_CLIPBOARD @@ -2416,11 +2908,14 @@ end if sel == MenuOptions::SCRIPT_TO_CLIPBOARD copy_to_clipboard(required_lines) elsif sel == MenuOptions::SAVE_SCRIPT - save_to_file(required_lines: required_lines, selected: selected) + save_to_file( + required_lines: required_lines, selected: selected, + shell: selected.shell + ) end sel == MenuOptions::YES end @@ -2466,100 +2961,38 @@ end # user prompt to exit if the menu will be displayed again # def prompt_user_exit(block_name_from_cli:, selected:) - selected.shell == BlockType::BASH && + selected.type == BlockType::SHELL && @delegate_object[:pause_after_script_execution] && prompt_select_continue == MenuState::EXIT end - # Handles the processing of a link block in Markdown Execution. - # It loads YAML data from the link_block_body content, pushes the state to history, - # sets environment variables, and decides on the next block to load. - # - # @param link_block_body [Array<String>] The body content as an array of strings. - # @param mdoc [Object] Markdown document object. - # @param selected [FCB] Selected code block. - # @return [LoadFileLinkState] Object indicating the next action for file loading. - def push_link_history_and_trigger_load(link_block_body: [], mdoc: nil, selected: FCB.new, - link_state: LinkState.new, block_source: {}) - link_block_data = HashDelegator.parse_yaml_data_from_body(link_block_body) + def publish_for_external_automation(message:) + return if @delegate_object[:publish_document_file_name].empty? - ## collect blocks specified by block - # - if mdoc - code_info = mdoc.collect_recursively_required_code( - anyname: selected.pub_name, - label_format_above: @delegate_object[:shell_code_label_format_above], - label_format_below: @delegate_object[:shell_code_label_format_below], - block_source: block_source - ) - code_lines = code_info[:code] - block_names = code_info[:block_names] - dependencies = code_info[:dependencies] - else - block_names = [] - code_lines = [] - dependencies = {} - end + pipe_path = absolute_path(@delegate_object[:publish_document_file_name]) - # load key and values from link block into current environment - # - if link_block_data[LinkKeys::VARS] - code_lines.push BashCommentFormatter.format_comment(selected.pub_name) - (link_block_data[LinkKeys::VARS] || []).each do |(key, value)| - ENV[key] = value.to_s - code_lines.push(assign_key_value_in_bash(key, value)) + case @delegate_object[:publish_document_file_mode] + when 'append' + File.write(pipe_path, message + "\n", mode: 'a') + when 'fifo' + unless @vux_pipe_open + unless File.exist?(pipe_path) + FileUtils.mkfifo(pipe_path) + @vux_pipe_created = pipe_path + end + @vux_pipe_open = File.open(pipe_path, 'w') end - end - - ## append blocks loaded - # - if (load_expr = link_block_data.fetch(LinkKeys::LOAD, '')).present? - load_filespec = load_filespec_from_expression(load_expr) - if load_filespec - code_lines += File.readlines(load_filespec, - chomp: true) - end - end - - # if an eval link block, evaluate code_lines and return its standard output - # - if link_block_data.fetch(LinkKeys::EVAL, - false) || link_block_data.fetch(LinkKeys::EXEC, - false) - code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, - block_source: block_source) - end - - next_document_filename = write_inherited_lines_to_file(link_state, - link_block_data) - next_block_name = link_block_data.fetch(LinkKeys::NEXT_BLOCK, - nil) || link_block_data.fetch(LinkKeys::BLOCK, - nil) || '' - - if link_block_data[LinkKeys::RETURN] - pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines, - dependencies, selected, next_block_name: next_block_name) - + @vux_pipe_open.puts(message + "\n") + @vux_pipe_open.flush + when 'write' + File.write(pipe_path, message) else - next_keep_code = link_state&.keep_code || link_block_data.fetch('keep', false) #/*LinkKeys::KEEP*/ - link_history_push_and_next( - curr_block_name: selected.pub_name, - curr_document_filename: @delegate_object[:filename], - inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq, - inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data - inherited_lines: HashDelegator.code_merge( - link_state&.inherited_lines, code_lines - ), - keep_code: link_state&.keep_code, - next_block_name: next_block_name, - next_document_filename: next_document_filename, - next_keep_code: next_keep_code, - next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD - ) + raise 'Invalid publish_document_file_mode:' \ + " #{@delegate_object[:publish_document_file_mode]}" end end # Handle expression with wildcard characters # allow user to select or enter @@ -2568,55 +3001,79 @@ { expr: filespec }) puts @delegate_object[:prompt_enter_filespec] gets.chomp end - def read_saved_assets_for_history_table - files = history_files(@dml_link_state).sort - files.map do |file| - if Regexp.new(@delegate_object[:saved_asset_match]) =~ file - begin - OpenStruct.new( - file: file, - row: format( - @delegate_object[:saved_history_format], - # create with default '*' so unknown parameters are given a wildcard - $~.names.each_with_object(Hash.new('*')) do |name, hash| - hash[name.to_sym] = $~[name] - end - ) - ) - rescue KeyError - # pp $!, $@ - warn "Cannot format with: #{@delegate_object[:saved_history_format]}" - error_handler('saved_history_format') - return nil - end - else + def read_saved_assets_for_history_table( + asset: nil, + filename: nil, + form: @delegate_object[:saved_history_format], + path: @delegate_object[:saved_script_folder], + regexp: @delegate_object[:saved_asset_match] + ) + history_files( + @dml_link_state, + filename: + asset.present? ? saved_asset_filename(asset, + @dml_link_state) : filename, + path: path + )&.map do |file| + unless Regexp.new(regexp) =~ file warn "Cannot parse name: #{file}" next end + + saved_asset = saved_asset_for_history( + file: file, form: form, + match_info: $LAST_MATCH_INFO + ) + saved_asset == :break ? nil : saved_asset end&.compact end - # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output. + def saved_asset_for_history( + file:, form:, match_info: + ) + begin + OpenStruct.new( + file: file[(Dir.pwd.length + 1)..-1], + full: file, + row: format( + form, + # default '*' so unknown parameters are given a wildcard + match_info.names.each_with_object(Hash.new('*')) do |name, hash| + hash[name.to_sym] = match_info[name] + end + ) + ) + rescue KeyError + # pp $!, $@ + warn "Cannot format with: #{@delegate_object[:saved_history_format]}" + error_handler('saved_history_format') + return :break + end + end + + # Processes YAML data from the selected menu item, updating delegate + # objects and optionally printing formatted output. # @param selected [Hash] Selected item from the menu containing a YAML body. # @param tgt2 [Hash, nil] An optional target hash to update with YAML data. - # @return [LoadFileLinkState] An instance indicating the next action for loading files. + # @return [LoadFileLinkState] An instance indicating the + # next action for loading files. def read_show_options_and_trigger_reuse(selected:, mdoc:, link_state: LinkState.new) obj = {} # concatenated body of all required blocks loaded a YAML data = (YAML.load( - collect_required_code_lines( + execute_block_type_port_code_lines( mdoc: mdoc, selected: selected, link_state: link_state, block_source: {} ).join("\n") ) || {}).transform_keys(&:to_sym) - if selected.shell == BlockType::OPTS + if selected.type == BlockType::OPTS obj = data else (data || []).each do |key, value| sym_key = key.to_sym obj[sym_key] = value @@ -2636,23 +3093,37 @@ # Registers console attributes by modifying the options hash. # This method handles terminal resizing and adjusts the console dimensions # and pagination settings based on the current terminal size. # - # @param opts [Hash] a hash containing various options for the console settings. - # - :console_width [Integer, nil] The width of the console. If not provided or if the terminal is resized, it will be set to the current console width. - # - :console_height [Integer, nil] The height of the console. If not provided or if the terminal is resized, it will be set to the current console height. - # - :console_winsize [Array<Integer>, nil] The dimensions of the console [height, width]. If not provided or if the terminal is resized, it will be set to the current console dimensions. - # - :select_page_height [Integer, nil] The height of the page for selection. If not provided or if not positive, it will be set to the maximum of (console height - 3) or 4. - # - :per_page [Integer, nil] The number of items per page. If :select_page_height is not provided or if not positive, it will be set to the maximum of (console height - 3) or 4. + # @param opts [Hash] a hash containing various options + # for the console settings. + # - :console_width [Integer, nil] The width of the console. If not + # provided or if the terminal is resized, it will be set to the + # current console width. + # - :console_height [Integer, nil] The height of the console. + # If not provided or if the terminal is resized, it will be set + # to the current console height. + # - :console_winsize [Array<Integer>, nil] The dimensions of the + # console [height, width]. If not provided or if the terminal + # is resized, it will be set to the current console dimensions. + # - :select_page_height [Integer, nil] The height of the page for + # selection. If not provided or if not positive, it will be set + # to the maximum of (console height - 3) or 4. + # - :per_page [Integer, nil] The number of items per page. If + # :select_page_height is not provided or if not positive, it + # will be set to the maximum of (console height - 3) or 4. # - # @raise [StandardError] If an error occurs during the process, it will be caught and handled by calling HashDelegator.error_handler with 'register_console_attributes' and { abort: true }. + # @raise [StandardError] If an error occurs during the process, it + # will be caught and handled by calling HashDelegator.error_handler + # with 'register_console_attributes' and { abort: true }. # # @example # opts = { console_width: nil, console_height: nil, select_page_height: nil } # register_console_attributes(opts) - # # opts will be updated with the current console dimensions and pagination settings. + # # opts will be updated with the current console dimensions + # # and pagination settings. def register_console_attributes(opts) if (resized = @delegate_object[:menu_resize_terminal]) resize_terminal end @@ -2669,44 +3140,56 @@ rescue StandardError HashDelegator.error_handler('register_console_attributes', { abort: true }) end + def report_error(err) + # Handle ENOENT error + @run_state.aborted_at = Time.now.utc + @run_state.error_message = err.message + @run_state.error = err + @run_state.files.append_stream_line(ExecutionStreams::STD_ERR, + @run_state.error_message) + @fout.fout err.inspect + end + # Check if the delegate object responds to a given method. # @param method_name [Symbol] The name of the method to check. - # @param include_private [Boolean] Whether to include private methods in the check. - # @return [Boolean] true if the delegate object responds to the method, false otherwise. + # @param include_private [Boolean] + # Whether to include private methods in the check. + # @return [Boolean] true if the delegate object responds + # to the method, false otherwise. def respond_to?(method_name, include_private = false) if super true elsif @delegate_object.respond_to?(method_name, include_private) true - elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, - include_private) + elsif method_name.to_s.end_with?('=') && + @delegate_object.respond_to?(:[]=, include_private) true else @delegate_object.respond_to?(method_name, include_private) end end def runtime_exception(exception_sym, name, items) if @delegate_object[exception_sym] != 0 data = { name: name, detail: items.join(', ') } warn( - AnsiString.new(format( - @delegate_object.fetch(:exception_format_name, - "\n%{name}"), - data - )).send(@delegate_object.fetch(:exception_color_name, - :red)) + - AnsiString.new(format( - @delegate_object.fetch(:exception_format_detail, - " - %{detail}\n"), - data - )).send(@delegate_object.fetch( - :exception_color_detail, :yellow - )) + AnsiString.new( + format( + @delegate_object.fetch(:exception_format_name, "\n%{name}"), + data + ) + ).send(@delegate_object.fetch(:exception_color_name, :red)) + + AnsiString.new( + format( + @delegate_object.fetch(:exception_format_detail, + " - %{detail}\n"), + data + ) + ).send(@delegate_object.fetch(:exception_color_detail, :yellow)) ) end return unless (@delegate_object[exception_sym]).positive? exit @delegate_object[exception_sym] @@ -2750,15 +3233,28 @@ name end end end - def save_to_file(required_lines:, selected:) - write_command_file(required_lines: required_lines, selected: selected) + def save_to_file(required_lines:, selected:, shell:) + write_command_file( + required_lines: required_lines, selected: selected, shell: shell + ) @fout.fout "File saved: #{@run_state.saved_filespec}" end + def saved_asset_filename(filename, link_state = LinkState.new) + SavedAsset.new( + filename: filename, + saved_asset_format: + shell_escape_asset_format( + code_lines: link_state&.inherited_lines, + shell: shell + ) + ).generate_name + end + def select_document_if_multiple(options, files, prompt:) # binding.irb return files if files.class == String ### return files[0] if (count = files.count) == 1 @@ -2773,20 +3269,28 @@ files, opts.merge(per_page: opts[:select_page_height]) ) end - # Presents a TTY prompt to select an option or exit, returns metadata including option and selected + # Presents a TTY prompt to select an option or exit, + # returns metadata including option and selected def select_option_with_metadata(prompt_text, menu_items, opts = {}) + # !!v prompt_text menu_items ## configure to environment # register_console_attributes(opts) # crashes if all menu options are disabled - selection = @prompt.select(prompt_text, - menu_items, - opts.merge(filter: true)) + begin + selection = @prompt.select(prompt_text, + menu_items, + opts.merge(filter: true)) + # !!v selection + rescue NoMethodError + # no enabled options in page + return + end selected = menu_items.find do |item| if item.instance_of?(Hash) (item[:name] || item[:dname]) == selection elsif item.instance_of?(MarkdownExec::FCB) @@ -2806,140 +3310,184 @@ exit 1 end if selection == menu_chrome_colored_option(:menu_option_back_name) selected.option = selection - selected.shell = BlockType::LINK + selected.type = BlockType::LINK elsif selection == menu_chrome_colored_option(:menu_option_exit_name) selected.option = selection else selected.selected = selection end selected end - def set_environment_variables_for_block(selected) - code_lines = [] - YAML.load(selected.body.join("\n"))&.each do |key, value| - ENV[key] = value.to_s + def shell + @delegate_object[:shell] + end - require 'shellwords' - code_lines.push "#{key}=\"#{Shellwords.escape(value)}\"" - - next unless @delegate_object[:menu_vars_set_format].present? - - formatted_string = format(@delegate_object[:menu_vars_set_format], - { key: key, value: value }) - print string_send_color(formatted_string, :menu_vars_set_color) - end - code_lines + def shell=(value) + @delegate_object[:shell] = value end - def shell_escape_asset_format(link_state) - raw = @delegate_object[:saved_asset_format] + def shell_escape_asset_format( + code_lines:, + enable: @delegate_object[:shell_parameter_expansion], + raw: @delegate_object[:saved_asset_format], + shell: + ) + return raw unless enable - return raw unless @delegate_object[:shell_parameter_expansion] - # unchanged if no parameter expansion takes place return raw unless /$/ =~ raw filespec = generate_temp_filename - cmd = [@delegate_object[:shell], '-c', filespec].join(' ') + cmd = [shell, '-c', filespec].join(' ') marker = Random.new.rand.to_s - code = (link_state&.inherited_lines || []) + ["echo -n \"#{marker}#{raw}\""] - # &bt code + code = (code_lines || []) + ["echo -n \"#{marker}#{raw}\""] + # !!t code File.write filespec, HashDelegator.join_code_lines(code) File.chmod 0o755, filespec out = `#{cmd}`.sub(/.*?#{marker}/m, '') File.delete filespec - out # &br + out # !!r end - def should_add_back_option? - @delegate_object[:menu_with_back] && @link_history.prior_state_exist? + def should_add_back_option?( + menu_with_back: @delegate_object[:menu_with_back] + ) + menu_with_back && @link_history.prior_state_exist? end - # Initializes a new fenced code block (FCB) object based on the provided line and heading information. + def simple_menu_options( + per_page: @delegate_object[:select_page_height] + ) + { cycle: true, + per_page: per_page } + end + + # Initializes a new fenced code block (FCB) object based + # on the provided line and heading information. # @param line [String] The line initiating the fenced block. # @param headings [Array<String>] Current headings hierarchy. - # @param fenced_start_extended_regex [Regexp] Regular expression to identify fenced block start. + # @param fenced_start_extended_regex [Regexp] + # Regular expression to identify fenced block start. # @return [MarkdownExec::FCB] A new FCB instance with the parsed attributes. def start_fenced_block(line, headings, fenced_start_extended_regex) - fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys + fcb_title_groups = NamedCaptureExtractor::extract_named_groups( + line, fenced_start_extended_regex + ) + rest = fcb_title_groups.fetch(:rest, '') reqs, wraps = ArrayUtil.partition_by_predicate(rest.scan(/\+[^\s]+/).map do |req| req[1..-1] end) do |name| !name.match(Regexp.new(@delegate_object[:block_name_wrapper_match])) end + # adjust captured type + if fcb_title_groups[:type].present? + case fcb_title_groups[:type] + when *ShellType::ALL + # convert type to shell + fcb_title_groups[:shell] = fcb_title_groups[:type] + fcb_title_groups[:type] = BlockType::SHELL + end + else + # treat as the default shell + fcb_title_groups[:shell] = @delegate_object[:block_type_default] + fcb_title_groups[:type] = BlockType::SHELL + end + dname = oname = title = '' nickname = nil - if @delegate_object[:block_name_nick_match].present? && oname =~ Regexp.new(@delegate_object[:block_name_nick_match]) + if @delegate_object[:block_name_nick_match].present? && + oname =~ Regexp.new(@delegate_object[:block_name_nick_match]) nickname = $~[0] else dname = oname = title = fcb_title_groups.fetch(:name, '') end # disable fcb for data blocks - disabled = fcb_title_groups.fetch(:shell, '') == 'yaml' ? '' : nil + disabled = if fcb_title_groups.fetch(:type, '') == BlockType::YAML + TtyMenu::DISABLE + else + nil + end MarkdownExec::FCB.new( body: [], - call: rest.match(Regexp.new(@delegate_object[:block_calls_scan]))&.to_a&.first, + call: rest.match( + Regexp.new(@delegate_object[:block_calls_scan]) + )&.to_a&.first, disabled: disabled, dname: dname, headings: headings, indent: fcb_title_groups.fetch(:indent, ''), nickname: nickname, oname: oname, reqs: reqs, shell: fcb_title_groups.fetch(:shell, ''), + start_line: line, stdin: if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/)) - tn.named_captures.sym_keys + NamedCaptureExtractor::extract_named_group2(tn) end, stdout: if (tn = rest.match(/>(?<type>\$)?(?<name>[\w.\-]+)/)) - tn.named_captures.sym_keys + NamedCaptureExtractor::extract_named_group2(tn) end, title: title, + type: fcb_title_groups.fetch(:type, ''), wraps: wraps ) end # Applies a color method to a string based on the provided color symbol. # The color method is fetched from @delegate_object and applied to the string. # @param string [String] The string to which the color will be applied. # @param color_sym [Symbol] The symbol representing the color method. - # @param default [String] Default color method to use if color_sym is not found in @delegate_object. + # @param default [String] Default color method to use if + # color_sym is not found in @delegate_object. # @return [String] The string with the applied color method. def string_send_color(string, color_sym) HashDelegator.apply_color_from_hash(string, @delegate_object, color_sym) end ## - # Processes an individual line within a loop, updating headings and handling fenced code blocks. - # This function is designed to be called within a loop that iterates through each line of a document. + # Processes an individual line within a loop, updating headings + # and handling fenced code blocks. + # This function is designed to be called within a loop that iterates + # through each line of a document. # # @param line [String] The current line being processed. - # @param state [Hash] The current state of the parser, including flags and data related to the processing. - # @param opts [Hash] A hash containing various options for line and block processing. - # @param selected_types [Array<String>] Accumulator for lines or messages that are subject to further processing. - # @param block [Proc] An optional block for further processing or transformation of lines. + # @param state [Hash] The current state of the parser, including flags + # and data related to the processing. + # @param opts [Hash] A hash containing various options for line + # and block processing. + # @param selected_types [Array<String>] Accumulator for lines + # or messages that are subject to further processing. + # @param block [Proc] An optional block for further processing + # or transformation of lines. # - # @option state [Array<String>] :headings Current headings to be updated based on the line. - # @option state [Regexp] :fenced_start_and_end_regex Regular expression to match the start and end of a fenced block. - # @option state [Boolean] :in_fenced_block Flag indicating whether the current line is inside a fenced block. - # @option state [Object] :fcb An object representing the current fenced code block being processed. + # @option state [Array<String>] :headings Current headings + # to be updated based on the line. + # @option state [Regexp] :fenced_start_and_end_regex Regular expression + # to match the start and end of a fenced block. + # @option state [Boolean] :in_fenced_block Flag indicating whether + # the current line is inside a fenced block. + # @option state [Object] :fcb An object representing + # the current fenced code block being processed. # - # @option opts [Boolean] :menu_blocks_with_headings Flag indicating whether to update headings while processing. + # @option opts [Boolean] :menu_blocks_with_headings Flag + # indicating whether to update headings while processing. # - # @return [Void] The function modifies the `state` and `selected_types` arguments in place. + # @return [Void] The function modifies the `state` + # and `selected_types` arguments in place. ## def update_line_and_block_state(nested_line, state, selected_types, &block) line = nested_line.to_s if line.match(@delegate_object[:fenced_start_and_end_regex]) @@ -2954,30 +3502,33 @@ ) state[:in_fenced_block] = false else ## start of code block # - state[:fcb] = - start_fenced_block(line, state[:headings], - @delegate_object[:fenced_start_extended_regex]) + state[:fcb] = start_fenced_block( + line, state[:headings], + @delegate_object[:fenced_start_extended_regex] + ) state[:fcb][:depth] = nested_line[:depth] + state[:fcb][:indention] = nested_line[:indention] state[:in_fenced_block] = true end elsif state[:in_fenced_block] && state[:fcb].body ## add line to fenced code block # remove fcb indent if possible # state[:fcb].body += [ line.chomp.sub(/^#{state[:fcb].indent}/, '') ] - elsif nested_line[:depth].zero? || @delegate_object[:menu_include_imported_notes] + elsif nested_line[:depth].zero? || + @delegate_object[:menu_include_imported_notes] # add line if it is depth 0 or option allows it # HashDelegator.yield_line_if_selected(line, selected_types, &block) else - # &bsp 'line is not recognized for block state' + # !!b 'line is not recognized for block state' end end ## apply options to current state @@ -2987,18 +3538,22 @@ @menu_base_options&.merge!(options) @delegate_object.merge!(options) end def vux_await_user_selection - @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file, - menu_blocks: @dml_menu_blocks, - default: @dml_menu_default_dname) - # &bsp '@run_state.source.block_name_from_cli:',@run_state.source.block_name_from_cli + @dml_block_state = load_cli_or_user_selected_block( + all_blocks: @dml_blocks_in_file, + menu_blocks: @dml_menu_blocks, + default: @dml_menu_default_dname + ) + # !!b '@run_state.source.block_name_from_cli:',@run_state.source.block_name_from_cli if !@dml_block_state - HashDelegator.error_handler('block_state missing', { abort: true }) + # HashDelegator.error_handler('block_state missing', { abort: true }) + # document has no enabled items + :break elsif @dml_block_state.state == MenuState::EXIT - # &bsp 'load_cli_or_user_selected_block -> break' + # !!b 'load_cli_or_user_selected_block -> break' :break end end def vux_clear_menu_state @@ -3011,11 +3566,12 @@ @dml_link_state.inherited_lines = edited.split("\n") if edited end def vux_execute_and_prompt(block_name) @dml_block_state = block_state_for_name_from_cli(block_name) - if @dml_block_state.block && @dml_block_state.block.shell == BlockType::OPTS + if @dml_block_state.block && + @dml_block_state.block.type == BlockType::OPTS debounce_reset link_state = LinkState.new options_state = read_show_options_and_trigger_reuse( link_state: link_state, mdoc: @dml_mdoc, @@ -3027,26 +3583,30 @@ return end return :break if execute_block_in_state(block_name) == :break - if prompt_user_exit(block_name_from_cli: @run_state.source.block_name_from_cli, - selected: @dml_block_state.block) + if prompt_user_exit( + block_name_from_cli: @run_state.source.block_name_from_cli, + selected: @dml_block_state.block + ) return :break end - ## order of block name processing: link block, cli, from user + ## order of block name processing: link block, cli, from + # user # - @dml_link_state.block_name, @run_state.source.block_name_from_cli, cli_break = + @dml_link_state.block_name, + @run_state.source.block_name_from_cli, cli_break = HashDelegator.next_link_state( block_name: @dml_link_state.block_name, block_name_from_cli: @dml_now_using_cli, block_state: @dml_block_state, was_using_cli: @dml_now_using_cli ) - # &bsp '!block_name_from_ui + cli_break -> break' + # !!b '!block_name_from_ui + cli_break -> break' !@dml_block_state.source.block_name_from_ui && cli_break && :break end def vux_execute_block_per_type(block_name, formatted_choice_ostructs) case block_name @@ -3061,12 +3621,11 @@ InputSequencer.next_link_state(prior_block_was_link: true) when formatted_choice_ostructs[:history].pub_name debounce_reset - files_table_rows = read_saved_assets_for_history_table - return :break unless files_table_rows + return :break unless files_table_rows = vux_history_files_table_rows execute_history_select(files_table_rows, stream: $stderr) return :break if pause_user_exit InputSequencer.next_link_state(prior_block_was_link: true) @@ -3084,11 +3643,11 @@ InputSequencer.next_link_state(prior_block_was_link: true) when formatted_choice_ostructs[:shell].pub_name debounce_reset - vux_input_and_execute_shell_commands(stream: $stderr) + vux_input_and_execute_shell_commands(stream: $stderr, shell: shell) return :break if pause_user_exit InputSequencer.next_link_state(prior_block_was_link: true) when formatted_choice_ostructs[:view].pub_name @@ -3101,11 +3660,11 @@ else return :break if vux_execute_and_prompt(block_name) == :break InputSequencer.next_link_state( block_name: @dml_link_state.block_name, - prior_block_was_link: @dml_block_state.block.shell != BlockType::BASH + prior_block_was_link: @dml_block_state.block.type != BlockType::SHELL ) end end def vux_formatted_names_for_state_chrome_blocks( @@ -3122,17 +3681,25 @@ pub_name: dname.pub_name ) end end + def vux_history_files_table_rows + read_saved_assets_for_history_table( + asset: @delegate_object[:filename], + form: @delegate_object[:saved_history_format] + ) + end + def vux_init @menu_base_options = @delegate_object @dml_link_state = LinkState.new( block_name: @delegate_object[:block_name], document_filename: @delegate_object[:filename] ) - @run_state.source.block_name_from_cli = @dml_link_state.block_name.present? + @run_state.source.block_name_from_cli = + @dml_link_state.block_name.present? @cli_block_name = @dml_link_state.block_name @dml_now_using_cli = @run_state.source.block_name_from_cli @dml_menu_default_dname = nil @dml_block_state = SelectedBlockMenuState.new @doc_saved_lines_files = [] @@ -3141,23 +3708,25 @@ @run_state.batch_index = 0 @run_state.files = StreamsOut.new end - def vux_input_and_execute_shell_commands(stream:) + def vux_input_and_execute_shell_commands(stream:, shell:) loop do - command = prompt_for_command(AnsiString.new(":MDE #{Time.now.strftime('%FT%TZ')}> ").send(:bgreen)) + command = prompt_for_command( + AnsiString.new(":MDE #{Time.now.strftime('%FT%TZ')}> ").send(:bgreen) + ) break if !command.present? || command == 'exit' exit_status = execute_command_with_streams( - [@delegate_object[:shell], '-c', command] + [shell, '-c', command] ) case exit_status when 0 - stream.puts "#{'OK'.green} #{exit_status}" + stream.puts "#{AnsiString.new('OK').green} #{exit_status}" else - stream.puts "#{'ERR'.bred} #{exit_status}" + stream.puts "#{AnsiString.new('ERR').bred} #{exit_status}" end end end ## load file with code lines per options @@ -3178,32 +3747,34 @@ code_lines, inherited_dependencies, selected ) end def vux_load_inherited - sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, - @delegate_object[:document_saved_lines_glob]) - load_filespec = load_filespec_from_expression(sf) - return unless load_filespec + return unless filespec = load_filespec_from_expression( + document_name_in_glob_as_file_name + ) @dml_link_state.inherited_lines_append( - File.readlines(load_filespec, chomp: true) + File.readlines(filespec, chomp: true) ) end # Select and execute a code block from a Markdown document. # # This method allows the user to interactively select a code block from a # Markdown document, obtain approval, and execute the chosen block of code. # - # @return [Nil] Returns nil if no code block is selected or an error occurs. + # @return [Nil] Returns nil if no code block is selected + # or an error occurs. def vux_main_loop vux_init vux_load_code_files_into_state formatted_choice_ostructs = vux_formatted_names_for_state_chrome_blocks - block_list = [@delegate_object[:block_name]].select(&:present?).compact + @delegate_object[:input_cli_rest] + block_list = [@delegate_object[:block_name]].select(&:present?).compact + + @delegate_object[:input_cli_rest] + @delegate_object[:block_name] = nil process_commands( arguments: @p_all_arguments, named_procs: yield(:command_names, @delegate_object), @@ -3237,20 +3808,31 @@ InputSequencer.new( @delegate_object[:filename], block_list ).run do |msg, data| - # &bt msg + # !!v msg data + # !!t msg case msg when :parse_document # once for each menu vux_parse_document vux_menu_append_history_files(formatted_choice_ostructs) vux_publish_document_file_name_for_external_automation when :display_menu vux_clear_menu_state + when :end_of_cli + # !!b + # yield :end_of_cli, @delegate_object + + if @delegate_object[:list_blocks] + # !!b + list_blocks + :exit + end + when :user_choice vux_user_selected_block_name when :execute_block ret = vux_execute_block_per_type(data, formatted_choice_ostructs) @@ -3280,55 +3862,69 @@ end end def vux_menu_append_history_files(formatted_choice_ostructs) if @delegate_object[:menu_for_history] - history_files(@dml_link_state).tap do |files| + history_files( + @dml_link_state, + filename: saved_asset_filename(@delegate_object[:filename], + @dml_link_state), + path: @delegate_object[:saved_script_folder] + ).tap do |files| if files.count.positive? dml_menu_append_chrome_item( formatted_choice_ostructs[:history].oname, files.count, 'files', menu_state: MenuState::HISTORY ) end end end - return unless @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present? + return unless @delegate_object[:menu_for_saved_lines] && + @delegate_object[:document_saved_lines_glob].present? - sf = document_name_in_glob_as_file_name( - @dml_link_state.document_filename, - @delegate_object[:document_saved_lines_glob] - ) - files = sf ? Dir.glob(sf) : [] + document_glob = document_name_in_glob_as_file_name + files = document_glob ? Dir.glob(document_glob) : [] @doc_saved_lines_files = files.count.positive? ? files : [] lines_count = @dml_link_state.inherited_lines_count # add menu items (glob, load, save) and enable selectively if files.count.positive? || lines_count.positive? - menu_add_disabled_option(sf) + menu_add_disabled_option(document_glob) end if files.count.positive? - dml_menu_append_chrome_item(formatted_choice_ostructs[:load].dname, files.count, 'files', - menu_state: MenuState::LOAD) + dml_menu_append_chrome_item( + formatted_choice_ostructs[:load].dname, files.count, 'files', + menu_state: MenuState::LOAD + ) end - if @delegate_object[:menu_inherited_lines_edit_always] || lines_count.positive? - dml_menu_append_chrome_item(formatted_choice_ostructs[:edit].dname, lines_count, 'lines', - menu_state: MenuState::EDIT) + if @delegate_object[:menu_inherited_lines_edit_always] || + lines_count.positive? + dml_menu_append_chrome_item( + formatted_choice_ostructs[:edit].dname, lines_count, 'lines', + menu_state: MenuState::EDIT + ) end if lines_count.positive? - dml_menu_append_chrome_item(formatted_choice_ostructs[:save].dname, 1, '', - menu_state: MenuState::SAVE) + dml_menu_append_chrome_item( + formatted_choice_ostructs[:save].dname, 1, '', + menu_state: MenuState::SAVE + ) end if lines_count.positive? - dml_menu_append_chrome_item(formatted_choice_ostructs[:view].dname, 1, '', - menu_state: MenuState::VIEW) + dml_menu_append_chrome_item( + formatted_choice_ostructs[:view].dname, 1, '', + menu_state: MenuState::VIEW + ) end # rubocop:disable Style/GuardClause if @delegate_object[:menu_with_shell] - dml_menu_append_chrome_item(formatted_choice_ostructs[:shell].dname, 1, '', - menu_state: MenuState::SHELL) + dml_menu_append_chrome_item( + formatted_choice_ostructs[:shell].dname, 1, '', + menu_state: MenuState::SHELL + ) end # rubocop:enable Style/GuardClause # # reflect new menu items # @dml_mdoc = MDoc.new(@dml_menu_blocks) @@ -3354,48 +3950,24 @@ link_state: @dml_link_state ) @delegate_object[:filename] = @dml_link_state.document_filename @dml_link_state.block_name = @delegate_object[:block_name] = - @run_state.source.block_name_from_cli ? - @cli_block_name : - @dml_link_state.block_name + if @run_state.source.block_name_from_cli + @cli_block_name + else + @dml_link_state.block_name + end # update @delegate_object and @menu_base_options in auto_load # @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = mdoc_menu_and_blocks_from_nested_files(@dml_link_state) dump_delobj(@dml_blocks_in_file, @dml_menu_blocks, @dml_link_state) - # &bsp 'loop', @run_state.source.block_name_from_cli, @cli_block_name + # !!b 'loop', @run_state.source.block_name_from_cli, @cli_block_name end - def publish_for_external_automation(message:) - return if @delegate_object[:publish_document_file_name].empty? - - pipe_path = absolute_path(@delegate_object[:publish_document_file_name]) - - case @delegate_object[:publish_document_file_mode] - when 'append' - File.write(pipe_path, message + "\n", mode: 'a') - when 'fifo' - unless @vux_pipe_open - unless File.exist?(pipe_path) - FileUtils.mkfifo(pipe_path) - @vux_pipe_created = pipe_path - end - @vux_pipe_open = File.open(pipe_path, 'w') - end - @vux_pipe_open.puts(message + "\n") - @vux_pipe_open.flush - when 'write' - File.write(pipe_path, message) - else - raise 'Invalid publish_document_file_mode:' \ - " #{@delegate_object[:publish_document_file_mode]}" - end - end - def vux_publish_block_name_for_external_automation(block_name) publish_for_external_automation( message: format( @delegate_object[:publish_block_name_format], { block: block_name, @@ -3421,18 +3993,22 @@ ) end # return :break to break from loop def vux_user_selected_block_name + # !!b if @dml_link_state.block_name.present? # @prior_block_was_link = true - @dml_block_state.block = blocks_find_by_block_name(@dml_blocks_in_file, - @dml_link_state.block_name) + @dml_block_state.block = blocks_find_by_block_name( + @dml_blocks_in_file, + @dml_link_state.block_name + ) @dml_link_state.block_name = nil else - # puts "? - Select a block to execute (or type #{$texit} to exit):" - return :break if vux_await_user_selection == :break # into @dml_block_state + # puts "? - Select a block to execute (or type #{$texit} + # to exit):" + return :break if vux_await_user_selection == :break return :break if @dml_block_state.block.nil? # no block matched end # puts "! - Executing block: #{data}" @dml_block_state.block&.pub_name end @@ -3448,63 +4024,80 @@ rescue Interrupt # user interrupts process end def wait_for_user_selected_block(all_blocks, menu_blocks, default) + # !!b block_state = wait_for_user_selection(all_blocks, menu_blocks, default) handle_back_or_continue(block_state) block_state end def wait_for_user_selection(_all_blocks, menu_blocks, default) + # !!b if @delegate_object[:clear_screen_for_select_block] printf("\e[1;1H\e[2J") end + # !!b prompt_title = string_send_color( - @delegate_object[:prompt_select_block].to_s, :prompt_color_after_script_execution + @delegate_object[:prompt_select_block].to_s, + :prompt_color_after_script_execution ) + # !!b menu_items = prepare_blocks_menu(menu_blocks) if menu_items.empty? return SelectedBlockMenuState.new(nil, OpenStruct.new, MenuState::EXIT) end - # default value may not match if color is different from originating menu (opts changed while processing) + # !!b + # default value may not match if color is different from + # originating menu (opts changed while processing) selection_opts = if default && menu_blocks.map(&:dname).include?(default) @delegate_object.merge(default: default) else @delegate_object end - sph = @delegate_object[:select_page_height] - selection_opts.merge!(per_page: sph) - + # !!b + selection_opts.merge!( + { cycle: @delegate_object[:select_page_cycle], + per_page: @delegate_object[:select_page_height] } + ) selected_option = select_option_with_metadata(prompt_title, menu_items, selection_opts) + # !!b determine_block_state(selected_option) end - # Handles the core logic for generating the command file's metadata and content. - def write_command_file(required_lines:, selected:) + # Handles the core logic for generating the command + # file's metadata and content. + def write_command_file(required_lines:, selected:, shell: nil) return unless @delegate_object[:save_executed_script] time_now = Time.now.utc @run_state.saved_script_filename = - SavedAsset.new(blockname: selected.pub_name, - exts: '.sh', - filename: @delegate_object[:filename], - prefix: @delegate_object[:saved_script_filename_prefix], - saved_asset_format: shell_escape_asset_format(@dml_link_state), - time: time_now).generate_name + SavedAsset.new( + blockname: selected.pub_name, + exts: '.sh', + filename: @delegate_object[:filename], + prefix: @delegate_object[:saved_script_filename_prefix], + saved_asset_format: + shell_escape_asset_format( + code_lines: @dml_link_state.inherited_lines, + shell: shell + ), + time: time_now + ).generate_name @run_state.saved_filespec = File.join(@delegate_object[:saved_script_folder], @run_state.saved_script_filename) shebang = if @delegate_object[:shebang]&.present? - "#{@delegate_object[:shebang]} #{@delegate_object[:shell]}\n" + "#{@delegate_object[:shebang]} #{shell}\n" else '' end content = shebang + @@ -3521,24 +4114,26 @@ rescue StandardError HashDelegator.error_handler('write_command_file') end # Ensure the directory exists before writing the file - def write_file_with_directory_creation(save_filespec, content) - directory = File.dirname(save_filespec) + def write_file_with_directory_creation(content:, filespec:) + directory = File.dirname(filespec) begin FileUtils.mkdir_p(directory) - File.write(save_filespec, content) + File.write(filespec, content) rescue Errno::EACCES - warn "Permission denied: Unable to write to file '#{save_filespec}'" + warn "Permission denied: Unable to write to file '#{filespec}'" nil rescue Errno::EROFS - warn "Read-only file system: Unable to write to file '#{save_filespec}'" + warn 'Read-only file system: Unable to write to file ' \ + "'#{filespec}'" nil rescue StandardError => err - warn "An error occurred while writing to file '#{save_filespec}': #{err.message}" + warn 'An error occurred while writing to file ' \ + "'#{filespec}': #{err.message}" nil end end # return next document file name @@ -3570,11 +4165,11 @@ else value end end - # Recursively cleans the given object (hash or struct) from unwanted values. + # Recursively cleans the given hash or struct from unwanted values. def self.clean_hash_recursively(obj) obj.each do |key, value| cleaned_value = clean_value(value) # Clean and possibly convert value obj[key] = cleaned_value if value.is_a?(Hash) || value.is_a?(Struct) end @@ -3645,78 +4240,87 @@ @hd = HashDelegator.new end # Test case for empty body def test_next_link_state - @hd.next_link_state(block_name_from_cli: nil, was_using_cli: nil, block_state: nil, - block_name: nil) + @hd.next_link_state( + block_name_from_cli: nil, was_using_cli: nil, + block_state: nil, block_name: nil + ) end end class TestHashDelegator < Minitest::Test def setup @hd = HashDelegator.new @mdoc = mock('MarkdownDocument') end - def test_calling_execute_required_lines_calls_command_execute_with_argument_args_value + def test_execute_required_lines_with_argument_args_value + # calling execute required lines + # calls command execute with argument args value pigeon = 'E' obj = { output_execution_label_format: '', output_execution_label_name_color: 'plain', output_execution_label_value_color: 'plain' } c = MarkdownExec::HashDelegator.new(obj) c.pass_args = pigeon - # Expect that method opts_command_execute is called with argument args having value pigeon + # Expect that method opts_command_execute is + # called with argument args having value pigeon c.expects(:command_execute).with( '', - args: pigeon + args: pigeon, + shell: ShellType::BASH ) # Call method opts_execute_required_lines - c.execute_required_lines + c.execute_required_lines(shell: ShellType::BASH) end # Test case for empty body - def test_push_link_history_and_trigger_load_with_empty_body + def test_execute_block_type_link_with_state_with_empty_body assert_equal LoadFile::REUSE, - @hd.push_link_history_and_trigger_load.load_file + @hd.execute_block_type_link_with_state.load_file end # Test case for non-empty body without 'file' key - def test_push_link_history_and_trigger_load_without_file_key + def test_execute_block_type_link_with_state_without_file_key body = ["vars:\n KEY: VALUE"] assert_equal LoadFile::REUSE, - @hd.push_link_history_and_trigger_load(link_block_body: body).load_file + @hd.execute_block_type_link_with_state( + link_block_body: body + ).load_file end # Test case for non-empty body with 'file' key - def test_push_link_history_and_trigger_load_with_file_key + def test_execute_block_type_link_with_state_with_file_key body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"] expected_result = LoadFileLinkState.new( LoadFile::LOAD, LinkState.new(block_name: 'sample_block', document_filename: 'sample_file', inherited_dependencies: {}, inherited_lines: ['# ', 'KEY="VALUE"']) ) assert_equal expected_result, - @hd.push_link_history_and_trigger_load( + @hd.execute_block_type_link_with_state( link_block_body: body, selected: FCB.new(block_name: 'sample_block', filename: 'sample_file') ) end def test_indent_all_lines_with_indent body = "Line 1\nLine 2" indent = ' ' # Two spaces expected_result = " Line 1\n Line 2" - assert_equal expected_result, HashDelegator.indent_all_lines(body, indent) + assert_equal expected_result, + HashDelegator.indent_all_lines(body, indent) end def test_indent_all_lines_without_indent body = "Line 1\nLine 2" indent = nil @@ -3740,11 +4344,12 @@ HashDelegator.safeval('invalid_code_raises_exception') end end def test_set_fcb_title - # sample input and output data for testing default_block_title_from_body method + # sample input and output data for + # testing default_block_title_from_body method input_output_data = [ { input: MarkdownExec::FCB.new(title: nil, body: ["puts 'Hello, world!'"]), output: "puts 'Hello, world!'" @@ -3764,657 +4369,717 @@ # iterate over the input and output data and # assert that the method sets the title as expected input_output_data.each do |data| input = data[:input] output = data[:output] - HashDelegator.default_block_title_from_body(input) - assert_equal output, input.title + title = HashDelegator.default_block_title_from_body(input) + assert_equal output, title end end + end - class TestHashDelegatorAppendDivider < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@delegate_object, { - menu_divider_format: 'Format', - menu_initial_divider: 'Initial Divider', - menu_final_divider: 'Final Divider', - menu_divider_color: :color - }) - @hd.stubs(:string_send_color).returns('Formatted Divider') - HashDelegator.stubs(:safeval).returns('Safe Value') - end + class TestHashDelegatorAppendDivider < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.instance_variable_set(:@delegate_object, { + menu_divider_format: 'Format', + menu_initial_divider: 'Initial Divider', + menu_final_divider: 'Final Divider', + menu_divider_color: :color + }) + @hd.stubs(:string_send_color).returns('Formatted Divider') + HashDelegator.stubs(:safeval).returns('Safe Value') + end - def test_append_divider_initial - menu_blocks = [] - @hd.append_divider(menu_blocks: menu_blocks, position: :initial) + def test_append_divider_initial + menu_blocks = [] + @hd.append_divider(menu_blocks: menu_blocks, position: :initial) - assert_equal 1, menu_blocks.size - assert_equal 'Formatted Divider', menu_blocks.first.dname - end + assert_equal 1, menu_blocks.size + assert_equal 'Formatted Divider', menu_blocks.first.dname + end - def test_append_divider_final - menu_blocks = [] - @hd.append_divider(menu_blocks: menu_blocks, position: :final) + def test_append_divider_final + menu_blocks = [] + @hd.append_divider(menu_blocks: menu_blocks, position: :final) - assert_equal 1, menu_blocks.size - assert_equal 'Formatted Divider', menu_blocks.last.dname - end + assert_equal 1, menu_blocks.size + assert_equal 'Formatted Divider', menu_blocks.last.dname + end - def test_append_divider_without_format - @hd.instance_variable_set(:@delegate_object, {}) - menu_blocks = [] - @hd.append_divider(menu_blocks: menu_blocks, position: :initial) + def test_append_divider_without_format + @hd.instance_variable_set(:@delegate_object, {}) + menu_blocks = [] + @hd.append_divider(menu_blocks: menu_blocks, position: :initial) - assert_empty menu_blocks - end + assert_empty menu_blocks end + end - class TestHashDelegatorBlockFind < Minitest::Test - def setup - @hd = HashDelegator.new - end + class TestHashDelegatorBlockFind < Minitest::Test + def setup + @hd = HashDelegator.new + end - def test_block_find_with_match - blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')] - result = HashDelegator.block_find(blocks, :text, 'value1') - assert_equal('value1', result.text) - end + def test_block_find_with_match + blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')] + result = HashDelegator.block_find(blocks, :text, 'value1') + assert_equal('value1', result.text) + end - def test_block_find_without_match - blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')] - result = HashDelegator.block_find(blocks, :text, 'missing_value') - assert_nil result - end + def test_block_find_without_match + blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')] + result = HashDelegator.block_find(blocks, :text, 'missing_value') + assert_nil result + end - def test_block_find_with_default - blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')] - result = HashDelegator.block_find(blocks, :text, 'missing_value', - 'default') - assert_equal 'default', result - end + def test_block_find_with_default + blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')] + result = HashDelegator.block_find(blocks, :text, 'missing_value', + 'default') + assert_equal 'default', result end + end - class TestHashDelegatorBlocksFromNestedFiles < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.stubs(:iter_blocks_from_nested_files).yields(:blocks, FCB.new) - @hd.stubs(:get_block_summary).returns(FCB.new) - @hd.stubs(:create_and_add_chrome_blocks) - @hd.instance_variable_set(:@delegate_object, {}) - HashDelegator.stubs(:error_handler) - end + class TestHashDelegatorBlocksFromNestedFiles < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.stubs(:iter_blocks_from_nested_files).yields(:blocks, FCB.new) + @hd.stubs(:create_and_add_chrome_blocks) + @hd.instance_variable_set(:@delegate_object, {}) + HashDelegator.stubs(:error_handler) + end - def test_blocks_from_nested_files - result = @hd.blocks_from_nested_files - assert_kind_of Array, result - assert_kind_of FCB, result.first - end + def test_blocks_from_nested_files + result = @hd.blocks_from_nested_files + assert_kind_of Array, result + assert_kind_of FCB, result.first + end - def test_blocks_from_nested_files_with_no_chrome - @hd.instance_variable_set(:@delegate_object, { no_chrome: true }) - @hd.expects(:create_and_add_chrome_blocks).never + def test_blocks_from_nested_files_with_no_chrome + @hd.instance_variable_set(:@delegate_object, { no_chrome: true }) + @hd.expects(:create_and_add_chrome_blocks).never - result = @hd.blocks_from_nested_files + result = @hd.blocks_from_nested_files - assert_kind_of Array, result - end + assert_kind_of Array, result end + end - class TestHashDelegatorCollectRequiredCodeLines < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@delegate_object, {}) - @mdoc = mock('YourMDocClass') - @selected = FCB.new(shell: BlockType::VARS, body: ['key: value']) - HashDelegator.stubs(:read_required_blocks_from_temp_file).returns([]) - @hd.stubs(:string_send_color) - @hd.stubs(:print) - end + class TestHashDelegatorCollectRequiredCodeLines < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.instance_variable_set(:@delegate_object, {}) + @mdoc = mock('YourMDocClass') + @selected = FCB.new( + body: ['key: value'], + type: BlockType::VARS + ) + HashDelegator.stubs(:read_required_blocks_from_temp_file).returns([]) + @hd.stubs(:string_send_color) + @hd.stubs(:print) + end - def test_collect_required_code_lines_with_vars - YAML.stubs(:load).returns({ 'key' => 'value' }) - @mdoc.stubs(:collect_recursively_required_code).returns({ code: ['code line'] }) - result = @hd.collect_required_code_lines(mdoc: @mdoc, selected: @selected, - block_source: {}) + def test_execute_block_type_port_code_lines_with_vars + YAML.stubs(:load).returns({ 'key' => 'value' }) + @mdoc.stubs(:collect_recursively_required_code) + .returns({ code: ['code line'] }) + result = @hd.execute_block_type_port_code_lines( + mdoc: @mdoc, selected: @selected, block_source: {} + ) - assert_equal ['code line', 'key="value"'], result - end + assert_equal ['code line', 'key="value"'], result end + end - class TestHashDelegatorCommandOrUserSelectedBlock < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@delegate_object, {}) - HashDelegator.stubs(:error_handler) - @hd.stubs(:wait_for_user_selected_block) - end + class TestHashDelegatorCommandOrUserSelectedBlock < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.instance_variable_set(:@delegate_object, {}) + HashDelegator.stubs(:error_handler) + @hd.stubs(:wait_for_user_selected_block) + end - def test_command_selected_block - all_blocks = [{ oname: 'block1' }, { oname: 'block2' }] - @hd.instance_variable_set(:@delegate_object, - { block_name: 'block1' }) + def test_command_selected_block + all_blocks = [{ oname: 'block1' }, { oname: 'block2' }] + @hd.instance_variable_set(:@delegate_object, + { block_name: 'block1' }) - result = @hd.load_cli_or_user_selected_block(all_blocks: all_blocks) + result = @hd.load_cli_or_user_selected_block(all_blocks: all_blocks) - assert_equal all_blocks.first, - result.block - assert_equal OpenStruct.new(block_name_from_ui: false), - result.source - assert_nil result.state - end + assert_equal all_blocks.first, + result.block + assert_equal OpenStruct.new(block_name_from_ui: false), + result.source + assert_nil result.state + end - def test_user_selected_block - block_state = SelectedBlockMenuState.new({ oname: 'block2' }, OpenStruct.new, - :some_state) - @hd.stubs(:wait_for_user_selected_block).returns(block_state) + def test_user_selected_block + block_state = SelectedBlockMenuState.new( + { oname: 'block2' }, OpenStruct.new, :some_state + ) + @hd.stubs(:wait_for_user_selected_block).returns(block_state) - result = @hd.load_cli_or_user_selected_block + result = @hd.load_cli_or_user_selected_block - assert_equal block_state.block, - result.block - assert_equal OpenStruct.new(block_name_from_ui: true), - result.source - assert_equal :some_state, result.state - end + assert_equal block_state.block, + result.block + assert_equal OpenStruct.new(block_name_from_ui: true), + result.source + assert_equal :some_state, result.state end + end - class TestHashDelegatorCountBlockInFilename < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@delegate_object, - { fenced_start_and_end_regex: '^```', - filename: '/path/to/file' }) - @hd.stubs(:cfile).returns(mock('cfile')) - end + class TestHashDelegatorCountBlockInFilename < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.instance_variable_set(:@delegate_object, + { fenced_start_and_end_regex: '^```', + filename: '/path/to/file' }) + @hd.stubs(:cfile).returns(mock('cfile')) + end - def test_count_blocks_in_filename - file_content = ["```ruby\n", "puts 'Hello'\n", "```\n", - "```python\n", "print('Hello')\n", "```\n"] - @hd.cfile.stubs(:readlines).with('/path/to/file', - import_paths: nil).returns(file_content) + def test_count_blocks_in_filename + file_content = ["```ruby\n", "puts 'Hello'\n", "```\n", + "```python\n", "print('Hello')\n", "```\n"] + @hd.cfile.stubs(:readlines) + .with('/path/to/file', import_paths: nil).returns(file_content) - count = @hd.count_blocks_in_filename + count = @hd.count_blocks_in_filename - assert_equal 2, count - end + assert_equal 2, count + end - def test_count_blocks_in_filename_with_no_matches - file_content = ["puts 'Hello'\n", "print('Hello')\n"] - @hd.cfile.stubs(:readlines).with('/path/to/file', - import_paths: nil).returns(file_content) + def test_count_blocks_in_filename_with_no_matches + file_content = ["puts 'Hello'\n", "print('Hello')\n"] + @hd.cfile.stubs(:readlines) + .with('/path/to/file', import_paths: nil).returns(file_content) - count = @hd.count_blocks_in_filename + count = @hd.count_blocks_in_filename - assert_equal 0, count - end + assert_equal 0, count end + end - class TestHashDelegatorCreateAndWriteFile < Minitest::Test - def setup - @hd = HashDelegator.new - HashDelegator.stubs(:error_handler) - FileUtils.stubs(:mkdir_p) - File.stubs(:write) - File.stubs(:chmod) - end + class TestHashDelegatorCreateAndWriteFile < Minitest::Test + def setup + @hd = HashDelegator.new + HashDelegator.stubs(:error_handler) + FileUtils.stubs(:mkdir_p) + File.stubs(:write) + File.stubs(:chmod) + end - def test_create_file_and_write_string_with_permissions - file_path = '/path/to/file' - content = 'sample content' - chmod_value = 0o644 + def test_create_file_and_write_string_with_permissions + file_path = '/path/to/file' + content = 'sample content' + chmod_value = 0o644 - FileUtils.expects(:mkdir_p).with('/path/to').once - File.expects(:write).with(file_path, content).once - File.expects(:chmod).with(chmod_value, file_path).once + FileUtils.expects(:mkdir_p).with('/path/to').once + File.expects(:write).with(file_path, content).once + File.expects(:chmod).with(chmod_value, file_path).once - HashDelegator.create_file_and_write_string_with_permissions(file_path, content, - chmod_value) + HashDelegator.create_file_and_write_string_with_permissions( + file_path, content, chmod_value + ) - assert true # Placeholder for actual test assertions - end + assert true # Placeholder for actual test assertions + end - def test_create_and_write_file_without_chmod - file_path = '/path/to/file' - content = 'sample content' - chmod_value = 0 + def test_create_and_write_file_without_chmod + file_path = '/path/to/file' + content = 'sample content' + chmod_value = 0 - FileUtils.expects(:mkdir_p).with('/path/to').once - File.expects(:write).with(file_path, content).once - File.expects(:chmod).never + FileUtils.expects(:mkdir_p).with('/path/to').once + File.expects(:write).with(file_path, content).once + File.expects(:chmod).never - HashDelegator.create_file_and_write_string_with_permissions(file_path, content, - chmod_value) + HashDelegator.create_file_and_write_string_with_permissions( + file_path, content, chmod_value + ) - assert true # Placeholder for actual test assertions - end + assert true # Placeholder for actual test assertions end + end - class TestHashDelegatorDetermineBlockState < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.stubs(:menu_chrome_formatted_option).returns('Formatted Option') - end + class TestHashDelegatorDetermineBlockState < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.stubs(:menu_chrome_formatted_option).returns('Formatted Option') + end - def test_determine_block_state_exit - selected_option = FCB.new(oname: 'Formatted Option') - @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_exit_name).returns('Formatted Option') + def test_determine_block_state_exit + selected_option = FCB.new(oname: 'Formatted Option') + @hd.stubs(:menu_chrome_formatted_option) + .with(:menu_option_exit_name).returns('Formatted Option') - result = @hd.determine_block_state(selected_option) + result = @hd.determine_block_state(selected_option) - assert_equal MenuState::EXIT, result.state - assert_nil result.block - end + assert_equal MenuState::EXIT, result.state + assert_nil result.block + end - def test_determine_block_state_back - selected_option = FCB.new(oname: 'Formatted Back Option') - @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_back_name).returns('Formatted Back Option') - result = @hd.determine_block_state(selected_option) + def test_determine_block_state_back + selected_option = FCB.new(oname: 'Formatted Back Option') + @hd.stubs(:menu_chrome_formatted_option) + .with(:menu_option_back_name).returns('Formatted Back Option') + result = @hd.determine_block_state(selected_option) - assert_equal MenuState::BACK, result.state - assert_equal selected_option, result.block - end + assert_equal MenuState::BACK, result.state + assert_equal selected_option, result.block + end - def test_determine_block_state_continue - selected_option = FCB.new(oname: 'Other Option') + def test_determine_block_state_continue + selected_option = FCB.new(oname: 'Other Option') - result = @hd.determine_block_state(selected_option) + result = @hd.determine_block_state(selected_option) - assert_equal MenuState::CONTINUE, result.state - assert_equal selected_option, result.block - end + assert_equal MenuState::CONTINUE, result.state + assert_equal selected_option, result.block end + end - class TestHashDelegatorDisplayRequiredCode < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@fout, mock('fout')) - @hd.instance_variable_set(:@delegate_object, {}) - @hd.stubs(:string_send_color) - end + class TestHashDelegatorDisplayRequiredCode < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.instance_variable_set(:@fout, mock('fout')) + @hd.instance_variable_set(:@delegate_object, {}) + @hd.stubs(:string_send_color) + end - def test_display_required_code - required_lines = %w[line1 line2] - @hd.instance_variable_get(:@delegate_object).stubs(:[]).with(:script_preview_head).returns('Header') - @hd.instance_variable_get(:@delegate_object).stubs(:[]).with(:script_preview_tail).returns('Footer') - @hd.instance_variable_get(:@fout).expects(:fout).times(4) + def test_display_required_code + required_lines = %w[line1 line2] + @hd.instance_variable_get(:@delegate_object) + .stubs(:[]).with(:script_preview_head).returns('Header') + @hd.instance_variable_get(:@delegate_object) + .stubs(:[]).with(:script_preview_tail).returns('Footer') + @hd.instance_variable_get(:@fout).expects(:fout).times(4) - @hd.display_required_code(required_lines: required_lines) + @hd.display_required_code(required_lines: required_lines) - # Verifying that fout is called for each line and for header & footer - assert true # Placeholder for actual test assertions - end + # Verifying that fout is called for each line and for header & footer + assert true # Placeholder for actual test assertions end + end - class TestHashDelegatorFetchColor < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@delegate_object, {}) - end + class TestHashDelegatorFetchColor < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.instance_variable_set(:@delegate_object, {}) + end - def test_fetch_color_with_valid_data - @hd.instance_variable_get(:@delegate_object).stubs(:fetch).with( - :execution_report_preview_head, '' - ).returns('Data String') - @hd.stubs(:string_send_color).with('Data String', - :execution_report_preview_frame_color).returns('Colored Data String') + def test_fetch_color_with_valid_data + @hd.instance_variable_get(:@delegate_object).stubs(:fetch).with( + :execution_report_preview_head, '' + ).returns('Data String') + @hd.stubs(:string_send_color) + .with('Data String', :execution_report_preview_frame_color) + .returns('Colored Data String') - result = @hd.fetch_color + result = @hd.fetch_color - assert_equal 'Colored Data String', result - end + assert_equal 'Colored Data String', result + end - def test_fetch_color_with_missing_data - @hd.instance_variable_get(:@delegate_object).stubs(:fetch).with( - :execution_report_preview_head, '' - ).returns('') - @hd.stubs(:string_send_color).with('', - :execution_report_preview_frame_color).returns('Default Colored String') + def test_fetch_color_with_missing_data + @hd.instance_variable_get(:@delegate_object).stubs(:fetch).with( + :execution_report_preview_head, '' + ).returns('') + @hd.stubs(:string_send_color) + .with('', :execution_report_preview_frame_color) + .returns('Default Colored String') - result = @hd.fetch_color + result = @hd.fetch_color - assert_equal 'Default Colored String', result - end + assert_equal 'Default Colored String', result end + end - class TestHashDelegatorFormatReferencesSendColor < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@delegate_object, {}) - end + class TestHashDelegatorFormatReferencesSendColor < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.instance_variable_set(:@delegate_object, {}) + end - def test_format_references_send_color_with_valid_data - @hd.instance_variable_get(:@delegate_object).stubs(:fetch).with( - :output_execution_label_format, '' - ).returns('Formatted: %{key}') - @hd.stubs(:string_send_color).returns('Colored String') + def test_format_references_send_color_with_valid_data + @hd.instance_variable_get(:@delegate_object).stubs(:fetch).with( + :output_execution_label_format, '' + ).returns('Formatted: %{key}') + @hd.stubs(:string_send_color).returns('Colored String') - result = @hd.format_references_send_color(context: { key: 'value' }, - color_sym: :execution_report_preview_frame_color) + result = @hd.format_references_send_color( + context: { key: 'value' }, + color_sym: :execution_report_preview_frame_color + ) - assert_equal 'Colored String', result - end + assert_equal 'Colored String', result + end - def test_format_references_send_color_with_missing_format - @hd.instance_variable_get(:@delegate_object).stubs(:fetch).with( - :output_execution_label_format, '' - ).returns('') - @hd.stubs(:string_send_color).returns('Default Colored String') + def test_format_references_send_color_with_missing_format + @hd.instance_variable_get(:@delegate_object).stubs(:fetch).with( + :output_execution_label_format, '' + ).returns('') + @hd.stubs(:string_send_color).returns('Default Colored String') - result = @hd.format_references_send_color(context: { key: 'value' }, - color_sym: :execution_report_preview_frame_color) + result = @hd.format_references_send_color( + context: { key: 'value' }, + color_sym: :execution_report_preview_frame_color + ) - assert_equal 'Default Colored String', result - end + assert_equal 'Default Colored String', result end + end - class TestHashDelegatorFormatExecutionStreams < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@run_state, mock('run_state')) - end + class TestHashDelegatorFormatExecutionStreams < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.instance_variable_set(:@run_state, mock('run_state')) + end - # def test_format_execution_stream_with_valid_key - # result = HashDelegator.format_execution_stream( - # { stdout: %w[output1 output2] }, - # ExecutionStreams::STD_OUT - # ) + # def test_format_execution_stream_with_valid_key + # result = HashDelegator.format_execution_stream( + # { stdout: %w[output1 output2] }, + # ExecutionStreams::STD_OUT + # ) - # assert_equal "output1\noutput2", result - # end + # assert_equal "output1\noutput2", result + # end - # def test_format_execution_stream_with_empty_key - # @hd.instance_variable_get(:@run_state).stubs(:files).returns({}) + # def test_format_execution_stream_with_empty_key + # @hd.instance_variable_get(:@run_state).stubs(:files).returns({}) - # result = HashDelegator.format_execution_stream(nil, - # ExecutionStreams::STD_ERR) + # result = HashDelegator.format_execution_stream( + # nil, ExecutionStreams::STD_ERR) - # assert_equal '', result - # end + # assert_equal '', result + # end - # def test_format_execution_stream_with_nil_files - # @hd.instance_variable_get(:@run_state).stubs(:files).returns(nil) + # def test_format_execution_stream_with_nil_files + # @hd.instance_variable_get(:@run_state).stubs(:files).returns(nil) - # result = HashDelegator.format_execution_stream(nil, :stdin) + # result = HashDelegator.format_execution_stream(nil, :stdin) - # assert_equal '', result - # end + # assert_equal '', result + # end + end + + class TestHashDelegatorHandleBackLink < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.stubs(:history_state_pop) end - class TestHashDelegatorHandleBackLink < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.stubs(:history_state_pop) - end + def test_pop_link_history_new_state + # Verifying that history_state_pop is called + # @hd.expects(:history_state_pop).once - def test_pop_link_history_new_state - # Verifying that history_state_pop is called - # @hd.expects(:history_state_pop).once + result = @hd.pop_link_history_new_state - result = @hd.pop_link_history_new_state + # Asserting the result is an instance of LinkState + assert_nil result.block_name + end + end - # Asserting the result is an instance of LinkState - assert_nil result.block_name - end + class TestHashDelegatorBlockType < Minitest::Test + def setup + @hd = HashDelegator.new end - class TestHashDelegatorHandleBlockState < Minitest::Test - def setup - @hd = HashDelegator.new - @mock_block_state = mock('block_state') - end + # def execute_block_type_history_ux( + # directory: @delegate_object[:document_configurations_directory], + # filename: '*', + # form: '%{line}', + # link_state:, + # regexp: "^(?<line>.*)$", + # selected: + # ) - def test_handle_back_or_continue_with_back - @mock_block_state.stubs(:state).returns(MenuState::BACK) - @mock_block_state.stubs(:block).returns({ oname: 'sample_block' }) + # def read_saved_assets_for_history_table( + # asset: nil, + # filename: nil, + # form: @delegate_object[:saved_history_format], + # path: @delegate_object[:saved_script_folder], + # regexp: @delegate_object[:saved_asset_match] + # ) - @hd.handle_back_or_continue(@mock_block_state) + # def history_files( + # link_state, + # direction: :reverse, + # filename: nil, + # home: Dir.pwd, + # order: :chronological, + # path: '' + # ) - assert_equal 'sample_block', - @hd.instance_variable_get(:@delegate_object)[:block_name] - assert @hd.instance_variable_get(:@menu_user_clicked_back_link) - end + def test_call + @hd.expects(:history_files).with(nil, filename: '*', path: nil).once + @hd.execute_block_type_history_ux(filename: '*', link_state: LinkState.new, + selected: FCB.new(body: [])) + end + end - def test_handle_back_or_continue_with_continue - @mock_block_state.stubs(:state).returns(MenuState::CONTINUE) - @mock_block_state.stubs(:block).returns({ oname: 'another_block' }) + class TestHashDelegatorHandleBlockState < Minitest::Test + def setup + @hd = HashDelegator.new + @mock_block_state = mock('block_state') + end - @hd.handle_back_or_continue(@mock_block_state) + def test_handle_back_or_continue_with_back + @mock_block_state.stubs(:state).returns(MenuState::BACK) + @mock_block_state.stubs(:block).returns({ oname: 'sample_block' }) - assert_equal 'another_block', - @hd.instance_variable_get(:@delegate_object)[:block_name] - refute @hd.instance_variable_get(:@menu_user_clicked_back_link) - end + @hd.handle_back_or_continue(@mock_block_state) - def test_handle_back_or_continue_with_other - @mock_block_state.stubs(:state).returns(nil) # MenuState::OTHER - @mock_block_state.stubs(:block).returns({ oname: 'other_block' }) - - @hd.handle_back_or_continue(@mock_block_state) - - assert_nil @hd.instance_variable_get(:@delegate_object)[:block_name] - assert_nil @hd.instance_variable_get(:@menu_user_clicked_back_link) - end + assert_equal 'sample_block', + @hd.instance_variable_get(:@delegate_object)[:block_name] + assert @hd.instance_variable_get(:@menu_user_clicked_back_link) end - class TestHashDelegatorHandleGenericBlock < Minitest::Test - def setup - @hd = HashDelegator.new - @mock_document = mock('MarkdownDocument') - @selected_item = mock('FCB') - end + def test_handle_back_or_continue_with_continue + @mock_block_state.stubs(:state).returns(MenuState::CONTINUE) + @mock_block_state.stubs(:block).returns({ oname: 'another_block' }) - def test_compile_execute_and_trigger_reuse_without_user_approval - # Mock the delegate object configuration - @hd.instance_variable_set(:@delegate_object, - { output_script: false, - user_must_approve: false }) + @hd.handle_back_or_continue(@mock_block_state) - # Test the method without user approval - # Expectations and assertions go here - end + assert_equal 'another_block', + @hd.instance_variable_get(:@delegate_object)[:block_name] + refute @hd.instance_variable_get(:@menu_user_clicked_back_link) + end - def test_compile_execute_and_trigger_reuse_with_user_approval - # Mock the delegate object configuration - @hd.instance_variable_set(:@delegate_object, - { output_script: false, - user_must_approve: true }) + def test_handle_back_or_continue_with_other + @mock_block_state.stubs(:state).returns(nil) # MenuState::OTHER + @mock_block_state.stubs(:block).returns({ oname: 'other_block' }) - # Test the method with user approval - # Expectations and assertions go here - end + @hd.handle_back_or_continue(@mock_block_state) - def test_compile_execute_and_trigger_reuse_with_output_script - # Mock the delegate object configuration - @hd.instance_variable_set(:@delegate_object, - { output_script: true, - user_must_approve: false }) - - # Test the method with output script option - # Expectations and assertions go here - end + assert_nil @hd.instance_variable_get(:@delegate_object)[:block_name] + assert_nil @hd.instance_variable_get(:@menu_user_clicked_back_link) end + end - # require 'stringio' + # class TestHashDelegatorHandleGenericBlock < Minitest::Test + # def setup + # @hd = HashDelegator.new + # @mock_document = mock('MarkdownDocument') + # @selected_item = mock('FCB') + # end - class TestHashDelegatorHandleStream < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@run_state, - OpenStruct.new(files: StreamsOut.new)) - @hd.instance_variable_set(:@delegate_object, - { output_stdout: true }) - end + # def test_compile_execute_and_trigger_reuse_without_user_approval + # # Mock the delegate object configuration + # @hd.instance_variable_set(:@delegate_object, + # { output_script: false, + # user_must_approve: false }) - def test_handle_stream - stream = StringIO.new("line 1\nline 2\n") - file_type = ExecutionStreams::STD_OUT + # # Test the method without user approval + # # Expectations and assertions go here + # end - Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) } + # def test_compile_execute_and_trigger_reuse_with_user_approval + # # Mock the delegate object configuration + # @hd.instance_variable_set(:@delegate_object, + # { output_script: false, + # user_must_approve: true }) - @hd.wait_for_stream_processing - assert_equal ['line 1', 'line 2'], - @hd.instance_variable_get(:@run_state).files.stream_lines(ExecutionStreams::STD_OUT) - end + # # Test the method with user approval + # # Expectations and assertions go here + # end - def test_handle_stream_with_io_error - stream = StringIO.new("line 1\nline 2\n") - file_type = ExecutionStreams::STD_OUT - stream.stubs(:each_line).raises(IOError) + # def test_compile_execute_and_trigger_reuse_with_output_script + # # Mock the delegate object configuration + # @hd.instance_variable_set(:@delegate_object, + # { output_script: true, + # user_must_approve: false }) - Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) } + # # Test the method with output script option + # # Expectations and assertions go here + # end + # end - @hd.wait_for_stream_processing + # require 'stringio' - assert_equal [], - @hd.instance_variable_get(:@run_state).files.stream_lines(ExecutionStreams::STD_OUT) - end + class TestHashDelegatorHandleStream < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.instance_variable_set(:@run_state, + OpenStruct.new(files: StreamsOut.new)) + @hd.instance_variable_set(:@delegate_object, + { output_stdout: true }) end - class TestHashDelegatorIterBlocksFromNestedFiles < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@delegate_object, - { filename: 'test.md' }) - @hd.stubs(:check_file_existence).with('test.md').returns(true) - @hd.stubs(:initial_state).returns({}) - @hd.stubs(:cfile).returns(Minitest::Mock.new) - @hd.stubs(:update_line_and_block_state) - end + def test_handle_stream + stream = StringIO.new("line 1\nline 2\n") + file_type = ExecutionStreams::STD_OUT - def test_iter_blocks_from_nested_files - @hd.cfile.expect(:readlines, ['line 1', 'line 2'], ['test.md'], - import_paths: nil) - selected_types = ['filtered message'] + Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) } - result = @hd.iter_blocks_from_nested_files { selected_types } - assert_equal ['line 1', 'line 2'], result + @hd.wait_for_stream_processing + assert_equal ['line 1', 'line 2'], + @hd.instance_variable_get(:@run_state) + .files.stream_lines(ExecutionStreams::STD_OUT) + end - @hd.cfile.verify - end + def test_handle_stream_with_io_error + stream = StringIO.new("line 1\nline 2\n") + file_type = ExecutionStreams::STD_OUT + stream.stubs(:each_line).raises(IOError) - def test_iter_blocks_from_nested_files_with_no_file - @hd.stubs(:check_file_existence).with('test.md').returns(false) + Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) } - assert_nil(@hd.iter_blocks_from_nested_files do - ['filtered message'] - end) - end + @hd.wait_for_stream_processing + + assert_equal [], + @hd.instance_variable_get(:@run_state) + .files.stream_lines(ExecutionStreams::STD_OUT) end + end - class TestHashDelegatorMenuChromeColoredOption < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@delegate_object, { - menu_option_back_name: 'Back', - menu_chrome_color: :red, - menu_chrome_format: '-- %s --' - }) - @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_back_name).returns('-- Back --') - @hd.stubs(:string_send_color).with('-- Back --', - :menu_chrome_color).returns(AnsiString.new('-- Back --').red) - end + class TestHashDelegatorIterBlocksFromNestedFiles < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.instance_variable_set(:@delegate_object, + { filename: 'test.md' }) + @hd.stubs(:check_file_existence).with('test.md').returns(true) + @hd.stubs(:initial_state).returns({}) + @hd.stubs(:cfile).returns(Minitest::Mock.new) + @hd.stubs(:update_line_and_block_state) + end - def test_menu_chrome_colored_option_with_color - assert_equal AnsiString.new('-- Back --').red, - @hd.menu_chrome_colored_option(:menu_option_back_name) - end + def test_iter_blocks_from_nested_files + @hd.cfile.expect(:readlines, ['line 1', 'line 2'], ['test.md'], + import_paths: nil) + selected_types = ['filtered message'] - def test_menu_chrome_colored_option_without_color - @hd.instance_variable_set(:@delegate_object, - { menu_option_back_name: 'Back' }) - assert_equal '-- Back --', - @hd.menu_chrome_colored_option(:menu_option_back_name) - end + result = @hd.iter_blocks_from_nested_files { selected_types } + assert_equal ['line 1', 'line 2'], result + + @hd.cfile.verify end - class TestHashDelegatorMenuChromeFormattedOptionWithoutFormat < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@delegate_object, { - menu_option_back_name: "'Back'", - menu_chrome_format: '-- %s --' - }) - HashDelegator.stubs(:safeval).with("'Back'").returns('Back') - end + def test_iter_blocks_from_nested_files_with_no_file + @hd.stubs(:check_file_existence).with('test.md').returns(false) - def test_menu_chrome_formatted_option_with_format - assert_equal '-- Back --', - @hd.menu_chrome_formatted_option(:menu_option_back_name) - end + assert_nil(@hd.iter_blocks_from_nested_files do + ['filtered message'] + end) + end + end - def test_menu_chrome_formatted_option_without_format - @hd.instance_variable_set(:@delegate_object, - { menu_option_back_name: "'Back'" }) - assert_equal 'Back', - @hd.menu_chrome_formatted_option(:menu_option_back_name) - end + class TestHashDelegatorMenuChromeColoredOption < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.instance_variable_set(:@delegate_object, { + menu_option_back_name: 'Back', + menu_chrome_color: :red, + menu_chrome_format: '-- %s --' + }) + @hd.stubs(:menu_chrome_formatted_option) + .with(:menu_option_back_name).returns('-- Back --') + @hd.stubs(:string_send_color) + .with('-- Back --', :menu_chrome_color) + .returns(AnsiString.new('-- Back --').red) end - class TestHashDelegatorStartFencedBlock < Minitest::Test - def setup - @hd = HashDelegator.new({ - block_name_wrapper_match: 'WRAPPER_REGEX', - block_calls_scan: 'CALLS_REGEX' + def test_menu_chrome_colored_option_with_color + assert_equal AnsiString.new('-- Back --').red, + @hd.menu_chrome_colored_option(:menu_option_back_name) + end + + def test_menu_chrome_colored_option_without_color + @hd.instance_variable_set(:@delegate_object, + { menu_option_back_name: 'Back' }) + assert_equal '-- Back --', + @hd.menu_chrome_colored_option(:menu_option_back_name) + end + end + + class TestHashDelegatorMenuChromeOption < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.instance_variable_set(:@delegate_object, { + menu_option_back_name: "'Back'", + menu_chrome_format: '-- %s --' }) - end + HashDelegator.stubs(:safeval).with("'Back'").returns('Back') + end - def test_start_fenced_block - line = '```fenced' - headings = ['Heading 1'] - regex = /```(?<name>\w+)(?<rest>.*)/ + def test_menu_chrome_formatted_option_with_format + assert_equal '-- Back --', + @hd.menu_chrome_formatted_option(:menu_option_back_name) + end - fcb = @hd.start_fenced_block(line, headings, regex) + def test_menu_chrome_formatted_option_without_format + @hd.instance_variable_set(:@delegate_object, + { menu_option_back_name: "'Back'" }) + assert_equal 'Back', + @hd.menu_chrome_formatted_option(:menu_option_back_name) + end + end - assert_instance_of MarkdownExec::FCB, fcb - assert_equal headings, fcb.headings - assert_equal 'fenced', fcb.dname - end + class TestHashDelegatorStartFencedBlock < Minitest::Test + def setup + @hd = HashDelegator.new({ + block_name_wrapper_match: 'WRAPPER_REGEX', + block_calls_scan: 'CALLS_REGEX' + }) end - class TestHashDelegatorStringSendColor < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@delegate_object, - { red: 'red', green: 'green' }) - end + def test_start_fenced_block + line = '```fenced' + headings = ['Heading 1'] + regex = /```(?<name>\w+)(?<rest>.*)/ - def test_string_send_color - assert_equal AnsiString.new('Hello').red, - @hd.string_send_color('Hello', :red) - assert_equal AnsiString.new('World').green, - @hd.string_send_color('World', :green) - assert_equal AnsiString.new('Default').plain, - @hd.string_send_color('Default', :blue) - end + fcb = @hd.start_fenced_block(line, headings, regex) + + assert_instance_of MarkdownExec::FCB, fcb + assert_equal headings, fcb.headings + assert_equal 'fenced', fcb.dname end + end - def test_yield_line_if_selected_with_line - block_called = false - HashDelegator.yield_line_if_selected('Test line', - [:line]) do |type, content| - block_called = true - assert_equal :line, type - assert_equal 'Test line', content.body[0] - end - assert block_called + class TestHashDelegatorStringSendColor < Minitest::Test + def setup + @hd = HashDelegator.new + @hd.instance_variable_set(:@delegate_object, + { red: 'red', green: 'green' }) end - def test_yield_line_if_selected_without_line - block_called = false - HashDelegator.yield_line_if_selected('Test line', [:other]) do |_| - block_called = true - end - refute block_called + def test_string_send_color + assert_equal AnsiString.new('Hello').red, + @hd.string_send_color('Hello', :red) + assert_equal AnsiString.new('World').green, + @hd.string_send_color('World', :green) + assert_equal AnsiString.new('Default').plain, + @hd.string_send_color('Default', :blue) end + end - def test_yield_line_if_selected_without_block - result = HashDelegator.yield_line_if_selected('Test line', [:line]) - assert_nil result + def test_yield_line_if_selected_with_line + block_called = false + HashDelegator.yield_line_if_selected('Test line', + [:line]) do |type, content| + block_called = true + assert_equal :line, type + assert_equal 'Test line', content.body[0] end + assert block_called end + def test_yield_line_if_selected_without_line + block_called = false + HashDelegator.yield_line_if_selected('Test line', [:other]) do |_| + block_called = true + end + refute block_called + end + + def test_yield_line_if_selected_without_block + result = HashDelegator.yield_line_if_selected('Test line', [:line]) + assert_nil result + end + # end + class TestHashDelegatorUpdateMenuAttribYieldSelectedWithBody < Minitest::Test def setup @hd = HashDelegator.new @fcb = mock('Fcb') @fcb.stubs(:body).returns(true) @@ -4446,11 +5111,11 @@ @hd = HashDelegator.new HashDelegator.stubs(:error_handler) end def test_wait_for_user_selected_block_with_back_state - mock_block_state = Struct.new(:state, :block).new(MenuState::BACK, - { oname: 'back_block' }) + mock_block_state = Struct.new(:state, :block) + .new(MenuState::BACK, { oname: 'back_block' }) @hd.stubs(:wait_for_user_selection).returns(mock_block_state) result = @hd.wait_for_user_selected_block([], ['Block 1', 'Block 2'], nil)