FIX: Do not cook icon with hashtags (#21676)
This commit makes some fundamental changes to how hashtag cooking and icon generation works in the new experimental hashtag autocomplete mode. Previously we cooked the appropriate SVG icon with the cooked hashtag, though this has proved inflexible especially for theming purposes. Instead, we now cook a data-ID attribute with the hashtag and add a new span as an icon placeholder. This is replaced on the client side with an icon (or a square span in the case of categories) on the client side via the decorateCooked API for posts and chat messages. This client side logic uses the generated hashtag, category, and channel CSS classes added in a previous commit. This is missing changes to the sidebar to use the new generated CSS classes and also colors and the split square for categories in the hashtag autocomplete menu -- I will tackle this in a separate PR so it is clearer.
This commit is contained in:
parent
ecb9a27e55
commit
0b3cf83e3c
|
@ -25,8 +25,7 @@ export default {
|
||||||
|
|
||||||
let generatedCssClasses = [];
|
let generatedCssClasses = [];
|
||||||
|
|
||||||
Object.values(getHashtagTypeClasses()).forEach((hashtagTypeClass) => {
|
Object.values(getHashtagTypeClasses()).forEach((hashtagType) => {
|
||||||
const hashtagType = new hashtagTypeClass(container);
|
|
||||||
hashtagType.preloadedData.forEach((model) => {
|
hashtagType.preloadedData.forEach((model) => {
|
||||||
generatedCssClasses = generatedCssClasses.concat(
|
generatedCssClasses = generatedCssClasses.concat(
|
||||||
hashtagType.generateColorCssClasses(model)
|
hashtagType.generateColorCssClasses(model)
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
|
import { replaceHashtagIconPlaceholder } from "discourse/lib/hashtag-autocomplete";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "hashtag-post-decorations",
|
||||||
|
after: "hashtag-css-generator",
|
||||||
|
|
||||||
|
initialize(container) {
|
||||||
|
const siteSettings = container.lookup("service:site-settings");
|
||||||
|
const site = container.lookup("service:site");
|
||||||
|
|
||||||
|
withPluginApi("0.8.7", (api) => {
|
||||||
|
if (siteSettings.enable_experimental_hashtag_autocomplete) {
|
||||||
|
api.decorateCookedElement(
|
||||||
|
(post) => replaceHashtagIconPlaceholder(post, site),
|
||||||
|
{
|
||||||
|
onlyStream: true,
|
||||||
|
id: "hashtag-icons",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -6,10 +6,10 @@ export default {
|
||||||
name: "register-hashtag-types",
|
name: "register-hashtag-types",
|
||||||
before: "hashtag-css-generator",
|
before: "hashtag-css-generator",
|
||||||
|
|
||||||
initialize() {
|
initialize(container) {
|
||||||
withPluginApi("0.8.7", (api) => {
|
withPluginApi("0.8.7", (api) => {
|
||||||
api.registerHashtagType("category", CategoryHashtagType);
|
api.registerHashtagType("category", new CategoryHashtagType(container));
|
||||||
api.registerHashtagType("tag", TagHashtagType);
|
api.registerHashtagType("tag", new TagHashtagType(container));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -46,7 +46,7 @@ export function isValidLink(link) {
|
||||||
return (
|
return (
|
||||||
link.classList.contains("track-link") ||
|
link.classList.contains("track-link") ||
|
||||||
!link.closest(
|
!link.closest(
|
||||||
".hashtag, .hashtag-cooked, .badge-category, .onebox-result, .onebox-body"
|
".hashtag, .hashtag-cooked, .hashtag-icon-placeholder, .badge-category, .onebox-result, .onebox-body"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { findRawTemplate } from "discourse-common/lib/raw-templates";
|
import { findRawTemplate } from "discourse-common/lib/raw-templates";
|
||||||
|
import domFromString from "discourse-common/lib/dom-from-string";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
import { INPUT_DELAY, isTesting } from "discourse-common/config/environment";
|
import { INPUT_DELAY, isTesting } from "discourse-common/config/environment";
|
||||||
import { cancel } from "@ember/runloop";
|
import { cancel } from "@ember/runloop";
|
||||||
|
@ -15,8 +16,8 @@ import { emojiUnescape } from "discourse/lib/text";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
|
|
||||||
let hashtagTypeClasses = {};
|
let hashtagTypeClasses = {};
|
||||||
export function registerHashtagType(type, typeClass) {
|
export function registerHashtagType(type, typeClassInstance) {
|
||||||
hashtagTypeClasses[type] = typeClass;
|
hashtagTypeClasses[type] = typeClassInstance;
|
||||||
}
|
}
|
||||||
export function cleanUpHashtagTypeClasses() {
|
export function cleanUpHashtagTypeClasses() {
|
||||||
hashtagTypeClasses = {};
|
hashtagTypeClasses = {};
|
||||||
|
@ -24,6 +25,24 @@ export function cleanUpHashtagTypeClasses() {
|
||||||
export function getHashtagTypeClasses() {
|
export function getHashtagTypeClasses() {
|
||||||
return hashtagTypeClasses;
|
return hashtagTypeClasses;
|
||||||
}
|
}
|
||||||
|
export function replaceHashtagIconPlaceholder(element, site) {
|
||||||
|
element.querySelectorAll(".hashtag-cooked").forEach((hashtagEl) => {
|
||||||
|
const iconPlaceholderEl = hashtagEl.querySelector(
|
||||||
|
".hashtag-icon-placeholder"
|
||||||
|
);
|
||||||
|
const hashtagType = hashtagEl.dataset.type;
|
||||||
|
const hashtagTypeClass = getHashtagTypeClasses()[hashtagType];
|
||||||
|
if (iconPlaceholderEl && hashtagTypeClass) {
|
||||||
|
const hashtagIconHTML = hashtagTypeClass
|
||||||
|
.generateIconHTML({
|
||||||
|
icon: site.hashtag_icons[hashtagType],
|
||||||
|
id: hashtagEl.dataset.id,
|
||||||
|
})
|
||||||
|
.trim();
|
||||||
|
iconPlaceholderEl.replaceWith(domFromString(hashtagIconHTML)[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up a textarea using the jQuery autocomplete plugin, specifically
|
* Sets up a textarea using the jQuery autocomplete plugin, specifically
|
||||||
|
@ -216,12 +235,12 @@ function _searchRequest(term, contextualHashtagConfiguration, resultFunc) {
|
||||||
data: { term, order: contextualHashtagConfiguration },
|
data: { term, order: contextualHashtagConfiguration },
|
||||||
});
|
});
|
||||||
currentSearch
|
currentSearch
|
||||||
.then((r) => {
|
.then((response) => {
|
||||||
r.results?.forEach((result) => {
|
response.results?.forEach((result) => {
|
||||||
// Convert :emoji: in the result text to HTML safely.
|
// Convert :emoji: in the result text to HTML safely.
|
||||||
result.text = htmlSafe(emojiUnescape(escapeExpression(result.text)));
|
result.text = htmlSafe(emojiUnescape(escapeExpression(result.text)));
|
||||||
});
|
});
|
||||||
resultFunc(r.results || CANCELLED_STATUS);
|
resultFunc(response.results || CANCELLED_STATUS);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
currentSearch = null;
|
currentSearch = null;
|
||||||
|
@ -235,7 +254,7 @@ function _findAndReplaceSeenHashtagPlaceholder(
|
||||||
hashtagSpan
|
hashtagSpan
|
||||||
) {
|
) {
|
||||||
contextualHashtagConfiguration.forEach((type) => {
|
contextualHashtagConfiguration.forEach((type) => {
|
||||||
// replace raw span for the hashtag with a cooked one
|
// Replace raw span for the hashtag with a cooked one
|
||||||
const matchingSeenHashtag = seenHashtags[type]?.[slugRef];
|
const matchingSeenHashtag = seenHashtags[type]?.[slugRef];
|
||||||
if (matchingSeenHashtag) {
|
if (matchingSeenHashtag) {
|
||||||
// NOTE: When changing the HTML structure here, you must also change
|
// NOTE: When changing the HTML structure here, you must also change
|
||||||
|
@ -244,8 +263,12 @@ function _findAndReplaceSeenHashtagPlaceholder(
|
||||||
link.classList.add("hashtag-cooked");
|
link.classList.add("hashtag-cooked");
|
||||||
link.href = matchingSeenHashtag.relative_url;
|
link.href = matchingSeenHashtag.relative_url;
|
||||||
link.dataset.type = type;
|
link.dataset.type = type;
|
||||||
|
link.dataset.id = matchingSeenHashtag.id;
|
||||||
link.dataset.slug = matchingSeenHashtag.slug;
|
link.dataset.slug = matchingSeenHashtag.slug;
|
||||||
link.innerHTML = `<svg class="fa d-icon d-icon-${matchingSeenHashtag.icon} svg-icon svg-node"><use href="#${matchingSeenHashtag.icon}"></use></svg><span>${matchingSeenHashtag.text}</span>`;
|
const hashtagType = new getHashtagTypeClasses()[type];
|
||||||
|
link.innerHTML = `${hashtagType.generateIconHTML(
|
||||||
|
matchingSeenHashtag
|
||||||
|
)}<span>${emojiUnescape(matchingSeenHashtag.text)}</span>`;
|
||||||
hashtagSpan.replaceWith(link);
|
hashtagSpan.replaceWith(link);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,4 +16,8 @@ export default class HashtagTypeBase {
|
||||||
generateColorCssClasses() {
|
generateColorCssClasses() {
|
||||||
throw "not implemented";
|
throw "not implemented";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateIconHTML() {
|
||||||
|
throw "not implemented";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,21 +12,25 @@ export default class CategoryHashtagType extends HashtagTypeBase {
|
||||||
return this.site.categories || [];
|
return this.site.categories || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
generateColorCssClasses(model) {
|
generateColorCssClasses(category) {
|
||||||
const generatedCssClasses = [];
|
const generatedCssClasses = [];
|
||||||
const backgroundGradient = [`var(--category-${model.id}-color) 50%`];
|
const backgroundGradient = [`var(--category-${category.id}-color) 50%`];
|
||||||
if (model.parentCategory) {
|
if (category.parentCategory) {
|
||||||
backgroundGradient.push(
|
backgroundGradient.push(
|
||||||
`var(--category-${model.parentCategory.id}-color) 50%`
|
`var(--category-${category.parentCategory.id}-color) 50%`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
backgroundGradient.push(`var(--category-${model.id}-color) 50%`);
|
backgroundGradient.push(`var(--category-${category.id}-color) 50%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
generatedCssClasses.push(`.hashtag-color--category-${model.id} {
|
generatedCssClasses.push(`.hashtag-color--category-${category.id} {
|
||||||
background: linear-gradient(90deg, ${backgroundGradient.join(", ")});
|
background: linear-gradient(90deg, ${backgroundGradient.join(", ")});
|
||||||
}`);
|
}`);
|
||||||
|
|
||||||
return generatedCssClasses;
|
return generatedCssClasses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateIconHTML(hashtag) {
|
||||||
|
return `<span class="hashtag-category-badge hashtag-color--${this.type}-${hashtag.id}"></span>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import HashtagTypeBase from "./base";
|
import HashtagTypeBase from "./base";
|
||||||
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
|
|
||||||
export default class TagHashtagType extends HashtagTypeBase {
|
export default class TagHashtagType extends HashtagTypeBase {
|
||||||
get type() {
|
get type() {
|
||||||
|
@ -12,4 +13,10 @@ export default class TagHashtagType extends HashtagTypeBase {
|
||||||
generateColorCssClasses() {
|
generateColorCssClasses() {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateIconHTML(hashtag) {
|
||||||
|
return iconHTML(hashtag.icon, {
|
||||||
|
class: `hashtag-color--${this.type}-${hashtag.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2226,10 +2226,11 @@ class PluginApi {
|
||||||
* This is used when generating CSS classes in the hashtag-css-generator.
|
* This is used when generating CSS classes in the hashtag-css-generator.
|
||||||
*
|
*
|
||||||
* @param {string} type - The type of the hashtag.
|
* @param {string} type - The type of the hashtag.
|
||||||
* @param {Class} typeClass - The class of the hashtag type.
|
* @param {Class} typeClassInstance - The initialized class of the hashtag type, which
|
||||||
|
* needs the `container`.
|
||||||
*/
|
*/
|
||||||
registerHashtagType(type, typeClass) {
|
registerHashtagType(type, typeClassInstance) {
|
||||||
registerHashtagType(type, typeClass);
|
registerHashtagType(type, typeClassInstance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -699,7 +699,7 @@ export default {
|
||||||
],
|
],
|
||||||
displayed_about_plugin_stat_groups: ["chat_messages"],
|
displayed_about_plugin_stat_groups: ["chat_messages"],
|
||||||
hashtag_configurations: { "topic-composer": ["category", "tag"] },
|
hashtag_configurations: { "topic-composer": ["category", "tag"] },
|
||||||
hashtag_icons: ["folder", "tag"],
|
hashtag_icons: { "category": "folder", "tag": "tag" },
|
||||||
anonymous_sidebar_sections: [
|
anonymous_sidebar_sections: [
|
||||||
{
|
{
|
||||||
id: 111,
|
id: 111,
|
||||||
|
|
|
@ -29,6 +29,7 @@ function addHashtag(buffer, matches, state) {
|
||||||
["href", result.relative_url],
|
["href", result.relative_url],
|
||||||
["data-type", result.type],
|
["data-type", result.type],
|
||||||
["data-slug", result.slug],
|
["data-slug", result.slug],
|
||||||
|
["data-id", result.id],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Most cases these will be the exact same, one standout is categories
|
// Most cases these will be the exact same, one standout is categories
|
||||||
|
@ -40,20 +41,7 @@ function addHashtag(buffer, matches, state) {
|
||||||
token.block = false;
|
token.block = false;
|
||||||
buffer.push(token);
|
buffer.push(token);
|
||||||
|
|
||||||
token = new state.Token("svg_open", "svg", 1);
|
addIconPlaceholder(buffer, state);
|
||||||
token.block = false;
|
|
||||||
token.attrs = [
|
|
||||||
["class", `fa d-icon d-icon-${result.icon} svg-icon svg-node`],
|
|
||||||
];
|
|
||||||
buffer.push(token);
|
|
||||||
|
|
||||||
token = new state.Token("use_open", "use", 1);
|
|
||||||
token.block = false;
|
|
||||||
token.attrs = [["href", `#${result.icon}`]];
|
|
||||||
buffer.push(token);
|
|
||||||
|
|
||||||
buffer.push(new state.Token("use_close", "use", -1));
|
|
||||||
buffer.push(new state.Token("svg_close", "svg", -1));
|
|
||||||
|
|
||||||
token = new state.Token("span_open", "span", 1);
|
token = new state.Token("span_open", "span", 1);
|
||||||
token.block = false;
|
token.block = false;
|
||||||
|
@ -82,24 +70,22 @@ function addHashtag(buffer, matches, state) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The svg icon is not baked into the HTML because we want
|
||||||
|
// to be able to use icon replacement via renderIcon, and
|
||||||
|
// because different hashtag types may render icons/CSS
|
||||||
|
// classes differently.
|
||||||
|
//
|
||||||
|
// Instead, the UI will dynamically replace these where hashtags
|
||||||
|
// are rendered, like within posts, using decorateCooked* APIs.
|
||||||
|
function addIconPlaceholder(buffer, state) {
|
||||||
|
const token = new state.Token("span_open", "span", 1);
|
||||||
|
token.block = false;
|
||||||
|
token.attrs = [["class", "hashtag-icon-placeholder"]];
|
||||||
|
buffer.push(token);
|
||||||
|
buffer.push(new state.Token("span_close", "span", -1));
|
||||||
|
}
|
||||||
|
|
||||||
export function setup(helper) {
|
export function setup(helper) {
|
||||||
const opts = helper.getOptions();
|
|
||||||
|
|
||||||
// we do this because plugins can register their own hashtag data
|
|
||||||
// sources which specify an icon, and each icon must be allowlisted
|
|
||||||
// or it will not render in the markdown pipeline
|
|
||||||
const hashtagIconAllowList = opts.hashtagIcons
|
|
||||||
? opts.hashtagIcons
|
|
||||||
.concat(["hashtag"])
|
|
||||||
.map((icon) => {
|
|
||||||
return [
|
|
||||||
`svg[class=fa d-icon d-icon-${icon} svg-icon svg-node]`,
|
|
||||||
`use[href=#${icon}]`,
|
|
||||||
];
|
|
||||||
})
|
|
||||||
.flat()
|
|
||||||
: [];
|
|
||||||
|
|
||||||
helper.registerPlugin((md) => {
|
helper.registerPlugin((md) => {
|
||||||
if (
|
if (
|
||||||
md.options.discourse.limitedSiteSettings
|
md.options.discourse.limitedSiteSettings
|
||||||
|
@ -114,13 +100,13 @@ export function setup(helper) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
helper.allowList(
|
helper.allowList([
|
||||||
hashtagIconAllowList.concat([
|
|
||||||
"a.hashtag-cooked",
|
"a.hashtag-cooked",
|
||||||
"span.hashtag-raw",
|
"span.hashtag-raw",
|
||||||
|
"span.hashtag-icon-placeholder",
|
||||||
"a[data-type]",
|
"a[data-type]",
|
||||||
"a[data-slug]",
|
"a[data-slug]",
|
||||||
"a[data-ref]",
|
"a[data-ref]",
|
||||||
])
|
"a[data-id]",
|
||||||
);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ const NONE = 0;
|
||||||
const MENTION = 1;
|
const MENTION = 1;
|
||||||
const HASHTAG_LINK = 2;
|
const HASHTAG_LINK = 2;
|
||||||
const HASHTAG_SPAN = 3;
|
const HASHTAG_SPAN = 3;
|
||||||
|
const HASHTAG_ICON_SPAN = 4;
|
||||||
|
|
||||||
export function setup(helper) {
|
export function setup(helper) {
|
||||||
const opts = helper.getOptions();
|
const opts = helper.getOptions();
|
||||||
|
@ -97,6 +98,7 @@ export function setup(helper) {
|
||||||
// We scan once to mark tokens that must be skipped because they are
|
// We scan once to mark tokens that must be skipped because they are
|
||||||
// mentions or hashtags
|
// mentions or hashtags
|
||||||
let lastType = NONE;
|
let lastType = NONE;
|
||||||
|
let currentType = NONE;
|
||||||
for (let i = 0; i < tokens.length; ++i) {
|
for (let i = 0; i < tokens.length; ++i) {
|
||||||
const currentToken = tokens[i];
|
const currentToken = tokens[i];
|
||||||
|
|
||||||
|
@ -109,14 +111,26 @@ export function setup(helper) {
|
||||||
currentToken.attrs.some(
|
currentToken.attrs.some(
|
||||||
(attr) =>
|
(attr) =>
|
||||||
attr[0] === "class" &&
|
attr[0] === "class" &&
|
||||||
(attr[1].includes("hashtag") ||
|
(attr[1] === "hashtag" ||
|
||||||
attr[1].includes("hashtag-cooked"))
|
attr[1] === "hashtag-cooked" ||
|
||||||
|
attr[1] === "hashtag-raw")
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
lastType =
|
lastType =
|
||||||
currentToken.type === "link_open" ? HASHTAG_LINK : HASHTAG_SPAN;
|
currentToken.type === "link_open" ? HASHTAG_LINK : HASHTAG_SPAN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentToken.type === "span_open" &&
|
||||||
|
currentToken.attrs &&
|
||||||
|
currentToken.attrs.some(
|
||||||
|
(attr) =>
|
||||||
|
attr[0] === "class" && attr[1] === "hashtag-icon-placeholder"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
currentType = HASHTAG_ICON_SPAN;
|
||||||
|
}
|
||||||
|
|
||||||
if (lastType !== NONE) {
|
if (lastType !== NONE) {
|
||||||
currentToken.skipReplace = true;
|
currentToken.skipReplace = true;
|
||||||
}
|
}
|
||||||
|
@ -124,7 +138,9 @@ export function setup(helper) {
|
||||||
if (
|
if (
|
||||||
(lastType === MENTION && currentToken.type === "mention_close") ||
|
(lastType === MENTION && currentToken.type === "mention_close") ||
|
||||||
(lastType === HASHTAG_LINK && currentToken.type === "link_close") ||
|
(lastType === HASHTAG_LINK && currentToken.type === "link_close") ||
|
||||||
(lastType === HASHTAG_SPAN && currentToken.type === "span_close")
|
(lastType === HASHTAG_SPAN &&
|
||||||
|
currentToken.type === "span_close" &&
|
||||||
|
currentType !== HASHTAG_ICON_SPAN)
|
||||||
) {
|
) {
|
||||||
lastType = NONE;
|
lastType = NONE;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,14 @@ a.hashtag-cooked {
|
||||||
svg {
|
svg {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hashtag-category-badge {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
margin-right: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hashtag-autocomplete {
|
.hashtag-autocomplete {
|
||||||
|
|
|
@ -241,7 +241,7 @@ class SiteSerializer < ApplicationSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def hashtag_icons
|
def hashtag_icons
|
||||||
HashtagAutocompleteService.data_source_icons
|
HashtagAutocompleteService.data_source_icon_map
|
||||||
end
|
end
|
||||||
|
|
||||||
def displayed_about_plugin_stat_groups
|
def displayed_about_plugin_stat_groups
|
||||||
|
|
|
@ -19,6 +19,7 @@ class CategoryHashtagDataSource
|
||||||
item.description = category.description_text
|
item.description = category.description_text
|
||||||
item.icon = icon
|
item.icon = icon
|
||||||
item.relative_url = category.url
|
item.relative_url = category.url
|
||||||
|
item.id = category.id
|
||||||
|
|
||||||
# Single-level category heirarchy should be enough to distinguish between
|
# Single-level category heirarchy should be enough to distinguish between
|
||||||
# categories here.
|
# categories here.
|
||||||
|
|
|
@ -34,8 +34,8 @@ class HashtagAutocompleteService
|
||||||
data_sources.map(&:type)
|
data_sources.map(&:type)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.data_source_icons
|
def self.data_source_icon_map
|
||||||
data_sources.map(&:icon)
|
data_sources.map { |ds| [ds.type, ds.icon] }.to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.data_source_from_type(type)
|
def self.data_source_from_type(type)
|
||||||
|
@ -88,6 +88,10 @@ class HashtagAutocompleteService
|
||||||
# item, used for the cooked hashtags, e.g. /c/2/staff
|
# item, used for the cooked hashtags, e.g. /c/2/staff
|
||||||
attr_accessor :relative_url
|
attr_accessor :relative_url
|
||||||
|
|
||||||
|
# The ID of the resource that is represented by the autocomplete item,
|
||||||
|
# e.g. category.id, tag.id
|
||||||
|
attr_accessor :id
|
||||||
|
|
||||||
def initialize(params = {})
|
def initialize(params = {})
|
||||||
@relative_url = params[:relative_url]
|
@relative_url = params[:relative_url]
|
||||||
@text = params[:text]
|
@text = params[:text]
|
||||||
|
@ -96,6 +100,7 @@ class HashtagAutocompleteService
|
||||||
@type = params[:type]
|
@type = params[:type]
|
||||||
@ref = params[:ref]
|
@ref = params[:ref]
|
||||||
@slug = params[:slug]
|
@slug = params[:slug]
|
||||||
|
@id = params[:id]
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_h
|
def to_h
|
||||||
|
@ -107,6 +112,7 @@ class HashtagAutocompleteService
|
||||||
type: self.type,
|
type: self.type,
|
||||||
ref: self.ref,
|
ref: self.ref,
|
||||||
slug: self.slug,
|
slug: self.slug,
|
||||||
|
id: self.id,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,6 +27,7 @@ class TagHashtagDataSource
|
||||||
item.slug = tag.name
|
item.slug = tag.name
|
||||||
item.relative_url = tag.url
|
item.relative_url = tag.url
|
||||||
item.icon = icon
|
item.icon = icon
|
||||||
|
item.id = tag.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
private_class_method :tag_to_hashtag_item
|
private_class_method :tag_to_hashtag_item
|
||||||
|
@ -66,7 +67,11 @@ class TagHashtagDataSource
|
||||||
TagsController
|
TagsController
|
||||||
.tag_counts_json(tags_with_counts, guardian)
|
.tag_counts_json(tags_with_counts, guardian)
|
||||||
.take(limit)
|
.take(limit)
|
||||||
.map { |tag| tag_to_hashtag_item(tag, guardian) }
|
.map do |tag|
|
||||||
|
# We want the actual ID here not the `name` as tag_counts_json gives us.
|
||||||
|
tag[:id] = tags_with_counts.find { |t| t.name == tag[:name] }.id
|
||||||
|
tag_to_hashtag_item(tag, guardian)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.search_sort(search_results, _)
|
def self.search_sort(search_results, _)
|
||||||
|
|
|
@ -22,6 +22,7 @@ class ExcerptParser < Nokogiri::XML::SAX::Document
|
||||||
@keep_svg = options[:keep_svg] == true
|
@keep_svg = options[:keep_svg] == true
|
||||||
@remap_emoji = options[:remap_emoji] == true
|
@remap_emoji = options[:remap_emoji] == true
|
||||||
@start_excerpt = false
|
@start_excerpt = false
|
||||||
|
@start_hashtag_icon = false
|
||||||
@in_details_depth = 0
|
@in_details_depth = 0
|
||||||
@summary_contents = +""
|
@summary_contents = +""
|
||||||
@detail_contents = +""
|
@detail_contents = +""
|
||||||
|
@ -112,10 +113,14 @@ class ExcerptParser < Nokogiri::XML::SAX::Document
|
||||||
when "header"
|
when "header"
|
||||||
@in_quote = !@keep_onebox_source if attributes.include?(%w[class source])
|
@in_quote = !@keep_onebox_source if attributes.include?(%w[class source])
|
||||||
when "div", "span"
|
when "div", "span"
|
||||||
if attributes.include?(%w[class excerpt])
|
attributes = Hash[*attributes.flatten]
|
||||||
|
if attributes["class"]&.include?("excerpt")
|
||||||
@excerpt = +""
|
@excerpt = +""
|
||||||
@current_length = 0
|
@current_length = 0
|
||||||
@start_excerpt = true
|
@start_excerpt = true
|
||||||
|
elsif attributes["class"]&.include?("hashtag-icon-placeholder")
|
||||||
|
@start_hashtag_icon = true
|
||||||
|
include_tag(name, attributes)
|
||||||
end
|
end
|
||||||
when "details"
|
when "details"
|
||||||
@detail_contents = +"" if @in_details_depth == 0
|
@detail_contents = +"" if @in_details_depth == 0
|
||||||
|
@ -180,6 +185,7 @@ class ExcerptParser < Nokogiri::XML::SAX::Document
|
||||||
@in_summary = false if @in_details_depth == 1
|
@in_summary = false if @in_details_depth == 1
|
||||||
when "div", "span"
|
when "div", "span"
|
||||||
throw :done if @start_excerpt
|
throw :done if @start_excerpt
|
||||||
|
characters("</span>", truncate: false, count_it: false, encode: false) if @start_hashtag_icon
|
||||||
when "svg"
|
when "svg"
|
||||||
characters("</svg>", truncate: false, count_it: false, encode: false) if @keep_svg
|
characters("</svg>", truncate: false, count_it: false, encode: false) if @keep_svg
|
||||||
@in_svg = false
|
@in_svg = false
|
||||||
|
|
|
@ -224,10 +224,8 @@ module PrettyText
|
||||||
.ordered_types_for_context(opts[:hashtag_context])
|
.ordered_types_for_context(opts[:hashtag_context])
|
||||||
.map { |t| "'#{t}'" }
|
.map { |t| "'#{t}'" }
|
||||||
.join(",")
|
.join(",")
|
||||||
hashtag_icons_as_js =
|
|
||||||
HashtagAutocompleteService.data_source_icons.map { |i| "'#{i}'" }.join(",")
|
|
||||||
buffer << "__optInput.hashtagTypesInPriorityOrder = [#{hashtag_types_as_js}];\n"
|
buffer << "__optInput.hashtagTypesInPriorityOrder = [#{hashtag_types_as_js}];\n"
|
||||||
buffer << "__optInput.hashtagIcons = [#{hashtag_icons_as_js}];\n"
|
buffer << "__optInput.hashtagIcons = #{HashtagAutocompleteService.data_source_icon_map.to_json};\n"
|
||||||
|
|
||||||
buffer << "__textOptions = __buildOptions(__optInput);\n"
|
buffer << "__textOptions = __buildOptions(__optInput);\n"
|
||||||
buffer << ("__pt = new __PrettyText(__textOptions);")
|
buffer << ("__pt = new __PrettyText(__textOptions);")
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { decorateGithubOneboxBody } from "discourse/initializers/onebox-decorators";
|
import { decorateGithubOneboxBody } from "discourse/initializers/onebox-decorators";
|
||||||
|
import { replaceHashtagIconPlaceholder } from "discourse/lib/hashtag-autocomplete";
|
||||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
import highlightSyntax from "discourse/lib/highlight-syntax";
|
import highlightSyntax from "discourse/lib/highlight-syntax";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
@ -12,6 +13,7 @@ export default {
|
||||||
|
|
||||||
initializeWithPluginApi(api, container) {
|
initializeWithPluginApi(api, container) {
|
||||||
const siteSettings = container.lookup("service:site-settings");
|
const siteSettings = container.lookup("service:site-settings");
|
||||||
|
const site = container.lookup("service:site");
|
||||||
api.decorateChatMessage((element) => decorateGithubOneboxBody(element), {
|
api.decorateChatMessage((element) => decorateGithubOneboxBody(element), {
|
||||||
id: "onebox-github-body",
|
id: "onebox-github-body",
|
||||||
});
|
});
|
||||||
|
@ -70,6 +72,11 @@ export default {
|
||||||
id: "lightbox",
|
id: "lightbox",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
api.decorateChatMessage(
|
||||||
|
(element) => replaceHashtagIconPlaceholder(element, site),
|
||||||
|
{ id: "hashtagIcons" }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_getScrollParent(node, maxParentSelector) {
|
_getScrollParent(node, maxParentSelector) {
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
withPluginApi("0.12.1", (api) => {
|
withPluginApi("0.12.1", (api) => {
|
||||||
api.registerHashtagType("channel", ChannelHashtagType);
|
api.registerHashtagType("channel", new ChannelHashtagType(container));
|
||||||
|
|
||||||
api.registerChatComposerButton({
|
api.registerChatComposerButton({
|
||||||
id: "chat-upload-btn",
|
id: "chat-upload-btn",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import HashtagTypeBase from "discourse/lib/hashtag-types/base";
|
import HashtagTypeBase from "discourse/lib/hashtag-types/base";
|
||||||
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default class ChannelHashtagType extends HashtagTypeBase {
|
export default class ChannelHashtagType extends HashtagTypeBase {
|
||||||
|
@ -17,9 +18,15 @@ export default class ChannelHashtagType extends HashtagTypeBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
generateColorCssClasses(model) {
|
generateColorCssClasses(channel) {
|
||||||
return [
|
return [
|
||||||
`.hashtag-color--${this.type}-${model.id} { color: var(--category-${model.chatable.id}-color); }`,
|
`.hashtag-cooked .d-icon.hashtag-color--${this.type}-${channel.id} { color: var(--category-${channel.chatable.id}-color); }`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateIconHTML(hashtag) {
|
||||||
|
return iconHTML(hashtag.icon, {
|
||||||
|
class: `hashtag-color--${this.type}-${hashtag.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ module Chat
|
||||||
item.icon = icon
|
item.icon = icon
|
||||||
item.relative_url = channel.relative_url
|
item.relative_url = channel.relative_url
|
||||||
item.type = "channel"
|
item.type = "channel"
|
||||||
|
item.id = channel.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -195,7 +195,7 @@ describe Chat::ChannelArchiveService do
|
||||||
expect(@channel_archive.reload.complete?).to eq(true)
|
expect(@channel_archive.reload.complete?).to eq(true)
|
||||||
pm_topic = Topic.private_messages.last
|
pm_topic = Topic.private_messages.last
|
||||||
expect(pm_topic.first_post.cooked).to include(
|
expect(pm_topic.first_post.cooked).to include(
|
||||||
"<a class=\"hashtag-cooked\" href=\"#{channel.relative_url}\" data-type=\"channel\" data-slug=\"#{channel.slug}\" data-ref=\"#{channel.slug}::channel\"><svg class=\"fa d-icon d-icon-comment svg-icon svg-node\"><use href=\"#comment\"></use></svg><span>#{channel.title(user)}</span></a>",
|
"<a class=\"hashtag-cooked\" href=\"#{channel.relative_url}\" data-type=\"channel\" data-slug=\"#{channel.slug}\" data-id=\"#{channel.id}\" data-ref=\"#{channel.slug}::channel\"><span class=\"hashtag-icon-placeholder\"></span><span>#{channel.title(user)}</span></a>",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -41,6 +41,7 @@ RSpec.describe Chat::ChannelHashtagDataSource do
|
||||||
text: "Zany Things",
|
text: "Zany Things",
|
||||||
description: "Just weird stuff",
|
description: "Just weird stuff",
|
||||||
icon: "comment",
|
icon: "comment",
|
||||||
|
id: channel1.id,
|
||||||
type: "channel",
|
type: "channel",
|
||||||
ref: nil,
|
ref: nil,
|
||||||
slug: "random",
|
slug: "random",
|
||||||
|
@ -60,6 +61,7 @@ RSpec.describe Chat::ChannelHashtagDataSource do
|
||||||
text: "Secret Stuff",
|
text: "Secret Stuff",
|
||||||
description: nil,
|
description: nil,
|
||||||
icon: "comment",
|
icon: "comment",
|
||||||
|
id: channel2.id,
|
||||||
type: "channel",
|
type: "channel",
|
||||||
ref: nil,
|
ref: nil,
|
||||||
slug: "secret",
|
slug: "secret",
|
||||||
|
@ -94,6 +96,7 @@ RSpec.describe Chat::ChannelHashtagDataSource do
|
||||||
text: "Zany Things",
|
text: "Zany Things",
|
||||||
description: "Just weird stuff",
|
description: "Just weird stuff",
|
||||||
icon: "comment",
|
icon: "comment",
|
||||||
|
id: channel1.id,
|
||||||
type: "channel",
|
type: "channel",
|
||||||
ref: nil,
|
ref: nil,
|
||||||
slug: "random",
|
slug: "random",
|
||||||
|
@ -109,6 +112,7 @@ RSpec.describe Chat::ChannelHashtagDataSource do
|
||||||
text: "Zany Things",
|
text: "Zany Things",
|
||||||
description: "Just weird stuff",
|
description: "Just weird stuff",
|
||||||
icon: "comment",
|
icon: "comment",
|
||||||
|
id: channel1.id,
|
||||||
type: "channel",
|
type: "channel",
|
||||||
ref: nil,
|
ref: nil,
|
||||||
slug: "random",
|
slug: "random",
|
||||||
|
@ -127,6 +131,7 @@ RSpec.describe Chat::ChannelHashtagDataSource do
|
||||||
text: "Secret Stuff",
|
text: "Secret Stuff",
|
||||||
description: nil,
|
description: nil,
|
||||||
icon: "comment",
|
icon: "comment",
|
||||||
|
id: channel2.id,
|
||||||
type: "channel",
|
type: "channel",
|
||||||
ref: nil,
|
ref: nil,
|
||||||
slug: "secret",
|
slug: "secret",
|
||||||
|
|
|
@ -259,7 +259,7 @@ describe Chat::Message do
|
||||||
cooked = described_class.cook("##{category.slug}", user_id: user.id)
|
cooked = described_class.cook("##{category.slug}", user_id: user.id)
|
||||||
|
|
||||||
expect(cooked).to eq(
|
expect(cooked).to eq(
|
||||||
"<p><a class=\"hashtag-cooked\" href=\"#{category.url}\" data-type=\"category\" data-slug=\"#{category.slug}\"><svg class=\"fa d-icon d-icon-folder svg-icon svg-node\"><use href=\"#folder\"></use></svg><span>#{category.name}</span></a></p>",
|
"<p><a class=\"hashtag-cooked\" href=\"#{category.url}\" data-type=\"category\" data-slug=\"#{category.slug}\" data-id=\"#{category.id}\"><span class=\"hashtag-icon-placeholder\"></span><span>#{category.name}</span></a></p>",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -71,13 +71,13 @@ describe "Using #hashtag autocompletion to search for and lookup channels",
|
||||||
cooked_hashtags = page.all(".hashtag-cooked", count: 3)
|
cooked_hashtags = page.all(".hashtag-cooked", count: 3)
|
||||||
|
|
||||||
expect(cooked_hashtags[0]["outerHTML"]).to eq(<<~HTML.chomp)
|
expect(cooked_hashtags[0]["outerHTML"]).to eq(<<~HTML.chomp)
|
||||||
<a class=\"hashtag-cooked\" href=\"#{channel2.relative_url}\" data-type=\"channel\" data-slug=\"random\"><svg class=\"fa d-icon d-icon-comment svg-icon svg-node\"><use href=\"#comment\"></use></svg><span>Random</span></a>
|
<a class=\"hashtag-cooked\" href=\"#{channel2.relative_url}\" data-type=\"channel\" data-slug=\"random\" data-id=\"#{channel2.id}\"><svg class=\"fa d-icon d-icon-comment svg-icon hashtag-color--channel-#{channel2.id} svg-string\" xmlns=\"http://www.w3.org/2000/svg\"><use href=\"#comment\"></use></svg><span>Random</span></a>
|
||||||
HTML
|
HTML
|
||||||
expect(cooked_hashtags[1]["outerHTML"]).to eq(<<~HTML.chomp)
|
expect(cooked_hashtags[1]["outerHTML"]).to eq(<<~HTML.chomp)
|
||||||
<a class=\"hashtag-cooked\" href=\"#{category.url}\" data-type=\"category\" data-slug=\"raspberry-beret\"><svg class=\"fa d-icon d-icon-folder svg-icon svg-node\"><use href=\"#folder\"></use></svg><span>Raspberry</span></a>
|
<a class=\"hashtag-cooked\" href=\"#{category.url}\" data-type=\"category\" data-slug=\"raspberry-beret\" data-id="#{category.id}"><span class=\"hashtag-category-badge hashtag-color--category-#{category.id}\"></span><span>Raspberry</span></a>
|
||||||
HTML
|
HTML
|
||||||
expect(cooked_hashtags[2]["outerHTML"]).to eq(<<~HTML.chomp)
|
expect(cooked_hashtags[2]["outerHTML"]).to eq(<<~HTML.chomp)
|
||||||
<a class=\"hashtag-cooked\" href=\"#{tag.url}\" data-type=\"tag\" data-slug=\"razed\"><svg class=\"fa d-icon d-icon-tag svg-icon svg-node\"><use href=\"#tag\"></use></svg><span>razed</span></a>
|
<a class=\"hashtag-cooked\" href=\"#{tag.url}\" data-type=\"tag\" data-slug=\"razed\" data-id="#{tag.id}"><svg class=\"fa d-icon d-icon-tag svg-icon hashtag-color--tag-#{tag.id} svg-string\" xmlns=\"http://www.w3.org/2000/svg\"><use href=\"#tag\"></use></svg><span>razed</span></a>
|
||||||
HTML
|
HTML
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -59,7 +59,7 @@ acceptance("Chat | Hashtag CSS Generator", function (needs) {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
cssTag.innerHTML,
|
cssTag.innerHTML,
|
||||||
|
|
||||||
".hashtag-color--category-1 {\n background: linear-gradient(90deg, var(--category-1-color) 50%, var(--category-1-color) 50%);\n}\n.hashtag-color--category-2 {\n background: linear-gradient(90deg, var(--category-2-color) 50%, var(--category-2-color) 50%);\n}\n.hashtag-color--category-4 {\n background: linear-gradient(90deg, var(--category-4-color) 50%, var(--category-1-color) 50%);\n}\n.hashtag-color--channel-44 { color: var(--category-1-color); }\n.hashtag-color--channel-74 { color: var(--category-2-color); }\n.hashtag-color--channel-88 { color: var(--category-4-color); }"
|
".hashtag-color--category-1 {\n background: linear-gradient(90deg, var(--category-1-color) 50%, var(--category-1-color) 50%);\n}\n.hashtag-color--category-2 {\n background: linear-gradient(90deg, var(--category-2-color) 50%, var(--category-2-color) 50%);\n}\n.hashtag-color--category-4 {\n background: linear-gradient(90deg, var(--category-4-color) 50%, var(--category-1-color) 50%);\n}\n.hashtag-cooked .d-icon.hashtag-color--channel-44 { color: var(--category-1-color); }\n.hashtag-cooked .d-icon.hashtag-color--channel-74 { color: var(--category-2-color); }\n.hashtag-cooked .d-icon.hashtag-color--channel-88 { color: var(--category-4-color); }"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -572,7 +572,7 @@ RSpec.describe Email::Sender do
|
||||||
reply.rebake!
|
reply.rebake!
|
||||||
Email::Sender.new(message, :valid_type).send
|
Email::Sender.new(message, :valid_type).send
|
||||||
expected = <<~HTML
|
expected = <<~HTML
|
||||||
<a href=\"#{Discourse.base_url}#{category.url}\" data-type=\"category\" data-slug=\"dev\" style=\"text-decoration: none; font-weight: bold; color: #006699;\"><span>#dev</span>
|
<a href=\"#{Discourse.base_url}#{category.url}\" data-type=\"category\" data-slug=\"dev\" data-id=\"#{category.id}\" style=\"text-decoration: none; font-weight: bold; color: #006699;\"><span>#dev</span>
|
||||||
HTML
|
HTML
|
||||||
expect(message.html_part.body.to_s).to include(expected.chomp)
|
expect(message.html_part.body.to_s).to include(expected.chomp)
|
||||||
end
|
end
|
||||||
|
|
|
@ -178,13 +178,20 @@ RSpec.describe Oneboxer do
|
||||||
expect(preview("/u/#{user.username}")).to include("Thunderland")
|
expect(preview("/u/#{user.username}")).to include("Thunderland")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "includes hashtag HTML and icons" do
|
it "includes hashtag HTML" do
|
||||||
SiteSetting.enable_experimental_hashtag_autocomplete = true
|
SiteSetting.enable_experimental_hashtag_autocomplete = true
|
||||||
category = Fabricate(:category, slug: "random")
|
category = Fabricate(:category, slug: "random")
|
||||||
Fabricate(:tag, name: "bug")
|
tag = Fabricate(:tag, name: "bug")
|
||||||
public_post = Fabricate(:post, raw: "This post has some hashtags, #random and #bug")
|
public_post = Fabricate(:post, raw: "This post has some hashtags, #random and #bug")
|
||||||
expect(preview(public_post.url).chomp).to include(<<~HTML.chomp)
|
preview =
|
||||||
<a class="hashtag-cooked" href="#{category.url}" data-type="category" data-slug="random"><svg class="fa d-icon d-icon-folder svg-icon svg-node"><use href="#folder"></use></svg>#{category.name}</a> and <a class="hashtag-cooked" href="/tag/bug" data-type="tag" data-slug="bug"><svg class="fa d-icon d-icon-tag svg-icon svg-node"><use href="#tag"></use></svg>bug</a>
|
Nokogiri::HTML5
|
||||||
|
.fragment(preview(public_post.url).chomp)
|
||||||
|
.css("blockquote")
|
||||||
|
.inner_html
|
||||||
|
.chomp
|
||||||
|
.strip
|
||||||
|
expect(preview).to eq(<<~HTML.chomp.strip)
|
||||||
|
This post has some hashtags, <a class="hashtag-cooked" href="#{category.url}" data-type="category" data-slug="random" data-id="#{category.id}"><span class="hashtag-icon-placeholder"></span>#{category.name}</a> and <a class="hashtag-cooked" href="#{tag.url}" data-type="tag" data-slug="bug" data-id="#{tag.id}"><span class="hashtag-icon-placeholder"></span>#{tag.name}</a>
|
||||||
HTML
|
HTML
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -68,6 +68,7 @@ RSpec.describe PrettyText::Helpers do
|
||||||
text: "somecooltag",
|
text: "somecooltag",
|
||||||
description: "Coolest things ever",
|
description: "Coolest things ever",
|
||||||
icon: "tag",
|
icon: "tag",
|
||||||
|
id: tag.id,
|
||||||
slug: "somecooltag",
|
slug: "somecooltag",
|
||||||
ref: "somecooltag::tag",
|
ref: "somecooltag::tag",
|
||||||
type: "tag",
|
type: "tag",
|
||||||
|
@ -85,6 +86,7 @@ RSpec.describe PrettyText::Helpers do
|
||||||
text: "Some Awesome Category",
|
text: "Some Awesome Category",
|
||||||
description: "Really great stuff here",
|
description: "Really great stuff here",
|
||||||
icon: "folder",
|
icon: "folder",
|
||||||
|
id: category.id,
|
||||||
slug: "someawesomecategory",
|
slug: "someawesomecategory",
|
||||||
ref: "someawesomecategory::category",
|
ref: "someawesomecategory::category",
|
||||||
type: "category",
|
type: "category",
|
||||||
|
@ -101,6 +103,7 @@ RSpec.describe PrettyText::Helpers do
|
||||||
text: "Some Awesome Category",
|
text: "Some Awesome Category",
|
||||||
description: "Really great stuff here",
|
description: "Really great stuff here",
|
||||||
icon: "folder",
|
icon: "folder",
|
||||||
|
id: category.id,
|
||||||
slug: "someawesomecategory",
|
slug: "someawesomecategory",
|
||||||
ref: "someawesomecategory",
|
ref: "someawesomecategory",
|
||||||
type: "category",
|
type: "category",
|
||||||
|
@ -115,6 +118,7 @@ RSpec.describe PrettyText::Helpers do
|
||||||
text: "somecooltag",
|
text: "somecooltag",
|
||||||
description: "Coolest things ever",
|
description: "Coolest things ever",
|
||||||
icon: "tag",
|
icon: "tag",
|
||||||
|
id: tag.id,
|
||||||
slug: "somecooltag",
|
slug: "somecooltag",
|
||||||
ref: "somecooltag",
|
ref: "somecooltag",
|
||||||
type: "tag",
|
type: "tag",
|
||||||
|
@ -128,6 +132,7 @@ RSpec.describe PrettyText::Helpers do
|
||||||
text: "Some Awesome Category",
|
text: "Some Awesome Category",
|
||||||
description: "Really great stuff here",
|
description: "Really great stuff here",
|
||||||
icon: "folder",
|
icon: "folder",
|
||||||
|
id: category.id,
|
||||||
slug: "someawesomecategory",
|
slug: "someawesomecategory",
|
||||||
ref: "someawesomecategory",
|
ref: "someawesomecategory",
|
||||||
type: "category",
|
type: "category",
|
||||||
|
@ -150,6 +155,7 @@ RSpec.describe PrettyText::Helpers do
|
||||||
text: "Manager Hideout",
|
text: "Manager Hideout",
|
||||||
description: nil,
|
description: nil,
|
||||||
icon: "folder",
|
icon: "folder",
|
||||||
|
id: private_category.id,
|
||||||
slug: "secretcategory",
|
slug: "secretcategory",
|
||||||
ref: "secretcategory",
|
ref: "secretcategory",
|
||||||
type: "category",
|
type: "category",
|
||||||
|
|
|
@ -1715,19 +1715,20 @@ RSpec.describe PrettyText do
|
||||||
category2 = Fabricate(:category, name: "known", slug: "known")
|
category2 = Fabricate(:category, name: "known", slug: "known")
|
||||||
group = Fabricate(:group)
|
group = Fabricate(:group)
|
||||||
private_category = Fabricate(:private_category, name: "secret", group: group, slug: "secret")
|
private_category = Fabricate(:private_category, name: "secret", group: group, slug: "secret")
|
||||||
Fabricate(:topic, tags: [Fabricate(:tag, name: "known")])
|
tag = Fabricate(:tag, name: "known")
|
||||||
|
Fabricate(:topic, tags: [tag])
|
||||||
|
|
||||||
cooked = PrettyText.cook(" #unknown::tag #known #known::tag #testing #secret", user_id: user.id)
|
cooked = PrettyText.cook(" #unknown::tag #known #known::tag #testing #secret", user_id: user.id)
|
||||||
|
|
||||||
expect(cooked).to include("<span class=\"hashtag-raw\">#unknown::tag</span>")
|
expect(cooked).to include("<span class=\"hashtag-raw\">#unknown::tag</span>")
|
||||||
expect(cooked).to include(
|
expect(cooked).to include(
|
||||||
"<a class=\"hashtag-cooked\" href=\"#{category2.url}\" data-type=\"category\" data-slug=\"known\"><svg class=\"fa d-icon d-icon-folder svg-icon svg-node\"><use href=\"#folder\"></use></svg><span>known</span></a>",
|
"<a class=\"hashtag-cooked\" href=\"#{category2.url}\" data-type=\"category\" data-slug=\"known\" data-id=\"#{category2.id}\"><span class=\"hashtag-icon-placeholder\"></span><span>known</span></a>",
|
||||||
)
|
)
|
||||||
expect(cooked).to include(
|
expect(cooked).to include(
|
||||||
"<a class=\"hashtag-cooked\" href=\"/tag/known\" data-type=\"tag\" data-slug=\"known\" data-ref=\"known::tag\"><svg class=\"fa d-icon d-icon-tag svg-icon svg-node\"><use href=\"#tag\"></use></svg><span>known</span></a>",
|
"<a class=\"hashtag-cooked\" href=\"/tag/known\" data-type=\"tag\" data-slug=\"known\" data-id=\"#{tag.id}\" data-ref=\"known::tag\"><span class=\"hashtag-icon-placeholder\"></span><span>known</span></a>",
|
||||||
)
|
)
|
||||||
expect(cooked).to include(
|
expect(cooked).to include(
|
||||||
"<a class=\"hashtag-cooked\" href=\"#{category.url}\" data-type=\"category\" data-slug=\"testing\"><svg class=\"fa d-icon d-icon-folder svg-icon svg-node\"><use href=\"#folder\"></use></svg><span>testing</span></a>",
|
"<a class=\"hashtag-cooked\" href=\"#{category.url}\" data-type=\"category\" data-slug=\"testing\" data-id=\"#{category.id}\"><span class=\"hashtag-icon-placeholder\"></span><span>testing</span></a>",
|
||||||
)
|
)
|
||||||
expect(cooked).to include("<span class=\"hashtag-raw\">#secret</span>")
|
expect(cooked).to include("<span class=\"hashtag-raw\">#secret</span>")
|
||||||
|
|
||||||
|
@ -1735,7 +1736,7 @@ RSpec.describe PrettyText do
|
||||||
group.add(user)
|
group.add(user)
|
||||||
cooked = PrettyText.cook(" #unknown::tag #known #known::tag #testing #secret", user_id: user.id)
|
cooked = PrettyText.cook(" #unknown::tag #known #known::tag #testing #secret", user_id: user.id)
|
||||||
expect(cooked).to include(
|
expect(cooked).to include(
|
||||||
"<a class=\"hashtag-cooked\" href=\"#{private_category.url}\" data-type=\"category\" data-slug=\"secret\"><svg class=\"fa d-icon d-icon-folder svg-icon svg-node\"><use href=\"#folder\"></use></svg><span>secret</span></a>",
|
"<a class=\"hashtag-cooked\" href=\"#{private_category.url}\" data-type=\"category\" data-slug=\"secret\" data-id=\"#{private_category.id}\"><span class=\"hashtag-icon-placeholder\"></span><span>secret</span></a>",
|
||||||
)
|
)
|
||||||
|
|
||||||
cooked = PrettyText.cook("[`a` #known::tag here](http://example.com)", user_id: user.id)
|
cooked = PrettyText.cook("[`a` #known::tag here](http://example.com)", user_id: user.id)
|
||||||
|
@ -1753,7 +1754,7 @@ RSpec.describe PrettyText do
|
||||||
|
|
||||||
cooked = PrettyText.cook("<A href='/a'>test</A> #known::tag", user_id: user.id)
|
cooked = PrettyText.cook("<A href='/a'>test</A> #known::tag", user_id: user.id)
|
||||||
html = <<~HTML
|
html = <<~HTML
|
||||||
<p><a href="/a">test</a> <a class="hashtag-cooked" href="/tag/known" data-type="tag" data-slug="known" data-ref="known::tag"><svg class="fa d-icon d-icon-tag svg-icon svg-node"><use href="#tag"></use></svg><span>known</span></a></p>
|
<p><a href="/a">test</a> <a class="hashtag-cooked" href="/tag/known" data-type="tag" data-slug="known" data-id=\"#{tag.id}\" data-ref="known::tag"><span class=\"hashtag-icon-placeholder\"></span><span>known</span></a></p>
|
||||||
HTML
|
HTML
|
||||||
expect(cooked).to eq(html.strip)
|
expect(cooked).to eq(html.strip)
|
||||||
|
|
||||||
|
@ -2006,7 +2007,7 @@ HTML
|
||||||
expect(PrettyText.cook("@test #test test")).to match_html(<<~HTML)
|
expect(PrettyText.cook("@test #test test")).to match_html(<<~HTML)
|
||||||
<p>
|
<p>
|
||||||
<a class="mention" href="/u/test">@test</a>
|
<a class="mention" href="/u/test">@test</a>
|
||||||
<a class="hashtag-cooked" href="#{category.url}" data-type="category" data-slug="test"><svg class="fa d-icon d-icon-folder svg-icon svg-node"><use href="#folder"></use></svg><span>test</span></a>
|
<a class="hashtag-cooked" href="#{category.url}" data-type="category" data-slug="test" data-id="#{category.id}"><span class="hashtag-icon-placeholder"></span><span>test</span></a>
|
||||||
tdiscourset
|
tdiscourset
|
||||||
</p>
|
</p>
|
||||||
HTML
|
HTML
|
||||||
|
|
|
@ -524,7 +524,7 @@
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"hashtag_icons" : {
|
"hashtag_icons" : {
|
||||||
"type": "array"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"displayed_about_plugin_stat_groups" : {
|
"displayed_about_plugin_stat_groups" : {
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
|
|
@ -163,6 +163,7 @@ RSpec.describe HashtagsController do
|
||||||
"type" => "category",
|
"type" => "category",
|
||||||
"ref" => category.slug,
|
"ref" => category.slug,
|
||||||
"slug" => category.slug,
|
"slug" => category.slug,
|
||||||
|
"id" => category.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"tag" => [
|
"tag" => [
|
||||||
|
@ -175,6 +176,7 @@ RSpec.describe HashtagsController do
|
||||||
"ref" => tag.name,
|
"ref" => tag.name,
|
||||||
"slug" => tag.name,
|
"slug" => tag.name,
|
||||||
"secondary_text" => "x0",
|
"secondary_text" => "x0",
|
||||||
|
"id" => tag.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -198,6 +200,7 @@ RSpec.describe HashtagsController do
|
||||||
"ref" => "#{tag.name}::tag",
|
"ref" => "#{tag.name}::tag",
|
||||||
"slug" => tag.name,
|
"slug" => tag.name,
|
||||||
"secondary_text" => "x0",
|
"secondary_text" => "x0",
|
||||||
|
"id" => tag.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -242,6 +245,7 @@ RSpec.describe HashtagsController do
|
||||||
"type" => "category",
|
"type" => "category",
|
||||||
"ref" => private_category.slug,
|
"ref" => private_category.slug,
|
||||||
"slug" => private_category.slug,
|
"slug" => private_category.slug,
|
||||||
|
"id" => private_category.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"tag" => [
|
"tag" => [
|
||||||
|
@ -254,6 +258,7 @@ RSpec.describe HashtagsController do
|
||||||
"ref" => hidden_tag.name,
|
"ref" => hidden_tag.name,
|
||||||
"slug" => hidden_tag.name,
|
"slug" => hidden_tag.name,
|
||||||
"secondary_text" => "x0",
|
"secondary_text" => "x0",
|
||||||
|
"id" => hidden_tag.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -337,6 +342,7 @@ RSpec.describe HashtagsController do
|
||||||
"type" => "category",
|
"type" => "category",
|
||||||
"ref" => category.slug,
|
"ref" => category.slug,
|
||||||
"slug" => category.slug,
|
"slug" => category.slug,
|
||||||
|
"id" => category.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"relative_url" => tag_2.url,
|
"relative_url" => tag_2.url,
|
||||||
|
@ -347,6 +353,7 @@ RSpec.describe HashtagsController do
|
||||||
"ref" => "#{tag_2.name}::tag",
|
"ref" => "#{tag_2.name}::tag",
|
||||||
"slug" => tag_2.name,
|
"slug" => tag_2.name,
|
||||||
"secondary_text" => "x0",
|
"secondary_text" => "x0",
|
||||||
|
"id" => tag_2.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -379,6 +386,7 @@ RSpec.describe HashtagsController do
|
||||||
"type" => "category",
|
"type" => "category",
|
||||||
"ref" => private_category.slug,
|
"ref" => private_category.slug,
|
||||||
"slug" => private_category.slug,
|
"slug" => private_category.slug,
|
||||||
|
"id" => private_category.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -396,6 +404,7 @@ RSpec.describe HashtagsController do
|
||||||
"ref" => "#{hidden_tag.name}",
|
"ref" => "#{hidden_tag.name}",
|
||||||
"slug" => hidden_tag.name,
|
"slug" => hidden_tag.name,
|
||||||
"secondary_text" => "x0",
|
"secondary_text" => "x0",
|
||||||
|
"id" => hidden_tag.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,9 +32,11 @@ RSpec.describe HashtagAutocompleteService do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe ".data_source_icons" do
|
describe ".data_source_icon_map" do
|
||||||
it "gets an array for all icons defined by data sources so they can be used for markdown allowlisting" do
|
it "gets an array for all icons defined by data sources so they can be used for markdown allowlisting" do
|
||||||
expect(HashtagAutocompleteService.data_source_icons).to eq(%w[folder tag])
|
expect(HashtagAutocompleteService.data_source_icon_map).to eq(
|
||||||
|
{ "category" => "folder", "tag" => "tag" },
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ describe "Using #hashtag autocompletion to search for and lookup categories and
|
||||||
expect(page).to have_css(".hashtag-cooked")
|
expect(page).to have_css(".hashtag-cooked")
|
||||||
cooked_hashtag = page.find(".hashtag-cooked")
|
cooked_hashtag = page.find(".hashtag-cooked")
|
||||||
expected = <<~HTML.chomp
|
expected = <<~HTML.chomp
|
||||||
<a class=\"hashtag-cooked\" href=\"#{category.url}\" data-type=\"category\" data-slug=\"cool-cat\" tabindex=\"-1\"><svg class=\"fa d-icon d-icon-folder svg-icon svg-node\"><use href=\"#folder\"></use></svg><span>Cool Category</span></a>
|
<a class=\"hashtag-cooked\" href=\"#{category.url}\" data-type=\"category\" data-id=\"#{category.id}\" data-slug=\"cool-cat\" tabindex=\"-1\"><span class="hashtag-category-badge hashtag-color--category-#{category.id}"></span><span>Cool Category</span></a>
|
||||||
HTML
|
HTML
|
||||||
expect(cooked_hashtag["outerHTML"].squish).to eq(expected)
|
expect(cooked_hashtag["outerHTML"].squish).to eq(expected)
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ describe "Using #hashtag autocompletion to search for and lookup categories and
|
||||||
expect(page).to have_css(".hashtag-cooked")
|
expect(page).to have_css(".hashtag-cooked")
|
||||||
cooked_hashtag = page.find(".hashtag-cooked")
|
cooked_hashtag = page.find(".hashtag-cooked")
|
||||||
expect(cooked_hashtag["outerHTML"].squish).to eq(<<~HTML.chomp)
|
expect(cooked_hashtag["outerHTML"].squish).to eq(<<~HTML.chomp)
|
||||||
<a class=\"hashtag-cooked\" href=\"#{tag.url}\" data-type=\"tag\" data-slug=\"cooltag\" tabindex=\"-1\"><svg class=\"fa d-icon d-icon-tag svg-icon svg-node\"><use href=\"#tag\"></use></svg><span>cooltag</span></a>
|
<a class=\"hashtag-cooked\" href=\"#{tag.url}\" data-type=\"tag\" data-id=\"#{tag.id}\" data-slug=\"cooltag\" tabindex=\"-1\"><svg class=\"fa d-icon d-icon-tag svg-icon hashtag-color--tag-#{tag.id} svg-string\" xmlns=\"http://www.w3.org/2000/svg\"><use href=\"#tag\"></use></svg><span>cooltag</span></a>
|
||||||
HTML
|
HTML
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -84,10 +84,10 @@ describe "Using #hashtag autocompletion to search for and lookup categories and
|
||||||
cooked_hashtags = page.all(".hashtag-cooked", count: 2)
|
cooked_hashtags = page.all(".hashtag-cooked", count: 2)
|
||||||
|
|
||||||
expect(cooked_hashtags[0]["outerHTML"]).to eq(<<~HTML.chomp)
|
expect(cooked_hashtags[0]["outerHTML"]).to eq(<<~HTML.chomp)
|
||||||
<a class=\"hashtag-cooked\" href=\"#{category.url}\" data-type=\"category\" data-slug=\"cool-cat\"><svg class=\"fa d-icon d-icon-folder svg-icon svg-node\"><use href=\"#folder\"></use></svg><span>Cool Category</span></a>
|
<a class=\"hashtag-cooked\" href=\"#{category.url}\" data-type=\"category\" data-slug=\"cool-cat\" data-id=\"#{category.id}\"><span class=\"hashtag-category-badge hashtag-color--category-#{category.id}\"></span><span>Cool Category</span></a>
|
||||||
HTML
|
HTML
|
||||||
expect(cooked_hashtags[1]["outerHTML"]).to eq(<<~HTML.chomp)
|
expect(cooked_hashtags[1]["outerHTML"]).to eq(<<~HTML.chomp)
|
||||||
<a class=\"hashtag-cooked\" href=\"#{tag.url}\" data-type=\"tag\" data-slug=\"cooltag\"><svg class=\"fa d-icon d-icon-tag svg-icon svg-node\"><use href=\"#tag\"></use></svg><span>cooltag</span></a>
|
<a class=\"hashtag-cooked\" href=\"#{tag.url}\" data-type=\"tag\" data-slug=\"cooltag\" data-id=\"#{tag.id}\"><svg class=\"fa d-icon d-icon-tag svg-icon hashtag-color--tag-#{tag.id} svg-string\" xmlns=\"http://www.w3.org/2000/svg\"><use href=\"#tag\"></use></svg><span>cooltag</span></a>
|
||||||
HTML
|
HTML
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue