diff --git a/app/logical/post_query.rb b/app/logical/post_query.rb index b6047530e..5d8a21594 100644 --- a/app/logical/post_query.rb +++ b/app/logical/post_query.rb @@ -3,14 +3,40 @@ class PostQuery extend Memoist - attr_reader :search, :parser, :builder, :ast - delegate :tag?, :metatag?, :wildcard?, :metatags, :wildcards, :tag_names, :metatags, to: :ast + private attr_reader :current_user, :tag_limit, :safe_mode, :hide_deleted_posts, :builder - def initialize(search, current_user: User.anonymous, tag_limit: nil, safe_mode: false, hide_deleted_posts: false) - @search = search - @parser = Parser.new(search) - @builder = PostQueryBuilder.new(search, current_user, tag_limit: tag_limit, safe_mode: safe_mode, hide_deleted_posts: hide_deleted_posts) - @ast = parser.parse.simplify + delegate :tag?, :metatag?, :wildcard?, :metatags, :wildcards, :tag_names, :metatags, to: :ast + alias_method :safe_mode?, :safe_mode + alias_method :hide_deleted_posts?, :hide_deleted_posts + + def initialize(search_or_ast, current_user: User.anonymous, tag_limit: nil, safe_mode: false, hide_deleted_posts: false) + if search_or_ast.is_a?(AST) + @ast = search_or_ast + else + @search = search_or_ast.to_s + end + + @current_user = current_user + @tag_limit = tag_limit + @safe_mode = safe_mode + @hide_deleted_posts = hide_deleted_posts + end + + # Build a new PostQuery from the given AST and the current settings. + def build(ast) + PostQuery.new(ast, current_user: current_user, tag_limit: tag_limit, safe_mode: safe_mode, hide_deleted_posts: hide_deleted_posts) + end + + def builder + @builder ||= PostQueryBuilder.new(search, current_user, tag_limit: tag_limit, safe_mode: safe_mode, hide_deleted_posts: hide_deleted_posts) + end + + def search + @search ||= ast.to_infix + end + + def ast + @ast ||= Parser.parse(search) end def fast_count(...) @@ -47,15 +73,47 @@ class PostQuery select_metatags(*names).first&.value end - # Return a new query AST, with aliased tags replaced with real tags. - def replace_aliases - ast.replace_tags(aliases) + # Return a new PostQuery with aliases replaced, implicit metatags added, and the query converted to conjunctive normal form. + def normalize + replace_aliases.with_implicit_metatags.to_cnf end - # A hash mapping aliased tag names to real tag names. + # Return a new PostQuery with aliases replaced. + def replace_aliases + return self if aliases.empty? + build(ast.replace_tags(aliases)) + end + + # Return a new PostQuery with implicit metatags (rating:safe and -status:deleted) added. + def with_implicit_metatags + return self if implicit_metatags.empty? + build(AST.new(:and, [ast, *implicit_metatags])) + end + + # Return a new PostQuery converted to conjunctive normal form. + def to_cnf + build(ast.to_cnf) + end + + # Return a hash mapping aliased tag names to real tag names. def aliases TagAlias.aliases_for(tag_names) end - memoize :tags, :aliases + # Implicit metatags are metatags added by the user's account settings. rating:s is implicit + # under safe mode. -status:deleted is implicit when the "hide deleted posts" setting is on. + def implicit_metatags + metatags = [] + metatags << AST.metatag("rating", "s") if safe_mode? + metatags << -AST.metatag("status", "deleted") if hide_deleted? + metatags + end + + # XXX unify with PostSets::Post#show_deleted? + def hide_deleted? + has_status_metatag = select_metatags(:status).any? { |metatag| metatag.value.downcase.in?(%w[deleted active any all unmoderated modqueue appealed]) } + hide_deleted_posts? && !has_status_metatag + end + + memoize :tags, :normalize, :replace_aliases, :with_implicit_metatags, :to_cnf, :aliases, :implicit_metatags, :hide_deleted? end diff --git a/app/logical/post_query/ast.rb b/app/logical/post_query/ast.rb index 4a59926a9..c1ab42aa5 100644 --- a/app/logical/post_query/ast.rb +++ b/app/logical/post_query/ast.rb @@ -54,9 +54,37 @@ class PostQuery @args = args end - # Create an AST node. - def node(type, *args) - AST.new(type, args) + concerning :ConstructorMethods do + class_methods do + def tag(name) + AST.new(:tag, [name]) + end + + def metatag(name, value) + AST.new(:metatag, [name, value]) + end + end + + def &(other) + AST.new(:and, [self, other]) + end + + def |(other) + AST.new(:or, [self, other]) + end + + def ~ + AST.new(:opt, [self]) + end + + def -@ + AST.new(:not, [self]) + end + + # Create an AST node. + def node(type, *args) + AST.new(type, args) + end end concerning :SimplificationMethods do