diff options
Diffstat (limited to 'app/lib/formatter.rb')
-rw-r--r-- | app/lib/formatter.rb | 104 |
1 files changed, 95 insertions, 9 deletions
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 2c5674869..f1a751f84 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -3,6 +3,27 @@ require 'singleton' require_relative './sanitize_config' +class HTMLRenderer < Redcarpet::Render::HTML + def block_code(code, language) + "<pre><code>#{encode(code).gsub("\n", "<br/>")}</code></pre>" + end + + def autolink(link, link_type) + return link if link_type == :email + Formatter.instance.link_url(link) + end + + private + + def html_entities + @html_entities ||= HTMLEntities.new + end + + def encode(html) + html_entities.encode(html) + end +end + class Formatter include Singleton include RoutingHelper @@ -36,14 +57,24 @@ class Formatter html = raw_content html = "RT @#{prepend_reblog} #{html}" if prepend_reblog - html = encode_and_link_urls(html, linkable_accounts) + html = format_markdown(html) if status.content_type == 'text/markdown' + html = encode_and_link_urls(html, linkable_accounts, keep_html: %w(text/markdown text/html).include?(status.content_type)) + html = reformat(html) if %w(text/markdown text/html).include?(status.content_type) html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify] - html = simple_format(html, {}, sanitize: false) - html = html.delete("\n") + + unless %w(text/markdown text/html).include?(status.content_type) + html = simple_format(html, {}, sanitize: false) + html = html.delete("\n") + end html.html_safe # rubocop:disable Rails/OutputSafety end + def format_markdown(html) + html = markdown_formatter.render(html) + html.delete("\r").delete("\n") + end + def reformat(html) sanitize(html, Sanitize::Config::MASTODON_STRICT) end @@ -97,8 +128,40 @@ class Formatter html.html_safe # rubocop:disable Rails/OutputSafety end + def link_url(url) + "<a href=\"#{encode(url)}\" target=\"blank\" rel=\"nofollow noopener\">#{link_html(url)}</a>" + end + private + def markdown_formatter + extensions = { + autolink: true, + no_intra_emphasis: true, + fenced_code_blocks: true, + disable_indented_code_blocks: true, + strikethrough: true, + lax_spacing: true, + space_after_headers: true, + superscript: true, + underline: true, + highlight: true, + footnotes: false, + } + + renderer = HTMLRenderer.new({ + filter_html: false, + escape_html: false, + no_images: true, + no_styles: true, + safe_links_only: true, + hard_wrap: true, + link_attributes: { target: '_blank', rel: 'nofollow noopener' }, + }) + + Redcarpet::Markdown.new(renderer, extensions) + end + def html_entities @html_entities ||= HTMLEntities.new end @@ -108,14 +171,14 @@ class Formatter end def encode_and_link_urls(html, accounts = nil, options = {}) - entities = utf8_friendly_extractor(html, extract_url_without_protocol: false) - if accounts.is_a?(Hash) options = accounts accounts = nil end - rewrite(html.dup, entities) do |entity| + entities = options[:keep_html] ? html_friendly_extractor(html) : utf8_friendly_extractor(html, extract_url_without_protocol: false) + + rewrite(html.dup, entities, options[:keep_html]) do |entity| if entity[:url] link_to_url(entity, options) elsif entity[:hashtag] @@ -188,7 +251,7 @@ class Formatter html end - def rewrite(text, entities) + def rewrite(text, entities, keep_html = false) text = text.to_s # Sort by start index @@ -201,12 +264,12 @@ class Formatter last_index = entities.reduce(0) do |index, entity| indices = entity.respond_to?(:indices) ? entity.indices : entity[:indices] - result << encode(text[index...indices.first]) + result << (keep_html ? text[index...indices.first] : encode(text[index...indices.first])) result << yield(entity) indices.last end - result << encode(text[last_index..-1]) + result << (keep_html ? text[last_index..-1] : encode(text[last_index..-1])) result.flatten.join end @@ -250,6 +313,29 @@ class Formatter Extractor.remove_overlapping_entities(special + standard + extra) end + def html_friendly_extractor(html, options = {}) + gaps = [] + total_offset = 0 + + escaped = html.gsub(/<[^>]*>|&#[0-9]+;/) do |match| + total_offset += match.length - 1 + end_offset = Regexp.last_match.end(0) + gaps << [end_offset - total_offset, total_offset] + "\u200b" + end + + entities = Extractor.extract_hashtags_with_indices(escaped, :check_url_overlap => false) + + Extractor.extract_mentions_or_lists_with_indices(escaped) + Extractor.remove_overlapping_entities(entities).map do |extract| + pos = extract[:indices].first + offset_idx = gaps.rindex { |gap| gap.first <= pos } + offset = offset_idx.nil? ? 0 : gaps[offset_idx].last + next extract.merge( + :indices => [extract[:indices].first + offset, extract[:indices].last + offset] + ) + end + end + def link_to_url(entity, options = {}) url = Addressable::URI.parse(entity[:url]) html_attrs = { target: '_blank', rel: 'nofollow noopener noreferrer' } |