From 09223e5ae72ff1ced8f08143b7578122b462c6d6 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Tue, 8 Aug 2023 11:18:55 +1000 Subject: [PATCH] DEV: Remove enable_experimental_hashtag_autocomplete logic (#22820) This commit removes any logic in the app and in specs around enable_experimental_hashtag_autocomplete and deletes some old category hashtag code that is no longer necessary. It also adds a `slug_ref` category instance method, which will generate a reference like `parent:child` for a category, with an optional depth, which hashtags use. Also refactors PostRevisor which was using CategoryHashtagDataSource directly which is a no-no. Deletes the old hashtag markdown rule as well. --- .../app/components/composer-editor.js | 32 +- .../discourse/app/components/d-editor.js | 12 +- .../hashtag-post-decorations.js | 11 +- .../discourse/app/lib/hashtag-autocomplete.js | 39 +- .../discourse/app/lib/link-hashtags.js | 3 + .../acceptance/hashtag-autocomplete-test.js | 1 - .../tests/acceptance/hashtags-test.js | 51 -- .../tests/unit/lib/pretty-text-test.js | 67 --- .../addon/engines/discourse-markdown-it.js | 2 - .../pretty-text/addon/pretty-text.js | 2 - .../discourse-markdown/category-hashtag.js | 65 --- .../hashtag-autocomplete.js | 15 +- app/controllers/hashtags_controller.rb | 6 +- app/models/category.rb | 16 + app/models/concerns/category_hashtag.rb | 21 +- app/services/category_hashtag_data_source.rb | 9 +- app/services/hashtag_autocomplete_service.rb | 45 -- app/services/tag_hashtag_data_source.rb | 2 +- lib/post_revisor.rb | 8 +- lib/pretty_text.rb | 6 +- lib/pretty_text/helpers.rb | 15 - lib/pretty_text/shims.js | 6 - .../chat/lib/chat/channel_archive_service.rb | 4 +- .../lib/chat/channel_hashtag_data_source.rb | 2 +- .../lib/chat/channel_archive_service_spec.rb | 40 +- .../chat/channel_hashtag_data_source_spec.rb | 1 - plugins/chat/spec/models/chat/message_spec.rb | 15 - .../spec/system/hashtag_autocomplete_spec.rb | 2 - .../config/locales/server.en.yml | 10 - .../advanced_user_narrative.rb | 39 +- .../advanced_user_narrative_spec.rb | 39 +- spec/integrity/common_mark_spec.rb | 5 +- spec/lib/concern/category_hashtag_spec.rb | 51 -- spec/lib/email/sender_spec.rb | 1 - spec/lib/oneboxer_spec.rb | 1 - spec/lib/pretty_text/helpers_spec.rb | 31 -- spec/lib/pretty_text_spec.rb | 88 +--- spec/models/category_spec.rb | 33 ++ spec/requests/hashtags_controller_spec.rb | 447 +++++++----------- .../hashtag_autocomplete_service_spec.rb | 47 -- spec/services/post_alerter_spec.rb | 3 +- spec/system/hashtag_autocomplete_spec.rb | 7 +- spec/tasks/hashtags_spec.rb | 11 +- 43 files changed, 310 insertions(+), 1001 deletions(-) delete mode 100644 app/assets/javascripts/discourse/tests/acceptance/hashtags-test.js delete mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/category-hashtag.js delete mode 100644 spec/lib/concern/category_hashtag_spec.rb diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index 6673c23f375..b74a0c5ec0d 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -14,10 +14,6 @@ import discourseComputed, { observes, on, } from "discourse-common/utils/decorators"; -import { - fetchUnseenHashtags, - linkSeenHashtags, -} from "discourse/lib/link-hashtags"; import { fetchUnseenHashtagsInContext, linkSeenHashtagsInContext, @@ -500,24 +496,12 @@ export default Component.extend( }, _renderUnseenHashtags(preview) { - let unseen; const hashtagContext = this.site.hashtag_configurations["topic-composer"]; - if (this.siteSettings.enable_experimental_hashtag_autocomplete) { - unseen = linkSeenHashtagsInContext(hashtagContext, preview); - } else { - unseen = linkSeenHashtags(preview); - } - + const unseen = linkSeenHashtagsInContext(hashtagContext, preview); if (unseen.length > 0) { - if (this.siteSettings.enable_experimental_hashtag_autocomplete) { - fetchUnseenHashtagsInContext(hashtagContext, unseen).then(() => { - linkSeenHashtagsInContext(hashtagContext, preview); - }); - } else { - fetchUnseenHashtags(unseen).then(() => { - linkSeenHashtags(preview); - }); - } + fetchUnseenHashtagsInContext(hashtagContext, unseen).then(() => { + linkSeenHashtagsInContext(hashtagContext, preview); + }); } }, @@ -946,15 +930,9 @@ export default Component.extend( this._warnCannotSeeMention(preview); // Paint category, tag, and other data source hashtags - let unseenHashtags; const hashtagContext = this.site.hashtag_configurations["topic-composer"]; - if (this.siteSettings.enable_experimental_hashtag_autocomplete) { - unseenHashtags = linkSeenHashtagsInContext(hashtagContext, preview); - } else { - unseenHashtags = linkSeenHashtags(preview); - } - if (unseenHashtags.length > 0) { + if (linkSeenHashtagsInContext(hashtagContext, preview).length > 0) { discourseDebounce(this, this._renderUnseenHashtags, preview, 450); } diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js index 019a6780964..22a685cd204 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.js +++ b/app/assets/javascripts/discourse/app/components/d-editor.js @@ -18,13 +18,15 @@ import I18n from "I18n"; import ItsATrap from "@discourse/itsatrap"; import { Promise } from "rsvp"; import { SKIP } from "discourse/lib/autocomplete"; -import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete"; +import { + linkSeenHashtagsInContext, + setupHashtagAutocomplete, +} from "discourse/lib/hashtag-autocomplete"; import deprecated from "discourse-common/lib/deprecated"; import discourseDebounce from "discourse-common/lib/debounce"; import { findRawTemplate } from "discourse-common/lib/raw-templates"; import { getRegister } from "discourse-common/lib/get-owner"; import { isTesting } from "discourse-common/config/environment"; -import { linkSeenHashtags } from "discourse/lib/link-hashtags"; import { linkSeenMentions } from "discourse/lib/link-mentions"; import { loadOneboxes } from "discourse/lib/load-oneboxes"; import loadScript from "discourse/lib/load-script"; @@ -438,8 +440,10 @@ export default Component.extend(TextareaTextManipulation, { if (this.siteSettings.enable_diffhtml_preview) { const cookedElement = document.createElement("div"); cookedElement.innerHTML = cooked; - - linkSeenHashtags(cookedElement); + linkSeenHashtagsInContext( + this.site.hashtag_configurations["topic-composer"], + cookedElement + ); linkSeenMentions(cookedElement, this.siteSettings); resolveCachedShortUrls(this.siteSettings, cookedElement); loadOneboxes( diff --git a/app/assets/javascripts/discourse/app/instance-initializers/hashtag-post-decorations.js b/app/assets/javascripts/discourse/app/instance-initializers/hashtag-post-decorations.js index 75b462d0ddc..f3221dc88ba 100644 --- a/app/assets/javascripts/discourse/app/instance-initializers/hashtag-post-decorations.js +++ b/app/assets/javascripts/discourse/app/instance-initializers/hashtag-post-decorations.js @@ -5,16 +5,13 @@ export default { after: "hashtag-css-generator", initialize(owner) { - const siteSettings = owner.lookup("service:site-settings"); const site = owner.lookup("service:site"); withPluginApi("0.8.7", (api) => { - if (siteSettings.enable_experimental_hashtag_autocomplete) { - api.decorateCookedElement((post) => decorateHashtags(post, site), { - onlyStream: true, - id: "hashtag-icons", - }); - } + api.decorateCookedElement((post) => decorateHashtags(post, site), { + onlyStream: true, + id: "hashtag-icons", + }); }); }, }; diff --git a/app/assets/javascripts/discourse/app/lib/hashtag-autocomplete.js b/app/assets/javascripts/discourse/app/lib/hashtag-autocomplete.js index defdb468bf5..d23af5d5b32 100644 --- a/app/assets/javascripts/discourse/app/lib/hashtag-autocomplete.js +++ b/app/assets/javascripts/discourse/app/lib/hashtag-autocomplete.js @@ -11,7 +11,6 @@ import { escapeExpression, inCodeBlock, } from "discourse/lib/utilities"; -import { search as searchCategoryTag } from "discourse/lib/category-tag-search"; import { emojiUnescape } from "discourse/lib/text"; import { htmlSafe } from "@ember/template"; @@ -76,16 +75,12 @@ export function setupHashtagAutocomplete( siteSettings, autocompleteOptions = {} ) { - if (siteSettings.enable_experimental_hashtag_autocomplete) { - _setupExperimental( - contextualHashtagConfiguration, - $textArea, - siteSettings, - autocompleteOptions - ); - } else { - _setup($textArea, siteSettings, autocompleteOptions.afterComplete); - } + _setup( + contextualHashtagConfiguration, + $textArea, + siteSettings, + autocompleteOptions + ); } export function hashtagTriggerRule(textarea) { @@ -147,7 +142,7 @@ export function linkSeenHashtagsInContext( .filter((slug) => !checkedHashtags.has(slug)); } -function _setupExperimental( +function _setup( contextualHashtagConfiguration, $textArea, siteSettings, @@ -171,22 +166,6 @@ function _setupExperimental( }); } -function _setup($textArea, siteSettings, afterComplete) { - $textArea.autocomplete({ - template: findRawTemplate("category-tag-autocomplete"), - key: "#", - afterComplete, - transformComplete: (obj) => obj.text, - dataSource: (term) => { - if (term.match(/\s/)) { - return null; - } - return searchCategoryTag(term, siteSettings); - }, - triggerRule: (textarea, opts) => hashtagTriggerRule(textarea, opts), - }); -} - let searchCache = {}; let searchCacheTime; let currentSearch; @@ -220,10 +199,6 @@ function _searchGeneric(term, siteSettings, contextualHashtagConfiguration) { resolve(CANCELLED_STATUS); }, 5000); - if (!siteSettings.enable_experimental_hashtag_autocomplete && term === "") { - return resolve(CANCELLED_STATUS); - } - const debouncedSearch = (q, ctx, resultFunc) => { discourseDebounce(this, _searchRequest, q, ctx, resultFunc, INPUT_DELAY); }; diff --git a/app/assets/javascripts/discourse/app/lib/link-hashtags.js b/app/assets/javascripts/discourse/app/lib/link-hashtags.js index 0138379fec5..42788dc269c 100644 --- a/app/assets/javascripts/discourse/app/lib/link-hashtags.js +++ b/app/assets/javascripts/discourse/app/lib/link-hashtags.js @@ -1,3 +1,6 @@ +// TODO (martin) Delete this after core PR and any other PRs that depend +// on this file (e.g. discourse-encrypt) are merged. + import deprecated from "discourse-common/lib/deprecated"; import { TAG_HASHTAG_POSTFIX } from "discourse/lib/tag-hashtags"; import { ajax } from "discourse/lib/ajax"; diff --git a/app/assets/javascripts/discourse/tests/acceptance/hashtag-autocomplete-test.js b/app/assets/javascripts/discourse/tests/acceptance/hashtag-autocomplete-test.js index f6051259273..c6f0b6fcd6d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/hashtag-autocomplete-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/hashtag-autocomplete-test.js @@ -9,7 +9,6 @@ acceptance("#hashtag autocompletion in composer", function (needs) { needs.user(); needs.settings({ tagging_enabled: true, - enable_experimental_hashtag_autocomplete: true, }); needs.pretender((server, helper) => { server.get("/hashtags", () => { diff --git a/app/assets/javascripts/discourse/tests/acceptance/hashtags-test.js b/app/assets/javascripts/discourse/tests/acceptance/hashtags-test.js deleted file mode 100644 index 17111f33952..00000000000 --- a/app/assets/javascripts/discourse/tests/acceptance/hashtags-test.js +++ /dev/null @@ -1,51 +0,0 @@ -import { - acceptance, - query, - visible, -} from "discourse/tests/helpers/qunit-helpers"; -import { click, fillIn, visit } from "@ember/test-helpers"; -import { test } from "qunit"; - -acceptance("Category and Tag Hashtags", function (needs) { - needs.user(); - needs.settings({ - tagging_enabled: true, - enable_experimental_hashtag_autocomplete: false, - }); - needs.pretender((server, helper) => { - server.get("/hashtags", () => { - return helper.response({ - categories: { bug: "/c/bugs" }, - tags: { - monkey: "/tag/monkey", - bug: "/tag/bug", - }, - }); - }); - }); - - test("hashtags are cooked properly", async function (assert) { - await visit("/t/internationalization-localization/280"); - await click("#topic-footer-buttons .btn.create"); - - await fillIn( - ".d-editor-input", - `this is a category hashtag #bug - -this is a tag hashtag #monkey - -category vs tag: #bug vs #bug::tag - -uppercase hashtag works too #BUG, #BUG::tag` - ); - - assert.ok(visible(".d-editor-preview")); - assert.strictEqual( - query(".d-editor-preview").innerHTML.trim(), - `

this is a category hashtag #bug

-

this is a tag hashtag #monkey

-

category vs tag: #bug vs #bug

-

uppercase hashtag works too #BUG, #BUG

` - ); - }); -}); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js index c909f43b9ac..f018c85d3e6 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js @@ -643,73 +643,6 @@ eviltrout

); }); - test("Category hashtags", function (assert) { - const alwaysTrue = { - categoryHashtagLookup: function () { - return [ - "http://test.discourse.org/category-hashtag", - "category-hashtag", - ]; - }, - }; - - assert.cookedOptions( - "Check out #category-hashtag", - alwaysTrue, - '

Check out #category-hashtag

', - "it translates category hashtag into links" - ); - - assert.cooked( - "Check out #category-hashtag", - '

Check out #category-hashtag

', - "it does not translate category hashtag into links if it is not a valid category hashtag" - ); - - assert.cookedOptions( - "[#category-hashtag](http://www.test.com)", - alwaysTrue, - '

#category-hashtag

', - "it does not translate category hashtag within links" - ); - - assert.cooked( - "```\n# #category-hashtag\n```", - '
# #category-hashtag\n
', - "it does not translate category hashtags to links in code blocks" - ); - - assert.cooked( - "># #category-hashtag\n", - '
\n

#category-hashtag

\n
', - "it handles category hashtags in simple quotes" - ); - - assert.cooked( - "# #category-hashtag", - '

#category-hashtag

', - "it works within ATX-style headers" - ); - - assert.cooked( - "don't `#category-hashtag`", - "

don't #category-hashtag

", - "it does not mention in an inline code block" - ); - - assert.cooked( - "#category-hashtag", - '

#category-hashtag

', - "it works between HTML tags" - ); - - assert.cooked( - "Checkout #ụdị", - '

Checkout #ụdị

', - "it works for non-english characters" - ); - }); - test("Heading", function (assert) { assert.cooked( "**Bold**\n----------", diff --git a/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js b/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js index 84f8ce8c37a..e15ff31af27 100644 --- a/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js +++ b/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js @@ -547,8 +547,6 @@ export function setup(opts, siteSettings, state) { markdownTypographerQuotationMarks: siteSettings.markdown_typographer_quotation_marks, markdownLinkifyTlds: siteSettings.markdown_linkify_tlds, - enableExperimentalHashtagAutocomplete: - siteSettings.enable_experimental_hashtag_autocomplete, }; const markdownitOpts = { diff --git a/app/assets/javascripts/pretty-text/addon/pretty-text.js b/app/assets/javascripts/pretty-text/addon/pretty-text.js index 7d572eb5be5..f0e93ae1282 100644 --- a/app/assets/javascripts/pretty-text/addon/pretty-text.js +++ b/app/assets/javascripts/pretty-text/addon/pretty-text.js @@ -28,7 +28,6 @@ export function buildOptions(state) { getTopicInfo, topicId, forceQuoteLink, - categoryHashtagLookup, userId, getCurrentUser, currentUser, @@ -67,7 +66,6 @@ export function buildOptions(state) { getTopicInfo, topicId, forceQuoteLink, - categoryHashtagLookup, userId, getCurrentUser, currentUser, diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/category-hashtag.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/category-hashtag.js deleted file mode 100644 index 65595603b85..00000000000 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/category-hashtag.js +++ /dev/null @@ -1,65 +0,0 @@ -// TODO (martin) Remove this once enable_experimental_hashtag_autocomplete -// (and by extension enableExperimentalHashtagAutocomplete) is not required -// anymore, the new hashtag-autocomplete rule replaces it. - -function addHashtag(buffer, matches, state) { - const options = state.md.options.discourse; - const slug = matches[1]; - const categoryHashtagLookup = options.categoryHashtagLookup; - const result = categoryHashtagLookup && categoryHashtagLookup(slug); - - let token; - - if (result) { - token = new state.Token("link_open", "a", 1); - token.attrs = [ - ["class", "hashtag"], - ["href", result[0]], - ]; - token.block = false; - buffer.push(token); - - token = new state.Token("text", "", 0); - token.content = "#"; - buffer.push(token); - - token = new state.Token("span_open", "span", 1); - token.block = false; - buffer.push(token); - - token = new state.Token("text", "", 0); - token.content = result[1]; - buffer.push(token); - - buffer.push(new state.Token("span_close", "span", -1)); - - buffer.push(new state.Token("link_close", "a", -1)); - } else { - token = new state.Token("span_open", "span", 1); - token.attrs = [["class", "hashtag"]]; - buffer.push(token); - - token = new state.Token("text", "", 0); - token.content = matches[0]; - buffer.push(token); - - token = new state.Token("span_close", "span", -1); - buffer.push(token); - } -} - -export function setup(helper) { - helper.registerPlugin((md) => { - if ( - !md.options.discourse.limitedSiteSettings - .enableExperimentalHashtagAutocomplete - ) { - const rule = { - matcher: /#([\u00C0-\u1FFF\u2C00-\uD7FF\w:-]{1,101})/, - onMatch: addHashtag, - }; - - md.core.textPostProcess.ruler.push("category-hashtag", rule); - } - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/hashtag-autocomplete.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/hashtag-autocomplete.js index 42a14b3d2d2..cc808d7afe3 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/hashtag-autocomplete.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/hashtag-autocomplete.js @@ -87,17 +87,12 @@ function addIconPlaceholder(buffer, state) { export function setup(helper) { helper.registerPlugin((md) => { - if ( - md.options.discourse.limitedSiteSettings - .enableExperimentalHashtagAutocomplete - ) { - const rule = { - matcher: /#([\u00C0-\u1FFF\u2C00-\uD7FF\w:-]{1,101})/, - onMatch: addHashtag, - }; + const rule = { + matcher: /#([\u00C0-\u1FFF\u2C00-\uD7FF\w:-]{1,101})/, + onMatch: addHashtag, + }; - md.core.textPostProcess.ruler.push("hashtag-autocomplete", rule); - } + md.core.textPostProcess.ruler.push("hashtag-autocomplete", rule); }); helper.allowList([ diff --git a/app/controllers/hashtags_controller.rb b/app/controllers/hashtags_controller.rb index e55b6840227..e650bf16943 100644 --- a/app/controllers/hashtags_controller.rb +++ b/app/controllers/hashtags_controller.rb @@ -4,11 +4,7 @@ class HashtagsController < ApplicationController requires_login def lookup - if SiteSetting.enable_experimental_hashtag_autocomplete - render json: HashtagAutocompleteService.new(guardian).lookup(params[:slugs], params[:order]) - else - render json: HashtagAutocompleteService.new(guardian).lookup_old(params[:slugs]) - end + render json: HashtagAutocompleteService.new(guardian).lookup(params[:slugs], params[:order]) end def search diff --git a/app/models/category.rb b/app/models/category.rb index b0cadea1853..c244b7bf3a8 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -18,6 +18,8 @@ class Category < ActiveRecord::Base REQUIRE_TOPIC_APPROVAL = "require_topic_approval" REQUIRE_REPLY_APPROVAL = "require_reply_approval" + NUM_AUTO_BUMP_DAILY = "num_auto_bump_daily" + SLUG_REF_SEPARATOR = ":" register_custom_field_type(REQUIRE_TOPIC_APPROVAL, :boolean) register_custom_field_type(REQUIRE_REPLY_APPROVAL, :boolean) @@ -1037,6 +1039,20 @@ class Category < ActiveRecord::Base end end + def slug_ref(depth: 1) + if self.parent_category_id.present? + built_ref = [self.slug] + parent = self.parent_category + while parent.present? && (built_ref.length < depth + 1) + built_ref << parent.slug + parent = parent.parent_category + end + built_ref.reverse.join(Category::SLUG_REF_SEPARATOR) + else + self.slug + end + end + def cannot_delete_reason return I18n.t("category.cannot_delete.uncategorized") if self.uncategorized? return I18n.t("category.cannot_delete.has_subcategories") if self.has_children? diff --git a/app/models/concerns/category_hashtag.rb b/app/models/concerns/category_hashtag.rb index 00f4e3e74f3..d8044399a8e 100644 --- a/app/models/concerns/category_hashtag.rb +++ b/app/models/concerns/category_hashtag.rb @@ -3,26 +3,7 @@ module CategoryHashtag extend ActiveSupport::Concern - SEPARATOR = ":" - class_methods do - # TODO (martin) Remove this when enable_experimental_hashtag_autocomplete - # becomes the norm, it is reimplemented below for CategoryHashtagDataSourcee - def query_from_hashtag_slug(category_slug) - slug_path = split_slug_path(category_slug) - return if slug_path.blank? - - slug_path.map! { |slug| CGI.escape(slug) } if SiteSetting.slug_generation_method == "encoded" - - parent_slug, child_slug = slug_path.last(2) - categories = Category.where(slug: parent_slug) - if child_slug - Category.where(slug: child_slug, parent_category_id: categories.select(:id)).first - else - categories.where(parent_category_id: nil).first - end - end - ## # Finds any categories that match the provided slugs, supporting # the parent:child format for category slugs (only one level of @@ -66,7 +47,7 @@ module CategoryHashtag end def split_slug_path(slug) - slug_path = slug.split(SEPARATOR) + slug_path = slug.split(Category::SLUG_REF_SEPARATOR) return if slug_path.empty? || slug_path.size > 2 slug_path end diff --git a/app/services/category_hashtag_data_source.rb b/app/services/category_hashtag_data_source.rb index 960c383ad89..e75d3e74c77 100644 --- a/app/services/category_hashtag_data_source.rb +++ b/app/services/category_hashtag_data_source.rb @@ -5,7 +5,7 @@ # categories via the # autocomplete character. class CategoryHashtagDataSource def self.enabled? - SiteSetting.enable_experimental_hashtag_autocomplete + true end def self.icon @@ -27,12 +27,7 @@ class CategoryHashtagDataSource # Single-level category heirarchy should be enough to distinguish between # categories here. - item.ref = - if category.parent_category_id - "#{category.parent_category.slug}:#{category.slug}" - else - category.slug - end + item.ref = category.slug_ref end end diff --git a/app/services/hashtag_autocomplete_service.rb b/app/services/hashtag_autocomplete_service.rb index 65ee0a2e061..4ed2bea573e 100644 --- a/app/services/hashtag_autocomplete_service.rb +++ b/app/services/hashtag_autocomplete_service.rb @@ -299,51 +299,6 @@ class HashtagAutocompleteService append_types_to_conflicts(limited_results, top_ranked_type, types_in_priority_order, limit) end - # TODO (martin) Remove this once plugins are not relying on the old lookup - # behavior via HashtagsController when enable_experimental_hashtag_autocomplete is removed - def lookup_old(slugs) - raise Discourse::InvalidParameters.new(:slugs) if !slugs.is_a?(Array) - - all_slugs = [] - tag_slugs = [] - - slugs[0..HashtagAutocompleteService::HASHTAGS_PER_REQUEST].each do |slug| - if slug.end_with?(PrettyText::Helpers::TAG_HASHTAG_POSTFIX) - tag_slugs << slug.chomp(PrettyText::Helpers::TAG_HASHTAG_POSTFIX) - else - all_slugs << slug - end - end - - # Try to resolve hashtags as categories first - category_slugs_and_ids = - all_slugs.map { |slug| [slug, Category.query_from_hashtag_slug(slug)&.id] }.to_h - category_ids_and_urls = - Category - .secured(guardian) - .select(:id, :slug, :parent_category_id) # fields required for generating category URL - .where(id: category_slugs_and_ids.values) - .map { |c| [c.id, c.url] } - .to_h - categories_hashtags = {} - category_slugs_and_ids.each do |slug, id| - if category_url = category_ids_and_urls[id] - categories_hashtags[slug] = category_url - end - end - - # Resolve remaining hashtags as tags - tag_hashtags = {} - if SiteSetting.tagging_enabled - tag_slugs += (all_slugs - categories_hashtags.keys) - DiscourseTagging - .filter_visible(Tag.where_name(tag_slugs), guardian) - .each { |tag| tag_hashtags[tag.name] = tag.full_url } - end - - { categories: categories_hashtags, tags: tag_hashtags } - end - private def search_using_condition(limited_results, term, type, limit, condition) diff --git a/app/services/tag_hashtag_data_source.rb b/app/services/tag_hashtag_data_source.rb index caaf8d3c4b4..531a3b16add 100644 --- a/app/services/tag_hashtag_data_source.rb +++ b/app/services/tag_hashtag_data_source.rb @@ -5,7 +5,7 @@ # tags via the # autocomplete character. class TagHashtagDataSource def self.enabled? - SiteSetting.enable_experimental_hashtag_autocomplete && SiteSetting.tagging_enabled + SiteSetting.tagging_enabled end def self.icon diff --git a/lib/post_revisor.rb b/lib/post_revisor.rb index 6ab8c9afaec..498ff537ab2 100644 --- a/lib/post_revisor.rb +++ b/lib/post_revisor.rb @@ -160,18 +160,14 @@ class PostRevisor user, I18n.t( "topic_category_changed", - from: category_name_raw(old_category), - to: category_name_raw(new_category), + from: "##{old_category.slug_ref}", + to: "##{new_category.slug_ref}", ), post_type: Post.types[:small_action], action_code: "category_changed", ) end - def self.category_name_raw(category) - "##{CategoryHashtagDataSource.category_to_hashtag_item(category).ref}" - end - def self.create_small_action_for_tag_changes(topic:, user:, added_tags:, removed_tags:) return if !SiteSetting.create_post_for_category_and_tag_changes diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index f33268f2127..12289d2a1b6 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -197,7 +197,6 @@ module PrettyText __optInput.lookupPrimaryUserGroup = __lookupPrimaryUserGroup; __optInput.formatUsername = __formatUsername; __optInput.getTopicInfo = __getTopicInfo; - __optInput.categoryHashtagLookup = __categoryLookup; __optInput.hashtagLookup = __hashtagLookup; __optInput.customEmoji = #{custom_emoji.to_json}; __optInput.customEmojiTranslation = #{Plugin::CustomEmoji.translations.to_json}; @@ -471,10 +470,7 @@ module PrettyText DiscourseEvent.trigger(:reduce_excerpt, doc, options) strip_image_wrapping(doc) strip_oneboxed_media(doc) - - if SiteSetting.enable_experimental_hashtag_autocomplete && options[:plain_hashtags] - convert_hashtag_links_to_plaintext(doc) - end + convert_hashtag_links_to_plaintext(doc) if options[:plain_hashtags] html = doc.to_html ExcerptParser.get_excerpt(html, max_length, options) diff --git a/lib/pretty_text/helpers.rb b/lib/pretty_text/helpers.rb index 5f48b28bbdc..b07951ed79a 100644 --- a/lib/pretty_text/helpers.rb +++ b/lib/pretty_text/helpers.rb @@ -96,21 +96,6 @@ module PrettyText end end - # TODO (martin) Remove this when everything is using hashtag_lookup - # after enable_experimental_hashtag_autocomplete is default. - def category_tag_hashtag_lookup(text) - is_tag = text =~ /#{TAG_HASHTAG_POSTFIX}\z/ - - if !is_tag && category = Category.query_from_hashtag_slug(text) - [category.url, text] - elsif (!is_tag && tag = Tag.find_by(name: text)) || - (is_tag && tag = Tag.find_by(name: text.gsub!(TAG_HASHTAG_POSTFIX, ""))) - [tag.url, text] - else - nil - end - end - def hashtag_lookup(slug, cooking_user_id, types_in_priority_order) # NOTE: This is _somewhat_ expected since we need to be able to cook posts # etc. without a user sometimes, but it is still an edge case. diff --git a/lib/pretty_text/shims.js b/lib/pretty_text/shims.js index f0d7b0849f1..ab8ded97ea1 100644 --- a/lib/pretty_text/shims.js +++ b/lib/pretty_text/shims.js @@ -106,12 +106,6 @@ function __getTopicInfo(i) { return __helpers.get_topic_info(i); } -// TODO (martin) Remove this when everything is using hashtag_lookup -// after enable_experimental_hashtag_autocomplete is default. -function __categoryLookup(c) { - return __helpers.category_tag_hashtag_lookup(c); -} - function __hashtagLookup(slug, cookingUserId, typesInPriorityOrder) { return __helpers.hashtag_lookup(slug, cookingUserId, typesInPriorityOrder); } diff --git a/plugins/chat/lib/chat/channel_archive_service.rb b/plugins/chat/lib/chat/channel_archive_service.rb index d3d7a349588..3dffdb76923 100644 --- a/plugins/chat/lib/chat/channel_archive_service.rb +++ b/plugins/chat/lib/chat/channel_archive_service.rb @@ -301,9 +301,7 @@ module Chat end def channel_hashtag_or_name - if chat_channel.slug.present? && SiteSetting.enable_experimental_hashtag_autocomplete - return "##{chat_channel.slug}::channel" - end + return "##{chat_channel.slug}::channel" if chat_channel.slug.present? chat_channel_title end end diff --git a/plugins/chat/lib/chat/channel_hashtag_data_source.rb b/plugins/chat/lib/chat/channel_hashtag_data_source.rb index c5ccf8eff49..38276982bd2 100644 --- a/plugins/chat/lib/chat/channel_hashtag_data_source.rb +++ b/plugins/chat/lib/chat/channel_hashtag_data_source.rb @@ -3,7 +3,7 @@ module Chat class ChannelHashtagDataSource def self.enabled? - SiteSetting.enable_experimental_hashtag_autocomplete && SiteSetting.enable_public_channels + SiteSetting.enable_public_channels end def self.icon diff --git a/plugins/chat/spec/lib/chat/channel_archive_service_spec.rb b/plugins/chat/spec/lib/chat/channel_archive_service_spec.rb index 1911e2fa3c6..2644914f2e7 100644 --- a/plugins/chat/spec/lib/chat/channel_archive_service_spec.rb +++ b/plugins/chat/spec/lib/chat/channel_archive_service_spec.rb @@ -186,28 +186,24 @@ describe Chat::ChannelArchiveService do expect(pm_topic.first_post.raw).to include("Title can't have more than 1 emoji") end - context "when enable_experimental_hashtag_autocomplete" do - before { SiteSetting.enable_experimental_hashtag_autocomplete = true } - - it "uses the channel slug to autolink a hashtag for the channel in the PM" do - create_messages(3) && start_archive - described_class.new(@channel_archive).execute - expect(@channel_archive.reload.complete?).to eq(true) - pm_topic = Topic.private_messages.last - expect(pm_topic.first_post.cooked).to have_tag( - "a", - with: { - class: "hashtag-cooked", - href: channel.relative_url, - "data-type": "channel", - "data-slug": channel.slug, - "data-id": channel.id, - "data-ref": "#{channel.slug}::channel", - }, - ) do - with_tag("span", with: { class: "hashtag-icon-placeholder" }) - with_tag("span", text: channel.title(user)) - end + it "uses the channel slug to autolink a hashtag for the channel in the PM" do + create_messages(3) && start_archive + described_class.new(@channel_archive).execute + expect(@channel_archive.reload.complete?).to eq(true) + pm_topic = Topic.private_messages.last + expect(pm_topic.first_post.cooked).to have_tag( + "a", + with: { + class: "hashtag-cooked", + href: channel.relative_url, + "data-type": "channel", + "data-slug": channel.slug, + "data-id": channel.id, + "data-ref": "#{channel.slug}::channel", + }, + ) do + with_tag("span", with: { class: "hashtag-icon-placeholder" }) + with_tag("span", text: channel.title(user)) end end diff --git a/plugins/chat/spec/lib/chat/channel_hashtag_data_source_spec.rb b/plugins/chat/spec/lib/chat/channel_hashtag_data_source_spec.rb index 87c44e90e1c..fc4ff6955fa 100644 --- a/plugins/chat/spec/lib/chat/channel_hashtag_data_source_spec.rb +++ b/plugins/chat/spec/lib/chat/channel_hashtag_data_source_spec.rb @@ -27,7 +27,6 @@ RSpec.describe Chat::ChannelHashtagDataSource do let!(:guardian) { Guardian.new(user) } before do - SiteSetting.enable_experimental_hashtag_autocomplete = true SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:trust_level_1] Group.refresh_automatic_groups! end diff --git a/plugins/chat/spec/models/chat/message_spec.rb b/plugins/chat/spec/models/chat/message_spec.rb index e0a5c232078..7b44fa94271 100644 --- a/plugins/chat/spec/models/chat/message_spec.rb +++ b/plugins/chat/spec/models/chat/message_spec.rb @@ -228,22 +228,8 @@ describe Chat::Message do expect(cooked).to eq("

@mention

") end - it "supports category-hashtag plugin" do - # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites - SiteSetting.enable_experimental_hashtag_autocomplete = false - - category = Fabricate(:category) - - cooked = described_class.cook("##{category.slug}") - - expect(cooked).to eq( - "

##{category.slug}

", - ) - end - it "supports hashtag autocomplete" do SiteSetting.chat_enabled = true - SiteSetting.enable_experimental_hashtag_autocomplete = true category = Fabricate(:category) user = Fabricate(:user) @@ -504,7 +490,6 @@ describe Chat::Message do before do SiteSetting.chat_enabled = true - SiteSetting.enable_experimental_hashtag_autocomplete = true SiteSetting.suppress_secured_categories_from_admin = true end diff --git a/plugins/chat/spec/system/hashtag_autocomplete_spec.rb b/plugins/chat/spec/system/hashtag_autocomplete_spec.rb index fa4b31e5648..898375f9ad4 100644 --- a/plugins/chat/spec/system/hashtag_autocomplete_spec.rb +++ b/plugins/chat/spec/system/hashtag_autocomplete_spec.rb @@ -15,8 +15,6 @@ describe "Using #hashtag autocompletion to search for and lookup channels", type let(:topic_page) { PageObjects::Pages::Topic.new } before do - SiteSetting.enable_experimental_hashtag_autocomplete = true - chat_system_bootstrap(user, [channel1, channel2]) sign_in(user) end diff --git a/plugins/discourse-narrative-bot/config/locales/server.en.yml b/plugins/discourse-narrative-bot/config/locales/server.en.yml index 97ce26d80f3..2bab753ac5a 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.en.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.en.yml @@ -400,18 +400,8 @@ en: instructions: |- Did you know that you can refer to categories and tags in your post? For example, have you seen the %{category} category? - Type `#` in the middle of a sentence and select any category or tag. - instructions_experimental: |- - Did you know that you can refer to categories and tags in your post? For example, have you seen the %{category} category? - Type `#` anywhere in a sentence and select any category or tag. not_found: |- - Hmm, I don’t see a category in there anywhere. Note that `#` can't be the first character. Can you copy this in your next reply? - - ```text - I can create a category link via # - ``` - not_found_experimental: |- Hmm, I don’t see a category in there anywhere. Can you copy this in your next reply? ```text diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb index 6fa8e6b6766..be66ba9ff77 100644 --- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb +++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb @@ -39,27 +39,11 @@ module DiscourseNarrativeBot next_state: :tutorial_category_hashtag, next_instructions: Proc.new do - category = Category.secured(Guardian.new(@user)).last - slug = category.slug - - if parent_category = category.parent_category - slug = "#{parent_category.slug}#{CategoryHashtag::SEPARATOR}#{slug}" - end - - # TODO (martin) When enable_experimental_hashtag_autocomplete is the only option - # update the instructions and remove instructions_experimental, as well as the - # not_found translation - if SiteSetting.enable_experimental_hashtag_autocomplete - I18n.t( - "#{I18N_KEY}.category_hashtag.instructions_experimental", - i18n_post_args(category: "##{slug}"), - ) - else - I18n.t( - "#{I18N_KEY}.category_hashtag.instructions", - i18n_post_args(category: "##{slug}"), - ) - end + category = Category.secured(@user.guardian).last + I18n.t( + "#{I18N_KEY}.category_hashtag.instructions", + i18n_post_args(category: "##{category.slug_ref}"), + ) end, recover: { action: :reply_to_recover, @@ -298,9 +282,7 @@ module DiscourseNarrativeBot topic_id = @post.topic_id return unless valid_topic?(topic_id) - hashtag_css_class = - SiteSetting.enable_experimental_hashtag_autocomplete ? ".hashtag-cooked" : ".hashtag" - if Nokogiri::HTML5.fragment(@post.cooked).css(hashtag_css_class).size > 0 + if Nokogiri::HTML5.fragment(@post.cooked).css(".hashtag-cooked").size > 0 raw = <<~MD #{I18n.t("#{I18N_KEY}.category_hashtag.reply", i18n_post_args)} @@ -312,14 +294,7 @@ module DiscourseNarrativeBot else fake_delay unless @data[:attempted] - if SiteSetting.enable_experimental_hashtag_autocomplete - reply_to( - @post, - I18n.t("#{I18N_KEY}.category_hashtag.not_found_experimental", i18n_post_args), - ) - else - reply_to(@post, I18n.t("#{I18N_KEY}.category_hashtag.not_found", i18n_post_args)) - end + reply_to(@post, I18n.t("#{I18N_KEY}.category_hashtag.not_found", i18n_post_args)) end enqueue_timeout_job(@user) false diff --git a/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb b/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb index f391a9c8d23..4bf9bcc34fb 100644 --- a/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb +++ b/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb @@ -377,9 +377,6 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do context "when reply contains the skip trigger" do it "should create the right reply" do - # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites - SiteSetting.enable_experimental_hashtag_autocomplete = false - parent_category = Fabricate(:category, name: "a") _category = Fabricate(:category, parent_category: parent_category, name: "b") @@ -417,9 +414,6 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do context "when user recovers a post in the right topic" do it "should create the right reply" do - # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites - SiteSetting.enable_experimental_hashtag_autocomplete = false - parent_category = Fabricate(:category, name: "a") _category = Fabricate(:category, parent_category: parent_category, name: "b") post @@ -448,9 +442,6 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do topic_id: topic.id, track: described_class.to_s, ) - - # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites - SiteSetting.enable_experimental_hashtag_autocomplete = false end context "when post is not in the right topic" do @@ -509,37 +500,15 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do narrative.input(:reply, user, post: post) expected_raw = <<~RAW - #{I18n.t("discourse_narrative_bot.advanced_user_narrative.category_hashtag.reply", base_uri: "")} - - #{I18n.t("discourse_narrative_bot.advanced_user_narrative.change_topic_notification_level.instructions", base_uri: "")} - RAW - - expect(Post.last.raw).to eq(expected_raw.chomp) - expect(narrative.get_data(user)[:state].to_sym).to eq( - :tutorial_change_topic_notification_level, - ) - end - - context "when enable_experimental_hashtag_autocomplete is true" do - before { SiteSetting.enable_experimental_hashtag_autocomplete = true } - - it "should create the right reply" do - category = Fabricate(:category) - - post.update!(raw: "Check out this ##{category.slug}") - narrative.input(:reply, user, post: post) - - expected_raw = <<~RAW #{I18n.t("discourse_narrative_bot.advanced_user_narrative.category_hashtag.reply", base_uri: "")} #{I18n.t("discourse_narrative_bot.advanced_user_narrative.change_topic_notification_level.instructions", base_uri: "")} RAW - expect(Post.last.raw).to eq(expected_raw.chomp) - expect(narrative.get_data(user)[:state].to_sym).to eq( - :tutorial_change_topic_notification_level, - ) - end + expect(Post.last.raw).to eq(expected_raw.chomp) + expect(narrative.get_data(user)[:state].to_sym).to eq( + :tutorial_change_topic_notification_level, + ) end end diff --git a/spec/integrity/common_mark_spec.rb b/spec/integrity/common_mark_spec.rb index 85c8c821227..8f74ce313b4 100644 --- a/spec/integrity/common_mark_spec.rb +++ b/spec/integrity/common_mark_spec.rb @@ -5,9 +5,6 @@ RSpec.describe "CommonMark" do SiteSetting.enable_markdown_typographer = false SiteSetting.highlighted_languages = "ruby|aa" - # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites - SiteSetting.enable_experimental_hashtag_autocomplete = false - html, state, md = nil failed = 0 @@ -34,7 +31,7 @@ RSpec.describe "CommonMark" do cooked = PrettyText.markdown(md, sanitize: false) cooked.strip! cooked.gsub!(" class=\"lang-auto\"", "") - cooked.gsub!(%r{(.*)}, "\\1") + cooked.gsub!(%r{(.*)}, "\\1") cooked.gsub!(%r{}, "") # we support data-attributes which is not in the spec cooked.gsub!("
", "
")
diff --git a/spec/lib/concern/category_hashtag_spec.rb b/spec/lib/concern/category_hashtag_spec.rb
deleted file mode 100644
index 9a815653d5d..00000000000
--- a/spec/lib/concern/category_hashtag_spec.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe CategoryHashtag do
-  describe "#query_from_hashtag_slug" do
-    fab!(:parent_category) { Fabricate(:category) }
-    fab!(:child_category) { Fabricate(:category, parent_category: parent_category) }
-
-    it "should return the right result for a parent category slug" do
-      expect(Category.query_from_hashtag_slug(parent_category.slug)).to eq(parent_category)
-    end
-
-    it "should return the right result for a parent and child category slug" do
-      expect(
-        Category.query_from_hashtag_slug(
-          "#{parent_category.slug}#{CategoryHashtag::SEPARATOR}#{child_category.slug}",
-        ),
-      ).to eq(child_category)
-    end
-
-    it "should return nil for incorrect parent category slug" do
-      expect(Category.query_from_hashtag_slug("random-slug")).to eq(nil)
-    end
-
-    it "should return nil for incorrect parent and child category slug" do
-      expect(
-        Category.query_from_hashtag_slug("random-slug#{CategoryHashtag::SEPARATOR}random-slug"),
-      ).to eq(nil)
-    end
-
-    it "should return nil for a non-existent root and a parent subcategory" do
-      expect(
-        Category.query_from_hashtag_slug(
-          "non-existent#{CategoryHashtag::SEPARATOR}#{parent_category.slug}",
-        ),
-      ).to eq(nil)
-    end
-
-    context "with multi-level categories" do
-      before { SiteSetting.max_category_nesting = 3 }
-
-      it "should return the right result for a grand child category slug" do
-        category = Fabricate(:category, parent_category: child_category)
-        expect(
-          Category.query_from_hashtag_slug(
-            "#{child_category.slug}#{CategoryHashtag::SEPARATOR}#{category.slug}",
-          ),
-        ).to eq(category)
-      end
-    end
-  end
-end
diff --git a/spec/lib/email/sender_spec.rb b/spec/lib/email/sender_spec.rb
index 2194b379ad0..b88f95712e5 100644
--- a/spec/lib/email/sender_spec.rb
+++ b/spec/lib/email/sender_spec.rb
@@ -566,7 +566,6 @@ RSpec.describe Email::Sender do
     end
 
     it "changes the hashtags to the slug with a # symbol beforehand rather than the full name of the resource" do
-      SiteSetting.enable_experimental_hashtag_autocomplete = true
       category = Fabricate(:category, slug: "dev")
       reply.update!(raw: reply.raw + "\n wow this is #dev")
       reply.rebake!
diff --git a/spec/lib/oneboxer_spec.rb b/spec/lib/oneboxer_spec.rb
index a3a68f5e414..8912b91adf2 100644
--- a/spec/lib/oneboxer_spec.rb
+++ b/spec/lib/oneboxer_spec.rb
@@ -179,7 +179,6 @@ RSpec.describe Oneboxer do
     end
 
     it "includes hashtag HTML" do
-      SiteSetting.enable_experimental_hashtag_autocomplete = true
       category = Fabricate(:category, slug: "random")
       tag = Fabricate(:tag, name: "bug")
       public_post = Fabricate(:post, raw: "This post has some hashtags, #random and #bug")
diff --git a/spec/lib/pretty_text/helpers_spec.rb b/spec/lib/pretty_text/helpers_spec.rb
index 0244da8a3b4..864c10c2463 100644
--- a/spec/lib/pretty_text/helpers_spec.rb
+++ b/spec/lib/pretty_text/helpers_spec.rb
@@ -16,37 +16,6 @@ RSpec.describe PrettyText::Helpers do
     end
   end
 
-  describe ".category_tag_hashtag_lookup" do
-    fab!(:tag) { Fabricate(:tag, name: "somecooltag") }
-    fab!(:category) do
-      Fabricate(:category, name: "Some Awesome Category", slug: "someawesomecategory")
-    end
-
-    it "handles tags based on slug with TAG_HASHTAG_POSTFIX" do
-      expect(
-        PrettyText::Helpers.category_tag_hashtag_lookup(
-          +"somecooltag#{PrettyText::Helpers::TAG_HASHTAG_POSTFIX}",
-        ),
-      ).to eq([tag.url, "somecooltag"])
-    end
-
-    it "handles categories based on slug" do
-      expect(PrettyText::Helpers.category_tag_hashtag_lookup("someawesomecategory")).to eq(
-        [category.url, "someawesomecategory"],
-      )
-    end
-
-    it "handles tags based on slug without TAG_HASHTAG_POSTFIX" do
-      expect(PrettyText::Helpers.category_tag_hashtag_lookup(+"somecooltag")).to eq(
-        [tag.url, "somecooltag"],
-      )
-    end
-
-    it "returns nil when no tag or category that matches exists" do
-      expect(PrettyText::Helpers.category_tag_hashtag_lookup("blah")).to eq(nil)
-    end
-  end
-
   describe ".hashtag_lookup" do
     fab!(:tag) { Fabricate(:tag, name: "somecooltag", description: "Coolest things ever") }
     fab!(:category) do
diff --git a/spec/lib/pretty_text_spec.rb b/spec/lib/pretty_text_spec.rb
index df6fc039632..19def8b7df0 100644
--- a/spec/lib/pretty_text_spec.rb
+++ b/spec/lib/pretty_text_spec.rb
@@ -1722,50 +1722,6 @@ RSpec.describe PrettyText do
   end
 
   it "produces hashtag links" do
-    # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites
-    SiteSetting.enable_experimental_hashtag_autocomplete = false
-
-    category = Fabricate(:category, name: "testing")
-    category2 = Fabricate(:category, name: "known")
-    Fabricate(:topic, tags: [Fabricate(:tag, name: "known")])
-
-    cooked = PrettyText.cook(" #unknown::tag #known #known::tag #testing")
-
-    [
-      "#unknown::tag",
-      "#known",
-      "#known",
-      "#testing",
-    ].each { |element| expect(cooked).to include(element) }
-
-    cooked = PrettyText.cook("[`a` #known::tag here](http://example.com)")
-
-    html = <<~HTML
-      

a #known::tag here

- HTML - - expect(cooked).to eq(html.strip) - - cooked = PrettyText.cook("`a` #known::tag here") - - expect(cooked).to eq(html.strip) - - cooked = PrettyText.cook("test #known::tag") - html = <<~HTML -

test #known

- HTML - - expect(cooked).to eq(html.strip) - - # ensure it does not fight with the autolinker - expect(PrettyText.cook(" http://somewhere.com/#known")).not_to include("hashtag") - expect(PrettyText.cook(" http://somewhere.com/?#known")).not_to include("hashtag") - expect(PrettyText.cook(" http://somewhere.com/?abc#known")).not_to include("hashtag") - end - - it "produces hashtag links when enable_experimental_hashtag_autocomplete is enabled" do - SiteSetting.enable_experimental_hashtag_autocomplete = true - user = Fabricate(:user) category = Fabricate(:category, name: "testing", slug: "testing") category2 = Fabricate(:category, name: "known", slug: "known") @@ -2059,11 +2015,8 @@ HTML end it "does not replace hashtags and mentions" do - # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites - SiteSetting.enable_experimental_hashtag_autocomplete = false - Fabricate(:user, username: "test") - category = Fabricate(:category, slug: "test") + category = Fabricate(:category, slug: "test", name: "test") Fabricate( :watched_word, action: WatchedWord.actions[:replace], @@ -2071,19 +2024,25 @@ HTML replacement: "discourse", ) - expect(PrettyText.cook("@test #test test")).to match_html(<<~HTML) -

- @test - #test - discourse -

- HTML + cooked = PrettyText.cook("@test #test test") + expect(cooked).to have_tag("a", text: "@test", with: { class: "mention", href: "/u/test" }) + expect(cooked).to have_tag( + "a", + text: "test", + with: { + class: "hashtag-cooked", + href: "/c/test/#{category.id}", + "data-type": "category", + "data-slug": category.slug, + "data-id": category.id, + }, + ) do + with_tag("span", with: { class: "hashtag-icon-placeholder" }) + end + expect(cooked).to include("discourse") end it "does not replace hashtags and mentions when watched words are regular expressions" do - # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites - SiteSetting.enable_experimental_hashtag_autocomplete = false - SiteSetting.watched_words_regular_expressions = true Fabricate(:user, username: "test") @@ -2095,19 +2054,6 @@ HTML replacement: "discourse", ) - cooked = PrettyText.cook("@test #test test") - expect(cooked).to have_tag("a", text: "@test", with: { class: "mention", href: "/u/test" }) - expect(cooked).to have_tag( - "a", - text: "#test", - with: { - class: "hashtag", - href: "/c/test/#{category.id}", - }, - ) - expect(cooked).to include("tdiscourset") - - SiteSetting.enable_experimental_hashtag_autocomplete = true cooked = PrettyText.cook("@test #test test") expect(cooked).to have_tag("a", text: "@test", with: { class: "mention", href: "/u/test" }) expect(cooked).to have_tag( diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb index 26c7eeea7af..e540433aee9 100644 --- a/spec/models/category_spec.rb +++ b/spec/models/category_spec.rb @@ -1451,4 +1451,37 @@ RSpec.describe Category do end end end + + describe "#slug_ref" do + fab!(:category) { Fabricate(:category, slug: "foo") } + + it "returns the slug for categories without parents" do + expect(category.slug_ref).to eq("foo") + end + + context "for category with parent" do + fab!(:subcategory) { Fabricate(:category, parent_category: category, slug: "bar") } + + it "returns the parent and child slug ref with separator" do + expect(subcategory.slug_ref).to eq("foo#{Category::SLUG_REF_SEPARATOR}bar") + end + end + + context "for category with multiple parents" do + let(:subcategory_1) { Fabricate(:category, parent_category: category, slug: "bar") } + let(:subcategory_2) { Fabricate(:category, parent_category: subcategory_1, slug: "boo") } + + before { SiteSetting.max_category_nesting = 3 } + + it "returns the parent and child slug ref with separator" do + expect(subcategory_2.slug_ref(depth: 2)).to eq( + "foo#{Category::SLUG_REF_SEPARATOR}bar#{Category::SLUG_REF_SEPARATOR}boo", + ) + end + + it "allows limiting depth" do + expect(subcategory_2.slug_ref(depth: 1)).to eq("bar#{Category::SLUG_REF_SEPARATOR}boo") + end + end + end end diff --git a/spec/requests/hashtags_controller_spec.rb b/spec/requests/hashtags_controller_spec.rb index 98b37199a10..f17a8eeb4d7 100644 --- a/spec/requests/hashtags_controller_spec.rb +++ b/spec/requests/hashtags_controller_spec.rb @@ -20,305 +20,184 @@ RSpec.describe HashtagsController do end describe "#lookup" do - context "when enable_experimental_hashtag_autocomplete disabled" do - # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites - before { SiteSetting.enable_experimental_hashtag_autocomplete = false } - context "when logged in" do - context "as regular user" do - before { sign_in(Fabricate(:user)) } + context "when logged in" do + context "as regular user" do + before { sign_in(Fabricate(:user)) } - it "returns only valid categories and tags" do - get "/hashtags.json", - params: { - slugs: [category.slug, private_category.slug, "none", tag.name, hidden_tag.name], - } + it "returns only valid categories and tags" do + get "/hashtags.json", + params: { + slugs: [category.slug, private_category.slug, "none", tag.name, hidden_tag.name], + order: %w[category tag], + } - expect(response.status).to eq(200) - expect(response.parsed_body).to eq( - "categories" => { - category.slug => category.url, - }, - "tags" => { - tag.name => tag.full_url, - }, - ) - end - - it "handles tags with the TAG_HASHTAG_POSTFIX" do - get "/hashtags.json", - params: { - slugs: ["#{tag.name}#{PrettyText::Helpers::TAG_HASHTAG_POSTFIX}"], - } - - expect(response.status).to eq(200) - expect(response.parsed_body).to eq( - "categories" => { - }, - "tags" => { - tag.name => tag.full_url, - }, - ) - end - - it "does not return restricted categories or hidden tags" do - get "/hashtags.json", params: { slugs: [private_category.slug, hidden_tag.name] } - - expect(response.status).to eq(200) - expect(response.parsed_body).to eq("categories" => {}, "tags" => {}) - end + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + { + "category" => [ + { + "relative_url" => category.url, + "text" => category.name, + "description" => nil, + "icon" => "folder", + "type" => "category", + "ref" => category.slug, + "slug" => category.slug, + "id" => category.id, + }, + ], + "tag" => [ + { + "relative_url" => tag.url, + "text" => tag.name, + "description" => nil, + "icon" => "tag", + "type" => "tag", + "ref" => tag.name, + "slug" => tag.name, + "secondary_text" => "x0", + "id" => tag.id, + }, + ], + }, + ) end - context "as admin" do - fab!(:admin) { Fabricate(:admin) } + it "handles tags with the ::tag type suffix" do + get "/hashtags.json", params: { slugs: ["#{tag.name}::tag"], order: %w[category tag] } - before { sign_in(admin) } - - it "returns restricted categories and hidden tags" do - group.add(admin) - - get "/hashtags.json", params: { slugs: [private_category.slug, hidden_tag.name] } - - expect(response.status).to eq(200) - expect(response.parsed_body).to eq( - "categories" => { - private_category.slug => private_category.url, - }, - "tags" => { - hidden_tag.name => hidden_tag.full_url, - }, - ) - end + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + { + "category" => [], + "tag" => [ + { + "relative_url" => tag.url, + "text" => tag.name, + "description" => nil, + "icon" => "tag", + "type" => "tag", + "ref" => "#{tag.name}::tag", + "slug" => tag.name, + "secondary_text" => "x0", + "id" => tag.id, + }, + ], + }, + ) end - context "with sub-sub-categories" do - before do - SiteSetting.max_category_nesting = 3 - sign_in(Fabricate(:user)) - end + it "does not return restricted categories or hidden tags" do + get "/hashtags.json", + params: { + slugs: [private_category.slug, hidden_tag.name], + order: %w[category tag], + } - it "works" do - foo = Fabricate(:category_with_definition, slug: "foo") - foobar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: foo.id) - foobarbaz = - Fabricate(:category_with_definition, slug: "baz", parent_category_id: foobar.id) - - qux = Fabricate(:category_with_definition, slug: "qux") - quxbar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: qux.id) - quxbarbaz = - Fabricate(:category_with_definition, slug: "baz", parent_category_id: quxbar.id) - - invalid_slugs = [":"] - child_slugs = %w[bar baz] - deeply_nested_slugs = %w[foo:bar:baz qux:bar:baz] - get "/hashtags.json", - params: { - slugs: - invalid_slugs + child_slugs + deeply_nested_slugs + - %w[foo foo:bar bar:baz qux qux:bar], - } - - expect(response.status).to eq(200) - expect(response.parsed_body["categories"]).to eq( - "foo" => foo.url, - "foo:bar" => foobar.url, - "bar:baz" => foobarbaz.id < quxbarbaz.id ? foobarbaz.url : quxbarbaz.url, - "qux" => qux.url, - "qux:bar" => quxbar.url, - ) - end + expect(response.status).to eq(200) + expect(response.parsed_body).to eq({ "category" => [], "tag" => [] }) end end - context "when not logged in" do - it "returns invalid access" do - get "/hashtags.json", params: { slugs: [] } - expect(response.status).to eq(403) + context "as admin" do + fab!(:admin) { Fabricate(:admin) } + + before { sign_in(admin) } + + it "returns restricted categories and hidden tags" do + group.add(admin) + + get "/hashtags.json", + params: { + slugs: [private_category.slug, hidden_tag.name], + order: %w[category tag], + } + + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + { + "category" => [ + { + "relative_url" => private_category.url, + "text" => private_category.name, + "description" => nil, + "icon" => "folder", + "type" => "category", + "ref" => private_category.slug, + "slug" => private_category.slug, + "id" => private_category.id, + }, + ], + "tag" => [ + { + "relative_url" => hidden_tag.url, + "text" => hidden_tag.name, + "description" => nil, + "icon" => "tag", + "type" => "tag", + "ref" => hidden_tag.name, + "slug" => hidden_tag.name, + "secondary_text" => "x0", + "id" => hidden_tag.id, + }, + ], + }, + ) + end + end + + context "with sub-sub-categories" do + before do + SiteSetting.max_category_nesting = 3 + sign_in(Fabricate(:user)) + end + + it "works" do + foo = Fabricate(:category_with_definition, slug: "foo") + foobar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: foo.id) + foobarbaz = + Fabricate(:category_with_definition, slug: "baz", parent_category_id: foobar.id) + + qux = Fabricate(:category_with_definition, slug: "qux") + quxbar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: qux.id) + quxbarbaz = + Fabricate(:category_with_definition, slug: "baz", parent_category_id: quxbar.id) + + invalid_slugs = [":"] + child_slugs = %w[bar baz] + deeply_nested_slugs = %w[foo:bar:baz qux:bar:baz] + get "/hashtags.json", + params: { + slugs: + invalid_slugs + child_slugs + deeply_nested_slugs + + %w[foo foo:bar bar:baz qux qux:bar], + order: %w[category tag], + } + + expect(response.status).to eq(200) + found_categories = response.parsed_body["category"] + expect(found_categories.map { |c| c["ref"] }).to match_array( + %w[foo foo:bar bar:baz qux qux:bar], + ) + expect(found_categories.find { |c| c["ref"] == "foo" }["relative_url"]).to eq(foo.url) + expect(found_categories.find { |c| c["ref"] == "foo:bar" }["relative_url"]).to eq( + foobar.url, + ) + expect(found_categories.find { |c| c["ref"] == "bar:baz" }["relative_url"]).to eq( + foobarbaz.url, + ) + expect(found_categories.find { |c| c["ref"] == "qux" }["relative_url"]).to eq(qux.url) + expect(found_categories.find { |c| c["ref"] == "qux:bar" }["relative_url"]).to eq( + quxbar.url, + ) end end end - context "when enable_experimental_hashtag_autocomplete enabled" do - before { SiteSetting.enable_experimental_hashtag_autocomplete = true } - - context "when logged in" do - context "as regular user" do - before { sign_in(Fabricate(:user)) } - - it "returns only valid categories and tags" do - get "/hashtags.json", - params: { - slugs: [category.slug, private_category.slug, "none", tag.name, hidden_tag.name], - order: %w[category tag], - } - - expect(response.status).to eq(200) - expect(response.parsed_body).to eq( - { - "category" => [ - { - "relative_url" => category.url, - "text" => category.name, - "description" => nil, - "icon" => "folder", - "type" => "category", - "ref" => category.slug, - "slug" => category.slug, - "id" => category.id, - }, - ], - "tag" => [ - { - "relative_url" => tag.url, - "text" => tag.name, - "description" => nil, - "icon" => "tag", - "type" => "tag", - "ref" => tag.name, - "slug" => tag.name, - "secondary_text" => "x0", - "id" => tag.id, - }, - ], - }, - ) - end - - it "handles tags with the ::tag type suffix" do - get "/hashtags.json", params: { slugs: ["#{tag.name}::tag"], order: %w[category tag] } - - expect(response.status).to eq(200) - expect(response.parsed_body).to eq( - { - "category" => [], - "tag" => [ - { - "relative_url" => tag.url, - "text" => tag.name, - "description" => nil, - "icon" => "tag", - "type" => "tag", - "ref" => "#{tag.name}::tag", - "slug" => tag.name, - "secondary_text" => "x0", - "id" => tag.id, - }, - ], - }, - ) - end - - it "does not return restricted categories or hidden tags" do - get "/hashtags.json", - params: { - slugs: [private_category.slug, hidden_tag.name], - order: %w[category tag], - } - - expect(response.status).to eq(200) - expect(response.parsed_body).to eq({ "category" => [], "tag" => [] }) - end - end - - context "as admin" do - fab!(:admin) { Fabricate(:admin) } - - before { sign_in(admin) } - - it "returns restricted categories and hidden tags" do - group.add(admin) - - get "/hashtags.json", - params: { - slugs: [private_category.slug, hidden_tag.name], - order: %w[category tag], - } - - expect(response.status).to eq(200) - expect(response.parsed_body).to eq( - { - "category" => [ - { - "relative_url" => private_category.url, - "text" => private_category.name, - "description" => nil, - "icon" => "folder", - "type" => "category", - "ref" => private_category.slug, - "slug" => private_category.slug, - "id" => private_category.id, - }, - ], - "tag" => [ - { - "relative_url" => hidden_tag.url, - "text" => hidden_tag.name, - "description" => nil, - "icon" => "tag", - "type" => "tag", - "ref" => hidden_tag.name, - "slug" => hidden_tag.name, - "secondary_text" => "x0", - "id" => hidden_tag.id, - }, - ], - }, - ) - end - end - - context "with sub-sub-categories" do - before do - SiteSetting.max_category_nesting = 3 - sign_in(Fabricate(:user)) - end - - it "works" do - foo = Fabricate(:category_with_definition, slug: "foo") - foobar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: foo.id) - foobarbaz = - Fabricate(:category_with_definition, slug: "baz", parent_category_id: foobar.id) - - qux = Fabricate(:category_with_definition, slug: "qux") - quxbar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: qux.id) - quxbarbaz = - Fabricate(:category_with_definition, slug: "baz", parent_category_id: quxbar.id) - - invalid_slugs = [":"] - child_slugs = %w[bar baz] - deeply_nested_slugs = %w[foo:bar:baz qux:bar:baz] - get "/hashtags.json", - params: { - slugs: - invalid_slugs + child_slugs + deeply_nested_slugs + - %w[foo foo:bar bar:baz qux qux:bar], - order: %w[category tag], - } - - expect(response.status).to eq(200) - found_categories = response.parsed_body["category"] - expect(found_categories.map { |c| c["ref"] }).to match_array( - %w[foo foo:bar bar:baz qux qux:bar], - ) - expect(found_categories.find { |c| c["ref"] == "foo" }["relative_url"]).to eq(foo.url) - expect(found_categories.find { |c| c["ref"] == "foo:bar" }["relative_url"]).to eq( - foobar.url, - ) - expect(found_categories.find { |c| c["ref"] == "bar:baz" }["relative_url"]).to eq( - foobarbaz.url, - ) - expect(found_categories.find { |c| c["ref"] == "qux" }["relative_url"]).to eq(qux.url) - expect(found_categories.find { |c| c["ref"] == "qux:bar" }["relative_url"]).to eq( - quxbar.url, - ) - end - end - end - - context "when not logged in" do - it "returns invalid access" do - get "/hashtags.json", params: { slugs: [], order: %w[category tag] } - expect(response.status).to eq(403) - end + context "when not logged in" do + it "returns invalid access" do + get "/hashtags.json", params: { slugs: [], order: %w[category tag] } + expect(response.status).to eq(403) end end end diff --git a/spec/services/hashtag_autocomplete_service_spec.rb b/spec/services/hashtag_autocomplete_service_spec.rb index 6419472a2bc..ed3d1960c3b 100644 --- a/spec/services/hashtag_autocomplete_service_spec.rb +++ b/spec/services/hashtag_autocomplete_service_spec.rb @@ -313,53 +313,6 @@ RSpec.describe HashtagAutocompleteService do end end - describe "#lookup_old" do - fab!(:tag2) { Fabricate(:tag, name: "fiction-books") } - - it "returns categories and tags in a hash format with the slug and url" do - result = service.lookup_old(%w[the-book-club great-books fiction-books]) - expect(result[:categories]).to eq({ "the-book-club" => "/c/the-book-club/#{category1.id}" }) - expect(result[:tags]).to eq( - { - "fiction-books" => "http://test.localhost/tag/fiction-books", - "great-books" => "http://test.localhost/tag/great-books", - }, - ) - end - - it "does not include categories the user cannot access" do - category1.update!(read_restricted: true) - result = service.lookup_old(%w[the-book-club great-books fiction-books]) - expect(result[:categories]).to eq({}) - end - - it "does not include tags the user cannot access" do - Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: ["great-books"]) - result = service.lookup_old(%w[the-book-club great-books fiction-books]) - expect(result[:tags]).to eq({ "fiction-books" => "http://test.localhost/tag/fiction-books" }) - end - - it "handles tags which have the ::tag suffix" do - result = service.lookup_old(%w[the-book-club great-books::tag fiction-books]) - expect(result[:tags]).to eq( - { - "fiction-books" => "http://test.localhost/tag/fiction-books", - "great-books" => "http://test.localhost/tag/great-books", - }, - ) - end - - context "when not tagging_enabled" do - before { SiteSetting.tagging_enabled = false } - - it "does not return tags" do - result = service.lookup_old(%w[the-book-club great-books fiction-books]) - expect(result[:categories]).to eq({ "the-book-club" => "/c/the-book-club/#{category1.id}" }) - expect(result[:tags]).to eq({}) - end - end - end - describe "#lookup" do fab!(:tag2) { Fabricate(:tag, name: "fiction-books") } diff --git a/spec/services/post_alerter_spec.rb b/spec/services/post_alerter_spec.rb index fe403d31d4a..18f7295f1b3 100644 --- a/spec/services/post_alerter_spec.rb +++ b/spec/services/post_alerter_spec.rb @@ -1332,8 +1332,7 @@ RSpec.describe PostAlerter do expect(JSON.parse(body)).to eq(payload) end - it "does not have invalid HTML in the excerpt when enable_experimental_hashtag_autocomplete is enabled" do - SiteSetting.enable_experimental_hashtag_autocomplete = true + it "does not have invalid HTML in the excerpt" do Fabricate(:category, slug: "random") Jobs.run_immediately! body = nil diff --git a/spec/system/hashtag_autocomplete_spec.rb b/spec/system/hashtag_autocomplete_spec.rb index 960ac099cdd..ef79c2d13e0 100644 --- a/spec/system/hashtag_autocomplete_spec.rb +++ b/spec/system/hashtag_autocomplete_spec.rb @@ -2,7 +2,7 @@ describe "Using #hashtag autocompletion to search for and lookup categories and tags", type: :system do - fab!(:user) { Fabricate(:user) } + fab!(:current_user) { Fabricate(:user) } fab!(:category) do Fabricate(:category, name: "Cool Category", slug: "cool-cat", topic_count: 3234) end @@ -16,10 +16,7 @@ describe "Using #hashtag autocompletion to search for and lookup categories and let(:uncategorized_category) { Category.find(SiteSetting.uncategorized_category_id) } let(:topic_page) { PageObjects::Pages::Topic.new } - before do - SiteSetting.enable_experimental_hashtag_autocomplete = true - sign_in user - end + before { sign_in(current_user) } def visit_topic_and_initiate_autocomplete(initiation_text: "something #co", expected_count: 2) topic_page.visit_topic_and_open_composer(topic) diff --git a/spec/tasks/hashtags_spec.rb b/spec/tasks/hashtags_spec.rb index 679f3e0e563..ac70bac1e49 100644 --- a/spec/tasks/hashtags_spec.rb +++ b/spec/tasks/hashtags_spec.rb @@ -9,9 +9,8 @@ RSpec.describe "tasks/hashtags" do describe "hashtag:mark_old_format_for_rebake" do fab!(:category) { Fabricate(:category, slug: "support") } - before { SiteSetting.enable_experimental_hashtag_autocomplete = false } - it "sets the baked_version to 0 for matching posts" do + hashtag_html = PrettyText.cook("#support").gsub("

", "").gsub("

", "") post_1 = Fabricate(:post, raw: "This is a cool #support hashtag") post_2 = Fabricate( @@ -19,10 +18,12 @@ RSpec.describe "tasks/hashtags" do raw: "Some other thing which will not match some weird custom thing", ) - - SiteSetting.enable_experimental_hashtag_autocomplete = true post_3 = Fabricate(:post, raw: "This is a cool #support hashtag") - SiteSetting.enable_experimental_hashtag_autocomplete = false + + # Update to use the old hashtag format. + post_1.update!( + cooked: post_1.cooked.gsub(hashtag_html, "#support"), + ) capture_stdout { Rake::Task["hashtags:mark_old_format_for_rebake"].invoke }