# frozen_string_literal: true class HTMLPipeline # Base class for user content HTML filters. Each filter takes an # HTML string, performs modifications on it, and/or writes information to a result hash. # Filters must return a String with HTML markup. # # The `context` Hash passes options to filters and should not be changed in # place. A `result` Hash allows filters to make extracted information # available to the caller, and is mutable. # # Common context options: # :base_url - The site's base URL # :repository - A Repository providing context for the HTML being processed # # Each filter may define additional options and output values. See the class # docs for more info. class Filter class InvalidDocumentException < StandardError; end def initialize(context: {}, result: {}) @context = context @result = result validate end # Public: Returns a simple Hash used to pass extra information into filters # and also to allow filters to make extracted information available to the # caller. attr_reader :context # Public: Returns a Hash used to allow filters to pass back information # to callers of the various Pipelines. This can be used for # #mentioned_users, for example. attr_reader :result # The main filter entry point. The doc attribute is guaranteed to be a # string when invoked. Subclasses should modify # this text in place or extract information and add it to the context # hash. def call raise NoMethodError end class << self # Perform a filter on doc with the given context. # # Returns a String comprised of HTML markup. def call(input, context: {}) raise NoMethodError end end # Make sure the context has everything we need. Noop: Subclasses can override. def validate; end # The site's base URL provided in the context hash, or '/' when no # base URL was specified. def base_url context[:base_url] || "/" end # Helper method for filter subclasses used to determine if any of a node's # ancestors have one of the tag names specified. # # node - The Node object to check. # tags - An array of tag name strings to check. These should be downcase. # # Returns true when the node has a matching ancestor. def has_ancestor?(element, ancestor) ancestors = element.ancestors ancestors.include?(ancestor) end # Validator for required context. This will check that anything passed in # contexts exists in @contexts # # If any errors are found an ArgumentError will be raised with a # message listing all the missing contexts and the filters that # require them. def needs(*keys) missing = keys.reject { |key| context.include?(key) } return unless missing.any? raise ArgumentError, "Missing context keys for #{self.class.name}: #{missing.map(&:inspect).join(", ")}" end end end