diff --git a/.eslintrc b/.eslintrc index bfbe34ea65..218cb72b81 100644 --- a/.eslintrc +++ b/.eslintrc @@ -41,11 +41,12 @@ "visible":true, "invisible":true, "asyncRender":true, - "selectDropdown":true, "selectKit":true, "expandSelectKit":true, "collapseSelectKit":true, - "selectKitSelectRow":true, + "selectKitSelectRowByValue":true, + "selectKitSelectRowByName":true, + "selectKitSelectRowByIndex":true, "selectKitSelectNoneRow":true, "selectKitFillInFilter":true, "asyncTestDiscourse":true, diff --git a/.travis.yml b/.travis.yml index e4f402f801..73ae46e5b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,8 +21,11 @@ addons: matrix: fast_finish: true + allow_failures: + - rvm: 2.5.0 rvm: + - 2.5.0 - 2.4.2 - 2.3.4 @@ -45,6 +48,7 @@ before_install: - git clone --depth=1 https://github.com/discourse/discourse-canned-replies.git plugins/discourse-canned-replies - git clone --depth=1 https://github.com/discourse/discourse-chat-integration.git plugins/discourse-chat-integration - git clone --depth=1 https://github.com/discourse/discourse-assign.git plugins/discourse-assign + - git clone --depth=1 https://github.com/discourse/discourse-patreon.git plugins/discourse-patreon - export PATH=$HOME/.yarn/bin:$PATH install: @@ -67,7 +71,7 @@ script: bundle exec rake db:create db:migrate if [ '$QUNIT_RUN' == '1' ]; then - bundle exec rake qunit:test['400000'] + bundle exec rake qunit:test['400000'] && \ bundle exec rake plugin:spec else bundle exec rspec && bundle exec rake plugin:spec diff --git a/Gemfile b/Gemfile index f242cd5692..e558ffb47c 100644 --- a/Gemfile +++ b/Gemfile @@ -36,7 +36,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.30' +gem 'onebox', '1.8.33' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index a763d7c89d..529aa37e43 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -232,7 +232,7 @@ GEM omniauth-twitter (1.3.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.30) + onebox (1.8.33) fast_blank (>= 1.0.0) htmlentities (~> 4.3) moneta (~> 1.0) @@ -469,7 +469,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.30) + onebox (= 1.8.33) openid-redis-store pg pry-nav diff --git a/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 index a26d927873..7bbdaa3de5 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 @@ -16,7 +16,7 @@ export default Ember.Controller.extend(ModalFunctionality, { @computed('name') nameValid(name) { - return name && name.match(/\A[a-z_][a-z0-9_-]*\z/i); + return name && name.match(/^[a-z_][a-z0-9_-]*$/i); }, @observes('name') diff --git a/app/assets/javascripts/admin/routes/admin-backups.js.es6 b/app/assets/javascripts/admin/routes/admin-backups.js.es6 index 6d1a425190..92ac4652fa 100644 --- a/app/assets/javascripts/admin/routes/admin-backups.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-backups.js.es6 @@ -68,7 +68,7 @@ export default Discourse.Route.extend({ function(confirmed) { if (confirmed) { backup.destroy().then(function() { - self.controllerFor("adminBackupsIndex").removeObject(backup); + self.controllerFor("adminBackupsIndex").get('model').removeObject(backup); }); } } diff --git a/app/assets/javascripts/admin/templates/admin.hbs b/app/assets/javascripts/admin/templates/admin.hbs index 01f9ffb673..af4aafde94 100644 --- a/app/assets/javascripts/admin/templates/admin.hbs +++ b/app/assets/javascripts/admin/templates/admin.hbs @@ -20,7 +20,9 @@ {{#if currentUser.admin}} {{nav-item route='adminCustomize' label='admin.customize.title'}} {{nav-item route='adminApi' label='admin.api.title'}} - {{nav-item route='admin.backups' label='admin.backups.title'}} + {{#if siteSettings.enable_backups}} + {{nav-item route='admin.backups' label='admin.backups.title'}} + {{/if}} {{/if}} {{nav-item route='adminPlugins' label='admin.plugins.title'}} {{plugin-outlet name="admin-menu" connectorTagName="li"}} diff --git a/app/assets/javascripts/admin/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/templates/customize-themes-show.hbs index 6f8ac614aa..7f339d63d7 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-show.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-show.hbs @@ -1,5 +1,5 @@
-

