# frozen_string_literal: true module Thredded # Generates HTML from content source. class ContentFormatter class << self # Sanitization whitelist options. attr_accessor :whitelist # Filters that run before processing the markup. # input: markup, output: markup. attr_accessor :before_markup_filters # Markup filters, such as BBCode, Markdown, Autolink, etc. # input: markup, output: html. attr_accessor :markup_filters # Filters that run after processing the markup. # input: html, output: html. attr_accessor :after_markup_filters # Filters that sanitize the resulting HTML. # input: html, output: sanitized html. attr_accessor :sanitization_filters # Filters that run after sanitization # input: sanitized html, output: html attr_accessor :after_sanitization_filters end self.whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST.deep_merge( elements: HTML::Pipeline::SanitizationFilter::WHITELIST[:elements] + %w[abbr iframe span figure figcaption], transformers: HTML::Pipeline::SanitizationFilter::WHITELIST[:transformers] + [ ->(env) { next unless env[:node_name] == 'a' a_tag = env[:node] a_tag['href'] ||= '#' if a_tag['href'] =~ %r{^(?:[a-z]+:)?//} a_tag['target'] = '_blank' a_tag['rel'] = 'nofollow noopener' end } ], attributes: { 'a' => %w[href rel], 'abbr' => %w[title], 'span' => %w[class], 'div' => %w[class], 'img' => %w[src longdesc class], :all => HTML::Pipeline::SanitizationFilter::WHITELIST[:attributes][:all] + %w[aria-expanded aria-label aria-labelledby aria-live aria-hidden aria-pressed role], } ) self.before_markup_filters = [ Thredded::HtmlPipeline::SpoilerTagFilter::BeforeMarkup ] self.markup_filters = [ Thredded::HtmlPipeline::KramdownFilter, ] self.after_markup_filters = [ # AutolinkFilter is required because Kramdown does not autolink by default. # https://github.com/gettalong/kramdown/issues/306 Thredded::HtmlPipeline::AutolinkFilter, Thredded::HtmlPipeline::AtMentionFilter, Thredded::HtmlPipeline::SpoilerTagFilter::AfterMarkup, ] self.sanitization_filters = [ HTML::Pipeline::SanitizationFilter, ] self.after_sanitization_filters = [ Thredded::HtmlPipeline::OneboxFilter, Thredded::HtmlPipeline::WrapIframesFilter, ] # All the HTML::Pipeline filters, read-only. def self.pipeline_filters filters = [ *before_markup_filters, *markup_filters, *after_markup_filters, *sanitization_filters, *after_sanitization_filters ] # Changing the result in-place has no effect on the ContentFormatter output, # and is most likely the result of a programmer error. # Freeze the array so that in-place changes raise an error. filters.freeze end # @param view_context [Object] the context of the rendering view. # @param pipeline_options [Hash] def initialize(view_context, pipeline_options = {}) @view_context = view_context @pipeline_options = pipeline_options end # @param content [String] # @return [String] formatted and sanitized html-safe content. def format_content(content) pipeline = HTML::Pipeline.new(content_pipeline_filters, content_pipeline_options.deep_merge(@pipeline_options)) result = pipeline.call(content, view_context: @view_context) # rubocop:disable Rails/OutputSafety result[:output].to_s.html_safe # rubocop:enable Rails/OutputSafety end # @param content [String] # @return [String] a quote containing the formatted content def self.quote_content(content) result = String.new(content) result.gsub!(/^(?!$)/, '> ') result.gsub!(/^$/, '>') result << "\n" unless result.end_with?("\n") result << "\n" result end protected # @return [Array] def content_pipeline_filters ContentFormatter.pipeline_filters end # @return [Hash] options for HTML::Pipeline.new def content_pipeline_options { asset_root: Rails.application.config.action_controller.asset_host || '', whitelist: ContentFormatter.whitelist } end end end