lib/troy/markdown.rb
# frozen_string_literal: true module Troy class Markdown # Match the id portion of a header, as in `# Title {#custom-id}`. HEADING_ID = /^(?<text>.*?)(?: {#(?<id>.*?)})?$/ # Create a new Redcarpet renderer, that prepares the code block # to use Prisme.js syntax. # module PrismJs def block_code(code, language) code = CGI.escapeHTML(code) %[<pre class="language-#{language}"><code>#{code}</code></pre>] end end # Create a new Redcarpet renderer, that prepares the code block # to use rouge syntax. # module Rouge include ::Rouge::Plugins::Redcarpet # Be more flexible than github and support any arbitrary name. ALERT_MARK = /^\[!(?<type>[A-Z]+)\](?<title>.*?)?$/ # Support alert boxes just like github. # https://github.com/orgs/community/discussions/16925 def block_quote(quote) html = Nokogiri::HTML.fragment(quote) element = html.children.first matches = element.text.to_s.match(ALERT_MARK) if element return "<blockquote>#{quote}</blockquote>" unless matches element.remove type = matches[:type].downcase title = matches[:title].to_s.strip title = I18n.t(type, scope: :alerts, default: title) html = Nokogiri::HTML.fragment <<~HTML <div class="alert-message #{type}"> <p class="alert-message--title"></p> #{html} </div> HTML if title.empty? html.css(".alert-message--title").first.remove else html.css(".alert-message--title").first.content = title end html.to_s end def header(text, level) matches = text.strip.match(HEADING_ID) title = matches[:text].strip html = Nokogiri::HTML.fragment("<h#{level}>#{title}</h#{level}>") heading = html.first_element_child title = heading.text id = matches[:id] id ||= permalink(title) heading_counter[id] += 1 id = "#{id}-#{heading_counter[id]}" if heading_counter[id] > 1 heading.add_child %[<a class="anchor" href="##{id}" aria-hidden="true" tabindex="-1"></a>] # rubocop:disable Style/LineLength heading.set_attribute :tabindex, "-1" heading.set_attribute(:id, id) heading.to_s end def permalink(text) str = text.dup.unicode_normalize(:nfkd) str = str.gsub(/[^\x00-\x7F]/, "").to_s str.gsub!(/[^-\w]+/xim, "-") str.gsub!(/-+/xm, "-") str.gsub!(/^-?(.*?)-?$/, '\1') str.downcase! str end def heading_counter @heading_counter ||= Hash.new {|h, k| h[k] = 0 } end end class Renderer < Redcarpet::Render::HTML include Redcarpet::Render::HTMLAbbreviations include Redcarpet::Render::SmartyPants include Rouge end # Set the Markdown markup that must be rendered. # attr_reader :markup def initialize(markup) @markup = markup end def renderer @renderer ||= Redcarpet::Markdown.new(Renderer, autolink: true, space_after_headers: true, fenced_code_blocks: true, footnotes: true, tables: true, strikethrough: true, highlight: true) end def to_html renderer.render(markup) end end end