lib/markdown_exec.rb in markdown_exec-1.6 vs lib/markdown_exec.rb in markdown_exec-1.7
- old
+ new
@@ -16,12 +16,15 @@
require_relative 'block_label'
require_relative 'cached_nested_file_reader'
require_relative 'cli'
require_relative 'colorize'
require_relative 'env'
+require_relative 'exceptions'
require_relative 'fcb'
require_relative 'filter'
+require_relative 'fout'
+require_relative 'hash_delegator'
require_relative 'markdown_exec/version'
require_relative 'mdoc'
require_relative 'option_value'
require_relative 'saved_assets'
require_relative 'saved_files_matcher'
@@ -40,93 +43,41 @@
# custom error: file specified is missing
#
class FileMissingError < StandardError; end
-# hash with keys sorted by name
-# add Hash.sym_keys
-#
-class Hash
- unless defined?(sort_by_key)
- def sort_by_key
- keys.sort.to_h { |key| [key, self[key]] }
- end
- end
-
- unless defined?(sym_keys)
- def sym_keys
- transform_keys(&:to_sym)
- end
- end
+def dp(str)
+ lout " => #{str}", level: DISPLAY_LEVEL_DEBUG
end
-class LoadFile
- Load = true
- Reuse = false
+def rbp
+ rpry
+ pp(caller.take(4).map.with_index { |line, ind| " - #{ind}: #{line}" })
+ binding.pry
end
-class MenuState
- BACK = :back
- CONTINUE = :continue
- EXIT = :exit
+def bpp(*args)
+ pp '+ bpp()'
+ pp(*args.map.with_index { |line, ind| " - #{ind}: #{line}" })
+ rbp
end
-# integer value for comparison
-#
-def options_fetch_display_level(options)
- options.fetch(:display_level, 1)
-end
-
-# integer value for comparison
-#
-def options_fetch_display_level_xbase_prefix(options)
- options.fetch(:level_xbase_prefix, '')
-end
-
-# stdout manager
-#
-module FOUT
- # standard output; not for debug
- #
- def fout(str)
- puts str
- end
-
- def fout_list(str)
- puts str
- end
-
- def fout_section(name, data)
- puts "# #{name}"
- puts data.to_yaml
- end
-
- def approved_fout?(level)
- level <= options_fetch_display_level(@options)
- end
-
- # display output at level or lower than filter (DISPLAY_LEVEL_DEFAULT)
- #
- def lout(str, level: DISPLAY_LEVEL_BASE)
- return unless approved_fout? level
-
- fout level == DISPLAY_LEVEL_BASE ? str : options_fetch_display_level_xbase_prefix(@options) + str
- end
-end
-
-def dp(str)
- lout " => #{str}", level: DISPLAY_LEVEL_DEBUG
-end
-
def rpry
require 'pry-nav'
require 'pry-stack_explorer'
end
public
+# convert regex match groups to a hash with symbol keys
+#
# :reek:UtilityFunction
+def extract_named_captures_from_option(str, option)
+ str.match(Regexp.new(option))&.named_captures&.sym_keys
+end
+
+# :reek:UtilityFunction
def list_recent_output(saved_stdout_folder, saved_stdout_glob,
list_count)
SavedFilesMatcher.most_recent_list(saved_stdout_folder,
saved_stdout_glob, list_count)
end
@@ -136,195 +87,52 @@
list_count)
SavedFilesMatcher.most_recent_list(saved_script_folder,
saved_script_glob, list_count)
end
-# convert regex match groups to a hash with symbol keys
-#
-# :reek:UtilityFunction
-def extract_named_captures_from_option(str, option)
- str.match(Regexp.new(option))&.named_captures&.sym_keys
-end
-
-module ArrayUtil
- def self.partition_by_predicate(arr)
- true_list = []
- false_list = []
-
- arr.each do |element|
- if yield(element)
- true_list << element
- else
- false_list << element
- end
- end
-
- [true_list, false_list]
- end
-end
-
-module StringUtil
- # Splits the given string on the first occurrence of the specified character.
- # Returns an array containing the portion of the string before the character and the rest of the string.
- #
- # @param input_str [String] The string to be split.
- # @param split_char [String] The character on which to split the string.
- # @return [Array<String>] An array containing two elements: the part of the string before split_char, and the rest of the string.
- def self.partition_at_first(input_str, split_char)
- split_index = input_str.index(split_char)
-
- if split_index.nil?
- [input_str, '']
- else
- [input_str[0...split_index], input_str[(split_index + 1)..-1]]
- end
- end
-end
-
# execute markdown documents
#
module MarkdownExec
- # :reek:IrresponsibleModule
- FNR11 = '/'
- FNR12 = ',~'
+ include Exceptions
- SHELL_COLOR_OPTIONS = {
- BlockType::BASH => :menu_bash_color,
- BlockType::LINK => :menu_link_color,
- BlockType::OPTS => :menu_opts_color,
- BlockType::VARS => :menu_vars_color
- }.freeze
-
##
#
# rubocop:disable Layout/LineLength
# :reek:DuplicateMethodCall { allow_calls: ['block', 'item', 'lm', 'opts', 'option', '@options', 'required_blocks'] }
# rubocop:enable Layout/LineLength
# :reek:MissingSafeMethod { exclude: [ read_configuration_file! ] }
# :reek:TooManyInstanceVariables ### temp
# :reek:TooManyMethods ### temp
class MarkParse
- attr_reader :options
+ attr_reader :options, :prompt, :run_state
include ArrayUtil
include StringUtil
- include FOUT
def initialize(options = {})
- @execute_aborted_at = nil
- @execute_completed_at = nil
- @execute_error = nil
- @execute_error_message = nil
- @execute_files = nil
- @execute_options = nil
- @execute_script_filespec = nil
- @execute_started_at = nil
@option_parser = nil
- @options = options
- @prompt = tty_prompt_without_disabled_symbol
- end
- # Adds Back and Exit options to the CLI menu
- #
- # @param blocks_in_file [Array] The current blocks in the menu
- def add_menu_chrome_blocks!(blocks_in_file)
- return unless @options[:menu_link_format].present?
-
- if @options[:menu_with_back] && history_state_exist?
- append_chrome_block(blocks_in_file, MenuState::BACK)
- end
- if @options[:menu_with_exit]
- append_chrome_block(blocks_in_file, MenuState::EXIT)
- end
- append_divider(blocks_in_file, @options, :initial)
- append_divider(blocks_in_file, @options, :final)
+ @options = HashDelegator.new(options)
+ @fout = FOut.new(@delegate_object)
end
- ##
- # Appends a summary of a block (FCB) to the blocks array.
- #
- def append_block_summary(blocks, fcb, opts)
- ## enhance fcb with block summary
- #
- blocks.push get_block_summary(opts, fcb)
- end
+ private
- # Appends a chrome block, which is a menu option for Back or Exit
- #
- # @param blocks_in_file [Array] The current blocks in the menu
- # @param type [Symbol] The type of chrome block to add (:back or :exit)
- def append_chrome_block(blocks_in_file, type)
- case type
- when MenuState::BACK
- state = history_state_partition(@options)
- @hs_curr = state[:unit]
- @hs_rest = state[:rest]
- option_name = @options[:menu_option_back_name]
- insert_at_top = @options[:menu_back_at_top]
- when MenuState::EXIT
- option_name = @options[:menu_option_exit_name]
- insert_at_top = @options[:menu_exit_at_top]
- end
-
- formatted_name = format(@options[:menu_link_format],
- safeval(option_name))
- chrome_block = FCB.new(
- chrome: true,
- dname: formatted_name.send(@options[:menu_link_color].to_sym),
- oname: formatted_name
+ def error_handler(name = '', opts = {})
+ Exceptions.error_handler(
+ "CachedNestedFileReader.#{name} -- #{$!}",
+ opts
)
-
- if insert_at_top
- blocks_in_file.unshift(chrome_block)
- else
- blocks_in_file.push(chrome_block)
- end
end
- # Appends a divider to the blocks array.
- # @param blocks [Array] The array of block elements.
- # @param opts [Hash] Options containing divider configuration.
- # @param position [Symbol] :initial or :final divider position.
- def append_divider(blocks, opts, position)
- divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider
- unless opts[:menu_divider_format].present? && opts[divider_key].present?
- return
- end
-
- oname = format(opts[:menu_divider_format],
- safeval(opts[divider_key]))
- divider = FCB.new(
- chrome: true,
- disabled: '',
- dname: oname.send(opts[:menu_divider_color].to_sym),
- oname: oname
+ def warn_format(name, message, opts = {})
+ Exceptions.warn_format(
+ "CachedNestedFileReader.#{name} -- #{message}",
+ opts
)
-
- position == :initial ? blocks.unshift(divider) : blocks.push(divider)
end
- # 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 approve_and_execute_block(selected, opts, mdoc)
- if selected.fetch(:shell, '') == BlockType::LINK
- handle_shell_link(opts, selected.fetch(:body, ''), mdoc)
- elsif opts.fetch(:s_back, false)
- handle_back_link(opts)
- elsif selected[:shell] == BlockType::OPTS
- handle_shell_opts(opts, selected)
- else
- handle_remainder_blocks(mdoc, opts, selected)
- end
- end
-
# return arguments before `--`
#
def arguments_for_mde(argv = ARGV)
case ind = argv.find_index('--')
when nil
@@ -353,359 +161,122 @@
[item[:opt_name],
item[:proccode] ? item[:proccode].call(value) : value]
end.compact.to_h
end
- # Finds the first hash-like element within an enumerable collection where the specified key
- # matches the given value. Returns a default value if no match is found.
- #
- # @param blocks [Enumerable] An enumerable collection of hash-like objects.
- # @param key [Object] The key to look up in each hash-like object.
- # @param value [Object] The value to compare against the value associated with the key.
- # @param default [Object] The default value to return if no match is found (optional).
- # @return [Object, nil] The found hash-like object, 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 blocks_per_opts(blocks, opts)
- return blocks if opts[:struct]
-
- blocks.map do |block|
- block.fetch(:text, nil) || block.oname
- end.compact.reject(&:empty?)
- end
-
def calculated_options
{
bash: true, # bash block parsing in get_block_summary()
- saved_script_filename: nil, # calculated
- struct: true # allow get_block_summary()
+ saved_script_filename: nil # calculated
}
end
- # Check whether the document exists and is readable
- def check_file_existence(filename)
- unless filename&.present?
- fout 'No blocks found.'
- return false
- end
-
- unless File.exist? filename
- fout 'Document is missing.'
- return false
- end
- true
- end
-
def clear_required_file
ENV['MDE_LINK_REQUIRED_FILE'] = ''
end
- # Collect required code blocks based on the provided options.
- #
- # @param opts [Hash] Options hash containing configuration settings.
- # @param mdoc [YourMDocClass] An instance of the MDoc class.
- # @return [Array<String>] Required code blocks as an array of lines.
- def collect_required_code_lines(mdoc, selected, opts: {})
- # Apply hash in opts block to environment variables
- if selected[:shell] == BlockType::VARS
- data = YAML.load(selected[:body].join("\n"))
- data.each_key do |key|
- ENV[key] = value = data[key].to_s
- next unless opts[:menu_vars_set_format].present?
+ # # Deletes a required temporary file specified by an environment variable.
+ # # The function checks if the file exists before attempting to delete it.
+ # # Clears the environment variable after deletion.
+ # #
+ # def delete_required_temp_file
+ # temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
- print format(
- opts[:menu_vars_set_format],
- { key: key,
- value: value }
- ).send(opts[:menu_vars_set_color].to_sym)
- end
- end
+ # return if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
- required = mdoc.collect_recursively_required_code(opts[:block_name],
- opts: opts)
- read_required_blocks_from_temp_file + required[:code]
- end
+ # FileUtils.rm_f(temp_blocks_file_path)
- def cfile
- @cfile ||= CachedNestedFileReader.new(
- import_pattern: @options.fetch(:import_pattern)
- )
- end
+ # clear_required_file
+ # rescue StandardError
+ # error_handler('delete_required_temp_file')
+ # end
- EF_STDOUT = :stdout
- EF_STDERR = :stderr
- EF_STDIN = :stdin
+ public
- # Existing command_execute method
- def command_execute(opts, command, args: [])
- @execute_files = Hash.new([])
- @execute_options = opts
- @execute_started_at = Time.now.utc
-
- Open3.popen3(opts[:shell], '-c', command, opts[:filename],
- *args) do |stdin, stdout, stderr, exec_thr|
- handle_stream(opts, stdout, EF_STDOUT) do |line|
- yield nil, line, nil, exec_thr if block_given?
- end
- handle_stream(opts, stderr, EF_STDERR) do |line|
- yield nil, nil, line, exec_thr if block_given?
- end
-
- in_thr = handle_stream(opts, $stdin, EF_STDIN) do |line|
- stdin.puts(line)
- yield line, nil, nil, exec_thr if block_given?
- end
-
- exec_thr.join
- sleep 0.1
- in_thr.kill if in_thr&.alive?
- end
-
- @execute_completed_at = Time.now.utc
- rescue Errno::ENOENT => err
- #d 'command error ENOENT triggered by missing command in script'
- @execute_aborted_at = Time.now.utc
- @execute_error_message = err.message
- @execute_error = err
- @execute_files[EF_STDERR] += [@execute_error_message]
- fout "Error ENOENT: #{err.inspect}"
- rescue SignalException => err
- #d 'command SIGTERM triggered by user or system'
- @execute_aborted_at = Time.now.utc
- @execute_error_message = 'SIGTERM'
- @execute_error = err
- @execute_files[EF_STDERR] += [@execute_error_message]
- fout "Error ENOENT: #{err.inspect}"
- end
-
- def command_or_user_selected_block(blocks_in_file, blocks_menu, default,
- opts)
- if opts[:block_name].present?
- block = blocks_in_file.find do |item|
- item[:oname] == opts[:block_name]
- end
- else
- block, state = wait_for_user_selected_block(blocks_in_file, blocks_menu, default,
- opts)
- end
-
- [block, state]
- end
-
- def copy_to_clipboard(required_lines)
- text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR)
- Clipboard.copy(text)
- fout "Clipboard updated: #{required_lines.count} blocks," \
- " #{required_lines.flatten.count} lines," \
- " #{text.length} characters"
- end
-
- def count_blocks_in_filename
- fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex]
- cnt = 0
- cfile.readlines(@options[:filename]).each do |line|
- cnt += 1 if line.match(fenced_start_and_end_regex)
- end
- cnt / 2
- end
-
- ##
- # 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 fcb [FCB] The file control block containing the line to match against.
- # @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.
- def create_and_add_chrome_block(blocks, _fcb, match_data, format_option,
- color_method)
- oname = format(format_option,
- match_data.named_captures.transform_keys(&:to_sym))
- blocks.push FCB.new(
- chrome: true,
- disabled: '',
- dname: oname.send(color_method),
- oname: oname
- )
- end
-
- ##
- # 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.
- def create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
- return unless use_chrome
-
- match_criteria = [
- { match: :menu_task_match, format: :menu_task_format,
- color: :menu_task_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_criteria.each do |criteria|
- unless opts[criteria[:match]].present? &&
- (mbody = fcb.body[0].match opts[criteria[:match]])
- next
- end
-
- create_and_add_chrome_block(blocks, fcb, mbody, opts[criteria[:format]],
- opts[criteria[:color]].to_sym)
- break
- end
- end
-
- def create_and_write_file_with_permissions(file_path, content,
- chmod_value)
- dirname = File.dirname(file_path)
- FileUtils.mkdir_p dirname
- File.write(file_path, content)
- return if chmod_value.zero?
-
- File.chmod chmod_value, file_path
- end
-
- # Deletes a required temporary file specified by an environment variable.
- # The function checks if the file exists before attempting to delete it.
- # Clears the environment variable after deletion.
- #
- def delete_required_temp_file
- temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
-
- if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
- return
- end
-
- FileUtils.rm_f(temp_blocks_file_path)
-
- clear_required_file
- end
-
- # Derives a title from the body of an FCB object.
- # @param fcb [Object] The FCB object whose title is to be derived.
- # @return [String] The derived title.
- def derive_title_from_body(fcb)
- body_content = fcb&.body
- return '' unless body_content
-
- if body_content.count == 1
- body_content.first
- else
- format_multiline_body_as_title(body_content)
- end
- end
-
## Determines the correct filename to use for searching files
#
def determine_filename(specified_filename: nil, specified_folder: nil, default_filename: nil,
default_folder: nil, filetree: nil)
if specified_filename&.present?
return specified_filename if specified_filename.start_with?('/')
- File.join(specified_folder || default_folder,
- specified_filename)
+ File.join(specified_folder || default_folder, specified_filename)
elsif specified_folder&.present?
File.join(specified_folder,
filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
else
File.join(default_folder, default_filename)
end
end
- # :reek:DuplicateMethodCall
- def display_required_code(opts, required_lines)
- frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
- fout frame
- required_lines.each { |cb| fout cb }
- fout frame
- end
+ private
- def execute_approved_block(opts, required_lines)
- write_command_file(opts, required_lines)
- command_execute(
- opts,
- required_lines.flatten.join("\n"),
- args: opts.fetch(:s_pass_args, [])
- )
- initialize_and_save_execution_output
- output_execution_summary
- output_execution_result
- end
+ # def error_handler(name = '', event = nil, backtrace = nil)
+ # warn(error = "\n * ERROR * #{name}; #{$!.inspect}")
+ # warn($@.take(4).map.with_index { |line, ind| " * #{ind}: #{line}" })
+ # binding.pry if $tap_enable
+ # raise ArgumentError, error
+ # end
# Reports and executes block logic
def execute_block_logic(files)
@options[:filename] = select_document_if_multiple(files)
- select_approve_and_execute_block({ bash: true,
- struct: true })
+ @options.select_approve_and_execute_block
end
## Executes the block specified in the options
#
def execute_block_with_error_handling(rest)
finalize_cli_argument_processing(rest)
@options[:s_cli_rest] = rest
execute_code_block_based_on_options(@options)
- rescue FileMissingError => err
- puts "File missing: #{err}"
- rescue StandardError => err
- warn(error = "ERROR ** MarkParse.execute_block_with_error_handling(); #{err.inspect}")
- binding.pry if $tap_enable
- raise ArgumentError, error
+ rescue FileMissingError
+ warn "File missing: #{$!}"
+ rescue StandardError
+ error_handler('execute_block_with_error_handling')
end
# Main method to execute a block based on options and block_name
def execute_code_block_based_on_options(options)
options = calculated_options.merge(options)
update_options(options, over: false)
simple_commands = {
- doc_glob: -> { fout options[:md_filename_glob] },
- list_blocks: lambda do
- fout_list (files.map do |file|
- menu_with_block_labels(filename: file,
- struct: true)
- end).flatten(1)
- end,
- list_default_yaml: -> { fout_list list_default_yaml },
- list_docs: -> { fout_list files },
- list_default_env: -> { fout_list list_default_env },
+ doc_glob: -> { @fout.fout options[:md_filename_glob] },
+ list_default_yaml: -> { @fout.fout_list list_default_yaml },
+ list_docs: -> { @fout.fout_list files },
+ list_default_env: -> { @fout.fout_list list_default_env },
list_recent_output: lambda {
- fout_list list_recent_output(
+ @fout.fout_list list_recent_output(
@options[:saved_stdout_folder],
@options[:saved_stdout_glob], @options[:list_count]
)
},
list_recent_scripts: lambda {
- fout_list list_recent_scripts(
+ @fout.fout_list list_recent_scripts(
options[:saved_script_folder],
options[:saved_script_glob], options[:list_count]
)
},
- pwd: -> { fout File.expand_path('..', __dir__) },
+ pwd: -> { @fout.fout File.expand_path('..', __dir__) },
run_last_script: -> { run_last_script },
select_recent_output: -> { select_recent_output },
select_recent_script: -> { select_recent_script },
- tab_completions: -> { fout tab_completions },
- menu_export: -> { fout menu_export }
+ tab_completions: -> { @fout.fout tab_completions },
+ menu_export: -> { @fout.fout menu_export }
}
return if execute_simple_commands(simple_commands)
- files = prepare_file_list(options)
+ files = opts_prepare_file_list(options)
execute_block_logic(files)
return unless @options[:output_saved_script_filename]
- fout "saved_filespec: #{@execute_script_filespec}"
- rescue StandardError => err
- warn(error = "ERROR ** MarkParse.execute_code_block_based_on_options(); #{err.inspect}")
- binding.pry if $tap_enable
- raise ArgumentError, error
+ @fout.fout "script_block_name: #{@options.run_state.script_block_name}"
+ @fout.fout "s_save_filespec: #{@options.run_state.saved_filespec}"
+ rescue StandardError
+ error_handler('execute_code_block_based_on_options')
end
# Executes command based on the provided option keys
def execute_simple_commands(simple_commands)
simple_commands.each_key do |key|
@@ -734,191 +305,26 @@
## position 1: block name (optional)
#
block_name = rest.shift
@options[:block_name] = block_name if block_name.present?
+ rescue FileMissingError
+ warn_format('finalize_cli_argument_processing',
+ "File missing -- #{$!}", { abort: true })
+ # @options[:block_name] = ''
+ # @options[:filename] = ''
+ # exit 1
+ rescue StandardError
+ error_handler('finalize_cli_argument_processing')
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
-
- ## summarize blocks
- #
- def get_block_summary(call_options, fcb)
- opts = optsmerge call_options
- # return fcb.body unless opts[:struct]
- return fcb unless opts[:bash]
-
- fcb.call = fcb.title.match(Regexp.new(opts[:block_calls_scan]))&.fetch(1, nil)
- titlexcall = if fcb.call
- fcb.title.sub("%#{fcb.call}", '')
- else
- fcb.title
- end
- bm = extract_named_captures_from_option(titlexcall,
- opts[:block_name_match])
- fcb.stdin = extract_named_captures_from_option(titlexcall,
- opts[:block_stdin_scan])
- fcb.stdout = extract_named_captures_from_option(titlexcall,
- opts[:block_stdout_scan])
-
- shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
- fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
- fcb.dname = if shell_color_option && opts[shell_color_option].present?
- fcb.oname.send(opts[shell_color_option].to_sym)
- else
- fcb.oname
- end
- fcb
- end
-
- # Handles the link-back operation.
- #
- # @param opts [Hash] Configuration options hash.
- # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
- def handle_back_link(opts)
- history_state_pop(opts)
- [LoadFile::Load, '']
- end
-
- # Handles the execution and display of remainder blocks from a selected menu item.
- #
- # @param mdoc [Object] Document object containing code blocks.
- # @param opts [Hash] Configuration options hash.
- # @param selected [Hash] Selected item from the menu.
- # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
- # @note The function can prompt the user for approval before executing code if opts[:user_must_approve] is true.
- def handle_remainder_blocks(mdoc, opts, selected)
- required_lines = collect_required_code_lines(mdoc, selected,
- opts: opts)
- if opts[:output_script] || opts[:user_must_approve]
- display_required_code(opts, required_lines)
- end
- allow = if opts[:user_must_approve]
- prompt_for_user_approval(opts,
- required_lines)
- else
- true
- end
- opts[:s_ir_approve] = allow
- if opts[:s_ir_approve]
- execute_approved_block(opts,
- required_lines)
- end
-
- [LoadFile::Reuse, '']
- end
-
- # Handles the link-shell operation.
- #
- # @param opts [Hash] Configuration options hash.
- # @param body [Array<String>] The body content.
- # @param mdoc [Object] Document object containing code blocks.
- # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and a block name.
- def handle_shell_link(opts, body, mdoc)
- data = body.present? ? YAML.load(body.join("\n")) : {}
- data_file = data.fetch('file', nil)
- return [LoadFile::Reuse, ''] unless data_file
-
- history_state_push(mdoc, data_file, opts)
-
- data.fetch('vars', []).each do |var|
- ENV[var[0]] = var[1].to_s
- end
-
- [LoadFile::Load, data.fetch('block', '')]
- end
-
- # Handles options for the shell.
- #
- # @param opts [Hash] Configuration options hash.
- # @param selected [Hash] Selected item from the menu.
- # @return [Array<Symbol, String>] A tuple containing a LoadFile::Reuse flag and an empty string.
- def handle_shell_opts(opts, selected, tgt2 = nil)
- data = YAML.load(selected[:body].join("\n"))
- data.each_key do |key|
- opts[key.to_sym] = value = data[key]
- tgt2[key.to_sym] = value if tgt2
- next unless opts[:menu_opts_set_format].present?
-
- print format(
- opts[:menu_opts_set_format],
- { key: key,
- value: value }
- ).send(opts[:menu_opts_set_color].to_sym)
- end
- [LoadFile::Reuse, '']
- end
-
- # Handles reading and processing lines from a given IO stream
- #
- # @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin).
- # @param file_type [Symbol] The type of file to which the stream corresponds.
- def handle_stream(opts, stream, file_type, swap: false)
- Thread.new do
- until (line = stream.gets).nil?
- @execute_files[file_type] =
- @execute_files[file_type] + [line.strip]
- print line if opts[:output_stdout]
- yield line if block_given?
- end
- rescue IOError
- #d 'stdout IOError, thread killed, do nothing'
- end
- end
-
- def history_state_exist?
- history = ENV.fetch(MDE_HISTORY_ENV_NAME, '')
- history.present? ? history : nil
- end
-
- def history_state_partition(opts)
- unit, rest = StringUtil.partition_at_first(
- ENV.fetch(MDE_HISTORY_ENV_NAME, ''),
- opts[:history_document_separator]
- )
- { unit: unit, rest: rest }.tap_inspect
- end
-
- def history_state_pop(opts)
- state = history_state_partition(opts)
- opts[:filename] = state[:unit]
- ENV[MDE_HISTORY_ENV_NAME] = state[:rest]
- delete_required_temp_file
- end
-
- def history_state_push(mdoc, data_file, opts)
- [data_file, opts[:block_name]].tap_inspect 'filename, blockname'
- new_history = opts[:filename] +
- opts[:history_document_separator] +
- ENV.fetch(MDE_HISTORY_ENV_NAME, '')
- opts[:filename] = data_file
- write_required_blocks_to_temp_file(mdoc, opts[:block_name], opts)
- ENV[MDE_HISTORY_ENV_NAME] = new_history
- 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 if !indent.present?
-
- body.lines.map { |line| indent + line.chomp }.join("\n")
- end
-
## Sets up the options and returns the parsed arguments
#
def initialize_and_parse_cli_options
- @options = base_options
+ # @options = base_options
+ @options = HashDelegator.new(base_options)
+
read_configuration_file!(@options,
".#{MarkdownExec::APP_NAME.downcase}.yml")
@option_parser = OptionParser.new do |opts|
executable_name = File.basename($PROGRAM_NAME)
@@ -927,67 +333,23 @@
" - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
"Usage: #{executable_name} [(path | filename [block_name])] [options]"
].join("\n")
menu_iter do |item|
- menu_option_append opts, @options, item
+ opts_menu_option_append opts, @options, item
end
end
@option_parser.load
@option_parser.environment
rest = @option_parser.parse!(arguments_for_mde)
@options[:s_pass_args] = ARGV[rest.count + 1..]
+ @options.merge(@options.run_state.to_h)
rest
end
- def initialize_and_save_execution_output
- return unless @options[:save_execution_output]
-
- @options[:logged_stdout_filename] =
- SavedAsset.stdout_name(blockname: @options[:block_name],
- filename: File.basename(@options[:filename],
- '.*'),
- prefix: @options[:logged_stdout_filename_prefix],
- time: Time.now.utc)
-
- @logged_stdout_filespec =
- @options[:logged_stdout_filespec] =
- File.join @options[:saved_stdout_folder],
- @options[:logged_stdout_filename]
- @logged_stdout_filespec = @options[:logged_stdout_filespec]
- write_execution_output_to_file
- end
-
- # Initializes variables for regex and other states
- def initialize_state(opts)
- {
- fenced_start_and_end_regex: Regexp.new(opts[:fenced_start_and_end_regex]),
- fenced_start_extended_regex: Regexp.new(opts[:fenced_start_extended_regex]),
- fcb: FCB.new,
- in_fenced_block: false,
- headings: []
- }
- end
-
- # Main function to iterate through blocks in file
- def iter_blocks_in_file(opts = {}, &block)
- return unless check_file_existence(opts[:filename])
-
- state = initialize_state(opts)
-
- selected_messages = yield :filter
-
- cfile.readlines(opts[:filename]).each do |line|
- next unless line
-
- update_line_and_block_state(line, state, opts, selected_messages,
- &block)
- end
- end
-
##
# Returns a lambda expression based on the given procname.
# @param procname [String] The name of the process to generate a lambda for.
# @param options [Hash] The options hash, necessary for some lambdas to access.
# @return [Lambda] The corresponding lambda expression.
@@ -999,19 +361,19 @@
}
when 'exit'
->(_) { exit }
when 'help'
lambda { |_|
- fout menu_help
+ @fout.fout menu_help
exit
}
when 'path'
->(value) { read_configuration_file!(options, value) }
when 'show_config'
lambda { |_|
finalize_cli_argument_processing(options)
- fout options.sort_by_key.to_yaml
+ @fout.fout options.sort_by_key.to_yaml
}
when 'val_as_bool'
lambda { |value|
value.instance_of?(::String) ? (value.chomp != '0') : value
}
@@ -1019,11 +381,11 @@
->(value) { value.to_i }
when 'val_as_str'
->(value) { value.to_s }
when 'version'
lambda { |_|
- fout MarkdownExec::VERSION
+ @fout.fout MarkdownExec::VERSION
exit
}
else
procname
end
@@ -1049,20 +411,11 @@
item[:description].present? ? item[:description] : nil
].compact.join(' # ')
end.compact.sort
end
- def list_files_per_options(options)
- list_files_specified(
- determine_filename(
- specified_filename: options[:filename]&.present? ? options[:filename] : nil,
- specified_folder: options[:path],
- default_filename: 'README.md',
- default_folder: '.'
- )
- )
- end
+ public
## Searches for files based on the specified or default filenames and folders
#
def list_files_specified(fn, filetree = nil)
return Dir.glob(fn) unless filetree
@@ -1075,85 +428,12 @@
def list_markdown_files_in_path
Dir.glob(File.join(@options[:path],
@options[:md_filename_glob]))
end
- ## output type (body string or full object) per option struct and bash
- #
- def list_named_blocks_in_file(call_options = {}, &options_block)
- opts = optsmerge call_options, options_block
- blocks_per_opts(
- menu_from_file(opts.merge(struct: true)).select do |fcb|
- Filter.fcb_select?(opts.merge(no_chrome: true), fcb)
- end, opts
- )
- end
+ private
- # return true if values were modified
- # execute block once per filename
- #
- def load_auto_blocks(opts, blocks_in_file)
- return unless opts[:document_load_opts_block_name].present?
- return if opts[:s_most_recent_filename] == opts[:filename]
-
- block = block_find(blocks_in_file, :oname,
- opts[:document_load_opts_block_name])
- return unless block
-
- handle_shell_opts(opts, block, @options)
- opts[:s_most_recent_filename] = opts[:filename]
- true
- end
-
- def mdoc_and_menu_from_file(opts)
- menu_blocks = menu_from_file(opts.merge(struct: true))
- mdoc = MDoc.new(menu_blocks) do |nopts|
- opts.merge!(nopts)
- end
- [menu_blocks, mdoc]
- end
-
- ## Handles the file loading and returns the blocks in the file and MDoc instance
- #
- def mdoc_menu_and_selected_from_file(opts)
- blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
- if load_auto_blocks(opts, blocks_in_file)
- # recreate menu with new options
- #
- blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
- end
-
- blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
- add_menu_chrome_blocks!(blocks_menu)
- [blocks_in_file, blocks_menu, mdoc]
- end
-
- def menu_chrome_colored_option(opts,
- option_symbol = :menu_option_back_name)
- if opts[:menu_chrome_color]
- menu_chrome_formatted_option(opts,
- option_symbol).send(opts[:menu_chrome_color].to_sym)
- else
- menu_chrome_formatted_option(opts, option_symbol)
- end
- end
-
- def menu_chrome_formatted_option(opts,
- option_symbol = :menu_option_back_name)
- val1 = safeval(opts.fetch(option_symbol, ''))
- val1 unless opts[:menu_chrome_format]
-
- format(opts[:menu_chrome_format], val1)
- end
-
- def menu_export(data = menu_for_optparse)
- data.map do |item|
- item.delete(:procname)
- item
- end.to_yaml
- end
-
##
# Generates a menu suitable for OptionParser from the menu items defined in YAML format.
# @return [Array<Hash>] The array of option hashes for OptionParser.
def menu_for_optparse
menu_from_yaml.map do |menu_item|
@@ -1162,54 +442,39 @@
proccode: lambda_for_procname(menu_item[:procname], options)
)
end
end
- ##
- # Returns a list of blocks in a given file, including dividers, tasks, and other types of blocks.
- # The list can be customized via call_options and options_block.
- #
- # @param call_options [Hash] Options passed as an argument.
- # @param options_block [Proc] Block for dynamic option manipulation.
- # @return [Array<FCB>] An array of FCB objects representing the blocks.
- #
- def menu_from_file(call_options = {},
- &options_block)
- opts = optsmerge(call_options, options_block)
- use_chrome = !opts[:no_chrome]
-
- blocks = []
- iter_blocks_in_file(opts) do |btype, fcb|
- case btype
- when :blocks
- append_block_summary(blocks, fcb, opts)
- when :filter # what btypes are responded to?
- %i[blocks line]
- when :line
- create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
- end
- end
- blocks
- rescue StandardError => err
- warn(error = "ERROR ** MarkParse.menu_from_file(); #{err.inspect}")
- warn(caller[0..4])
- raise StandardError, error
- end
-
def menu_help
@option_parser.help
end
def menu_iter(data = menu_for_optparse, &block)
data.map(&block)
end
- def menu_option_append(opts, options, item)
- unless item[:long_name].present? || item[:short_name].present?
- return
- end
+ def opts_list_files(options)
+ list_files_specified(
+ determine_filename(
+ specified_filename: options[:filename]&.present? ? options[:filename] : nil,
+ specified_folder: options[:path],
+ default_filename: 'README.md',
+ default_folder: '.'
+ )
+ )
+ end
+ def menu_export(data = menu_for_optparse)
+ data.map do |item|
+ item.delete(:procname)
+ item
+ end.to_yaml
+ end
+
+ def opts_menu_option_append(opts, options, item)
+ return unless item[:long_name].present? || item[:short_name].present?
+
opts.on(*[
# - long name
if item[:long_name].present?
"--#{item[:long_name]}#{item[:arg_name].present? ? " #{item[:arg_name]}" : ''}"
end,
@@ -1232,385 +497,82 @@
end
}
].compact)
end
- def menu_with_block_labels(call_options = {})
- opts = options.merge(call_options)
- menu_from_file(opts).map do |fcb|
- BlockLabel.make(
- filename: opts[:filename],
- headings: fcb.fetch(:headings, []),
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
- title: fcb[:title],
- text: fcb[:text],
- body: fcb[:body]
- )
- end.compact
- end
-
- def next_block_name_from_command_line_arguments(opts)
- if opts[:s_cli_rest].present?
- opts[:block_name] = opts[:s_cli_rest].pop
- false # repeat_menu
- else
- true # repeat_menu
- end
- end
-
- # :reek:ControlParameter
- def optsmerge(call_options = {}, options_block = nil)
- class_call_options = @options.merge(call_options || {})
- if options_block
- options_block.call class_call_options
- else
- class_call_options
- end
- end
-
- def output_execution_result
- oq = [['Block', @options[:block_name], DISPLAY_LEVEL_ADMIN],
- ['Command',
- [MarkdownExec::BIN_NAME,
- @options[:filename],
- @options[:block_name]].join(' '),
- DISPLAY_LEVEL_ADMIN]]
-
- [['Script', :saved_filespec],
- ['StdOut', :logged_stdout_filespec]].each do |label, name|
- if @options[name]
- oq << [label, @options[name],
- DISPLAY_LEVEL_ADMIN]
- end
- end
-
- oq.map do |label, value, level|
- lout ["#{label}:".yellow, value.to_s].join(' '), level: level
- end
- end
-
- def output_execution_summary
- return unless @options[:output_execution_summary]
-
- fout_section 'summary', {
- execute_aborted_at: @execute_aborted_at,
- execute_completed_at: @execute_completed_at,
- execute_error: @execute_error,
- execute_error_message: @execute_error_message,
- execute_files: @execute_files,
- execute_options: @execute_options,
- execute_started_at: @execute_started_at,
- execute_script_filespec: @execute_script_filespec
- }
- end
-
- # Prepare the blocks menu by adding labels and other necessary details.
- #
- # @param blocks_in_file [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(blocks_in_file, opts)
- # next if fcb.fetch(:disabled, false)
- # next unless fcb.fetch(:name, '').present?
- replace_consecutive_blanks(blocks_in_file).map do |fcb|
- next if Filter.prepared_not_in_menu?(opts, fcb)
-
- fcb.merge!(
- name: indent_all_lines(fcb.dname, fcb.fetch(:indent, nil)),
- label: BlockLabel.make(
- body: fcb[:body],
- filename: opts[:filename],
- headings: fcb.fetch(:headings, []),
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
- text: fcb[:text],
- title: fcb[:title]
- )
- )
- fcb.to_h
- end.compact
- end
-
# Prepares and fetches file listings
- def prepare_file_list(options)
- list_files_per_options(options)
+ def opts_prepare_file_list(options)
+ opts_list_files(options)
end
- def process_fenced_block(fcb, opts, selected_messages, &block)
- fcb.oname = fcb.dname = fcb.title || ''
- return unless fcb.body
-
- update_title_from_body(fcb)
-
- if block &&
- selected_messages.include?(:blocks) &&
- Filter.fcb_select?(opts, fcb)
- block.call :blocks, fcb
- end
- end
-
- def process_line(line, _opts, selected_messages, &block)
- return unless block && selected_messages.include?(:line)
-
- # dp 'text outside of fcb'
- fcb = FCB.new
- fcb.body = [line]
- block.call(:line, fcb)
- end
-
- class MenuOptions
- YES = 1
- NO = 2
- SCRIPT_TO_CLIPBOARD = 3
- SAVE_SCRIPT = 4
- 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.
- #
- # @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.
- #
- # @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.
- ##
- def prompt_for_user_approval(opts, required_lines)
- # Present a selection menu for user approval.
-
- sel = @prompt.select(opts[:prompt_approve_block],
- filter: true) do |menu|
- menu.default MenuOptions::YES
- menu.choice opts[:prompt_yes], MenuOptions::YES
- menu.choice opts[:prompt_no], MenuOptions::NO
- menu.choice opts[:prompt_script_to_clipboard],
- MenuOptions::SCRIPT_TO_CLIPBOARD
- menu.choice opts[:prompt_save_script], MenuOptions::SAVE_SCRIPT
- end
-
- if sel == MenuOptions::SCRIPT_TO_CLIPBOARD
- copy_to_clipboard(required_lines)
- elsif sel == MenuOptions::SAVE_SCRIPT
- save_to_file(opts, required_lines)
- end
-
- sel == MenuOptions::YES
- rescue TTY::Reader::InputInterrupt
- exit 1
- end
-
- def prompt_select_continue(opts)
- sel = @prompt.select(
- opts[:prompt_after_bash_exec],
- filter: true,
- quiet: true
- ) do |menu|
- menu.choice opts[:prompt_yes]
- menu.choice opts[:prompt_exit]
- end
- sel == opts[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
- rescue TTY::Reader::InputInterrupt
- exit 1
- end
-
# :reek:UtilityFunction ### temp
def read_configuration_file!(options, configuration_path)
return unless File.exist?(configuration_path)
options.merge!((YAML.load(File.open(configuration_path)) || {})
.transform_keys(&:to_sym))
end
- # Reads required code blocks from a temporary file specified by an environment variable.
- #
- # @return [Array<String>] An array containing the lines read from the temporary file.
- # @note Relies on the 'MDE_LINK_REQUIRED_FILE' environment variable to locate the file.
- def read_required_blocks_from_temp_file
- temp_blocks = []
+ public
- temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
- if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
- return temp_blocks
- end
-
- if File.exist?(temp_blocks_file_path)
- temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
- end
-
- temp_blocks
- end
-
- # Replace duplicate blanks (where :oname is not present) with a single blank line.
- #
- # @param [Array<Hash>] lines Array of hashes to process.
- # @return [Array<Hash>] Cleaned array with consecutive blanks collapsed into one.
- def replace_consecutive_blanks(lines)
- lines.chunk_while do |i, j|
- i[:oname].to_s.empty? && j[:oname].to_s.empty?
- end.map do |chunk|
- if chunk.any? do |line|
- line[:oname].to_s.strip.empty?
- end
- chunk.first
- else
- chunk
- end
- end.flatten
- end
-
def run
clear_required_file
execute_block_with_error_handling(initialize_and_parse_cli_options)
- delete_required_temp_file
- rescue StandardError => err
- warn(error = "ERROR ** MarkParse.run(); #{err.inspect}")
- binding.pry if $tap_enable
- raise ArgumentError, error
+ @options.delete_required_temp_file
+ rescue StandardError
+ error_handler('run')
end
+ private
+
def run_last_script
filename = SavedFilesMatcher.most_recent(@options[:saved_script_folder],
@options[:saved_script_glob])
return unless filename
saved_name_split filename
@options[:save_executed_script] = false
- select_approve_and_execute_block
- end
-
- def safeval(str)
- eval(str)
+ @options.select_approve_and_execute_block
rescue StandardError
- warn $!
- binding.pry if $tap_enable
- raise StandardError, $!
+ error_handler('run_last_script')
end
- def save_to_file(opts, required_lines)
- write_command_file(opts.merge(save_executed_script: true),
- required_lines)
- fout "File saved: #{@options[:saved_filespec]}"
- end
-
def saved_name_split(name)
# rubocop:disable Layout/LineLength
- mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match name
+ mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match(name)
# rubocop:enable Layout/LineLength
return unless mf
@options[:block_name] = mf[:block]
- @options[:filename] = mf[:file].gsub(FNR12, FNR11)
+ @options[:filename] = mf[:file].gsub(@options[:saved_filename_pattern],
+ @options[:saved_filename_replacement])
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.
- #
- # @param call_options [Hash] Initial options for the method.
- # @param options_block [Block] Block of options to be merged with call_options.
- # @return [Nil] Returns nil if no code block is selected or an error occurs.
- def select_approve_and_execute_block(call_options = {},
- &options_block)
- base_opts = optsmerge(call_options, options_block)
- repeat_menu = true && !base_opts[:block_name].present?
- load_file = LoadFile::Reuse
- default = nil
- block = nil
-
- loop do
- loop do
- opts = base_opts.dup
- opts[:s_back] = false
- blocks_in_file, blocks_menu, mdoc = mdoc_menu_and_selected_from_file(opts)
- block, state = command_or_user_selected_block(blocks_in_file, blocks_menu,
- default, opts)
- return if state == MenuState::EXIT
-
- load_file, next_block_name = approve_and_execute_block(block, opts,
- mdoc)
- default = load_file == LoadFile::Load ? nil : opts[:block_name]
- base_opts[:block_name] = opts[:block_name] = next_block_name
- base_opts[:filename] = opts[:filename]
-
- # user prompt to exit if the menu will be displayed again
- #
- if repeat_menu &&
- block[:shell] == BlockType::BASH &&
- opts[:pause_after_bash_exec] &&
- prompt_select_continue(opts) == MenuState::EXIT
- return
- end
-
- # exit current document/menu if loading next document or single block_name was specified
- #
- if state == MenuState::CONTINUE && load_file == LoadFile::Load
- break
- end
- break unless repeat_menu
- end
- break if load_file == LoadFile::Reuse
-
- repeat_menu = next_block_name_from_command_line_arguments(base_opts)
- end
- rescue StandardError => err
- warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
- warn err.backtrace
- binding.pry if $tap_enable
- raise ArgumentError, error
- end
-
def select_document_if_multiple(files = list_markdown_files_in_path)
return files[0] if (count = files.count) == 1
return unless count >= 2
opts = options.dup
- select_option_or_exit opts[:prompt_select_md].to_s, files,
- opts.merge(per_page: opts[:select_page_height])
+ select_option_or_exit(HashDelegator.new(@options).string_send_color(opts[:prompt_select_md].to_s, :prompt_color_after_script_execution),
+ files,
+ opts.merge(per_page: opts[:select_page_height]))
end
# Presents a TTY prompt to select an option or exit, returns selected option or nil
- def select_option_or_exit(prompt_text, items, opts = {})
- result = select_option_with_metadata(prompt_text, items, opts)
+ def select_option_or_exit(prompt_text, strings, opts = {})
+ result = @options.select_option_with_metadata(prompt_text, strings,
+ opts)
return unless result.fetch(:option, nil)
result[:selected]
end
- # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
- def select_option_with_metadata(prompt_text, items, opts = {})
- selection = @prompt.select(prompt_text,
- items,
- opts.merge(filter: true))
-
- items.find { |item| item[:dname] == selection }
- .merge(
- if selection == menu_chrome_colored_option(opts,
- :menu_option_back_name)
- { option: selection, curr: @hs_curr, rest: @hs_rest,
- shell: BlockType::LINK }
- elsif selection == menu_chrome_colored_option(opts,
- :menu_option_exit_name)
- { option: selection }
- else
- { selected: selection }
- end
- )
- rescue TTY::Reader::InputInterrupt
- exit 1
- end
-
def select_recent_output
filename = select_option_or_exit(
- @options[:prompt_select_output].to_s,
+ HashDelegator.new(@options).string_send_color(@options[:prompt_select_output].to_s,
+ :prompt_color_after_script_execution),
list_recent_output(
@options[:saved_stdout_folder],
@options[:saved_stdout_glob],
@options[:list_count]
),
@@ -1621,11 +583,12 @@
`open #{filename} #{options[:output_viewer_options]}`
end
def select_recent_script
filename = select_option_or_exit(
- @options[:prompt_select_md].to_s,
+ HashDelegator.new(@options).string_send_color(@options[:prompt_select_md].to_s,
+ :prompt_color_after_script_execution),
list_recent_scripts(
@options[:saved_script_folder],
@options[:saved_script_glob],
@options[:list_count]
),
@@ -1633,324 +596,40 @@
)
return if filename.nil?
saved_name_split(filename)
- select_approve_and_execute_block({ bash: true,
- save_executed_script: false,
- struct: true })
+ @options.select_approve_and_execute_block ### ({ save_executed_script: false })
end
- def start_fenced_block(opts, line, headings,
- fenced_start_extended_regex)
- fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
- rest = fcb_title_groups.fetch(:rest, '')
+ public
- fcb = FCB.new
- fcb.headings = headings
- fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
- fcb.indent = fcb_title_groups.fetch(:indent, '')
- fcb.shell = fcb_title_groups.fetch(:shell, '')
- fcb.title = fcb_title_groups.fetch(:name, '')
- fcb.body = []
- fcb.reqs, fcb.wraps =
- ArrayUtil.partition_by_predicate(rest.scan(/\+[^\s]+/).map do |req|
- req[1..-1]
- end) do |name|
- !name.match(Regexp.new(opts[:block_name_wrapper_match]))
- end
- fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
- fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
- tn.named_captures.sym_keys
- end
- fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
- tn.named_captures.sym_keys
- end
- fcb
- end
-
def tab_completions(data = menu_for_optparse)
data.map do |item|
"--#{item[:long_name]}" if item[:long_name]
end.compact
end
- 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 and existing headings.
- # The function uses regular expressions specified in the `opts` to identify different levels of headings.
- #
- # @param line [String] The line of text to examine for heading content.
- # @param headings [Array<String>] The existing list of document headings.
- # @param opts [Hash] A hash containing options for regular expression matches for different heading levels.
- #
- # @option opts [String] :heading1_match Regular expression for matching first-level headings.
- # @option opts [String] :heading2_match Regular expression for matching second-level headings.
- # @option opts [String] :heading3_match Regular expression for matching third-level headings.
- #
- # @return [Array<String>] Updated list of headings.
- def update_document_headings(line, headings, opts)
- if (lm = line.match(Regexp.new(opts[:heading3_match])))
- [headings[0], headings[1], lm[:name]]
- elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
- [headings[0], lm[:name]]
- elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
- [lm[: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.
- # @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_messages [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 opts [Boolean] :menu_blocks_with_headings Flag indicating whether to update headings while processing.
- #
- # @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
- ##
- def update_line_and_block_state(line, state, opts, selected_messages,
- &block)
- if opts[:menu_blocks_with_headings]
- state[:headings] =
- update_document_headings(line, state[:headings], opts)
- end
-
- if line.match(state[:fenced_start_and_end_regex])
- if state[:in_fenced_block]
- process_fenced_block(state[:fcb], opts, selected_messages,
- &block)
- state[:in_fenced_block] = false
- else
- state[:fcb] =
- start_fenced_block(opts, line, state[:headings],
- state[:fenced_start_extended_regex])
- 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}/, '')
- ]
-
- else
- process_line(line, opts, selected_messages, &block)
- end
- end
-
# :reek:BooleanParameter
# :reek:ControlParameter
def update_options(opts = {}, over: true)
if over
@options = @options.merge opts
else
@options.merge! opts
end
@options
end
-
- # Updates the title of an FCB object from its body content if the title is nil or empty.
- def update_title_from_body(fcb)
- return unless fcb.title.nil? || fcb.title.empty?
-
- fcb.title = derive_title_from_body(fcb)
- end
-
- def wait_for_user_selected_block(blocks_in_file, blocks_menu,
- default, opts)
- block, state = wait_for_user_selection(blocks_in_file, blocks_menu,
- default, opts)
- case state
- when MenuState::BACK
- opts[:block_name] = block[:dname]
- opts[:s_back] = true
- when MenuState::CONTINUE
- opts[:block_name] = block[:dname]
- end
-
- [block, state]
- end
-
- ## Handles the menu interaction and returns selected block and option state
- #
- def wait_for_user_selection(blocks_in_file, blocks_menu, default,
- opts)
- pt = opts[:prompt_select_block].to_s
- bm = prepare_blocks_menu(blocks_menu, opts)
- return [nil, MenuState::EXIT] if bm.count.zero?
-
- o2 = if default
- opts.merge(default: default)
- else
- opts
- end
-
- obj = select_option_with_metadata(pt, bm, o2.merge(
- per_page: opts[:select_page_height]
- ))
-
- case obj.fetch(:oname, nil)
- when menu_chrome_formatted_option(opts, :menu_option_exit_name)
- [nil, MenuState::EXIT]
- when menu_chrome_formatted_option(opts, :menu_option_back_name)
- [obj, MenuState::BACK]
- else
- [obj, MenuState::CONTINUE]
- end
- rescue StandardError => err
- warn(error = "ERROR ** MarkParse.wait_for_user_selection(); #{err.inspect}")
- warn caller.take(3)
- binding.pry if $tap_enable
- raise ArgumentError, error
- end
-
- # Handles the core logic for generating the command file's metadata and content.
- def write_command_file(call_options, required_lines)
- return unless call_options[:save_executed_script]
-
- time_now = Time.now.utc
- opts = optsmerge call_options
- opts[:saved_script_filename] =
- SavedAsset.script_name(blockname: opts[:block_name],
- filename: opts[:filename],
- prefix: opts[:saved_script_filename_prefix],
- time: time_now)
-
- @execute_script_filespec =
- @options[:saved_filespec] =
- File.join opts[:saved_script_folder],
- opts[:saved_script_filename]
-
- shebang = if @options[:shebang]&.present?
- "#{@options[:shebang]} #{@options[:shell]}\n"
- else
- ''
- end
-
- content = shebang +
- "# file_name: #{opts[:filename]}\n" \
- "# block_name: #{opts[:block_name]}\n" \
- "# time: #{time_now}\n" \
- "#{required_lines.flatten.join("\n")}\n"
-
- create_and_write_file_with_permissions(@options[:saved_filespec], content,
- @options[:saved_script_chmod])
- end
-
- def write_execution_output_to_file
- FileUtils.mkdir_p File.dirname(@options[:logged_stdout_filespec])
-
- ol = ["-STDOUT-\n"]
- ol += @execute_files&.fetch(EF_STDOUT, [])
- ol += ["\n-STDERR-\n"]
- ol += @execute_files&.fetch(EF_STDERR, [])
- ol += ["\n-STDIN-\n"]
- ol += @execute_files&.fetch(EF_STDIN, [])
- ol += ["\n"]
- File.write(@options[:logged_stdout_filespec], ol.join)
- end
-
- # Writes required code blocks to a temporary file and sets an environment variable with its path.
- #
- # @param block_name [String] The name of the block to collect code for.
- # @param opts [Hash] Additional options for collecting code.
- # @note Sets the 'MDE_LINK_REQUIRED_FILE' environment variable to the temporary file path.
- def write_required_blocks_to_temp_file(mdoc, block_name, opts = {})
- code_blocks = (read_required_blocks_from_temp_file +
- mdoc.collect_recursively_required_code(
- block_name,
- opts: opts
- )[:code]).join("\n")
-
- Dir::Tmpname.create(self.class.to_s) do |path|
- File.write(path, code_blocks)
- ENV['MDE_LINK_REQUIRED_FILE'] = path
- end
- end
end # class MarkParse
end # module MarkdownExec
if $PROGRAM_NAME == __FILE__
require 'bundler/setup'
Bundler.require(:default)
require 'minitest/autorun'
module MarkdownExec
- class TestMarkParse < Minitest::Test
- require 'mocha/minitest'
-
- def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
- pigeon = 'E'
- obj = { s_pass_args: pigeon }
-
- c = MarkdownExec::MarkParse.new
-
- # Expect that method command_execute is called with argument args having value pigeon
- c.expects(:command_execute).with(
- obj,
- '',
- args: pigeon
- )
-
- # Call method execute_approved_block
- c.execute_approved_block(obj, [])
- end
-
- def setup
- @mark_parse = MarkdownExec::MarkParse.new
- end
-
- def test_set_fcb_title
- # sample input and output data for testing update_title_from_body method
- input_output_data = [
- {
- input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
- output: "puts 'Hello, world!'"
- },
- {
- input: FCB.new(title: '',
- body: ['def add(x, y)',
- ' x + y', 'end']),
- output: "def add(x, y)\n x + y\n end\n"
- },
- {
- input: FCB.new(title: 'foo', body: %w[bar baz]),
- output: 'foo' # expect the title to remain unchanged
- }
- ]
-
- # 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]
- @mark_parse.update_title_from_body(input)
- assert_equal output, input.title
- end
- end
- end
-
def test_select_block
blocks = [block1, block2]
menu = [m1, m2]
block, state = obj.select_block(blocks, menu, nil, {})