# frozen_string_literal: true require "open-uri" require "fileutils" require "trellodon/formatters/base" module Trellodon module Formatters class Markdown < Base attr_reader :output_dir MD_FILENAME = "README.md" ATTACHMENTS_DIRNAME = "attachments" DIRNAME_MAXLENGTH = 50 def initialize(output_dir:, **opts) super(**opts) @output_dir = output_dir @board_dirname = Concurrent::Map.new @board_subdirs = Concurrent::Map.new @list_dirname = Concurrent::Map.new @list_subdirs = Concurrent::Map.new @card_dirname = Concurrent::Map.new end def card_added(card) card_mdfile = File.join(card_path(card), MD_FILENAME) raise "File #{card_mdfile} already exists" if File.exist?(card_mdfile) File.write(card_mdfile, format_card(card)) download_attachments(card) end def finish logger.info "Markdown dump is here: #{@output_dir}" end private def card_path(card) raise "Board is undefined" unless @board raise "List id is undefined" if card.list_id.nil? || card.list_id.empty? list = @board.get_list(card.list_id) raise "List #{card.list_id} is not found" if list.nil? card_dir = File.join(@output_dir, board_dirname(board), list_dirname(list), card_dirname(card)) FileUtils.mkdir_p(card_dir) unless File.directory?(card_dir) card_dir end def card_attachments_path(card) attachments_dir = File.join(card_path(card), ATTACHMENTS_DIRNAME) FileUtils.mkdir_p(attachments_dir) unless File.directory?(attachments_dir) attachments_dir end def board_dirname(board) @board_dirname.compute_if_absent(board.id) { sanitize(board.name) } end def list_dirname(list) @list_dirname.compute_if_absent(list.id) do uniq_dirname( dir: sanitize(list.name), entity: list, dirlist: @board_subdirs, parent_id: list.board_id ) end end def card_dirname(card) @card_dirname.compute_if_absent(card.id) do uniq_dirname( dir: sanitize(card.name), entity: card, dirlist: @list_subdirs, parent_id: card.list_id ) end end def uniq_dirname(dir:, entity:, dirlist:, parent_id:) return dir unless [List, Card].include?(entity.class) dirlist.put_if_absent(parent_id, Concurrent::Map.new) dirlist[parent_id].put_if_absent(dir, Concurrent::Array.new([entity.id])) dirlist[parent_id][dir].push(entity.id) unless dirlist[parent_id][dir].include?(entity.id) idx = dirlist[parent_id][dir].index(entity.id) idx > 0 ? dir + "(#{idx})" : dir end def sanitize(name, separator: "") name.gsub(/[\/\\"*?<>|:]+/, separator).slice(0, DIRNAME_MAXLENGTH) end def download_attachments(card) return if card.attachments.nil? || card.attachments.empty? attachments_path = card_attachments_path(card) card.attachments.select(&:is_upload).each do |att| att_file_path = File.join(attachments_path, att.file_name) if File.file?(att_file_path) logger.warn "Attachment file already exists: #{att_file_path}" else IO.copy_stream(URI.parse(att.url).open(att.headers), att_file_path) end end end def format_card(card) create_card_header(card) + create_card_title(card) + create_card_description(card) + create_card_checklists(card) + create_card_comments(card) + create_card_attachments(card) end def create_card_header(card) <<~EOS --- title: #{card.name} last_updated_at: #{card.last_activity_date} dumped_at: #{Time.now} labels: #{create_card_labels(card)} --- EOS end def create_card_title(card) <<~EOS # #{card.name} EOS end def create_card_description(card) <<~EOS ## Description #{card.desc} EOS end def create_card_labels(card) return "" if card.labels.nil? || card.labels.empty? card.labels.map { |label| "\n - " + label }.join end def create_card_checklists(card) return "" if card.checklists.nil? || card.checklists.empty? <<~EOS ## Checklists #{card.checklists.map { |checklist| create_card_checklist(checklist) }.join} EOS end def create_card_checklist(checklist) <<~EOS ### #{checklist.name} #{checklist.items&.map { |item| create_card_checklist_item(item) }&.join} EOS end def create_card_checklist_item(item) formatted_item = "\n- [#{item.checked? ? "x" : " "}] #{item.name}" formatted_item += " | *assigned_to: #{create_member(item.member)}*" if item.member formatted_item += " | *due: #{item.due_date}*" if item.due_date formatted_item end def create_card_comments(card) return "" if card.comments.nil? || card.comments.empty? <<~EOS ## Comments #{card.comments.map { |comment| create_card_comment(comment) }.join} EOS end def create_card_comment(comment) <<~EOS ### #{create_member(comment.creator)} at #{comment.date} #{comment.text} EOS end def create_card_attachments(card) return "" if card.attachments.nil? || card.attachments.empty? <<~EOS ## Attachments #{card.attachments.map { |attachment| create_card_attachment(attachment) }.join} EOS end def create_card_attachment(attachment) <<~EOS ### #{attachment.name} **date**: #{attachment.date} **added_by**: #{create_member(attachment.added_by)} **url**: #{attachment.url} **local copy**: #{create_attachment_local_link(attachment)} EOS end def create_attachment_local_link(attachment) return "-" unless attachment.is_upload "[#{attachment.file_name}](#{File.join("./", ATTACHMENTS_DIRNAME, attachment.file_name)})" end def create_member(member) return "" unless member "#{member.full_name} (@#{member.username})" end end end end