From 73489b652e2aea5dfb6c391168e13bc7dcdb395c Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 27 Aug 2013 12:24:17 -0400 Subject: [PATCH 01/84] FIX: Allow intra-word underscores. --- .../dialects/bold_italics_dialect.js | 41 ++++++---- .../{markdown.js => better_markdown.js} | 78 ++++--------------- lib/pretty_text.rb | 2 +- test/javascripts/components/markdown_test.js | 6 ++ 4 files changed, 48 insertions(+), 79 deletions(-) rename app/assets/javascripts/external/{markdown.js => better_markdown.js} (95%) diff --git a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js index 704b6a16465..b4fadffb778 100644 --- a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js +++ b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js @@ -10,23 +10,34 @@ Discourse.Dialect.on("register", function(event) { var dialect = event.dialect, MD = event.MD; - /** - Handles simultaneous bold and italics - @method parseMentions - @param {String} text the text match - @param {Array} match the match found - @param {Array} prev the previous jsonML - @return {Array} an array containing how many chars we've replaced and the jsonML content for it. - @namespace Discourse.Dialect - **/ - dialect.inline['***'] = function boldItalics(text, match, prev) { - var regExp = /^\*{3}([^\*]+)\*{3}/, - m = regExp.exec(text); + var inlineBuilder = function(symbol, tag, surround) { + return function(text, match, prev) { + if (prev && (prev.length > 0)) { + var last = prev[prev.length - 1]; + if (typeof last === "string" && (!last.match(/\W$/))) { return; } + } - if (m) { - return [m[0].length, ['strong', ['em'].concat(this.processInline(m[1]))]]; - } + var regExp = new RegExp("^\\" + symbol + "([^\\" + symbol + "]+)" + "\\" + symbol, "igm"), + m = regExp.exec(text); + + if (m) { + + var contents = [tag].concat(this.processInline(m[1])); + if (surround) { + contents = [surround, contents]; + } + + return [m[0].length, contents]; + } + }; }; + dialect.inline['***'] = inlineBuilder('**', 'em', 'strong'); + dialect.inline['**'] = inlineBuilder('**', 'strong'); + dialect.inline['*'] = inlineBuilder('*', 'em'); + dialect.inline['_'] = inlineBuilder('_', 'em'); + + + }); diff --git a/app/assets/javascripts/external/markdown.js b/app/assets/javascripts/external/better_markdown.js similarity index 95% rename from app/assets/javascripts/external/markdown.js rename to app/assets/javascripts/external/better_markdown.js index e11d71e6c27..29e708fb857 100644 --- a/app/assets/javascripts/external/markdown.js +++ b/app/assets/javascripts/external/better_markdown.js @@ -1,3 +1,18 @@ +/* + This is a fork of markdown-js with a few changes to support discourse: + + * We have replaced the strong/em handlers because we prefer them only to work on word + boundaries. + + * We removed the maraku support as we don't use it. + + * We don't escape the contents of HTML as we prefer to use a whitelist. + + * Note the name BetterMarkdown doesn't mean it's *better* than markdown-js, it refers + to it being better than our previous markdown parser! + +*/ + // Released under MIT license // Copyright (c) 2009-2010 Dominic Baggott // Copyright (c) 2009-2010 Ash Berlin @@ -1004,69 +1019,6 @@ Markdown.dialects.Gruber.inline = { }; -// Meta Helper/generator method for em and strong handling -function strong_em( tag, md ) { - - var state_slot = tag + "_state", - other_slot = tag == "strong" ? "em_state" : "strong_state"; - - function CloseTag(len) { - this.len_after = len; - this.name = "close_" + md; - } - - return function ( text, orig_match ) { - - if ( this[state_slot][0] == md ) { - // Most recent em is of this type - //D:this.debug("closing", md); - this[state_slot].shift(); - - // "Consume" everything to go back to the recrusion in the else-block below - return[ text.length, new CloseTag(text.length-md.length) ]; - } - else { - // Store a clone of the em/strong states - var other = this[other_slot].slice(), - state = this[state_slot].slice(); - - this[state_slot].unshift(md); - - //D:this.debug_indent += " "; - - // Recurse - var res = this.processInline( text.substr( md.length ) ); - //D:this.debug_indent = this.debug_indent.substr(2); - - var last = res[res.length - 1]; - - //D:this.debug("processInline from", tag + ": ", uneval( res ) ); - - var check = this[state_slot].shift(); - if ( last instanceof CloseTag ) { - res.pop(); - // We matched! Huzzah. - var consumed = text.length - last.len_after; - return [ consumed, [ tag ].concat(res) ]; - } - else { - // Restore the state of the other kind. We might have mistakenly closed it. - this[other_slot] = other; - this[state_slot] = state; - - // We can't reuse the processed result as it could have wrong parsing contexts in it. - return [ md.length, md ]; - } - } - }; // End returned function -} - -Markdown.dialects.Gruber.inline["**"] = strong_em("strong", "**"); -Markdown.dialects.Gruber.inline["__"] = strong_em("strong", "__"); -Markdown.dialects.Gruber.inline["*"] = strong_em("em", "*"); -Markdown.dialects.Gruber.inline["_"] = strong_em("em", "_"); - - // Build default order from insertion order. Markdown.buildBlockOrder = function(d) { var ord = []; diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 48fe7fdf23c..eecb5ca7eda 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -105,7 +105,7 @@ module PrettyText ctx.eval("var I18n = {}; I18n.t = function(a,b){ return helpers.t(a,b); }"); ctx_load(ctx, - "app/assets/javascripts/external/markdown.js", + "app/assets/javascripts/external/better_markdown.js", "app/assets/javascripts/discourse/dialects/dialect.js", "app/assets/javascripts/discourse/components/utilities.js", "app/assets/javascripts/discourse/components/markdown.js") diff --git a/test/javascripts/components/markdown_test.js b/test/javascripts/components/markdown_test.js index f4714baee30..2370f0b0a6d 100644 --- a/test/javascripts/components/markdown_test.js +++ b/test/javascripts/components/markdown_test.js @@ -17,7 +17,13 @@ var cookedOptions = function(input, opts, expected, text) { test("basic cooking", function() { cooked("hello", "

hello

", "surrounds text with paragraphs"); + cooked("**evil**", "

evil

", "it bolds text."); + cooked("*trout*", "

trout

", "it italicizes text."); + cooked("_trout_", "

trout

", "it italicizes text."); cooked("***hello***", "

hello

", "it can do bold and italics at once."); + cooked("word_with_underscores", "

word_with_underscores

", "it doesn't do intraword italics"); + cooked("hello \\*evil\\*", "

hello *evil*

", "it supports escaping of asterisks"); + cooked("hello \\_evil\\_", "

hello _evil_

", "it supports escaping of italics"); }); test("Traditional Line Breaks", function() { From 96772af35b4640f4592d6c837f839c5809cb7555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 27 Aug 2013 23:01:35 +0200 Subject: [PATCH 02/84] FIX: avatar thumbnail won't save after upload --- .../controllers/avatar_selector_controller.js | 14 +++++-- .../javascripts/discourse/models/user.js | 23 ++++++---- .../discourse/routes/preferences_routes.js | 42 ++++++++----------- .../modal/avatar_selector.js.handlebars | 4 +- .../avatar_selector_controller_test.js | 28 +++++++++++++ 5 files changed, 73 insertions(+), 38 deletions(-) create mode 100644 test/javascripts/controllers/avatar_selector_controller_test.js diff --git a/app/assets/javascripts/discourse/controllers/avatar_selector_controller.js b/app/assets/javascripts/discourse/controllers/avatar_selector_controller.js index 861853ca41c..91ce9648bc5 100644 --- a/app/assets/javascripts/discourse/controllers/avatar_selector_controller.js +++ b/app/assets/javascripts/discourse/controllers/avatar_selector_controller.js @@ -8,7 +8,15 @@ @module Discourse **/ Discourse.AvatarSelectorController = Discourse.Controller.extend(Discourse.ModalFunctionality, { - toggleUseUploadedAvatar: function(toggle) { - this.set("use_uploaded_avatar", toggle); - } + useUploadedAvatar: function() { + this.set("use_uploaded_avatar", true); + }, + + useGravatar: function() { + this.set("use_uploaded_avatar", false); + }, + + avatarTemplate: function() { + return this.get("use_uploaded_avatar") ? this.get("uploaded_avatar_template") : this.get("gravatar_template"); + }.property("use_uploaded_avatar", "uploaded_avatar_template", "gravatar_template") }); diff --git a/app/assets/javascripts/discourse/models/user.js b/app/assets/javascripts/discourse/models/user.js index 4abb65886f6..c367dd87573 100644 --- a/app/assets/javascripts/discourse/models/user.js +++ b/app/assets/javascripts/discourse/models/user.js @@ -28,7 +28,11 @@ Discourse.User = Discourse.Model.extend({ searchContext: function() { - return ({ type: 'user', id: this.get('username_lower'), user: this }); + return { + type: 'user', + id: this.get('username_lower'), + user: this + }; }.property('username_lower'), /** @@ -101,7 +105,7 @@ Discourse.User = Discourse.Model.extend({ @returns Result of ajax call **/ changeUsername: function(newUsername) { - return Discourse.ajax("/users/" + (this.get('username_lower')) + "/preferences/username", { + return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/username", { type: 'PUT', data: { new_username: newUsername } }); @@ -115,7 +119,7 @@ Discourse.User = Discourse.Model.extend({ @returns Result of ajax call **/ changeEmail: function(email) { - return Discourse.ajax("/users/" + (this.get('username_lower')) + "/preferences/email", { + return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/email", { type: 'PUT', data: { email: email } }); @@ -173,9 +177,7 @@ Discourse.User = Discourse.Model.extend({ changePassword: function() { return Discourse.ajax("/session/forgot_password", { dataType: 'json', - data: { - login: this.get('username') - }, + data: { login: this.get('username') }, type: 'POST' }); }, @@ -266,11 +268,14 @@ Discourse.User = Discourse.Model.extend({ Change avatar selection @method toggleAvatarSelection + @param {Boolean} useUploadedAvatar true if the user is using the uploaded avatar @returns {Promise} the result of the toggle avatar selection */ - toggleAvatarSelection: function() { - var data = { use_uploaded_avatar: this.get("use_uploaded_avatar") }; - return Discourse.ajax("/users/" + this.get("username") + "/preferences/avatar/toggle", { type: 'PUT', data: data }); + toggleAvatarSelection: function(useUploadedAvatar) { + return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/avatar/toggle", { + type: 'PUT', + data: { use_uploaded_avatar: useUploadedAvatar } + }); } }); diff --git a/app/assets/javascripts/discourse/routes/preferences_routes.js b/app/assets/javascripts/discourse/routes/preferences_routes.js index e623cf3c4c5..e337a43a81b 100644 --- a/app/assets/javascripts/discourse/routes/preferences_routes.js +++ b/app/assets/javascripts/discourse/routes/preferences_routes.js @@ -18,35 +18,29 @@ Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({ events: { showAvatarSelector: function() { Discourse.Route.showModal(this, 'avatarSelector'); - var user = this.modelFor("user"); - console.log(user); - this.controllerFor("avatarSelector").setProperties(user.getProperties( - "username", - "email", - "has_uploaded_avatar", - "use_uploaded_avatar", - "gravatar_template", - "uploaded_avatar_template" - )); + // all the properties needed for displaying the avatar selector modal + var avatarSelector = this.modelFor('user').getProperties( + 'username', 'email', + 'has_uploaded_avatar', 'use_uploaded_avatar', + 'gravatar_template', 'uploaded_avatar_template'); + this.controllerFor('avatarSelector').setProperties(avatarSelector); }, saveAvatarSelection: function() { - var user = this.modelFor("user"); - var avatar = this.controllerFor("avatarSelector"); + var user = this.modelFor('user'); + var avatarSelector = this.controllerFor('avatarSelector'); // sends the information to the server if it has changed - if (avatar.get("use_uploaded_avatar") !== user.get("use_uploaded_avatar")) { user.toggleAvatarSelection(); } - // saves the data back - user.setProperties(avatar.getProperties( - "has_uploaded_avatar", - "use_uploaded_avatar", - "gravatar_template", - "uploaded_avatar_template" - )); - if (avatar.get("use_uploaded_avatar")) { - user.set("avatar_template", avatar.get("uploaded_avatar_template")); - } else { - user.set("avatar_template", avatar.get("gravatar_template")); + if (avatarSelector.get('use_uploaded_avatar') !== user.get('use_uploaded_avatar')) { + user.toggleAvatarSelection(avatarSelector.get('use_uploaded_avatar')); } + // saves the data back + user.setProperties(avatarSelector.getProperties( + 'has_uploaded_avatar', + 'use_uploaded_avatar', + 'gravatar_template', + 'uploaded_avatar_template' + )); + user.set('avatar_template', avatarSelector.get('avatarTemplate')); } } }); diff --git a/app/assets/javascripts/discourse/templates/modal/avatar_selector.js.handlebars b/app/assets/javascripts/discourse/templates/modal/avatar_selector.js.handlebars index 4548c091b79..63fcf8e34d1 100644 --- a/app/assets/javascripts/discourse/templates/modal/avatar_selector.js.handlebars +++ b/app/assets/javascripts/discourse/templates/modal/avatar_selector.js.handlebars @@ -1,12 +1,12 @@