lib/howzit/buildnote.rb in howzit-2.0.15 vs lib/howzit/buildnote.rb in howzit-2.0.16

- old
+ new

@@ -9,28 +9,20 @@ ## ## Initialize a build note ## ## @param file [String] The path to the build note file - ## @param args [Array] additional args ## - def initialize(file: nil, args: []) + def initialize(file: nil) @topics = [] - if note_file.nil? - res = Prompt.yn('No build notes file found, create one?', default: true) + create_note(prompt: true) if note_file.nil? - create_note if res - Process.exit 0 - end content = Util.read_file(note_file) - if content.nil? || content.empty? - Howzit.console.error("{br}No content found in build note (#{note_file}){x}".c) - Process.exit 1 - else - @metadata = content.split(/^#/)[0].strip.get_metadata - end + raise "{br}No content found in build note (#{note_file}){x}".c if content.nil? || content.empty? + @metadata = content.split(/^#/)[0].strip.get_metadata + read_help(file) end ## ## Inspect @@ -54,16 +46,28 @@ def edit edit_note end ## + ## Public method to open a template in the editor + ## + ## @param template [String] The template title + ## + def edit_template(template) + file = template.sub(/(\.md)?$/i, '.md') + file = File.join(Howzit.config.template_folder, file) + edit_template_file(file) + end + + ## ## Find a topic based on a fuzzy match ## ## @param term [String] The search term ## def find_topic(term = nil) return @topics if term.nil? + @topics.filter do |topic| rx = term.to_rx topic.title.downcase =~ rx end end @@ -135,19 +139,18 @@ output.push(%({bg}"Runnable" Topics:{x}\n).c) find_topic(Howzit.options[:for_topic]).each do |topic| s_out = [] - topic.tasks.each do |task| - s_out.push(task.to_list) - end + topic.tasks.each { |task| s_out.push(task.to_list) } - unless s_out.empty? - output.push("- {bw}#{topic.title}{x}".c) - output.push(s_out.join("\n")) - end + next if s_out.empty? + + output.push("- {bw}#{topic.title}{x}".c) + output.push(s_out.join("\n")) end + output.join("\n") end ## ## Read the help file contents @@ -156,26 +159,79 @@ ## def read_file(file) read_help_file(file) end + ## + ## Create a template file + ## + ## @param file [String] file path + ## @param prompt [Boolean] confirm file creation? + ## + def create_template_file(file, prompt: false) + trap('SIGINT') do + Howzit.console.info "\nCancelled" + exit! + end + + default = !$stdout.isatty || Howzit.options[:default] + + if prompt && !default && !File.exist?(file) + res = Prompt.yn("{bg}Template {bw}#{File.basename(file)}{bg} not found, create it?{x}".c, default: true) + Process.exit 0 unless res + end + + title = File.basename(file, '.md') + + note = <<~EOBUILDNOTES + # #{title} + + ## Template Topic + + EOBUILDNOTES + + if File.exist?(file) && !default + file = "{by}#{file}".c + unless Prompt.yn("Are you sure you want to overwrite #{file}", default: false) + puts 'Cancelled' + Process.exit 0 + end + end + + File.open(file, 'w') do |f| + f.puts note + puts "{by}Template {bw}#{title}{by} written to {bw}#{file}{x}".c + end + + if File.exist?(file) && !default && Prompt.yn("{bg}Do you want to open {bw}#{file} {bg}for editing?{x}".c, + default: false) + edit_template_file(file) + end + + Process.exit 0 + end + # Create a buildnotes skeleton - def create_note + def create_note(prompt: false) trap('SIGINT') do Howzit.console.info "\nCancelled" exit! end + default = !$stdout.isatty || Howzit.options[:default] + + if prompt && !default + res = Prompt.yn('No build notes file found, create one?', default: true) + Process.exit 0 unless res + end + # First make sure there isn't already a buildnotes file if note_file fname = "{by}#{note_file}{bw}".c unless default res = Prompt.yn("#{fname} exists and appears to be a build note, continue anyway?", default: false) - unless res - puts 'Canceled' - Process.exit 0 - end + Process.exit 0 unless res end end title = File.basename(Dir.pwd) # prompt = TTY::Prompt.new @@ -226,27 +282,24 @@ EOBUILDNOTES if File.exist?(fname) && !default file = "{by}#{fname}".c - res = Prompt.yn("Are you absolutely sure you want to overwrite #{file}", default: false) - - unless res + unless Prompt.yn("Are you absolutely sure you want to overwrite #{file}", default: false) puts 'Canceled' Process.exit 0 end end File.open(fname, 'w') do |f| f.puts note - puts "{by}Build notes for #{title} written to #{fname}".c + puts "{by}Build notes for {bw}#{title}{by} written to {bw}#{fname}{x}".c end - if File.exist?(fname) && !default - res = Prompt.yn("{bg}Do you want to open {bw}#{file} {bg}for editing?{x}".c, default: false) - - edit_note if res + if File.exist?(fname) && !default && Prompt.yn("{bg}Do you want to open {bw}#{fname} {bg}for editing?{x}".c, + default: false) + edit_note end Process.exit 0 end @@ -260,10 +313,143 @@ end private ## + ## Import the contents of a filename as new topics + ## + ## @param mtch [MatchData] the filename match from + ## the include directive + ## + def include_file(mtch) + file = File.expand_path(mtch[1]) + + return mtch[0] unless File.exist?(file) + + content = Util.read_file(file) + home = ENV['HOME'] + short_path = File.dirname(file.sub(/^#{home}/, '~')) + prefix = "#{short_path}/#{File.basename(file)}:" + parts = content.split(/^##+/) + parts.shift + if parts.empty? + content + else + "## #{parts.join('## ')}".gsub(/^(##+ *)(?=\S)/, "\\1#{prefix}") + end + end + + ## + ## Test to ensure that any `required` metadata in a + ## template is fulfilled by the build note + ## + ## @param template [String] The template to read + ## from + ## + def ensure_requirements(template) + t_leader = Util.read_file(template).split(/^#/)[0].strip + if t_leader.length > 0 + t_meta = t_leader.get_metadata + if t_meta.key?('required') + required = t_meta['required'].strip.split(/\s*,\s*/) + required.each do |req| + unless @metadata.keys.include?(req.downcase) + Howzit.console.error %({bRw}ERROR:{xbr} Missing required metadata key from template '{bw}#{File.basename(template, '.md')}{xr}'{x}).c + Howzit.console.error %({br}Please define {by}#{req.downcase}{xr} in build notes{x}).c + Process.exit 1 + end + end + end + end + end + + ## + ## Test a template string for bracketed subtopics + ## + ## @param template [String] The template name + ## + ## @return [Array] [[String] updated template name, [Array] + ## subtopic titles] + ## + def detect_subtopics(template) + subtopics = nil + + if template =~ /\[(.*?)\]$/ + subtopics = Regexp.last_match[1].split(/\s*\|\s*/).map { |t| t.gsub(/\*/, '.*?')} + template.sub!(/\[.*?\]$/, '').strip + end + + [template, subtopics] + end + + ## + ## Enumerate templates and read their associated files + ## into topics + ## + ## @param templates [Array] The templates to read + ## + ## @return [Array] template topics + ## + def gather_templates(templates) + template_topics = [] + + templates.each do |template| + template, subtopics = detect_subtopics(template) + + file = template.sub(/(\.md)?$/i, '.md') + file = File.join(Howzit.config.template_folder, file) + + next unless File.exist?(file) + + ensure_requirements(file) + + template_topics.concat(read_template(template, file, subtopics)) + end + + template_topics + end + + ## + ## Filter topics based on subtopic titles + ## + ## @param note [BuildNote] The note + ## @param subtopics [Array] The subtopics to + ## extract + ## + ## @return [Array] extracted subtopics + ## + def extract_subtopics(note, subtopics) + template_topics = [] + + subtopics.each do |subtopic| + note.topics.each { |topic| template_topics.push(topic) if topic.title =~ /^(.*?:)?#{subtopic}$/i } + end + + template_topics + end + + ## + ## Read a template file + ## + ## @param template [String] The template title + ## @param file [String] The file path + ## @param subtopics [Array] The subtopics to + ## extract, nil to return all + ## + ## @return [Array] extracted topics + ## + def read_template(template, file, subtopics = nil) + note = BuildNote.new(file: file) + + template_topics = subtopics.nil? ? note.topics : extract_subtopics(note, subtopics) + template_topics.map do |topic| + topic.parent = template + topic + end + end + + ## ## Traverse up directory tree looking for build notes ## ## @return topics dictionary ## def glob_upstream @@ -340,126 +526,32 @@ end topics_dict end ## - ## Test to ensure that any `required` metadata in a - ## template is fulfilled by the build note - ## - ## @param template [String] The template to read - ## from - ## - def ensure_requirements(template) - t_leader = Util.read_file(template).split(/^#/)[0].strip - if t_leader.length > 0 - t_meta = t_leader.get_metadata - if t_meta.key?('required') - required = t_meta['required'].strip.split(/\s*,\s*/) - required.each do |req| - unless @metadata.keys.include?(req.downcase) - Howzit.console.error %({bRw}ERROR:{xbr} Missing required metadata key from template '{bw}#{File.basename(template, '.md')}{xr}'{x}).c - Howzit.console.error %({br}Please define {by}#{req.downcase}{xr} in build notes{x}).c - Process.exit 1 - end - end - end - end - end - - ## ## Read a list of topics from an included template ## ## @param content [String] The template contents ## def get_template_topics(content) leader = content.split(/^#/)[0].strip template_topics = [] - if leader.length > 0 - data = leader.get_metadata + return template_topics if leader.empty? - if data.key?('template') - templates = data['template'].strip.split(/\s*,\s*/) - templates.each do |t| - tasks = nil - if t =~ /\[(.*?)\]$/ - tasks = Regexp.last_match[1].split(/\s*,\s*/).map {|t| t.gsub(/\*/, '.*?')} - t = t.sub(/\[.*?\]$/, '').strip - end + data = leader.get_metadata - t_file = t.sub(/(\.md)?$/, '.md') - template = File.join(Howzit.config.template_folder, t_file) - if File.exist?(template) - ensure_requirements(template) + if data.key?('template') + templates = data['template'].strip.split(/\s*,\s*/) - t_topics = BuildNote.new(file: template) - if tasks - tasks.each do |task| - t_topics.topics.each do |topic| - if topic.title =~ /^(.*?:)?#{task}$/i - topic.parent = t - template_topics.push(topic) - end - end - end - else - t_topics.topics.map! do |topic| - topic.parent = t - topic - end - - template_topics.concat(t_topics.topics) - end - end - end - end + template_topics.concat(gather_templates(templates)) end + template_topics end - ## - ## Import the contents of a filename as new topics - ## - ## @param mtch [MatchData] the filename match from - ## the include directive - ## - def include_file(mtch) - file = File.expand_path(mtch[1]) - - return mtch[0] unless File.exist?(file) - - content = Util.read_file(file) - home = ENV['HOME'] - short_path = File.dirname(file.sub(/^#{home}/, '~')) - prefix = "#{short_path}/#{File.basename(file)}:" - parts = content.split(/^##+/) - parts.shift - if parts.empty? - content - else - "## #{parts.join('## ')}".gsub(/^(##+ *)(?=\S)/, "\\1#{prefix}") - end - end - - ## - ## Get the title of the build note (top level header) - ## - ## @param truncate [Integer] Truncate to width - ## - def note_title(truncate = 0) - help = Util.read_file(note_file) - title = help.match(/(?:^(\S.*?)(?=\n==)|^# ?(.*?)$)/) - title = if title - title[1].nil? ? title[2] : title[1] - else - note_file.sub(/(\.\w+)?$/, '') - end - - title && truncate.positive? ? title.trunc(truncate) : title - end - # Read in the build notes file and output a hash of # "Title" => contents # # @param path [String] The build note path # @@ -475,11 +567,11 @@ if help.nil? || help.empty? Howzit.console.error("{br}No content found in #{filename}{x}".c) Process.exit 1 end - @title = note_title + @title = help.note_title(filename) help.gsub!(/@include\((.*?)\)/) do include_file(Regexp.last_match) end @@ -545,21 +637,40 @@ raise 'No editor defined' if editor.nil? raise "Invalid editor (#{editor})" unless Util.valid_command?(editor) - if note_file.nil? - res = Prompt.yn('No build notes file found, create one?', default: true) + create_note(prompt: true) if note_file.nil? + `#{editor} "#{note_file}"` + end - create_note if res - edit_note - else - `#{editor} "#{note_file}"` - end + ## + ## Public method to create a new template + ## + ## @param template [String] The template name + ## + def create_template(template) + file = template.sub(/(\.md)?$/i, '.md') + file = File.join(Howzit.config.template_folder, file) + create_template_file(file, prompt: false) end ## + ## Open template in editor + ## + def edit_template_file(file) + editor = Howzit.options.fetch(:editor, ENV['EDITOR']) + + raise 'No editor defined' if editor.nil? + + raise "Invalid editor (#{editor})" unless Util.valid_command?(editor) + + create_template_file(file, prompt: true) unless File.exist?(file) + `#{editor} "#{file}"` + end + + ## ## Run or print a topic ## ## @param topic [Topic] The topic ## @param run [Boolean] execute directives if ## true @@ -587,20 +698,14 @@ output = [] unless note_file Process.exit 0 if Howzit.options[:list_runnable_titles] || Howzit.options[:list_topic_titles] - # clear the buffer - ARGV.length.times do - ARGV.shift - end - res = yn("No build notes file found, create one?", false) - create_note if res - Process.exit 1 + create_note(prompt: true) end if Howzit.options[:title_only] - out = note_title(20) + out = Util.read_file(note_file).note_title(note_file, 20) $stdout.print(out.strip) Process.exit(0) elsif Howzit.options[:output_title] && !Howzit.options[:run] if @title && !@title.empty? header = @title.format_header({ hr: "\u{2550}", color: '{bwK}' })