mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 11:44:49 +08:00
REFACTOR: Migrate markdown functionality in ES6
This commit is contained in:
parent
bc25d9a7a0
commit
a546395397
|
@ -5,7 +5,6 @@ app/assets/javascripts/preload_store.js
|
|||
app/assets/javascripts/pagedown_custom.js
|
||||
app/assets/javascripts/vendor.js
|
||||
app/assets/javascripts/locales/i18n.js
|
||||
app/assets/javascripts/defer/html-sanitizer-bundle.js
|
||||
app/assets/javascripts/ember-addons/
|
||||
app/assets/javascripts/discourse/lib/autosize.js.es6
|
||||
lib/javascripts/locale/
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* global ace:true */
|
||||
import loadScript from 'discourse/lib/load-script';
|
||||
import escapeExpression from 'discourse/lib/utilities';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
mode: 'css',
|
||||
|
@ -16,7 +17,7 @@ export default Ember.Component.extend({
|
|||
render(buffer) {
|
||||
buffer.push("<div class='ace'>");
|
||||
if (this.get('content')) {
|
||||
buffer.push(Discourse.Utilities.escapeExpression(this.get('content')));
|
||||
buffer.push(escapeExpression(this.get('content')));
|
||||
}
|
||||
buffer.push("</div>");
|
||||
},
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
needs: ['modal'],
|
||||
|
||||
|
@ -22,7 +24,7 @@ export default Ember.Controller.extend({
|
|||
returned = "<pre class='badge-query-plan'>";
|
||||
|
||||
_.each(raw, function(linehash) {
|
||||
returned += Discourse.Utilities.escapeExpression(linehash["QUERY PLAN"]);
|
||||
returned += escapeExpression(linehash["QUERY PLAN"]);
|
||||
returned += "<br>";
|
||||
});
|
||||
|
||||
|
@ -32,7 +34,7 @@ export default Ember.Controller.extend({
|
|||
|
||||
processed_sample: Ember.computed.map('model.sample', function(grant) {
|
||||
var i18nKey = 'admin.badges.preview.grant.with',
|
||||
i18nParams = { username: Discourse.Utilities.escapeExpression(grant.username) };
|
||||
i18nParams = { username: escapeExpression(grant.username) };
|
||||
|
||||
if (grant.post_id) {
|
||||
i18nKey += "_post";
|
||||
|
@ -41,7 +43,7 @@ export default Ember.Controller.extend({
|
|||
|
||||
if (grant.granted_at) {
|
||||
i18nKey += "_time";
|
||||
i18nParams.time = Discourse.Utilities.escapeExpression(moment(grant.granted_at).format(I18n.t('dates.long_with_year')));
|
||||
i18nParams.time = escapeExpression(moment(grant.granted_at).format(I18n.t('dates.long_with_year')));
|
||||
}
|
||||
|
||||
return I18n.t(i18nKey, i18nParams);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { htmlHelper } from 'discourse/lib/helpers';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
|
||||
export default htmlHelper(str => Discourse.Utilities.escapeExpression(str).replace(/\n/g, "<br>"));
|
||||
export default htmlHelper(str => escapeExpression(str).replace(/\n/g, "<br>"));
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import AdminUser from 'admin/models/admin-user';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
|
||||
const StaffActionLog = Discourse.Model.extend({
|
||||
showFullDetails: false,
|
||||
|
@ -19,14 +20,14 @@ const StaffActionLog = Discourse.Model.extend({
|
|||
formatted += this.format('admin.logs.staff_actions.previous_value', 'previous_value');
|
||||
}
|
||||
if (!this.get('useModalForDetails')) {
|
||||
if (this.get('details')) formatted += Discourse.Utilities.escapeExpression(this.get('details')) + '<br/>';
|
||||
if (this.get('details')) formatted += escapeExpression(this.get('details')) + '<br/>';
|
||||
}
|
||||
return formatted;
|
||||
}.property('ip_address', 'email', 'topic_id', 'post_id', 'category_id'),
|
||||
|
||||
format: function(label, propertyName) {
|
||||
if (this.get(propertyName)) {
|
||||
return ('<b>' + I18n.t(label) + ':</b> ' + Discourse.Utilities.escapeExpression(this.get(propertyName)) + '<br/>');
|
||||
return ('<b>' + I18n.t(label) + ':</b> ' + escapeExpression(this.get(propertyName)) + '<br/>');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import debounce from 'discourse/lib/debounce';
|
||||
import { renderSpinner } from 'discourse/helpers/loading-spinner';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
|
||||
export default Ember.View.extend({
|
||||
classNames: ["admin-backups-logs"],
|
||||
|
@ -19,7 +20,7 @@ export default Ember.View.extend({
|
|||
let formattedLogs = this.get("formattedLogs");
|
||||
for (let i = this.get("index"), length = logs.length; i < length; i++) {
|
||||
const date = logs[i].get("timestamp"),
|
||||
message = Discourse.Utilities.escapeExpression(logs[i].get("message"));
|
||||
message = escapeExpression(logs[i].get("message"));
|
||||
formattedLogs += "[" + date + "] " + message + "\n";
|
||||
}
|
||||
// update the formatted logs & cache index
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -26,7 +26,7 @@ window.Discourse = Ember.Application.extend(Discourse.Ajax, {
|
|||
},
|
||||
|
||||
getURLWithCDN: function(url) {
|
||||
url = this.getURL(url);
|
||||
url = Discourse.getURL(url);
|
||||
// only relative urls
|
||||
if (Discourse.CDN && /^\/[^\/]/.test(url)) {
|
||||
url = Discourse.CDN + url;
|
||||
|
@ -142,6 +142,15 @@ window.Discourse = Ember.Application.extend(Discourse.Ajax, {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
var utils = require('discourse/lib/utilities');
|
||||
Discourse.Utilities = {};
|
||||
Object.keys(utils).forEach(function(k) {
|
||||
Discourse.Utilities[k] = function() {
|
||||
Ember.warn('Discourse.Utilities is deprecated. Import it as a module');
|
||||
return utils[k].apply(utils, arguments);
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
requiresRefresh: function(){
|
||||
|
@ -170,20 +179,17 @@ window.Discourse = Ember.Application.extend(Discourse.Ajax, {
|
|||
})
|
||||
}).create();
|
||||
|
||||
function RemovedObject(name) {
|
||||
this._removedName = name;
|
||||
}
|
||||
|
||||
function methodMissing() {
|
||||
console.warn("The " + this._removedName + " object has been removed from Discourse " +
|
||||
"and your plugin needs to be updated.");
|
||||
Discourse.Markdown = {
|
||||
whiteListTag: Ember.K,
|
||||
whiteListIframe: Ember.K
|
||||
};
|
||||
|
||||
Discourse.RemovedObject = RemovedObject;
|
||||
|
||||
['reopen', 'registerButton', 'on', 'off'].forEach(function(m) { RemovedObject.prototype[m] = methodMissing; });
|
||||
|
||||
['discourse/views/post', 'discourse/components/post-menu'].forEach(function(moduleName) {
|
||||
define(moduleName, [], function() { return new RemovedObject(moduleName); });
|
||||
});
|
||||
|
||||
Discourse.Dialect = {
|
||||
inlineRegexp: Ember.K,
|
||||
addPreProcessor: Ember.K,
|
||||
replaceBlock: Ember.K,
|
||||
inlineReplace: Ember.K,
|
||||
registerInline: Ember.K,
|
||||
registerEmoji: Ember.K
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { emojiUnescape } from 'discourse/lib/text';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
size: 'medium',
|
||||
|
@ -38,7 +39,7 @@ export default Ember.Component.extend({
|
|||
if (size === 'large') {
|
||||
const longDescription = this.get('badge.long_description');
|
||||
if (!_.isEmpty(longDescription)) {
|
||||
return Discourse.Emoji.unescape(longDescription);
|
||||
return emojiUnescape(longDescription);
|
||||
}
|
||||
}
|
||||
return this.get('badge.description');
|
||||
|
|
|
@ -3,6 +3,12 @@ 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';
|
||||
import { fetchUnseenTagHashtags, linkSeenTagHashtags } from 'discourse/lib/link-tag-hashtag';
|
||||
import { load } from 'pretty-text/oneboxer';
|
||||
|
||||
import { tinyAvatar,
|
||||
displayErrorForUpload,
|
||||
getUploadMarkdown,
|
||||
validateUploadedFiles } from 'discourse/lib/utilities';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['wmd-controls'],
|
||||
|
@ -60,7 +66,7 @@ export default Ember.Component.extend({
|
|||
if (posts && topicId === topic.get('id')) {
|
||||
const quotedPost = posts.findProperty("post_number", postNumber);
|
||||
if (quotedPost) {
|
||||
return Discourse.Utilities.tinyAvatar(quotedPost.get('avatar_template'));
|
||||
return tinyAvatar(quotedPost.get('avatar_template'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -199,7 +205,7 @@ export default Ember.Component.extend({
|
|||
});
|
||||
|
||||
$element.on('fileuploadsubmit', (e, data) => {
|
||||
const isUploading = Discourse.Utilities.validateUploadedFiles(data.files);
|
||||
const isUploading = validateUploadedFiles(data.files);
|
||||
data.formData = { type: "composer" };
|
||||
this.setProperties({ uploadProgress: 0, isUploading });
|
||||
return isUploading;
|
||||
|
@ -227,7 +233,7 @@ export default Ember.Component.extend({
|
|||
this._xhr = null;
|
||||
|
||||
if (!userCancelled) {
|
||||
Discourse.Utilities.displayErrorForUpload(data);
|
||||
displayErrorForUpload(data);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -235,7 +241,7 @@ export default Ember.Component.extend({
|
|||
// replace upload placeholder
|
||||
if (upload && upload.url) {
|
||||
if (!this._xhr || !this._xhr._userCancelled) {
|
||||
const markdown = Discourse.Utilities.getUploadMarkdown(upload);
|
||||
const markdown = getUploadMarkdown(upload);
|
||||
this.set('composer.reply', this.get('composer.reply').replace(uploadPlaceholder, markdown));
|
||||
this._resetUpload(false);
|
||||
} else {
|
||||
|
@ -243,7 +249,7 @@ export default Ember.Component.extend({
|
|||
}
|
||||
} else {
|
||||
this._resetUpload(true);
|
||||
Discourse.Utilities.displayErrorForUpload(upload);
|
||||
displayErrorForUpload(upload);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -492,7 +498,7 @@ export default Ember.Component.extend({
|
|||
}
|
||||
|
||||
// Paint oneboxes
|
||||
$('a.onebox', $preview).each((i, e) => Discourse.Onebox.load(e, refresh));
|
||||
$('a.onebox', $preview).each((i, e) => load(e, refresh));
|
||||
this.trigger('previewRefreshed', $preview);
|
||||
this.sendAction('afterRefresh', $preview);
|
||||
},
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
/*global Mousetrap:true */
|
||||
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 { showSelector } from "discourse/lib/emoji/toolbar";
|
||||
import Category from 'discourse/models/category';
|
||||
import { categoryHashtagTriggerRule } from 'discourse/lib/category-hashtags';
|
||||
import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags';
|
||||
import { search as searchCategoryTag } from 'discourse/lib/category-tag-search';
|
||||
import { SEPARATOR } from 'discourse/lib/category-hashtags';
|
||||
import { cook } from 'discourse/lib/text';
|
||||
import { translations } from 'pretty-text/emoji/data';
|
||||
import { emojiSearch } from 'pretty-text/emoji';
|
||||
import { emojiUrlFor } from 'discourse/lib/text';
|
||||
|
||||
// Our head can be a static string or a function that returns a string
|
||||
// based on input (like for numbered lists).
|
||||
|
@ -193,7 +196,7 @@ export default Ember.Component.extend({
|
|||
this._applyEmojiAutocomplete(container, $editorInput);
|
||||
this._applyCategoryHashtagAutocomplete(container, $editorInput);
|
||||
|
||||
loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true));
|
||||
this.set('ready', true);
|
||||
|
||||
const mouseTrap = Mousetrap(this.$('.d-editor-input')[0]);
|
||||
|
||||
|
@ -241,9 +244,10 @@ export default Ember.Component.extend({
|
|||
|
||||
const value = this.get('value');
|
||||
const markdownOptions = this.get('markdownOptions') || {};
|
||||
markdownOptions.sanitize = true;
|
||||
|
||||
this.set('preview', Discourse.Dialect.cook(value || "", markdownOptions));
|
||||
markdownOptions.siteSettings = this.siteSettings;
|
||||
|
||||
this.set('preview', cook(value));
|
||||
Ember.run.scheduleOnce('afterRender', () => {
|
||||
if (this._state !== "inDOM") { return; }
|
||||
const $preview = this.$('.d-editor-preview');
|
||||
|
@ -326,15 +330,15 @@ export default Ember.Component.extend({
|
|||
return resolve(["slight_smile", "smile", "wink", "sunny", "blush"]);
|
||||
}
|
||||
|
||||
if (Discourse.Emoji.translations[full]) {
|
||||
return resolve([Discourse.Emoji.translations[full]]);
|
||||
if (translations[full]) {
|
||||
return resolve([translations[full]]);
|
||||
}
|
||||
|
||||
const options = Discourse.Emoji.search(term, {maxResults: 5});
|
||||
const options = emojiSearch(term, {maxResults: 5});
|
||||
|
||||
return resolve(options);
|
||||
}).then(list => list.map(code => {
|
||||
return {code, src: Discourse.Emoji.urlFor(code)};
|
||||
return {code, src: emojiUrlFor(code)};
|
||||
})).then(list => {
|
||||
if (list.length) {
|
||||
list.push({ label: I18n.t("composer.more_emoji") });
|
||||
|
@ -524,7 +528,6 @@ export default Ember.Component.extend({
|
|||
const linkUrl = (origLink.indexOf('://') === -1) ? `http://${origLink}` : origLink;
|
||||
const sel = this._lastSel;
|
||||
|
||||
|
||||
if (Ember.isEmpty(linkUrl)) { return; }
|
||||
|
||||
const linkText = this.get('linkText') || '';
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
const removed = new Discourse.RemovedObject('discourse/components/poster-name');
|
||||
export default removed;
|
|
@ -1,5 +1,6 @@
|
|||
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||
import StringBuffer from 'discourse/mixins/string-buffer';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
|
||||
export default Ember.Component.extend(StringBuffer, {
|
||||
classNames: ['topic-statuses'],
|
||||
|
@ -29,7 +30,7 @@ export default Ember.Component.extend(StringBuffer, {
|
|||
const self = this;
|
||||
|
||||
const renderIcon = function(name, key, actionable) {
|
||||
const title = Discourse.Utilities.escapeExpression(I18n.t(`topic_statuses.${key}.help`)),
|
||||
const title = escapeExpression(I18n.t(`topic_statuses.${key}.help`)),
|
||||
startTag = actionable ? "a href" : "span",
|
||||
endTag = actionable ? "a" : "span",
|
||||
iconArgs = key === 'unpinned' ? { 'class': 'unpinned' } : null,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import LoadMore from "discourse/mixins/load-more";
|
||||
import ClickTrack from 'discourse/lib/click-track';
|
||||
import { selectedText } from 'discourse/lib/utilities';
|
||||
|
||||
export default Ember.Component.extend(LoadMore, {
|
||||
loading: false,
|
||||
|
@ -19,7 +20,7 @@ export default Ember.Component.extend(LoadMore, {
|
|||
// bypass if we are selecting stuff
|
||||
const selection = window.getSelection && window.getSelection();
|
||||
if (selection.type === "Range" || selection.rangeCount > 0) {
|
||||
if (Discourse.Utilities.selectedText() !== "") {
|
||||
if (selectedText() !== "") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
import { allowsImages } from 'discourse/lib/utilities';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
@computed("selected", "system_avatar_upload_id", "gravatar_avatar_upload_id", "custom_avatar_upload_id")
|
||||
selectedUploadId(selected, system, gravatar, custom) {
|
||||
|
@ -22,7 +24,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
|
||||
@computed()
|
||||
allowAvatarUpload() {
|
||||
return this.siteSettings.allow_uploaded_avatars && Discourse.Utilities.allowsImages();
|
||||
return this.siteSettings.allow_uploaded_avatars && allowsImages();
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
|
|
@ -4,6 +4,7 @@ import Draft from 'discourse/models/draft';
|
|||
import Composer from 'discourse/models/composer';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { relativeAge } from 'discourse/lib/formatter';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
|
||||
function loadDraft(store, opts) {
|
||||
opts = opts || {};
|
||||
|
@ -355,7 +356,7 @@ export default Ember.Controller.extend({
|
|||
|
||||
if (currentTopic) {
|
||||
buttons.push({
|
||||
"label": I18n.t("composer.reply_here") + "<br/><div class='topic-title overflow-ellipsis'>" + Discourse.Utilities.escapeExpression(currentTopic.get('title')) + "</div>",
|
||||
"label": I18n.t("composer.reply_here") + "<br/><div class='topic-title overflow-ellipsis'>" + escapeExpression(currentTopic.get('title')) + "</div>",
|
||||
"class": "btn btn-reply-here",
|
||||
"callback": function() {
|
||||
composer.set('topic', currentTopic);
|
||||
|
@ -366,7 +367,7 @@ export default Ember.Controller.extend({
|
|||
}
|
||||
|
||||
buttons.push({
|
||||
"label": I18n.t("composer.reply_original") + "<br/><div class='topic-title overflow-ellipsis'>" + Discourse.Utilities.escapeExpression(this.get('model.topic.title')) + "</div>",
|
||||
"label": I18n.t("composer.reply_original") + "<br/><div class='topic-title overflow-ellipsis'>" + escapeExpression(this.get('model.topic.title')) + "</div>",
|
||||
"class": "btn-primary btn-reply-on-original",
|
||||
"callback": function() {
|
||||
self.save(true);
|
||||
|
|
|
@ -2,6 +2,7 @@ import debounce from 'discourse/lib/debounce';
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { setting } from 'discourse/lib/computed';
|
||||
import { on } from 'ember-addons/ember-computed-decorators';
|
||||
import { emailValid } from 'discourse/lib/utilities';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
needs: ['login'],
|
||||
|
@ -120,7 +121,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
});
|
||||
}
|
||||
|
||||
if (Discourse.Utilities.emailValid(email)) {
|
||||
if (emailValid(email)) {
|
||||
return Discourse.InputValidation.create({
|
||||
ok: true,
|
||||
reason: I18n.t('user.email.ok')
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
|
@ -23,7 +24,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
|
||||
var success = function(data) {
|
||||
// don't tell people what happened, this keeps it more secure (ensure same on server)
|
||||
var escaped = Discourse.Utilities.escapeExpression(self.get('accountEmailOrUsername'));
|
||||
var escaped = escapeExpression(self.get('accountEmailOrUsername'));
|
||||
var isEmail = self.get('accountEmailOrUsername').match(/@/);
|
||||
|
||||
var key = 'forgot_password.complete_' + (isEmail ? 'email' : 'username');
|
||||
|
|
|
@ -2,6 +2,7 @@ import { translateResults, searchContextDescription, getSearchKey, isValidSearch
|
|||
import showModal from 'discourse/lib/show-modal';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import Category from 'discourse/models/category';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
|
||||
const SortOrders = [
|
||||
{name: I18n.t('search.relevance'), id: 0},
|
||||
|
@ -75,7 +76,7 @@ export default Ember.Controller.extend({
|
|||
}
|
||||
});
|
||||
}
|
||||
return Discourse.Utilities.escapeExpression(q);
|
||||
return escapeExpression(q);
|
||||
},
|
||||
|
||||
_searchOnSortChange: true,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { emailValid } from 'discourse/lib/utilities';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
needs: ['user-invited-show'],
|
||||
|
@ -19,11 +20,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
if (Ember.isEmpty(this.get('emailOrUsername'))) return true;
|
||||
const emailOrUsername = this.get('emailOrUsername').trim();
|
||||
// when inviting to forum, email must be valid
|
||||
if (!this.get('invitingToTopic') && !Discourse.Utilities.emailValid(emailOrUsername)) return true;
|
||||
if (!this.get('invitingToTopic') && !emailValid(emailOrUsername)) return true;
|
||||
// normal users (not admin) can't invite users to private topic via email
|
||||
if (!this.get('isAdmin') && this.get('isPrivateTopic') && Discourse.Utilities.emailValid(emailOrUsername)) return true;
|
||||
if (!this.get('isAdmin') && this.get('isPrivateTopic') && emailValid(emailOrUsername)) return true;
|
||||
// when inviting to private topic via email, group name must be specified
|
||||
if (this.get('isPrivateTopic') && Ember.isEmpty(this.get('model.groupNames')) && Discourse.Utilities.emailValid(emailOrUsername)) return true;
|
||||
if (this.get('isPrivateTopic') && Ember.isEmpty(this.get('model.groupNames')) && emailValid(emailOrUsername)) return true;
|
||||
if (this.get('model.details.can_invite_to')) return false;
|
||||
return false;
|
||||
}.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving'),
|
||||
|
@ -34,11 +35,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
if (Ember.isEmpty(this.get('emailOrUsername'))) return true;
|
||||
const emailOrUsername = this.get('emailOrUsername').trim();
|
||||
// email must be valid
|
||||
if (!Discourse.Utilities.emailValid(emailOrUsername)) return true;
|
||||
if (!emailValid(emailOrUsername)) return true;
|
||||
// normal users (not admin) can't invite users to private topic via email
|
||||
if (!this.get('isAdmin') && this.get('isPrivateTopic') && Discourse.Utilities.emailValid(emailOrUsername)) return true;
|
||||
if (!this.get('isAdmin') && this.get('isPrivateTopic') && emailValid(emailOrUsername)) return true;
|
||||
// when inviting to private topic via email, group name must be specified
|
||||
if (this.get('isPrivateTopic') && Ember.isEmpty(this.get('model.groupNames')) && Discourse.Utilities.emailValid(emailOrUsername)) return true;
|
||||
if (this.get('isPrivateTopic') && Ember.isEmpty(this.get('model.groupNames')) && emailValid(emailOrUsername)) return true;
|
||||
return false;
|
||||
}.property('emailOrUsername', 'model.saving', 'isPrivateTopic', 'model.groupNames', 'hasCustomMessage'),
|
||||
|
||||
|
@ -71,11 +72,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
|
||||
// Show Groups? (add invited user to private group)
|
||||
showGroups: function() {
|
||||
return this.get('isAdmin') && (Discourse.Utilities.emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso && Discourse.SiteSettings.enable_local_logins && !this.get('isMessage');
|
||||
return this.get('isAdmin') && (emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso && Discourse.SiteSettings.enable_local_logins && !this.get('isMessage');
|
||||
}.property('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'isMessage', 'invitingToTopic'),
|
||||
|
||||
showCustomMessage: function() {
|
||||
return (this.get('model') === this.currentUser || Discourse.Utilities.emailValid(this.get('emailOrUsername')));
|
||||
return (this.get('model') === this.currentUser || emailValid(this.get('emailOrUsername')));
|
||||
}.property('emailOrUsername'),
|
||||
|
||||
// Instructional text for the modal.
|
||||
|
@ -95,7 +96,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
// when inviting to a topic, display instructions based on provided entity
|
||||
if (Ember.isEmpty(this.get('emailOrUsername'))) {
|
||||
return I18n.t('topic.invite_reply.to_topic_blank');
|
||||
} else if (Discourse.Utilities.emailValid(this.get('emailOrUsername'))) {
|
||||
} else if (emailValid(this.get('emailOrUsername'))) {
|
||||
this.set("inviteIcon", "envelope");
|
||||
return I18n.t('topic.invite_reply.to_topic_email');
|
||||
} else {
|
||||
|
@ -125,7 +126,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
return I18n.t('topic.invite_private.success_group');
|
||||
} else if (this.get('isMessage')) {
|
||||
return I18n.t('topic.invite_private.success');
|
||||
} else if ( Discourse.Utilities.emailValid(this.get('emailOrUsername')) ) {
|
||||
} else if ( emailValid(this.get('emailOrUsername')) ) {
|
||||
return I18n.t('topic.invite_reply.success_email', { emailOrUsername: this.get('emailOrUsername') });
|
||||
} else {
|
||||
return I18n.t('topic.invite_reply.success_username');
|
||||
|
|
|
@ -2,6 +2,7 @@ import { setting } from 'discourse/lib/computed';
|
|||
import CanCheckEmails from 'discourse/mixins/can-check-emails';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { cook } from 'discourse/lib/text';
|
||||
|
||||
export default Ember.Controller.extend(CanCheckEmails, {
|
||||
|
||||
|
@ -155,7 +156,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
if (Discourse.User.currentProp('id') === model.get('id')) {
|
||||
Discourse.User.currentProp('name', model.get('name'));
|
||||
}
|
||||
model.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(model.get('bio_raw'))));
|
||||
model.set('bio_cooked', cook(model.get('bio_raw')));
|
||||
this.set('saved', true);
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import loadScript from 'discourse/lib/load-script';
|
||||
import Quote from 'discourse/lib/quote';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { selectedText } from 'discourse/lib/utilities';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
needs: ['topic', 'composer'],
|
||||
|
||||
_loadSanitizer: function() {
|
||||
loadScript('defer/html-sanitizer-bundle');
|
||||
}.on('init'),
|
||||
|
||||
@computed('buffer', 'postId')
|
||||
post(buffer, postId) {
|
||||
if (!postId || Ember.isEmpty(buffer)) { return null; }
|
||||
|
@ -49,12 +45,12 @@ export default Ember.Controller.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
const selectedText = Discourse.Utilities.selectedText();
|
||||
if (this.get('buffer') === selectedText) return;
|
||||
const selVal = selectedText();
|
||||
if (this.get('buffer') === selVal) return;
|
||||
|
||||
// we need to retrieve the post data from the posts collection in the topic controller
|
||||
this.set('postId', postId);
|
||||
this.set('buffer', selectedText);
|
||||
this.set('buffer', selVal);
|
||||
|
||||
// create a marker element
|
||||
const markerElement = document.createElement("span");
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { allowsAttachments, authorizesAllExtensions, authorizedExtensions } from 'discourse/lib/utilities';
|
||||
|
||||
export function uploadTranslate(key, options) {
|
||||
options = options || {};
|
||||
if (Discourse.Utilities.allowsAttachments()) { key += "_with_attachments"; }
|
||||
if (allowsAttachments()) { key += "_with_attachments"; }
|
||||
return I18n.t(`upload_selector.${key}`, options);
|
||||
}
|
||||
|
||||
|
@ -16,13 +17,13 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
|
||||
@computed
|
||||
uploadIcon() {
|
||||
return Discourse.Utilities.allowsAttachments() ? "upload" : "picture-o";
|
||||
return allowsAttachments() ? "upload" : "picture-o";
|
||||
},
|
||||
|
||||
@computed('controller.local')
|
||||
tip(local) {
|
||||
const source = local ? "local" : "remote";
|
||||
const authorized_extensions = Discourse.Utilities.authorizesAllExtensions() ? "" : `(${Discourse.Utilities.authorizedExtensions()})`;
|
||||
const authorized_extensions = authorizesAllExtensions() ? "" : `(${authorizedExtensions()})`;
|
||||
return uploadTranslate(`${source}_tip`, { authorized_extensions });
|
||||
},
|
||||
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
/**
|
||||
This addition handles auto linking of text. When included, it will parse out links and create
|
||||
a hrefs for them.
|
||||
**/
|
||||
var urlReplacerArgs = {
|
||||
matcher: /^((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/,
|
||||
spaceOrTagBoundary: true,
|
||||
|
||||
emitter: function(matches) {
|
||||
var url = matches[1],
|
||||
displayUrl = url;
|
||||
|
||||
// Don't autolink a markdown link to something
|
||||
if (url.match(/\]\[\d$/)) { return; }
|
||||
|
||||
// If we improperly caught a markdown link abort
|
||||
if (url.match(/\(http/)) { return; }
|
||||
|
||||
if (url.match(/^www/)) { url = "http://" + url; }
|
||||
return ['a', {href: url}, displayUrl];
|
||||
}
|
||||
};
|
||||
|
||||
Discourse.Dialect.inlineRegexp(_.merge({start: 'http'}, urlReplacerArgs));
|
||||
Discourse.Dialect.inlineRegexp(_.merge({start: 'www'}, urlReplacerArgs));
|
|
@ -1,182 +0,0 @@
|
|||
Discourse.BBCode = {};
|
||||
|
||||
/**
|
||||
Create a simple BBCode tag handler
|
||||
|
||||
@method replaceBBCode
|
||||
@param {tag} tag the tag we want to match
|
||||
@param {function} emitter the function that creates JsonML for the tag
|
||||
@param {Object} opts options to pass to Discourse.Dialect.inlineBetween
|
||||
@param {Function} [opts.emitter] The function that will be called with the contents and returns JsonML.
|
||||
@param {String} [opts.start] The starting token we want to find
|
||||
@param {String} [opts.stop] The ending token we want to find
|
||||
@param {String} [opts.between] A shortcut for when the `start` and `stop` are the same.
|
||||
@param {Boolean} [opts.rawContents] If true, the contents between the tokens will not be parsed.
|
||||
@param {Boolean} [opts.wordBoundary] If true, the match must be on a word boundary
|
||||
@param {Boolean} [opts.spaceBoundary] If true, the match must be on a sppace boundary
|
||||
**/
|
||||
|
||||
Discourse.BBCode.register = function(codeName, args, emitter) {
|
||||
|
||||
// Optional second param for args
|
||||
if (typeof args === "function") {
|
||||
emitter = args;
|
||||
args = {};
|
||||
}
|
||||
|
||||
Discourse.Dialect.replaceBlock({
|
||||
start: new RegExp("\\[" + codeName + "(=[^\\[\\]]+)?\\]([\\s\\S]*)", "igm"),
|
||||
stop: new RegExp("\\[\\/" + codeName + "\\]", "igm"),
|
||||
emitter: function(blockContents, matches, options) {
|
||||
while (blockContents.length && (typeof blockContents[0] === "string" || blockContents[0] instanceof String)) {
|
||||
blockContents[0] = String(blockContents[0]).replace(/^\s+/, '');
|
||||
if (!blockContents[0].length) {
|
||||
blockContents.shift();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var contents = [];
|
||||
if (blockContents.length) {
|
||||
var self = this;
|
||||
|
||||
var nextContents = blockContents.slice(1);
|
||||
blockContents = this.processBlock(blockContents[0], nextContents).concat(nextContents);
|
||||
|
||||
blockContents.forEach(function (bc) {
|
||||
if (typeof bc === "string" || bc instanceof String) {
|
||||
var processed = self.processInline(String(bc));
|
||||
if (processed.length) {
|
||||
contents.push(['p'].concat(processed));
|
||||
}
|
||||
} else {
|
||||
contents.push(bc);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!args.singlePara && contents.length === 1 && contents[0] instanceof Array && contents[0][0] === "para") {
|
||||
contents[0].shift();
|
||||
contents = contents[0];
|
||||
}
|
||||
var result = emitter(contents, matches[1] ? matches[1].replace(/^=|\"/g, '') : null, options);
|
||||
return args.noWrap ? result : ['p', result];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Discourse.BBCode.replaceBBCode = function (tag, emitter, opts) {
|
||||
opts = opts || {};
|
||||
opts = _.merge(opts, { start: "[" + tag + "]", stop: "[/" + tag + "]", emitter: emitter });
|
||||
Discourse.Dialect.inlineBetween(opts);
|
||||
|
||||
tag = tag.toUpperCase();
|
||||
opts = _.merge(opts, { start: "[" + tag + "]", stop: "[/" + tag + "]", emitter: emitter });
|
||||
Discourse.Dialect.inlineBetween(opts);
|
||||
};
|
||||
|
||||
/**
|
||||
Shortcut to call replaceBBCode with `rawContents` as true.
|
||||
|
||||
@method replaceBBCode
|
||||
@param {tag} tag the tag we want to match
|
||||
@param {function} emitter the function that creates JsonML for the tag
|
||||
**/
|
||||
Discourse.BBCode.rawBBCode = function (tag, emitter) {
|
||||
Discourse.BBCode.replaceBBCode(tag, emitter, { rawContents: true });
|
||||
};
|
||||
|
||||
/**
|
||||
Creates a BBCode handler that accepts parameters. Passes them to the emitter.
|
||||
|
||||
@method replaceBBCodeParamsRaw
|
||||
@param {tag} tag the tag we want to match
|
||||
@param {function} emitter the function that creates JsonML for the tag
|
||||
**/
|
||||
Discourse.BBCode.replaceBBCodeParamsRaw = function (tag, emitter) {
|
||||
var opts = {
|
||||
rawContents: true,
|
||||
emitter: function(contents) {
|
||||
var regexp = /^([^\]]+)\]([\S\s]*)$/,
|
||||
m = regexp.exec(contents);
|
||||
|
||||
if (m) { return emitter.call(this, m[1], m[2]); }
|
||||
}
|
||||
};
|
||||
|
||||
Discourse.Dialect.inlineBetween(_.merge(opts, { start: "[" + tag + "=", stop: "[/" + tag + "]" }));
|
||||
|
||||
tag = tag.toUpperCase();
|
||||
Discourse.Dialect.inlineBetween(_.merge(opts, { start: "[" + tag + "=", stop: "[/" + tag + "]" }));
|
||||
};
|
||||
|
||||
/**
|
||||
Filters an array of JSON-ML nodes, removing nodes that represent empty lines ("\n").
|
||||
|
||||
@method removeEmptyLines
|
||||
@param {Array} [contents] Array of JSON-ML nodes
|
||||
**/
|
||||
Discourse.BBCode.removeEmptyLines = function (contents) {
|
||||
var result = [];
|
||||
for (var i=0; i < contents.length; i++) {
|
||||
if (contents[i] !== "\n") { result.push(contents[i]); }
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
Discourse.BBCode.replaceBBCode('b', function(contents) { return ['span', {'class': 'bbcode-b'}].concat(contents); });
|
||||
Discourse.BBCode.replaceBBCode('i', function(contents) { return ['span', {'class': 'bbcode-i'}].concat(contents); });
|
||||
Discourse.BBCode.replaceBBCode('u', function(contents) { return ['span', {'class': 'bbcode-u'}].concat(contents); });
|
||||
Discourse.BBCode.replaceBBCode('s', function(contents) { return ['span', {'class': 'bbcode-s'}].concat(contents); });
|
||||
Discourse.Markdown.whiteListTag('span', 'class', /^bbcode-[bius]$/);
|
||||
|
||||
Discourse.BBCode.replaceBBCode('ul', function(contents) { return ['ul'].concat(Discourse.BBCode.removeEmptyLines(contents)); });
|
||||
Discourse.BBCode.replaceBBCode('ol', function(contents) { return ['ol'].concat(Discourse.BBCode.removeEmptyLines(contents)); });
|
||||
Discourse.BBCode.replaceBBCode('li', function(contents) { return ['li'].concat(Discourse.BBCode.removeEmptyLines(contents)); });
|
||||
|
||||
Discourse.BBCode.rawBBCode('img', function(contents) { return ['img', {href: contents}]; });
|
||||
Discourse.BBCode.rawBBCode('email', function(contents) { return ['a', {href: "mailto:" + contents, 'data-bbcode': true}, contents]; });
|
||||
|
||||
Discourse.BBCode.replaceBBCode('url', function(contents) {
|
||||
if (!Array.isArray(contents)) { return; }
|
||||
if (contents.length === 1 && contents[0][0] === 'a') {
|
||||
// single-line bbcode links shouldn't be oneboxed, so we mark this as a bbcode link.
|
||||
if (typeof contents[0][1] !== 'object') { contents[0].splice(1, 0, {}); }
|
||||
contents[0][1]['data-bbcode'] = true;
|
||||
}
|
||||
return ['concat'].concat(contents);
|
||||
});
|
||||
Discourse.BBCode.replaceBBCodeParamsRaw('url', function(param, contents) {
|
||||
var url = param.replace(/(^")|("$)/g, '');
|
||||
return ['a', {'href': url}].concat(this.processInline(contents));
|
||||
});
|
||||
Discourse.Dialect.on('parseNode', function(event) {
|
||||
if (!Array.isArray(event.node)) { return; }
|
||||
var result = [ event.node[0] ];
|
||||
var nodes = event.node.slice(1);
|
||||
var i, j;
|
||||
for (i = 0; i < nodes.length; i++) {
|
||||
if (Array.isArray(nodes[i]) && nodes[i][0] === 'concat') {
|
||||
for (j = 1; j < nodes[i].length; j++) { result.push(nodes[i][j]); }
|
||||
} else {
|
||||
result.push(nodes[i]);
|
||||
}
|
||||
}
|
||||
for (i = 0; i < result.length; i++) { event.node[i] = result[i]; }
|
||||
});
|
||||
|
||||
Discourse.BBCode.replaceBBCodeParamsRaw("email", function(param, contents) {
|
||||
return ['a', {href: "mailto:" + param, 'data-bbcode': true}].concat(contents);
|
||||
});
|
||||
|
||||
// Handles `[code] ... [/code]` blocks
|
||||
Discourse.Dialect.replaceBlock({
|
||||
start: /(\[code\])([\s\S]*)/igm,
|
||||
stop: /\[\/code\]/igm,
|
||||
rawContents: true,
|
||||
|
||||
emitter: function(blockContents) {
|
||||
var inner = blockContents.join("\n");
|
||||
return ['p', ['pre', ['code', {'class': Discourse.SiteSettings.default_code_lang}, inner]]];
|
||||
}
|
||||
});
|
|
@ -1,78 +0,0 @@
|
|||
/**
|
||||
markdown-js doesn't ensure that em/strong codes are present on word boundaries.
|
||||
So we create our own handlers here.
|
||||
**/
|
||||
|
||||
// From PageDown
|
||||
var aLetter = /[a-zA-Z0-9\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376-\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0523\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0621-\u064a\u0660-\u0669\u066e-\u066f\u0671-\u06d3\u06d5\u06e5-\u06e6\u06ee-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07c0-\u07ea\u07f4-\u07f5\u07fa\u0904-\u0939\u093d\u0950\u0958-\u0961\u0966-\u096f\u0971-\u0972\u097b-\u097f\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc-\u09dd\u09df-\u09e1\u09e6-\u09f1\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a66-\u0a6f\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0-\u0ae1\u0ae6-\u0aef\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b35-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b66-\u0b6f\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0be6-\u0bef\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58-\u0c59\u0c60-\u0c61\u0c66-\u0c6f\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0-\u0ce1\u0ce6-\u0cef\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d28\u0d2a-\u0d39\u0d3d\u0d60-\u0d61\u0d66-\u0d6f\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32-\u0e33\u0e40-\u0e46\u0e50-\u0e59\u0e81-\u0e82\u0e84\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa-\u0eab\u0ead-\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0ed0-\u0ed9\u0edc-\u0edd\u0f00\u0f20-\u0f29\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8b\u1000-\u102a\u103f-\u1049\u1050-\u1055\u105a-\u105d\u1061\u1065-\u1066\u106e-\u1070\u1075-\u1081\u108e\u1090-\u1099\u10a0-\u10c5\u10d0-\u10fa\u10fc\u1100-\u1159\u115f-\u11a2\u11a8-\u11f9\u1200-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u1676\u1681-\u169a\u16a0-\u16ea\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u17e0-\u17e9\u1810-\u1819\u1820-\u1877\u1880-\u18a8\u18aa\u1900-\u191c\u1946-\u196d\u1970-\u1974\u1980-\u19a9\u19c1-\u19c7\u19d0-\u19d9\u1a00-\u1a16\u1b05-\u1b33\u1b45-\u1b4b\u1b50-\u1b59\u1b83-\u1ba0\u1bae-\u1bb9\u1c00-\u1c23\u1c40-\u1c49\u1c4d-\u1c7d\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u203f-\u2040\u2054\u2071\u207f\u2090-\u2094\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2183-\u2184\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2c6f\u2c71-\u2c7d\u2c80-\u2ce4\u2d00-\u2d25\u2d30-\u2d65\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3006\u3031-\u3035\u303b-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31b7\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fc3\ua000-\ua48c\ua500-\ua60c\ua610-\ua62b\ua640-\ua65f\ua662-\ua66e\ua67f-\ua697\ua717-\ua71f\ua722-\ua788\ua78b-\ua78c\ua7fb-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8d0-\ua8d9\ua900-\ua925\ua930-\ua946\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa50-\uaa59\uac00-\ud7a3\uf900-\ufa2d\ufa30-\ufa6a\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe33-\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]/;
|
||||
|
||||
|
||||
var unhoist = function(obj,from,to){
|
||||
var unhoisted = 0;
|
||||
var regex = new RegExp(from, "g");
|
||||
|
||||
if(_.isArray(obj)){
|
||||
for(var i=0; i<obj.length; i++){
|
||||
var item = obj[i];
|
||||
|
||||
if(_.isString(item)){
|
||||
// Odd, but we need +1 for the / in front of /*
|
||||
var matches = item.match(regex);
|
||||
unhoisted -= matches ? matches.length : 0;
|
||||
|
||||
obj[i] = item.replace(regex, to);
|
||||
unhoisted += item.length - obj[i].length;
|
||||
}
|
||||
if(_.isArray(item)){
|
||||
unhoisted += unhoist(item, from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
return unhoisted;
|
||||
};
|
||||
|
||||
var replaceMarkdown = function(match, tag) {
|
||||
var hash = Discourse.Dialect.guid();
|
||||
|
||||
Discourse.Dialect.registerInline(match, function(text, matched, prev){
|
||||
if(!text || text.length < match.length + 1) {
|
||||
return;
|
||||
}
|
||||
var lastText = prev[prev.length-1];
|
||||
lastText = typeof lastText === "string" && lastText;
|
||||
lastText = lastText && lastText[lastText.length-1];
|
||||
if(lastText && (lastText === "/" || lastText.match(aLetter))){
|
||||
return;
|
||||
}
|
||||
|
||||
if(text[match.length].match(/\s/)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// hoist out escaped \*
|
||||
text = text.replace(new RegExp("\\\\\\" + match[0], "g"), hash);
|
||||
|
||||
var endText = new RegExp("[^\\s|" + match[0] + "]" + match.replace(/\*/g,"\\*") + "([^" + match[0] + "]|$)");
|
||||
|
||||
var finish = text.split("\n")[0].search(endText);
|
||||
if(finish && finish >= 0) {
|
||||
var newText = text.substring(match.length, finish+1);
|
||||
newText = this.processInline(newText);
|
||||
|
||||
var unhoisted_length = unhoist(newText,hash,match[0]);
|
||||
|
||||
var array = typeof tag === "string" ? [tag].concat(newText) : [tag[0], [tag[1]].concat(newText)];
|
||||
|
||||
return [(finish + match.length + 1) - unhoisted_length, array];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
replaceMarkdown('***', ['strong','em']);
|
||||
replaceMarkdown('___', ['strong','em']);
|
||||
replaceMarkdown('**', 'strong');
|
||||
replaceMarkdown('__', 'strong');
|
||||
replaceMarkdown('*', 'em');
|
||||
replaceMarkdown('_', 'em');
|
||||
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
/**
|
||||
Supports Discourse's category hashtags (#category-slug) for automatically
|
||||
generating a link to the category.
|
||||
**/
|
||||
Discourse.Dialect.inlineRegexp({
|
||||
start: '#',
|
||||
matcher: /^#([\w-:]{1,101})/i,
|
||||
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) {
|
||||
return ['a', { class: attributeClass, href: result[0] }, '#', ["span", {}, result[1]]];
|
||||
} else {
|
||||
return ['span', { class: attributeClass }, hashtag];
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
Discourse.Dialect.addPreProcessor(function(text) {
|
||||
return Discourse.CensoredWords.censor(text);
|
||||
});
|
|
@ -1,81 +0,0 @@
|
|||
/**
|
||||
Support for various code blocks
|
||||
**/
|
||||
|
||||
var acceptableCodeClasses;
|
||||
|
||||
function init() {
|
||||
acceptableCodeClasses = Discourse.SiteSettings.highlighted_languages.split("|");
|
||||
if (Discourse.SiteSettings.highlighted_languages.length > 0) {
|
||||
var regexpSource = "^lang-(" + "nohighlight|auto|" + Discourse.SiteSettings.highlighted_languages + ")$";
|
||||
Discourse.Markdown.whiteListTag('code', 'class', new RegExp(regexpSource, "i"));
|
||||
}
|
||||
}
|
||||
|
||||
if (Discourse.SiteSettings) {
|
||||
init();
|
||||
} else {
|
||||
Discourse.initializer({initialize: init, name: 'load-acceptable-code-classes'});
|
||||
}
|
||||
|
||||
|
||||
var textCodeClasses = ["text", "pre", "plain"];
|
||||
|
||||
function codeFlattenBlocks(blocks) {
|
||||
var result = "";
|
||||
blocks.forEach(function(b) {
|
||||
result += b;
|
||||
if (b.trailing) { result += b.trailing; }
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
Discourse.Dialect.replaceBlock({
|
||||
start: /^`{3}([^\n\[\]]+)?\n?([\s\S]*)?/gm,
|
||||
stop: /^```$/gm,
|
||||
withoutLeading: /\[quote/gm, //if leading text contains a quote this should not match
|
||||
emitter: function(blockContents, matches) {
|
||||
|
||||
var klass = Discourse.SiteSettings.default_code_lang;
|
||||
|
||||
if (acceptableCodeClasses && matches[1] && acceptableCodeClasses.indexOf(matches[1]) !== -1) {
|
||||
klass = matches[1];
|
||||
}
|
||||
|
||||
if (textCodeClasses.indexOf(matches[1]) !== -1) {
|
||||
return ['p', ['pre', ['code', {'class': 'lang-nohighlight'}, codeFlattenBlocks(blockContents) ]]];
|
||||
} else {
|
||||
return ['p', ['pre', ['code', {'class': 'lang-' + klass}, codeFlattenBlocks(blockContents) ]]];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Discourse.Dialect.replaceBlock({
|
||||
start: /(<pre[^\>]*\>)([\s\S]*)/igm,
|
||||
stop: /<\/pre>/igm,
|
||||
rawContents: true,
|
||||
skipIfTradtionalLinebreaks: true,
|
||||
|
||||
emitter: function(blockContents) {
|
||||
return ['p', ['pre', codeFlattenBlocks(blockContents)]];
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure that content in a code block is fully escaped. This way it's not white listed
|
||||
// and we can use HTML and Javascript examples.
|
||||
Discourse.Dialect.on('parseNode', function (event) {
|
||||
var node = event.node,
|
||||
path = event.path;
|
||||
|
||||
if (node[0] === 'code') {
|
||||
var contents = node[node.length-1],
|
||||
regexp;
|
||||
|
||||
if (path && path[path.length-1] && path[path.length-1][0] && path[path.length-1][0] === "pre") {
|
||||
regexp = / +$/g;
|
||||
} else {
|
||||
regexp = /^ +| +$/g;
|
||||
}
|
||||
node[node.length-1] = Discourse.Utilities.escapeExpression(contents.replace(regexp,''));
|
||||
}
|
||||
});
|
|
@ -1,51 +0,0 @@
|
|||
/**
|
||||
If a row begins with HTML tags, don't parse it.
|
||||
**/
|
||||
var blockTags = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'div',
|
||||
'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
|
||||
'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'iframe', 'noscript', 'ol', 'output',
|
||||
'p', 'pre', 'section', 'table', 'tfoot', 'ul', 'video'],
|
||||
|
||||
splitAtLast = function(tag, block, next, first) {
|
||||
var endTag = "</" + tag + ">",
|
||||
endTagIndex = first ? block.indexOf(endTag) : block.lastIndexOf(endTag);
|
||||
|
||||
if (endTagIndex !== -1) {
|
||||
endTagIndex += endTag.length;
|
||||
|
||||
var leading = block.substr(0, endTagIndex),
|
||||
trailing = block.substr(endTagIndex).replace(/^\s+/, '');
|
||||
|
||||
if (trailing.length) {
|
||||
next.unshift(trailing);
|
||||
}
|
||||
|
||||
return [ leading ];
|
||||
}
|
||||
};
|
||||
|
||||
Discourse.Dialect.registerBlock('html', function(block, next) {
|
||||
var split, pos;
|
||||
|
||||
// Fix manual blockquote paragraphing even though it's not strictly correct
|
||||
// PERF NOTE: /\S+<blockquote/ is a perf hog for search, try on huge string
|
||||
if (pos = block.search(/<blockquote/) >= 0) {
|
||||
if(block.substring(0, pos).search(/\s/) === -1) {
|
||||
split = splitAtLast('blockquote', block, next, true);
|
||||
if (split) { return this.processInline(split[0]); }
|
||||
}
|
||||
}
|
||||
|
||||
var m = /^<([^>]+)\>/.exec(block);
|
||||
if (m && m[1]) {
|
||||
var tag = m[1].split(/\s/);
|
||||
if (tag && tag[0] && blockTags.indexOf(tag[0]) !== -1) {
|
||||
split = splitAtLast(tag[0], block, next);
|
||||
if (split) {
|
||||
if (split.length === 1 && split[0] === block) { return; }
|
||||
return split;
|
||||
}
|
||||
return [ block.toString() ];
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,45 +0,0 @@
|
|||
/**
|
||||
Supports Discourse's custom @mention syntax for calling out a user in a post.
|
||||
It will add a special class to them, and create a link if the user is found in a
|
||||
local map.
|
||||
**/
|
||||
Discourse.Dialect.inlineRegexp({
|
||||
start: '@',
|
||||
// NOTE: since we can't use SiteSettings here (they loads later in process)
|
||||
// we are being less strict to account for more cases than allowed
|
||||
matcher: /^@(\w[\w.-]{0,59})\b/i,
|
||||
wordBoundary: true,
|
||||
|
||||
emitter: function(matches) {
|
||||
var mention = matches[0].trim(),
|
||||
name = matches[1],
|
||||
mentionLookup = this.dialect.options.mentionLookup;
|
||||
|
||||
var type = mentionLookup && mentionLookup(name);
|
||||
if (type === "user") {
|
||||
return ['a', {'class': 'mention', href: Discourse.getURL("/users/") + name.toLowerCase()}, mention];
|
||||
} else if (type === "group") {
|
||||
return ['a', {'class': 'mention-group', href: Discourse.getURL("/groups/") + name}, mention];
|
||||
} else {
|
||||
return ['span', {'class': 'mention'}, mention];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// We have to prune @mentions that are within links.
|
||||
Discourse.Dialect.on("parseNode", function(event) {
|
||||
var node = event.node,
|
||||
path = event.path;
|
||||
|
||||
if (node[1] && node[1]["class"] === 'mention') {
|
||||
var parent = path[path.length - 1];
|
||||
// If the parent is an 'a', remove it
|
||||
if (parent && parent[0] === 'a') {
|
||||
var name = node[2];
|
||||
node.length = 0;
|
||||
node[0] = "__RAW";
|
||||
node[1] = name;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -1,21 +0,0 @@
|
|||
Discourse.Dialect.on('parseNode', function (event) {
|
||||
var node = event.node,
|
||||
path = event.path;
|
||||
|
||||
if (node[0] === 'a') {
|
||||
|
||||
// It's invalid HTML to nest a link within another so strip it out.
|
||||
for (var i=0; i<path.length; i++) {
|
||||
if (path[i][0] === 'a') {
|
||||
var parent = path[path.length - 1],
|
||||
pos = parent.indexOf(node);
|
||||
|
||||
// Just leave the link text
|
||||
if (pos !== -1) {
|
||||
parent[pos] = node[2];
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,32 +0,0 @@
|
|||
/**
|
||||
Support for the newline behavior in markdown that most expect. Look through all text nodes
|
||||
in the tree, replace any new lines with `br`s.
|
||||
**/
|
||||
Discourse.Dialect.postProcessText(function (text, event) {
|
||||
var opts = event.dialect.options,
|
||||
insideCounts = event.insideCounts,
|
||||
linebreaks = opts.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks;
|
||||
|
||||
if (linebreaks || (insideCounts.pre > 0)) { return; }
|
||||
|
||||
if (text === "\n") {
|
||||
// If the tag is just a new line, replace it with a `<br>`
|
||||
return [['br']];
|
||||
} else {
|
||||
|
||||
|
||||
// If the text node contains new lines, perhaps with text between them, insert the
|
||||
// `<br>` tags.
|
||||
var split = text.split(/\n+/);
|
||||
if (split.length) {
|
||||
var replacement = [];
|
||||
for (var i=0; i<split.length; i++) {
|
||||
if (split[i].length > 0) { replacement.push(split[i]); }
|
||||
if (i !== split.length-1) { replacement.push(['br']); }
|
||||
}
|
||||
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -1,79 +0,0 @@
|
|||
/**
|
||||
Given a node in the document and its parent, determine whether it is on its
|
||||
own line or not.
|
||||
|
||||
@method isOnOneLine
|
||||
@namespace Discourse.Dialect
|
||||
**/
|
||||
var isOnOneLine = function(link, parent) {
|
||||
if (!parent) { return false; }
|
||||
|
||||
var siblings = parent.slice(1);
|
||||
if ((!siblings) || (siblings.length < 1)) { return false; }
|
||||
|
||||
var idx = siblings.indexOf(link);
|
||||
if (idx === -1) { return false; }
|
||||
|
||||
if (idx > 0) {
|
||||
var prev = siblings[idx-1];
|
||||
if (prev[0] !== 'br') { return false; }
|
||||
}
|
||||
|
||||
if (idx < siblings.length) {
|
||||
var next = siblings[idx+1];
|
||||
if (next && (!((next[0] === 'br') || (typeof next === 'string' && next.trim() === "")))) { return false; }
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
We only onebox stuff that is on its own line. This navigates the JsonML tree and
|
||||
correctly inserts the oneboxes.
|
||||
|
||||
@event parseNode
|
||||
@namespace Discourse.Dialect
|
||||
**/
|
||||
Discourse.Dialect.on("parseNode", function(event) {
|
||||
var node = event.node,
|
||||
path = event.path;
|
||||
|
||||
// We only care about links
|
||||
if (node[0] !== 'a') { return; }
|
||||
|
||||
var parent = path[path.length - 1];
|
||||
|
||||
// We don't onebox bbcode
|
||||
if (node[1]['data-bbcode']) {
|
||||
delete node[1]['data-bbcode'];
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't onebox mentions
|
||||
if (node[1]['class'] === 'mention') { return; }
|
||||
|
||||
// Don't onebox links within a list
|
||||
for (var i=0; i<path.length; i++) {
|
||||
if (path[i][0] === 'li') { return; }
|
||||
}
|
||||
|
||||
// If the link has a different label text than the link itself, don't onebox it.
|
||||
var label = node[node.length-1];
|
||||
if (label !== node[1]['href']) { return; }
|
||||
|
||||
if (isOnOneLine(node, parent)) {
|
||||
|
||||
node[1]['class'] = 'onebox';
|
||||
node[1].target = '_blank';
|
||||
|
||||
if (Discourse && Discourse.Onebox) {
|
||||
var contents = Discourse.Onebox.lookupCache(node[1].href);
|
||||
if (contents) {
|
||||
node[0] = '__RAW';
|
||||
node[1] = contents;
|
||||
node.length = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
var esc = Handlebars.Utils.escapeExpression;
|
||||
|
||||
Discourse.BBCode.register('quote', {noWrap: true, singlePara: true}, function(contents, bbParams, options) {
|
||||
var params = {'class': 'quote'},
|
||||
username = null;
|
||||
|
||||
if (bbParams) {
|
||||
var paramsSplit = bbParams.split(/\,\s*/);
|
||||
username = paramsSplit[0];
|
||||
|
||||
paramsSplit.forEach(function(p,i) {
|
||||
if (i > 0) {
|
||||
var assignment = p.split(':');
|
||||
if (assignment[0] && assignment[1]) {
|
||||
var escaped = esc(assignment[0]);
|
||||
// don't escape attributes, makes no sense
|
||||
if(escaped === assignment[0]) {
|
||||
params['data-' + assignment[0]] = esc(assignment[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var avatarImg;
|
||||
var postNumber = parseInt(params['data-post'], 10);
|
||||
var topicId = parseInt(params['data-topic'], 10);
|
||||
|
||||
if (options.lookupAvatarByPostNumber) {
|
||||
// client-side, we can retrieve the avatar from the post
|
||||
avatarImg = options.lookupAvatarByPostNumber(postNumber, topicId);
|
||||
} else if (options.lookupAvatar) {
|
||||
// server-side, we need to lookup the avatar from the username
|
||||
avatarImg = options.lookupAvatar(username);
|
||||
}
|
||||
|
||||
// If there's no username just return a simple quote
|
||||
if (!username) {
|
||||
return ['p', ['aside', params, ['blockquote'].concat(contents)]];
|
||||
}
|
||||
|
||||
var header = [ 'div', {'class': 'title'},
|
||||
['div', {'class': 'quote-controls'}],
|
||||
avatarImg ? ['__RAW', avatarImg] : "",
|
||||
username ? I18n.t('user.said', {username: username}) : ""
|
||||
];
|
||||
|
||||
if (options.topicId && postNumber && options.getTopicInfo && topicId !== options.topicId) {
|
||||
var topicInfo = options.getTopicInfo(topicId);
|
||||
if (topicInfo) {
|
||||
var href = topicInfo.href;
|
||||
if (postNumber > 0) { href += "/" + postNumber; }
|
||||
// get rid of username said stuff
|
||||
header.pop();
|
||||
header.push(['a', {'href': href}, topicInfo.title]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return ['aside', params, header, ['blockquote'].concat(contents)];
|
||||
});
|
|
@ -1,51 +0,0 @@
|
|||
var tableFlattenBlocks = function(blocks) {
|
||||
var result = "";
|
||||
blocks.forEach(function(b) {
|
||||
result += b;
|
||||
if (b.trailing) { result += b.trailing; }
|
||||
});
|
||||
|
||||
// bypass newline insertion
|
||||
return result.replace(/[\n\r]/g, " ");
|
||||
};
|
||||
|
||||
var emitter = function(contents) {
|
||||
// TODO event should be fired when sanitizer loads
|
||||
if (window.html4 && window.html4.ELEMENTS.td !== 1) {
|
||||
window.html4.ELEMENTS.table = 0;
|
||||
window.html4.ELEMENTS.tbody = 1;
|
||||
window.html4.ELEMENTS.td = 1;
|
||||
window.html4.ELEMENTS.thead = 1;
|
||||
window.html4.ELEMENTS.th = 1;
|
||||
window.html4.ELEMENTS.tr = 1;
|
||||
}
|
||||
return ['table', {"class": "md-table"}, tableFlattenBlocks.apply(this, [contents])];
|
||||
};
|
||||
|
||||
var tableBlock = {
|
||||
start: /(<table[^>]*>)([\S\s]*)/igm,
|
||||
stop: /<\/table>/igm,
|
||||
rawContents: true,
|
||||
emitter: emitter,
|
||||
priority: 1
|
||||
};
|
||||
|
||||
var init = function(){
|
||||
if (Discourse.SiteSettings.allow_html_tables) {
|
||||
Discourse.Markdown.whiteListTag("table");
|
||||
Discourse.Markdown.whiteListTag("table", "class", "md-table");
|
||||
Discourse.Markdown.whiteListTag("tbody");
|
||||
Discourse.Markdown.whiteListTag("thead");
|
||||
Discourse.Markdown.whiteListTag("tr");
|
||||
Discourse.Markdown.whiteListTag("th");
|
||||
Discourse.Markdown.whiteListTag("td");
|
||||
Discourse.Dialect.replaceBlock(tableBlock);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
if (Discourse.SiteSettings) {
|
||||
init();
|
||||
} else {
|
||||
Discourse.initializer({initialize: init, name: 'enable-html-tables'});
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { htmlHelper } from 'discourse/lib/helpers';
|
||||
import { avatarImg } from 'discourse/lib/utilities';
|
||||
|
||||
export default htmlHelper((avatarTemplate, size) => {
|
||||
return Discourse.Utilities.avatarImg({ size, avatarTemplate });
|
||||
});
|
||||
export default htmlHelper((avatarTemplate, size) => avatarImg({ size, avatarTemplate }));
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { htmlHelper } from 'discourse/lib/helpers';
|
||||
import { avatarImg } from 'discourse/lib/utilities';
|
||||
|
||||
export default htmlHelper((user, size) => {
|
||||
if (Ember.isEmpty(user)) {
|
||||
|
@ -6,5 +7,5 @@ export default htmlHelper((user, size) => {
|
|||
}
|
||||
|
||||
const avatarTemplate = Em.get(user, 'avatar_template');
|
||||
return Discourse.Utilities.avatarImg({ size, avatarTemplate });
|
||||
return avatarImg({ size, avatarTemplate });
|
||||
});
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { cook } from 'discourse/lib/text';
|
||||
import { registerUnbound } from 'discourse/lib/helpers';
|
||||
|
||||
registerUnbound('cook-text', function(text) {
|
||||
return new Handlebars.SafeString(Discourse.Markdown.cook(text, {sanitize: true}));
|
||||
});
|
||||
|
||||
registerUnbound('cook-text', cook);
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import { registerUnbound } from 'discourse/lib/helpers';
|
||||
import { emojiUnescape } from 'discourse/lib/text';
|
||||
|
||||
registerUnbound('i18n', function(key, params) {
|
||||
return I18n.t(key, params);
|
||||
});
|
||||
|
||||
registerUnbound('replace-emoji', function(text) {
|
||||
return new Handlebars.SafeString(Discourse.Emoji.unescape(text));
|
||||
});
|
||||
registerUnbound('i18n', (key, params) => I18n.t(key, params));
|
||||
registerUnbound('replace-emoji', text => new Handlebars.SafeString(emojiUnescape(text)));
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { registerUnbound } from 'discourse/lib/helpers';
|
||||
import { avatarImg } from 'discourse/lib/utilities';
|
||||
|
||||
function renderAvatar(user, options) {
|
||||
options = options || {};
|
||||
|
@ -26,7 +27,7 @@ function renderAvatar(user, options) {
|
|||
}
|
||||
}
|
||||
|
||||
return Discourse.Utilities.avatarImg({
|
||||
return avatarImg({
|
||||
size: options.imageSize,
|
||||
extraClasses: Em.get(user, 'extras') || options.extraClasses,
|
||||
title: title || username,
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||
import { htmlHelper } from 'discourse/lib/helpers';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
|
||||
export default htmlHelper((user, args) => {
|
||||
if (!user) { return; }
|
||||
|
||||
const name = Discourse.Utilities.escapeExpression(user.get('name'));
|
||||
const name = escapeExpression(user.get('name'));
|
||||
const currentUser = args.hash.currentUser;
|
||||
|
||||
if (currentUser && user.get('admin') && currentUser.get('staff')) {
|
||||
|
|
|
@ -1,26 +1,25 @@
|
|||
import { withPluginApi } from 'discourse/lib/plugin-api';
|
||||
import { registerEmoji } from 'pretty-text/emoji';
|
||||
|
||||
export default {
|
||||
name: 'enable-emoji',
|
||||
|
||||
initialize(container) {
|
||||
const siteSettings = container.lookup('site-settings:main');
|
||||
if (!siteSettings.enable_emoji) { return; }
|
||||
|
||||
if (siteSettings.enable_emoji) {
|
||||
withPluginApi('0.1', api => {
|
||||
api.onToolbarCreate(toolbar => {
|
||||
toolbar.addButton({
|
||||
id: 'emoji',
|
||||
group: 'extras',
|
||||
icon: 'smile-o',
|
||||
action: 'emoji',
|
||||
title: 'composer.emoji'
|
||||
});
|
||||
withPluginApi('0.1', api => {
|
||||
api.onToolbarCreate(toolbar => {
|
||||
toolbar.addButton({
|
||||
id: 'emoji',
|
||||
group: 'extras',
|
||||
icon: 'smile-o',
|
||||
action: 'emoji',
|
||||
title: 'composer.emoji'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// enable plugin emojis
|
||||
Discourse.Emoji.applyCustomEmojis();
|
||||
}
|
||||
(PreloadStore.get("customEmoji") || []).forEach(emoji => registerEmoji(emoji.name, emoji.url));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
|
||||
@module $.fn.autocomplete
|
||||
**/
|
||||
export var CANCELLED_STATUS = "__CANCELLED";
|
||||
export const CANCELLED_STATUS = "__CANCELLED";
|
||||
import { setCaretPosition, caretPosition } from 'discourse/lib/utilities';
|
||||
|
||||
const allowedLettersRegex = /[\s\t\[\{\(\/]/;
|
||||
|
||||
var keys = {
|
||||
const keys = {
|
||||
backSpace: 8,
|
||||
tab: 9,
|
||||
enter: 13,
|
||||
|
@ -37,7 +38,7 @@ var keys = {
|
|||
let inputTimeout;
|
||||
|
||||
export default function(options) {
|
||||
var autocompletePlugin = this;
|
||||
const autocompletePlugin = this;
|
||||
|
||||
if (this.length === 0) return;
|
||||
|
||||
|
@ -67,21 +68,21 @@ export default function(options) {
|
|||
return this;
|
||||
}
|
||||
|
||||
var disabled = options && options.disabled;
|
||||
var wrap = null;
|
||||
var autocompleteOptions = null;
|
||||
var selectedOption = null;
|
||||
var completeStart = null;
|
||||
var completeEnd = null;
|
||||
var me = this;
|
||||
var div = null;
|
||||
var prevTerm = null;
|
||||
const disabled = options && options.disabled;
|
||||
let wrap = null;
|
||||
let autocompleteOptions = null;
|
||||
let selectedOption = null;
|
||||
let completeStart = null;
|
||||
let completeEnd = null;
|
||||
let me = this;
|
||||
let div = null;
|
||||
let prevTerm = null;
|
||||
|
||||
// input is handled differently
|
||||
var isInput = this[0].tagName === "INPUT";
|
||||
var inputSelectedItems = [];
|
||||
const isInput = this[0].tagName === "INPUT";
|
||||
let inputSelectedItems = [];
|
||||
|
||||
var closeAutocomplete = function() {
|
||||
function closeAutocomplete() {
|
||||
if (div) {
|
||||
div.hide().remove();
|
||||
}
|
||||
|
@ -89,9 +90,9 @@ export default function(options) {
|
|||
completeStart = null;
|
||||
autocompleteOptions = null;
|
||||
prevTerm = null;
|
||||
};
|
||||
}
|
||||
|
||||
var addInputSelectedItem = function(item) {
|
||||
function addInputSelectedItem(item) {
|
||||
var transformed,
|
||||
transformedItem = item;
|
||||
|
||||
|
@ -100,7 +101,7 @@ export default function(options) {
|
|||
if (options.single) { inputSelectedItems = []; }
|
||||
transformed = _.isArray(transformedItem) ? transformedItem : [transformedItem || item];
|
||||
|
||||
var divs = transformed.map(function(itm) {
|
||||
const divs = transformed.map(itm => {
|
||||
let d = $(`<div class='item'><span>${itm}<a class='remove' href><i class='fa fa-times'></i></a></span></div>`);
|
||||
const $parent = me.parent();
|
||||
const prev = $parent.find('.item:last');
|
||||
|
@ -148,7 +149,7 @@ export default function(options) {
|
|||
var text = me.val();
|
||||
text = text.substring(0, completeStart) + (options.key || "") + term + ' ' + text.substring(completeEnd + 1, text.length);
|
||||
me.val(text);
|
||||
Discourse.Utilities.setCaretPosition(me[0], completeStart + 1 + term.length);
|
||||
setCaretPosition(me[0], completeStart + 1 + term.length);
|
||||
|
||||
if (options && options.afterComplete) {
|
||||
options.afterComplete(text);
|
||||
|
@ -160,7 +161,7 @@ export default function(options) {
|
|||
};
|
||||
|
||||
if (isInput) {
|
||||
var width = this.width();
|
||||
const width = this.width();
|
||||
wrap = this.wrap("<div class='ac-wrap clearfix" + (disabled ? " disabled": "") + "'/>").parent();
|
||||
wrap.width(width);
|
||||
if(options.single) {
|
||||
|
@ -191,13 +192,13 @@ export default function(options) {
|
|||
});
|
||||
}
|
||||
|
||||
var markSelected = function() {
|
||||
var links = div.find('li a');
|
||||
function markSelected() {
|
||||
const links = div.find('li a');
|
||||
links.removeClass('selected');
|
||||
return $(links[selectedOption]).addClass('selected');
|
||||
};
|
||||
|
||||
var renderAutocomplete = function() {
|
||||
function renderAutocomplete() {
|
||||
if (div) {
|
||||
div.hide().remove();
|
||||
}
|
||||
|
@ -258,7 +259,7 @@ export default function(options) {
|
|||
|
||||
const SKIP = "skip";
|
||||
|
||||
const dataSource = (term, opts) => {
|
||||
function dataSource(term, opts) {
|
||||
if (prevTerm === term) {
|
||||
return SKIP;
|
||||
}
|
||||
|
@ -272,7 +273,7 @@ export default function(options) {
|
|||
}
|
||||
};
|
||||
|
||||
const updateAutoComplete = function(r) {
|
||||
function updateAutoComplete(r) {
|
||||
|
||||
if (completeStart === null || r === SKIP) return;
|
||||
|
||||
|
@ -299,7 +300,7 @@ export default function(options) {
|
|||
};
|
||||
|
||||
// chain to allow multiples
|
||||
var oldClose = me.data("closeAutocomplete");
|
||||
const oldClose = me.data("closeAutocomplete");
|
||||
me.data("closeAutocomplete", function() {
|
||||
if (oldClose) {
|
||||
oldClose();
|
||||
|
@ -307,9 +308,7 @@ export default function(options) {
|
|||
closeAutocomplete();
|
||||
});
|
||||
|
||||
$(this).on('click.autocomplete', function() {
|
||||
closeAutocomplete();
|
||||
});
|
||||
$(this).on('click.autocomplete', () => closeAutocomplete());
|
||||
|
||||
$(this).on('paste.autocomplete', function() {
|
||||
_.delay(function(){
|
||||
|
@ -317,42 +316,39 @@ export default function(options) {
|
|||
}, 50);
|
||||
});
|
||||
|
||||
const checkTriggerRule = (opts) => {
|
||||
if (options.triggerRule) {
|
||||
return options.triggerRule(me[0], opts);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
function checkTriggerRule(opts) {
|
||||
return options.triggerRule ? options.triggerRule(me[0], opts) : true;
|
||||
};
|
||||
|
||||
$(this).on('keyup.autocomplete', function(e) {
|
||||
if ([keys.esc, keys.enter].indexOf(e.which) !== -1) return true;
|
||||
|
||||
var caretPosition = Discourse.Utilities.caretPosition(me[0]);
|
||||
var cp = caretPosition(me[0]);
|
||||
|
||||
if (options.key && completeStart === null && caretPosition > 0) {
|
||||
var key = me[0].value[caretPosition-1];
|
||||
if (options.key && completeStart === null && cp > 0) {
|
||||
var key = me[0].value[cp-1];
|
||||
if (key === options.key) {
|
||||
var prevChar = me.val().charAt(caretPosition-2);
|
||||
var prevChar = me.val().charAt(cp-2);
|
||||
if (checkTriggerRule() && (!prevChar || allowedLettersRegex.test(prevChar))) {
|
||||
completeStart = completeEnd = caretPosition-1;
|
||||
completeStart = completeEnd = cp-1;
|
||||
updateAutoComplete(dataSource("", options));
|
||||
}
|
||||
}
|
||||
} else if (completeStart !== null) {
|
||||
var term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition);
|
||||
var term = me.val().substring(completeStart + (options.key ? 1 : 0), cp);
|
||||
updateAutoComplete(dataSource(term, options));
|
||||
}
|
||||
});
|
||||
|
||||
$(this).on('keydown.autocomplete', function(e) {
|
||||
var c, caretPosition, i, initial, prev, prevIsGood, stopFound, term, total, userToComplete;
|
||||
var c, i, initial, prev, prevIsGood, stopFound, term, total, userToComplete;
|
||||
let cp;
|
||||
|
||||
if(e.ctrlKey || e.altKey || e.metaKey){
|
||||
if (e.ctrlKey || e.altKey || e.metaKey){
|
||||
return true;
|
||||
}
|
||||
|
||||
if(options.allowAny){
|
||||
if (options.allowAny){
|
||||
// saves us wiring up a change event as well
|
||||
|
||||
Ember.run.cancel(inputTimeout);
|
||||
|
@ -377,7 +373,7 @@ export default function(options) {
|
|||
}
|
||||
if (e.which === keys.shift) return;
|
||||
if ((completeStart === null) && e.which === keys.backSpace && options.key) {
|
||||
c = Discourse.Utilities.caretPosition(me[0]);
|
||||
c = caretPosition(me[0]);
|
||||
c -= 1;
|
||||
initial = c;
|
||||
prevIsGood = true;
|
||||
|
@ -389,7 +385,7 @@ export default function(options) {
|
|||
prev = me[0].value[c - 1];
|
||||
if (checkTriggerRule({ backSpace: true }) && (!prev || allowedLettersRegex.test(prev))) {
|
||||
completeStart = c;
|
||||
caretPosition = completeEnd = initial;
|
||||
cp = completeEnd = initial;
|
||||
term = me[0].value.substring(c + 1, initial);
|
||||
updateAutoComplete(dataSource(term, options));
|
||||
return true;
|
||||
|
@ -409,16 +405,16 @@ export default function(options) {
|
|||
}
|
||||
|
||||
if (completeStart !== null) {
|
||||
caretPosition = Discourse.Utilities.caretPosition(me[0]);
|
||||
cp = caretPosition(me[0]);
|
||||
|
||||
// allow people to right arrow out of completion
|
||||
if (e.which === keys.rightArrow && me[0].value[caretPosition] === ' ') {
|
||||
if (e.which === keys.rightArrow && me[0].value[cp] === ' ') {
|
||||
closeAutocomplete();
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we've backspaced past the beginning, cancel unless no key
|
||||
if (caretPosition <= completeStart && options.key) {
|
||||
if (cp <= completeStart && options.key) {
|
||||
closeAutocomplete();
|
||||
return true;
|
||||
}
|
||||
|
@ -455,10 +451,10 @@ export default function(options) {
|
|||
markSelected();
|
||||
return false;
|
||||
case keys.backSpace:
|
||||
completeEnd = caretPosition;
|
||||
caretPosition--;
|
||||
completeEnd = cp;
|
||||
cp--;
|
||||
|
||||
if (caretPosition < 0) {
|
||||
if (cp < 0) {
|
||||
closeAutocomplete();
|
||||
if (isInput) {
|
||||
i = wrap.find('a:last');
|
||||
|
@ -469,16 +465,16 @@ export default function(options) {
|
|||
return true;
|
||||
}
|
||||
|
||||
term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition);
|
||||
term = me.val().substring(completeStart + (options.key ? 1 : 0), cp);
|
||||
|
||||
if ((completeStart === caretPosition) && (term === options.key)) {
|
||||
if ((completeStart === cp) && (term === options.key)) {
|
||||
closeAutocomplete();
|
||||
}
|
||||
|
||||
updateAutoComplete(dataSource(term, options));
|
||||
return true;
|
||||
default:
|
||||
completeEnd = caretPosition;
|
||||
completeEnd = cp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
export const SEPARATOR = ":";
|
||||
import { caretRowCol } from 'discourse/lib/utilities';
|
||||
|
||||
export function replaceSpan($elem, categorySlug, categoryLink) {
|
||||
$elem.replaceWith(`<a href="${categoryLink}" class="hashtag">#<span>${categorySlug}</span></a>`);
|
||||
};
|
||||
|
||||
export function categoryHashtagTriggerRule(textarea, opts) {
|
||||
const result = Discourse.Utilities.caretRowCol(textarea);
|
||||
const result = caretRowCol(textarea);
|
||||
const row = result.rowNum;
|
||||
var col = result.colNum;
|
||||
var line = textarea.value.split("\n")[row - 1];
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
Discourse.CensoredWords = {
|
||||
censor: function(text) {
|
||||
var censorRegexp,
|
||||
censored = Discourse.SiteSettings.censored_words;
|
||||
|
||||
if (censored && censored.length) {
|
||||
if (!censorRegexp) {
|
||||
var split = censored.split("|");
|
||||
if (split && split.length) {
|
||||
censorRegexp = new RegExp("(\\b(?:" + split.map(function (t) { return "(" + t.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + ")"; }).join("|") + ")\\b)(?![^\\(]*\\))", "ig");
|
||||
}
|
||||
}
|
||||
if (censorRegexp) {
|
||||
var m = censorRegexp.exec(text);
|
||||
while (m && m[0]) {
|
||||
var replacement = new Array(m[0].length+1).join('■');
|
||||
text = text.replace(new RegExp("(\\b" + m[0] + "\\b)(?![^\\(]*\\))", "ig"), replacement);
|
||||
m = censorRegexp.exec(text);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { wantsNewWindow } from 'discourse/lib/intercept-click';
|
||||
import { selectedText } from 'discourse/lib/utilities';
|
||||
|
||||
export function isValidLink($link) {
|
||||
return ($link.hasClass("track-link") ||
|
||||
|
@ -9,7 +10,7 @@ export function isValidLink($link) {
|
|||
export default {
|
||||
trackClick(e) {
|
||||
// cancel click if triggered as part of selection.
|
||||
if (Discourse.Utilities.selectedText() !== "") { return false; }
|
||||
if (selectedText() !== "") { return false; }
|
||||
|
||||
var $link = $(e.currentTarget);
|
||||
|
||||
|
|
|
@ -1,265 +0,0 @@
|
|||
// TODO @robin to move this whole thing to es6
|
||||
Discourse.Emoji = {};
|
||||
|
||||
// bump up this number to expire all emojis
|
||||
Discourse.Emoji.ImageVersion = "2"
|
||||
|
||||
var emoji = <%= Emoji.standard.map(&:name).flatten.inspect %>;
|
||||
var aliases = <%= Emoji.aliases.inspect.gsub("=>", ":") %>;
|
||||
|
||||
var extendedEmoji = {};
|
||||
if (Discourse.Dialect) {
|
||||
Discourse.Dialect.registerEmoji = function(code, url) {
|
||||
code = code.toLowerCase();
|
||||
extendedEmoji[code] = url;
|
||||
};
|
||||
}
|
||||
|
||||
var _unicodeReplacements;
|
||||
var _unicodeRegexp;
|
||||
Discourse.Dialect.setUnicodeReplacements = function(replacements) {
|
||||
_unicodeReplacements = replacements;
|
||||
if (replacements) {
|
||||
_unicodeRegexp = new RegExp(Object.keys(replacements).join("|"), "g");
|
||||
}
|
||||
}
|
||||
|
||||
// This method is used by PrettyText to reset custom emojis in multisites
|
||||
Discourse.Dialect.resetEmojis = function() {
|
||||
extendedEmoji = {};
|
||||
};
|
||||
|
||||
var customEmojiCallbacks = [];
|
||||
Discourse.Emoji.addCustomEmojis = function(cb) {
|
||||
customEmojiCallbacks.push(cb);
|
||||
};
|
||||
|
||||
Discourse.Emoji.applyCustomEmojis = function() {
|
||||
var self = this;
|
||||
_.each(customEmojiCallbacks, function(cb) { cb.apply(self); });
|
||||
};
|
||||
|
||||
Discourse.Emoji.list = function(){
|
||||
var list = emoji.slice(0);
|
||||
_.each(extendedEmoji, function(v,k){ list.push(k); });
|
||||
return list;
|
||||
};
|
||||
|
||||
|
||||
var emojiHash = {};
|
||||
// add all default emojis
|
||||
emoji.forEach(function(code){ emojiHash[code] = true; });
|
||||
// and their aliases
|
||||
|
||||
var aliasHash = {};
|
||||
for (var name in aliases) {
|
||||
aliases[name].forEach(function(alias) {
|
||||
aliasHash[alias] = name;
|
||||
});
|
||||
}
|
||||
|
||||
Discourse.Emoji.unescape = function(string) {
|
||||
//this can be further improved by supporting matches of emoticons that don't begin with a colon
|
||||
if (Discourse.SiteSettings.enable_emoji && string.indexOf(":") >= 0) {
|
||||
string = string.replace(/\B:[^\s:]+:?\B/g, function(m) {
|
||||
var isEmoticon = !!Discourse.Emoji.translations[m],
|
||||
emoji = isEmoticon ? Discourse.Emoji.translations[m] : m.slice(1, m.length - 1),
|
||||
hasEndingColon = m.lastIndexOf(":") === m.length - 1,
|
||||
url = Discourse.Emoji.urlFor(emoji);
|
||||
return url && (isEmoticon || hasEndingColon) ? "<img src='" + url + "' title='" + emoji + "' alt='" + emoji + "' class='emoji'>" : m;
|
||||
});
|
||||
}
|
||||
|
||||
return string;
|
||||
};
|
||||
|
||||
Discourse.Emoji.urlFor = urlFor = function(code) {
|
||||
var url, set = Discourse.SiteSettings.emoji_set;
|
||||
|
||||
code = code.toLowerCase();
|
||||
|
||||
if(extendedEmoji.hasOwnProperty(code)) {
|
||||
url = extendedEmoji[code];
|
||||
}
|
||||
|
||||
if(!url && emojiHash.hasOwnProperty(code)) {
|
||||
url = Discourse.getURL('/images/emoji/' + set + '/' + code + '.png');
|
||||
}
|
||||
|
||||
if(url && url[0] !== 'h' && Discourse.CDN) {
|
||||
url = Discourse.CDN + url;
|
||||
}
|
||||
|
||||
if(url){
|
||||
url = url + "?v=" + Discourse.Emoji.ImageVersion;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
Discourse.Emoji.exists = function(code){
|
||||
code = code.toLowerCase();
|
||||
return !!(extendedEmoji.hasOwnProperty(code) || emojiHash.hasOwnProperty(code));
|
||||
};
|
||||
|
||||
function imageFor(code) {
|
||||
code = code.toLowerCase();
|
||||
var url = urlFor(code);
|
||||
if (url) {
|
||||
var code = ':' + code + ':';
|
||||
return ['img', { href: url, title: code, 'class': 'emoji', alt: code }];
|
||||
}
|
||||
}
|
||||
|
||||
// Also support default emotions
|
||||
var translations = {
|
||||
':)' : 'slight_smile',
|
||||
':-)' : 'slight_smile',
|
||||
':(' : 'frowning',
|
||||
':-(' : 'frowning',
|
||||
';)' : 'wink',
|
||||
';-)' : 'wink',
|
||||
':\'(' : 'cry',
|
||||
':\'-(': 'cry',
|
||||
':-\'(': 'cry',
|
||||
':p' : 'stuck_out_tongue',
|
||||
':P' : 'stuck_out_tongue',
|
||||
':-P' : 'stuck_out_tongue',
|
||||
':O' : 'open_mouth',
|
||||
':-O' : 'open_mouth',
|
||||
':D' : 'smiley',
|
||||
':-D' : 'smiley',
|
||||
':|' : 'expressionless',
|
||||
':-|' : 'expressionless',
|
||||
':/' : 'confused',
|
||||
'8-)' : 'sunglasses',
|
||||
";P" : 'stuck_out_tongue_winking_eye',
|
||||
";-P" : 'stuck_out_tongue_winking_eye',
|
||||
":$" : 'blush',
|
||||
":-$" : 'blush'
|
||||
};
|
||||
|
||||
Discourse.Emoji.translations = translations;
|
||||
|
||||
function checkPrev(prev) {
|
||||
if (prev && prev.length) {
|
||||
var lastToken = prev[prev.length-1];
|
||||
if (lastToken && lastToken.charAt) {
|
||||
var lastChar = lastToken.charAt(lastToken.length-1);
|
||||
if (!/\W/.test(lastChar)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var translationsWithColon = {};
|
||||
Object.keys(translations).forEach(function (t) {
|
||||
if (t[0] === ':') {
|
||||
translationsWithColon[t] = translations[t];
|
||||
} else {
|
||||
var replacement = translations[t];
|
||||
Discourse.Dialect.inlineReplace(t, function (token, match, prev) {
|
||||
if (!Discourse.SiteSettings.enable_emoji) { return token; }
|
||||
return checkPrev(prev) ? imageFor(replacement) : token;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Discourse.Dialect.addPreProcessor(function(text) {
|
||||
if (_unicodeReplacements) {
|
||||
_unicodeRegexp.lastIndex = 0;
|
||||
|
||||
var m;
|
||||
while ((m = _unicodeRegexp.exec(text)) !== null) {
|
||||
|
||||
var replacement = ":" + _unicodeReplacements[m[0]] + ":";
|
||||
|
||||
var before = text.charAt(m.index-1);
|
||||
if (!/\B/.test(before)) {
|
||||
replacement = "\u200b" + replacement;
|
||||
}
|
||||
|
||||
text = text.replace(m[0], replacement);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
});
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return s.replace(/[-/\\^$*+?.()|[\]{}]/gi, '\\$&');
|
||||
}
|
||||
|
||||
var translationColonRegexp = new RegExp(Object.keys(translationsWithColon).map(function (t) {
|
||||
return "(" + escapeRegExp(t) + ")";
|
||||
}).join("|"));
|
||||
|
||||
Discourse.Dialect.registerInline(':', function(text, match, prev) {
|
||||
if (!Discourse.SiteSettings.enable_emoji) { return; }
|
||||
|
||||
var endPos = text.indexOf(':', 1),
|
||||
firstSpace = text.search(/\s/),
|
||||
contents;
|
||||
|
||||
if (!checkPrev(prev)) { return; }
|
||||
|
||||
// If there is no trailing colon, check our translations that begin with colons
|
||||
if (endPos === -1 || (firstSpace !== -1 && endPos > firstSpace)) {
|
||||
translationColonRegexp.lastIndex = 0;
|
||||
var m = translationColonRegexp.exec(text);
|
||||
if (m && m[0] && text.indexOf(m[0]) === 0) {
|
||||
// Check outer edge
|
||||
var lastChar = text.charAt(m[0].length);
|
||||
if (lastChar && !/\s/.test(lastChar)) return;
|
||||
contents = imageFor(translationsWithColon[m[0]]);
|
||||
if (contents) {
|
||||
return [m[0].length, contents];
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple find and replace from our array
|
||||
var between = text.slice(1, endPos);
|
||||
contents = imageFor(between);
|
||||
if (contents) {
|
||||
return [endPos+1, contents];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var toSearch;
|
||||
Discourse.Emoji.search = function(term, options) {
|
||||
var maxResults = (options && options["maxResults"]) || -1;
|
||||
if (maxResults === 0) { return []; }
|
||||
|
||||
toSearch = toSearch || _.union(_.keys(emojiHash), _.keys(extendedEmoji), _.keys(aliasHash)).sort();
|
||||
|
||||
var i, results = [];
|
||||
function addResult(term) {
|
||||
var val = aliasHash[term] || term;
|
||||
if (results.indexOf(val) === -1) {
|
||||
results.push(val);
|
||||
}
|
||||
return maxResults > 0 && results.length >= maxResults;
|
||||
}
|
||||
|
||||
var item;
|
||||
for (i=0; i<toSearch.length; i++) {
|
||||
item = toSearch[i];
|
||||
if (item.indexOf(term) === 0 && addResult(item)) {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
for (i=0; i<toSearch.length; i++) {
|
||||
item = toSearch[i];
|
||||
if (item.indexOf(term) > 0 && addResult(item)) {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
Discourse.Markdown.whiteListTag('img', 'class', 'emoji');
|
|
@ -1,3 +1,5 @@
|
|||
import { emojiExists } from 'pretty-text/emoji';
|
||||
|
||||
// note that these categories are copied from Slack
|
||||
// be careful, there are ~20 differences in synonyms, e.g. :boom: vs. :collision:
|
||||
// a few Emoji are actually missing from the Slack categories as well (?), and were added
|
||||
|
@ -1106,10 +1108,7 @@ const groups = [
|
|||
|
||||
// scrub groups
|
||||
groups.forEach(group => {
|
||||
group.icons = group.icons.reject(obj => !Discourse.Emoji.exists(obj));
|
||||
group.icons = group.icons.reject(obj => !emojiExists(obj));
|
||||
});
|
||||
|
||||
// export so others can modify
|
||||
Discourse.Emoji.groups = groups;
|
||||
|
||||
export default groups;
|
|
@ -1,5 +1,7 @@
|
|||
import groups from 'discourse/lib/emoji/emoji-groups';
|
||||
import groups from 'discourse/lib/emoji/groups';
|
||||
import KeyValueStore from "discourse/lib/key-value-store";
|
||||
import { emojiList } from 'pretty-text/emoji';
|
||||
import { emojiUrlFor } from 'discourse/lib/text';
|
||||
|
||||
const keyValueStore = new KeyValueStore("discourse_emojis_");
|
||||
const EMOJI_USAGE = "emojiUsage";
|
||||
|
@ -26,7 +28,7 @@ function initializeUngroupedIcons() {
|
|||
});
|
||||
|
||||
ungroupedIcons = [];
|
||||
const emojis = Discourse.Emoji.list();
|
||||
const emojis = emojiList();
|
||||
emojis.forEach(emoji => {
|
||||
if (groupedIcons[emoji] !== true) {
|
||||
ungroupedIcons.push(emoji);
|
||||
|
@ -90,7 +92,7 @@ function toolbar(selected) {
|
|||
title = "Custom";
|
||||
}
|
||||
|
||||
return { src: Discourse.Emoji.urlFor(icon),
|
||||
return { src: emojiUrlFor(icon),
|
||||
title,
|
||||
groupId: i,
|
||||
selected: i === selected };
|
||||
|
@ -106,7 +108,7 @@ function bindEvents(page, offset, options) {
|
|||
return false;
|
||||
}).hover(e => {
|
||||
const title = $(e.currentTarget).attr('title');
|
||||
const html = "<img src='" + Discourse.Emoji.urlFor(title) + "' class='emoji'> <span>:" + title + ":<span>";
|
||||
const html = "<img src='" + emojiUrlFor(title) + "' class='emoji'> <span>:" + title + ":<span>";
|
||||
$('.emoji-modal .info').html(html);
|
||||
}, () => $('.emoji-modal .info').html(""));
|
||||
|
||||
|
@ -136,7 +138,7 @@ function render(page, offset, options) {
|
|||
rows.push(row);
|
||||
row = [];
|
||||
}
|
||||
row.push({src: Discourse.Emoji.urlFor(icons[i]), title: icons[i]});
|
||||
row.push({src: emojiUrlFor(icons[i]), title: icons[i]});
|
||||
}
|
||||
rows.push(row);
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
/* global assetPath */
|
||||
|
||||
const _loaded = {};
|
||||
const _loading = {};
|
||||
|
||||
|
@ -26,7 +24,7 @@ export default function loadScript(url, opts) {
|
|||
opts = opts || {};
|
||||
|
||||
return new Ember.RSVP.Promise(function(resolve) {
|
||||
url = Discourse.getURL((assetPath && assetPath(url)) || url);
|
||||
url = Discourse.getURL(url);
|
||||
|
||||
// If we already loaded this url
|
||||
if (_loaded[url]) { return resolve(); }
|
||||
|
|
|
@ -1,268 +0,0 @@
|
|||
/**
|
||||
Contains methods to help us with markdown formatting.
|
||||
|
||||
@class Markdown
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
|
||||
/**
|
||||
* An object mapping from HTML tag names to an object mapping the valid
|
||||
* attributes on that tag to an array of permitted values.
|
||||
*
|
||||
* The permitted values can be strings or regexes.
|
||||
*
|
||||
* The pseduo-attribute 'data-*' can be used to validate any data-foo
|
||||
* attributes without any specified validations.
|
||||
*
|
||||
* Code can insert into this map by calling Discourse.Markdown.whiteListTag().
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* <pre><code>
|
||||
* {
|
||||
* a: {
|
||||
* href: ['*'],
|
||||
* data-mention-id: [/^\d+$/],
|
||||
* ...
|
||||
* },
|
||||
* code: {
|
||||
* class: ['ada', 'haskell', 'c', 'cpp', ... ]
|
||||
* },
|
||||
* ...
|
||||
* }
|
||||
* </code></pre>
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
var _validTags = {};
|
||||
/**
|
||||
* Classes valid on all elements. Map from class name to 'true'.
|
||||
* @private
|
||||
*/
|
||||
var _validClasses = {};
|
||||
var _validIframes = [];
|
||||
var _decoratedCaja = false;
|
||||
|
||||
function validateAttribute(tagName, attribName, value) {
|
||||
var tag = _validTags[tagName];
|
||||
|
||||
// Handle classes
|
||||
if (attribName === "class") {
|
||||
if (_validClasses[value]) { return value; }
|
||||
}
|
||||
|
||||
if (attribName.indexOf('data-') === 0) {
|
||||
// data-* catch-all validators
|
||||
if (tag && tag['data-*'] && !tag[attribName]) {
|
||||
var permitted = tag['data-*'];
|
||||
if (permitted && (
|
||||
permitted.indexOf(value) !== -1 ||
|
||||
permitted.indexOf('*') !== -1 ||
|
||||
((permitted instanceof RegExp) && permitted.test(value)))
|
||||
) { return value; }
|
||||
}
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
var attrs = tag[attribName];
|
||||
if (attrs && (attrs.indexOf(value) !== -1 ||
|
||||
attrs.indexOf('*') !== -1) ||
|
||||
_.any(attrs, function(r) { return (r instanceof RegExp) && r.test(value); })
|
||||
) { return value; }
|
||||
}
|
||||
|
||||
// return undefined;
|
||||
}
|
||||
|
||||
function anchorRegexp(regex) {
|
||||
if (/^\^.*\$$/.test(regex.source)) {
|
||||
return regex; // already anchored
|
||||
}
|
||||
|
||||
var flags = "";
|
||||
if (regex.global) {
|
||||
if (typeof console !== 'undefined') {
|
||||
console.warn("attribute validation regex should not be global");
|
||||
}
|
||||
}
|
||||
|
||||
if (regex.ignoreCase) { flags += "i"; }
|
||||
if (regex.multiline) { flags += "m"; }
|
||||
if (regex.sticky) { throw "Invalid attribute validation regex - cannot be sticky"; }
|
||||
|
||||
return new RegExp("^" + regex.source + "$", flags);
|
||||
}
|
||||
|
||||
Discourse.Markdown = {
|
||||
|
||||
/**
|
||||
Add to the attribute whitelist for a certain HTML tag.
|
||||
|
||||
@param {String} tagName tag to whitelist the attr for
|
||||
@param {String} attribName attr to whitelist for the tag
|
||||
@param {String | RegExp} [value] whitelisted value for the attribute
|
||||
**/
|
||||
whiteListTag: function(tagName, attribName, value) {
|
||||
if (value instanceof RegExp) {
|
||||
value = anchorRegexp(value);
|
||||
}
|
||||
_validTags[tagName] = _validTags[tagName] || {};
|
||||
_validTags[tagName][attribName] = _validTags[tagName][attribName] || [];
|
||||
_validTags[tagName][attribName].push(value || '*');
|
||||
},
|
||||
|
||||
/**
|
||||
Whitelists more classes for sanitization.
|
||||
|
||||
@param {...String} var_args Classes to whitelist
|
||||
@method whiteListClass
|
||||
**/
|
||||
whiteListClass: function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
args.forEach(function (a) { _validClasses[a] = true; });
|
||||
},
|
||||
|
||||
/**
|
||||
Whitelists iframes for sanitization
|
||||
|
||||
@method whiteListIframe
|
||||
@param {Regexp} regexp The regexp to whitelist.
|
||||
**/
|
||||
whiteListIframe: function(regexp) {
|
||||
_validIframes.push(regexp);
|
||||
},
|
||||
|
||||
/**
|
||||
Convert a raw string to a cooked markdown string.
|
||||
|
||||
@method cook
|
||||
@param {String} raw the raw string we want to apply markdown to
|
||||
@param {Object} opts the options for the rendering
|
||||
@return {String} the cooked markdown string
|
||||
**/
|
||||
cook: function(raw, opts) {
|
||||
if (!opts) opts = {};
|
||||
|
||||
// Make sure we've got a string
|
||||
if (!raw || raw.length === 0) return "";
|
||||
|
||||
return this.markdownConverter(opts).makeHtml(raw);
|
||||
},
|
||||
|
||||
/**
|
||||
Checks to see if a URL is allowed in the cooked content
|
||||
|
||||
@method urlAllowed
|
||||
@param {String} uri Url to check
|
||||
@param {Number} effect ignored
|
||||
@param {Number} ltype ignored
|
||||
@param {Object} hints an object with hints, used to check if this url is from an iframe
|
||||
@return {String} url to insert in the cooked content
|
||||
**/
|
||||
urlAllowed: function (uri, effect, ltype, hints) {
|
||||
var url = typeof(uri) === "string" ? uri : uri.toString();
|
||||
|
||||
// escape single quotes
|
||||
url = url.replace(/'/g, "%27");
|
||||
|
||||
// whitelist some iframe only
|
||||
if (hints && hints.XML_TAG === "iframe" && hints.XML_ATTR === "src") {
|
||||
for (var i = 0, length = _validIframes.length; i < length; i++) {
|
||||
if(_validIframes[i].test(url)) { return url; }
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// absolute urls
|
||||
if(/^(https?:)?\/\/[\w\.\-]+/i.test(url)) { return url; }
|
||||
// relative urls
|
||||
if(/^\/[\w\.\-]+/i.test(url)) { return url; }
|
||||
// anchors
|
||||
if(/^#[\w\.\-]+/i.test(url)) { return url; }
|
||||
// mailtos
|
||||
if(/^mailto:[\w\.\-@]+/i.test(url)) { return url; }
|
||||
},
|
||||
|
||||
/**
|
||||
Sanitize text using the sanitizer
|
||||
|
||||
@method sanitize
|
||||
@param {String} text The text to sanitize
|
||||
@return {String} text The sanitized text
|
||||
**/
|
||||
sanitize: function(text) {
|
||||
if (!window.html_sanitize || !text) return "";
|
||||
|
||||
// Allow things like <3 and <_<
|
||||
text = text.replace(/<([^A-Za-z\/\!]|$)/g, "<$1");
|
||||
|
||||
// The first time, let's add some more whitelisted tags
|
||||
if (!_decoratedCaja) {
|
||||
|
||||
// Add anything whitelisted to the list of elements if it's not in there already.
|
||||
var elements = window.html4.ELEMENTS;
|
||||
Object.keys(_validTags).forEach(function(t) {
|
||||
if (!elements[t]) {
|
||||
elements[t] = 0;
|
||||
}
|
||||
});
|
||||
|
||||
_decoratedCaja = true;
|
||||
}
|
||||
|
||||
return window.html_sanitize(text, Discourse.Markdown.urlAllowed, validateAttribute);
|
||||
},
|
||||
|
||||
/**
|
||||
Creates a Markdown.Converter that we we can use for formatting
|
||||
|
||||
@method markdownConverter
|
||||
@param {Object} opts the converting options
|
||||
**/
|
||||
markdownConverter: function(opts) {
|
||||
if (!opts) opts = {};
|
||||
|
||||
return {
|
||||
makeHtml: function(text) {
|
||||
text = Discourse.Dialect.cook(text, opts);
|
||||
return !text ? "" : text;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
RSVP.EventTarget.mixin(Discourse.Markdown);
|
||||
|
||||
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');
|
||||
Discourse.Markdown.whiteListTag('a', 'data-bbcode');
|
||||
Discourse.Markdown.whiteListTag('a', 'name');
|
||||
|
||||
Discourse.Markdown.whiteListTag('img', 'src', /^data:image.*$/i);
|
||||
|
||||
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-*');
|
||||
|
||||
Discourse.Markdown.whiteListTag('span', 'bbcode-b');
|
||||
Discourse.Markdown.whiteListTag('span', 'bbcode-i');
|
||||
Discourse.Markdown.whiteListTag('span', 'bbcode-u');
|
||||
Discourse.Markdown.whiteListTag('span', 'bbcode-s');
|
||||
|
||||
// used for pinned topics
|
||||
Discourse.Markdown.whiteListTag('span', 'class', 'excerpt');
|
||||
|
||||
Discourse.Markdown.whiteListIframe(/^(https?:)?\/\/www\.google\.com\/maps\/embed\?.+/i);
|
||||
Discourse.Markdown.whiteListIframe(/^(https?:)?\/\/www\.openstreetmap\.org\/export\/embed.html\?.+/i);
|
|
@ -1,106 +0,0 @@
|
|||
/**
|
||||
A helper for looking up oneboxes and displaying them
|
||||
|
||||
For now it only stores in a local Javascript Object, in future we can change it so it uses localStorage
|
||||
or some other mechanism.
|
||||
|
||||
@class Onebox
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.Onebox = {
|
||||
|
||||
// The cache is just a JS Object
|
||||
localCache: {},
|
||||
|
||||
// A cache of failed URLs
|
||||
failedCache: {},
|
||||
|
||||
/**
|
||||
Perform a lookup of a onebox based an anchor element. It will insert a loading
|
||||
indicator and remove it when the loading is complete or fails.
|
||||
|
||||
@method load
|
||||
@param {HTMLElement} e the anchor element whose onebox we want to look up
|
||||
@param {Boolean} refresh true if we want to force a refresh of the onebox
|
||||
**/
|
||||
load: function(e, refresh) {
|
||||
|
||||
var $elem = $(e);
|
||||
|
||||
// If the onebox has loaded, return
|
||||
if ($elem.data('onebox-loaded')) return;
|
||||
if ($elem.hasClass('loading-onebox')) return;
|
||||
|
||||
var url = e.href;
|
||||
|
||||
// Unless we're forcing a refresh...
|
||||
if (!refresh) {
|
||||
// If we have it in our cache, return it.
|
||||
var cached = this.localCache[url];
|
||||
if (cached) return cached;
|
||||
|
||||
// If the request failed, don't do anything
|
||||
var failed = this.failedCache[url];
|
||||
if (failed) return;
|
||||
}
|
||||
|
||||
// Add the loading CSS class
|
||||
$elem.addClass('loading-onebox');
|
||||
|
||||
// Retrieve the onebox
|
||||
var promise = Discourse.ajax("/onebox", {
|
||||
dataType: 'html',
|
||||
data: { url: url, refresh: refresh },
|
||||
cache: true
|
||||
});
|
||||
|
||||
// We can call this when loading is complete
|
||||
var loadingFinished = function() {
|
||||
$elem.removeClass('loading-onebox');
|
||||
$elem.data('onebox-loaded');
|
||||
};
|
||||
|
||||
var onebox = this;
|
||||
promise.then(function(html) {
|
||||
|
||||
// loaded onebox
|
||||
loadingFinished();
|
||||
|
||||
onebox.localCache[url] = html;
|
||||
$elem.replaceWith(html);
|
||||
|
||||
}, function() {
|
||||
// If the request failed log it as such
|
||||
onebox.failedCache[url] = true;
|
||||
loadingFinished();
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
Return the cached contents of a Onebox
|
||||
|
||||
@method lookupCache
|
||||
@param {String} url the url of the onebox
|
||||
@return {String} the cached contents of the onebox or null if not found
|
||||
**/
|
||||
lookupCache: function(url) {
|
||||
return this.localCache[url];
|
||||
},
|
||||
|
||||
/**
|
||||
Store the contents of a Onebox in our local cache.
|
||||
|
||||
@method cache
|
||||
@private
|
||||
@param {String} url the url of the onebox we crawled
|
||||
@param {String} contents the contents we want to cache
|
||||
**/
|
||||
cache: function(url, contents) {
|
||||
this.localCache[url] = contents;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -11,6 +11,7 @@ import { preventCloak } from 'discourse/widgets/post-stream';
|
|||
import { h } from 'virtual-dom';
|
||||
import { addFlagProperty } from 'discourse/components/site-header';
|
||||
import { addPopupMenuOptionsCallback } from 'discourse/controllers/composer';
|
||||
import { emojiUrlFor } from 'discourse/lib/text';
|
||||
|
||||
class PluginApi {
|
||||
constructor(version, container) {
|
||||
|
@ -93,7 +94,7 @@ class PluginApi {
|
|||
iconBody = iconNode(result.icon);
|
||||
} else if (result.emoji) {
|
||||
iconBody = result.emoji.split('|').map(emoji => {
|
||||
const src = Discourse.Emoji.urlFor(emoji);
|
||||
const src = emojiUrlFor(emoji);
|
||||
return dec.h('img', { className: 'emoji', attributes: { src } });
|
||||
});
|
||||
|
||||
|
|
33
app/assets/javascripts/discourse/lib/text.js.es6
Normal file
33
app/assets/javascripts/discourse/lib/text.js.es6
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { default as PrettyText, buildOptions } from 'pretty-text/pretty-text';
|
||||
import { performEmojiUnescape, buildEmojiUrl } from 'pretty-text/emoji';
|
||||
|
||||
// Use this to easily create a pretty text instance with proper options
|
||||
export function cook(text) {
|
||||
const siteSettings = Discourse.__container__.lookup('site-settings:main');
|
||||
|
||||
const opts = {
|
||||
getURL: Discourse.getURLWithCDN,
|
||||
siteSettings
|
||||
};
|
||||
|
||||
return new Handlebars.SafeString(new PrettyText(buildOptions(opts)).cook(text));
|
||||
}
|
||||
|
||||
function emojiOptions() {
|
||||
const siteSettings = Discourse.__container__.lookup('site-settings:main');
|
||||
if (!siteSettings.enable_emoji) { return; }
|
||||
|
||||
return { getURL: Discourse.getURLWithCDN, emojiSet: siteSettings.emoji_set };
|
||||
}
|
||||
|
||||
export function emojiUnescape(string) {
|
||||
const opts = emojiOptions();
|
||||
return opts ? performEmojiUnescape(string, opts) : string;
|
||||
}
|
||||
|
||||
export function emojiUrlFor(code) {
|
||||
const opts = emojiOptions();
|
||||
if (opts) {
|
||||
return buildEmojiUrl(code, opts);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import offsetCalculator from 'discourse/lib/offset-calculator';
|
||||
import LockOn from 'discourse/lib/lock-on';
|
||||
import { defaultHomepage } from 'discourse/lib/utilities';
|
||||
|
||||
let _jumpScheduled = false;
|
||||
const rewrites = [];
|
||||
|
@ -243,7 +244,7 @@ const DiscourseURL = Ember.Object.extend({
|
|||
@param {String} path the path we're navigating to
|
||||
**/
|
||||
navigatedToHome: function(oldPath, path) {
|
||||
const homepage = Discourse.Utilities.defaultHomepage();
|
||||
const homepage = defaultHomepage();
|
||||
|
||||
if (window.history &&
|
||||
window.history.pushState &&
|
||||
|
|
|
@ -1,327 +0,0 @@
|
|||
|
||||
var discourseEscape = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
'`': '`'
|
||||
};
|
||||
var discourseBadChars = /[&<>"'`]/g;
|
||||
var discoursePossible = /[&<>"'`]/;
|
||||
|
||||
function discourseEscapeChar(chr) {
|
||||
return discourseEscape[chr];
|
||||
}
|
||||
Discourse.Utilities = {
|
||||
|
||||
translateSize: function(size) {
|
||||
switch (size) {
|
||||
case 'tiny': return 20;
|
||||
case 'small': return 25;
|
||||
case 'medium': return 32;
|
||||
case 'large': return 45;
|
||||
case 'extra_large': return 60;
|
||||
case 'huge': return 120;
|
||||
}
|
||||
return size;
|
||||
},
|
||||
|
||||
// Handlebars no longer allows spaces in its `escapeExpression` code which makes it
|
||||
// unsuitable for many of Discourse's uses. Use `Handlebars.Utils.escapeExpression`
|
||||
// when escaping an attribute in HTML, otherwise this one will do.
|
||||
escapeExpression: function(string) {
|
||||
// don't escape SafeStrings, since they're already safe
|
||||
if (string instanceof Handlebars.SafeString) {
|
||||
return string.toString();
|
||||
} else if (string == null) {
|
||||
return "";
|
||||
} else if (!string) {
|
||||
return string + '';
|
||||
}
|
||||
|
||||
// Force a string conversion as this will be done by the append regardless and
|
||||
// the regex test will do this transparently behind the scenes, causing issues if
|
||||
// an object's to string has escaped characters in it.
|
||||
string = "" + string;
|
||||
|
||||
if(!discoursePossible.test(string)) { return string; }
|
||||
return string.replace(discourseBadChars, discourseEscapeChar);
|
||||
},
|
||||
|
||||
avatarUrl: function(template, size) {
|
||||
if (!template) { return ""; }
|
||||
var rawSize = Discourse.Utilities.getRawSize(Discourse.Utilities.translateSize(size));
|
||||
return template.replace(/\{size\}/g, rawSize);
|
||||
},
|
||||
|
||||
getRawSize: function(size) {
|
||||
var pixelRatio = window.devicePixelRatio || 1;
|
||||
return size * Math.min(3, Math.max(1, Math.round(pixelRatio)));
|
||||
},
|
||||
|
||||
avatarImg: function(options) {
|
||||
var size = Discourse.Utilities.translateSize(options.size);
|
||||
var url = Discourse.Utilities.avatarUrl(options.avatarTemplate, size);
|
||||
|
||||
// We won't render an invalid url
|
||||
if (!url || url.length === 0) { return ""; }
|
||||
|
||||
var classes = "avatar" + (options.extraClasses ? " " + options.extraClasses : "");
|
||||
var title = (options.title) ? " title='" + Handlebars.Utils.escapeExpression(options.title || "") + "'" : "";
|
||||
|
||||
return "<img alt='' width='" + size + "' height='" + size + "' src='" + Discourse.getURLWithCDN(url) + "' class='" + classes + "'" + title + ">";
|
||||
},
|
||||
|
||||
tinyAvatar: function(avatarTemplate, options) {
|
||||
return Discourse.Utilities.avatarImg(_.merge({avatarTemplate: avatarTemplate, size: 'tiny' }, options));
|
||||
},
|
||||
|
||||
postUrl: function(slug, topicId, postNumber) {
|
||||
var url = Discourse.getURL("/t/");
|
||||
if (slug) {
|
||||
url += slug + "/";
|
||||
} else {
|
||||
url += 'topic/';
|
||||
}
|
||||
url += topicId;
|
||||
if (postNumber > 1) {
|
||||
url += "/" + postNumber;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
userUrl: function(username) {
|
||||
return Discourse.getURL("/users/" + username.toLowerCase());
|
||||
},
|
||||
|
||||
emailValid: function(email) {
|
||||
// see: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
|
||||
var re = /^[a-zA-Z0-9!#$%&'*+\/=?\^_`{|}~\-]+(?:\.[a-zA-Z0-9!#$%&'\*+\/=?\^_`{|}~\-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/;
|
||||
return re.test(email);
|
||||
},
|
||||
|
||||
selectedText: function() {
|
||||
var html = '';
|
||||
|
||||
if (typeof window.getSelection !== "undefined") {
|
||||
var sel = window.getSelection();
|
||||
if (sel.rangeCount) {
|
||||
var container = document.createElement("div");
|
||||
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
|
||||
container.appendChild(sel.getRangeAt(i).cloneContents());
|
||||
}
|
||||
html = container.innerHTML;
|
||||
}
|
||||
} else if (typeof document.selection !== "undefined") {
|
||||
if (document.selection.type === "Text") {
|
||||
html = document.selection.createRange().htmlText;
|
||||
}
|
||||
}
|
||||
|
||||
// Strip out any .click elements from the HTML before converting it to text
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
var $div = $(div);
|
||||
// Find all emojis and replace with its title attribute.
|
||||
$div.find('img.emoji').replaceWith(function() { return this.title; });
|
||||
$('.clicks', $div).remove();
|
||||
var text = div.textContent || div.innerText || "";
|
||||
|
||||
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;
|
||||
if (el.selectionStart) {
|
||||
return el.selectionStart;
|
||||
}
|
||||
if (document.selection) {
|
||||
el.focus();
|
||||
r = document.selection.createRange();
|
||||
if (!r) return 0;
|
||||
|
||||
re = el.createTextRange();
|
||||
rc = re.duplicate();
|
||||
re.moveToBookmark(r.getBookmark());
|
||||
rc.setEndPoint('EndToStart', re);
|
||||
return rc.text.length;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
// Set the caret's position
|
||||
setCaretPosition: function(ctrl, pos) {
|
||||
var range;
|
||||
if (ctrl.setSelectionRange) {
|
||||
ctrl.focus();
|
||||
ctrl.setSelectionRange(pos, pos);
|
||||
return;
|
||||
}
|
||||
if (ctrl.createTextRange) {
|
||||
range = ctrl.createTextRange();
|
||||
range.collapse(true);
|
||||
range.moveEnd('character', pos);
|
||||
range.moveStart('character', pos);
|
||||
return range.select();
|
||||
}
|
||||
},
|
||||
|
||||
validateUploadedFiles: function(files, bypassNewUserRestriction) {
|
||||
if (!files || files.length === 0) { return false; }
|
||||
|
||||
if (files.length > 1) {
|
||||
bootbox.alert(I18n.t('post.errors.too_many_uploads'));
|
||||
return false;
|
||||
}
|
||||
|
||||
var upload = files[0];
|
||||
|
||||
// CHROME ONLY: if the image was pasted, sets its name to a default one
|
||||
if (typeof Blob !== "undefined" && typeof File !== "undefined") {
|
||||
if (upload instanceof Blob && !(upload instanceof File) && upload.type === "image/png") { upload.name = "blob.png"; }
|
||||
}
|
||||
|
||||
var type = Discourse.Utilities.uploadTypeFromFileName(upload.name);
|
||||
|
||||
return Discourse.Utilities.validateUploadedFile(upload, type, bypassNewUserRestriction);
|
||||
},
|
||||
|
||||
validateUploadedFile: function(file, type, bypassNewUserRestriction) {
|
||||
// check that the uploaded file is authorized
|
||||
if (!Discourse.Utilities.authorizesAllExtensions() &&
|
||||
!Discourse.Utilities.isAuthorizedUpload(file)) {
|
||||
var extensions = Discourse.Utilities.authorizedExtensions();
|
||||
bootbox.alert(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: extensions }));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!bypassNewUserRestriction) {
|
||||
// ensures that new users can upload a file
|
||||
if (!Discourse.User.current().isAllowedToUploadAFile(type)) {
|
||||
bootbox.alert(I18n.t('post.errors.' + type + '_upload_not_allowed_for_new_user'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// everything went fine
|
||||
return true;
|
||||
},
|
||||
|
||||
uploadTypeFromFileName: function(fileName) {
|
||||
return Discourse.Utilities.isAnImage(fileName) ? 'image' : 'attachment';
|
||||
},
|
||||
|
||||
authorizesAllExtensions: function() {
|
||||
return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0;
|
||||
},
|
||||
|
||||
isAuthorizedUpload: function(file) {
|
||||
if (file && file.name) {
|
||||
var extensions = _.chain(Discourse.SiteSettings.authorized_extensions.split("|"))
|
||||
.reject(function(extension) { return extension.indexOf("*") >= 0; })
|
||||
.map(function(extension) { return (extension.indexOf(".") === 0 ? extension.substring(1) : extension).replace(".", "\\."); })
|
||||
.value();
|
||||
return new RegExp("\\.(" + extensions.join("|") + ")$", "i").test(file.name);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
authorizedExtensions: function() {
|
||||
return _.chain(Discourse.SiteSettings.authorized_extensions.split("|"))
|
||||
.reject(function(extension) { return extension.indexOf("*") >= 0; })
|
||||
.map(function(extension) { return extension.toLowerCase(); })
|
||||
.value()
|
||||
.join(", ");
|
||||
},
|
||||
|
||||
uploadLocation: function(url) {
|
||||
if (Discourse.CDN) {
|
||||
url = Discourse.getURLWithCDN(url);
|
||||
return url.startsWith('//') ? 'http:' + url : url;
|
||||
} else if (Discourse.SiteSettings.enable_s3_uploads) {
|
||||
return 'https:' + url;
|
||||
} else {
|
||||
var protocol = window.location.protocol + '//',
|
||||
hostname = window.location.hostname,
|
||||
port = ':' + window.location.port;
|
||||
return protocol + hostname + port + url;
|
||||
}
|
||||
},
|
||||
|
||||
getUploadMarkdown: function(upload) {
|
||||
if (Discourse.Utilities.isAnImage(upload.original_filename)) {
|
||||
return '<img src="' + upload.url + '" width="' + upload.width + '" height="' + upload.height + '">';
|
||||
} else if (!Discourse.SiteSettings.prevent_anons_from_downloading_files && (/\.(mov|mp4|webm|ogv|mp3|ogg|wav|m4a)$/i).test(upload.original_filename)) {
|
||||
// is Audio/Video
|
||||
return Discourse.Utilities.uploadLocation(upload.url);
|
||||
} else {
|
||||
return '<a class="attachment" href="' + upload.url + '">' + upload.original_filename + '</a> (' + I18n.toHumanSize(upload.filesize) + ')\n';
|
||||
}
|
||||
},
|
||||
|
||||
isAnImage: function(path) {
|
||||
return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)$/i).test(path);
|
||||
},
|
||||
|
||||
allowsImages: function() {
|
||||
return Discourse.Utilities.authorizesAllExtensions() ||
|
||||
(/(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)/i).test(Discourse.Utilities.authorizedExtensions());
|
||||
},
|
||||
|
||||
allowsAttachments: function() {
|
||||
return Discourse.Utilities.authorizesAllExtensions() ||
|
||||
!(/((png|jpe?g|gif|bmp|tiff?|svg|web|ico)(,\s)?)+$/i).test(Discourse.Utilities.authorizedExtensions());
|
||||
},
|
||||
|
||||
displayErrorForUpload: function(data) {
|
||||
// deal with meaningful errors first
|
||||
if (data.jqXHR) {
|
||||
switch (data.jqXHR.status) {
|
||||
// cancelled by the user
|
||||
case 0: return;
|
||||
|
||||
// entity too large, usually returned from the web server
|
||||
case 413:
|
||||
var type = Discourse.Utilities.uploadTypeFromFileName(data.files[0].name);
|
||||
var maxSizeKB = Discourse.SiteSettings['max_' + type + '_size_kb'];
|
||||
bootbox.alert(I18n.t('post.errors.file_too_large', { max_size_kb: maxSizeKB }));
|
||||
return;
|
||||
|
||||
// the error message is provided by the server
|
||||
case 422:
|
||||
if (data.jqXHR.responseJSON.message) {
|
||||
bootbox.alert(data.jqXHR.responseJSON.message);
|
||||
} else {
|
||||
bootbox.alert(data.jqXHR.responseJSON.join("\n"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (data.errors && data.errors.length > 0) {
|
||||
bootbox.alert(data.errors.join("\n"));
|
||||
return;
|
||||
}
|
||||
// otherwise, display a generic error message
|
||||
bootbox.alert(I18n.t('post.errors.upload'));
|
||||
},
|
||||
|
||||
defaultHomepage: function() {
|
||||
// the homepage is the first item of the 'top_menu' site setting
|
||||
return Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
|
||||
}
|
||||
|
||||
};
|
302
app/assets/javascripts/discourse/lib/utilities.js.es6
Normal file
302
app/assets/javascripts/discourse/lib/utilities.js.es6
Normal file
|
@ -0,0 +1,302 @@
|
|||
import { escape } from 'pretty-text/sanitizer';
|
||||
|
||||
export function translateSize(size) {
|
||||
switch (size) {
|
||||
case 'tiny': return 20;
|
||||
case 'small': return 25;
|
||||
case 'medium': return 32;
|
||||
case 'large': return 45;
|
||||
case 'extra_large': return 60;
|
||||
case 'huge': return 120;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
export function escapeExpression(string) {
|
||||
// don't escape SafeStrings, since they're already safe
|
||||
if (string instanceof Handlebars.SafeString) {
|
||||
return string.toString();
|
||||
}
|
||||
|
||||
return escape(string);
|
||||
}
|
||||
|
||||
export function avatarUrl(template, size) {
|
||||
if (!template) { return ""; }
|
||||
const rawSize = getRawSize(translateSize(size));
|
||||
return template.replace(/\{size\}/g, rawSize);
|
||||
}
|
||||
|
||||
export function getRawSize(size) {
|
||||
const pixelRatio = window.devicePixelRatio || 1;
|
||||
return size * Math.min(3, Math.max(1, Math.round(pixelRatio)));
|
||||
}
|
||||
|
||||
export function avatarImg(options, getURL) {
|
||||
getURL = getURL || Discourse.getURLWithCDN;
|
||||
|
||||
const size = translateSize(options.size);
|
||||
const url = avatarUrl(options.avatarTemplate, size);
|
||||
|
||||
// We won't render an invalid url
|
||||
if (!url || url.length === 0) { return ""; }
|
||||
|
||||
const classes = "avatar" + (options.extraClasses ? " " + options.extraClasses : "");
|
||||
const title = (options.title) ? " title='" + escapeExpression(options.title || "") + "'" : "";
|
||||
|
||||
return "<img alt='' width='" + size + "' height='" + size + "' src='" + getURL(url) + "' class='" + classes + "'" + title + ">";
|
||||
}
|
||||
|
||||
export function tinyAvatar(avatarTemplate, options) {
|
||||
return avatarImg(_.merge({avatarTemplate: avatarTemplate, size: 'tiny' }, options));
|
||||
}
|
||||
|
||||
export function postUrl(slug, topicId, postNumber) {
|
||||
var url = Discourse.getURL("/t/");
|
||||
if (slug) {
|
||||
url += slug + "/";
|
||||
} else {
|
||||
url += 'topic/';
|
||||
}
|
||||
url += topicId;
|
||||
if (postNumber > 1) {
|
||||
url += "/" + postNumber;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function userUrl(username) {
|
||||
return Discourse.getURL("/users/" + username.toLowerCase());
|
||||
}
|
||||
|
||||
export function emailValid(email) {
|
||||
// see: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
|
||||
var re = /^[a-zA-Z0-9!#$%&'*+\/=?\^_`{|}~\-]+(?:\.[a-zA-Z0-9!#$%&'\*+\/=?\^_`{|}~\-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/;
|
||||
return re.test(email);
|
||||
}
|
||||
|
||||
export function selectedText() {
|
||||
var html = '';
|
||||
|
||||
if (typeof window.getSelection !== "undefined") {
|
||||
var sel = window.getSelection();
|
||||
if (sel.rangeCount) {
|
||||
var container = document.createElement("div");
|
||||
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
|
||||
container.appendChild(sel.getRangeAt(i).cloneContents());
|
||||
}
|
||||
html = container.innerHTML;
|
||||
}
|
||||
} else if (typeof document.selection !== "undefined") {
|
||||
if (document.selection.type === "Text") {
|
||||
html = document.selection.createRange().htmlText;
|
||||
}
|
||||
}
|
||||
|
||||
// Strip out any .click elements from the HTML before converting it to text
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
var $div = $(div);
|
||||
// Find all emojis and replace with its title attribute.
|
||||
$div.find('img.emoji').replaceWith(function() { return this.title; });
|
||||
$('.clicks', $div).remove();
|
||||
var text = div.textContent || div.innerText || "";
|
||||
|
||||
return String(text).trim();
|
||||
}
|
||||
|
||||
// Determine the row and col of the caret in an element
|
||||
export function caretRowCol(el) {
|
||||
var cp = caretPosition(el);
|
||||
var rows = el.value.slice(0, cp).split("\n");
|
||||
var rowNum = rows.length;
|
||||
|
||||
var colNum = cp - 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
|
||||
export function caretPosition(el) {
|
||||
var r, rc, re;
|
||||
if (el.selectionStart) {
|
||||
return el.selectionStart;
|
||||
}
|
||||
if (document.selection) {
|
||||
el.focus();
|
||||
r = document.selection.createRange();
|
||||
if (!r) return 0;
|
||||
|
||||
re = el.createTextRange();
|
||||
rc = re.duplicate();
|
||||
re.moveToBookmark(r.getBookmark());
|
||||
rc.setEndPoint('EndToStart', re);
|
||||
return rc.text.length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Set the caret's position
|
||||
export function setCaretPosition(ctrl, pos) {
|
||||
var range;
|
||||
if (ctrl.setSelectionRange) {
|
||||
ctrl.focus();
|
||||
ctrl.setSelectionRange(pos, pos);
|
||||
return;
|
||||
}
|
||||
if (ctrl.createTextRange) {
|
||||
range = ctrl.createTextRange();
|
||||
range.collapse(true);
|
||||
range.moveEnd('character', pos);
|
||||
range.moveStart('character', pos);
|
||||
return range.select();
|
||||
}
|
||||
}
|
||||
|
||||
export function validateUploadedFiles(files, bypassNewUserRestriction) {
|
||||
if (!files || files.length === 0) { return false; }
|
||||
|
||||
if (files.length > 1) {
|
||||
bootbox.alert(I18n.t('post.errors.too_many_uploads'));
|
||||
return false;
|
||||
}
|
||||
|
||||
var upload = files[0];
|
||||
|
||||
// CHROME ONLY: if the image was pasted, sets its name to a default one
|
||||
if (typeof Blob !== "undefined" && typeof File !== "undefined") {
|
||||
if (upload instanceof Blob && !(upload instanceof File) && upload.type === "image/png") { upload.name = "blob.png"; }
|
||||
}
|
||||
|
||||
var type = uploadTypeFromFileName(upload.name);
|
||||
|
||||
return validateUploadedFile(upload, type, bypassNewUserRestriction);
|
||||
}
|
||||
|
||||
export function validateUploadedFile(file, type, bypassNewUserRestriction) {
|
||||
// check that the uploaded file is authorized
|
||||
if (!authorizesAllExtensions() &&
|
||||
!isAuthorizedUpload(file)) {
|
||||
var extensions = authorizedExtensions();
|
||||
bootbox.alert(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: extensions }));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!bypassNewUserRestriction) {
|
||||
// ensures that new users can upload a file
|
||||
if (!Discourse.User.current().isAllowedToUploadAFile(type)) {
|
||||
bootbox.alert(I18n.t('post.errors.' + type + '_upload_not_allowed_for_new_user'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// everything went fine
|
||||
return true;
|
||||
}
|
||||
|
||||
export function uploadTypeFromFileName(fileName) {
|
||||
return isAnImage(fileName) ? 'image' : 'attachment';
|
||||
}
|
||||
|
||||
export function authorizesAllExtensions() {
|
||||
return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0;
|
||||
}
|
||||
|
||||
export function isAuthorizedUpload(file) {
|
||||
if (file && file.name) {
|
||||
var extensions = _.chain(Discourse.SiteSettings.authorized_extensions.split("|"))
|
||||
.reject(function(extension) { return extension.indexOf("*") >= 0; })
|
||||
.map(function(extension) { return (extension.indexOf(".") === 0 ? extension.substring(1) : extension).replace(".", "\\."); })
|
||||
.value();
|
||||
return new RegExp("\\.(" + extensions.join("|") + ")$", "i").test(file.name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function authorizedExtensions() {
|
||||
return _.chain(Discourse.SiteSettings.authorized_extensions.split("|"))
|
||||
.reject(function(extension) { return extension.indexOf("*") >= 0; })
|
||||
.map(function(extension) { return extension.toLowerCase(); })
|
||||
.value()
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
export function uploadLocation(url) {
|
||||
if (Discourse.CDN) {
|
||||
url = Discourse.getURLWithCDN(url);
|
||||
return url.startsWith('//') ? 'http:' + url : url;
|
||||
} else if (Discourse.SiteSettings.enable_s3_uploads) {
|
||||
return 'https:' + url;
|
||||
} else {
|
||||
var protocol = window.location.protocol + '//',
|
||||
hostname = window.location.hostname,
|
||||
port = ':' + window.location.port;
|
||||
return protocol + hostname + port + url;
|
||||
}
|
||||
}
|
||||
|
||||
export function getUploadMarkdown(upload) {
|
||||
if (isAnImage(upload.original_filename)) {
|
||||
return '<img src="' + upload.url + '" width="' + upload.width + '" height="' + upload.height + '">';
|
||||
} else if (!Discourse.SiteSettings.prevent_anons_from_downloading_files && (/\.(mov|mp4|webm|ogv|mp3|ogg|wav|m4a)$/i).test(upload.original_filename)) {
|
||||
// is Audio/Video
|
||||
return uploadLocation(upload.url);
|
||||
} else {
|
||||
return '<a class="attachment" href="' + upload.url + '">' + upload.original_filename + '</a> (' + I18n.toHumanSize(upload.filesize) + ')\n';
|
||||
}
|
||||
}
|
||||
|
||||
export function isAnImage(path) {
|
||||
return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)$/i).test(path);
|
||||
}
|
||||
|
||||
export function allowsImages() {
|
||||
return authorizesAllExtensions() ||
|
||||
(/(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)/i).test(authorizedExtensions());
|
||||
}
|
||||
|
||||
export function allowsAttachments() {
|
||||
return authorizesAllExtensions() ||
|
||||
!(/((png|jpe?g|gif|bmp|tiff?|svg|web|ico)(,\s)?)+$/i).test(authorizedExtensions());
|
||||
}
|
||||
|
||||
export function displayErrorForUpload(data) {
|
||||
// deal with meaningful errors first
|
||||
if (data.jqXHR) {
|
||||
switch (data.jqXHR.status) {
|
||||
// cancelled by the user
|
||||
case 0: return;
|
||||
|
||||
// entity too large, usually returned from the web server
|
||||
case 413:
|
||||
var type = uploadTypeFromFileName(data.files[0].name);
|
||||
var maxSizeKB = Discourse.SiteSettings['max_' + type + '_size_kb'];
|
||||
bootbox.alert(I18n.t('post.errors.file_too_large', { max_size_kb: maxSizeKB }));
|
||||
return;
|
||||
|
||||
// the error message is provided by the server
|
||||
case 422:
|
||||
if (data.jqXHR.responseJSON.message) {
|
||||
bootbox.alert(data.jqXHR.responseJSON.message);
|
||||
} else {
|
||||
bootbox.alert(data.jqXHR.responseJSON.join("\n"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (data.errors && data.errors.length > 0) {
|
||||
bootbox.alert(data.errors.join("\n"));
|
||||
return;
|
||||
}
|
||||
// otherwise, display a generic error message
|
||||
bootbox.alert(I18n.t('post.errors.upload'));
|
||||
}
|
||||
|
||||
export function defaultHomepage() {
|
||||
// the homepage is the first item of the 'top_menu' site setting
|
||||
return Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
|
||||
}
|
||||
|
||||
// This prevents a mini racer crash
|
||||
export default {};
|
|
@ -1,3 +1,5 @@
|
|||
import { displayErrorForUpload, validateUploadedFiles } from 'discourse/lib/utilities';
|
||||
|
||||
export default Em.Mixin.create({
|
||||
uploading: false,
|
||||
uploadProgress: 0,
|
||||
|
@ -16,7 +18,7 @@ export default Em.Mixin.create({
|
|||
if (upload && upload.url) {
|
||||
this.uploadDone(upload);
|
||||
} else {
|
||||
Discourse.Utilities.displayErrorForUpload(upload);
|
||||
displayErrorForUpload(upload);
|
||||
}
|
||||
reset();
|
||||
});
|
||||
|
@ -38,7 +40,7 @@ export default Em.Mixin.create({
|
|||
});
|
||||
|
||||
$upload.on("fileuploadsubmit", (e, data) => {
|
||||
const isValid = Discourse.Utilities.validateUploadedFiles(data.files, true);
|
||||
const isValid = validateUploadedFiles(data.files, true);
|
||||
let form = { type: this.get("type") };
|
||||
if (this.get("data")) { form = $.extend(form, this.get("data")); }
|
||||
data.formData = form;
|
||||
|
@ -52,7 +54,7 @@ export default Em.Mixin.create({
|
|||
});
|
||||
|
||||
$upload.on("fileuploadfail", (e, data) => {
|
||||
Discourse.Utilities.displayErrorForUpload(data);
|
||||
displayErrorForUpload(data);
|
||||
reset();
|
||||
});
|
||||
}.on("didInsertElement"),
|
||||
|
|
|
@ -4,6 +4,7 @@ import { throwAjaxError } from 'discourse/lib/ajax-error';
|
|||
import Quote from 'discourse/lib/quote';
|
||||
import Draft from 'discourse/models/draft';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { escapeExpression, tinyAvatar } from 'discourse/lib/utilities';
|
||||
|
||||
const CLOSED = 'closed',
|
||||
SAVING = 'saving',
|
||||
|
@ -141,7 +142,7 @@ const Composer = RestModel.extend({
|
|||
const postNumber = this.get('post.post_number');
|
||||
postLink = "<a href='" + (topic.get('url')) + "/" + postNumber + "'>" +
|
||||
I18n.t("post.post_number", { number: postNumber }) + "</a>";
|
||||
topicLink = "<a href='" + (topic.get('url')) + "'> " + Discourse.Utilities.escapeExpression(topic.get('title')) + "</a>";
|
||||
topicLink = "<a href='" + (topic.get('url')) + "'> " + escapeExpression(topic.get('title')) + "</a>";
|
||||
usernameLink = "<a href='" + (topic.get('url')) + "/" + postNumber + "'>" + this.get('post.username') + "</a>";
|
||||
}
|
||||
|
||||
|
@ -151,7 +152,7 @@ const Composer = RestModel.extend({
|
|||
if (post) {
|
||||
postDescription = I18n.t('post.' + this.get('action'), {
|
||||
link: postLink,
|
||||
replyAvatar: Discourse.Utilities.tinyAvatar(post.get('avatar_template')),
|
||||
replyAvatar: tinyAvatar(post.get('avatar_template')),
|
||||
username: this.get('post.username'),
|
||||
usernameLink
|
||||
});
|
||||
|
@ -160,7 +161,7 @@ const Composer = RestModel.extend({
|
|||
const replyUsername = post.get('reply_to_user.username');
|
||||
const replyAvatarTemplate = post.get('reply_to_user.avatar_template');
|
||||
if (replyUsername && replyAvatarTemplate && this.get('action') === EDIT) {
|
||||
postDescription += " <i class='fa fa-mail-forward reply-to-glyph'></i> " + Discourse.Utilities.tinyAvatar(replyAvatarTemplate) + " " + replyUsername;
|
||||
postDescription += " <i class='fa fa-mail-forward reply-to-glyph'></i> " + tinyAvatar(replyAvatarTemplate) + " " + replyUsername;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import ActionSummary from 'discourse/models/action-summary';
|
|||
import { url, propertyEqual } from 'discourse/lib/computed';
|
||||
import Quote from 'discourse/lib/quote';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { postUrl } from 'discourse/lib/utilities';
|
||||
import { cook } from 'discourse/lib/text';
|
||||
|
||||
const Post = RestModel.extend({
|
||||
|
||||
|
@ -48,13 +50,13 @@ const Post = RestModel.extend({
|
|||
}.property('firstPost', 'deleted_at', 'topic.deleted_at'),
|
||||
|
||||
url: function() {
|
||||
return Discourse.Utilities.postUrl(this.get('topic.slug') || this.get('topic_slug'), this.get('topic_id'), this.get('post_number'));
|
||||
return postUrl(this.get('topic.slug') || this.get('topic_slug'), this.get('topic_id'), this.get('post_number'));
|
||||
}.property('post_number', 'topic_id', 'topic.slug'),
|
||||
|
||||
// Don't drop the /1
|
||||
@computed('post_number', 'url')
|
||||
urlWithNumber(postNumber, postUrl) {
|
||||
return postNumber === 1 ? postUrl + "/1" : postUrl;
|
||||
urlWithNumber(postNumber, baseUrl) {
|
||||
return postNumber === 1 ? baseUrl + "/1" : baseUrl;
|
||||
},
|
||||
|
||||
usernameUrl: url('username', '/users/%@'),
|
||||
|
@ -163,8 +165,9 @@ const Post = RestModel.extend({
|
|||
can_delete: false
|
||||
});
|
||||
} else {
|
||||
|
||||
this.setProperties({
|
||||
cooked: Discourse.Markdown.cook(I18n.t("post.deleted_by_author", {count: Discourse.SiteSettings.delete_removed_posts_after})),
|
||||
cooked: cook(I18n.t("post.deleted_by_author", {count: Discourse.SiteSettings.delete_removed_posts_after})),
|
||||
can_delete: false,
|
||||
version: this.get('version') + 1,
|
||||
can_recover: true,
|
||||
|
|
|
@ -197,6 +197,7 @@ export default Ember.Object.extend({
|
|||
// TODO: Have injections be automatic
|
||||
obj.topicTrackingState = this.container.lookup('topic-tracking-state:main');
|
||||
obj.keyValueStore = this.container.lookup('key-value-store:main');
|
||||
obj.siteSettings = this.container.lookup('site-settings:main');
|
||||
|
||||
const klass = this.container.lookupFactory('model:' + type) || RestModel;
|
||||
const model = klass.create(obj);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { NotificationLevels } from 'discourse/lib/notification-levels';
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { on } from "ember-addons/ember-computed-decorators";
|
||||
import { defaultHomepage } from 'discourse/lib/utilities';
|
||||
|
||||
function isNew(topic) {
|
||||
return topic.last_read_post_number === null &&
|
||||
|
@ -114,7 +115,7 @@ const TopicTrackingState = Discourse.Model.extend({
|
|||
}
|
||||
}
|
||||
|
||||
if (filter === Discourse.Utilities.defaultHomepage()) {
|
||||
if (filter === defaultHomepage()) {
|
||||
const suppressed_from_homepage_category_ids = Discourse.Site.currentProp("suppressed_from_homepage_category_ids");
|
||||
if (_.include(suppressed_from_homepage_category_ids, data.payload.category_id)) {
|
||||
return;
|
||||
|
|
|
@ -5,6 +5,8 @@ import { longDate } from 'discourse/lib/formatter';
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import ActionSummary from 'discourse/models/action-summary';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import { censor } from 'pretty-text/censored-words';
|
||||
import { emojiUnescape } from 'discourse/lib/text';
|
||||
|
||||
export function loadTopicView(topic, args) {
|
||||
const topicId = topic.get('id');
|
||||
|
@ -45,9 +47,11 @@ const Topic = RestModel.extend({
|
|||
|
||||
@computed('fancy_title')
|
||||
fancyTitle(title) {
|
||||
title = title || "";
|
||||
title = Discourse.Emoji.unescape(title);
|
||||
return Discourse.CensoredWords.censor(title);
|
||||
// TODO: `siteSettings` should always be present, but there are places in the code
|
||||
// that call Discourse.Topic.create instead of using the store. When the store is
|
||||
// used, remove this.
|
||||
const siteSettings = this.siteSettings || Discourse.SiteSettings;
|
||||
return censor(emojiUnescape(title || ""), siteSettings.censored_words);
|
||||
},
|
||||
|
||||
// returns createdAt if there's no bumped date
|
||||
|
|
|
@ -3,6 +3,7 @@ import { url } from 'discourse/lib/computed';
|
|||
import { on } from 'ember-addons/ember-computed-decorators';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import UserActionGroup from 'discourse/models/user-action-group';
|
||||
import { postUrl } from 'discourse/lib/utilities';
|
||||
|
||||
const UserActionTypes = {
|
||||
likes_given: 1,
|
||||
|
@ -89,12 +90,12 @@ const UserAction = RestModel.extend({
|
|||
|
||||
@computed()
|
||||
postUrl() {
|
||||
return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('post_number'));
|
||||
return postUrl(this.get('slug'), this.get('topic_id'), this.get('post_number'));
|
||||
},
|
||||
|
||||
@computed()
|
||||
replyUrl() {
|
||||
return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('reply_to_post_number'));
|
||||
return postUrl(this.get('slug'), this.get('topic_id'), this.get('reply_to_post_number'));
|
||||
},
|
||||
|
||||
replyType: Em.computed.equal('action_type', UserActionTypes.replies),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { url } from 'discourse/lib/computed';
|
||||
import RestModel from 'discourse/models/rest';
|
||||
import UserAction from 'discourse/models/user-action';
|
||||
import { emojiUnescape } from 'discourse/lib/text';
|
||||
|
||||
export default RestModel.extend({
|
||||
loaded: false,
|
||||
|
@ -70,7 +71,7 @@ export default RestModel.extend({
|
|||
if (result && result.user_actions) {
|
||||
const copy = Em.A();
|
||||
result.user_actions.forEach(function(action) {
|
||||
action.title = Discourse.Emoji.unescape(Handlebars.Utils.escapeExpression(action.title));
|
||||
action.title = emojiUnescape(Handlebars.Utils.escapeExpression(action.title));
|
||||
copy.pushObject(UserAction.create(action));
|
||||
});
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import UserActionStat from 'discourse/models/user-action-stat';
|
|||
import UserAction from 'discourse/models/user-action';
|
||||
import Group from 'discourse/models/group';
|
||||
import Topic from 'discourse/models/topic';
|
||||
import { emojiUnescape } from 'discourse/lib/text';
|
||||
|
||||
const User = RestModel.extend({
|
||||
|
||||
|
@ -231,7 +232,7 @@ const User = RestModel.extend({
|
|||
if ((this.get('stream.filter') || ua.action_type) !== ua.action_type) return;
|
||||
if (!this.get('stream.filter') && !this.inAllStream(ua)) return;
|
||||
|
||||
ua.title = Discourse.Emoji.unescape(Handlebars.Utils.escapeExpression(ua.title));
|
||||
ua.title = emojiUnescape(Handlebars.Utils.escapeExpression(ua.title));
|
||||
const action = UserAction.collapseStream([UserAction.create(ua)]);
|
||||
stream.set('itemsLoaded', stream.get('itemsLoaded') + 1);
|
||||
stream.get('content').insertAt(0, action[0]);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { defaultHomepage } from 'discourse/lib/utilities';
|
||||
|
||||
export default function() {
|
||||
// Error page
|
||||
this.route('exception', { path: '/exception' });
|
||||
|
@ -45,7 +47,7 @@ export default function() {
|
|||
this.route('categoryWithID', { path: '/c/:parentSlug/:slug/:id' });
|
||||
|
||||
// homepage
|
||||
this.route(Discourse.Utilities.defaultHomepage(), { path: '/' });
|
||||
this.route(defaultHomepage(), { path: '/' });
|
||||
});
|
||||
|
||||
this.resource('group', { path: '/groups/:name' }, function() {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { queryParams } from 'discourse/controllers/discovery-sortable';
|
||||
import { defaultHomepage } from 'discourse/lib/utilities';
|
||||
|
||||
// A helper to build a topic route for a filter
|
||||
function filterQueryParams(params, defaultParams) {
|
||||
|
@ -77,7 +78,7 @@ export default function(filter, extras) {
|
|||
},
|
||||
|
||||
titleToken() {
|
||||
if (filter === Discourse.Utilities.defaultHomepage()) { return; }
|
||||
if (filter === defaultHomepage()) { return; }
|
||||
|
||||
const filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title');
|
||||
return I18n.t('filters.with_topics', {filter: filterText});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import showModal from "discourse/lib/show-modal";
|
||||
import OpenComposer from "discourse/mixins/open-composer";
|
||||
import CategoryList from "discourse/models/category-list";
|
||||
import { defaultHomepage } from 'discourse/lib/utilities';
|
||||
|
||||
const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
||||
renderTemplate() {
|
||||
|
@ -28,7 +29,7 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
|||
},
|
||||
|
||||
titleToken() {
|
||||
if (Discourse.Utilities.defaultHomepage() === "categories") { return; }
|
||||
if (defaultHomepage() === "categories") { return; }
|
||||
return I18n.t("filters.categories.title");
|
||||
},
|
||||
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import loadScript from 'discourse/lib/load-script';
|
||||
import DiscourseRoute from 'discourse/routes/discourse';
|
||||
|
||||
export default DiscourseRoute.extend({
|
||||
|
||||
// this route requires the sanitizer
|
||||
beforeModel() {
|
||||
loadScript('defer/html-sanitizer-bundle');
|
||||
},
|
||||
|
||||
model() {
|
||||
return this.store.find('queuedPost', {status: 'new'});
|
||||
},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
{{site-header canSignUp=canSignUp
|
||||
showCreateAccount="showCreateAccount"
|
||||
showLogin="showLogin"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import ModalBodyView from "discourse/views/modal-body";
|
||||
import ClickTrack from 'discourse/lib/click-track';
|
||||
import { selectedText } from 'discourse/lib/utilities';
|
||||
|
||||
export default ModalBodyView.extend({
|
||||
templateName: 'modal/history',
|
||||
|
@ -15,7 +16,7 @@ export default ModalBodyView.extend({
|
|||
// bypass if we are selecting stuff
|
||||
const selection = window.getSelection && window.getSelection();
|
||||
if (selection.type === "Range" || selection.rangeCount > 0) {
|
||||
if (Discourse.Utilities.selectedText() !== "") {
|
||||
if (selectedText() !== "") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import AddCategoryClass from 'discourse/mixins/add-category-class';
|
|||
import AddArchetypeClass from 'discourse/mixins/add-archetype-class';
|
||||
import ClickTrack from 'discourse/lib/click-track';
|
||||
import Scrolling from 'discourse/mixins/scrolling';
|
||||
import { selectedText } from 'discourse/lib/utilities';
|
||||
|
||||
const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolling, {
|
||||
templateName: 'topic',
|
||||
|
@ -50,7 +51,7 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli
|
|||
// bypass if we are selecting stuff
|
||||
const selection = window.getSelection && window.getSelection();
|
||||
if (selection.type === "Range" || selection.rangeCount > 0) {
|
||||
if (Discourse.Utilities.selectedText() !== "") {
|
||||
if (selectedText() !== "") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { createWidget } from 'discourse/widgets/widget';
|
||||
import { emojiUrlFor } from 'discourse/lib/text';
|
||||
|
||||
export default createWidget('emoji', {
|
||||
tagName: 'img.emoji',
|
||||
|
||||
buildAttributes(attrs) {
|
||||
return { src: Discourse.Emoji.urlFor(attrs.name) };
|
||||
return { src: emojiUrlFor(attrs.name) };
|
||||
},
|
||||
});
|
||||
|
|
|
@ -3,6 +3,8 @@ import RawHtml from 'discourse/widgets/raw-html';
|
|||
import { createWidget } from 'discourse/widgets/widget';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { h } from 'virtual-dom';
|
||||
import { emojiUnescape } from 'discourse/lib/text';
|
||||
import { postUrl, escapeExpression } from 'discourse/lib/utilities';
|
||||
|
||||
const LIKED_TYPE = 5;
|
||||
const INVITED_TYPE = 8;
|
||||
|
@ -38,7 +40,7 @@ createWidget('notification-item', {
|
|||
|
||||
const topicId = attrs.topic_id;
|
||||
if (topicId) {
|
||||
return Discourse.Utilities.postUrl(attrs.slug, topicId, attrs.post_number);
|
||||
return postUrl(attrs.slug, topicId, attrs.post_number);
|
||||
}
|
||||
|
||||
if (attrs.notification_type === INVITED_TYPE) {
|
||||
|
@ -53,10 +55,10 @@ createWidget('notification-item', {
|
|||
description() {
|
||||
const data = this.attrs.data;
|
||||
const badgeName = data.badge_name;
|
||||
if (badgeName) { return Discourse.Utilities.escapeExpression(badgeName); }
|
||||
if (badgeName) { return escapeExpression(badgeName); }
|
||||
|
||||
const title = data.topic_title;
|
||||
return Ember.isEmpty(title) ? "" : Discourse.Utilities.escapeExpression(title);
|
||||
return Ember.isEmpty(title) ? "" : escapeExpression(title);
|
||||
},
|
||||
|
||||
text(notificationType, notName) {
|
||||
|
@ -90,7 +92,7 @@ createWidget('notification-item', {
|
|||
const lookup = this.site.get('notificationLookup');
|
||||
const notName = lookup[notificationType];
|
||||
|
||||
const contents = new RawHtml({ html: `<div>${Discourse.Emoji.unescape(this.text(notificationType, notName))}</div>` });
|
||||
const contents = new RawHtml({ html: `<div>${emojiUnescape(this.text(notificationType, notName))}</div>` });
|
||||
const href = this.url();
|
||||
const alt = I18n.t(`notifications.alt.${notName}`);
|
||||
return href ? h('a', { attributes: { href, alt, 'data-auto-route': true } }, contents) : contents;
|
||||
|
|
|
@ -2,6 +2,7 @@ import { iconNode } from 'discourse/helpers/fa-icon';
|
|||
import { createWidget } from 'discourse/widgets/widget';
|
||||
import { h } from 'virtual-dom';
|
||||
import RawHtml from 'discourse/widgets/raw-html';
|
||||
import { emojiUnescape } from 'discourse/lib/text';
|
||||
|
||||
export default createWidget('post-links', {
|
||||
tagName: 'div.post-links-container',
|
||||
|
@ -12,7 +13,7 @@ export default createWidget('post-links', {
|
|||
},
|
||||
|
||||
linkHtml(link) {
|
||||
const escapedTitle = Discourse.Emoji.unescape(Handlebars.Utils.escapeExpression(link.title));
|
||||
const escapedTitle = emojiUnescape(Handlebars.Utils.escapeExpression(link.title));
|
||||
const linkBody = [new RawHtml({ html: `<span>${escapedTitle}</span>` })];
|
||||
if (link.clicks) {
|
||||
linkBody.push(h('span.badge.badge-notification.clicks', link.clicks.toString()));
|
||||
|
|
|
@ -6,10 +6,11 @@ import { transformBasicPost } from 'discourse/lib/transform-post';
|
|||
import { h } from 'virtual-dom';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { dateNode } from 'discourse/helpers/node';
|
||||
import { translateSize, avatarUrl } from 'discourse/lib/utilities';
|
||||
|
||||
export function avatarImg(wanted, attrs) {
|
||||
const size = Discourse.Utilities.translateSize(wanted);
|
||||
const url = Discourse.Utilities.avatarUrl(attrs.template, size);
|
||||
const size = translateSize(wanted);
|
||||
const url = avatarUrl(attrs.template, size);
|
||||
|
||||
// We won't render an invalid url
|
||||
if (!url || url.length === 0) { return; }
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { createWidget } from 'discourse/widgets/widget';
|
||||
import { iconNode } from 'discourse/helpers/fa-icon';
|
||||
import { h } from 'virtual-dom';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
|
||||
function renderIcon(name, key, canAct) {
|
||||
const iconArgs = key === 'unpinned' ? { 'class': 'unpinned' } : null,
|
||||
icon = iconNode(name, iconArgs);
|
||||
|
||||
const attributes = { title: Discourse.Utilities.escapeExpression(I18n.t(`topic_statuses.${key}.help`)) };
|
||||
const attributes = { title: escapeExpression(I18n.t(`topic_statuses.${key}.help`)) };
|
||||
return h(`${canAct ? 'a' : 'span'}.topic-status`, attributes, icon);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
//= require ./ember-addons/decorator-alias
|
||||
//= require ./ember-addons/macro-alias
|
||||
//= require ./ember-addons/ember-computed-decorators
|
||||
//= require ./discourse/lib/utilities
|
||||
//= require ./discourse/lib/text
|
||||
//= require ./discourse/lib/hash
|
||||
//= require ./discourse/lib/load-script
|
||||
//= require ./discourse/lib/notification-levels
|
||||
|
@ -33,7 +35,6 @@
|
|||
//= require ./discourse/models/user-action-group
|
||||
//= require ./discourse/models/category
|
||||
//= require ./discourse/lib/ajax-error
|
||||
//= require ./discourse/lib/markdown
|
||||
//= require ./discourse/lib/search
|
||||
//= require ./discourse/lib/user-search
|
||||
//= require ./discourse/lib/export-csv
|
||||
|
@ -69,11 +70,8 @@
|
|||
//= require ./discourse/components/notifications-button
|
||||
//= require ./discourse/lib/link-mentions
|
||||
//= require ./discourse/components/site-header
|
||||
//= require ./discourse/lib/utilities
|
||||
//= require ./discourse/dialects/dialect
|
||||
//= require ./discourse/lib/emoji/emoji
|
||||
//= require ./discourse/lib/emoji/emoji-groups
|
||||
//= require ./discourse/lib/emoji/emoji-toolbar
|
||||
//= require ./discourse/lib/emoji/groups
|
||||
//= require ./discourse/lib/emoji/toolbar
|
||||
//= require ./discourse/components/d-editor
|
||||
//= require ./discourse/views/composer
|
||||
//= require ./discourse/lib/show-modal
|
||||
|
@ -95,7 +93,6 @@
|
|||
//= require_tree ./discourse/lib
|
||||
//= require ./discourse/router
|
||||
|
||||
//= require_tree ./discourse/dialects
|
||||
//= require_tree ./discourse/controllers
|
||||
//= require_tree ./discourse/models
|
||||
//= require_tree ./discourse/components
|
||||
|
|
13
app/assets/javascripts/pretty-text-bundle.js
Normal file
13
app/assets/javascripts/pretty-text-bundle.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
//= require ./pretty-text/pretty-text
|
||||
//= require ./pretty-text/guid
|
||||
//= require ./pretty-text/censored-words
|
||||
//= require ./pretty-text/emoji/data
|
||||
//= require ./pretty-text/emoji
|
||||
//= require ./pretty-text/engines/discourse-markdown
|
||||
//= require_tree ./pretty-text/engines/discourse-markdown
|
||||
//= require xss.min
|
||||
//= require better_markdown.js
|
||||
//= require ./pretty-text/xss
|
||||
//= require ./pretty-text/white-lister
|
||||
//= require ./pretty-text/sanitizer
|
||||
//= require ./pretty-text/oneboxer
|
19
app/assets/javascripts/pretty-text/censored-words.js.es6
Normal file
19
app/assets/javascripts/pretty-text/censored-words.js.es6
Normal file
|
@ -0,0 +1,19 @@
|
|||
export function censor(text, censoredWords) {
|
||||
if (censoredWords && censoredWords.length) {
|
||||
const split = censoredWords.split("|");
|
||||
let censorRegexp;
|
||||
if (split && split.length) {
|
||||
censorRegexp = new RegExp("(\\b(?:" + split.map(function (t) { return "(" + t.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + ")"; }).join("|") + ")\\b)(?![^\\(]*\\))", "ig");
|
||||
}
|
||||
|
||||
if (censorRegexp) {
|
||||
let m = censorRegexp.exec(text);
|
||||
while (m && m[0]) {
|
||||
const replacement = new Array(m[0].length+1).join('■');
|
||||
text = text.replace(new RegExp("(\\b" + m[0] + "\\b)(?![^\\(]*\\))", "ig"), replacement);
|
||||
m = censorRegexp.exec(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
107
app/assets/javascripts/pretty-text/emoji.js.es6
Normal file
107
app/assets/javascripts/pretty-text/emoji.js.es6
Normal file
|
@ -0,0 +1,107 @@
|
|||
import { emoji, aliases, translations } from 'pretty-text/emoji/data';
|
||||
|
||||
// bump up this number to expire all emojis
|
||||
export const IMAGE_VERSION = "2";
|
||||
|
||||
const extendedEmoji = {};
|
||||
|
||||
export function registerEmoji(code, url) {
|
||||
code = code.toLowerCase();
|
||||
extendedEmoji[code] = url;
|
||||
}
|
||||
|
||||
export function emojiList() {
|
||||
const result = emoji.slice(0);
|
||||
_.each(extendedEmoji, (v,k) => result.push(k));
|
||||
return result;
|
||||
}
|
||||
|
||||
const emojiHash = {};
|
||||
|
||||
// add all default emojis
|
||||
emoji.forEach(code => emojiHash[code] = true);
|
||||
|
||||
// and their aliases
|
||||
const aliasHash = {};
|
||||
Object.keys(aliases).forEach(name => {
|
||||
aliases[name].forEach(alias => aliasHash[alias] = name);
|
||||
});
|
||||
|
||||
export function performEmojiUnescape(string, opts) {
|
||||
// this can be further improved by supporting matches of emoticons that don't begin with a colon
|
||||
if (string.indexOf(":") >= 0) {
|
||||
return string.replace(/\B:[^\s:]+:?\B/g, m => {
|
||||
const isEmoticon = !!translations[m];
|
||||
const emojiVal = isEmoticon ? translations[m] : m.slice(1, m.length - 1);
|
||||
const hasEndingColon = m.lastIndexOf(":") === m.length - 1;
|
||||
const url = buildEmojiUrl(emojiVal, opts);
|
||||
|
||||
return url && (isEmoticon || hasEndingColon) ?
|
||||
`<img src='${url}' title='${emojiVal}' alt='${emojiVal}' class='emoji'>` : m;
|
||||
});
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
export function buildEmojiUrl(code, opts) {
|
||||
let url;
|
||||
code = code.toLowerCase();
|
||||
|
||||
if (extendedEmoji.hasOwnProperty(code)) {
|
||||
url = extendedEmoji[code];
|
||||
}
|
||||
|
||||
if (opts && opts.customEmoji && opts.customEmoji[code]) {
|
||||
url = opts.customEmoji[code];
|
||||
}
|
||||
|
||||
if (!url && emojiHash.hasOwnProperty(code)) {
|
||||
url = opts.getURL(`/images/emoji/${opts.emojiSet}/${code}.png`);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
url = url + "?v=" + IMAGE_VERSION;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function emojiExists(code) {
|
||||
code = code.toLowerCase();
|
||||
return !!(extendedEmoji.hasOwnProperty(code) || emojiHash.hasOwnProperty(code));
|
||||
};
|
||||
|
||||
let toSearch;
|
||||
export function emojiSearch(term, options) {
|
||||
const maxResults = (options && options["maxResults"]) || -1;
|
||||
if (maxResults === 0) { return []; }
|
||||
|
||||
toSearch = toSearch || _.union(_.keys(emojiHash), _.keys(extendedEmoji), _.keys(aliasHash)).sort();
|
||||
|
||||
const results = [];
|
||||
|
||||
function addResult(t) {
|
||||
const val = aliasHash[t] || t;
|
||||
if (results.indexOf(val) === -1) {
|
||||
results.push(val);
|
||||
}
|
||||
return maxResults > 0 && results.length >= maxResults;
|
||||
}
|
||||
|
||||
for (let i=0; i<toSearch.length; i++) {
|
||||
const item = toSearch[i];
|
||||
if (item.indexOf(term) === 0 && addResult(item)) {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i=0; i<toSearch.length; i++) {
|
||||
const item = toSearch[i];
|
||||
if (item.indexOf(term) > 0 && addResult(item)) {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
28
app/assets/javascripts/pretty-text/emoji/data.js.es6.erb
Normal file
28
app/assets/javascripts/pretty-text/emoji/data.js.es6.erb
Normal file
|
@ -0,0 +1,28 @@
|
|||
export const emoji = <%= Emoji.standard.map(&:name).flatten.inspect %>;
|
||||
export const aliases = <%= Emoji.aliases.inspect.gsub("=>", ":") %>;
|
||||
export const translations = {
|
||||
':)' : 'slight_smile',
|
||||
':-)' : 'slight_smile',
|
||||
':(' : 'frowning',
|
||||
':-(' : 'frowning',
|
||||
';)' : 'wink',
|
||||
';-)' : 'wink',
|
||||
':\'(' : 'cry',
|
||||
':\'-(': 'cry',
|
||||
':-\'(': 'cry',
|
||||
':p' : 'stuck_out_tongue',
|
||||
':P' : 'stuck_out_tongue',
|
||||
':-P' : 'stuck_out_tongue',
|
||||
':O' : 'open_mouth',
|
||||
':-O' : 'open_mouth',
|
||||
':D' : 'smiley',
|
||||
':-D' : 'smiley',
|
||||
':|' : 'expressionless',
|
||||
':-|' : 'expressionless',
|
||||
':/' : 'confused',
|
||||
'8-)' : 'sunglasses',
|
||||
";P" : 'stuck_out_tongue_winking_eye',
|
||||
";-P" : 'stuck_out_tongue_winking_eye',
|
||||
":$" : 'blush',
|
||||
":-$" : 'blush'
|
||||
};
|
|
@ -1,334 +1,56 @@
|
|||
/*eslint no-bitwise:0 */
|
||||
import guid from 'pretty-text/guid';
|
||||
import { default as WhiteLister, whiteListFeature } from 'pretty-text/white-lister';
|
||||
import { escape } from 'pretty-text/sanitizer';
|
||||
|
||||
/**
|
||||
|
||||
Discourse uses the Markdown.js as its main parser. `Discourse.Dialect` is the framework
|
||||
for extending it with additional formatting.
|
||||
|
||||
**/
|
||||
var parser = window.BetterMarkdown,
|
||||
MD = parser.Markdown,
|
||||
DialectHelpers = parser.DialectHelpers,
|
||||
dialect = MD.dialects.Discourse = DialectHelpers.subclassDialect( MD.dialects.Gruber ),
|
||||
initialized = false,
|
||||
emitters = [],
|
||||
hoisted,
|
||||
preProcessors = [],
|
||||
escape = Discourse.Utilities.escapeExpression;
|
||||
hoisted;
|
||||
|
||||
/**
|
||||
Initialize our dialects for processing.
|
||||
let currentOpts;
|
||||
|
||||
@method initializeDialects
|
||||
**/
|
||||
function initializeDialects() {
|
||||
MD.buildBlockOrder(dialect.block);
|
||||
var index = dialect.block.__order__.indexOf("code");
|
||||
if (index > -1) {
|
||||
dialect.block.__order__.splice(index, 1);
|
||||
dialect.block.__order__.unshift("code");
|
||||
}
|
||||
MD.buildInlinePatterns(dialect.inline);
|
||||
initialized = true;
|
||||
const emitters = [];
|
||||
const preProcessors = [];
|
||||
const parseNodes = [];
|
||||
|
||||
function findEndPos(text, start, stop, args, offset) {
|
||||
let endPos, nextStart;
|
||||
do {
|
||||
endPos = text.indexOf(stop, offset);
|
||||
if (endPos === -1) { return -1; }
|
||||
nextStart = text.indexOf(start, offset);
|
||||
offset = endPos + stop.length;
|
||||
} while (nextStart !== -1 && nextStart < endPos);
|
||||
return endPos;
|
||||
}
|
||||
|
||||
/**
|
||||
Process the text nodes in the JsonML tree, calling any emitters that have
|
||||
been added.
|
||||
|
||||
@method processTextNodes
|
||||
@param {Array} node the JsonML tree
|
||||
@param {Object} event the parse node event data
|
||||
@param {Function} emitter the function to call on the text node
|
||||
**/
|
||||
function processTextNodes(node, event, emitter) {
|
||||
if (node.length < 2) { return; }
|
||||
|
||||
if (node[0] === '__RAW') {
|
||||
var hash = Discourse.Dialect.guid();
|
||||
hoisted[hash] = node[1];
|
||||
node[1] = hash;
|
||||
return;
|
||||
class DialectHelper {
|
||||
constructor() {
|
||||
this._dialect = MD.dialects.Discourse = DialectHelpers.subclassDialect(MD.dialects.Gruber);
|
||||
this._setup = false;
|
||||
}
|
||||
|
||||
for (var j=1; j<node.length; j++) {
|
||||
var textContent = node[j];
|
||||
if (typeof textContent === "string") {
|
||||
var result = emitter(textContent, event);
|
||||
if (result) {
|
||||
if (result instanceof Array) {
|
||||
node.splice.apply(node, [j, 1].concat(result));
|
||||
} else {
|
||||
node[j] = result;
|
||||
}
|
||||
} else {
|
||||
node[j] = textContent;
|
||||
}
|
||||
|
||||
}
|
||||
escape(str) {
|
||||
return escape(str);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Parse a JSON ML tree, using registered handlers to adjust it if necessary.
|
||||
|
||||
@method parseTree
|
||||
@param {Array} tree the JsonML tree to parse
|
||||
@param {Array} path the path of ancestors to the current node in the tree. Can be used for matching.
|
||||
@param {Object} insideCounts counts what tags we're inside
|
||||
@returns {Array} the parsed tree
|
||||
**/
|
||||
function parseTree(tree, path, insideCounts) {
|
||||
|
||||
if (tree instanceof Array) {
|
||||
var event = {node: tree, path: path, dialect: dialect, insideCounts: insideCounts || {}};
|
||||
Discourse.Dialect.trigger('parseNode', event);
|
||||
|
||||
for (var j=0; j<emitters.length; j++) {
|
||||
processTextNodes(tree, event, emitters[j]);
|
||||
}
|
||||
|
||||
path = path || [];
|
||||
insideCounts = insideCounts || {};
|
||||
|
||||
path.push(tree);
|
||||
|
||||
for (var i=1; i<tree.length; i++) {
|
||||
var n = tree[i],
|
||||
tagName = n[0];
|
||||
|
||||
insideCounts[tagName] = (insideCounts[tagName] || 0) + 1;
|
||||
|
||||
if (n && n.length === 2 && n[0] === "p" && /^<!--([\s\S]*)-->$/.exec(n[1])) {
|
||||
// Remove paragraphs around comment-only nodes.
|
||||
tree[i] = n[1];
|
||||
} else {
|
||||
parseTree(n, path, insideCounts);
|
||||
}
|
||||
|
||||
insideCounts[tagName] = insideCounts[tagName] - 1;
|
||||
}
|
||||
|
||||
// If raw nodes are in paragraphs, pull them up
|
||||
if (tree.length === 2 && tree[0] === 'p' && tree[1] instanceof Array && tree[1][0] === "__RAW") {
|
||||
var text = tree[1][1];
|
||||
tree[0] = "__RAW";
|
||||
tree[1] = text;
|
||||
}
|
||||
|
||||
path.pop();
|
||||
getOptions() {
|
||||
return currentOpts;
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
Returns true if there's an invalid word boundary for a match.
|
||||
|
||||
@method invalidBoundary
|
||||
@param {Object} args our arguments, including whether we care about boundaries
|
||||
@param {Array} prev the previous content, if exists
|
||||
@returns {Boolean} whether there is an invalid word boundary
|
||||
**/
|
||||
function invalidBoundary(args, prev) {
|
||||
if (!(args.wordBoundary || args.spaceBoundary || args.spaceOrTagBoundary)) { return false; }
|
||||
|
||||
var last = prev[prev.length - 1];
|
||||
if (typeof last !== "string") { return false; }
|
||||
|
||||
if (args.wordBoundary && (!last.match(/\W$/))) { return true; }
|
||||
if (args.spaceBoundary && (!last.match(/\s$/))) { return true; }
|
||||
if (args.spaceOrTagBoundary && (!last.match(/(\s|\>)$/))) { return true; }
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the number of (terminated) lines in a string.
|
||||
|
||||
@method countLines
|
||||
@param {string} str the string.
|
||||
@returns {Integer} number of terminated lines in str
|
||||
**/
|
||||
function countLines(str) {
|
||||
var index = -1, count = 0;
|
||||
while ((index = str.indexOf("\n", index + 1)) !== -1) { count++; }
|
||||
return count;
|
||||
}
|
||||
|
||||
function hoister(t, target, replacement) {
|
||||
var regexp = new RegExp(target.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), "g");
|
||||
if (t.match(regexp)) {
|
||||
var hash = Discourse.Dialect.guid();
|
||||
t = t.replace(regexp, hash);
|
||||
hoisted[hash] = replacement;
|
||||
registerInlineFeature(featureName, start, fn) {
|
||||
this._dialect.inline[start] = function() {
|
||||
if (!currentOpts.features[featureName]) { return; }
|
||||
return fn.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
function outdent(t) {
|
||||
return t.replace(/^([ ]{4}|\t)/gm, "");
|
||||
}
|
||||
|
||||
function removeEmptyLines(t) {
|
||||
return t.replace(/^\n+/, "")
|
||||
.replace(/\s+$/, "");
|
||||
}
|
||||
|
||||
function hideBackslashEscapedCharacters(t) {
|
||||
return t.replace(/\\\\/g, "\u1E800")
|
||||
.replace(/\\`/g, "\u1E8001");
|
||||
}
|
||||
|
||||
function showBackslashEscapedCharacters(t) {
|
||||
return t.replace(/\u1E8001/g, "\\`")
|
||||
.replace(/\u1E800/g, "\\\\");
|
||||
}
|
||||
|
||||
function hoistCodeBlocksAndSpans(text) {
|
||||
// replace all "\`" with a single character
|
||||
text = hideBackslashEscapedCharacters(text);
|
||||
|
||||
// /!\ the order is important /!\
|
||||
|
||||
// fenced code blocks (AKA GitHub code blocks)
|
||||
text = text.replace(/(^\n*|\n)```([a-z0-9\-]*)\n([\s\S]*?)\n```/g, function(_, before, language, content) {
|
||||
var hash = Discourse.Dialect.guid();
|
||||
hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
|
||||
return before + "```" + language + "\n" + hash + "\n```";
|
||||
});
|
||||
|
||||
// markdown code blocks
|
||||
text = text.replace(/(^\n*|\n\n)((?:(?:[ ]{4}|\t).*\n*)+)/g, function(match, before, content, index) {
|
||||
// make sure we aren't in a list
|
||||
var previousLine = text.slice(0, index).trim().match(/.*$/);
|
||||
if (previousLine && previousLine[0].length) {
|
||||
previousLine = previousLine[0].trim();
|
||||
if (/^(?:\*|\+|-|\d+\.)\s+/.test(previousLine)) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
// we can safely hoist the code block
|
||||
var hash = Discourse.Dialect.guid();
|
||||
hoisted[hash] = escape(outdent(showBackslashEscapedCharacters(removeEmptyLines(content))));
|
||||
return before + " " + hash + "\n";
|
||||
});
|
||||
|
||||
// <pre>...</pre> code blocks
|
||||
text = text.replace(/(\s|^)<pre>([\s\S]*?)<\/pre>/ig, function(_, before, content) {
|
||||
var hash = Discourse.Dialect.guid();
|
||||
hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
|
||||
return before + "<pre>" + hash + "</pre>";
|
||||
});
|
||||
|
||||
// code spans (double & single `)
|
||||
["``", "`"].forEach(function(delimiter) {
|
||||
var regexp = new RegExp("(^|[^`])" + delimiter + "([^`\\n]+?)" + delimiter + "([^`]|$)", "g");
|
||||
text = text.replace(regexp, function(_, before, content, after) {
|
||||
var hash = Discourse.Dialect.guid();
|
||||
hoisted[hash] = escape(showBackslashEscapedCharacters(content.trim()));
|
||||
return before + delimiter + hash + delimiter + after;
|
||||
addPreProcessorFeature(featureName, fn) {
|
||||
preProcessors.push(raw => {
|
||||
if (!currentOpts.features[featureName]) { return raw; }
|
||||
return fn(raw, hoister);
|
||||
});
|
||||
});
|
||||
|
||||
// replace back all weird character with "\`"
|
||||
return showBackslashEscapedCharacters(text);
|
||||
}
|
||||
|
||||
/**
|
||||
An object used for rendering our dialects.
|
||||
|
||||
@class Dialect
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.Dialect = {
|
||||
|
||||
// http://stackoverflow.com/a/8809472/17174
|
||||
guid: function(){
|
||||
var d = new Date().getTime();
|
||||
if(window.performance && typeof window.performance.now === "function"){
|
||||
d += performance.now(); //use high-precision timer if available
|
||||
}
|
||||
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
var r = (d + Math.random() * 16) % 16 | 0;
|
||||
d = Math.floor(d/16);
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||
});
|
||||
return uuid;
|
||||
},
|
||||
|
||||
/**
|
||||
Cook text using the dialects.
|
||||
|
||||
@method cook
|
||||
@param {String} text the raw text to cook
|
||||
@param {Object} opts hash of options
|
||||
@returns {String} the cooked text
|
||||
**/
|
||||
cook: function(text, opts) {
|
||||
if (!initialized) { initializeDialects(); }
|
||||
|
||||
dialect.options = opts;
|
||||
|
||||
// Helps us hoist out HTML
|
||||
hoisted = {};
|
||||
|
||||
// pre-hoist all code-blocks/spans
|
||||
text = hoistCodeBlocksAndSpans(text);
|
||||
|
||||
// pre-processors
|
||||
preProcessors.forEach(function(p) {
|
||||
text = p(text, hoister);
|
||||
});
|
||||
|
||||
var tree = parser.toHTMLTree(text, 'Discourse'),
|
||||
result = parser.renderJsonML(parseTree(tree));
|
||||
|
||||
if (opts.sanitize) {
|
||||
result = Discourse.Markdown.sanitize(result);
|
||||
} else if (opts.sanitizerFunction) {
|
||||
result = opts.sanitizerFunction(result);
|
||||
}
|
||||
|
||||
// If we hoisted out anything, put it back
|
||||
var keys = Object.keys(hoisted);
|
||||
if (keys.length) {
|
||||
var found = true;
|
||||
|
||||
var unhoist = function(key) {
|
||||
result = result.replace(new RegExp(key, "g"), function() {
|
||||
found = true;
|
||||
return hoisted[key];
|
||||
});
|
||||
};
|
||||
|
||||
while(found) {
|
||||
found = false;
|
||||
keys.forEach(unhoist);
|
||||
}
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
},
|
||||
|
||||
/**
|
||||
Adds a text pre-processor. Use only if necessary, as a dialect
|
||||
that emits JsonML is much better if possible.
|
||||
**/
|
||||
addPreProcessor: function(preProc) {
|
||||
preProcessors.push(preProc);
|
||||
},
|
||||
|
||||
/**
|
||||
Registers an inline replacer function
|
||||
|
||||
@method registerInline
|
||||
@param {String} start The token the replacement begins with
|
||||
@param {Function} fn The replacing function
|
||||
**/
|
||||
registerInline: function(start, fn) {
|
||||
dialect.inline[start] = fn;
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
The simplest kind of replacement possible. Replace a stirng token with JsonML.
|
||||
|
@ -336,21 +58,35 @@ Discourse.Dialect = {
|
|||
For example to replace all occurrances of :) with a smile image:
|
||||
|
||||
```javascript
|
||||
Discourse.Dialect.inlineReplace(':)', function (text) {
|
||||
return ['img', {src: '/images/smile.png'}];
|
||||
});
|
||||
|
||||
helper.inlineReplace(':)', text => ['img', {src: '/images/smile.png'}]);
|
||||
```
|
||||
|
||||
@method inlineReplace
|
||||
@param {String} token The token we want to replace
|
||||
@param {Function} emitter A function that emits the JsonML for the replacement.
|
||||
**/
|
||||
inlineReplace: function(token, emitter) {
|
||||
this.registerInline(token, function(text, match, prev) {
|
||||
inlineReplaceFeature(featureName, token, emitter) {
|
||||
this.registerInline(token, (text, match, prev) => {
|
||||
if (!currentOpts.features[featureName]) { return; }
|
||||
return [token.length, emitter.call(this, token, match, prev)];
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
After the parser has been executed, change the contents of a HTML tag.
|
||||
|
||||
Let's say you want to replace the contents of all code tags to prepend
|
||||
"EVIL TROUT HACKED YOUR CODE!":
|
||||
|
||||
```javascript
|
||||
helper.postProcessTag('code', contents => `EVIL TROUT HACKED YOUR CODE!\n\n${contents}`);
|
||||
```
|
||||
**/
|
||||
postProcessTagFeature(featureName, tag, emitter) {
|
||||
this.onParseNode(event => {
|
||||
if (!currentOpts.features[featureName]) { return; }
|
||||
const node = event.node;
|
||||
if (node[0] === tag) {
|
||||
node[node.length-1] = emitter(node[node.length-1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
Matches inline using a regular expression. The emitter function is passed
|
||||
|
@ -359,40 +95,33 @@ Discourse.Dialect = {
|
|||
For example, this auto links URLs:
|
||||
|
||||
```javascript
|
||||
Discourse.Dialect.inlineRegexp({
|
||||
helper.inlineRegexp({
|
||||
matcher: /((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/gm,
|
||||
spaceBoundary: true,
|
||||
start: 'http',
|
||||
|
||||
emitter: function(matches) {
|
||||
var url = matches[1];
|
||||
emitter(matches) {
|
||||
const url = matches[1];
|
||||
return ['a', {href: url}, url];
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@method inlineRegexp
|
||||
@param {Object} args Our replacement options
|
||||
@param {Function} [opts.emitter] The function that will be called with the contents and regular expresison match and returns JsonML.
|
||||
@param {String} [opts.start] The starting token we want to find
|
||||
@param {String} [opts.matcher] The regular expression to match
|
||||
@param {Boolean} [opts.wordBoundary] If true, the match must be on a word boundary
|
||||
@param {Boolean} [opts.spaceBoundary] If true, the match must be on a space boundary
|
||||
**/
|
||||
inlineRegexp: function(args) {
|
||||
inlineRegexpFeature(featureName, args) {
|
||||
this.registerInline(args.start, function(text, match, prev) {
|
||||
if (!currentOpts.features[featureName]) { return; }
|
||||
if (invalidBoundary(args, prev)) { return; }
|
||||
|
||||
args.matcher.lastIndex = 0;
|
||||
var m = args.matcher.exec(text);
|
||||
const m = args.matcher.exec(text);
|
||||
if (m) {
|
||||
var result = args.emitter.call(this, m);
|
||||
const result = args.emitter.call(this, m);
|
||||
if (result) {
|
||||
return [m[0].length, result];
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
Handles inline replacements surrounded by tokens.
|
||||
|
@ -402,36 +131,25 @@ Discourse.Dialect = {
|
|||
recursive markup.
|
||||
|
||||
```javascript
|
||||
|
||||
Discourse.Dialect.inlineBetween({
|
||||
helper.inlineBetween({
|
||||
between: '**',
|
||||
wordBoundary: true.
|
||||
emitter: function(contents) {
|
||||
emitter(contents) {
|
||||
return ['strong'].concat(contents);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@method inlineBetween
|
||||
@param {Object} args Our replacement options
|
||||
@param {Function} [opts.emitter] The function that will be called with the contents and returns JsonML.
|
||||
@param {String} [opts.start] The starting token we want to find
|
||||
@param {String} [opts.stop] The ending token we want to find
|
||||
@param {String} [opts.between] A shortcut for when the `start` and `stop` are the same.
|
||||
@param {Boolean} [opts.rawContents] If true, the contents between the tokens will not be parsed.
|
||||
@param {Boolean} [opts.wordBoundary] If true, the match must be on a word boundary
|
||||
@param {Boolean} [opts.spaceBoundary] If true, the match must be on a space boundary
|
||||
**/
|
||||
inlineBetween: function(args) {
|
||||
var start = args.start || args.between,
|
||||
stop = args.stop || args.between,
|
||||
startLength = start.length,
|
||||
self = this;
|
||||
inlineBetweenFeature(featureName, args) {
|
||||
const start = args.start || args.between;
|
||||
const stop = args.stop || args.between;
|
||||
const startLength = start.length;
|
||||
|
||||
this.registerInline(start, function(text, match, prev) {
|
||||
if (!currentOpts.features[featureName]) { return; }
|
||||
if (invalidBoundary(args, prev)) { return; }
|
||||
|
||||
var endPos = self.findEndPos(text, start, stop, args, startLength);
|
||||
const endPos = findEndPos(text, start, stop, args, startLength);
|
||||
if (endPos === -1) { return; }
|
||||
var between = text.slice(startLength, endPos);
|
||||
|
||||
|
@ -445,30 +163,7 @@ Discourse.Dialect = {
|
|||
return [endPos+stop.length, contents];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
findEndPos: function(text, start, stop, args, offset) {
|
||||
var endPos, nextStart;
|
||||
do {
|
||||
endPos = text.indexOf(stop, offset);
|
||||
if (endPos === -1) { return -1; }
|
||||
nextStart = text.indexOf(start, offset);
|
||||
offset = endPos + stop.length;
|
||||
} while (nextStart !== -1 && nextStart < endPos);
|
||||
return endPos;
|
||||
},
|
||||
|
||||
/**
|
||||
Registers a block for processing. This is more complicated than using one of
|
||||
the other helpers such as `replaceBlock` so consider using them first!
|
||||
|
||||
@method registerBlock
|
||||
@param {String} name the name of the block handler
|
||||
@param {Function} handler the handler
|
||||
**/
|
||||
registerBlock: function(name, handler) {
|
||||
dialect.block[name] = handler;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
Replaces a block of text between a start and stop. As opposed to inline, these
|
||||
|
@ -478,47 +173,38 @@ Discourse.Dialect = {
|
|||
puts them inside a `pre` tag:
|
||||
|
||||
```javascript
|
||||
Discourse.Dialect.replaceBlock({
|
||||
helper.replaceBlock({
|
||||
start: /(\[code\])([\s\S]*)/igm,
|
||||
stop: '[/code]',
|
||||
rawContents: true,
|
||||
|
||||
emitter: function(blockContents) {
|
||||
emitter(blockContents) {
|
||||
return ['p', ['pre'].concat(blockContents)];
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@method replaceBlock
|
||||
@param {Object} args Our replacement options
|
||||
@param {RegExp} [args.start] The starting regexp we want to find
|
||||
@param {String} [args.stop] The ending token we want to find
|
||||
@param {Boolean} [args.rawContents] True to skip recursive processing
|
||||
@param {Function} [args.emitter] The emitting function to transform the contents of the block into jsonML
|
||||
|
||||
**/
|
||||
replaceBlock: function(args) {
|
||||
var fn = function(block, next) {
|
||||
replaceBlockFeature(featureName, args) {
|
||||
function blockFunc(block, next) {
|
||||
if (!currentOpts.features[featureName]) { return; }
|
||||
|
||||
var linebreaks = dialect.options.traditional_markdown_linebreaks ||
|
||||
Discourse.SiteSettings.traditional_markdown_linebreaks;
|
||||
const linebreaks = currentOpts.traditionalMarkdownLinebreaks;
|
||||
if (linebreaks && args.skipIfTradtionalLinebreaks) { return; }
|
||||
|
||||
args.start.lastIndex = 0;
|
||||
var result = [], match = (args.start).exec(block);
|
||||
const result = [];
|
||||
const match = (args.start).exec(block);
|
||||
if (!match) { return; }
|
||||
|
||||
var lastChance = function() {
|
||||
return !next.some(function(blk) { return blk.match(args.stop); });
|
||||
};
|
||||
const lastChance = () => !next.some(blk => blk.match(args.stop));
|
||||
|
||||
// shave off start tag and leading text, if any.
|
||||
var pos = args.start.lastIndex - match[0].length,
|
||||
leading = block.slice(0, pos),
|
||||
trailing = match[2] ? match[2].replace(/^\n*/, "") : "";
|
||||
const pos = args.start.lastIndex - match[0].length;
|
||||
const leading = block.slice(0, pos);
|
||||
const trailing = match[2] ? match[2].replace(/^\n*/, "") : "";
|
||||
|
||||
if(args.withoutLeading && args.withoutLeading.test(leading)) {
|
||||
//The other leading block should be processed first! eg a code block wrapped around a code block.
|
||||
// The other leading block should be processed first! eg a code block wrapped around a code block.
|
||||
if (args.withoutLeading && args.withoutLeading.test(leading)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -537,24 +223,31 @@ Discourse.Dialect = {
|
|||
}
|
||||
|
||||
// go through the available blocks to find the matching stop tag.
|
||||
var contentBlocks = [], nesting = 0, actualEndPos = -1, currentBlock;
|
||||
const contentBlocks = [];
|
||||
let nesting = 0;
|
||||
let actualEndPos = -1;
|
||||
let currentBlock;
|
||||
|
||||
blockloop:
|
||||
while (currentBlock = next.shift()) {
|
||||
|
||||
// collect all the start and stop tags in the current block
|
||||
args.start.lastIndex = 0;
|
||||
var startPos = [], m;
|
||||
const startPos = [];
|
||||
let m;
|
||||
while (m = (args.start).exec(currentBlock)) {
|
||||
startPos.push(args.start.lastIndex - m[0].length);
|
||||
args.start.lastIndex = args.start.lastIndex - (m[2] ? m[2].length : 0);
|
||||
}
|
||||
args.stop.lastIndex = 0;
|
||||
var endPos = [];
|
||||
const endPos = [];
|
||||
while (m = (args.stop).exec(currentBlock)) {
|
||||
endPos.push(args.stop.lastIndex - m[0].length);
|
||||
}
|
||||
|
||||
// go through the available end tags:
|
||||
var ep = 0, sp = 0; // array indices
|
||||
let ep = 0;
|
||||
let sp = 0;
|
||||
while (ep < endPos.length) {
|
||||
if (sp < startPos.length && startPos[sp] < endPos[ep]) {
|
||||
// there's an end tag, but there's also another start tag first. we need to go deeper.
|
||||
|
@ -585,23 +278,23 @@ Discourse.Dialect = {
|
|||
contentBlocks.push(currentBlock);
|
||||
}
|
||||
|
||||
var stopLen = currentBlock.match(args.stop)[0].length,
|
||||
before = currentBlock.slice(0, actualEndPos).replace(/\n*$/, ""),
|
||||
after = currentBlock.slice(actualEndPos + stopLen).replace(/^\n*/, "");
|
||||
const stopLen = currentBlock.match(args.stop)[0].length;
|
||||
const before = currentBlock.slice(0, actualEndPos).replace(/\n*$/, "");
|
||||
const after = currentBlock.slice(actualEndPos + stopLen).replace(/^\n*/, "");
|
||||
if (before.length > 0) contentBlocks.push(MD.mk_block(before, "", currentBlock.lineNumber));
|
||||
if (after.length > 0) next.unshift(MD.mk_block(after, currentBlock.trailing, currentBlock.lineNumber + countLines(before)));
|
||||
|
||||
var emitterResult = args.emitter.call(this, contentBlocks, match, dialect.options);
|
||||
const emitterResult = args.emitter.call(this, contentBlocks, match);
|
||||
if (emitterResult) { result.push(emitterResult); }
|
||||
return result;
|
||||
};
|
||||
|
||||
if (args.priority) {
|
||||
fn.priority = args.priority;
|
||||
blockFunc.priority = args.priority;
|
||||
}
|
||||
|
||||
this.registerBlock(args.start.toString(), fn);
|
||||
},
|
||||
this.registerBlock(args.start.toString(), blockFunc);
|
||||
}
|
||||
|
||||
/**
|
||||
After the parser has been executed, post process any text nodes in the HTML document.
|
||||
|
@ -615,47 +308,279 @@ Discourse.Dialect = {
|
|||
For example, to convert all text to upper case:
|
||||
|
||||
```javascript
|
||||
|
||||
Discourse.Dialect.postProcessText(function (text) {
|
||||
helper.postProcessText(function (text) {
|
||||
return text.toUpperCase();
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
@method postProcessText
|
||||
@param {Function} emitter The function to call with the text. It returns JsonML to modify the tree.
|
||||
**/
|
||||
postProcessText: function(emitter) {
|
||||
emitters.push(emitter);
|
||||
},
|
||||
|
||||
/**
|
||||
After the parser has been executed, change the contents of a HTML tag.
|
||||
|
||||
Let's say you want to replace the contents of all code tags to prepend
|
||||
"EVIL TROUT HACKED YOUR CODE!":
|
||||
|
||||
```javascript
|
||||
Discourse.Dialect.postProcessTag('code', function (contents) {
|
||||
return "EVIL TROUT HACKED YOUR CODE!\n\n" + contents;
|
||||
});
|
||||
```
|
||||
|
||||
@method postProcessTag
|
||||
@param {String} tag The HTML tag you want to match on
|
||||
@param {Function} emitter The function to call with the text. It returns JsonML to modify the tree.
|
||||
**/
|
||||
postProcessTag: function(tag, emitter) {
|
||||
Discourse.Dialect.on('parseNode', function (event) {
|
||||
var node = event.node;
|
||||
if (node[0] === tag) {
|
||||
node[node.length-1] = emitter(node[node.length-1]);
|
||||
}
|
||||
postProcessTextFeature(featureName, fn) {
|
||||
emitters.push(function () {
|
||||
if (!currentOpts.features[featureName]) { return; }
|
||||
return fn.apply(this, arguments);
|
||||
});
|
||||
}
|
||||
|
||||
onParseNodeFeature(featureName, fn) {
|
||||
parseNodes.push(function () {
|
||||
if (!currentOpts.features[featureName]) { return; }
|
||||
return fn.apply(this, arguments);
|
||||
});
|
||||
}
|
||||
|
||||
registerBlockFeature(featureName, name, fn) {
|
||||
const blockFunc = function() {
|
||||
if (!currentOpts.features[featureName]) { return; }
|
||||
return fn.apply(this, arguments);
|
||||
};
|
||||
|
||||
blockFunc.priority = fn.priority;
|
||||
this._dialect.block[name] = blockFunc;
|
||||
}
|
||||
|
||||
applyFeature(featureName, module) {
|
||||
helper.registerInline = (code, fn) => helper.registerInlineFeature(featureName, code, fn);
|
||||
helper.replaceBlock = args => helper.replaceBlockFeature(featureName, args);
|
||||
helper.addPreProcessor = fn => helper.addPreProcessorFeature(featureName, fn);
|
||||
helper.inlineReplace = (token, emitter) => helper.inlineReplaceFeature(featureName, token, emitter);
|
||||
helper.postProcessTag = (token, emitter) => helper.postProcessTagFeature(featureName, token, emitter);
|
||||
helper.inlineRegexp = args => helper.inlineRegexpFeature(featureName, args);
|
||||
helper.inlineBetween = args => helper.inlineBetweenFeature(featureName, args);
|
||||
helper.postProcessText = fn => helper.postProcessTextFeature(featureName, fn);
|
||||
helper.onParseNode = fn => helper.onParseNodeFeature(featureName, fn);
|
||||
helper.registerBlock = (name, fn) => helper.registerBlockFeature(featureName, name, fn);
|
||||
|
||||
module.setup(this);
|
||||
}
|
||||
|
||||
setup() {
|
||||
if (this._setup) { return; }
|
||||
this._setup = true;
|
||||
|
||||
Object.keys(require._eak_seen).forEach(entry => {
|
||||
if (entry.indexOf('discourse-markdown') !== -1) {
|
||||
const module = require(entry);
|
||||
if (module && module.setup) {
|
||||
const featureName = entry.split('/').reverse()[0];
|
||||
helper.whiteList = info => whiteListFeature(featureName, info);
|
||||
|
||||
this.applyFeature(featureName, module);
|
||||
helper.whiteList = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
MD.buildBlockOrder(this._dialect.block);
|
||||
var index = this._dialect.block.__order__.indexOf("code");
|
||||
if (index > -1) {
|
||||
this._dialect.block.__order__.splice(index, 1);
|
||||
this._dialect.block.__order__.unshift("code");
|
||||
}
|
||||
MD.buildInlinePatterns(this._dialect.inline);
|
||||
}
|
||||
};
|
||||
|
||||
RSVP.EventTarget.mixin(Discourse.Dialect);
|
||||
const helper = new DialectHelper();
|
||||
|
||||
export function cook(raw, opts) {
|
||||
currentOpts = opts;
|
||||
|
||||
hoisted = {};
|
||||
raw = hoistCodeBlocksAndSpans(raw);
|
||||
|
||||
preProcessors.forEach(p => raw = p(raw));
|
||||
|
||||
const whiteLister = new WhiteLister(opts.features);
|
||||
|
||||
const tree = parser.toHTMLTree(raw, 'Discourse');
|
||||
let result = opts.sanitizer(parser.renderJsonML(parseTree(tree, opts)), whiteLister);
|
||||
|
||||
// If we hoisted out anything, put it back
|
||||
const keys = Object.keys(hoisted);
|
||||
if (keys.length) {
|
||||
let found = true;
|
||||
|
||||
function unhoist(key) {
|
||||
result = result.replace(new RegExp(key, "g"), function() {
|
||||
found = true;
|
||||
return hoisted[key];
|
||||
});
|
||||
};
|
||||
|
||||
while (found) {
|
||||
found = false;
|
||||
keys.forEach(unhoist);
|
||||
}
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
export function setup() {
|
||||
helper.setup();
|
||||
}
|
||||
|
||||
function processTextNodes(node, event, emitter) {
|
||||
if (node.length < 2) { return; }
|
||||
|
||||
if (node[0] === '__RAW') {
|
||||
const hash = guid();
|
||||
hoisted[hash] = node[1];
|
||||
node[1] = hash;
|
||||
return;
|
||||
}
|
||||
|
||||
for (var j=1; j<node.length; j++) {
|
||||
var textContent = node[j];
|
||||
if (typeof textContent === "string") {
|
||||
var result = emitter(textContent, event);
|
||||
if (result) {
|
||||
if (result instanceof Array) {
|
||||
node.splice.apply(node, [j, 1].concat(result));
|
||||
} else {
|
||||
node[j] = result;
|
||||
}
|
||||
} else {
|
||||
node[j] = textContent;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a JSON ML tree, using registered handlers to adjust it if necessary.
|
||||
function parseTree(tree, options, path, insideCounts) {
|
||||
|
||||
if (tree instanceof Array) {
|
||||
const event = {node: tree, options, path, insideCounts: insideCounts || {}};
|
||||
parseNodes.forEach(fn => fn(event));
|
||||
|
||||
for (var j=0; j<emitters.length; j++) {
|
||||
processTextNodes(tree, event, emitters[j]);
|
||||
}
|
||||
|
||||
path = path || [];
|
||||
insideCounts = insideCounts || {};
|
||||
|
||||
path.push(tree);
|
||||
|
||||
for (var i=1; i<tree.length; i++) {
|
||||
var n = tree[i],
|
||||
tagName = n[0];
|
||||
|
||||
insideCounts[tagName] = (insideCounts[tagName] || 0) + 1;
|
||||
|
||||
if (n && n.length === 2 && n[0] === "p" && /^<!--([\s\S]*)-->$/.exec(n[1])) {
|
||||
// Remove paragraphs around comment-only nodes.
|
||||
tree[i] = n[1];
|
||||
} else {
|
||||
parseTree(n, options, path, insideCounts);
|
||||
}
|
||||
|
||||
insideCounts[tagName] = insideCounts[tagName] - 1;
|
||||
}
|
||||
|
||||
// If raw nodes are in paragraphs, pull them up
|
||||
if (tree.length === 2 && tree[0] === 'p' && tree[1] instanceof Array && tree[1][0] === "__RAW") {
|
||||
var text = tree[1][1];
|
||||
tree[0] = "__RAW";
|
||||
tree[1] = text;
|
||||
}
|
||||
|
||||
path.pop();
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
// Returns true if there's an invalid word boundary for a match.
|
||||
function invalidBoundary(args, prev) {
|
||||
if (!(args.wordBoundary || args.spaceBoundary || args.spaceOrTagBoundary)) { return false; }
|
||||
|
||||
var last = prev[prev.length - 1];
|
||||
if (typeof last !== "string") { return false; }
|
||||
|
||||
if (args.wordBoundary && (!last.match(/\W$/))) { return true; }
|
||||
if (args.spaceBoundary && (!last.match(/\s$/))) { return true; }
|
||||
if (args.spaceOrTagBoundary && (!last.match(/(\s|\>)$/))) { return true; }
|
||||
}
|
||||
|
||||
function countLines(str) {
|
||||
let index = -1, count = 0;
|
||||
while ((index = str.indexOf("\n", index + 1)) !== -1) { count++; }
|
||||
return count;
|
||||
}
|
||||
|
||||
function hoister(t, target, replacement) {
|
||||
const regexp = new RegExp(target.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), "g");
|
||||
if (t.match(regexp)) {
|
||||
const hash = guid();
|
||||
t = t.replace(regexp, hash);
|
||||
hoisted[hash] = replacement;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
function outdent(t) {
|
||||
return t.replace(/^([ ]{4}|\t)/gm, "");
|
||||
}
|
||||
|
||||
function removeEmptyLines(t) {
|
||||
return t.replace(/^\n+/, "").replace(/\s+$/, "");
|
||||
}
|
||||
|
||||
function hideBackslashEscapedCharacters(t) {
|
||||
return t.replace(/\\\\/g, "\u1E800").replace(/\\`/g, "\u1E8001");
|
||||
}
|
||||
|
||||
function showBackslashEscapedCharacters(t) {
|
||||
return t.replace(/\u1E8001/g, "\\`").replace(/\u1E800/g, "\\\\");
|
||||
}
|
||||
|
||||
function hoistCodeBlocksAndSpans(text) {
|
||||
// replace all "\`" with a single character
|
||||
text = hideBackslashEscapedCharacters(text);
|
||||
|
||||
// /!\ the order is important /!\
|
||||
|
||||
// fenced code blocks (AKA GitHub code blocks)
|
||||
text = text.replace(/(^\n*|\n)```([a-z0-9\-]*)\n([\s\S]*?)\n```/g, function(_, before, language, content) {
|
||||
const hash = guid();
|
||||
hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
|
||||
return before + "```" + language + "\n" + hash + "\n```";
|
||||
});
|
||||
|
||||
// markdown code blocks
|
||||
text = text.replace(/(^\n*|\n\n)((?:(?:[ ]{4}|\t).*\n*)+)/g, function(match, before, content, index) {
|
||||
// make sure we aren't in a list
|
||||
var previousLine = text.slice(0, index).trim().match(/.*$/);
|
||||
if (previousLine && previousLine[0].length) {
|
||||
previousLine = previousLine[0].trim();
|
||||
if (/^(?:\*|\+|-|\d+\.)\s+/.test(previousLine)) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
// we can safely hoist the code block
|
||||
const hash = guid();
|
||||
hoisted[hash] = escape(outdent(showBackslashEscapedCharacters(removeEmptyLines(content))));
|
||||
return before + " " + hash + "\n";
|
||||
});
|
||||
|
||||
// <pre>...</pre> code blocks
|
||||
text = text.replace(/(\s|^)<pre>([\s\S]*?)<\/pre>/ig, function(_, before, content) {
|
||||
const hash = guid();
|
||||
hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
|
||||
return before + "<pre>" + hash + "</pre>";
|
||||
});
|
||||
|
||||
// code spans (double & single `)
|
||||
["``", "`"].forEach(function(delimiter) {
|
||||
var regexp = new RegExp("(^|[^`])" + delimiter + "([^`\\n]+?)" + delimiter + "([^`]|$)", "g");
|
||||
text = text.replace(regexp, function(_, before, content, after) {
|
||||
const hash = guid();
|
||||
hoisted[hash] = escape(showBackslashEscapedCharacters(content.trim()));
|
||||
return before + delimiter + hash + delimiter + after;
|
||||
});
|
||||
});
|
||||
|
||||
// replace back all weird character with "\`"
|
||||
return showBackslashEscapedCharacters(text);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// This addition handles auto linking of text. When included, it will parse out links and create
|
||||
// `<a href>`s for them.
|
||||
|
||||
const urlReplacerArgs = {
|
||||
matcher: /^((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/,
|
||||
spaceOrTagBoundary: true,
|
||||
|
||||
emitter(matches) {
|
||||
const url = matches[1];
|
||||
let href = url;
|
||||
|
||||
// Don't autolink a markdown link to something
|
||||
if (url.match(/\]\[\d$/)) { return; }
|
||||
|
||||
// If we improperly caught a markdown link abort
|
||||
if (url.match(/\(http/)) { return; }
|
||||
|
||||
if (url.match(/^www/)) { href = "http://" + url; }
|
||||
return ['a', { href }, url];
|
||||
}
|
||||
};
|
||||
|
||||
export function setup(helper) {
|
||||
helper.inlineRegexp(_.merge({start: 'http'}, urlReplacerArgs));
|
||||
helper.inlineRegexp(_.merge({start: 'www'}, urlReplacerArgs));
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
export function register(helper, codeName, args, emitter) {
|
||||
// Optional second param for args
|
||||
if (typeof args === "function") {
|
||||
emitter = args;
|
||||
args = {};
|
||||
}
|
||||
|
||||
helper.replaceBlock({
|
||||
start: new RegExp("\\[" + codeName + "(=[^\\[\\]]+)?\\]([\\s\\S]*)", "igm"),
|
||||
stop: new RegExp("\\[\\/" + codeName + "\\]", "igm"),
|
||||
emitter(blockContents, matches) {
|
||||
const options = helper.getOptions();
|
||||
while (blockContents.length && (typeof blockContents[0] === "string" || blockContents[0] instanceof String)) {
|
||||
blockContents[0] = String(blockContents[0]).replace(/^\s+/, '');
|
||||
if (!blockContents[0].length) {
|
||||
blockContents.shift();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let contents = [];
|
||||
if (blockContents.length) {
|
||||
const nextContents = blockContents.slice(1);
|
||||
blockContents = this.processBlock(blockContents[0], nextContents).concat(nextContents);
|
||||
|
||||
blockContents.forEach(bc => {
|
||||
if (typeof bc === "string" || bc instanceof String) {
|
||||
var processed = this.processInline(String(bc));
|
||||
if (processed.length) {
|
||||
contents.push(['p'].concat(processed));
|
||||
}
|
||||
} else {
|
||||
contents.push(bc);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!args.singlePara && contents.length === 1 && contents[0] instanceof Array && contents[0][0] === "para") {
|
||||
contents[0].shift();
|
||||
contents = contents[0];
|
||||
}
|
||||
const result = emitter(contents, matches[1] ? matches[1].replace(/^=|\"/g, '') : null, options);
|
||||
return args.noWrap ? result : ['p', result];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export function setup(helper) {
|
||||
|
||||
helper.whiteList(['span.bbcode-b', 'span.bbcode-i', 'span.bbcode-u', 'span.bbcode-s']);
|
||||
|
||||
function replaceBBCode(tag, emitter, opts) {
|
||||
const start = `[${tag}]`;
|
||||
const stop = `[/${tag}]`;
|
||||
|
||||
opts = opts || {};
|
||||
opts = _.merge(opts, { start, stop, emitter });
|
||||
helper.inlineBetween(opts);
|
||||
|
||||
opts = _.merge(opts, { start: start.toUpperCase(), stop: stop.toUpperCase(), emitter });
|
||||
helper.inlineBetween(opts);
|
||||
};
|
||||
|
||||
function rawBBCode(tag, emitter) {
|
||||
replaceBBCode(tag, emitter, { rawContents: true });
|
||||
}
|
||||
|
||||
function removeEmptyLines(contents) {
|
||||
const result = [];
|
||||
for (let i=0; i < contents.length; i++) {
|
||||
if (contents[i] !== "\n") { result.push(contents[i]); }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function replaceBBCodeParamsRaw(tag, emitter) {
|
||||
var opts = {
|
||||
rawContents: true,
|
||||
emitter(contents) {
|
||||
const m = /^([^\]]+)\]([\S\s]*)$/.exec(contents);
|
||||
if (m) { return emitter.call(this, m[1], m[2]); }
|
||||
}
|
||||
};
|
||||
|
||||
helper.inlineBetween(_.merge(opts, { start: "[" + tag + "=", stop: "[/" + tag + "]" }));
|
||||
|
||||
tag = tag.toUpperCase();
|
||||
helper.inlineBetween(_.merge(opts, { start: "[" + tag + "=", stop: "[/" + tag + "]" }));
|
||||
}
|
||||
|
||||
replaceBBCode('b', contents => ['span', {'class': 'bbcode-b'}].concat(contents));
|
||||
replaceBBCode('i', contents => ['span', {'class': 'bbcode-i'}].concat(contents));
|
||||
replaceBBCode('u', contents => ['span', {'class': 'bbcode-u'}].concat(contents));
|
||||
replaceBBCode('s', contents => ['span', {'class': 'bbcode-s'}].concat(contents));
|
||||
|
||||
replaceBBCode('ul', contents => ['ul'].concat(removeEmptyLines(contents)));
|
||||
replaceBBCode('ol', contents => ['ol'].concat(removeEmptyLines(contents)));
|
||||
replaceBBCode('li', contents => ['li'].concat(removeEmptyLines(contents)));
|
||||
|
||||
rawBBCode('img', href => ['img', {href}]);
|
||||
rawBBCode('email', contents => ['a', {href: "mailto:" + contents, 'data-bbcode': true}, contents]);
|
||||
|
||||
replaceBBCode('url', contents => {
|
||||
if (!Array.isArray(contents)) { return; }
|
||||
if (contents.length === 1 && contents[0][0] === 'a') {
|
||||
// single-line bbcode links shouldn't be oneboxed, so we mark this as a bbcode link.
|
||||
if (typeof contents[0][1] !== 'object') { contents[0].splice(1, 0, {}); }
|
||||
contents[0][1]['data-bbcode'] = true;
|
||||
}
|
||||
return ['concat'].concat(contents);
|
||||
});
|
||||
|
||||
replaceBBCodeParamsRaw('url', function(param, contents) {
|
||||
const url = param.replace(/(^")|("$)/g, '');
|
||||
return ['a', {'href': url}].concat(this.processInline(contents));
|
||||
});
|
||||
|
||||
replaceBBCodeParamsRaw("email", function(param, contents) {
|
||||
return ['a', {href: "mailto:" + param, 'data-bbcode': true}].concat(contents);
|
||||
});
|
||||
|
||||
helper.onParseNode(event => {
|
||||
if (!Array.isArray(event.node)) { return; }
|
||||
const result = [event.node[0]];
|
||||
const nodes = event.node.slice(1);
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (Array.isArray(nodes[i]) && nodes[i][0] === 'concat') {
|
||||
for (let j = 1; j < nodes[i].length; j++) { result.push(nodes[i][j]); }
|
||||
} else {
|
||||
result.push(nodes[i]);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < result.length; i++) { event.node[i] = result[i]; }
|
||||
});
|
||||
|
||||
helper.replaceBlock({
|
||||
start: /(\[code\])([\s\S]*)/igm,
|
||||
stop: /\[\/code\]/igm,
|
||||
rawContents: true,
|
||||
|
||||
emitter(blockContents) {
|
||||
const options = helper.getOptions();
|
||||
const inner = blockContents.join("\n");
|
||||
const defaultCodeLang = options.defaultCodeLang;
|
||||
return ['p', ['pre', ['code', {'class': `lang-${defaultCodeLang}`}, inner]]];
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import guid from 'pretty-text/guid';
|
||||
|
||||
/**
|
||||
markdown-js doesn't ensure that em/strong codes are present on word boundaries.
|
||||
So we create our own handlers here.
|
||||
**/
|
||||
|
||||
// From PageDown
|
||||
const aLetter = /[a-zA-Z0-9\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376-\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0523\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0621-\u064a\u0660-\u0669\u066e-\u066f\u0671-\u06d3\u06d5\u06e5-\u06e6\u06ee-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07c0-\u07ea\u07f4-\u07f5\u07fa\u0904-\u0939\u093d\u0950\u0958-\u0961\u0966-\u096f\u0971-\u0972\u097b-\u097f\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc-\u09dd\u09df-\u09e1\u09e6-\u09f1\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a66-\u0a6f\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0-\u0ae1\u0ae6-\u0aef\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b35-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b66-\u0b6f\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0be6-\u0bef\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58-\u0c59\u0c60-\u0c61\u0c66-\u0c6f\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0-\u0ce1\u0ce6-\u0cef\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d28\u0d2a-\u0d39\u0d3d\u0d60-\u0d61\u0d66-\u0d6f\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32-\u0e33\u0e40-\u0e46\u0e50-\u0e59\u0e81-\u0e82\u0e84\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa-\u0eab\u0ead-\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0ed0-\u0ed9\u0edc-\u0edd\u0f00\u0f20-\u0f29\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8b\u1000-\u102a\u103f-\u1049\u1050-\u1055\u105a-\u105d\u1061\u1065-\u1066\u106e-\u1070\u1075-\u1081\u108e\u1090-\u1099\u10a0-\u10c5\u10d0-\u10fa\u10fc\u1100-\u1159\u115f-\u11a2\u11a8-\u11f9\u1200-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u1676\u1681-\u169a\u16a0-\u16ea\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u17e0-\u17e9\u1810-\u1819\u1820-\u1877\u1880-\u18a8\u18aa\u1900-\u191c\u1946-\u196d\u1970-\u1974\u1980-\u19a9\u19c1-\u19c7\u19d0-\u19d9\u1a00-\u1a16\u1b05-\u1b33\u1b45-\u1b4b\u1b50-\u1b59\u1b83-\u1ba0\u1bae-\u1bb9\u1c00-\u1c23\u1c40-\u1c49\u1c4d-\u1c7d\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u203f-\u2040\u2054\u2071\u207f\u2090-\u2094\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2183-\u2184\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2c6f\u2c71-\u2c7d\u2c80-\u2ce4\u2d00-\u2d25\u2d30-\u2d65\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3006\u3031-\u3035\u303b-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31b7\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fc3\ua000-\ua48c\ua500-\ua60c\ua610-\ua62b\ua640-\ua65f\ua662-\ua66e\ua67f-\ua697\ua717-\ua71f\ua722-\ua788\ua78b-\ua78c\ua7fb-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8d0-\ua8d9\ua900-\ua925\ua930-\ua946\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa50-\uaa59\uac00-\ud7a3\uf900-\ufa2d\ufa30-\ufa6a\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe33-\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]/;
|
||||
|
||||
|
||||
function unhoist(obj,from,to){
|
||||
let unhoisted = 0;
|
||||
const regex = new RegExp(from, "g");
|
||||
|
||||
if(_.isArray(obj)){
|
||||
for (let i=0; i<obj.length; i++){
|
||||
const item = obj[i];
|
||||
|
||||
if (_.isString(item)) {
|
||||
// Odd, but we need +1 for the / in front of /*
|
||||
const matches = item.match(regex);
|
||||
unhoisted -= matches ? matches.length : 0;
|
||||
|
||||
obj[i] = item.replace(regex, to);
|
||||
unhoisted += item.length - obj[i].length;
|
||||
}
|
||||
if (_.isArray(item)) {
|
||||
unhoisted += unhoist(item, from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
return unhoisted;
|
||||
};
|
||||
|
||||
export function setup(helper) {
|
||||
|
||||
function replaceMarkdown(match, tag) {
|
||||
const hash = guid();
|
||||
|
||||
helper.registerInline(match, function(text, matched, prev) {
|
||||
if (!text || text.length < match.length + 1) { return; }
|
||||
|
||||
let lastText = prev[prev.length-1];
|
||||
lastText = typeof lastText === "string" && lastText;
|
||||
lastText = lastText && lastText[lastText.length-1];
|
||||
|
||||
if (lastText && (lastText === "/" || lastText.match(aLetter))) { return; }
|
||||
if (text[match.length].match(/\s/)) { return; }
|
||||
|
||||
// hoist out escaped \*
|
||||
text = text.replace(new RegExp("\\\\\\" + match[0], "g"), hash);
|
||||
|
||||
const endText = new RegExp("[^\\s|" + match[0] + "]" + match.replace(/\*/g,"\\*") + "([^" + match[0] + "]|$)");
|
||||
const finish = text.split("\n")[0].search(endText);
|
||||
if (finish && finish >= 0) {
|
||||
const newText = this.processInline(text.substring(match.length, finish+1));
|
||||
const unhoisted_length = unhoist(newText,hash,match[0]);
|
||||
const array = typeof tag === "string" ? [tag].concat(newText) : [tag[0], [tag[1]].concat(newText)];
|
||||
return [(finish + match.length + 1) - unhoisted_length, array];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
replaceMarkdown('***', ['strong','em']);
|
||||
replaceMarkdown('___', ['strong','em']);
|
||||
replaceMarkdown('**', 'strong');
|
||||
replaceMarkdown('__', 'strong');
|
||||
replaceMarkdown('*', 'em');
|
||||
replaceMarkdown('_', 'em');
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
export function setup(helper) {
|
||||
helper.inlineRegexp({
|
||||
start: '#',
|
||||
matcher: /^#([\w-:]{1,101})/i,
|
||||
spaceOrTagBoundary: true,
|
||||
|
||||
emitter(matches) {
|
||||
const options = helper.getOptions();
|
||||
const [hashtag, slug] = matches;
|
||||
const categoryHashtagLookup = options.categoryHashtagLookup;
|
||||
const result = categoryHashtagLookup && categoryHashtagLookup(slug);
|
||||
|
||||
return result ? ['a', { class: 'hashtag', href: result[0] }, '#', ["span", {}, result[1]]]
|
||||
: ['span', { class: 'hashtag' }, hashtag];
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { censor } from 'pretty-text/censored-words';
|
||||
import { registerOption } from 'pretty-text/pretty-text';
|
||||
|
||||
registerOption((siteSettings, opts) => {
|
||||
opts.features.censored = true;
|
||||
opts.censoredWords = siteSettings.censored_words;
|
||||
});
|
||||
|
||||
export function setup(helper) {
|
||||
helper.addPreProcessor(text => {
|
||||
return censor(text, helper.getOptions().censoredWords);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import { escape } from 'pretty-text/sanitizer';
|
||||
import { registerOption } from 'pretty-text/pretty-text';
|
||||
|
||||
// Support for various code blocks
|
||||
const TEXT_CODE_CLASSES = ["text", "pre", "plain"];
|
||||
|
||||
function codeFlattenBlocks(blocks) {
|
||||
let result = "";
|
||||
blocks.forEach(function(b) {
|
||||
result += b;
|
||||
if (b.trailing) { result += b.trailing; }
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
registerOption((siteSettings, opts) => {
|
||||
opts.features.code = true;
|
||||
opts.defaultCodeLang = siteSettings.default_code_lang;
|
||||
opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "").split("|").concat(['auto', 'nohighlight']);
|
||||
});
|
||||
|
||||
export function setup(helper) {
|
||||
|
||||
helper.whiteList({
|
||||
custom(tag, name, value) {
|
||||
if (tag === 'code' && name === 'class') {
|
||||
const m = /^lang\-(.+)$/.exec(value);
|
||||
if (m) {
|
||||
return helper.getOptions().acceptableCodeClasses.indexOf(m[1]) !== -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
helper.replaceBlock({
|
||||
start: /^`{3}([^\n\[\]]+)?\n?([\s\S]*)?/gm,
|
||||
stop: /^```$/gm,
|
||||
withoutLeading: /\[quote/gm, //if leading text contains a quote this should not match
|
||||
emitter(blockContents, matches) {
|
||||
const opts = helper.getOptions();
|
||||
|
||||
let codeLang = opts.defaultCodeLang;
|
||||
const acceptableCodeClasses = opts.acceptableCodeClasses;
|
||||
if (acceptableCodeClasses && matches[1] && acceptableCodeClasses.indexOf(matches[1]) !== -1) {
|
||||
codeLang = matches[1];
|
||||
}
|
||||
|
||||
if (TEXT_CODE_CLASSES.indexOf(matches[1]) !== -1) {
|
||||
return ['p', ['pre', ['code', {'class': 'lang-nohighlight'}, codeFlattenBlocks(blockContents) ]]];
|
||||
} else {
|
||||
return ['p', ['pre', ['code', {'class': 'lang-' + codeLang}, codeFlattenBlocks(blockContents) ]]];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
helper.replaceBlock({
|
||||
start: /(<pre[^\>]*\>)([\s\S]*)/igm,
|
||||
stop: /<\/pre>/igm,
|
||||
rawContents: true,
|
||||
skipIfTradtionalLinebreaks: true,
|
||||
|
||||
emitter(blockContents) {
|
||||
return ['p', ['pre', codeFlattenBlocks(blockContents)]];
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure that content in a code block is fully escaped. This way it's not white listed
|
||||
// and we can use HTML and Javascript examples.
|
||||
helper.onParseNode(function(event) {
|
||||
const node = event.node,
|
||||
path = event.path;
|
||||
|
||||
if (node[0] === 'code') {
|
||||
const regexp = (path && path[path.length-1] && path[path.length-1][0] && path[path.length-1][0] === "pre") ?
|
||||
/ +$/g : /^ +| +$/g;
|
||||
|
||||
const contents = node[node.length-1];
|
||||
node[node.length-1] = escape(contents.replace(regexp,''));
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
import { registerOption } from 'pretty-text/pretty-text';
|
||||
import { buildEmojiUrl } from 'pretty-text/emoji';
|
||||
import { translations } from 'pretty-text/emoji/data';
|
||||
|
||||
let _unicodeReplacements;
|
||||
let _unicodeRegexp;
|
||||
export function setUnicodeReplacements(replacements) {
|
||||
_unicodeReplacements = replacements;
|
||||
if (replacements) {
|
||||
_unicodeRegexp = new RegExp(Object.keys(replacements).join("|"), "g");
|
||||
}
|
||||
};
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return s.replace(/[-/\\^$*+?.()|[\]{}]/gi, '\\$&');
|
||||
}
|
||||
|
||||
function checkPrev(prev) {
|
||||
if (prev && prev.length) {
|
||||
const lastToken = prev[prev.length-1];
|
||||
if (lastToken && lastToken.charAt) {
|
||||
const lastChar = lastToken.charAt(lastToken.length-1);
|
||||
if (!/\W/.test(lastChar)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
registerOption((siteSettings, opts, state) => {
|
||||
opts.features.emoji = !!siteSettings.enable_emoji;
|
||||
opts.emojiSet = siteSettings.emoji_set || "";
|
||||
opts.customEmoji = state.customEmoji;
|
||||
});
|
||||
|
||||
export function setup(helper) {
|
||||
|
||||
helper.whiteList('img.emoji');
|
||||
|
||||
function imageFor(code) {
|
||||
code = code.toLowerCase();
|
||||
const url = buildEmojiUrl(code, helper.getOptions());
|
||||
if (url) {
|
||||
const title = `:${code}:`;
|
||||
return ['img', { href: url, title, 'class': 'emoji', alt: title }];
|
||||
}
|
||||
}
|
||||
|
||||
const translationsWithColon = {};
|
||||
Object.keys(translations).forEach(t => {
|
||||
if (t[0] === ':') {
|
||||
translationsWithColon[t] = translations[t];
|
||||
} else {
|
||||
const replacement = translations[t];
|
||||
helper.inlineReplace(t, (token, match, prev) => {
|
||||
return checkPrev(prev) ? imageFor(replacement) : token;
|
||||
});
|
||||
}
|
||||
});
|
||||
const translationColonRegexp = new RegExp(Object.keys(translationsWithColon).map(t => `(${escapeRegExp(t)})`).join("|"));
|
||||
|
||||
helper.registerInline(':', (text, match, prev) => {
|
||||
const endPos = text.indexOf(':', 1);
|
||||
const firstSpace = text.search(/\s/);
|
||||
if (!checkPrev(prev)) { return; }
|
||||
|
||||
// If there is no trailing colon, check our translations that begin with colons
|
||||
if (endPos === -1 || (firstSpace !== -1 && endPos > firstSpace)) {
|
||||
translationColonRegexp.lastIndex = 0;
|
||||
const m = translationColonRegexp.exec(text);
|
||||
if (m && m[0] && text.indexOf(m[0]) === 0) {
|
||||
// Check outer edge
|
||||
const lastChar = text.charAt(m[0].length);
|
||||
if (lastChar && !/\s/.test(lastChar)) return;
|
||||
const contents = imageFor(translationsWithColon[m[0]]);
|
||||
if (contents) {
|
||||
return [m[0].length, contents];
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple find and replace from our array
|
||||
const between = text.slice(1, endPos);
|
||||
const contents = imageFor(between);
|
||||
if (contents) {
|
||||
return [endPos+1, contents];
|
||||
}
|
||||
});
|
||||
|
||||
helper.addPreProcessor(text => {
|
||||
if (_unicodeReplacements) {
|
||||
_unicodeRegexp.lastIndex = 0;
|
||||
|
||||
let m;
|
||||
while ((m = _unicodeRegexp.exec(text)) !== null) {
|
||||
let replacement = ":" + _unicodeReplacements[m[0]] + ":";
|
||||
const before = text.charAt(m.index-1);
|
||||
if (!/\B/.test(before)) {
|
||||
replacement = "\u200b" + replacement;
|
||||
}
|
||||
text = text.replace(m[0], replacement);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
});
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
const BLOCK_TAGS = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'div',
|
||||
'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
|
||||
'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'iframe', 'noscript', 'ol', 'output',
|
||||
'p', 'pre', 'section', 'table', 'tfoot', 'ul', 'video'];
|
||||
|
||||
function splitAtLast(tag, block, next, first) {
|
||||
const endTag = `</${tag}>`;
|
||||
let endTagIndex = first ? block.indexOf(endTag) : block.lastIndexOf(endTag);
|
||||
|
||||
if (endTagIndex !== -1) {
|
||||
endTagIndex += endTag.length;
|
||||
|
||||
const trailing = block.substr(endTagIndex).replace(/^\s+/, '');
|
||||
if (trailing.length) {
|
||||
next.unshift(trailing);
|
||||
}
|
||||
|
||||
return [ block.substr(0, endTagIndex) ];
|
||||
}
|
||||
};
|
||||
|
||||
export function setup(helper) {
|
||||
|
||||
// If a row begins with HTML tags, don't parse it.
|
||||
helper.registerBlock('html', function(block, next) {
|
||||
let split, pos;
|
||||
|
||||
// Fix manual blockquote paragraphing even though it's not strictly correct
|
||||
// PERF NOTE: /\S+<blockquote/ is a perf hog for search, try on huge string
|
||||
if (pos = block.search(/<blockquote/) >= 0) {
|
||||
if(block.substring(0, pos).search(/\s/) === -1) {
|
||||
split = splitAtLast('blockquote', block, next, true);
|
||||
if (split) { return this.processInline(split[0]); }
|
||||
}
|
||||
}
|
||||
|
||||
const m = /^<([^>]+)\>/.exec(block);
|
||||
if (m && m[1]) {
|
||||
const tag = m[1].split(/\s/);
|
||||
if (tag && tag[0] && BLOCK_TAGS.indexOf(tag[0]) !== -1) {
|
||||
split = splitAtLast(tag[0], block, next);
|
||||
if (split) {
|
||||
if (split.length === 1 && split[0] === block) { return; }
|
||||
return split;
|
||||
}
|
||||
return [ block.toString() ];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
Supports our custom @mention syntax for calling out a user in a post.
|
||||
It will add a special class to them, and create a link if the user is found in a
|
||||
local map.
|
||||
**/
|
||||
export function setup(helper) {
|
||||
|
||||
// We have to prune @mentions that are within links.
|
||||
helper.onParseNode(event => {
|
||||
const node = event.node,
|
||||
path = event.path;
|
||||
|
||||
if (node[1] && node[1]["class"] === 'mention') {
|
||||
const parent = path[path.length - 1];
|
||||
|
||||
// If the parent is an 'a', remove it
|
||||
if (parent && parent[0] === 'a') {
|
||||
const name = node[2];
|
||||
node.length = 0;
|
||||
node[0] = "__RAW";
|
||||
node[1] = name;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
helper.inlineRegexp({
|
||||
start: '@',
|
||||
// NOTE: since we can't use SiteSettings here (they loads later in process)
|
||||
// we are being less strict to account for more cases than allowed
|
||||
matcher: /^@(\w[\w.-]{0,59})\b/i,
|
||||
wordBoundary: true,
|
||||
|
||||
emitter(matches) {
|
||||
const mention = matches[0].trim();
|
||||
const name = matches[1];
|
||||
const opts = helper.getOptions();
|
||||
const mentionLookup = opts.mentionLookup;
|
||||
|
||||
const type = mentionLookup && mentionLookup(name);
|
||||
if (type === "user") {
|
||||
return ['a', {'class': 'mention', href: opts.getURL("/users/") + name.toLowerCase()}, mention];
|
||||
} else if (type === "group") {
|
||||
return ['a', {'class': 'mention-group', href: opts.getURL("/groups/") + name}, mention];
|
||||
} else {
|
||||
return ['span', {'class': 'mention'}, mention];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// Support for the newline behavior in markdown that most expect. Look through all text nodes
|
||||
// in the tree, replace any new lines with `br`s.
|
||||
|
||||
export function setup(helper) {
|
||||
helper.postProcessText((text, event) => {
|
||||
const { options, insideCounts } = event;
|
||||
if (options.traditionalMarkdownLinebreaks || (insideCounts.pre > 0)) { return; }
|
||||
|
||||
if (text === "\n") {
|
||||
// If the tag is just a new line, replace it with a `<br>`
|
||||
return [['br']];
|
||||
} else {
|
||||
// If the text node contains new lines, perhaps with text between them, insert the
|
||||
// `<br>` tags.
|
||||
const split = text.split(/\n+/);
|
||||
if (split.length) {
|
||||
const replacement = [];
|
||||
for (var i=0; i<split.length; i++) {
|
||||
if (split[i].length > 0) { replacement.push(split[i]); }
|
||||
if (i !== split.length-1) { replacement.push(['br']); }
|
||||
}
|
||||
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import { lookupCache } from 'pretty-text/oneboxer';
|
||||
|
||||
// Given a node in the document and its parent, determine whether it is on its own line or not.
|
||||
function isOnOneLine(link, parent) {
|
||||
if (!parent) { return false; }
|
||||
|
||||
const siblings = parent.slice(1);
|
||||
if ((!siblings) || (siblings.length < 1)) { return false; }
|
||||
|
||||
const idx = siblings.indexOf(link);
|
||||
if (idx === -1) { return false; }
|
||||
|
||||
if (idx > 0) {
|
||||
const prev = siblings[idx-1];
|
||||
if (prev[0] !== 'br') { return false; }
|
||||
}
|
||||
|
||||
if (idx < siblings.length) {
|
||||
const next = siblings[idx+1];
|
||||
if (next && (!((next[0] === 'br') || (typeof next === 'string' && next.trim() === "")))) { return false; }
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// We only onebox stuff that is on its own line.
|
||||
export function setup(helper) {
|
||||
helper.onParseNode(event => {
|
||||
const node = event.node,
|
||||
path = event.path;
|
||||
|
||||
// We only care about links
|
||||
if (node[0] !== 'a') { return; }
|
||||
|
||||
const parent = path[path.length - 1];
|
||||
|
||||
// We don't onebox bbcode
|
||||
if (node[1]['data-bbcode']) {
|
||||
delete node[1]['data-bbcode'];
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't onebox mentions
|
||||
if (node[1]['class'] === 'mention') { return; }
|
||||
|
||||
// Don't onebox links within a list
|
||||
for (var i=0; i<path.length; i++) {
|
||||
if (path[i][0] === 'li') { return; }
|
||||
}
|
||||
|
||||
// If the link has a different label text than the link itself, don't onebox it.
|
||||
const label = node[node.length-1];
|
||||
if (label !== node[1]['href']) { return; }
|
||||
|
||||
if (isOnOneLine(node, parent)) {
|
||||
|
||||
node[1]['class'] = 'onebox';
|
||||
node[1].target = '_blank';
|
||||
|
||||
const contents = lookupCache(node[1].href);
|
||||
if (contents) {
|
||||
node[0] = '__RAW';
|
||||
node[1] = contents;
|
||||
node.length = 2;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { register } from 'pretty-text/engines/discourse-markdown/bbcode';
|
||||
|
||||
export function setup(helper) {
|
||||
register(helper, 'quote', {noWrap: true, singlePara: true}, (contents, bbParams, options) => {
|
||||
const params = {'class': 'quote'};
|
||||
let username = null;
|
||||
|
||||
if (bbParams) {
|
||||
const paramsSplit = bbParams.split(/\,\s*/);
|
||||
username = paramsSplit[0];
|
||||
|
||||
paramsSplit.forEach(function(p,i) {
|
||||
if (i > 0) {
|
||||
var assignment = p.split(':');
|
||||
if (assignment[0] && assignment[1]) {
|
||||
const escaped = helper.escape(assignment[0]);
|
||||
// don't escape attributes, makes no sense
|
||||
if (escaped === assignment[0]) {
|
||||
params['data-' + assignment[0]] = helper.escape(assignment[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let avatarImg;
|
||||
const postNumber = parseInt(params['data-post'], 10);
|
||||
const topicId = parseInt(params['data-topic'], 10);
|
||||
|
||||
if (options.lookupAvatarByPostNumber) {
|
||||
// client-side, we can retrieve the avatar from the post
|
||||
avatarImg = options.lookupAvatarByPostNumber(postNumber, topicId);
|
||||
} else if (options.lookupAvatar) {
|
||||
// server-side, we need to lookup the avatar from the username
|
||||
avatarImg = options.lookupAvatar(username);
|
||||
}
|
||||
|
||||
// If there's no username just return a simple quote
|
||||
if (!username) {
|
||||
return ['p', ['aside', params, ['blockquote'].concat(contents)]];
|
||||
}
|
||||
|
||||
const header = ['div', {'class': 'title'},
|
||||
['div', {'class': 'quote-controls'}],
|
||||
avatarImg ? ['__RAW', avatarImg] : "",
|
||||
username ? `${username}:` : "" ];
|
||||
|
||||
if (options.topicId && postNumber && options.getTopicInfo && topicId !== options.topicId) {
|
||||
const topicInfo = options.getTopicInfo(topicId);
|
||||
if (topicInfo) {
|
||||
var href = topicInfo.href;
|
||||
if (postNumber > 0) { href += "/" + postNumber; }
|
||||
// get rid of username said stuff
|
||||
header.pop();
|
||||
header.push(['a', {'href': href}, topicInfo.title]);
|
||||
}
|
||||
}
|
||||
|
||||
return ['aside', params, header, ['blockquote'].concat(contents)];
|
||||
});
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { registerOption } from 'pretty-text/pretty-text';
|
||||
|
||||
function tableFlattenBlocks(blocks) {
|
||||
let result = "";
|
||||
|
||||
blocks.forEach(b => {
|
||||
result += b;
|
||||
if (b.trailing) { result += b.trailing; }
|
||||
});
|
||||
|
||||
// bypass newline insertion
|
||||
return result.replace(/[\n\r]/g, " ");
|
||||
};
|
||||
|
||||
registerOption((siteSettings, opts) => {
|
||||
opts.features.table = !!siteSettings.allow_html_tables;
|
||||
});
|
||||
|
||||
export function setup(helper) {
|
||||
|
||||
helper.whiteList(['table', 'table.md-table', 'tbody', 'thead', 'tr', 'th', 'td']);
|
||||
|
||||
helper.replaceBlock({
|
||||
start: /(<table[^>]*>)([\S\s]*)/igm,
|
||||
stop: /<\/table>/igm,
|
||||
rawContents: true,
|
||||
priority: 1,
|
||||
|
||||
emitter(contents) {
|
||||
return ['table', {"class": "md-table"}, tableFlattenBlocks.apply(this, [contents])];
|
||||
}
|
||||
});
|
||||
}
|
14
app/assets/javascripts/pretty-text/guid.js.es6
Normal file
14
app/assets/javascripts/pretty-text/guid.js.es6
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*eslint no-bitwise:0 */
|
||||
|
||||
// http://stackoverflow.com/a/8809472/17174
|
||||
export default function() {
|
||||
let d = new Date().getTime();
|
||||
if (window.performance && typeof window.performance.now === "function") {
|
||||
d += performance.now(); //use high-precision timer if available
|
||||
}
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = (d + Math.random() * 16) % 16 | 0;
|
||||
d = Math.floor(d/16);
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||
});
|
||||
};
|
54
app/assets/javascripts/pretty-text/oneboxer.js.es6
Normal file
54
app/assets/javascripts/pretty-text/oneboxer.js.es6
Normal file
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
A helper for looking up oneboxes and displaying them
|
||||
|
||||
For now it only stores in a local Javascript Object, in future we can change it so it uses localStorage
|
||||
or some other mechanism.
|
||||
**/
|
||||
|
||||
const localCache = {};
|
||||
const failedCache = {};
|
||||
|
||||
// Perform a lookup of a onebox based an anchor element. It will insert a loading
|
||||
// indicator and remove it when the loading is complete or fails.
|
||||
export function load(e, refresh) {
|
||||
var $elem = $(e);
|
||||
|
||||
// If the onebox has loaded, return
|
||||
if ($elem.data('onebox-loaded')) return;
|
||||
if ($elem.hasClass('loading-onebox')) return;
|
||||
|
||||
const url = e.href;
|
||||
|
||||
// Unless we're forcing a refresh...
|
||||
if (!refresh) {
|
||||
// If we have it in our cache, return it.
|
||||
const cached = localCache[url];
|
||||
if (cached) return cached;
|
||||
|
||||
// If the request failed, don't do anything
|
||||
const failed = failedCache[url];
|
||||
if (failed) return;
|
||||
}
|
||||
|
||||
// Add the loading CSS class
|
||||
$elem.addClass('loading-onebox');
|
||||
|
||||
// Retrieve the onebox
|
||||
return Discourse.ajax("/onebox", {
|
||||
dataType: 'html',
|
||||
data: { url, refresh },
|
||||
cache: true
|
||||
}).then(html => {
|
||||
localCache[url] = html;
|
||||
$elem.replaceWith(html);
|
||||
}, function() {
|
||||
failedCache[url] = true;
|
||||
}).finally(() => {
|
||||
$elem.removeClass('loading-onebox');
|
||||
$elem.data('onebox-loaded');
|
||||
});
|
||||
}
|
||||
|
||||
export function lookupCache(url) {
|
||||
return localCache[url];
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user