################################################################################## # RBCli -- A framework for developing command line applications in Ruby # # Copyright (C) 2018 Andrew Khoury # # # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation, either version 3 of the License, or # # (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program. If not, see . # # # # For questions regarding licensing, please contact andrew@blacknex.us # ################################################################################## require 'mdless' def class_exists?(class_name) klass = Module.const_get(class_name) return klass.is_a?(Class) rescue NameError return false end if class_exists? 'Encoding' Encoding.default_external = Encoding::UTF_8 if Encoding.respond_to?('default_external') Encoding.default_internal = Encoding::UTF_8 if Encoding.respond_to?('default_internal') end module CLIMarkdown class Converter def convert_markdown(input) @headers = get_headers(input) # yaml/MMD headers in_yaml = false if input.split("\n")[0] =~ /(?i-m)^---[ \t]*?(\n|$)/ @log.info("Found YAML") # YAML in_yaml = true input.sub!(/(?i-m)^---[ \t]*\n([\s\S]*?)\n[\-.]{3}[ \t]*\n/) do |yaml| m = Regexp.last_match @log.warn("Processing YAML Header") m[0].split(/\n/).map {|line| if line =~ /^[\-.]{3}\s*$/ line = c([:d,:black,:on_black]) + "% " + c([:d,:black,:on_black]) + line else line.sub!(/^(.*?:)[ \t]+(\S)/, '\1 \2') line = c([:d,:black,:on_black]) + "% " + c([:d,:white]) + line end if @cols - line.uncolor.size > 0 line += " "*(@cols-line.uncolor.size) end }.join("\n") + "#{xc}\n" end end if !in_yaml && input.gsub(/\n/,' ') =~ /(?i-m)^\w.+:\s+\S+ / @log.info("Found MMD Headers") input.sub!(/(?i-m)^([\S ]+:[\s\S]*?)+(?=\n\n)/) do |mmd| puts mmd mmd.split(/\n/).map {|line| line.sub!(/^(.*?:)[ \t]+(\S)/, '\1 \2') line = c([:d,:black,:on_black]) + "% " + c([:d,:white,:on_black]) + line if @cols - line.uncolor.size > 0 line += " "*(@cols - line.uncolor.size) end }.join("\n") + " "*@cols + "#{xc}\n" end end # Gather reference links input.gsub!(/^\s{,3}(?= top_level new_content.push(graf) else in_section = false break end elsif title.downcase == "#{@headers[@options[:section] - 1][1].downcase}" in_section = true top_level = level + 1 new_content.push(graf) else next end elsif in_section new_content.push(graf) end } input = new_content.join("\n") end h_adjust = highest_header(input) - 1 input.gsub!(/^(#+)/) do |m| match = Regexp.last_match "#" * (match[1].length - h_adjust) end input.gsub!(/(?i-m)([`~]{3,})([\s\S]*?)\n([\s\S]*?)\1/ ) do |cb| m = Regexp.last_match leader = m[2] ? m[2].upcase + ":" : 'CODE:' leader += xc if exec_available('pygmentize') lexer = m[2].nil? ? '-g' : "-l #{m[2]}" begin hilite, s = Open3.capture2(%Q{pygmentize #{lexer} 2> /dev/null}, :stdin_data=>m[3]) if s.success? hilite = hilite.split(/\n/).map{|l| "#{c([:x,:black])}~ #{xc}" + l}.join("\n") end rescue => e @log.error(e) hilite = m[0] end else hilite = m[3].split(/\n/).map{|l| new_code_line = l.gsub(/\t/,' ') orig_length = new_code_line.size + 3 new_code_line.gsub!(/ /,"#{c([:x,:white,:on_black])} ") "#{c([:x,:black])}~ #{c([:x,:white,:on_black])} " + new_code_line + c([:x,:white,:on_black]) + xc }.join("\n") end "#{c([:x,:magenta])}#{leader}\n#{hilite}#{xc}" end # remove empty links input.gsub!(/\[(.*?)\]\(\s*?\)/, '\1') input.gsub!(/\[(.*?)\]\[\]/, '[\1][\1]') lines = input.split(/\n/) # previous_indent = 0 lines.map!.with_index do |aLine, i| line = aLine.dup clean_line = line.dup.uncolor if clean_line.uncolor =~ /(^[%~])/ # || clean_line.uncolor =~ /^( {4,}|\t+)/ ## TODO: find indented code blocks and prevent highlighting ## Needs to miss block indented 1 level in lists ## Needs to catch lists in code ## Needs to avoid within fenced code blocks # if line =~ /^([ \t]+)([^*-+]+)/ # indent = $1.gsub(/\t/, " ").size # if indent >= previous_indent # line = "~" + line # end # p [indent, previous_indent] # previous_indent = indent # end else # Headlines line.gsub!(/^(#+) *(.*?)(\s*#+)?\s*$/) do |match| m = Regexp.last_match pad = "" ansi = '' case m[1].length when 1 ansi = c([:b, :black, :on_intense_white]) pad = c([:b,:white]) pad += m[2].length + 2 > @cols ? "*"*m[2].length : "*"*(@cols - (m[2].length + 2)) when 2 ansi = c([:b, :green, :on_black]) pad = c([:b,:black]) pad += m[2].length + 2 > @cols ? "-"*m[2].length : "-"*(@cols - (m[2].length + 2)) when 3 ansi = c([:u, :b, :yellow]) when 4 ansi = c([:x, :u, :yellow]) else ansi = c([:b, :white]) end "\n#{xc}#{ansi}#{m[2]} #{pad}#{xc}\n" end # place footnotes under paragraphs that reference them if line =~ /\[(?:\e\[[\d;]+m)*\^(?:\e\[[\d;]+m)*(\S+)(?:\e\[[\d;]+m)*\]/ key = $1.uncolor if @footnotes.key? key line += "\n\n#{c([:b,:black,:on_black])}[#{c([:b,:cyan,:on_black])}^#{c([:x,:yellow,:on_black])}#{key}#{c([:b,:black,:on_black])}]: #{c([:u,:white,:on_black])}#{@footnotes[key]}#{xc}" @footnotes.delete(key) end end # color footnote references line.gsub!(/\[\^(\S+)\]/) do |m| match = Regexp.last_match last = find_color(match.pre_match, true) counter = i while last.nil? && counter > 0 counter -= 1 find_color(lines[counter]) end "#{c([:b,:black])}[#{c([:b,:yellow])}^#{c([:x,:yellow])}#{match[1]}#{c([:b,:black])}]" + (last ? last : xc) end # blockquotes line.gsub!(/^(\s*>)+( .*?)?$/) do |m| match = Regexp.last_match last = find_color(match.pre_match, true) counter = i while last.nil? && counter > 0 counter -= 1 find_color(lines[counter]) end "#{c([:b,:black])}#{match[1]}#{c([:x,:magenta])} #{match[2]}" + (last ? last : xc) end # make reference links inline line.gsub!(/(? 0 counter -= 1 find_color(lines[counter]) end "#{match[1]}#{c([:b])}#{match[2]}" + (last ? last : xc) end # italic line.gsub!(/(^|\s)[\*_]([^\*_\s][^\*_]+?[^\*_\s])[\*_]/) do |m| match = Regexp.last_match last = find_color(match.pre_match, true) counter = i while last.nil? && counter > 0 counter -= 1 find_color(lines[counter]) end "#{match[1]}#{c([:u])}#{match[2]}" + (last ? last : xc) end # equations line.gsub!(/((\\\\\[)(.*?)(\\\\\])|(\\\\\()(.*?)(\\\\\)))/) do |m| match = Regexp.last_match last = find_color(match.pre_match) if match[2] brackets = [match[2], match[4]] equat = match[3] else brackets = [match[5], match[7]] equat = match[6] end "#{c([:b, :black])}#{brackets[0]}#{xc}#{c([:b,:blue])}#{equat}#{c([:b, :black])}#{brackets[1]}" + (last ? last : xc) end # list items # TODO: Fix ordered list numbering, pad numbers based on total number of list items line.gsub!(/^(\s*)([*\-+]|\d+\.) /) do |m| match = Regexp.last_match last = find_color(match.pre_match) indent = match[1] || '' "#{indent}#{c([:d, :red])}#{match[2]} " + (last ? last : xc) end # definition lists line.gsub!(/^(:\s*)(.*?)/) do |m| match = Regexp.last_match "#{c([:d, :red])}#{match[1]} #{c([:b, :white])}#{match[2]}#{xc}" end # misc html line.gsub!(//, "\n") line.gsub!(/(?i-m)((<\/?)(\w+[\s\S]*?)(>))/) do |tag| match = Regexp.last_match last = find_color(match.pre_match) "#{c([:d,:black])}#{match[2]}#{c([:b,:black])}#{match[3]}#{c([:d,:black])}#{match[4]}" + (last ? last : xc) end end line end input = lines.join("\n") # images input.gsub!(/^(.*?)!\[(.*)?\]\((.*?\.(?:png|gif|jpg))( +.*)?\)/) do |m| match = Regexp.last_match if match[1].uncolor =~ /^( {4,}|\t)+/ match[0] else tail = match[4].nil? ? '' : " "+match[4].strip result = nil if exec_available('imgcat') && @options[:local_images] if match[3] img_path = match[3] if img_path =~ /^http/ && @options[:remote_images] begin res, s = Open3.capture2(%Q{curl -sS "#{img_path}" 2> /dev/null | imgcat}) if s.success? pre = match[2].size > 0 ? " #{c([:d,:blue])}[#{match[2].strip}]\n" : '' post = tail.size > 0 ? "\n #{c([:b,:blue])}-- #{tail} --" : '' result = pre + res + post end rescue => e @log.error(e) end else if img_path =~ /^[~\/]/ img_path = File.expand_path(img_path) elsif @file base = File.expand_path(File.dirname(@file)) img_path = File.join(base,img_path) end if File.exist?(img_path) pre = match[2].size > 0 ? " #{c([:d,:blue])}[#{match[2].strip}]\n" : '' post = tail.size > 0 ? "\n #{c([:b,:blue])}-- #{tail} --" : '' img = %x{imgcat "#{img_path}"} result = pre + img + post end end end end if result.nil? match[1] + color_image(match.pre_match, match[2], match[3] + tail) + xc else match[1] + result + xc end end end @footnotes.each {|t, v| input += "\n\n#{c([:b,:black,:on_black])}[#{c([:b,:yellow,:on_black])}^#{c([:x,:yellow,:on_black])}#{t}#{c([:b,:black,:on_black])}]: #{c([:u,:white,:on_black])}#{v}#{xc}" } @output += input end end end