Spike potential redcarpet replacement

This commit is contained in:
Jose Farias
2024-12-17 00:32:58 -06:00
parent 326bdc74cf
commit 4fc7ab1582
6 changed files with 511 additions and 22 deletions
+27 -20
View File
@@ -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
+117
View File
@@ -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
+135
View File
@@ -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