mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-23 23:58:31 -05:00
Merge pull request #3937 from tgxworld/implement_#_category
FEATURE: Autolinking to category using hashtags.
This commit is contained in:
commit
0ba1e8a76f
20 changed files with 365 additions and 14 deletions
|
@ -1,18 +1,19 @@
|
|||
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
|
||||
import Category from 'discourse/models/category';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
|
||||
_initializeAutocomplete: function() {
|
||||
const self = this,
|
||||
template = this.container.lookup('template:category-group-autocomplete.raw'),
|
||||
regexp = new RegExp("href=['\"]" + Discourse.getURL('/c/') + "([^'\"]+)");
|
||||
regexp = new RegExp(`href=['\"]${Discourse.getURL('/c/')}([^'\"]+)`);
|
||||
|
||||
this.$('input').autocomplete({
|
||||
items: this.get('categories'),
|
||||
single: false,
|
||||
allowAny: false,
|
||||
dataSource(term){
|
||||
return Discourse.Category.list().filter(function(category){
|
||||
return Category.list().filter(function(category){
|
||||
const regex = new RegExp(term, "i");
|
||||
return category.get("name").match(regex) &&
|
||||
!_.contains(self.get('blacklist') || [], category) &&
|
||||
|
@ -22,7 +23,7 @@ export default Ember.Component.extend({
|
|||
onChangeItems(items) {
|
||||
const categories = _.map(items, function(link) {
|
||||
const slug = link.match(regexp)[1];
|
||||
return Discourse.Category.findSingleBySlug(slug);
|
||||
return Category.findSingleBySlug(slug);
|
||||
});
|
||||
Em.run.next(() => self.set("categories", categories));
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import userSearch from 'discourse/lib/user-search';
|
||||
import { default as computed, on } from 'ember-addons/ember-computed-decorators';
|
||||
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
|
||||
import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['wmd-controls'],
|
||||
|
@ -111,13 +112,19 @@ export default Ember.Component.extend({
|
|||
$preview.scrollTop(desired + 50);
|
||||
},
|
||||
|
||||
_renderUnseen: function($preview, unseen) {
|
||||
fetchUnseenMentions($preview, unseen, this.siteSettings).then(() => {
|
||||
_renderUnseenMentions: function($preview, unseen) {
|
||||
fetchUnseenMentions($preview, unseen).then(() => {
|
||||
linkSeenMentions($preview, this.siteSettings);
|
||||
this._warnMentionedGroups($preview);
|
||||
});
|
||||
},
|
||||
|
||||
_renderUnseenCategoryHashtags: function($preview, unseen) {
|
||||
fetchUnseenCategoryHashtags(unseen).then(() => {
|
||||
linkSeenCategoryHashtags($preview);
|
||||
});
|
||||
},
|
||||
|
||||
_warnMentionedGroups($preview) {
|
||||
Ember.run.scheduleOnce('afterRender', () => {
|
||||
this._warnedMentions = this._warnedMentions || [];
|
||||
|
@ -386,11 +393,17 @@ export default Ember.Component.extend({
|
|||
// Paint mentions
|
||||
const unseen = linkSeenMentions($preview, this.siteSettings);
|
||||
if (unseen.length) {
|
||||
Ember.run.debounce(this, this._renderUnseen, $preview, unseen, 500);
|
||||
Ember.run.debounce(this, this._renderUnseenMentions, $preview, unseen, 500);
|
||||
}
|
||||
|
||||
this._warnMentionedGroups($preview);
|
||||
|
||||
// Paint category hashtags
|
||||
const unseenHashtags = linkSeenCategoryHashtags($preview);
|
||||
if (unseenHashtags.length) {
|
||||
Ember.run.debounce(this, this._renderUnseenCategoryHashtags, $preview, unseenHashtags, 500);
|
||||
}
|
||||
|
||||
const post = this.get('composer.post');
|
||||
let refresh = false;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import loadScript from 'discourse/lib/load-script';
|
||||
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { showSelector } from "discourse/lib/emoji/emoji-toolbar";
|
||||
import Category from 'discourse/models/category';
|
||||
|
||||
// Our head can be a static string or a function that returns a string
|
||||
// based on input (like for numbered lists).
|
||||
|
@ -175,7 +176,11 @@ export default Ember.Component.extend({
|
|||
|
||||
@on('didInsertElement')
|
||||
_startUp() {
|
||||
this._applyEmojiAutocomplete();
|
||||
const container = this.get('container'),
|
||||
$editorInput = this.$('.d-editor-input');
|
||||
|
||||
this._applyEmojiAutocomplete(container, $editorInput);
|
||||
this._applyCategoryHashtagAutocomplete(container, $editorInput);
|
||||
|
||||
loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true));
|
||||
|
||||
|
@ -243,14 +248,52 @@ export default Ember.Component.extend({
|
|||
Ember.run.debounce(this, this._updatePreview, 30);
|
||||
},
|
||||
|
||||
_applyEmojiAutocomplete() {
|
||||
_applyCategoryHashtagAutocomplete(container, $editorInput) {
|
||||
const template = container.lookup('template:category-group-autocomplete.raw');
|
||||
|
||||
$editorInput.autocomplete({
|
||||
template: template,
|
||||
key: '#',
|
||||
transformComplete(category) {
|
||||
return category.get('slug');
|
||||
},
|
||||
dataSource(term) {
|
||||
return Category.list().filter(category => {
|
||||
const regexp = new RegExp(term, 'i');
|
||||
return category.get('name').match(regexp);
|
||||
});
|
||||
},
|
||||
triggerRule(textarea, opts) {
|
||||
const result = Discourse.Utilities.caretRowCol(textarea);
|
||||
const row = result.rowNum;
|
||||
var col = result.colNum;
|
||||
var line = textarea.value.split("\n")[row - 1];
|
||||
|
||||
if (opts && opts.backSpace) {
|
||||
col = col - 1;
|
||||
line = line.slice(0, line.length - 1);
|
||||
|
||||
// Don't trigger autocomplete when backspacing into a `#category |` => `#category|`
|
||||
if (/^#{1}\w+/.test(line)) return false;
|
||||
}
|
||||
|
||||
if (col < 6) {
|
||||
// Don't trigger autocomplete when ATX-style headers are used
|
||||
return (line.slice(0, col) !== "#".repeat(col));
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_applyEmojiAutocomplete(container, $editorInput) {
|
||||
if (!this.siteSettings.enable_emoji) { return; }
|
||||
|
||||
const container = this.container;
|
||||
const template = container.lookup('template:emoji-selector-autocomplete.raw');
|
||||
const self = this;
|
||||
|
||||
this.$('.d-editor-input').autocomplete({
|
||||
$editorInput.autocomplete({
|
||||
template: template,
|
||||
key: ":",
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
Supports Discourse's category hashtags (#category-slug) for automatically
|
||||
generating a link to the category.
|
||||
**/
|
||||
Discourse.Dialect.inlineRegexp({
|
||||
start: '#',
|
||||
matcher: /^#([A-Za-z0-9][A-Za-z0-9\-]{0,40}[A-Za-z0-9])/,
|
||||
spaceOrTagBoundary: true,
|
||||
|
||||
emitter: function(matches) {
|
||||
var slug = matches[1],
|
||||
hashtag = matches[0],
|
||||
attributeClass = 'hashtag',
|
||||
categoryHashtagLookup = this.dialect.options.categoryHashtagLookup,
|
||||
result = categoryHashtagLookup && categoryHashtagLookup(slug);
|
||||
|
||||
if (result && result[0] === "category") {
|
||||
return ['a', { class: attributeClass, href: result[1] }, hashtag];
|
||||
} else {
|
||||
return ['span', { class: attributeClass }, hashtag];
|
||||
}
|
||||
}
|
||||
});
|
|
@ -282,6 +282,14 @@ export default function(options) {
|
|||
}, 50);
|
||||
});
|
||||
|
||||
const checkTriggerRule = (opts) => {
|
||||
if (options.triggerRule) {
|
||||
return options.triggerRule(me[0], opts);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
$(this).on('keypress.autocomplete', function(e) {
|
||||
var caretPosition, term;
|
||||
|
||||
|
@ -289,7 +297,7 @@ export default function(options) {
|
|||
if (options.key && e.which === options.key.charCodeAt(0)) {
|
||||
caretPosition = Discourse.Utilities.caretPosition(me[0]);
|
||||
var prevChar = me.val().charAt(caretPosition - 1);
|
||||
if (!prevChar || allowedLettersRegex.test(prevChar)) {
|
||||
if (checkTriggerRule() && (!prevChar || allowedLettersRegex.test(prevChar))) {
|
||||
completeStart = completeEnd = caretPosition;
|
||||
updateAutoComplete(options.dataSource(""));
|
||||
}
|
||||
|
@ -343,7 +351,7 @@ export default function(options) {
|
|||
stopFound = prev === options.key;
|
||||
if (stopFound) {
|
||||
prev = me[0].value[c - 1];
|
||||
if (!prev || allowedLettersRegex.test(prev)) {
|
||||
if (checkTriggerRule({ backSpace: true }) && (!prev || allowedLettersRegex.test(prev))) {
|
||||
completeStart = c;
|
||||
caretPosition = completeEnd = initial;
|
||||
term = me[0].value.substring(c + 1, initial);
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
const validCategoryHashtags = {};
|
||||
const checkedCategoryHashtags = [];
|
||||
const testedKey = 'tested';
|
||||
const testedClass = `hashtag-${testedKey}`;
|
||||
|
||||
function replaceSpan($elem, categorySlug, categoryLink) {
|
||||
$elem.replaceWith(`<a href="${categoryLink}" class="hashtag">#${categorySlug}</a>`);
|
||||
}
|
||||
|
||||
function updateFound($hashtags, categorySlugs) {
|
||||
Ember.run.schedule('afterRender', () => {
|
||||
$hashtags.each((index, hashtag) => {
|
||||
const categorySlug = categorySlugs[index];
|
||||
const link = validCategoryHashtags[categorySlug];
|
||||
const $hashtag = $(hashtag);
|
||||
|
||||
if (link) {
|
||||
replaceSpan($hashtag, categorySlug, link);
|
||||
} else if (checkedCategoryHashtags.indexOf(categorySlug) !== -1) {
|
||||
$hashtag.addClass(testedClass);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export function linkSeenCategoryHashtags($elem) {
|
||||
const $hashtags = $(`span.hashtag:not(.${testedClass})`, $elem);
|
||||
const unseen = [];
|
||||
|
||||
if ($hashtags.length) {
|
||||
const categorySlugs = $hashtags.map((_, hashtag) => $(hashtag).text().substr(1));
|
||||
if (categorySlugs.length) {
|
||||
_.uniq(categorySlugs).forEach((categorySlug) => {
|
||||
if (checkedCategoryHashtags.indexOf(categorySlug) === -1) {
|
||||
unseen.push(categorySlug);
|
||||
}
|
||||
});
|
||||
}
|
||||
updateFound($hashtags, categorySlugs);
|
||||
}
|
||||
|
||||
return unseen;
|
||||
};
|
||||
|
||||
export function fetchUnseenCategoryHashtags(categorySlugs) {
|
||||
return Discourse.ajax("/category_hashtags/check", { data: { category_slugs: categorySlugs } })
|
||||
.then((response) => {
|
||||
response.valid.forEach((category) => {
|
||||
validCategoryHashtags[category.slug] = category.url;
|
||||
});
|
||||
checkedCategoryHashtags.push.apply(checkedCategoryHashtags, categorySlugs);
|
||||
});
|
||||
}
|
|
@ -239,6 +239,7 @@ Discourse.Markdown.whiteListTag('a', 'class', 'attachment');
|
|||
Discourse.Markdown.whiteListTag('a', 'class', 'onebox');
|
||||
Discourse.Markdown.whiteListTag('a', 'class', 'mention');
|
||||
Discourse.Markdown.whiteListTag('a', 'class', 'mention-group');
|
||||
Discourse.Markdown.whiteListTag('a', 'class', 'hashtag');
|
||||
|
||||
Discourse.Markdown.whiteListTag('a', 'target', '_blank');
|
||||
Discourse.Markdown.whiteListTag('a', 'rel', 'nofollow');
|
||||
|
@ -251,6 +252,7 @@ Discourse.Markdown.whiteListTag('div', 'class', 'title');
|
|||
Discourse.Markdown.whiteListTag('div', 'class', 'quote-controls');
|
||||
|
||||
Discourse.Markdown.whiteListTag('span', 'class', 'mention');
|
||||
Discourse.Markdown.whiteListTag('span', 'class', 'hashtag');
|
||||
Discourse.Markdown.whiteListTag('aside', 'class', 'quote');
|
||||
Discourse.Markdown.whiteListTag('aside', 'data-*');
|
||||
|
||||
|
|
|
@ -143,6 +143,19 @@ Discourse.Utilities = {
|
|||
return String(text).trim();
|
||||
},
|
||||
|
||||
// Determine the row and col of the caret in an element
|
||||
caretRowCol: function(el) {
|
||||
var caretPosition = Discourse.Utilities.caretPosition(el);
|
||||
var rows = el.value.slice(0, caretPosition).split("\n");
|
||||
var rowNum = rows.length;
|
||||
|
||||
var colNum = caretPosition - rows.splice(0, rowNum - 1).reduce(function(sum, row) {
|
||||
return sum + row.length + 1;
|
||||
}, 0);
|
||||
|
||||
return { rowNum: rowNum, colNum: colNum};
|
||||
},
|
||||
|
||||
// Determine the position of the caret in an element
|
||||
caretPosition: function(el) {
|
||||
var r, rc, re;
|
||||
|
|
|
@ -42,6 +42,7 @@ export default function() {
|
|||
this.route('parentCategory', { path: '/c/:slug' });
|
||||
this.route('categoryNone', { path: '/c/:slug/none' });
|
||||
this.route('category', { path: '/c/:parentSlug/:slug' });
|
||||
this.route('categoryWithID', { path: '/c/:parentSlug/:slug/:id' });
|
||||
|
||||
// homepage
|
||||
this.route(Discourse.Utilities.defaultHomepage(), { path: '/' });
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import Category from 'discourse/models/category';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
model: function(params) {
|
||||
return Category.findById(params.id);
|
||||
},
|
||||
|
||||
redirect: function(model) {
|
||||
this.transitionTo(`/c/${Category.slugFor(model)}`);
|
||||
}
|
||||
});
|
14
app/controllers/category_hashtags_controller.rb
Normal file
14
app/controllers/category_hashtags_controller.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
class CategoryHashtagsController < ApplicationController
|
||||
before_filter :ensure_logged_in
|
||||
|
||||
def check
|
||||
category_slugs = params[:category_slugs]
|
||||
category_slugs.each(&:downcase!)
|
||||
|
||||
valid_categories = Category.secured(guardian).where(slug: category_slugs).map do |category|
|
||||
{ slug: category.slug, url: category.url_with_id }
|
||||
end.compact
|
||||
|
||||
render json: { valid: valid_categories }
|
||||
end
|
||||
end
|
|
@ -224,14 +224,22 @@ class ListController < ApplicationController
|
|||
def set_category
|
||||
slug_or_id = params.fetch(:category)
|
||||
parent_slug_or_id = params[:parent_category]
|
||||
id = params[:id].to_i
|
||||
|
||||
parent_category_id = nil
|
||||
if parent_slug_or_id.present?
|
||||
parent_category_id = Category.query_parent_category(parent_slug_or_id)
|
||||
redirect_or_not_found and return if parent_category_id.blank?
|
||||
redirect_or_not_found and return if parent_category_id.blank? && !id
|
||||
end
|
||||
|
||||
@category = Category.query_category(slug_or_id, parent_category_id)
|
||||
|
||||
# Redirect if we have `/c/:parent_category/:category/:id`
|
||||
if id
|
||||
category = Category.find_by_id(id)
|
||||
(redirect_to category.url, status: 301) && return if category
|
||||
end
|
||||
|
||||
redirect_or_not_found and return if !@category
|
||||
|
||||
@description_meta = @category.description_text
|
||||
|
|
|
@ -416,6 +416,10 @@ SQL
|
|||
url
|
||||
end
|
||||
|
||||
def url_with_id
|
||||
self.parent_category ? "#{url}/#{self.id}" : "#{Discourse.base_uri}/c/#{self.id}-#{self.slug}"
|
||||
end
|
||||
|
||||
# If the name changes, try and update the category definition topic too if it's
|
||||
# an exact match
|
||||
def rename_category_definition
|
||||
|
|
|
@ -425,11 +425,12 @@ Discourse::Application.routes.draw do
|
|||
get "c/:parent_category/:category.rss" => "list#category_feed", format: :rss
|
||||
get "c/:category" => "list#category_latest"
|
||||
get "c/:category/none" => "list#category_none_latest"
|
||||
get "c/:parent_category/:category" => "list#parent_category_category_latest"
|
||||
get "c/:parent_category/:category/(:id)" => "list#parent_category_category_latest", constraints: { id: /\d+/ }
|
||||
get "c/:category/l/top" => "list#category_top", as: "category_top"
|
||||
get "c/:category/none/l/top" => "list#category_none_top", as: "category_none_top"
|
||||
get "c/:parent_category/:category/l/top" => "list#parent_category_category_top", as: "parent_category_category_top"
|
||||
|
||||
get "category_hashtags/check" => "category_hashtags#check"
|
||||
|
||||
TopTopic.periods.each do |period|
|
||||
get "top/#{period}" => "list#top_#{period}"
|
||||
|
|
|
@ -48,6 +48,15 @@ module PrettyText
|
|||
end
|
||||
end
|
||||
|
||||
def category_hashtag_lookup(category_slug)
|
||||
if category_slug
|
||||
category = Category.find_by_slug(category_slug)
|
||||
return ['category', category.url_with_id] if category
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def get_topic_info(topic_id)
|
||||
return unless Fixnum === topic_id
|
||||
# TODO this only handles public topics, secured one do not get this
|
||||
|
@ -207,6 +216,7 @@ module PrettyText
|
|||
context.eval("Discourse.Emoji.applyCustomEmojis();")
|
||||
|
||||
context.eval('opts["mentionLookup"] = function(u){return helpers.mention_lookup(u);}')
|
||||
context.eval('opts["categoryHashtagLookup"] = function(c){return helpers.category_hashtag_lookup(c);}')
|
||||
context.eval('opts["lookupAvatar"] = function(p){return Discourse.Utilities.avatarImg({size: "tiny", avatarTemplate: helpers.avatar_template(p)});}')
|
||||
context.eval('opts["getTopicInfo"] = function(i){return helpers.get_topic_info(i)};')
|
||||
baked = context.eval('Discourse.Markdown.markdownConverter(opts).makeHtml(raw)')
|
||||
|
|
46
spec/controllers/category_hashtags_controller_spec.rb
Normal file
46
spec/controllers/category_hashtags_controller_spec.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe CategoryHashtagsController do
|
||||
describe "check" do
|
||||
describe "logged in" do
|
||||
before do
|
||||
log_in(:user)
|
||||
end
|
||||
|
||||
it 'only returns the categories that are valid' do
|
||||
category = Fabricate(:category)
|
||||
xhr :get, :check, category_slugs: [category.slug, 'none']
|
||||
|
||||
expect(JSON.parse(response.body)).to eq(
|
||||
{ "valid" => [{ "slug" => category.slug, "url" => category.url_with_id }] }
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not return restricted categories for a normal user' do
|
||||
group = Fabricate(:group)
|
||||
private_category = Fabricate(:private_category, group: group)
|
||||
xhr :get, :check, category_slugs: [private_category.slug]
|
||||
|
||||
expect(JSON.parse(response.body)).to eq({ "valid" => [] })
|
||||
end
|
||||
|
||||
it 'returns restricted categories for an admin' do
|
||||
admin = log_in(:admin)
|
||||
group = Fabricate(:group)
|
||||
group.add(admin)
|
||||
private_category = Fabricate(:private_category, group: group)
|
||||
xhr :get, :check, category_slugs: [private_category.slug]
|
||||
|
||||
expect(JSON.parse(response.body)).to eq(
|
||||
{ "valid" => [{ "slug" => private_category.slug, "url" => private_category.url_with_id }] }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "not logged in" do
|
||||
it 'raises an exception' do
|
||||
expect { xhr :get, :check, category_slugs: [] }.to raise_error(Discourse::NotLoggedIn)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -83,6 +83,26 @@ describe ListController do
|
|||
it { is_expected.to respond_with(:success) }
|
||||
end
|
||||
|
||||
context 'with a link that has a parent slug, slug and id in its path' do
|
||||
let(:child_category) { Fabricate(:category, parent_category: category) }
|
||||
|
||||
context "with valid slug" do
|
||||
before do
|
||||
xhr :get, :category_latest, parent_category: category.slug, category: child_category.slug, id: child_category.id
|
||||
end
|
||||
|
||||
it { is_expected.to redirect_to(child_category.url) }
|
||||
end
|
||||
|
||||
context "with invalid slug" do
|
||||
before do
|
||||
xhr :get, :category_latest, parent_category: 'random slug', category: 'random slug', id: child_category.id
|
||||
end
|
||||
|
||||
it { is_expected.to redirect_to(child_category.url) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'another category exists with a number at the beginning of its name' do
|
||||
# One category has another category's id at the beginning of its name
|
||||
let!(:other_category) { Fabricate(:category, name: "#{category.id} name") }
|
||||
|
|
|
@ -503,6 +503,22 @@ describe Category do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#url_with_id" do
|
||||
let(:category) { Fabricate(:category, name: 'cats') }
|
||||
|
||||
it "includes the id in the URL" do
|
||||
expect(category.url_with_id).to eq("/c/#{category.id}-cats")
|
||||
end
|
||||
|
||||
context "child category" do
|
||||
let(:child_category) { Fabricate(:category, parent_category_id: category.id, name: 'dogs') }
|
||||
|
||||
it "includes the id in the URL" do
|
||||
expect(child_category.url_with_id).to eq("/c/cats/dogs/#{child_category.id}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "uncategorized" do
|
||||
let(:cat) { Category.where(id: SiteSetting.uncategorized_category_id).first }
|
||||
|
||||
|
|
|
@ -289,6 +289,46 @@ test("Mentions", function() {
|
|||
"it allows mentions within HTML tags");
|
||||
});
|
||||
|
||||
test("Category hashtags", () => {
|
||||
var alwaysTrue = { categoryHashtagLookup: (function() { return ["category", "http://test.discourse.org/category-hashtag"]; }) };
|
||||
|
||||
cookedOptions("Check out #category-hashtag", alwaysTrue,
|
||||
"<p>Check out <a class=\"hashtag\" href=\"http://test.discourse.org/category-hashtag\">#category-hashtag</a></p>",
|
||||
"it translates category hashtag into links");
|
||||
|
||||
cooked("Check out #category-hashtag",
|
||||
"<p>Check out <span class=\"hashtag\">#category-hashtag</span></p>",
|
||||
"it does not translate category hashtag into links if it is not a valid category hashtag");
|
||||
|
||||
cookedOptions("[#category-hashtag](http://www.test.com)", alwaysTrue,
|
||||
"<p><a href=\"http://www.test.com\">#category-hashtag</a></p>",
|
||||
"it does not translate category hashtag within links");
|
||||
|
||||
cooked("```\n# #category-hashtag\n```",
|
||||
"<p><pre><code class=\"lang-auto\"># #category-hashtag</code></pre></p>",
|
||||
"it does not translate category hashtags to links in code blocks");
|
||||
|
||||
cooked("># #category-hashtag\n",
|
||||
"<blockquote><h1><span class=\"hashtag\">#category-hashtag</span></h1></blockquote>",
|
||||
"it handles category hashtags in simple quotes");
|
||||
|
||||
cooked("# #category-hashtag",
|
||||
"<h1><span class=\"hashtag\">#category-hashtag</span></h1>",
|
||||
"it works within ATX-style headers");
|
||||
|
||||
cooked("don't `#category-hashtag`",
|
||||
"<p>don't <code>#category-hashtag</code></p>",
|
||||
"it does not mention in an inline code block");
|
||||
|
||||
cooked("test #hashtag1/#hashtag2",
|
||||
"<p>test <span class=\"hashtag\">#hashtag1</span>/#hashtag2</p>",
|
||||
"it does not convert category hashtag not bounded by spaces");
|
||||
|
||||
cooked("<small>#category-hashtag</small>",
|
||||
"<p><small><span class=\"hashtag\">#category-hashtag</span></small></p>",
|
||||
"it works between HTML tags");
|
||||
});
|
||||
|
||||
|
||||
test("Heading", function() {
|
||||
cooked("**Bold**\n----------", "<h2><strong>Bold</strong></h2>", "It will bold the heading");
|
||||
|
|
|
@ -158,3 +158,27 @@ test("defaultHomepage", function() {
|
|||
Discourse.SiteSettings.top_menu = "latest|top|hot";
|
||||
equal(utils.defaultHomepage(), "latest", "default homepage is the first item in the top_menu site setting");
|
||||
});
|
||||
|
||||
test("caretRowCol", () => {
|
||||
var textarea = document.createElement('textarea');
|
||||
const content = document.createTextNode("01234\n56789\n012345");
|
||||
textarea.appendChild(content);
|
||||
textarea.setAttribute('id', 'test');
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
const assertResult = (setCaretPos, expectedRowNum, expectedColNum) => {
|
||||
Discourse.Utilities.setCaretPosition(textarea, setCaretPos);
|
||||
|
||||
const result = Discourse.Utilities.caretRowCol(textarea);
|
||||
equal(result.rowNum, expectedRowNum, "returns the right row of the caret");
|
||||
equal(result.colNum, expectedColNum, "returns the right col of the caret");
|
||||
};
|
||||
|
||||
assertResult(0, 1, 0);
|
||||
assertResult(5, 1, 5);
|
||||
assertResult(6, 2, 0);
|
||||
assertResult(11, 2, 5);
|
||||
assertResult(14, 3, 2);
|
||||
|
||||
document.body.removeChild(textarea);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue