lib/markdown_exec.rb in markdown_exec-1.3.3.5 vs lib/markdown_exec.rb in markdown_exec-1.3.6

- old
+ new

@@ -10,14 +10,21 @@ require 'optparse' require 'shellwords' require 'tty-prompt' require 'yaml' +require_relative 'block_label' require_relative 'cached_nested_file_reader' require_relative 'cli' require_relative 'colorize' require_relative 'env' +require_relative 'fcb' +require_relative 'filter' +require_relative 'mdoc' +require_relative 'option_value' +require_relative 'saved_assets' +require_relative 'saved_files_matcher' require_relative 'shared' require_relative 'tap' require_relative 'markdown_exec/version' include CLI @@ -88,18 +95,18 @@ public # :reek:UtilityFunction def list_recent_output(saved_stdout_folder, saved_stdout_glob, list_count) - Sfiles.new(saved_stdout_folder, - saved_stdout_glob).most_recent_list(list_count) + SavedFilesMatcher.most_recent_list(saved_stdout_folder, + saved_stdout_glob, list_count) end # :reek:UtilityFunction def list_recent_scripts(saved_script_folder, saved_script_glob, list_count) - Sfiles.new(saved_script_folder, - saved_script_glob).most_recent_list(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 @@ -109,431 +116,13 @@ # execute markdown documents # module MarkdownExec # :reek:IrresponsibleModule - class Error < StandardError; end - - # fenced code block - # - class FCB - def initialize(options = {}) - @attrs = { - body: nil, - call: nil, - headings: [], - name: nil, - reqs: [], - shell: '', - title: '', - random: Random.new.rand, - text: nil # displayable in menu - }.merge options - end - - def to_h - @attrs - end - - def to_yaml - @attrs.to_yaml - end - - private - - # :reek:ManualDispatch - def method_missing(method, *args, &block) - method_name = method.to_s - - if @attrs.respond_to?(method_name) - @attrs.send(method_name, *args, &block) - elsif method_name[-1] == '=' - @attrs[method_name.chop.to_sym] = args[0] - else - @attrs[method_name.to_sym] - end - rescue StandardError => err - warn(error = "ERROR ** FCB.method_missing(method: #{method_name}," \ - " *args: #{args.inspect}, &block)") - warn err.inspect - warn(caller[0..4]) - raise StandardError, error - end - - # option names are available as methods - # - def respond_to_missing?(_method_name, _include_private = false) - true # recognize all hash methods, rest are treated as hash keys - end - end - - # select fcb per options - # - # :reek:UtilityFunction - class Filter - # def self.fcb_title_parse(opts, fcb_title) - # fcb_title.match(Regexp.new(opts[:fenced_start_ex_match])).named_captures.sym_keys - # end - - def self.fcb_select?(options, fcb) - # options.tap_yaml 'options' - # fcb.tap_inspect 'fcb' - name = fcb.fetch(:name, '').tap_inspect 'name' - shell = fcb.fetch(:shell, '').tap_inspect 'shell' - - ## include hidden blocks for later use - # - name_default = true - name_exclude = nil - name_select = nil - shell_default = true - shell_exclude = nil - shell_select = nil - hidden_name = nil - - if name.present? && options[:block_name] - if name =~ /#{options[:block_name]}/ - '=~ block_name'.tap_puts - name_select = true - name_exclude = false - else - '!~ block_name'.tap_puts - name_exclude = true - name_select = false - end - end - - if name.present? && name_select.nil? && options[:select_by_name_regex].present? - '+select_by_name_regex'.tap_puts - name_select = (!!(name =~ /#{options[:select_by_name_regex]}/)).tap_inspect 'name_select' - end - - if shell.present? && options[:select_by_shell_regex].present? - '+select_by_shell_regex'.tap_puts - shell_select = (!!(shell =~ /#{options[:select_by_shell_regex]}/)).tap_inspect 'shell_select' - end - - if name.present? && name_exclude.nil? && options[:exclude_by_name_regex].present? - '-exclude_by_name_regex'.tap_puts - name_exclude = (!!(name =~ /#{options[:exclude_by_name_regex]}/)).tap_inspect 'name_exclude' - end - - if shell.present? && options[:exclude_by_shell_regex].present? - '-exclude_by_shell_regex'.tap_puts - shell_exclude = (!!(shell =~ /#{options[:exclude_by_shell_regex]}/)).tap_inspect 'shell_exclude' - end - - if name.present? && options[:hide_blocks_by_name] && - options[:block_name_hidden_match].present? - '+block_name_hidden_match'.tap_puts - hidden_name = (!!(name =~ /#{options[:block_name_hidden_match]}/)).tap_inspect 'hidden_name' - end - - if shell.present? && options[:hide_blocks_by_shell] && - options[:block_shell_hidden_match].present? - '-hide_blocks_by_shell'.tap_puts - (!!(shell =~ /#{options[:block_shell_hidden_match]}/)).tap_inspect 'hidden_shell' - end - - if options[:bash_only] - '-bash_only'.tap_puts - shell_default = (shell == 'bash').tap_inspect 'shell_default' - end - - ## name matching does not filter hidden blocks - # - case - when options[:no_chrome] && fcb.fetch(:chrome, false) - '-no_chrome'.tap_puts - false - when options[:exclude_expect_blocks] && shell == 'expect' - '-exclude_expect_blocks'.tap_puts - false - when hidden_name == true - true - when name_exclude == true, shell_exclude == true, - name_select == false, shell_select == false - false - when name_select == true, shell_select == true - true - when name_default == false, shell_default == false - false - else - true - end.tap_inspect - rescue StandardError => err - warn("ERROR ** Filter::fcb_select?(); #{err.inspect}") - raise err - end - end # class Filter - - ## an imported markdown document - # - class MDoc - attr_reader :table - - # convert block name to fcb_parse - # - def initialize(table) - @table = table - end - - def collect_recursively_required_code(name) - get_required_blocks(name) - .map do |fcb| - body = fcb[:body].join("\n") - - if fcb[:cann] - xcall = fcb[:cann][1..-2] - mstdin = xcall.match(/<(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/) - mstdout = xcall.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/) - - yqcmd = if mstdin[:type] - "echo \"$#{mstdin[:name]}\" | yq '#{body}'" - else - "yq e '#{body}' '#{mstdin[:name]}'" - end - if mstdout[:type] - "export #{mstdout[:name]}=$(#{yqcmd})" - else - "#{yqcmd} > '#{mstdout[:name]}'" - end - elsif fcb[:stdout] - stdout = fcb[:stdout] - body = fcb[:body].join("\n") - if stdout[:type] - %(export #{stdout[:name]}=$(cat <<"EOF"\n#{body}\nEOF\n)) - else - "cat > '#{stdout[:name]}' <<\"EOF\"\n" \ - "#{body}\n" \ - "EOF\n" - end - else - fcb[:body] - end - end.flatten(1) - end - - def get_block_by_name(name, default = {}) - @table.select { |fcb| fcb.fetch(:name, '') == name }.fetch(0, default) - end - - def get_required_blocks(name) - name_block = get_block_by_name(name) - raise "Named code block `#{name}` not found." if name_block.nil? || name_block.keys.empty? - - all = [name_block.fetch(:name, '')] + recursively_required(name_block[:reqs]) - - # in order of appearance in document - # insert function blocks - @table.select { |fcb| all.include? fcb.fetch(:name, '') } - .map do |fcb| - if (call = fcb[:call]) - [get_block_by_name("[#{call.match(/^%\((\S+) |\)/)[1]}]") - .merge({ cann: call })] - else - [] - end + [fcb] - end.flatten(1) - end - - # :reek:UtilityFunction - def hide_menu_block_per_options(opts, block) - (opts[:hide_blocks_by_name] && - block[:name]&.match(Regexp.new(opts[:block_name_hidden_match])) && - (block[:name]&.present? || block[:label]&.present?) - ).tap_inspect - end - - # def blocks_for_menu(opts) - # if opts[:hide_blocks_by_name] - # @table.reject { |block| hide_menu_block_per_options opts, block } - # else - # @table - # end - # end - - def fcbs_per_options(opts = {}) - options = opts.merge(block_name_hidden_match: nil) - selrows = @table.select do |fcb_title_groups| - Filter.fcb_select? options, fcb_title_groups - end - - ### hide rows correctly - - if opts[:hide_blocks_by_name] - selrows.reject { |block| hide_menu_block_per_options opts, block } - else - selrows - end.map do |block| - # block[:name] = block[:text] if block[:name].nil? - block - end - end - - def recursively_required(reqs) - return [] unless reqs - - rem = reqs - memo = [] - while rem.count.positive? - rem = rem.map do |req| - next if memo.include? req - - memo += [req] - get_block_by_name(req).fetch(:reqs, []) - end - .compact - .flatten(1) - end - memo - end - end # class MDoc - - # format option defaults and values - # - # :reek:TooManyInstanceVariables - class BlockLabel - def initialize(filename:, headings:, menu_blocks_with_docname:, - menu_blocks_with_headings:, title:, body:, text:) - @filename = filename - @headings = headings - @menu_blocks_with_docname = menu_blocks_with_docname - @menu_blocks_with_headings = menu_blocks_with_headings - # @title = title.present? ? title : body - @title = title - @body = body - @text = text - rescue StandardError => err - warn(error = "ERROR ** BlockLabel.initialize(); #{err.inspect}") - binding.pry if $tap_enable - raise ArgumentError, error - end - - # join title, headings, filename - # - def make - label = @title - label = @body unless label.present? - label = @text unless label.present? - label.tap_inspect - ([label] + - (if @menu_blocks_with_headings - [@headings.compact.join(' # ')] - else - [] - end) + - ( - if @menu_blocks_with_docname - [@filename] - else - [] - end - )).join(' ') - rescue StandardError => err - warn(error = "ERROR ** BlockLabel.make(); #{err.inspect}") - binding.pry if $tap_enable - raise ArgumentError, error - end - end # class BlockLabel - FNR11 = '/' FNR12 = ',~' - # format option defaults and values - # - class SavedAsset - def initialize(filename:, prefix:, time:, blockname:) - @filename = filename - @prefix = prefix - @time = time - @blockname = blockname - end - - def script_name - fne = @filename.gsub(FNR11, FNR12) - "#{[@prefix, @time.strftime('%F-%H-%M-%S'), fne, ',', - @blockname].join('_')}.sh" - end - - def stdout_name - "#{[@prefix, @time.strftime('%F-%H-%M-%S'), @filename, - @blockname].join('_')}.out.txt" - end - end # class SavedAsset - - # format option defaults and values - # - class OptionValue - def initialize(value) - @value = value - end - - # as default value in env_str() - # - def for_hash(default = nil) - return default if @value.nil? - - case @value.class.to_s - when 'String', 'Integer' - @value - when 'FalseClass', 'TrueClass' - @value ? true : false - when @value.empty? - default - else - @value.to_s - end - end - - # for output as default value in list_default_yaml() - # - def for_yaml(default = nil) - return default if @value.nil? - - case @value.class.to_s - when 'String' - "'#{@value}'" - when 'Integer' - @value - when 'FalseClass', 'TrueClass' - @value ? true : false - when @value.empty? - default - else - @value.to_s - end - end - end # class OptionValue - - # a generated list of saved files - # - class Sfiles - def initialize(folder, glob) - @folder = folder - @glob = glob - end - - def list_all - Dir.glob(File.join(@folder, @glob)) - end - - def most_recent(arr = nil) - arr = list_all if arr.nil? - return if arr.count < 1 - - arr.max - end - - def most_recent_list(list_count, arr = nil) - arr = list_all if arr.nil? - return if (ac = arr.count) < 1 - - arr.sort[-[ac, list_count].min..].reverse - end - end # class Sfiles - ## # # rubocop:disable Layout/LineLength # :reek:DuplicateMethodCall { allow_calls: ['block', 'item', 'lm', 'opts', 'option', '@options', 'required_blocks'] } # rubocop:enable Layout/LineLength @@ -568,39 +157,37 @@ argv when 0 [] else argv[0..ind - 1] - end #.tap_inspect + end end # return arguments after `--` # def arguments_for_child(argv = ARGV) case ind = argv.find_index('--') when nil, argv.count - 1 [] else argv[ind + 1..-1] - end #.tap_inspect + end end ## # options necessary to start, parse input, defaults for cli options # def base_options menu_iter do |item| - # noisy item.tap_yaml name: :item next unless item[:opt_name].present? item_default = item[:default] - # noisy item_default.tap_inspect name: :item_default value = if item_default.nil? item_default else env_str(item[:env_var], - default: OptionValue.new(item_default).for_hash) + default: OptionValue.for_hash(item_default)) end [item[:opt_name], item[:proccode] ? item[:proccode].call(value) : value] end.compact.to_h end @@ -755,11 +342,11 @@ fout frame end # :reek:DuplicateMethodCall def exec_block(options, _block_name = '') - options = calculated_options.merge(options).tap_yaml 'options' + options = calculated_options.merge(options) update_options options, over: false # document and block reports # files = list_files_per_options(options) @@ -933,13 +520,15 @@ # return body, title if option.struct # return body if not struct # def list_blocks_in_file(call_options = {}, &options_block) - opts = optsmerge(call_options, options_block) #.tap_yaml 'opts' + opts = optsmerge(call_options, options_block) + use_chrome = !opts[:no_chrome] + blocks = [] - if opts[:menu_initial_divider].present? + if opts[:menu_initial_divider].present? && use_chrome blocks.push FCB.new({ # name: '', chrome: true, name: format( opts[:menu_divider_format], @@ -959,44 +548,50 @@ when :line ## convert line to block # if opts[:menu_divider_match].present? && (mbody = fcb.body[0].match opts[:menu_divider_match]) - blocks.push FCB.new( - { chrome: true, - disabled: '', - name: format(opts[:menu_divider_format], - mbody[:name]).send(opts[:menu_divider_color].to_sym) } - ) + if use_chrome + blocks.push FCB.new( + { chrome: true, + disabled: '', + name: format(opts[:menu_divider_format], + mbody[:name]).send(opts[:menu_divider_color].to_sym) } + ) + end elsif opts[:menu_task_match].present? && (mbody = fcb.body[0].match opts[:menu_task_match]) - blocks.push FCB.new( - { chrome: true, - disabled: '', - name: format(opts[:menu_task_format], - mbody[:name]).send(opts[:menu_task_color].to_sym) } - ) + if use_chrome + blocks.push FCB.new( + { chrome: true, + disabled: '', + name: format( + opts[:menu_task_format], + $~.named_captures.transform_keys(&:to_sym) + ).send(opts[:menu_task_color].to_sym) } + ) + end else # line not added end when :blocks ## enhance fcb with block summary # blocks.push get_block_summary(opts, fcb) ### if Filter.fcb_select? opts, fcb end end - if opts[:menu_divider_format].present? && opts[:menu_final_divider].present? + if opts[:menu_divider_format].present? && opts[:menu_final_divider].present? && use_chrome && use_chrome blocks.push FCB.new( { chrome: true, disabled: '', name: format(opts[:menu_divider_format], opts[:menu_final_divider]) .send(opts[:menu_divider_color].to_sym) } ) end - blocks.tap_inspect + blocks rescue StandardError => err warn(error = "ERROR ** MarkParse.list_blocks_in_file(); #{err.inspect}") warn(caller[0..4]) raise StandardError, error end @@ -1015,11 +610,11 @@ def list_default_yaml menu_iter do |item| next unless item[:opt_name].present? && item[:default].present? [ - "#{item[:opt_name]}: #{OptionValue.new(item[:default]).for_yaml}", + "#{item[:opt_name]}: #{OptionValue.for_yaml(item[:default])}", item[:description].present? ? item[:description] : nil ].compact.join(' # ') end.compact.sort end @@ -1072,11 +667,11 @@ else # blocks.map(&:name) blocks.map do |block| block.fetch(:text, nil) || block.fetch(:name, nil) end - end.compact.reject(&:empty?).tap_inspect + end.compact.reject(&:empty?) end ## output type (body string or full object) per option struct and bash # def list_named_blocks_in_file(call_options = {}, &options_block) @@ -1084,23 +679,25 @@ blocks = list_blocks_in_file(opts.merge(struct: true)).select do |fcb| # fcb.fetch(:name, '') != '' && Filter.fcb_select?(opts, fcb) Filter.fcb_select?(opts.merge(no_chrome: true), fcb) end - blocks_per_opts(blocks, opts).tap_inspect + blocks_per_opts(blocks, opts) end def make_block_labels(call_options = {}) opts = options.merge(call_options) list_blocks_in_file(opts).map do |fcb| - BlockLabel.new(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]).make + 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 # :reek:DuplicateMethodCall # :reek:NestedIterators @@ -1350,12 +947,12 @@ @options[:block_name] = mf[:block] @options[:filename] = mf[:file].gsub(FNR12, FNR11) end def run_last_script - filename = Sfiles.new(@options[:saved_script_folder], - @options[:saved_script_glob]).most_recent + 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({}) @@ -1387,11 +984,11 @@ File.write(@options[:logged_stdout_filespec], ol.join) end def select_approve_and_execute_block(call_options, &options_block) opts = optsmerge call_options, options_block - blocks_in_file = list_blocks_in_file(opts.merge(struct: true)).tap_inspect + blocks_in_file = list_blocks_in_file(opts.merge(struct: true)) mdoc = MDoc.new(blocks_in_file) do |nopts| opts.merge!(nopts) end blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true)) @@ -1403,19 +1000,19 @@ bm = blocks_menu.map do |fcb| # next if fcb.fetch(:disabled, false) # next unless fcb.fetch(:name, '').present? fcb.merge!( - label: BlockLabel.new( + 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] - ).make + ) ) fcb.to_h end.compact return nil if bm.count.zero? @@ -1527,16 +1124,16 @@ @options[:saved_filespec] = File.join opts[:saved_script_folder], opts[:saved_script_filename] dirname = File.dirname(@options[:saved_filespec]) FileUtils.mkdir_p dirname - (shebang = if @options[:shebang]&.present? - "#{@options[:shebang]} #{@options[:shell]}\n" - else - '' - end - ).tap_inspect name: :shebang + shebang = if @options[:shebang]&.present? + "#{@options[:shebang]} #{@options[:shell]}\n" + else + '' + end + File.write(@options[:saved_filespec], shebang + "# file_name: #{opts[:filename]}\n" \ "# block_name: #{opts[:block_name]}\n" \ "# time: #{time_now}\n" \ "#{required_blocks.flatten.join("\n")}\n") @@ -1544,5 +1141,7 @@ File.chmod @options[:saved_script_chmod], @options[:saved_filespec] end end # class MarkParse end # module MarkdownExec + +require 'minitest/autorun' if $PROGRAM_NAME == __FILE__