# frozen_string_literal: true require "cgi" HTML::Pipeline.require_dependency("negarmoji", "NegarehEmojiFilter") module HTML class Pipeline class NegarMojiHtmlPipeline # HTML filter that replaces :emoji: with images. # # Context: # :asset_root (required) - base url to link to emoji sprite. # # :asset_path (optional) - url path to link to emoji sprite. :file_name can be used as # a placeholder for the sprite file name. # If no asset_path is set ":file_name" is used. # # :extension (optional) - extension to be use for emoji files, default extension is svg. # # :ignored_ancestor_tags (optional) - Tags to stop the emojification. Node has matched # ancestor HTML tags will not be emojified. Default to pre, code, and tt tags. Extra tags # please pass in the form of array, e.g., %w(blockquote summary). # # :img_attrs (optional) - Attributes for generated img tag. # E.g. Pass { "draggble" => true, "height" => nil } to set draggable attribute to "true" # and clear height attribute of generated img tag. class NegarehEmojiFilter < Filter DEFAULT_IGNORED_ANCESTOR_TAGS = %w(pre code tt).freeze def call doc.search(".//text()").each do |node| content = node.text next unless content.include?(":") next if has_ancestor?(node, ignored_ancestor_tags) html = emoji_image_filter(content) next if html == content node.replace(html) end doc end # Implementation of validate hook. # Errors should raise exceptions or use an existing validator. def validate needs :asset_root end # Replace :emoji: with corresponding images. # # text - String text to replace :emoji: in. # # Returns a String with :emoji: replaced with images. def emoji_image_filter(text) text.gsub(emoji_pattern) do |_match| emoji_image_tag(Regexp.last_match(1)) end end # The base url to link emoji sprites # # Raises ArgumentError if context option has not been provided. # Returns the context's asset_root. def asset_root context[:asset_root] end # The url path to link emoji sprites # # :file_name can be used in the asset_path as a placeholder for the sprite file name. # If no asset_path is set in the context ":file_name" is used. # Returns the context's asset_path or the default path if no context asset_path is given. def asset_path(name) if context[:asset_path] context[:asset_path].gsub(":file_name", emoji_filename(name)) else File.join(emoji_filename(name)) end end # Emoji file extension. # # Extension to be use for emoji files, default extension is svg. def emoji_extension context[:extension] || "svg" end private # Build an emoji image tag def emoji_image_tag(name) require "active_support/core_ext/hash/indifferent_access" html_attrs = default_img_attrs(name) .merge!((context[:img_attrs] || {}).with_indifferent_access) .map do |attr, value| !value.nil? && %(#{attr}="#{value.respond_to?(:call) && value.call(name) || value}") end .reject(&:blank?).join(" ") "" end # Default attributes for img tag def default_img_attrs(name) { :class => "emoji", :title => ":#{name}:", :alt => ":#{name}:", :src => emoji_url(name).to_s, :height => "20", :width => "20", :align => "absmiddle", } end def emoji_url(name) File.join(asset_root, asset_path(name)) end def emoji_pattern self.class.emoji_pattern end def emoji_filename(name) Emoji.find_by_alias(name).image_filename(emoji_extension) end # Return ancestor tags to stop the emojification. # # @return [Array] Ancestor tags. def ignored_ancestor_tags if context[:ignored_ancestor_tags] DEFAULT_IGNORED_ANCESTOR_TAGS | context[:ignored_ancestor_tags] else DEFAULT_IGNORED_ANCESTOR_TAGS end end class << self # Build a regexp that matches all valid :emoji: names. def self.emoji_pattern @emoji_pattern ||= %r!:(#{emoji_names.map { |name| Regexp.escape(name) }.join("|")}):! end def self.emoji_names Emoji.all.map(&:aliases).flatten.sort end end end end end end