require "loofah" require "oembed" # TODO: Not sure we need this just for the convenience of content_tag, etc. require "sinatra" require "padrino-helpers" module Vapid # Base class for directives class Directive include Padrino::Helpers::TagHelpers attr_reader :subject # Allow iframe in sanitizer # @todo Let individual directives request tags/attributes Loofah::HTML5::WhiteList::ALLOWED_ELEMENTS_WITH_LIBXML2.merge %w(iframe) Loofah::HTML5::WhiteList::ALLOWED_ATTRIBUTES.merge %w(frameborder scrolling allowfullscreen) class << self # Whether the directive writes content to the database. # # @return [Boolean] def modifies_content? respond_to?(:form_field) end # Whether the directive previews the content. # # @return [Boolean] def previewable? preview end # Whether the directive can be destroyed (usually via a checkbox). # # @return [Boolean] def destroyable? destroy end private attr_accessor :filters, :preview, :destroy def renders(content_type, &block) define_input_modifier "render_#{content_type}", &block end def form(preview: false, destroy: false, &block) self.preview = preview self.destroy = destroy (class << self; self; end).instance_eval do define_method :form_field do |template, name, value| template.instance_exec name, value, &block end end end def filter(name, &block) @filters ||= {} @filters[name] = block end def before_render(&block) define_input_modifier :callback_before_render, &block end def inherited(subclass) # Auto-register directives # @todo Shouldn't do this Vapid.register_directive subclass.name.split("::").last.downcase, subclass end def define_input_modifier(name, &block) define_method name do if block_given? instance_exec @input, &block else @input end end private name end end def initialize(expression) parse_expression(expression) end # Renders content # @param input [String] # @return [Object] a Nokogiri-friendly hash of modifiers, # or the original input value def render(input = nil) @input = input run_before_callback apply_filters render_all end private # rubocop:disable Style/EmptyLineBetweenDefs def render_text; end def render_html; end def render_styles; end def render_attributes; end # rubocop:enable Style/EmptyLineBetweenDefs def render_all { content: render_text, inner_html: sanitize(render_html), styles: render_styles, attributes: render_attributes, placeholder: placeholder? }.reject { |_k, v| v.blank? } end def run_before_callback return unless respond_to?(:callback_before_render, true) @input = callback_before_render end def apply_filters @input = @args.reduce(@input) do |input, (name, args)| begin filter = self.class.send(:filters)[name.to_sym] instance_exec input, *args, &filter rescue input end end end def parse_expression(expression) parts = expression.split("|") @subject = parts.shift @args = parts.map do |arg| arg_args = arg.split(":", 2).map { |a| clean_arg(a) } arg_name = arg_args.shift [arg_name, arg_args] end end def clean_arg(arg) quoted = %w('' "").include? "#{arg[0]}#{arg[-1]}" quoted ? arg[1..-2] : arg end def sanitize(input) return if input.blank? Loofah.fragment(input).scrub!(:strip).to_s end def placeholder? @input.blank? && self.class.modifies_content? end def block_is_template?(_block) false end end end