lib/hash_delegator.rb in markdown_exec-1.8.6 vs lib/hash_delegator.rb in markdown_exec-1.8.7

- old
+ new

@@ -35,10 +35,277 @@ def non_empty? !empty? end end +module HashDelegatorSelf + # def add_back_option(menu_blocks) + # append_chrome_block(menu_blocks, MenuState::BACK) + # end + + # Applies an ANSI color method to a string using a specified color key. + # 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 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 + 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 key 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 key [Object] The key to search for in each element of the collection. + # @param value [Object] The value to match against each element's corresponding key value. + # @param default [Object, nil] The default value to return if no match is found (optional). + # @return [Object, nil] The first matching element or the default value if no match is found. + def block_find(blocks, key, value, default = nil) + blocks.find { |item| item[key] == value } || default + end + + def code_merge(*bodies) + merge_lists(*bodies) + end + + def count_matches_in_lines(lines, regex) + lines.count { |line| line.to_s.match(regex) } + end + + 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. + # + # @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. + 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? + rescue StandardError + error_handler('create_file_and_write_string_with_permissions') + end + + # def create_temp_file + # Dir::Tmpname.create(self.class.to_s) { |path| path } + # 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? + + 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| + prev_item&.fetch(:chrome, nil) && !prev_item&.fetch(:oname).present? && + current_item&.fetch(:chrome, nil) && !current_item&.fetch(: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 = {}) + Exceptions.error_handler( + "HashDelegator.#{name} -- #{$!}", + 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 + + # Formats and returns the execution streams (like stdin, stdout, stderr) for a given key. + # It concatenates the array of strings found under the specified key in the run_state's files. + # + # @param key [Symbol] The key corresponding to the desired execution stream. + # @return [String] A concatenated string of the execution stream's contents. + def format_execution_streams(key, files = {}) + (files || {}).fetch(key, []).join + 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). + # @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") + end + + def initialize_fcb_names(fcb) + fcb.oname = fcb.dname = fcb.title || '' + 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 + merged = args.compact.flatten + merged.empty? ? [] : merged + end + + def next_link_state(block_name_from_cli, was_using_cli, block_state) + # &bsp 'next_link_state', block_name_from_cli, was_using_cli, block_state + # Set block_name based on block_name_from_cli + block_name = block_name_from_cli ? @cli_block_name : nil + # &bsp 'block_name:', block_name + + # Determine the state of breaker based on was_using_cli and the block type + breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block[:shell] == BlockType::BASH + # &bsp 'breaker:', breaker + + # Reset block_name_from_cli if the conditions are not met + block_name_from_cli ||= false + # &bsp 'block_name_from_cli:', block_name_from_cli + + [block_name, block_name_from_cli, breaker] + end + + def parse_yaml_data_from_body(body) + body.any? ? YAML.load(body.join("\n")) : {} + 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. + 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( + temp_blocks_file_path, chomp: true + ) + else + [] + end + end + + def remove_file_without_standard_errors(path) + FileUtils.rm_f(path) + end + + # Evaluates the given string as Ruby code and rescues any StandardErrors. + # If an error occurs, it calls the error_handler method with 'safeval'. + # @param str [String] The string to be evaluated. + # @return [Object] The result of evaluating the string. + def safeval(str) + eval(str) + rescue StandardError # catches NameError, StandardError + error_handler('safeval') + end + + def set_file_permissions(file_path, chmod_value) + File.chmod(chmod_value, file_path) + end + + # 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. + def tty_prompt_without_disabled_symbol + TTY::Prompt.new( + interrupt: lambda { + puts + raise TTY::Reader::InputInterrupt + }, + symbols: { cross: ' ' } + ) + end + + # 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. + # + # @param fcb [Object] The fcb object whose attributes are to be updated. + # @param selected_messages [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, selected_messages, configuration = {}, &block) + initialize_fcb_names(fcb) + return unless fcb.body + + default_block_title_from_body(fcb) + MarkdownExec::Filter.yield_to_block_if_applicable(fcb, selected_messages, configuration, + &block) + end + + # Writes the provided code blocks to a file. + # @param code_blocks [String] Code blocks to write into the file. + def write_code_to_file(content, path) + File.write(path, content) + end + + def write_execution_output_to_file(files, filespec) + FileUtils.mkdir_p File.dirname(filespec) + + File.write( + filespec, + ["-STDOUT-\n", + format_execution_streams(ExecutionStreams::StdOut, files), + "-STDERR-\n", + format_execution_streams(ExecutionStreams::StdErr, files), + "-STDIN-\n", + format_execution_streams(ExecutionStreams::StdIn, files), + "\n"].join + ) + end + + # Yields a line as a new block if the selected message type includes :line. + # @param [String] line The line to be processed. + # @param [Array<Symbol>] selected_messages A list of message types to check. + # @param [Proc] block The block to be called with the line data. + def yield_line_if_selected(line, selected_messages, &block) + return unless block && selected_messages.include?(:line) + + block.call(:line, MarkdownExec::FCB.new(body: [line])) + end +end +### require_relative 'hash_delegator_self' + # 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. # Each value is processed by `compact_hash` to remove ineligible elements. # @@ -104,15 +371,16 @@ end class HashDelegator attr_accessor :most_recent_loaded_filename, :pass_args, :run_state + extend HashDelegatorSelf include CompactionHelpers def initialize(delegate_object = {}) @delegate_object = delegate_object - @prompt = tty_prompt_without_disabled_symbol + @prompt = HashDelegator.tty_prompt_without_disabled_symbol @most_recent_loaded_filename = nil @pass_args = [] @run_state = OpenStruct.new( link_history: [] @@ -136,31 +404,44 @@ # Modifies the provided menu blocks array by adding 'Back' and 'Exit' options, # along with initial and final dividers, based on the delegate object's configuration. # # @param menu_blocks [Array] The array of menu block elements to be modified. - def add_menu_chrome_blocks!(menu_blocks) + def add_menu_chrome_blocks!(menu_blocks, link_state) return unless @delegate_object[:menu_link_format].present? + if @delegate_object[:menu_with_inherited_lines] + add_inherited_lines(menu_blocks, + link_state) + end + + # back before exit add_back_option(menu_blocks) if should_add_back_option? + + # exit after other options add_exit_option(menu_blocks) if @delegate_object[:menu_with_exit] + add_dividers(menu_blocks) end private def add_back_option(menu_blocks) append_chrome_block(menu_blocks, MenuState::BACK) end + def add_dividers(menu_blocks) + append_divider(menu_blocks, :initial) + append_divider(menu_blocks, :final) + end + def add_exit_option(menu_blocks) append_chrome_block(menu_blocks, MenuState::EXIT) end - def add_dividers(menu_blocks) - append_divider(menu_blocks, :initial) - append_divider(menu_blocks, :final) + def add_inherited_lines(menu_blocks, link_state) + append_inherited_lines(menu_blocks, link_state) end public # Appends a chrome block, which is a menu option for Back or Exit @@ -177,11 +458,11 @@ option_name = @delegate_object[:menu_option_exit_name] insert_at_top = @delegate_object[:menu_exit_at_top] end formatted_name = format(@delegate_object[:menu_link_format], - safeval(option_name)) + HashDelegator.safeval(option_name)) chrome_block = FCB.new( chrome: true, dname: HashDelegator.new(@delegate_object).string_send_color( formatted_name, :menu_link_color ), @@ -198,10 +479,43 @@ # Appends a formatted divider to the specified position in a menu block array. # The method checks for the presence of formatting options before appending. # # @param menu_blocks [Array] The array of menu block elements. # @param position [Symbol] The position to insert the divider (:initial or :final). + def append_inherited_lines(menu_blocks, link_state, position: top) + return unless link_state.inherited_lines.present? + + insert_at_top = @delegate_object[:menu_inherited_lines_at_top] + chrome_blocks = link_state.inherited_lines.map do |line| + formatted = format(@delegate_object[:menu_inherited_lines_format], + { line: line }) + FCB.new( + chrome: true, + disabled: '', + dname: HashDelegator.new(@delegate_object).string_send_color( + formatted, :menu_inherited_lines_color + ), + oname: formatted + ) + end + + if insert_at_top + # Prepend an array of elements to the beginning + menu_blocks.unshift(*chrome_blocks) + else + # Append an array of elements to the end + menu_blocks.concat(chrome_blocks) + end + rescue StandardError + HashDelegator.error_handler('append_inherited_lines') + end + + # Appends a formatted divider to the specified position in a menu block array. + # The method checks for the presence of formatting options before appending. + # + # @param menu_blocks [Array] The array of menu block elements. + # @param position [Symbol] The position to insert the divider (:initial or :final). def append_divider(menu_blocks, position) return unless divider_formatting_present?(position) divider = create_divider(position) position == :initial ? menu_blocks.unshift(divider) : menu_blocks.push(divider) @@ -222,35 +536,23 @@ end end # private - # Searches for the first element in a collection where the specified key 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 key [Object] The key to search for in each element of the collection. - # @param value [Object] The value to match against each element's corresponding key value. - # @param default [Object, nil] The default value to return if no match is found (optional). - # @return [Object, nil] The first matching element or the default value if no match is found. - def block_find(blocks, key, value, default = nil) - blocks.find { |item| item[key] == value } || default - end - # 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 blocks = [] iter_blocks_from_nested_files do |btype, fcb| process_block_based_on_type(blocks, btype, fcb) end + # &bc 'blocks.count:', blocks.count blocks rescue StandardError - error_handler('blocks_from_nested_files') + HashDelegator.error_handler('blocks_from_nested_files') end # private def cfile @@ -271,19 +573,10 @@ return false end true end - def code_join(*bodies) - bc = bodies&.compact - bc.count.positive? ? bc.join("\n") : nil - end - - def code_merge(*bodies) - merge_lists(*bodies) - 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. @@ -310,11 +603,11 @@ elsif true warn format_and_highlight_dependencies(dependencies, highlight: [@delegate_object[:block_name]]) end - code_merge link_state&.inherited_lines, required[:code] + HashDelegator.code_merge(link_state&.inherited_lines, required[:code]) end def command_execute(command, args: []) @run_state.files = Hash.new([]) @run_state.options = @delegate_object @@ -361,21 +654,21 @@ def load_cli_or_user_selected_block(all_blocks, menu_blocks, default) if @delegate_object[:block_name].present? block = all_blocks.find do |item| item[:oname] == @delegate_object[:block_name] - end + end&.merge(block_name_from_ui: false) else block_state = wait_for_user_selected_block(all_blocks, menu_blocks, default) - block = block_state.block + block = block_state.block&.merge(block_name_from_ui: true) state = block_state.state end SelectedBlockMenuState.new(block, state) rescue StandardError - error_handler('load_cli_or_user_selected_block') + HashDelegator.error_handler('load_cli_or_user_selected_block') 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. @@ -410,21 +703,13 @@ # # @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]) - count_matches_in_lines(lines, regex) / 2 + HashDelegator.count_matches_in_lines(lines, regex) / 2 end - # private - - def count_matches_in_lines(lines, regex) - lines.count { |line| line.to_s.match(regex) } - end - - # private - ## # 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. @@ -447,16 +732,17 @@ # @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. def create_and_add_chrome_blocks(blocks, fcb) match_criteria = [ - { match: :menu_task_match, format: :menu_task_format, - color: :menu_task_color }, + { match: :heading1_match, format: :menu_heading1_format, color: :menu_heading1_color }, + { match: :heading2_match, format: :menu_heading2_format, color: :menu_heading2_color }, + { match: :heading3_match, format: :menu_heading3_format, color: :menu_heading3_color }, { match: :menu_divider_match, format: :menu_divider_format, color: :menu_divider_color }, - { match: :menu_note_match, format: :menu_note_format, - color: :menu_note_color } + { match: :menu_note_match, format: :menu_note_format, color: :menu_note_color }, + { match: :menu_task_match, format: :menu_task_format, color: :menu_task_color } ] match_criteria.each do |criteria| unless @delegate_object[criteria[:match]].present? && (mbody = fcb.body[0].match @delegate_object[criteria[:match]]) next @@ -466,76 +752,23 @@ @delegate_object[criteria[:color]].to_sym) break end 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. - # - # @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. - 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? - rescue StandardError - error_handler('create_file_and_write_string_with_permissions') - end - - # private - - def create_directory_for_file(file_path) - FileUtils.mkdir_p(File.dirname(file_path)) - end - def create_divider(position) divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider oname = format(@delegate_object[:menu_divider_format], - safeval(@delegate_object[divider_key])) + HashDelegator.safeval(@delegate_object[divider_key])) FCB.new( chrome: true, disabled: '', dname: string_send_color(oname, :menu_divider_color), oname: oname ) end - # private - - def create_temp_file - Dir::Tmpname.create(self.class.to_s) { |path| path } - 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? - - 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| - prev_item&.fetch(:chrome, nil) && !prev_item&.fetch(:oname).present? && - current_item&.fetch(:chrome, nil) && !current_item&.fetch(: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? - - safely_remove_file(temp_blocks_file_path) - rescue StandardError - error_handler('delete_required_temp_file') - 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. # # @param selected_option [Hash] The selected menu option. # @return [SelectedBlockMenuState] An object representing the state of the selected block. @@ -568,28 +801,19 @@ 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? end - def error_handler(name = '', opts = {}) - Exceptions.error_handler( - "HashDelegator.#{name} -- #{$!}", - opts - ) - end - - # public - # 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 # 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 = []) - # set_script_block_name(selected) - save_executed_script_if_specified(required_lines) + # @run_state.script_block_name = selected[:oname] + write_command_file(required_lines) if @delegate_object[:save_executed_script] format_and_execute_command(required_lines) post_execution_process end # Execute a code block after approval and provide user interaction options. @@ -634,21 +858,10 @@ color_sym: :execution_report_preview_frame_color) data_string = @delegate_object.fetch(data_sym, default).to_s string_send_color(data_string, color_sym) 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 - def format_and_execute_command(lines) formatted_command = 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) @@ -670,20 +883,10 @@ formatted_string = format(@delegate_object.fetch(format_sym, ''), context).to_s string_send_color(formatted_string, color_sym) end - # Formats and returns the execution streams (like stdin, stdout, stderr) for a given key. - # It concatenates the array of strings found under the specified key in the run_state's files. - # - # @param key [Symbol] The key corresponding to the desired execution stream. - # @return [String] A concatenated string of the execution stream's contents. - def format_execution_streams(key) - files = @run_state.files || {} - files.fetch(key, []).join - 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. @@ -709,11 +912,11 @@ # 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. # # @param block_state [Object] An object representing the state of a block in the menu. - def handle_block_state(block_state) + def handle_back_or_continue(block_state) return if block_state.nil? unless [MenuState::BACK, MenuState::CONTINUE].include?(block_state.state) return end @@ -742,20 +945,10 @@ @process_cv.signal end end 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). - # @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") - end - # Initializes variables for regex and other states def initial_state { fenced_start_and_end_regex: Regexp.new(@delegate_object.fetch( :fenced_start_and_end_regex, '^(?<indent> *)`{3,}' @@ -782,17 +975,14 @@ @logged_stdout_filespec = @delegate_object[:logged_stdout_filespec] = File.join @delegate_object[:saved_stdout_folder], @delegate_object[:logged_stdout_filename] @logged_stdout_filespec = @delegate_object[:logged_stdout_filespec] - write_execution_output_to_file + HashDelegator.write_execution_output_to_file(@run_state.files, + @delegate_object[:logged_stdout_filespec]) end - def initialize_fcb_names(fcb) - fcb.oname = fcb.dname = fcb.title || '' - end - # 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]) @@ -843,11 +1033,11 @@ block_name = @delegate_object[:document_load_opts_block_name] unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename] return end - block = block_find(all_blocks, :oname, block_name) + block = HashDelegator.block_find(all_blocks, :oname, block_name) return unless block options_state = read_show_options_and_trigger_reuse(block) @menu_base_options.merge!(options_state.options) @delegate_object.merge!(options_state.options) @@ -864,20 +1054,21 @@ [menu_blocks, mdoc] end ## Handles the file loading and returns the blocks in the file and MDoc instance # - def mdoc_menu_and_blocks_from_nested_files + 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_blocks(all_blocks) menu_blocks = mdoc.fcbs_per_options(@delegate_object) - add_menu_chrome_blocks!(menu_blocks) - delete_consecutive_blank_lines!(menu_blocks) if true ### compress empty lines + add_menu_chrome_blocks!(menu_blocks, link_state) + ### compress empty lines + HashDelegator.delete_consecutive_blank_lines!(menu_blocks) if true [all_blocks, menu_blocks, mdoc] 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. @@ -892,25 +1083,19 @@ # 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. # @return [String] The formatted or original value of the menu option. def menu_chrome_formatted_option(option_symbol = :menu_option_back_name) - option_value = safeval(@delegate_object.fetch(option_symbol, '')) + option_value = HashDelegator.safeval(@delegate_object.fetch(option_symbol, '')) if @delegate_object[:menu_chrome_format] format(@delegate_object[:menu_chrome_format], option_value) else option_value end 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 - merged = args.compact.flatten - merged.empty? ? [] : merged - end - # If a method is missing, treat it as a key for the @delegate_object. def method_missing(method_name, *args, &block) if @delegate_object.respond_to?(method_name) @delegate_object.send(method_name, *args, &block) elsif method_name.to_s.end_with?('=') && args.size == 1 @@ -919,21 +1104,17 @@ @delegate_object[method_name] # super end end - def shift_cli_argument! - return false unless @menu_base_options[:input_cli_rest].present? + def shift_cli_argument + return true unless @menu_base_options[:input_cli_rest].present? @cli_block_name = @menu_base_options[:input_cli_rest].shift - # @delegate_object[:input_cli_rest].shift - # p [__LINE__, @cli_block_name, @menu_base_options[:input_cli_rest]] - true + false end - # private - def output_color_formatted(data_sym, color_sym) formatted_string = string_send_color(@delegate_object[data_sym], color_sym) @fout.fout formatted_string end @@ -982,33 +1163,27 @@ :output_execution_label_value_color) }, format_sym: :output_execution_label_format ), level: level end - # private - - def parse_yaml_data_from_body(body) - body.any? ? YAML.load(body.join("\n")) : {} - end - def pop_add_current_code_to_head_and_trigger_load(_link_state, block_names, code_lines, dependencies) pop = @link_history.pop # updatable - next_link_state = LinkState.new( + next_state = LinkState.new( block_name: pop.block_name, document_filename: pop.document_filename, inherited_block_names: (pop.inherited_block_names + block_names).sort.uniq, inherited_dependencies: dependencies.merge(pop.inherited_dependencies || {}), ### merge, not replace, key data inherited_lines: - code_merge(pop.inherited_lines, code_lines) + HashDelegator.code_merge(pop.inherited_lines, code_lines) ) - @link_history.push(next_link_state) + @link_history.push(next_state) - next_link_state.block_name = nil - LoadFileLinkState.new(LoadFile::Load, next_link_state) + next_state.block_name = nil + LoadFileLinkState.new(LoadFile::Load, next_state) end # This method handles the back-link operation in the Markdown execution context. # It updates the history state and prepares to load the next block. # @@ -1039,11 +1214,11 @@ menu_blocks.map do |fcb| next if Filter.prepared_not_in_menu?(@delegate_object, fcb, %i[block_name_include_match block_name_wrapper_match]) fcb.merge!( - name: indent_all_lines(fcb.dname, fcb.fetch(:indent, nil)), + name: HashDelegator.indent_all_lines(fcb.dname, fcb.fetch(:indent, nil)), label: BlockLabel.make( body: fcb[:body], filename: @delegate_object[:filename], headings: fcb.fetch(:headings, []), menu_blocks_with_docname: @delegate_object[:menu_blocks_with_docname], @@ -1069,14 +1244,11 @@ 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 + create_and_add_chrome_blocks(blocks, fcb) unless @delegate_object[:no_chrome] end end ## # Presents a menu to the user for approving an action and performs additional tasks based on the selection. @@ -1146,11 +1318,11 @@ # @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, selected, link_state = LinkState.new) - link_block_data = parse_yaml_data_from_body(link_block_body) + link_block_data = HashDelegator.parse_yaml_data_from_body(link_block_body) # load key and values from link block into current environment # (link_block_data['vars'] || []).each do |(key, value)| ENV[key] = value.to_s @@ -1176,11 +1348,11 @@ next_document_filename = link_block_data['file'] || @delegate_object[:filename] # if an eval link block, evaluate code_lines and return its standard output # if link_block_data.fetch('eval', false) - all_code = code_merge(link_state&.inherited_lines, code_lines) + all_code = HashDelegator.code_merge(link_state&.inherited_lines, code_lines) output = `#{all_code.join("\n")}`.split("\n") label_format_above = @delegate_object[:shell_code_label_format_above] label_format_below = @delegate_object[:shell_code_label_format_below] block_source = { document_filename: link_state&.document_filename } @@ -1205,32 +1377,18 @@ link_history_push_and_next( curr_block_name: selected[:oname], 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: code_merge(link_state&.inherited_lines, code_lines), + inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines), next_block_name: link_block_data['block'] || '', next_document_filename: next_document_filename, next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load ) end 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. - 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( - temp_blocks_file_path, chomp: true - ) - else - [] - end - end - def runtime_exception(exception_sym, name, items) if @delegate_object[exception_sym] != 0 data = { name: name, detail: items.join(', ') } warn( format( @@ -1246,24 +1404,10 @@ return unless (@delegate_object[exception_sym]).positive? exit @delegate_object[exception_sym] end - def safely_remove_file(path) - FileUtils.rm_f(path) - end - - # Evaluates the given string as Ruby code and rescues any StandardErrors. - # If an error occurs, it calls the error_handler method with 'safeval'. - # @param str [String] The string to be evaluated. - # @return [Object] The result of evaluating the string. - def safeval(str) - eval(str) - rescue StandardError - error_handler('safeval') - end - def save_to_file(required_lines) write_command_file(required_lines) @fout.fout "File saved: #{@run_state.saved_filespec}" end @@ -1273,57 +1417,57 @@ # 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. def document_menu_loop @menu_base_options = @delegate_object - link_state, block_name_from_cli, now_using_cli = initialize_selection_states + link_state = LinkState.new( + block_name: @delegate_object[:block_name], + document_filename: @delegate_object[:filename] + ) + block_name_from_cli = link_state.block_name.present? + @cli_block_name = link_state.block_name + now_using_cli = block_name_from_cli menu_default_dname = nil loop do - # @bsp 'loop',block_name_from_cli,@cli_block_name + # &bsp 'loop', block_name_from_cli, @cli_block_name block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc = \ set_delobj_menu_loop_vars(block_name_from_cli, now_using_cli, link_state) # cli or user selection # block_state = load_cli_or_user_selected_block(blocks_in_file, menu_blocks, menu_default_dname) - if block_state.state == MenuState::EXIT - # @bsp 'MenuState::EXIT -> break' + # &bsp 'block_name_from_cli:',block_name_from_cli + if !block_state + HashDelegator.error_handler('block_state missing', { abort: true }) + elsif block_state.state == MenuState::EXIT + # &bsp 'load_cli_or_user_selected_block -> break' break end dump_and_warn_block_state(block_state.block) link_state, menu_default_dname = exec_bash_next_state(block_state.block, mdoc, link_state) if prompt_user_exit(block_name_from_cli, block_state.block) - # @bsp 'prompt_user_exit -> break' + # &bsp 'prompt_user_exit -> break' break end link_state.block_name, block_name_from_cli, cli_break = \ - next_state_from_cli(now_using_cli, block_state) + HashDelegator.next_link_state(!shift_cli_argument, now_using_cli, block_state) - if cli_break - # @bsp 'read_block_name_from_cli + next_link_state -> break' + if !block_state.block[:block_name_from_ui] && cli_break + # &bsp '!block_name_from_ui + cli_break -> break' break end end rescue StandardError - error_handler('document_menu_loop', - { abort: true }) + HashDelegator.error_handler('document_menu_loop', + { abort: true }) end - def next_state_from_cli(now_using_cli, block_state) - was_using_cli = now_using_cli - block_name_from_cli, = read_block_name_from_cli(now_using_cli) - block_name, block_name_from_cli, cli_break = \ - next_link_state(block_name_from_cli, was_using_cli, block_state) - - [block_name, block_name_from_cli, cli_break] - end - def exec_bash_next_state(block_state_block, mdoc, link_state) lfls = execute_shell_type( block_state_block, mdoc, link_state, @@ -1340,11 +1484,11 @@ manage_cli_selection_state(block_name_from_cli, now_using_cli, link_state) set_delob_filename_block_name(link_state, block_name_from_cli) # update @delegate_object and @menu_base_options in auto_load # - blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files + blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state) dump_delobj(blocks_in_file, menu_blocks, link_state) [block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc] end @@ -1356,12 +1500,12 @@ @delegate_object[:pause_after_script_execution] && prompt_select_continue == MenuState::EXIT end def manage_cli_selection_state(block_name_from_cli, now_using_cli, link_state) - if block_name_from_cli && @cli_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] + # &bsp '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 = \ @@ -1371,49 +1515,10 @@ @delegate_object = @menu_base_options.dup @menu_user_clicked_back_link = false [block_name_from_cli, now_using_cli] end - def next_link_state(block_name_from_cli, was_using_cli, block_state) - # @bsp 'next_link_state',block_name_from_cli, was_using_cli, block_state - # Set block_name based on block_name_from_cli - block_name = block_name_from_cli ? @cli_block_name : nil - - # Determine the state of breaker based on was_using_cli and the block type - breaker = !block_name_from_cli && was_using_cli && block_state.block[:shell] == BlockType::BASH - - # Reset block_name_from_cli if the conditions are not met - block_name_from_cli ||= false - - [block_name, block_name_from_cli, breaker] - end - - # Initialize the selection states for the execution loop. - def initialize_selection_states - link_state = LinkState.new( - block_name: @delegate_object[:block_name], - document_filename: @delegate_object[:filename] - ) - block_name_from_cli, now_using_cli = handle_cli_block_name(link_state) - [link_state, block_name_from_cli, now_using_cli] - end - - # Update the state related to CLI block name. - # - # This method updates the flags indicating whether a CLI block name is being used - # and if it was being used previously. - # - # @param block_name_from_cli [Boolean] Indicates if the block name is from CLI. - # @return [Array] Returns the updated state of block name from CLI and its usage. - def read_block_name_from_cli(block_name_from_cli) - # was_using_cli = block_name_from_cli - block_name_from_cli = shift_cli_argument! - now_using_cli = block_name_from_cli - - [block_name_from_cli, now_using_cli] - end - # Update the block name in the link state and delegate object. # # This method updates the block name based on whether it was specified # through the CLI or derived from the link state. # @@ -1423,25 +1528,10 @@ @delegate_object[:filename] = link_state.document_filename link_state.block_name = @delegate_object[:block_name] = block_name_from_cli ? @cli_block_name : link_state.block_name end - # Handle CLI block name and determine the current CLI usage state. - # - # This method processes the CLI block name from the link state and sets - # the initial state for CLI usage. - # - # @param link_state [LinkState] The current link state object. - # @return [Array] Returns the state of block name from CLI and current usage of CLI. - def handle_cli_block_name(link_state) - block_name_from_cli = link_state.block_name.present? - @cli_block_name = link_state.block_name - now_using_cli = block_name_from_cli - - [block_name_from_cli, now_using_cli] - 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 menu_blocks [Hash] Hash of menu blocks. @@ -1498,11 +1588,11 @@ end ) rescue TTY::Reader::InputInterrupt exit 1 rescue StandardError - error_handler('select_option_with_metadata') + HashDelegator.error_handler('select_option_with_metadata') end def set_environment_variables_for_block(selected) YAML.load(selected[:body].join("\n")).each do |key, value| ENV[key] = value.to_s @@ -1512,26 +1602,12 @@ { key: key, value: value }) print string_send_color(formatted_string, :menu_vars_set_color) end end - def set_environment_variables_per_array(vars) - vars ||= [] - vars.each { |key, value| ENV[key] = value.to_s } - end - - def set_file_permissions(file_path, chmod_value) - File.chmod(chmod_value, file_path) - end - - def set_script_block_name(selected) - @run_state.script_block_name = selected[:oname] - end - def should_add_back_option? @delegate_object[:menu_with_back] && @link_history.prior_state_exist? - # @delegate_object[: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. # @param line [String] The line initiating the fenced block. # @param headings [Array<String>] Current headings hierarchy. @@ -1571,50 +1647,14 @@ # 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. # @return [String] The string with the applied color method. - def string_send_color(string, color_sym, default: 'plain') - color_method = @delegate_object.fetch(color_sym, default).to_sym - string.to_s.send(color_method) + def string_send_color(string, color_sym) + HashDelegator.apply_color_from_hash(string, @delegate_object, color_sym) end - # 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. - def tty_prompt_without_disabled_symbol - TTY::Prompt.new( - interrupt: lambda { - puts - raise TTY::Reader::InputInterrupt - }, - symbols: { cross: ' ' } - ) - end - - # Updates the hierarchy of document headings based on the given line. - # Utilizes regular expressions to identify heading levels. - # @param line [String] The line of text to check for headings. - # @param headings [Array<String>] Current headings hierarchy. - # @return [Array<String>] Updated headings hierarchy. - def update_document_headings(line, headings) - heading3_match = Regexp.new(@delegate_object[:heading3_match]) - heading2_match = Regexp.new(@delegate_object[:heading2_match]) - heading1_match = Regexp.new(@delegate_object[:heading1_match]) - - case line - when heading3_match - [headings[0], headings[1], $~[:name]] - when heading2_match - [headings[0], $~[:name]] - when heading1_match - [$~[:name]] - else - headings - end - 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. # # @param line [String] The current line being processed. @@ -1633,20 +1673,16 @@ # @return [Void] The function modifies the `state` and `selected_messages` arguments in place. ## def update_line_and_block_state(nested_line, state, selected_messages, &block) line = nested_line.to_s - if @delegate_object[:menu_blocks_with_headings] - state[:headings] = update_document_headings(line, state[:headings]) - end - if line.match(@delegate_object[:fenced_start_and_end_regex]) if state[:in_fenced_block] ## end of code block # - update_menu_attrib_yield_selected(state[:fcb], selected_messages, - &block) + HashDelegator.update_menu_attrib_yield_selected(state[:fcb], selected_messages, @delegate_object, + &block) state[:in_fenced_block] = false else ## start of code block # state[:fcb] = @@ -1663,33 +1699,18 @@ line.chomp.sub(/^#{state[:fcb].indent}/, '') ] elsif nested_line[:depth].zero? || @delegate_object[:menu_include_imported_notes] # add line if it is depth 0 or option allows it # - yield_line_if_selected(line, selected_messages, &block) + HashDelegator.yield_line_if_selected(line, selected_messages, &block) else - # @bsp 'line is not recognized for block state' + # &bsp 'line is not recognized for block state' end end - # 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. - # - # @param fcb [Object] The fcb object whose attributes are to be updated. - # @param selected_messages [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, selected_messages, &block) - initialize_fcb_names(fcb) - return unless fcb.body - - default_block_title_from_body(fcb) - yield_to_block_if_applicable(fcb, selected_messages, &block) - 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. def read_show_options_and_trigger_reuse(selected, link_state = LinkState.new) @@ -1715,14 +1736,14 @@ end end def wait_for_user_selected_block(all_blocks, menu_blocks, default) block_state = wait_for_user_selection(all_blocks, menu_blocks, default) - handle_block_state(block_state) + handle_back_or_continue(block_state) block_state rescue StandardError - error_handler('wait_for_user_selected_block') + HashDelegator.error_handler('wait_for_user_selected_block') end def wait_for_user_selection(_all_blocks, menu_blocks, default) prompt_title = string_send_color( @delegate_object[:prompt_select_block].to_s, :prompt_color_after_script_execution @@ -1769,38 +1790,19 @@ "# file_name: #{@delegate_object[:filename]}\n" \ "# block_name: #{@delegate_object[:block_name]}\n" \ "# time: #{time_now}\n" \ "#{required_lines.flatten.join("\n")}\n" - create_file_and_write_string_with_permissions( + HashDelegator.create_file_and_write_string_with_permissions( @run_state.saved_filespec, content, @delegate_object[:saved_script_chmod] ) rescue StandardError - error_handler('write_command_file') + HashDelegator.error_handler('write_command_file') end - def save_executed_script_if_specified(lines) - write_command_file(lines) if @delegate_object[:save_executed_script] - end - - def write_execution_output_to_file - FileUtils.mkdir_p File.dirname(@delegate_object[:logged_stdout_filespec]) - - File.write( - @delegate_object[:logged_stdout_filespec], - ["-STDOUT-\n", - format_execution_streams(ExecutionStreams::StdOut), - "-STDERR-\n", - format_execution_streams(ExecutionStreams::StdErr), - "-STDIN-\n", - format_execution_streams(ExecutionStreams::StdIn), - "\n"].join - ) - end - # Writes required code blocks to a temporary file and sets an environment variable with its path. # # @param mdoc [Object] The Markdown document object. # @param block_name [String] The name of the block to collect code for. def write_required_blocks_to_file(mdoc, block_name, temp_file_path, import_filename: nil) @@ -1812,45 +1814,15 @@ )[:code] else [] end - code_blocks = (read_required_blocks_from_temp_file(import_filename) + + code_blocks = (HashDelegator.read_required_blocks_from_temp_file(import_filename) + c1).join("\n") - write_code_to_file(code_blocks, temp_file_path) + HashDelegator.write_code_to_file(code_blocks, temp_file_path) end - - # Writes the provided code blocks to a file. - # @param code_blocks [String] Code blocks to write into the file. - def write_code_to_file(content, path) - File.write(path, content) - end - - # Yields a line as a new block if the selected message type includes :line. - # @param [String] line The line to be processed. - # @param [Array<Symbol>] selected_messages A list of message types to check. - # @param [Proc] block The block to be called with the line data. - def yield_line_if_selected(line, selected_messages, &block) - return unless block && selected_messages.include?(:line) - - block.call(:line, FCB.new(body: [line])) - end - - # Yields to the provided block with specified parameters if certain conditions are met. - # The method checks if a block is given, if the selected_messages include :blocks, - # and if the fcb_select? method from MarkdownExec::Filter returns true for the given fcb. - # - # @param fcb [Object] The object to be evaluated and potentially passed to the block. - # @param selected_messages [Array<Symbol>] A collection of message types, one of which must be :blocks. - # @param block [Block] A block to be called if conditions are met. - def yield_to_block_if_applicable(fcb, selected_messages, &block) - if block_given? && selected_messages.include?(:blocks) && - MarkdownExec::Filter.fcb_select?(@delegate_object, fcb) - block.call :blocks, fcb - end - end end end if $PROGRAM_NAME == __FILE__ require 'bundler/setup' @@ -1915,34 +1887,34 @@ 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, @hd.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 - assert_equal body, @hd.indent_all_lines(body, indent) + assert_equal body, HashDelegator.indent_all_lines(body, indent) end def test_indent_all_lines_with_empty_indent body = "Line 1\nLine 2" indent = '' - assert_equal body, @hd.indent_all_lines(body, indent) + assert_equal body, HashDelegator.indent_all_lines(body, indent) end def test_safeval_successful_evaluation - assert_equal 4, @hd.safeval('2 + 2') + assert_equal 4, HashDelegator.safeval('2 + 2') end def test_safeval_rescue_from_error - @hd.stubs(:error_handler).with('safeval') - assert_nil @hd.safeval('invalid code') + HashDelegator.stubs(:error_handler).with('safeval') + assert_nil HashDelegator.safeval('invalid code') end def test_set_fcb_title # sample input and output data for testing default_block_title_from_body method input_output_data = [ @@ -1966,11 +1938,11 @@ # 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] - @hd.default_block_title_from_body(input) + HashDelegator.default_block_title_from_body(input) assert_equal output, input.title end end class TestHashDelegatorAppendDivider < Minitest::Test @@ -1981,11 +1953,11 @@ menu_initial_divider: 'Initial Divider', menu_final_divider: 'Final Divider', menu_divider_color: :color }) @hd.stubs(:string_send_color).returns('Formatted Divider') - @hd.stubs(:safeval).returns('Safe Value') + HashDelegator.stubs(:safeval).returns('Safe Value') end def test_append_divider_initial menu_blocks = [] @hd.append_divider(menu_blocks, :initial) @@ -2016,23 +1988,23 @@ @hd = HashDelegator.new end def test_block_find_with_match blocks = [{ key: 'value1' }, { key: 'value2' }] - result = @hd.block_find(blocks, :key, 'value1') + result = HashDelegator.block_find(blocks, :key, 'value1') assert_equal({ key: 'value1' }, result) end def test_block_find_without_match blocks = [{ key: 'value1' }, { key: 'value2' }] - result = @hd.block_find(blocks, :key, 'value3') + result = HashDelegator.block_find(blocks, :key, 'value3') assert_nil result end def test_block_find_with_default blocks = [{ key: 'value1' }, { key: 'value2' }] - result = @hd.block_find(blocks, :key, 'value3', 'default') + result = HashDelegator.block_find(blocks, :key, 'value3', 'default') assert_equal 'default', result end end class TestHashDelegatorBlocksFromNestedFiles < Minitest::Test @@ -2040,11 +2012,11 @@ @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, {}) - @hd.stubs(:error_handler) + HashDelegator.stubs(:error_handler) end def test_blocks_from_nested_files result = @hd.blocks_from_nested_files @@ -2066,11 +2038,11 @@ def setup @hd = HashDelegator.new @hd.instance_variable_set(:@delegate_object, {}) @mdoc = mock('YourMDocClass') @selected = { shell: BlockType::VARS, body: ['key: value'] } - @hd.stubs(:read_required_blocks_from_temp_file).returns([]) + 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 @@ -2084,33 +2056,33 @@ class TestHashDelegatorCommandOrUserSelectedBlock < Minitest::Test def setup @hd = HashDelegator.new @hd.instance_variable_set(:@delegate_object, {}) - @hd.stubs(:error_handler) + 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' }) result = @hd.load_cli_or_user_selected_block(all_blocks, [], nil) - assert_equal all_blocks.first, result.block + assert_equal all_blocks.first.merge(block_name_from_ui: false), result.block assert_nil result.state end def test_user_selected_block block_state = SelectedBlockMenuState.new({ oname: 'block2' }, :some_state) @hd.stubs(:wait_for_user_selected_block).returns(block_state) result = @hd.load_cli_or_user_selected_block([], [], nil) - assert_equal block_state.block, result.block + assert_equal block_state.block.merge(block_name_from_ui: true), result.block assert_equal :some_state, result.state end end class TestHashDelegatorCountBlockInFilename < Minitest::Test @@ -2143,11 +2115,11 @@ end class TestHashDelegatorCreateAndWriteFile < Minitest::Test def setup @hd = HashDelegator.new - @hd.stubs(:error_handler) + HashDelegator.stubs(:error_handler) FileUtils.stubs(:mkdir_p) File.stubs(:write) File.stubs(:chmod) end @@ -2158,12 +2130,12 @@ 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 - @hd.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 def test_create_and_write_file_without_chmod @@ -2173,12 +2145,12 @@ FileUtils.expects(:mkdir_p).with('/path/to').once File.expects(:write).with(file_path, content).once File.expects(:chmod).never - @hd.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 end @@ -2305,31 +2277,28 @@ @hd = HashDelegator.new @hd.instance_variable_set(:@run_state, mock('run_state')) end def test_format_execution_streams_with_valid_key - @hd.instance_variable_get(:@run_state).stubs(:files).returns({ stdout: %w[ - output1 output2 - ] }) + result = HashDelegator.format_execution_streams(:stdout, + { stdout: %w[output1 output2] }) - result = @hd.format_execution_streams(:stdout) - assert_equal 'output1output2', result end def test_format_execution_streams_with_empty_key @hd.instance_variable_get(:@run_state).stubs(:files).returns({}) - result = @hd.format_execution_streams(:stderr) + result = HashDelegator.format_execution_streams(:stderr) assert_equal '', result end def test_format_execution_streams_with_nil_files @hd.instance_variable_get(:@run_state).stubs(:files).returns(nil) - result = @hd.format_execution_streams(:stdin) + result = HashDelegator.format_execution_streams(:stdin) assert_equal '', result end end @@ -2356,37 +2325,37 @@ def setup @hd = HashDelegator.new @mock_block_state = mock('block_state') end - def test_handle_block_state_with_back + 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' }) - @hd.handle_block_state(@mock_block_state) + @hd.handle_back_or_continue(@mock_block_state) 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_handle_block_state_with_continue + 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' }) - @hd.handle_block_state(@mock_block_state) + @hd.handle_back_or_continue(@mock_block_state) 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_handle_block_state_with_other + 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_block_state(@mock_block_state) + @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 end @@ -2527,11 +2496,11 @@ @hd = HashDelegator.new @hd.instance_variable_set(:@delegate_object, { menu_option_back_name: "'Back'", menu_chrome_format: '-- %s --' }) - @hd.stubs(:safeval).with("'Back'").returns('Back') + HashDelegator.stubs(:safeval).with("'Back'").returns('Back') end def test_menu_chrome_formatted_option_with_format assert_equal '-- Back --', @hd.menu_chrome_formatted_option(:menu_option_back_name) @@ -2582,84 +2551,61 @@ end end def test_yield_line_if_selected_with_line block_called = false - @hd.yield_line_if_selected('Test line', [:line]) do |type, content| + 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 - @hd.yield_line_if_selected('Test line', [:other]) do |_| + 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 = @hd.yield_line_if_selected('Test line', [:line]) + result = HashDelegator.yield_line_if_selected('Test line', [:line]) assert_nil result end end - class TestHashDelegator < Minitest::Test - def setup - @hd = HashDelegator.new - @hd.instance_variable_set(:@delegate_object, { - heading1_match: '^# (?<name>.+)$', - heading2_match: '^## (?<name>.+)$', - heading3_match: '^### (?<name>.+)$' - }) - end - - def test_update_document_headings - assert_equal(['Heading 1'], - @hd.update_document_headings('# Heading 1', [])) - assert_equal(['Heading 1', 'Heading 2'], - @hd.update_document_headings('## Heading 2', - ['Heading 1'])) - assert_equal(['Heading 1', 'Heading 2', 'Heading 3'], - @hd.update_document_headings('### Heading 3', - ['Heading 1', 'Heading 2'])) - assert_equal([], @hd.update_document_headings('Regular text', [])) - end - end - class TestHashDelegatorUpdateMenuAttribYieldSelectedWithBody < Minitest::Test def setup @hd = HashDelegator.new @fcb = mock('Fcb') @fcb.stubs(:body).returns(true) - @hd.stubs(:initialize_fcb_names) - @hd.stubs(:default_block_title_from_body) - @hd.stubs(:yield_to_block_if_applicable) + HashDelegator.stubs(:initialize_fcb_names) + HashDelegator.stubs(:default_block_title_from_body) + Filter.stubs(:yield_to_block_if_applicable) end def test_update_menu_attrib_yield_selected_with_body - @hd.expects(:initialize_fcb_names).with(@fcb) - @hd.expects(:default_block_title_from_body).with(@fcb) - @hd.expects(:yield_to_block_if_applicable).with(@fcb, [:some_message]) + HashDelegator.expects(:initialize_fcb_names).with(@fcb) + HashDelegator.expects(:default_block_title_from_body).with(@fcb) + Filter.expects(:yield_to_block_if_applicable).with(@fcb, [:some_message], {}) - @hd.update_menu_attrib_yield_selected(@fcb, [:some_message]) + HashDelegator.update_menu_attrib_yield_selected(@fcb, [:some_message]) end def test_update_menu_attrib_yield_selected_without_body @fcb.stubs(:body).returns(nil) - @hd.expects(:initialize_fcb_names).with(@fcb) - @hd.update_menu_attrib_yield_selected(@fcb, [:some_message]) + HashDelegator.expects(:initialize_fcb_names).with(@fcb) + HashDelegator.update_menu_attrib_yield_selected(@fcb, [:some_message]) end end class TestHashDelegatorWaitForUserSelectedBlock < Minitest::Test def setup @hd = HashDelegator.new - @hd.stubs(:error_handler) + 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' }) @@ -2697,26 +2643,26 @@ MarkdownExec::Filter.stubs(:fcb_select?).returns(true) end def test_yield_to_block_if_applicable_with_correct_conditions block_called = false - @hd.yield_to_block_if_applicable(@fcb, [:blocks]) do |type, fcb| + Filter.yield_to_block_if_applicable(@fcb, [:blocks]) do |type, fcb| block_called = true assert_equal :blocks, type assert_equal @fcb, fcb end assert block_called end def test_yield_to_block_if_applicable_without_block - result = @hd.yield_to_block_if_applicable(@fcb, [:blocks]) + result = Filter.yield_to_block_if_applicable(@fcb, [:blocks]) assert_nil result end def test_yield_to_block_if_applicable_with_incorrect_conditions block_called = false MarkdownExec::Filter.stubs(:fcb_select?).returns(false) - @hd.yield_to_block_if_applicable(@fcb, [:non_blocks]) do |_| + Filter.yield_to_block_if_applicable(@fcb, [:non_blocks]) do |_| block_called = true end refute block_called end end