Truncated markup can be displayed in forums user profile view
FOR-581
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
describe "DiscussionThreadProfileView", ->
|
||||
|
||||
beforeEach ->
|
||||
|
||||
setFixtures """
|
||||
<article class="discussion-thread" id="thread_1"></article>
|
||||
<script type='text/template' id='_profile_thread'>
|
||||
<article class="discussion-article" data-id="{{id}}">
|
||||
<div class="discussion-post local">
|
||||
<div class="post-body">{{{abbreviatedBody}}}</div>
|
||||
</div>
|
||||
</article>
|
||||
</script>
|
||||
"""
|
||||
@threadData = {
|
||||
id: "1",
|
||||
body: "dummy body",
|
||||
discussion: new Discussion()
|
||||
abuse_flaggers: [],
|
||||
votes: {up_count: "42"}
|
||||
}
|
||||
@imageTag = '<img src="https://www.google.com.pk/images/srpr/logo11w.png">'
|
||||
window.MathJax = { Hub: { Queue: -> } }
|
||||
|
||||
makeView = (thread) ->
|
||||
view = new DiscussionThreadProfileView(el: $("article#thread_#{thread.id}"), model: thread)
|
||||
spyConvertMath(view)
|
||||
return view
|
||||
|
||||
makeThread = (threadData) ->
|
||||
thread = new Thread(threadData)
|
||||
thread.discussion = new Discussion()
|
||||
return thread
|
||||
|
||||
spyConvertMath = (view) ->
|
||||
spyOn(view, "convertMath").andCallFake( ->
|
||||
@model.set('markdownBody', @model.get('body'))
|
||||
)
|
||||
|
||||
checkPostWithImages = (numberOfImages, truncatedText, threadData, imageTag) ->
|
||||
expectedHtml = '<p>'
|
||||
threadData.body = '<p>'
|
||||
testText = ''
|
||||
expectedText = ''
|
||||
|
||||
if truncatedText
|
||||
testText = new Array(100).join('test ')
|
||||
expectedText = testText.substring(0, 139)+ '…'
|
||||
else
|
||||
testText = 'Test body'
|
||||
expectedText = 'Test body'
|
||||
|
||||
for i in [0..numberOfImages-1]
|
||||
threadData.body = threadData.body + imageTag
|
||||
if i == 0
|
||||
expectedHtml = expectedHtml + imageTag
|
||||
else
|
||||
expectedHtml = expectedHtml + '<em>image omitted</em>'
|
||||
|
||||
threadData.body = threadData.body + '<em>' + testText + '</em></p>'
|
||||
if numberOfImages > 1
|
||||
expectedHtml = expectedHtml + '<em>' + expectedText + '</em></p><p><em>Some images in this post have been omitted</em></p>'
|
||||
else
|
||||
expectedHtml = expectedHtml + '<em>' + expectedText + '</em></p>'
|
||||
|
||||
view = makeView(makeThread(threadData))
|
||||
view.render()
|
||||
expect(view.$el.find(".post-body").html()).toEqual(expectedHtml)
|
||||
|
||||
checkBody = (truncated, view, threadData) ->
|
||||
view.render()
|
||||
if not truncated
|
||||
expect(view.model.get("body")).toEqual(view.model.get("abbreviatedBody"))
|
||||
expect(view.$el.find(".post-body").html()).toEqual(threadData.body)
|
||||
else
|
||||
expect(view.model.get("body")).not.toEqual(view.model.get("abbreviatedBody"))
|
||||
expect(view.$el.find(".post-body").html()).not.toEqual(threadData.body)
|
||||
outputHtmlStripped = view.$el.find(".post-body").html().replace(/(<([^>]+)>)/ig,"");
|
||||
outputHtmlStripped = outputHtmlStripped.replace("Some images in this post have been omitted","")
|
||||
outputHtmlStripped = outputHtmlStripped.replace("image omitted","")
|
||||
inputHtmlStripped = threadData.body.replace(/(<([^>]+)>)/ig,"");
|
||||
expectedOutput = inputHtmlStripped.substring(0, 139)+ '…'
|
||||
expect(outputHtmlStripped).toEqual(expectedOutput)
|
||||
expect(view.$el.find(".post-body").html().indexOf("…")).toBeGreaterThan(0)
|
||||
|
||||
describe "Body markdown should be correct", ->
|
||||
|
||||
it "untruncated text without markdown body", ->
|
||||
@threadData.body = "Test body"
|
||||
view = makeView(makeThread(@threadData))
|
||||
checkBody(false, view, @threadData)
|
||||
|
||||
it "truncated text without markdown body", ->
|
||||
@threadData.body = new Array(100).join("test ")
|
||||
view = makeView(makeThread(@threadData))
|
||||
checkBody(true, view, @threadData)
|
||||
|
||||
it "untruncated text with markdown body", ->
|
||||
@threadData.body = '<p>' + @imageTag + '<em>Google top search engine</em></p>'
|
||||
view = makeView(makeThread(@threadData))
|
||||
checkBody(false, view, @threadData)
|
||||
|
||||
it "truncated text with markdown body", ->
|
||||
testText = new Array(100).join("test ")
|
||||
@threadData.body = '<p>' + @imageTag + @imageTag + '<em>' + testText + '</em></p>'
|
||||
view = makeView(makeThread(@threadData))
|
||||
checkBody(true, view, @threadData)
|
||||
|
||||
for numImages in [1, 2, 10]
|
||||
for truncatedText in [true, false]
|
||||
it "body with #{numImages} images and #{if truncatedText then "truncated" else "untruncated"} text", ->
|
||||
checkPostWithImages(numImages, truncatedText, @threadData, @imageTag)
|
||||
@@ -309,6 +309,16 @@ class @DiscussionUtil
|
||||
minLength++
|
||||
return text.substr(0, minLength) + gettext('…')
|
||||
|
||||
@abbreviateHTML: (html, minLength) ->
|
||||
# Abbreviates the html to at least minLength characters, stopping at word boundaries
|
||||
truncated_text = jQuery.truncate(html, {length: minLength, noBreaks: true, ellipsis: gettext('…')})
|
||||
$result = $("<div>" + truncated_text + "</div>")
|
||||
imagesToReplace = $result.find("img:not(:first)")
|
||||
if imagesToReplace.length > 0
|
||||
$result.append("<p><em>Some images in this post have been omitted</em></p>")
|
||||
imagesToReplace.replaceWith("<em>image omitted</em>")
|
||||
$result.html()
|
||||
|
||||
@getPaginationParams: (curPage, numPages, pageUrlFunc) =>
|
||||
delta = 2
|
||||
minPage = Math.max(curPage - delta, 1)
|
||||
|
||||
@@ -2,21 +2,20 @@ if Backbone?
|
||||
class @DiscussionThreadProfileView extends Backbone.View
|
||||
render: ->
|
||||
@template = DiscussionUtil.getTemplate("_profile_thread")
|
||||
if not @model.has('abbreviatedBody')
|
||||
@abbreviateBody()
|
||||
@convertMath()
|
||||
@abbreviateBody()
|
||||
params = $.extend(@model.toJSON(),{permalink: @model.urlFor('retrieve')})
|
||||
if not @model.get('anonymous')
|
||||
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
|
||||
@$el.html(Mustache.render(@template, params))
|
||||
@$("span.timeago").timeago()
|
||||
@convertMath()
|
||||
element = @$(".post-body")
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
|
||||
@
|
||||
|
||||
convertMath: ->
|
||||
element = @$(".post-body")
|
||||
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
|
||||
@model.set('markdownBody', DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight @model.get('body'))
|
||||
|
||||
abbreviateBody: ->
|
||||
abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140
|
||||
abbreviated = DiscussionUtil.abbreviateHTML @model.get('markdownBody'), 140
|
||||
@model.set('abbreviatedBody', abbreviated)
|
||||
|
||||
107
common/static/js/vendor/jquery.truncate.js
vendored
Executable file
107
common/static/js/vendor/jquery.truncate.js
vendored
Executable file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
Copyright (c) 2010-2012 Pathable
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
https://github.com/pathable/truncate
|
||||
**/
|
||||
(function($) {
|
||||
|
||||
// Matches trailing non-space characters.
|
||||
var chop = /(\s*\S+|\s)$/;
|
||||
|
||||
// Return a truncated html string. Delegates to $.fn.truncate.
|
||||
$.truncate = function(html, options) {
|
||||
return $('<div></div>').append(html).truncate(options).html();
|
||||
};
|
||||
|
||||
// Truncate the contents of an element in place.
|
||||
$.fn.truncate = function(options) {
|
||||
if ($.isNumeric(options)) options = {length: options};
|
||||
var o = $.extend({}, $.truncate.defaults, options);
|
||||
|
||||
return this.each(function() {
|
||||
var self = $(this);
|
||||
|
||||
if (o.noBreaks) self.find('br').replaceWith(' ');
|
||||
|
||||
var text = self.text();
|
||||
var excess = text.length - o.length;
|
||||
|
||||
if (o.stripTags) self.text(text);
|
||||
|
||||
// Chop off any partial words if appropriate.
|
||||
if (o.words && excess > 0) {
|
||||
excess = text.length - text.slice(0, o.length).replace(chop, '').length - 1;
|
||||
}
|
||||
|
||||
if (excess < 0 || !excess && !o.truncated) return;
|
||||
|
||||
// Iterate over each child node in reverse, removing excess text.
|
||||
$.each(self.contents().get().reverse(), function(i, el) {
|
||||
var $el = $(el);
|
||||
var text = $el.text();
|
||||
var length = text.length;
|
||||
|
||||
// If the text is longer than the excess, remove the node and continue.
|
||||
if (length <= excess) {
|
||||
o.truncated = true;
|
||||
excess -= length;
|
||||
$el.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the excess text and append the ellipsis.
|
||||
if (el.nodeType === 3) {
|
||||
$(el.splitText(length - excess - 1)).replaceWith(o.ellipsis);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recursively truncate child nodes.
|
||||
$el.truncate($.extend(o, {length: length - excess}));
|
||||
return false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$.truncate.defaults = {
|
||||
|
||||
// Strip all html elements, leaving only plain text.
|
||||
stripTags: false,
|
||||
|
||||
// Only truncate at word boundaries.
|
||||
words: false,
|
||||
|
||||
// Replace instances of <br> with a single space.
|
||||
noBreaks: false,
|
||||
|
||||
// The maximum length of the truncated html.
|
||||
length: Infinity,
|
||||
|
||||
// The character to use as the ellipsis. The word joiner (U+2060) can be
|
||||
// used to prevent a hanging ellipsis, but displays incorrectly in Chrome
|
||||
// on Windows 7.
|
||||
// http://code.google.com/p/chromium/issues/detail?id=68323
|
||||
ellipsis: '\u2026' // '\u2060\u2026'
|
||||
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
@@ -31,6 +31,7 @@ lib_paths:
|
||||
- js/vendor/jquery.min.js
|
||||
- js/vendor/jasmine-jquery.js
|
||||
- js/vendor/jasmine-imagediff.js
|
||||
- js/vendor/jquery.truncate.js
|
||||
- js/vendor/mustache.js
|
||||
- js/vendor/underscore-min.js
|
||||
- js/vendor/backbone-min.js
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/split.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/jquery.truncate.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/jquery.ajaxfileupload.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/Markdown.Converter.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/Markdown.Sanitizer.js')}"></script>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</span>
|
||||
</p>
|
||||
</header>
|
||||
<div class="post-body">{{abbreviatedBody}}</div>
|
||||
<div class="post-body">{{{abbreviatedBody}}}</div>
|
||||
</div>
|
||||
<div class="local post-tools">
|
||||
<a href="{{permalink}}">${_("View discussion")}</a>
|
||||
|
||||
Reference in New Issue
Block a user