Commit 8150d32b authored by danielgrippi's avatar danielgrippi

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
......@@ -9,10 +9,9 @@ javascripts:
main:
- public/javascripts/vendor/underscore.js
- public/javascripts/vendor/backbone.js
- public/javascripts/vendor/markdown.js
- public/javascripts/vendor/markdown/*
- public/javascripts/app/app.js
- public/javascripts/app/helpers/*
- public/javascripts/app/router.js
- public/javascripts/app/views.js
- public/javascripts/app/models/post.js
......
var app = {
collections: {},
models: {},
helpers: {},
views: {},
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({
presenter : function(){
var model = this.model
return _.extend(this.defaultPresenter(), {
text : metafyText(model.get("text")),
text : app.helpers.textFormatter(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){
if(!model.get("o_embed_cache")) { return ""; }
return model.get("o_embed_cache").data.html
......@@ -57,7 +13,6 @@ app.views.Content = app.views.StreamObject.extend({
}
})
app.views.StatusMessage = app.views.Content.extend({
template_name : "#status-message-template"
});
......
This diff is collapsed.
(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(){
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(){
it("provides oembed html from the model response", function(){
......
......@@ -22,7 +22,7 @@ src_files:
- public/javascripts/vendor/jquery.charcount.js
- public/javascripts/vendor/timeago.js
- public/javascripts/vendor/facebox.js
- public/javascripts/vendor/markdown.js
- public/javascripts/vendor/markdown/*
- public/javascripts/jquery.infieldlabel-custom.js
- public/javascripts/vendor/underscore.js
- public/javascripts/vendor/backbone.js
......@@ -36,6 +36,7 @@ src_files:
- public/javascripts/widgets/*
- public/javascripts/app/app.js
- public/javascripts/app/helpers/*
- public/javascripts/app/router.js
- public/javascripts/app/views.js
- public/javascripts/app/models/post.js
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment