From 33f2725ae7067dc1aa5f14e56ef9b3896ef0ebf4 Mon Sep 17 00:00:00 2001 From: evazion Date: Fri, 11 Oct 2019 18:17:50 -0500 Subject: [PATCH] Fix #4112: Colorize tags in DText. DText is processed in three phases: a preprocessing phase, the regular parsing phases, and a postprocessing phase. In the preprocessing phase we extract all the wiki links from all the dtext messages on the page (more precisely, we do this in forum threads and on comment pages, because these are the main places with lots of dtext). This is so we can lookup all the tags and wiki pages in one query, which is necessary because in the worst case (in certain forum threads and in certain list_of_* wiki pages) there can be hundreds of tags per page. In the postprocessing phase we fixup the html generated by the ragel parser to add CSS classes to wiki links. We do this in a postprocessing step because it's easier than doing it in the ragel parser itself. --- app/helpers/application_helper.rb | 4 +- app/javascript/src/styles/common/dtext.scss | 4 ++ app/logical/d_text.rb | 48 +++++++++++++++++++ app/models/wiki_page.rb | 6 +++ app/views/comments/_index_by_comment.html.erb | 2 +- app/views/comments/index_for_post.js.erb | 2 +- .../comments/partials/index/_list.html.erb | 2 +- .../comments/partials/show/_comment.html.erb | 2 +- app/views/forum_posts/_forum_post.html.erb | 2 +- app/views/forum_posts/_listing.html.erb | 3 +- app/views/forum_topics/show.html.erb | 2 +- test/unit/d_text_test.rb | 18 +++++++ 12 files changed, 85 insertions(+), 10 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c9bc60d5f..b43e6b7ad 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -33,9 +33,7 @@ module ApplicationHelper end def format_text(text, **options) - raw DTextRagel.parse(text, **options) - rescue DTextRagel::Error => e - raw "" + raw DText.format_text(text, **options) end def strip_dtext(text) diff --git a/app/javascript/src/styles/common/dtext.scss b/app/javascript/src/styles/common/dtext.scss index 15f0d2d24..d51cb8c7b 100644 --- a/app/javascript/src/styles/common/dtext.scss +++ b/app/javascript/src/styles/common/dtext.scss @@ -108,6 +108,10 @@ div.prose { padding: 0 0.2em 0 0.3em; vertical-align: 1px; } + + a.dtext-wiki-does-not-exist, a.dtext-tag-does-not-exist { + text-decoration: dotted underline; + } } // avoid empty gaps beneath dtext blocks in table rows. diff --git a/app/logical/d_text.rb b/app/logical/d_text.rb index ad9d31eae..f93699beb 100644 --- a/app/logical/d_text.rb +++ b/app/logical/d_text.rb @@ -4,6 +4,54 @@ require 'uri' class DText MENTION_REGEXP = /(?<=^| )@\S+/ + def self.format_text(text, data: nil, **options) + data = preprocess([text]) if data.nil? + html = DTextRagel.parse(text, **options) + html = postprocess(html, *data) + html + rescue DTextRagel::Error => e + "" + end + + def self.preprocess(dtext_messages) + names = dtext_messages.map { |message| parse_wiki_titles(message) }.flatten.uniq + wiki_pages = WikiPage.where(title: names) + tags = Tag.where(name: names) + + [wiki_pages, tags] + end + + def self.postprocess(html, wiki_pages, tags) + fragment = Nokogiri::HTML.fragment(html) + + fragment.css("a.dtext-wiki-link").each do |node| + name = node["href"][%r!\A/wiki_pages/show_or_new\?title=(.*)\z!i, 1] + name = CGI.unescape(name) + name = WikiPage.normalize_title(name) + wiki = wiki_pages.find { |wiki| wiki.title == name } + tag = tags.find { |tag| tag.name == name } + + if wiki.blank? + node["class"] += " dtext-wiki-does-not-exist" + node["title"] = "This wiki page does not exist" + end + + if WikiPage.is_meta_wiki?(name) + # skip (meta wikis aren't expected to have a tag) + elsif tag.blank? + node["class"] += " dtext-tag-does-not-exist" + node["title"] = "This wiki page does not have a tag" + elsif tag.post_count <= 0 + node["class"] += " dtext-tag-empty" + node["title"] = "This wiki page does not have a tag" + else + node["class"] += " tag-type-#{tag.category}" + end + end + + fragment.to_s + end + def self.quote(message, creator_name) stripped_body = DText.strip_blocks(message, "quote") "[quote]\n#{creator_name} said:\n\n#{stripped_body}\n[/quote]\n\n" diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 236bb24cf..8e3c50e7e 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -1,6 +1,8 @@ class WikiPage < ApplicationRecord class RevertError < Exception ; end + META_WIKIS = ["list_of_", "tag_group:", "pool_group:", "howto:", "about:", "help:", "template:"] + before_save :normalize_title before_save :normalize_other_names after_save :create_version @@ -155,6 +157,10 @@ class WikiPage < ApplicationRecord title.tr("_", " ") end + def self.is_meta_wiki?(title) + title.starts_with?(*META_WIKIS) + end + def wiki_page_changed? saved_change_to_title? || saved_change_to_body? || saved_change_to_is_locked? || saved_change_to_is_deleted? || saved_change_to_other_names? end diff --git a/app/views/comments/_index_by_comment.html.erb b/app/views/comments/_index_by_comment.html.erb index 03a321ed4..0ec786a7b 100644 --- a/app/views/comments/_index_by_comment.html.erb +++ b/app/views/comments/_index_by_comment.html.erb @@ -8,7 +8,7 @@ <%= link_to(image_tag(comment.post.preview_file_url), post_path(comment.post)) %> <% end %> - <%= render :partial => "comments/partials/show/comment", :collection => [comment] %> + <%= render partial: "comments/partials/show/comment", collection: [comment], locals: { dtext_data: DText.preprocess(@comments.map(&:body)) } %> <% end %> <% end %> <% end %> diff --git a/app/views/comments/index_for_post.js.erb b/app/views/comments/index_for_post.js.erb index 504ac10c1..0c0d584ff 100644 --- a/app/views/comments/index_for_post.js.erb +++ b/app/views/comments/index_for_post.js.erb @@ -1,5 +1,5 @@ $("#threshold-comments-notice-for-<%= @post.id %>").hide(); var current_comment_section = $("div.comments-for-post[data-post-id=<%= @post.id %>] div.list-of-comments"); -current_comment_section.html("<%= j(render(:partial => 'comments/partials/show/comment', :collection => @comments))%>"); +current_comment_section.html("<%= j(render(partial: 'comments/partials/show/comment', collection: @comments, locals: { dtext_data: DText.preprocess(@comments.map(&:body)) })) %>"); $(window).trigger("danbooru:index_for_post", [<%= @post.id %>]); diff --git a/app/views/comments/partials/index/_list.html.erb b/app/views/comments/partials/index/_list.html.erb index a7a712058..d8e816947 100644 --- a/app/views/comments/partials/index/_list.html.erb +++ b/app/views/comments/partials/index/_list.html.erb @@ -13,7 +13,7 @@
<% if comments.present? %> - <%= render partial: "comments/partials/show/comment", collection: comments %> + <%= render partial: "comments/partials/show/comment", collection: comments, locals: { dtext_data: DText.preprocess(comments.map(&:body)) } %> <% elsif post.last_commented_at.present? %>

There are no visible comments.

<% else %> diff --git a/app/views/comments/partials/show/_comment.html.erb b/app/views/comments/partials/show/_comment.html.erb index 3db1cc28a..b8571e163 100644 --- a/app/views/comments/partials/show/_comment.html.erb +++ b/app/views/comments/partials/show/_comment.html.erb @@ -22,7 +22,7 @@
- <%= format_text(comment.body) %> + <%= format_text(comment.body, data: dtext_data) %>
<%= render "application/update_notice", record: comment %> diff --git a/app/views/forum_posts/_forum_post.html.erb b/app/views/forum_posts/_forum_post.html.erb index da49b2c17..f7cc58ac0 100644 --- a/app/views/forum_posts/_forum_post.html.erb +++ b/app/views/forum_posts/_forum_post.html.erb @@ -11,7 +11,7 @@
- <%= format_text(parse_embedded_tag_request_text(forum_post.body)) %> + <%= format_text(parse_embedded_tag_request_text(forum_post.body), data: dtext_data) %>
<%= render "application/update_notice", record: forum_post %> diff --git a/app/views/forum_posts/_listing.html.erb b/app/views/forum_posts/_listing.html.erb index 93d9e3768..0612f2522 100644 --- a/app/views/forum_posts/_listing.html.erb +++ b/app/views/forum_posts/_listing.html.erb @@ -1,8 +1,9 @@ <%- # forum_post %> <%- # original_forum_post_id %> +<%- # dtext_data %>
<% forum_posts.each do |forum_post| %> - <%= render "forum_posts/forum_post", :forum_post => forum_post, :original_forum_post_id => original_forum_post_id %> + <%= render "forum_posts/forum_post", forum_post: forum_post, original_forum_post_id: original_forum_post_id, dtext_data: dtext_data %> <% end %>
diff --git a/app/views/forum_topics/show.html.erb b/app/views/forum_topics/show.html.erb index 699175cfc..c8d64d5dc 100644 --- a/app/views/forum_topics/show.html.erb +++ b/app/views/forum_topics/show.html.erb @@ -20,7 +20,7 @@
<% end %> - <%= render "forum_posts/listing", :forum_posts => @forum_posts, :original_forum_post_id => @forum_topic.original_post.id %> + <%= render "forum_posts/listing", forum_posts: @forum_posts, original_forum_post_id: @forum_topic.original_post.id, dtext_data: DText.preprocess(@forum_posts.map(&:body)) %> <% if CurrentUser.is_member? %> <% if CurrentUser.is_moderator? || !@forum_topic.is_locked? %> diff --git a/test/unit/d_text_test.rb b/test/unit/d_text_test.rb index 81c2e61f4..053abc2db 100644 --- a/test/unit/d_text_test.rb +++ b/test/unit/d_text_test.rb @@ -38,5 +38,23 @@ class DTextTest < ActiveSupport::TestCase assert_strip_dtext("one two three\nfour\n\nfive six", "one [b]two[/b] three\nfour\n\nfive six") end end + + context "#format_text" do + should "add tag types to wiki links" do + create(:tag, name: "bkub", category: Tag.categories.artist, post_count: 42) + assert_match(/tag-type-#{Tag.categories.artist}/, DText.format_text("[[bkub]]")) + end + + should "mark links to nonexistent tags or wikis" do + create(:tag, name: "no_wiki", post_count: 42) + create(:tag, name: "empty_tag", post_count: 0) + + assert_match(/dtext-wiki-does-not-exist/, DText.format_text("[[no wiki]]")) + assert_match(/dtext-tag-does-not-exist/, DText.format_text("[[no tag]]")) + assert_match(/dtext-tag-empty/, DText.format_text("[[empty tag]]")) + + refute_match(/dtext-tag-does-not-exist/, DText.format_text("[[help:nothing]]")) + end + end end end