# frozen_string_literal: true # A PostEdit represents a tag edit being performed on a post. It contains most # of the logic for performing a tag edit, including methods for parsing the tag # string into tags and metatags, methods for determining which tags were added # or removed by the edit, and methods for calculating the final list of tags. class PostEdit extend Memoist Tag = Struct.new(:name, :negated, keyword_init: true) Metatag = Struct.new(:name, :value, keyword_init: true) # Metatags that change the tag's category: `art:bkub`, `char:chen`, `copy:touhou`, `gen:1girl`, `meta:animated`. CATEGORIZATION_METATAGS = TagCategory.mapping.keys # Pre-metatags affect the post itself, so they must be applied before the post is saved. PRE_METATAGS = %w[parent -parent rating source] + CATEGORIZATION_METATAGS # Post-metatags rely on the post's ID, so they must be applied after the post is saved to ensure the ID has been created. POST_METATAGS = %w[newpool pool -pool favgroup -favgroup fav -fav child -child upvote downvote disapproved status -status] METATAGS = PRE_METATAGS + POST_METATAGS METATAG_NAME_REGEX = /(#{METATAGS.join("|")}):/io private attr_reader :post, :current_tag_names, :old_tag_names, :new_tag_string, :parser private delegate :accept, :expect, :error, :skip, :zero_or_more, :one_of, to: :parser # @param post [Post] The post being edited. # @param current_tag_string [String] The space-separated list of tags currently on the post. # @param old_tag_string [String] The space-separated list of tags the user saw before the edit. # @param new_tag_string [String] The space-separated list of tags after the edit. def initialize(post, current_tag_string, old_tag_string, new_tag_string) @post = post @current_tag_names = current_tag_string.to_s.split @old_tag_names = old_tag_string.to_s.split @new_tag_string = new_tag_string.to_s.gsub(/[[:space:]]/, " ") @parser = StringParser.new(@new_tag_string) end concerning :HelperMethods do # @return [Array] The final list of tags on the post after the edit. def tag_names tag_names = current_tag_names + effective_added_tag_names - user_removed_tag_names tag_names = post.add_automatic_tags(tag_names) tag_names += ::Tag.automatic_tags_for(tag_names) tag_names += TagImplication.tags_implied_by(tag_names).map(&:name) tag_names.uniq.sort end # @return [Array] The list of tags in the edited tag string, including regular tags and tags with a category prefix (e.g. `artist:bkub`) def new_tag_names tag_terms.reject(&:negated).map(&:name) + tag_categorization_terms.map(&:value) end # @return [Array] The list of tags in the edited tag string. Includes negated and non-negated tags, but not tags with a category prefix. def tag_terms terms.grep(Tag) end # @return [Array] The list of metatags in the edited tag string. def metatag_terms terms.grep(Metatag) end # @return [Array] The list of pre-save metatags in the edit (metatags that are applied before the post is saved). def pre_metatag_terms metatag_terms.select { |term| term.name.in?(PRE_METATAGS) } end # @return [Array] The list of post-save metatags in the edit (metatags that are applied after the post is saved). def post_metatag_terms metatag_terms.select { |term| term.name.in?(POST_METATAGS) } end # @return [Array] The list of tags with a category prefix (e.g. `artist:bkub`). def tag_categorization_terms metatag_terms.select { |term| term.name.in?(TagCategory.categories) } end # @return [Array] The list of tags actually added by the user, excluding invalid or deprecated tags. def effective_added_tag_names user_added_tag_names - invalid_added_tags.map(&:name) - deprecated_added_tag_names end # @return [Array] The list of tags the user is trying to add. Includes tags that won't # actually be added, such as invalid or deprecated tags. Does not include tags not explicitly # added by the user, such as implied or automatic tags. def user_added_tag_names TagAlias.to_aliased(new_tag_names - old_tag_names - user_removed_tag_names).uniq.sort end # @return [Array] The list of tags the user is trying to remove. Includes tags that # won't actually be removed, such as implied tags, automatic tags, and nonexistent tags. def user_removed_tag_names (explicit_removed_tag_names + implicit_removed_tag_names).uniq.sort end # @return [Array] The list of tags explicitly removed using the '-' operator (e.g. `-tagme`). def explicit_removed_tag_names TagAlias.to_aliased(tag_terms.select(&:negated).map(&:name)) end # @return [Array] The list of tags implicitly removed by being deleted from the tag string (e.g. `1girl tagme` => `1girl`) def implicit_removed_tag_names old_tag_names - new_tag_names end # @return [Array] The list of user-added tags that have invalid names. def invalid_added_tags user_added_tag_names.map { |name| ::Tag.new(name: name) }.select { |tag| tag.invalid?(:name) } end # @return [Array] The list of user-added tags that are deprecated. def deprecated_added_tag_names ::Tag.deprecated.where(name: user_added_tag_names).map(&:name) end end concerning :ParserMethods do # @return [Array] The list of tags and metatags in the edit. def terms zero_or_more { skip(/[[:space:]]+/); term } end private def term one_of([method(:tag), method(:metatag)]) end private def tag negated = accept("-").present? error("Invalid tag name") if accept(METATAG_NAME_REGEX) name = expect(/[^[:space:]]+/) Tag.new(name: name.downcase, negated: negated) end private def metatag name = expect(METATAG_NAME_REGEX) value = quoted_string name = name.delete_suffix(":").downcase name = TagCategory.short_name_mapping.fetch(name, name) # 'art:bkub' => 'artist:bkub' value = value.downcase unless name.in?(["newpool", "source"]) value = value.gsub(/[[:space:]]/, "_") unless name == "source" Metatag.new(name: name, value: value) end private def quoted_string if accept('"') string = expect(/(\\"|[^"])*/).gsub(/\\"/, '"') # handle backslash escaped quotes expect('"') string elsif accept("'") string = expect(/(\\'|[^'])*/).gsub(/\\'/, "'") # handle backslash escaped quotes expect("'") string else expect(/(\\ |[^ ])*/).gsub(/\\ /, " ") # handle backslash escaped spaces end end end memoize :tag_names, :new_tag_names, :user_added_tag_names, :user_removed_tag_names, :invalid_added_tags, :deprecated_added_tag_names, :terms, :tag_terms, :metatag_terms end