Spike potential redcarpet replacement
This commit is contained in:
+27
-20
@@ -1,33 +1,40 @@
|
||||
require "rouge/plugins/redcarpet"
|
||||
class MarkdownRenderer
|
||||
require_relative "markdown_renderer/parsing"
|
||||
require_relative "markdown_renderer/markup"
|
||||
|
||||
class MarkdownRenderer < Redcarpet::Render::HTML
|
||||
include Rouge::Plugins::Redcarpet
|
||||
include Parsing, Markup
|
||||
|
||||
def self.build
|
||||
renderer = MarkdownRenderer.new(ActionText::Markdown::DEFAULT_RENDERER_OPTIONS)
|
||||
Redcarpet::Markdown.new(renderer, ActionText::Markdown::DEFAULT_MARKDOWN_EXTENSIONS)
|
||||
end
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
def initialize
|
||||
@id_counts = Hash.new(0)
|
||||
end
|
||||
|
||||
def header(text, header_level)
|
||||
unique_id(text).then do |id|
|
||||
"<h#{header_level} id='#{id}'>#{text} <a href='##{id}' class='heading__link' aria-hidden='true'>#</a></h#{header_level}>"
|
||||
end
|
||||
end
|
||||
|
||||
def image(url, title, alt_text)
|
||||
%(<a title="#{title}" data-action="lightbox#open:prevent" data-lightbox-target="image" data-lightbox-url-value="#{url}?disposition=attachment" href="#{url}"><img src="#{url}" alt="#{alt_text}"></a>)
|
||||
def render(content)
|
||||
content
|
||||
.then { |c| parse_paragraphs(c) }
|
||||
.then { |c| parse_bold_italics(c) }
|
||||
.then { |c| parse_bold(c) }
|
||||
.then { |c| parse_italics(c) }
|
||||
.then { |c| parse_strikethrough(c) }
|
||||
.then { |c| parse_highlight(c) }
|
||||
.then { |c| parse_headers(c) }
|
||||
.then { |c| parse_tables(c) }
|
||||
.then { |c| parse_ordered_lists(c) }
|
||||
.then { |c| parse_unordered_lists(c) }
|
||||
.then { |c| parse_block_quotes(c) }
|
||||
.then { |c| parse_horizontal_rules(c) }
|
||||
.then { |c| parse_images(c) }
|
||||
.then { |c| parse_links(c) }
|
||||
.then { |c| parse_code_blocks(c) }
|
||||
.then { |c| parse_code_spans(c) }
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :id_counts
|
||||
|
||||
def unique_id(text)
|
||||
text.parameterize.then do |base_id|
|
||||
@id_counts[base_id] += 1
|
||||
@id_counts[base_id] > 1 ? "#{base_id}-#{@id_counts[base_id]}" : base_id
|
||||
id_counts[base_id] += 1
|
||||
id_counts[base_id] > 1 ? "#{base_id}-#{id_counts[base_id]}" : base_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
require "rouge/plugins/redcarpet"
|
||||
|
||||
module MarkdownRenderer::Markup
|
||||
include Rouge::Plugins::Redcarpet
|
||||
|
||||
TABLE_ROW_INDENT = " " * 4
|
||||
LIST_ITEM_INDENT = " " * 2
|
||||
BLOCK_QUOTE_INDENT = " " * 2
|
||||
TABLE_HEADER_INDENT = " " * 6
|
||||
|
||||
# FIXME: the attributes suggest this should be an app-level override instead
|
||||
def header(text, header_level)
|
||||
unique_id(text).then do |id|
|
||||
<<~HTML.chomp
|
||||
<h#{header_level} id="#{id}">
|
||||
#{text} <a href="##{id}" class="heading__link" aria-hidden="true">#</a>
|
||||
</h#{header_level}>
|
||||
HTML
|
||||
end
|
||||
end
|
||||
|
||||
def bold(text)
|
||||
"<strong>#{text}</strong>"
|
||||
end
|
||||
|
||||
def italics(text)
|
||||
"<em>#{text}</em>"
|
||||
end
|
||||
|
||||
def strikethrough(text)
|
||||
"<s>#{text}</s>"
|
||||
end
|
||||
|
||||
def highlight(text)
|
||||
"<mark>#{text}</mark>"
|
||||
end
|
||||
|
||||
def table(headers, rows)
|
||||
<<~HTML
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
#{headers.map { |header| "<th>#{header}</th>" }.join("\n" + TABLE_HEADER_INDENT)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
#{rows.map { |row| "<tr>#{row.map { |cell| "<td>#{cell}</td>" }.join}</tr>" }.join("\n" + TABLE_ROW_INDENT)}
|
||||
</tbody>
|
||||
</table>
|
||||
HTML
|
||||
end
|
||||
|
||||
def ordered_list(contents)
|
||||
<<~HTML
|
||||
<ol>
|
||||
#{contents.split("\n").join("\n" + LIST_ITEM_INDENT)}
|
||||
</ol>
|
||||
HTML
|
||||
end
|
||||
|
||||
def unordered_list(contents)
|
||||
<<~HTML
|
||||
<ul>
|
||||
#{contents.split("\n").join("\n" + LIST_ITEM_INDENT)}
|
||||
</ul>
|
||||
HTML
|
||||
end
|
||||
|
||||
def list_item(text)
|
||||
"<li>#{text}</li>"
|
||||
end
|
||||
|
||||
def block_quote(text)
|
||||
<<~HTML
|
||||
<blockquote>
|
||||
#{text.gsub(/^>\s?/, '').split("\n").join("\n" + BLOCK_QUOTE_INDENT)}
|
||||
</blockquote>
|
||||
HTML
|
||||
end
|
||||
|
||||
def horizontal_rule
|
||||
"<hr>"
|
||||
end
|
||||
|
||||
def link(text, href)
|
||||
"<a href=\"#{href}\">#{text}</a>"
|
||||
end
|
||||
|
||||
def code_block(code, language)
|
||||
block_code(code, language) # call Rouge Redcarpet plugin
|
||||
end
|
||||
|
||||
def code_span(text)
|
||||
"<code>#{text}</code>"
|
||||
end
|
||||
|
||||
# FIXME: the attributes suggest this should be an app-level override instead
|
||||
def image(url, alt_text)
|
||||
<<~HTML.chomp
|
||||
<a href="#{url}" data-action="lightbox#open:prevent" data-lightbox-target="image" data-lightbox-url-value="#{url}?disposition=attachment">
|
||||
<img src="#{url}" alt="#{alt_text}">
|
||||
</a>
|
||||
HTML
|
||||
end
|
||||
|
||||
def paragraph(text)
|
||||
"<p>#{text}</p>"
|
||||
end
|
||||
|
||||
def soft_line_break
|
||||
"\n#{line_break}\n"
|
||||
end
|
||||
|
||||
def line_break
|
||||
"<br>"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,135 @@
|
||||
module MarkdownRenderer::Parsing
|
||||
HR = /^---$/
|
||||
INLINE_HTML_BLOCK_START = /^</
|
||||
HEADING_START = /^#/
|
||||
CODE_BLOCK_START = /^```/
|
||||
|
||||
NEWLINE_OR_EOF = /\n|$/
|
||||
SOFT_LINE_BREAK = /(?<=[^\n])\n(?=[^\n])/
|
||||
|
||||
NUMBERED_LIST_START = /^\d+\.\s/
|
||||
NUMBERED_LIST_ITEM = /#{NUMBERED_LIST_START}[^\n]*#{NEWLINE_OR_EOF}/
|
||||
|
||||
UNORDERED_LIST_START = /^-\s/
|
||||
UNORDERED_LIST_ITEM = /#{UNORDERED_LIST_START}[^\n]*#{NEWLINE_OR_EOF}/
|
||||
|
||||
BLOCK_QUOTE_START = /^>\s?/
|
||||
BLOCK_QUOTE_CONTENT = /#{BLOCK_QUOTE_START}[^\n]*#{NEWLINE_OR_EOF}/
|
||||
|
||||
TABLE_START = /^\|/
|
||||
TABLE_HEADER_ROW = /^\|(.+)\|/
|
||||
TABLE_SEPARATOR = /^\|[-|]+\|/
|
||||
TABLE_DATA_ROW = /^\|.+\|/
|
||||
TABLE = /#{TABLE_HEADER_ROW}\r?\n#{TABLE_SEPARATOR}\r?\n((?:#{TABLE_DATA_ROW}\r?\n)*)/
|
||||
|
||||
BLOCK_STARTERS = Regexp.union [
|
||||
HR,
|
||||
TABLE_START,
|
||||
HEADING_START,
|
||||
CODE_BLOCK_START,
|
||||
BLOCK_QUOTE_START,
|
||||
NUMBERED_LIST_START,
|
||||
UNORDERED_LIST_START,
|
||||
INLINE_HTML_BLOCK_START ]
|
||||
|
||||
private
|
||||
def parse_paragraphs(content)
|
||||
content.split(/\n\n+/).map do |text|
|
||||
if text.match?(BLOCK_STARTERS)
|
||||
text
|
||||
else
|
||||
paragraph text.gsub(SOFT_LINE_BREAK, soft_line_break)
|
||||
end
|
||||
end.join("\n\n")
|
||||
end
|
||||
|
||||
def parse_bold_italics(content)
|
||||
transform = ->(match) { bold(italics(match)) }
|
||||
|
||||
content
|
||||
.gsub(/(?<!\*)\*\*\*([^*]+)\*\*\*(?!\*)/) { transform.($1) }
|
||||
.gsub(/(?<!_)___([^_]+)___(?!_)/) { transform.($1) }
|
||||
end
|
||||
|
||||
def parse_bold(content)
|
||||
transform = ->(match) { bold(match) }
|
||||
|
||||
content
|
||||
.gsub(/(?<!\*)\*\*([^*]+)\*\*(?!\*)/) { transform.($1) }
|
||||
.gsub(/(?<!_)__([^_]+)__(?!_)/) { transform.($1) }
|
||||
end
|
||||
|
||||
def parse_italics(content)
|
||||
transform = ->(match) { italics(match) }
|
||||
|
||||
content
|
||||
.gsub(/(?<!\*)\*([^*]+)\*(?!\*)/) { transform.($1) }
|
||||
.gsub(/(?<!_)_([^_]+)_(?!_)/) { transform.($1) }
|
||||
end
|
||||
|
||||
def parse_strikethrough(content)
|
||||
content.gsub(/~~(.*?)~~/) { strikethrough($1) }
|
||||
end
|
||||
|
||||
def parse_highlight(content)
|
||||
content.gsub(/==(.*?)==/) { highlight($1) }
|
||||
end
|
||||
|
||||
def parse_headers(content)
|
||||
content
|
||||
.gsub(/^# (.*)$/) { header($1, 1) }
|
||||
.gsub(/^## (.*)$/) { header($1, 2) }
|
||||
.gsub(/^### (.*)$/) { header($1, 3) }
|
||||
.gsub(/^#### (.*)$/) { header($1, 4) }
|
||||
.gsub(/^##### (.*)$/) { header($1, 5) }
|
||||
.gsub(/^###### (.*)$/) { header($1, 6) }
|
||||
end
|
||||
|
||||
def parse_tables(content)
|
||||
content.gsub(TABLE) do
|
||||
headers = $1.split("|").map(&:strip).compact_blank
|
||||
rows = $2.split("\n").map { |row| row.split("|").map(&:strip).compact_blank unless row.blank? }.compact
|
||||
table(headers, rows) if rows.map(&:size).all?(headers.size)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_ordered_lists(content)
|
||||
content.gsub(/(?:#{NUMBERED_LIST_ITEM})+/) { |list| ordered_list(parse_ordered_list_items(list.strip)) }
|
||||
end
|
||||
|
||||
def parse_ordered_list_items(content)
|
||||
content.gsub(/#{NUMBERED_LIST_START}(.*)$/) { list_item($1) }
|
||||
end
|
||||
|
||||
def parse_unordered_lists(content)
|
||||
content.gsub(/(?:#{UNORDERED_LIST_ITEM})+/) { |list| unordered_list(parse_unordered_list_items(list.strip)) }
|
||||
end
|
||||
|
||||
def parse_unordered_list_items(content)
|
||||
content.gsub(/#{UNORDERED_LIST_START}(.*)$/) { list_item($1) }
|
||||
end
|
||||
|
||||
def parse_block_quotes(content)
|
||||
content.gsub(/(?:#{BLOCK_QUOTE_CONTENT})+/) { |text| block_quote(text.strip) }
|
||||
end
|
||||
|
||||
def parse_horizontal_rules(content)
|
||||
content.gsub(HR) { horizontal_rule }
|
||||
end
|
||||
|
||||
def parse_images(content)
|
||||
content.gsub(/!\[(.*?)\]\((.*?)\)/) { image($2, $1) }
|
||||
end
|
||||
|
||||
def parse_links(content)
|
||||
content.gsub(/\[(.*?)\]\((.*?)\)/) { link($1, $2) }
|
||||
end
|
||||
|
||||
def parse_code_blocks(content)
|
||||
content.gsub(/#{CODE_BLOCK_START}(\w*)\n(.*?)```$/m) { code_block($2, $1) }
|
||||
end
|
||||
|
||||
def parse_code_spans(content)
|
||||
content.gsub(/`([^`\n]+)`/) { code_span($1) }
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user