+

{{#if editingName}} {{text-field value=model.name autofocus="true"}} {{d-button action="finishedEditingName" class="btn-primary btn-small submit-edit" icon="check"}} @@ -7,7 +7,7 @@ {{else}} {{model.name}} {{d-icon "pencil"}} {{/if}} -

+ {{#if model.remote_theme}}

diff --git a/app/assets/javascripts/discourse/components/badge-button.js.es6 b/app/assets/javascripts/discourse/components/badge-button.js.es6 index c32e1f1cbb..58b0d1358c 100644 --- a/app/assets/javascripts/discourse/components/badge-button.js.es6 +++ b/app/assets/javascripts/discourse/components/badge-button.js.es6 @@ -1,6 +1,6 @@ export default Ember.Component.extend({ tagName: 'span', - classNameBindings: [':user-badge', 'badge.badgeTypeClassName'], + classNameBindings: [':user-badge', 'badge.badgeTypeClassName', 'badge.enabled::disabled'], title: function(){ return $("

"+this.get('badge.description')+"
").text(); }.property('badge.description'), diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 6d56b6b915..968e1ade9a 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -1,5 +1,5 @@ import userSearch from 'discourse/lib/user-search'; -import { default as computed, on } from 'ember-addons/ember-computed-decorators'; +import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators'; import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions'; import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags'; import { linkSeenTagHashtags, fetchUnseenTagHashtags } from 'discourse/lib/link-tag-hashtag'; @@ -36,6 +36,18 @@ export default Ember.Component.extend({ return `[${I18n.t('uploading')}]() `; }, + @observes('composer.uploadCancelled') + _cancelUpload() { + if (!this.get('composer.uploadCancelled')) { return; } + this.set('composer.uploadCancelled', false); + + if (this._xhr) { + this._xhr._userCancelled = true; + this._xhr.abort(); + } + this._resetUpload(true); + }, + @computed markdownOptions() { return { @@ -363,7 +375,7 @@ export default Ember.Component.extend({ const $e = $(e); var name = $e.data('name'); if (found.indexOf(name) === -1){ - this.sendAction('groupsMentioned', [{name: name, user_count: $e.data('mentionable-user-count')}]); + this.sendAction('groupsMentioned', [{name: name, user_count: $e.data('mentionable-user-count'), max_mentions: $e.data('max-mentions')}]); found.push(name); } }); @@ -401,7 +413,9 @@ export default Ember.Component.extend({ }, _resetUpload(removePlaceholder) { - this._validUploads--; + if (this._validUploads > 0) { + this._validUploads--; + } if (this._validUploads === 0) { this.setProperties({ uploadProgress: 0, isUploading: false, isCancellable: false }); } @@ -493,7 +507,7 @@ export default Ember.Component.extend({ this._xhr = null; if (!userCancelled) { - displayErrorForUpload(data.jqXHR.responseJSON); + displayErrorForUpload(data); } }); @@ -624,14 +638,6 @@ export default Ember.Component.extend({ this.sendAction('importQuote', toolbarEvent); }, - cancelUpload() { - if (this._xhr) { - this._xhr._userCancelled = true; - this._xhr.abort(); - } - this._resetUpload(true); - }, - onExpandPopupMenuOptions(toolbarEvent) { const selected = toolbarEvent.selected; toolbarEvent.selectText(selected.start, selected.end - selected.start); diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index f7ff2e51c1..3d357ec225 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -279,6 +279,7 @@ export default Ember.Component.extend({ const markdownOptions = this.get('markdownOptions') || {}; cookAsync(value, markdownOptions).then(cooked => { + if (this.get('isDestroyed')) { return; } this.set('preview', cooked); Ember.run.scheduleOnce('afterRender', () => { if (this._state !== "inDOM") { return; } @@ -632,7 +633,8 @@ export default Ember.Component.extend({ if (rows.length > 1) { const columns = rows.map(r => r.split("\t").length); - const isTable = columns.reduce((a, b) => a && columns[0] === b && b > 1); + const isTable = columns.reduce((a, b) => a && columns[0] === b && b > 1) && + !(columns[0] === 2 && rows[0].split("\t")[0].match(/^•$|^\d+.$/)); // to skip tab delimited lists if (isTable) { const splitterRow = [...Array(columns[0])].map(() => "---").join("\t"); @@ -662,8 +664,6 @@ export default Ember.Component.extend({ if (table) { this.appEvents.trigger('composer:insert-text', table); handled = true; - } else if (html && html.includes("urn:schemas-microsoft-com:office:word")) { - html = ""; // use plain text data for microsoft word } } diff --git a/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6 index d8fd285316..6a78bf877d 100644 --- a/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6 +++ b/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6 @@ -1,7 +1,21 @@ -import { showEntrance } from "discourse/components/topic-list-item"; +import { showEntrance, navigateToTopic } from "discourse/components/topic-list-item"; export default Ember.Component.extend({ - click: showEntrance, attributeBindings: ['topic.id:data-topic-id'], - classNameBindings: [':latest-topic-list-item', 'topic.archived', 'topic.visited'] + classNameBindings: [':latest-topic-list-item', 'topic.archived', 'topic.visited'], + + showEntrance, + navigateToTopic, + + click(e) { + // for events undefined has a different meaning than false + if (this.showEntrance(e) === false) { + return false; + } + + return this.unhandledRowClick(e, this.get('topic')); + }, + + // Can be overwritten by plugins to handle clicks on other parts of the row + unhandledRowClick() { }, }); diff --git a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/topic-list-item.js.es6 index 7793fbd1b9..f9b3cd38b7 100644 --- a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-list-item.js.es6 @@ -20,6 +20,12 @@ export function showEntrance(e) { } } +export function navigateToTopic(topic, href) { + this.appEvents.trigger('header:update-topic', topic); + DiscourseURL.routeTo(href || topic.get('url')); + return false; +} + export default Ember.Component.extend(bufferedRender({ rerenderTriggers: ['bulkSelectEnabled', 'topic.pinned'], tagName: 'tr', @@ -107,8 +113,10 @@ export default Ember.Component.extend(bufferedRender({ return false; }.property(), + showEntrance, + click(e) { - const result = showEntrance.call(this, e); + const result = this.showEntrance(e); if (result === false) { return result; } const topic = this.get('topic'); @@ -124,19 +132,23 @@ export default Ember.Component.extend(bufferedRender({ } if (target.hasClass('raw-topic-link')) { - if (wantsNewWindow(e)) { return true; } - - this.appEvents.trigger('header:update-topic', topic); - DiscourseURL.routeTo(target.attr('href')); - return false; + if (wantsNewWindow(e)) { return true; } + return this.navigateToTopic(topic, target.attr('href')); } if (target.closest('a.topic-status').length === 1) { this.get('topic').togglePinnedForUser(); return false; } + + return this.unhandledRowClick(e, topic); }, + navigateToTopic, + + // Can be overwritten by plugins to handle clicks on other parts of the row + unhandledRowClick() { }, + highlight(opts = { isLastViewedTopic: false }) { const $topic = this.$(); $topic diff --git a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 index 238cd97495..b7d3b4d31f 100644 --- a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 +++ b/app/assets/javascripts/discourse/components/user-card-contents.js.es6 @@ -115,8 +115,7 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, { return false; } - // XSS protection (should be encapsulated) - username = username.toString().replace(/[^A-Za-z0-9_\.\-]/g, ""); + username = Ember.Handlebars.Utils.escapeExpression(username.toString()); // Don't show on mobile if (this.site.mobileView) { diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 20b8736c6f..d08f59c9f4 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -49,6 +49,10 @@ function loadDraft(store, opts) { const _popupMenuOptionsCallbacks = []; +export function clearPopupMenuOptionsCallback() { + _popupMenuOptionsCallbacks.length = 0; +} + export function addPopupMenuOptionsCallback(callback) { _popupMenuOptionsCallbacks.push(callback); } @@ -220,6 +224,10 @@ export default Ember.Controller.extend({ }, actions: { + cancelUpload() { + this.set('model.uploadCancelled', true); + }, + onPopupMenuAction(action) { this.send(action); }, @@ -378,11 +386,21 @@ export default Ember.Controller.extend({ groupsMentioned(groups) { if (!this.get('model.creatingPrivateMessage') && !this.get('model.topic.isPrivateMessage')) { groups.forEach(group => { - const body = I18n.t('composer.group_mentioned', { - group: "@" + group.name, - count: group.user_count, - group_link: Discourse.getURL(`/groups/${group.name}/members`) - }); + let body; + + if (group.max_mentions < group.user_count) { + body = I18n.t('composer.group_mentioned_limit', { + group: "@" + group.name, + max: group.max_mentions, + group_link: Discourse.getURL(`/groups/${group.name}/members`) + }); + } else { + body = I18n.t('composer.group_mentioned', { + group: "@" + group.name, + count: group.user_count, + group_link: Discourse.getURL(`/groups/${group.name}/members`) + }); + } this.appEvents.trigger('composer-messages:create', { extraClass: 'custom-body', diff --git a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 index cdbc957b15..741e74a897 100644 --- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -151,6 +151,12 @@ export default Ember.Controller.extend({ this.set("application.showFooter", !this.get("loading")); }, + @computed('resultCount', 'noSortQ') + resultCountLabel(count, term) { + const plus = (count % 50 === 0 ? "+" : ""); + return I18n.t('search.result_count', {count, plus, term}); + }, + @observes('model.posts.length') resultCountChanged() { this.set("resultCount", this.get("model.posts.length")); diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 index 00aeb2f8f0..9dc6083f15 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 @@ -10,6 +10,7 @@ export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController, saveAttrNames: ['name'], canEditName: setting('enable_names'), + canSaveUser: true, newNameInput: null, diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index a2a8392ba5..80209d7b73 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -272,7 +272,7 @@ export default Ember.Controller.extend(BufferedContent, { const quoteState = this.get('quoteState'); const postStream = this.get('model.postStream'); - if (!postStream) return; + if (!postStream || !topic || !topic.get('details.can_create_post')) { return; } const quotedPost = postStream.findLoadedPost(quoteState.postId); const quotedText = Quote.build(quotedPost, quoteState.buffer); diff --git a/app/assets/javascripts/discourse/initializers/localization.js.es6 b/app/assets/javascripts/discourse/initializers/localization.js.es6 index 5d3ce55f98..2603ddaf4b 100644 --- a/app/assets/javascripts/discourse/initializers/localization.js.es6 +++ b/app/assets/javascripts/discourse/initializers/localization.js.es6 @@ -55,7 +55,9 @@ export default { node = node[segs[i]]; } - node[segs[segs.length-1]] = v; + if (typeof node === "object") { + node[segs[segs.length-1]] = v; + } }); } diff --git a/app/assets/javascripts/discourse/lib/click-track.js.es6 b/app/assets/javascripts/discourse/lib/click-track.js.es6 index 648b49299b..6247973d45 100644 --- a/app/assets/javascripts/discourse/lib/click-track.js.es6 +++ b/app/assets/javascripts/discourse/lib/click-track.js.es6 @@ -26,7 +26,7 @@ export default { } // don't track links in quotes or in elided part - if ($link.parents('aside.quote,.elided').length) { return true; } + let tracking = $link.parents('aside.quote,.elided').length === 0; let href = $link.attr('href') || $link.data('href'); @@ -39,26 +39,31 @@ export default { const userId = $link.data('user-id') || $article.data('user-id'); const ownLink = userId && (userId === Discourse.User.currentProp('id')); - let trackingUrl = Discourse.getURL('/clicks/track?url=' + encodeURIComponent(href)); + let destUrl = href; - if (postId && !$link.data('ignore-post-id')) { - trackingUrl += "&post_id=" + encodeURI(postId); - } - if (topicId) { - trackingUrl += "&topic_id=" + encodeURI(topicId); - } + if (tracking) { - // Update badge clicks unless it's our own - if (!ownLink) { - const $badge = $('span.badge', $link); - if ($badge.length === 1) { - // don't update counts in category badge nor in oneboxes (except when we force it) - if (isValidLink($link)) { - const html = $badge.html(); - const key = `${new Date().toLocaleDateString()}-${postId}-${href}`; - if (/^\d+$/.test(html) && !sessionStorage.getItem(key)) { - sessionStorage.setItem(key, true); - $badge.html(parseInt(html, 10) + 1); + destUrl = Discourse.getURL('/clicks/track?url=' + encodeURIComponent(href)); + + if (postId && !$link.data('ignore-post-id')) { + destUrl += "&post_id=" + encodeURI(postId); + } + if (topicId) { + destUrl += "&topic_id=" + encodeURI(topicId); + } + + // Update badge clicks unless it's our own + if (!ownLink) { + const $badge = $('span.badge', $link); + if ($badge.length === 1) { + // don't update counts in category badge nor in oneboxes (except when we force it) + if (isValidLink($link)) { + const html = $badge.html(); + const key = `${new Date().toLocaleDateString()}-${postId}-${href}`; + if (/^\d+$/.test(html) && !sessionStorage.getItem(key)) { + sessionStorage.setItem(key, true); + $badge.html(parseInt(html, 10) + 1); + } } } } @@ -66,12 +71,12 @@ export default { // If they right clicked, change the destination href if (e.which === 3) { - $link.attr('href', Discourse.SiteSettings.track_external_right_clicks ? trackingUrl : href); + $link.attr('href', Discourse.SiteSettings.track_external_right_clicks ? destUrl : href); return true; } // if they want to open in a new tab, do an AJAX request - if (wantsNewWindow(e)) { + if (tracking && wantsNewWindow(e)) { ajax("/clicks/track", { data: { url: href, @@ -109,7 +114,7 @@ export default { } // If we're on the same site, use the router and track via AJAX - if (DiscourseURL.isInternal(href) && !$link.hasClass('attachment')) { + if (tracking && DiscourseURL.isInternal(href) && !$link.hasClass('attachment')) { ajax("/clicks/track", { data: { url: href, @@ -125,9 +130,9 @@ export default { // Otherwise, use a custom URL with a redirect if (Discourse.User.currentProp('external_links_in_new_tab')) { - window.open(trackingUrl, '_blank').focus(); + window.open(destUrl, '_blank').focus(); } else { - DiscourseURL.redirectTo(trackingUrl); + DiscourseURL.redirectTo(destUrl); } return false; diff --git a/app/assets/javascripts/discourse/lib/formatter.js.es6 b/app/assets/javascripts/discourse/lib/formatter.js.es6 index b26b270b2c..dcce95ca99 100644 --- a/app/assets/javascripts/discourse/lib/formatter.js.es6 +++ b/app/assets/javascripts/discourse/lib/formatter.js.es6 @@ -310,8 +310,10 @@ export function number(val) { if (val > 999999) { formattedNumber = I18n.toNumber(val / 1000000, {precision: 1}); return I18n.t("number.short.millions", {number: formattedNumber}); - } - if (val > 999) { + } else if (val > 99999) { + formattedNumber = I18n.toNumber(val / 1000, {precision: 0}); + return I18n.t("number.short.thousands", {number: formattedNumber}); + } else if (val > 999) { formattedNumber = I18n.toNumber(val / 1000, {precision: 1}); return I18n.t("number.short.thousands", {number: formattedNumber}); } diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index b011f8bab0..c3a7ecd27c 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -169,7 +169,9 @@ export default { }, createTopic() { - this.container.lookup('controller:composer').open({action: Composer.CREATE_TOPIC, draftKey: Composer.CREATE_TOPIC}); + if (this.currentUser && this.currentUser.can_create_topic) { + this.container.lookup('controller:composer').open({action: Composer.CREATE_TOPIC, draftKey: Composer.CREATE_TOPIC}); + } }, pinUnpinTopic() { diff --git a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 index 688207c5b9..8aa802f80f 100644 --- a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 +++ b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 @@ -2,13 +2,15 @@ import { ajax } from 'discourse/lib/ajax'; import { userPath } from 'discourse/lib/url'; import { formatUsername } from 'discourse/lib/utilities'; +let maxGroupMention; + function replaceSpan($e, username, opts) { let extra = ""; let extraClass = ""; if (opts && opts.group) { if (opts.mentionable) { - extra = `data-name='${username}' data-mentionable-user-count='${opts.mentionable.user_count}'`; + extra = `data-name='${username}' data-mentionable-user-count='${opts.mentionable.user_count}' data-max-mentions='${maxGroupMention}'`; extraClass = "notify"; } $e.replaceWith(`@${username}`); @@ -61,6 +63,7 @@ export function fetchUnseenMentions(usernames, topic_id) { r.valid_groups.forEach(vg => foundGroups[vg] = true); r.mentionable_groups.forEach(mg => mentionableGroups[mg.name] = mg); r.cannot_see.forEach(cs => cannotSee[cs] = true); + maxGroupMention = r.max_users_notified_per_group_mention; usernames.forEach(u => checked[u] = true); return r; }); diff --git a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 index 8f81536f22..2fd90c59e0 100644 --- a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 +++ b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 @@ -2,17 +2,23 @@ import parseHTML from 'discourse/helpers/parse-html'; const trimLeft = text => text.replace(/^\s+/,""); const trimRight = text => text.replace(/\s+$/,""); +const countPipes = text => text.replace(/\\\|/,"").match(/\|/g).length; class Tag { - constructor(name, prefix = "", suffix = "") { + constructor(name, prefix = "", suffix = "", inline = false) { this.name = name; this.prefix = prefix; this.suffix = suffix; + this.inline = inline; } decorate(text) { if (this.prefix || this.suffix) { - return [this.prefix, text, this.suffix].join(""); + text = [this.prefix, text, this.suffix].join(""); + } + + if (this.inline) { + text = " " + text + " "; } return text; @@ -30,7 +36,7 @@ class Tag { static blocks() { return ["address", "article", "aside", "dd", "div", "dl", "dt", "fieldset", "figcaption", "figure", - "footer", "form", "header", "hgroup", "hr", "main", "nav", "p", "pre", "section", "ul"]; + "footer", "form", "header", "hgroup", "hr", "main", "nav", "p", "pre", "section"]; } static headings() { @@ -38,25 +44,26 @@ class Tag { } static emphases() { - return [ ["b", "**"], ["strong", "**"], ["i", "_"], ["em", "_"], ["s", "~~"], ["strike", "~~"] ]; + return [ ["b", "**"], ["strong", "**"], ["i", "*"], ["em", "*"], ["s", "~~"], ["strike", "~~"] ]; } static slices() { - return ["dt", "dd", "tr", "thead", "tbody", "tfoot"]; + return ["dt", "dd", "thead", "tbody", "tfoot"]; } static trimmable() { - return [...Tag.blocks(), ...Tag.headings(), ...Tag.slices(), "li", "td", "th", "br", "hr", "blockquote", "table", "ol"]; + return [...Tag.blocks(), ...Tag.headings(), ...Tag.slices(), "li", "td", "th", "br", "hr", "blockquote", "table", "ol", "tr", "ul"]; } static block(name, prefix, suffix) { return class extends Tag { constructor() { super(name, prefix, suffix); + this.gap = "\n\n"; } decorate(text) { - return `\n\n${this.prefix}${text}${this.suffix}\n\n`; + return `${this.gap}${this.prefix}${text}${this.suffix}${this.gap}`; } }; } @@ -69,18 +76,21 @@ class Tag { static emphasis(name, decorator) { return class extends Tag { constructor() { - super(name, decorator, decorator); + super(name, decorator, decorator, true); } decorate(text) { - text = text.trim(); - if (text.includes("\n")) { this.prefix = `<${this.name}>`; this.suffix = ``; } - return super.decorate(text); + let space = text.match(/^\s/) || [""]; + this.prefix = space[0] + this.prefix; + space = text.match(/\s$/) || [""]; + this.suffix = this.suffix + space[0]; + + return super.decorate(text.trim()); } }; } @@ -109,7 +119,7 @@ class Tag { static link() { return class extends Tag { constructor() { - super("a"); + super("a", "", "", true); } decorate(text) { @@ -128,7 +138,7 @@ class Tag { static image() { return class extends Tag { constructor() { - super("img"); + super("img", "", "", true); } toMarkdown() { @@ -143,7 +153,8 @@ class Tag { const height = attr.height || pAttr.height; if (width && height) { - alt = `${alt}|${width}x${height}`; + const pipe = this.element.parentNames.includes("table") ? "\\|" : "|"; + alt = `${alt}${pipe}${width}x${height}`; } return "![" + alt + "](" + src + ")"; @@ -178,14 +189,10 @@ class Tag { toMarkdown() { const text = this.element.innerMarkdown().trim(); - if (text.includes("\n") || text.includes("[![")) { + if (text.includes("\n")) { throw "Unsupported format inside Markdown table cells"; } - if (!this.element.next) { - this.suffix = "|"; - } - return this.decorate(text); } }; @@ -194,7 +201,7 @@ class Tag { static li() { return class extends Tag.slice("li", "\n") { decorate(text) { - const indent = this.element.filterParentNames(["ol", "ul"]).slice(1).map(() => " ").join(""); + const indent = this.element.filterParentNames(["ol", "ul"]).slice(1).map(() => "\t").join(""); return super.decorate(`${indent}* ${trimLeft(text)}`); } }; @@ -210,6 +217,8 @@ class Tag { if (this.element.parentNames.includes("pre")) { this.prefix = '\n\n```\n'; this.suffix = '\n```\n\n'; + } else { + this.inline = true; } text = $('