diff --git a/app/components/tag_list_component.rb b/app/components/tag_list_component.rb
new file mode 100644
index 000000000..48379e8ba
--- /dev/null
+++ b/app/components/tag_list_component.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+class TagListComponent < ApplicationComponent
+ attr_reader :tags, :current_query, :show_extra_links
+
+ def initialize(tags: [], current_query: nil, show_extra_links: false)
+ @tags = tags
+ @current_query = current_query
+ @show_extra_links = show_extra_links
+ end
+
+ def self.tags_from_names(tag_names)
+ names_to_tags = Tag.where(name: tag_names).map { |tag| [tag.name, tag] }.to_h
+
+ tag_names.map do |name|
+ names_to_tags.fetch(name) { Tag.new(name: name).freeze }
+ end
+ end
+
+ def categorized_tags(categories, &block)
+ return to_enum(:categorized_tags, categories) unless block_given?
+
+ categories.each do |category|
+ tags = tags_for_category(category)
+ yield category, tags if tags.present?
+ end
+ end
+
+ def tags_for_category(category_name)
+ category = TagCategory.mapping[category_name.downcase]
+ tags_by_category[category] || []
+ end
+
+ def tags_by_category
+ @tags_by_category ||= tags.sort_by(&:name).group_by(&:category)
+ end
+
+ def is_underused_tag?(tag)
+ tag.post_count <= 1 && tag.general?
+ end
+
+ def humanized_post_count(tag)
+ if tag.post_count >= 10_000
+ "#{tag.post_count / 1_000}k"
+ elsif tag.post_count >= 1_000
+ "%.1fk" % (tag.post_count / 1_000.0)
+ else
+ tag.post_count.to_s
+ end
+ end
+end
diff --git a/app/components/tag_list_component/tag_list_component.html+categorized.erb b/app/components/tag_list_component/tag_list_component.html+categorized.erb
new file mode 100644
index 000000000..5a63bfa6f
--- /dev/null
+++ b/app/components/tag_list_component/tag_list_component.html+categorized.erb
@@ -0,0 +1,30 @@
+
+ <% categorized_tags(TagCategory.split_header_list).each do |category_name, tags| %>
+
+ <%= category_name.capitalize.pluralize(tags) %>
+
+
+
+ <% tags.each do |t| %>
+ -
+ <% if t.artist? %>
+ <%= link_to "?", show_or_new_artists_path(t.name), class: "wiki-link" %>
+ <% elsif t.name =~ /\A\d+\z/ %>
+ <%= link_to "?", wiki_page_path("~#{t.name}"), class: "wiki-link" %>
+ <% else %>
+ <%= link_to "?", wiki_page_path(t.name), class: "wiki-link" %>
+ <% end %>
+
+ <% if show_extra_links && current_query.present? %>
+ <%= link_to "+", posts_path(tags: "#{current_query} #{t.name}"), class: "search-inc-tag" %>
+ <%= link_to "-", posts_path(tags: "#{current_query} -#{t.name}"), class: "search-exl-tag" %>
+ <% end %>
+
+ <%= link_to t.pretty_name, posts_path(tags: t.name), class: "search-tag" %>
+
+ <%= tag.span humanized_post_count(t), class: ["post-count", ("low-post-count" if is_underused_tag?(t))], title: t.post_count %>
+
+ <% end %>
+
+ <% end %>
+
diff --git a/app/components/tag_list_component/tag_list_component.html+inline.erb b/app/components/tag_list_component/tag_list_component.html+inline.erb
new file mode 100644
index 000000000..d8cc0b8cc
--- /dev/null
+++ b/app/components/tag_list_component/tag_list_component.html+inline.erb
@@ -0,0 +1,7 @@
+
+ <% categorized_tags(TagCategory.categorized_list).each do |category_name, tags| %>
+ <% tags.each do |t| %>
+ <%= link_to t.name, posts_path(tags: t.name), class: "search-tag tag-type-#{t.category}", "data-tag-name": t.name %>
+ <% end %>
+ <% end %>
+
diff --git a/app/components/tag_list_component/tag_list_component.html+search.erb b/app/components/tag_list_component/tag_list_component.html+search.erb
new file mode 100644
index 000000000..09e5fccc1
--- /dev/null
+++ b/app/components/tag_list_component/tag_list_component.html+search.erb
@@ -0,0 +1,22 @@
+
+ <% tags.each do |t| %>
+ -
+ <%# ignore search:foo metatags %>
+ <% if t.artist? %>
+ <%= link_to "?", show_or_new_artists_path(t.name), class: "wiki-link" %>
+ <% elsif t.name =~ /\A\d+\z/ %>
+ <%= link_to "?", wiki_page_path("~#{t.name}"), class: "wiki-link" %>
+ <% else %>
+ <%= link_to "?", wiki_page_path(t.name), class: "wiki-link" %>
+ <% end %>
+
+ <% if show_extra_links && current_query.present? %>
+ <%= link_to "+", posts_path(tags: "#{current_query} #{t.name}"), class: "search-inc-tag" %>
+ <%= link_to "-", posts_path(tags: "#{current_query} -#{t.name}"), class: "search-exl-tag" %>
+ <% end %>
+
+ <%= link_to t.pretty_name, posts_path(tags: t.name), class: "search-tag" %>
+ <%= tag.span humanized_post_count(t), class: "post-count", title: t.post_count %>
+
+ <% end %>
+
diff --git a/app/components/tag_list_component/tag_list_component.html+simple.erb b/app/components/tag_list_component/tag_list_component.html+simple.erb
new file mode 100644
index 000000000..c26e2f70a
--- /dev/null
+++ b/app/components/tag_list_component/tag_list_component.html+simple.erb
@@ -0,0 +1,5 @@
+
+ <% tags.each do |t| %>
+ <%= link_to t.pretty_name, posts_path(tags: t.name), class: "search-tag tag-type-#{t.category}", "data-tag-name": t.name %>
+ <% end %>
+
diff --git a/app/components/tag_list_component/tag_list_component.html.scss b/app/components/tag_list_component/tag_list_component.html.scss
new file mode 100644
index 000000000..a9e74226a
--- /dev/null
+++ b/app/components/tag_list_component/tag_list_component.html.scss
@@ -0,0 +1,25 @@
+.tag-list {
+ a.search-tag {
+ overflow-wrap: normal;
+ }
+
+ &.inline-tag-list {
+ display: inline;
+
+ a {
+ margin-right: 0.5em;
+ }
+ }
+
+ &.categorized-tag-list {
+ ul {
+ margin-bottom: 1em;
+ }
+ }
+
+ &.simple-tag-list {
+ a {
+ display: block;
+ }
+ }
+}
diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb
index b12be2897..41f733d05 100644
--- a/app/helpers/components_helper.rb
+++ b/app/helpers/components_helper.rb
@@ -22,4 +22,32 @@ module ComponentsHelper
def render_post_navbar(post, **options)
render PostNavbarComponent.new(post: post, **options)
end
+
+ # A simple vertical tag list with no post counts. Used in related tags.
+ def render_simple_tag_list(tag_names, **options)
+ tags = TagListComponent.tags_from_names(tag_names)
+ render TagListComponent.new(tags: tags, **options).with_variant(:simple)
+ end
+
+ # A horizontal tag list, with tags grouped by category. Used in post
+ # tooltips, on the comments index, and in the modqueue.
+ def render_inline_tag_list(post, **options)
+ render TagListComponent.new(tags: post.tags, **options).with_variant(:inline)
+ end
+
+ def render_inline_tag_list_from_names(tag_names, **options)
+ tags = TagListComponent.tags_from_names(tag_names)
+ render TagListComponent.new(tags: tags, **options).with_variant(:inline)
+ end
+
+ # A vertical tag list, with tags split into categories. Used on post show pages.
+ def render_categorized_tag_list(post, **options)
+ render TagListComponent.new(tags: post.tags, **options).with_variant(:categorized)
+ end
+
+ # A vertical tag list, used in the post index sidebar.
+ def render_search_tag_list(tag_names, **options)
+ tags = TagListComponent.tags_from_names(tag_names)
+ render TagListComponent.new(tags: tags, **options).with_variant(:search)
+ end
end
diff --git a/app/javascript/src/styles/common/tags.scss.erb b/app/javascript/src/styles/common/tags.scss.erb
index 6d5e2ca59..31dc5147d 100644
--- a/app/javascript/src/styles/common/tags.scss.erb
+++ b/app/javascript/src/styles/common/tags.scss.erb
@@ -34,18 +34,3 @@
}
}
<% end %>
-
-a.search-tag {
- word-break: break-word;
-}
-
-.inline-tag-list {
- ul {
- display: inline;
-
- li {
- display: inline;
- margin-right: 0.5em;
- }
- }
-}
diff --git a/app/javascript/src/styles/specific/mod_queue.scss.erb b/app/javascript/src/styles/specific/mod_queue.scss.erb
index 9874a248c..16c5ab575 100644
--- a/app/javascript/src/styles/specific/mod_queue.scss.erb
+++ b/app/javascript/src/styles/specific/mod_queue.scss.erb
@@ -27,7 +27,7 @@ div#c-modqueue {
}
<% Danbooru.config.modqueue_warning_tags.each do |tag| %>
- li[data-tag-name="<%= tag %>"] {
+ a[data-tag-name="<%= tag %>"] {
background-color: var(--modqueue-tag-warning-color);
}
<% end %>
diff --git a/app/logical/post_sets/post.rb b/app/logical/post_sets/post.rb
index fff5581ac..9c377581c 100644
--- a/app/logical/post_sets/post.rb
+++ b/app/logical/post_sets/post.rb
@@ -201,14 +201,6 @@ module PostSets
searches = ["search:all"] + SavedSearch.labels_for(CurrentUser.user.id).map {|x| "search:#{x}"}
searches.take(MAX_SIDEBAR_TAGS)
end
-
- def tag_set_presenter
- @tag_set_presenter ||= TagSetPresenter.new(related_tags)
- end
-
- def tag_list_html(**options)
- tag_set_presenter.tag_list_html(name_only: query.is_metatag?(:search), **options)
- end
end
end
end
diff --git a/app/presenters/post_presenter.rb b/app/presenters/post_presenter.rb
index 5b308d150..a0de157b5 100644
--- a/app/presenters/post_presenter.rb
+++ b/app/presenters/post_presenter.rb
@@ -1,6 +1,6 @@
class PostPresenter
attr_reader :pool, :next_post_in_pool
- delegate :tag_list_html, :split_tag_list_html, :split_tag_list_text, :inline_tag_list_html, to: :tag_set_presenter
+ delegate :split_tag_list_text, to: :tag_set_presenter
def initialize(post)
@post = post
diff --git a/app/presenters/tag_set_presenter.rb b/app/presenters/tag_set_presenter.rb
index d167c3cd1..f95f836c5 100644
--- a/app/presenters/tag_set_presenter.rb
+++ b/app/presenters/tag_set_presenter.rb
@@ -14,48 +14,6 @@ class TagSetPresenter
@tag_names = tag_names
end
- def tag_list_html(current_query: "", show_extra_links: false, name_only: false)
- html = ""
-
- if ordered_tags.present?
- html << ''
- ordered_tags.each do |tag|
- html << build_list_item(tag, current_query: current_query, show_extra_links: show_extra_links, name_only: name_only)
- end
- html << "
"
- end
-
- html.html_safe
- end
-
- def split_tag_list_html(headers: true, category_list: TagCategory.split_header_list, current_query: "", show_extra_links: false, name_only: false, humanize_tags: true)
- html = ""
-
- category_list.each do |category|
- typetags = tags_for_category(category)
-
- if typetags.any?
- if headers
- html << %{#{category.capitalize.pluralize(typetags.size)}
}
- end
-
- html << %{}
- typetags.each do |tag|
- html << build_list_item(tag, current_query: current_query, show_extra_links: show_extra_links, name_only: name_only, humanize_tags: humanize_tags)
- end
- html << "
"
- end
- end
-
- html.html_safe
- end
-
- # compact (horizontal) list, as seen in the /comments index.
- def inline_tag_list_html(humanize_tags: false)
- html = split_tag_list_html(category_list: TagCategory.categorized_list, headers: false, show_extra_links: false, name_only: true, humanize_tags: humanize_tags)
- %{#{html}}.html_safe
- end
-
# the list of tags inside the tag box in the post edit form.
def split_tag_list_text(category_list: TagCategory.categorized_list)
category_list.map do |category|
@@ -104,57 +62,5 @@ class TagSetPresenter
end
end
- def build_list_item(tag, name_only: false, humanize_tags: true, show_extra_links: false, current_query: "")
- name = tag.name
- count = tag.post_count
- category = tag.category
-
- html = %{}
-
- unless name_only
- if tag.artist?
- html << %{? }
- elsif name =~ /\A\d+\z/
- html << %{? }
- else
- html << %{? }
- end
-
- if show_extra_links && current_query.present?
- html << %{+ }
- html << %{– }
- end
- end
-
- humanized_tag = humanize_tags ? name.tr("_", " ") : name
- html << %{#{h(humanized_tag)} }
-
- unless name_only || tag.new_record?
- if count >= 10_000
- post_count = "#{count / 1_000}k"
- elsif count >= 1_000
- post_count = format("%.1fk", (count / 1_000.0))
- else
- post_count = count
- end
-
- is_underused_tag = count <= 1 && tag.general?
- klass = "post-count#{is_underused_tag ? " low-post-count" : ""}"
-
- html << %{#{post_count}}
- end
-
- html << ""
- html
- end
-
- def h(s)
- CGI.escapeHTML(s)
- end
-
- def u(s)
- CGI.escape(s)
- end
-
memoize :tags, :tags_by_category, :ordered_tags, :humanized_essential_tag_string
end
diff --git a/app/views/comments/partials/index/_header.html.erb b/app/views/comments/partials/index/_header.html.erb
index 53a1a1dc0..dd806d822 100644
--- a/app/views/comments/partials/index/_header.html.erb
+++ b/app/views/comments/partials/index/_header.html.erb
@@ -19,7 +19,7 @@
Tags
- <%= post.presenter.inline_tag_list_html %>
+ <%= render_inline_tag_list(post) %>
diff --git a/app/views/modqueue/_post.html.erb b/app/views/modqueue/_post.html.erb
index b2c29e1d3..e09951d85 100644
--- a/app/views/modqueue/_post.html.erb
+++ b/app/views/modqueue/_post.html.erb
@@ -51,7 +51,7 @@
Tags
- <%= post.presenter.inline_tag_list_html %>
+ <%= render_inline_tag_list(post) %>
diff --git a/app/views/post_versions/_listing.html.erb b/app/views/post_versions/_listing.html.erb
index 87b04a8a6..11477b4f6 100644
--- a/app/views/post_versions/_listing.html.erb
+++ b/app/views/post_versions/_listing.html.erb
@@ -22,7 +22,10 @@
<%= post_version_field(post_version, :rating) %>
<%= post_version_field(post_version, :parent_id) %>
- Tags: <%= TagSetPresenter.new(post_version.tag_array).inline_tag_list_html %>
+
+ Tags:
+ <%= render_inline_tag_list_from_names(post_version.tag_array) %>
+
<%= post_source_tag(post_version.source) %>
diff --git a/app/views/post_votes/index.html.erb b/app/views/post_votes/index.html.erb
index 2b87427b8..609ef2f7b 100644
--- a/app/views/post_votes/index.html.erb
+++ b/app/views/post_votes/index.html.erb
@@ -13,7 +13,7 @@
<%= post_preview(vote.post, show_deleted: true) %>
<% end %>
<% t.column "Tags", td: {class: "col-expand"} do |vote| %>
- <%= TagSetPresenter.new(vote.post.tag_array).inline_tag_list_html %>
+ <%= render_inline_tag_list(vote.post) %>
<% end %>
<% t.column "Score" do |vote| %>
<%= link_to sprintf("%+d", vote.score), post_votes_path(search: { score: vote.score }) %>
diff --git a/app/views/posts/index.html.erb b/app/views/posts/index.html.erb
index 1b5330f92..2e2e8f9f6 100644
--- a/app/views/posts/index.html.erb
+++ b/app/views/posts/index.html.erb
@@ -7,7 +7,7 @@
Tags
- <%= @post_set.tag_list_html(current_query: params[:tags], show_extra_links: policy(Post).show_extra_links?) %>
+ <%= render_search_tag_list(@post_set.related_tags, current_query: params[:tags], show_extra_links: policy(Post).show_extra_links?) %>
<%= render "posts/partials/index/options" %>
diff --git a/app/views/posts/show.html+tooltip.erb b/app/views/posts/show.html+tooltip.erb
index 4063090ca..1783a2746 100644
--- a/app/views/posts/show.html+tooltip.erb
+++ b/app/views/posts/show.html+tooltip.erb
@@ -52,6 +52,6 @@
<% end %>
- <%= @post.presenter.inline_tag_list_html %>
+ <%= render_inline_tag_list(@post) %>
diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb
index fb56206fb..679d0c189 100644
--- a/app/views/posts/show.html.erb
+++ b/app/views/posts/show.html.erb
@@ -11,7 +11,7 @@
<%= render "posts/partials/index/blacklist" %>
- <%= @post.presenter.split_tag_list_html(current_query: params[:q], show_extra_links: policy(@post).show_extra_links?) %>
+ <%= render_categorized_tag_list(@post, current_query: params[:q], show_extra_links: policy(@post).show_extra_links?) %>
diff --git a/app/views/related_tags/_source_tags.html.erb b/app/views/related_tags/_source_tags.html.erb
index 2b1899c67..512135444 100644
--- a/app/views/related_tags/_source_tags.html.erb
+++ b/app/views/related_tags/_source_tags.html.erb
@@ -9,11 +9,11 @@
none
<% else %>
<% if source.artists.any?(&:is_banned?) %>
- <%= TagSetPresenter.new(["banned_artist"]).tag_list_html(name_only: true) %>
+ <%= render_simple_tag_list(["banned_artist"]) %>
<% end %>
<% source.artists.each do |artist| %>
- <%= TagSetPresenter.new([artist.name]).tag_list_html(name_only: true) %>
+ <%= render_simple_tag_list(["banned_artist"]) %>
<% artist.sorted_urls.each do |url| %>
diff --git a/app/views/related_tags/_tag_column.html.erb b/app/views/related_tags/_tag_column.html.erb
index 0cd87960c..de9d194cb 100644
--- a/app/views/related_tags/_tag_column.html.erb
+++ b/app/views/related_tags/_tag_column.html.erb
@@ -3,6 +3,6 @@
<%= content_tag(:div, class: ["tag-column", local_assigns[:class] || "#{title.parameterize}-related-tags-column", "is-empty-#{tags.empty?}"].join(" ")) do %>
<% if tags.present? %>
<%= title %>
- <%= TagSetPresenter.new(tags).tag_list_html(name_only: true) %>
+ <%= render_simple_tag_list(tags) %>
<% end %>
<% end %>
diff --git a/app/views/reports/upload_tags.html.erb b/app/views/reports/upload_tags.html.erb
index b3183d879..76ef6a44f 100644
--- a/app/views/reports/upload_tags.html.erb
+++ b/app/views/reports/upload_tags.html.erb
@@ -11,7 +11,7 @@
<%= post_preview(upload_report.becomes(Post), show_deleted: true, tags: "user:#{upload_report.uploader.name}") %>
<% end %>
<% t.column "Tags added by uploader", width: "45%" do |upload_report| %>
- <%= TagSetPresenter.new(upload_report.uploader_tags_array).inline_tag_list_html %>
+ <%= render_inline_tag_list(upload_report.uploader_tags_array) %>
<% end %>
<% t.column "Tags changed by other users", width: "45%" do |upload_report| %>
<%= render "reports/tag_diff_list", added_tags: upload_report.added_tags_array, removed_tags: upload_report.removed_tags_array %>
diff --git a/app/views/uploads/index.html.erb b/app/views/uploads/index.html.erb
index 5ada2e452..3a85f5888 100644
--- a/app/views/uploads/index.html.erb
+++ b/app/views/uploads/index.html.erb
@@ -47,7 +47,7 @@
<% if policy(upload).can_view_tags? %>
Tags
- <%= TagSetPresenter.new(upload.tag_string.split).inline_tag_list_html %>
+ <%= render_inline_tag_list_from_names(upload.tag_string.split) %>
<% end %>
<% end %>
diff --git a/test/components/tag_list_component_test.rb b/test/components/tag_list_component_test.rb
new file mode 100644
index 000000000..5f0a25ca0
--- /dev/null
+++ b/test/components/tag_list_component_test.rb
@@ -0,0 +1,63 @@
+require "test_helper"
+
+class TagListComponentTest < ViewComponent::TestCase
+ def render_tag_list(tags, variant, **options)
+ with_variant(variant) do
+ render_inline(TagListComponent.new(tags: tags, **options))
+ end
+ end
+
+ context "The TagListComponent" do
+ setup do
+ @arttag = create(:artist_tag)
+ @copytag = create(:copyright_tag)
+ @chartag = create(:character_tag)
+ @gentag = create(:general_tag)
+ @metatag = create(:meta_tag)
+ @tags = Tag.all
+ end
+
+ context "for a simple tag list" do
+ should "render" do
+ render_tag_list(@tags, :simple)
+
+ assert_css(".simple-tag-list a.search-tag", count: 5)
+ end
+ end
+
+ context "for an inline tag list" do
+ should "render" do
+ render_tag_list(@tags, :inline)
+
+ assert_css(".inline-tag-list a.search-tag", count: 5)
+ end
+ end
+
+ context "for a search tag list" do
+ context "with +/- links" do
+ should "render" do
+ render_tag_list(@tags, :search, current_query: "touhou", show_extra_links: true)
+
+ assert_css(".search-tag-list li a.search-tag", count: 5)
+ end
+ end
+
+ context "without +/- links" do
+ should "render" do
+ render_tag_list(@tags, :search)
+
+ assert_css(".search-tag-list li a.search-tag", count: 5)
+ end
+ end
+ end
+
+ context "for a categorized tag list" do
+ should "render" do
+ render_tag_list(@tags, :categorized)
+
+ assert_css(".categorized-tag-list li a.search-tag", count: 5)
+ assert_css(".categorized-tag-list h3", count: 5)
+ end
+ end
+ end
+end
diff --git a/test/factories/tag.rb b/test/factories/tag.rb
index a2eb396e7..8ac217ca5 100644
--- a/test/factories/tag.rb
+++ b/test/factories/tag.rb
@@ -4,6 +4,10 @@ FactoryBot.define do
post_count { 100 }
category {Tag.categories.general}
+ factory(:general_tag) do
+ category {Tag.categories.general}
+ end
+
factory(:artist_tag) do
category {Tag.categories.artist}
end
@@ -15,5 +19,9 @@ FactoryBot.define do
factory(:character_tag) do
category {Tag.categories.character}
end
+
+ factory(:meta_tag) do
+ category {Tag.categories.meta}
+ end
end
end