Skip to content
Extraits de code Groupes Projets
Valider 8150d32b rédigé par danielgrippi's avatar danielgrippi
Parcourir les fichiers

using pageDown Markdown library, fixing autolinking. created...

using pageDown Markdown library, fixing autolinking.  created app.helpers.textFormatter, which takes care of text formatting; functions can be called individually throughout the app
parent f2cc8b4e
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
...@@ -9,10 +9,9 @@ javascripts: ...@@ -9,10 +9,9 @@ javascripts:
main: main:
- public/javascripts/vendor/underscore.js - public/javascripts/vendor/underscore.js
- public/javascripts/vendor/backbone.js - public/javascripts/vendor/backbone.js
- public/javascripts/vendor/markdown/*
- public/javascripts/vendor/markdown.js
- public/javascripts/app/app.js - public/javascripts/app/app.js
- public/javascripts/app/helpers/*
- public/javascripts/app/router.js - public/javascripts/app/router.js
- public/javascripts/app/views.js - public/javascripts/app/views.js
- public/javascripts/app/models/post.js - public/javascripts/app/models/post.js
......
var app = { var app = {
collections: {}, collections: {},
models: {}, models: {},
helpers: {},
views: {}, views: {},
user: function(user) { user: function(user) {
......
(function(){
var textFormatter = function textFormatter(model) {
var text = model.get("text");
var mentions = model.get("mentioned_people");
return textFormatter.mentionify(
textFormatter.hashtagify(
textFormatter.markdownify(text)
), mentions
)
};
textFormatter.markdownify = function markdownify(text){
var converter = Markdown.getSanitizingConverter();
return converter.makeHtml(text)
};
textFormatter.hashtagify = function hashtagify(text){
var utf8WordCharcters =/(\s|^|>)#([\u0080-\uFFFF|\w|-]+|<3)/g
return text.replace(utf8WordCharcters, function(hashtag, preceeder, tagText) {
return preceeder + "<a href='/tags/" + tagText + "' class='tag'>#" + tagText + "</a>"
})
};
textFormatter.mentionify = function mentionify(text, mentions) {
var mentionRegex = /@\{([^;]+); ([^\}]+)\}/g
return text.replace(mentionRegex, function(mentionText, fullName, diasporaId) {
var personId = _.find(mentions, function(person){
return person.diaspora_id == diasporaId
}).id
return "<a href='/people/" + personId + "' class='mention'>" + fullName + "</a>"
})
}
app.helpers.textFormatter = textFormatter;
})();
...@@ -2,54 +2,10 @@ app.views.Content = app.views.StreamObject.extend({ ...@@ -2,54 +2,10 @@ app.views.Content = app.views.StreamObject.extend({
presenter : function(){ presenter : function(){
var model = this.model var model = this.model
return _.extend(this.defaultPresenter(), { return _.extend(this.defaultPresenter(), {
text : metafyText(model.get("text")), text : app.helpers.textFormatter(model),
o_embed_html : embedHTML(model) o_embed_html : embedHTML(model)
}) })
function metafyText(text) {
//we want it to return at least a <p> from markdown
text = text || ""
return urlify(
mentionify(
hashtagify(
markdownify(text)
)
)
)
}
function markdownify(text){
//markdown returns falsy when it performs no substitutions, apparently...
return markdown.toHTML(text) || text
}
function hashtagify(text){
var utf8WordCharcters =/(\s|^|>)#([\u0080-\uFFFF|\w|-]+|&lt;3)/g
return text.replace(utf8WordCharcters, function(hashtag, preceeder, tagText) {
return preceeder + "<a href='/tags/" + tagText + "' class='tag'>#" + tagText + "</a>"
})
}
function mentionify(text) {
var mentionRegex = /@\{([^;]+); ([^\}]+)\}/g
return text.replace(mentionRegex, function(mentionText, fullName, diasporaId) {
var personId = _.find(model.get("mentioned_people"), function(person){
return person.diaspora_id == diasporaId
}).id
return "<a href='/people/" + personId + "' class='mention'>" + fullName + "</a>"
})
}
function urlify(text) {
var urlRegex = /(=\s?'|=\s?")?[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?(#!)&//=;]*)?/gi
return text.replace(urlRegex, function(url, preceeder, bang) {
if(preceeder) return url
var protocol = (url.search(/:\/\//) == -1 ? "http://" : "")
return "<a href='" + protocol + url + "' target=_blank>" + url + "</a>"
})
}
function embedHTML(model){ function embedHTML(model){
if(!model.get("o_embed_cache")) { return ""; } if(!model.get("o_embed_cache")) { return ""; }
return model.get("o_embed_cache").data.html return model.get("o_embed_cache").data.html
...@@ -57,7 +13,6 @@ app.views.Content = app.views.StreamObject.extend({ ...@@ -57,7 +13,6 @@ app.views.Content = app.views.StreamObject.extend({
} }
}) })
app.views.StatusMessage = app.views.Content.extend({ app.views.StatusMessage = app.views.Content.extend({
template_name : "#status-message-template" template_name : "#status-message-template"
}); });
......
Ce diff est replié.
(function () {
var output, Converter;
if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
output = exports;
Converter = require("./Markdown.Converter").Converter;
} else {
output = window.Markdown;
Converter = output.Converter;
}
output.getSanitizingConverter = function () {
var converter = new Converter();
converter.hooks.chain("postConversion", sanitizeHtml);
converter.hooks.chain("postConversion", balanceTags);
return converter;
}
function sanitizeHtml(html) {
return html.replace(/<[^>]*>?/gi, sanitizeTag);
}
// (tags that can be opened/closed) | (tags that stand alone)
var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
// <a href="url..." optional title>|</a>
var a_white = /^(<a\shref="((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\stitle="[^"<>]+")?\s?>|<\/a>)$/i;
// <img src="url..." optional width optional height optional alt optional title
var img_white = /^(<img\ssrc="(https?:\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\salt="[^"<>]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
function sanitizeTag(tag) {
if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white))
return tag;
else
return "";
}
/// <summary>
/// attempt to balance HTML tags in the html string
/// by removing any unmatched opening or closing tags
/// IMPORTANT: we *assume* HTML has *already* been
/// sanitized and is safe/sane before balancing!
///
/// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
/// </summary>
function balanceTags(html) {
if (html == "")
return "";
var re = /<\/?\w+[^>]*(\s|$|>)/g;
// convert everything to lower case; this makes
// our case insensitive comparisons easier
var tags = html.toLowerCase().match(re);
// no HTML tags present? nothing to do; exit now
var tagcount = (tags || []).length;
if (tagcount == 0)
return html;
var tagname, tag;
var ignoredtags = "<p><img><br><li><hr>";
var match;
var tagpaired = [];
var tagremove = [];
var needsRemoval = false;
// loop through matched tags in forward order
for (var ctag = 0; ctag < tagcount; ctag++) {
tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
// skip any already paired tags
// and skip tags in our ignore list; assume they're self-closed
if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
continue;
tag = tags[ctag];
match = -1;
if (!/^<\//.test(tag)) {
// this is an opening tag
// search forwards (next tags), look for closing tags
for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
if (!tagpaired[ntag] && tags[ntag] == "</" + tagname + ">") {
match = ntag;
break;
}
}
}
if (match == -1)
needsRemoval = tagremove[ctag] = true; // mark for removal
else
tagpaired[match] = true; // mark paired
}
if (!needsRemoval)
return html;
// delete all orphaned tags from the string
var ctag = 0;
html = html.replace(re, function (match) {
var res = tagremove[ctag] ? "" : match;
ctag++;
return res;
});
return html;
}
})();
describe("app.helpers.textFormatter", function(){
beforeEach(function(){
this.statusMessage = factory.post();
this.formatter = app.helpers.textFormatter;
})
describe("main", function(){
it("calls mentionify, hashtagify, and markdownify", function(){
spyOn(app.helpers.textFormatter, "mentionify")
spyOn(app.helpers.textFormatter, "hashtagify")
spyOn(app.helpers.textFormatter, "markdownify")
app.helpers.textFormatter(this.statusMessage)
expect(app.helpers.textFormatter.mentionify).toHaveBeenCalled()
expect(app.helpers.textFormatter.hashtagify).toHaveBeenCalled()
expect(app.helpers.textFormatter.markdownify).toHaveBeenCalled()
})
// A couple of complex (intergration) test cases here would be rad.
})
describe(".markdownify", function(){
// NOTE: for some strange reason, links separated by just a whitespace character
// will not be autolinked; thus we join our URLS here with (" and ").
// This test will fail if our join is just (" ") -- an edge case that should be addressed.
it("autolinks", function(){
var links = ["http://google.com",
"https://joindiaspora.com",
"http://www.yahooligans.com",
"http://obama.com",
"http://japan.co.jp"]
// The join that would make this particular test fail:
//
// var formattedText = this.formatter.markdownify(links.join(" "))
var formattedText = this.formatter.markdownify(links.join(" and "))
var wrapper = $("<div>").html(formattedText);
_.each(links, function(link) {
expect(wrapper.find("a[href='" + link + "']").text()).toContain(link)
})
})
})
describe(".hashtagify", function(){
context("changes hashtags to links", function(){
it("creates links to hashtags", function(){
var formattedText = this.formatter.hashtagify("I love #parties and #rockstars and #unicorns")
var wrapper = $("<div>").html(formattedText);
_.each(["parties", "rockstars", "unicorns"], function(tagName){
expect(wrapper.find("a[href='/tags/" + tagName + "']").text()).toContain(tagName)
})
})
it("requires hashtags to be preceeded with a space", function(){
var formattedText = this.formatter.hashtagify("I love the#parties")
expect(formattedText).not.toContain('/tags/parties')
})
// NOTE THIS DIVERGES FROM GRUBER'S ORIGINAL DIALECT OF MARKDOWN.
// We had to edit Markdown.Converter.js line 747
//
// text = text.replace(/^(\#{1,6})[ \t]+(.+?)[ \t]*\#*\n+/gm,
// [ \t]* changed to [ \t]+
//
it("doesn't create a header tag if the first word is a hashtag", function(){
var formattedText = this.formatter.hashtagify("#parties, I love")
var wrapper = $("<div>").html(formattedText);
expect(wrapper.find("h1").length).toBe(0)
expect(wrapper.find("a[href='/tags/parties']").text()).toContain("#parties")
})
})
})
describe(".mentionify", function(){
context("changes mention markup to links", function(){
beforeEach(function(){
this.alice = factory.author({
name : "Alice Smith",
diaspora_id : "alice@example.com",
id : "555"
})
this.bob = factory.author({
name : "Bob Grimm",
diaspora_id : "bob@example.com",
id : "666"
})
this.statusMessage.set({text: "hey there @{Alice Smith; alice@example.com} and @{Bob Grimm; bob@example.com}"})
this.statusMessage.set({mentioned_people : [this.alice, this.bob]})
})
it("matches mentions", function(){
var formattedText = this.formatter.mentionify(this.statusMessage.get("text"), this.statusMessage.get("mentioned_people"))
var wrapper = $("<div>").html(formattedText);
_.each([this.alice, this.bob], function(person) {
expect(wrapper.find("a[href='/people/" + person.id + "']").text()).toContain(person.name)
})
})
})
})
})
...@@ -40,132 +40,6 @@ describe("app.views.Post", function(){ ...@@ -40,132 +40,6 @@ describe("app.views.Post", function(){
expect(view.$(".post_initial_info").html()).not.toContain("0 Reshares") expect(view.$(".post_initial_info").html()).not.toContain("0 Reshares")
}) })
it("should markdownify the post's text", function(){
this.statusMessage.set({text: "I have three Belly Buttons"})
spyOn(window.markdown, "toHTML")
new app.views.Post({model : this.statusMessage}).render();
expect(window.markdown.toHTML).toHaveBeenCalledWith("I have three Belly Buttons")
})
context("changes hashtags to links", function(){
it("links to a hashtag to the tag page", function(){
this.statusMessage.set({text: "I love #parties"})
var view = new app.views.Post({model : this.statusMessage}).render();
expect(view.$("a:contains('#parties')").attr('href')).toBe('/tags/parties')
})
it("changes all hashtags", function(){
this.statusMessage.set({text: "I love #parties and #rockstars and #unicorns"})
var view = new app.views.Post({model : this.statusMessage}).render();
expect(view.$("a.tag").length).toBe(3)
expect(view.$("a:contains('#parties')")).toExist();
expect(view.$("a:contains('#rockstars')")).toExist();
expect(view.$("a:contains('#unicorns')")).toExist();
})
it("requires hashtags to be preceeded with a space", function(){
this.statusMessage.set({text: "I love the#parties"})
var view = new app.views.Post({model : this.statusMessage}).render();
expect(view.$(".tag").length).toBe(0)
})
// NOTE THIS DIVERGES FROM GRUBER'S ORIGINAL DIALECT OF MARKDOWN.
// We had to edit markdown.js line 291 - good people would have made a new dialect.
//
// original : var m = block.match( /^(#{1,6})\s*(.*?)\s*#*\s*(?:\n|$)/ );
// \s* changed to \s+
//
it("doesn't create a header tag if the first word is a hashtag", function(){
this.statusMessage.set({text: "#parties, I love"})
var view = new app.views.Post({model : this.statusMessage}).render();
expect(view.$("h1:contains(parties)")).not.toExist();
expect(view.$("a:contains('#parties')")).toExist();
})
it("works on reshares", function(){
this.statusMessage.set({text: "I love #parties"})
var reshare = new app.models.Reshare(factory.post({
text : this.statusMessage.get("text"),
root : this.statusMessage
}))
var view = new app.views.Post({model : reshare}).render();
expect(view.$("a:contains('#parties')").attr('href')).toBe('/tags/parties')
})
})
context("changes mention markup to links", function(){
beforeEach(function(){
this.alice = factory.author({
name : "Alice Smith",
diaspora_id : "alice@example.com",
id : "555"
})
this.bob = factory.author({
name : "Bob Grimm",
diaspora_id : "bob@example.com",
id : "666"
})
this.statusMessage.set({mentioned_people : [this.alice, this.bob]})
this.statusMessage.set({text: "hey there @{Alice Smith; alice@example.com} and @{Bob Grimm; bob@example.com}"})
})
it("links to the mentioned person's page", function(){
var view = new app.views.Post({model : this.statusMessage}).render();
expect(view.$("a:contains('Alice Smith')").attr('href')).toBe('/people/555')
})
it("matches all mentions", function(){
var view = new app.views.Post({model : this.statusMessage}).render();
expect(view.$("a.mention").length).toBe(2)
})
it("works on reshares", function(){
var reshare = new app.models.Reshare(factory.post({
text : this.statusMessage.get("text"),
mentioned_people : this.statusMessage.get("mentioned_people"),
root : this.statusMessage
}))
var view = new app.views.Post({model : reshare}).render();
expect(view.$("a.mention").length).toBe(2)
})
})
context("generates urls from plaintext", function(){
it("works", function(){
links = ["http://google.com",
"https://joindiaspora.com",
"http://www.yahooligans.com",
"http://obama.com",
"http://japan.co.jp"]
this.statusMessage.set({text : links.join(" ")})
var view = new app.views.Post({model : this.statusMessage}).render();
_.each(links, function(link) {
expect(view.$("a[href='" + link + "']").text()).toContain(link)
})
})
it("works with urls that use #! syntax (i'm looking at you, twitter)')", function(){
link = "http://twitter.com/#!/hashbangs?gross=true"
this.statusMessage.set({text : link})
var view = new app.views.Post({model : this.statusMessage}).render();
expect(view.$("a[href='" + link + "']").text()).toContain(link)
})
it("doesn't create link tags for links that are already in <a/> or <img/> tags", function(){
link = "http://google.com"
this.statusMessage.set({text : "![cats](http://google.com/cats)"})
var view = new app.views.Content({model : this.statusMessage})
expect(view.presenter().text).toNotContain('</a>')
})
})
context("embed_html", function(){ context("embed_html", function(){
it("provides oembed html from the model response", function(){ it("provides oembed html from the model response", function(){
......
...@@ -22,7 +22,7 @@ src_files: ...@@ -22,7 +22,7 @@ src_files:
- public/javascripts/vendor/jquery.charcount.js - public/javascripts/vendor/jquery.charcount.js
- public/javascripts/vendor/timeago.js - public/javascripts/vendor/timeago.js
- public/javascripts/vendor/facebox.js - public/javascripts/vendor/facebox.js
- public/javascripts/vendor/markdown.js - public/javascripts/vendor/markdown/*
- public/javascripts/jquery.infieldlabel-custom.js - public/javascripts/jquery.infieldlabel-custom.js
- public/javascripts/vendor/underscore.js - public/javascripts/vendor/underscore.js
- public/javascripts/vendor/backbone.js - public/javascripts/vendor/backbone.js
...@@ -36,6 +36,7 @@ src_files: ...@@ -36,6 +36,7 @@ src_files:
- public/javascripts/widgets/* - public/javascripts/widgets/*
- public/javascripts/app/app.js - public/javascripts/app/app.js
- public/javascripts/app/helpers/*
- public/javascripts/app/router.js - public/javascripts/app/router.js
- public/javascripts/app/views.js - public/javascripts/app/views.js
- public/javascripts/app/models/post.js - public/javascripts/app/models/post.js
......
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter