diff --git a/core/server/helpers/index.js b/core/server/helpers/index.js index 80f39cdfd5..c6f8daf1d5 100644 --- a/core/server/helpers/index.js +++ b/core/server/helpers/index.js @@ -9,6 +9,7 @@ var downsize = require('downsize'), filters = require('../filters'), template = require('./template'), schema = require('../data/schema').checks, + downzero = require('../utils/downzero'), assetTemplate = _.template('<%= source %>?v=<%= version %>'), linkTemplate = _.template('<%= text %>'), @@ -290,6 +291,14 @@ coreHelpers.content = function (options) { }); if (truncateOptions.hasOwnProperty('words') || truncateOptions.hasOwnProperty('characters')) { + + // Legacy function: {{content words="0"}} should return leading tags. + if (truncateOptions.hasOwnProperty('words') && truncateOptions.words === 0) { + return new hbs.handlebars.SafeString( + downzero(this.html) + ); + } + // Due to weirdness in downsize the 'words' option // must be passed as a string. refer to #1796 // TODO: when downsize fixes this quirk remove this hack. diff --git a/core/server/utils/downzero.js b/core/server/utils/downzero.js new file mode 100644 index 0000000000..3cf214a27b --- /dev/null +++ b/core/server/utils/downzero.js @@ -0,0 +1,111 @@ +// Functions to imitate the behavior of Downsize@0.0.5 with 'words: "0"' (heavily based on Downsize) + +var stack, tagName, tagBuffer, truncatedText, parseState, pointer, + states = {unitialized: 0, tag_commenced: 1, tag_string: -1, tag_string_single: -2, comment: -3 }, + voidElements = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', + 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']; + +function getTagName(tag) { + var tagName = (tag || "").match(/<\/*([a-z0-9\:\-\_]+)/i); + return tagName ? tagName[1] : null; +} + +function closeTag(openingTag) { + var tagName = (getTagName(openingTag)) ? "" : ""; + return tagName; +} + +function downzero(text) { + + stack = []; + tagName = ''; + tagBuffer = ''; + truncatedText = ""; + parseState = 0; + pointer = 0; + + for (; pointer < text.length; pointer += 1) { + + if (parseState !== states.unitialized) { + tagBuffer += text[pointer]; + } + + switch (text[pointer]) { + case "<": + if (parseState === states.unitialized && text[pointer + 1].match(/[a-z0-9\-\_\/\!]/)) { + parseState = states.tag_commenced; + tagBuffer += text[pointer]; + } + + break; + case "!": + if (parseState === states.tag_commenced && text[pointer - 1] === "<") { + parseState = states.comment; + } + + break; + case "\"": + if (parseState === states.tag_string) { + parseState = states.tag_commenced; + } else if (parseState === states.tag_string_single) { + // if double quote is found in a single quote string, ignore it and let the string finish + break; + } else if (parseState !== states.unitialized) { + parseState = states.tag_string; + } + + break; + case "'": + if (parseState === states.tag_string_single) { + parseState = states.tag_commenced; + } else if (parseState === states.tag_string) { + break; + } else if (parseState !== states.unitialized) { + parseState = states.tag_string_single; + } + + break; + case ">": + if (parseState === states.tag_commenced) { + parseState = states.unitialized; + truncatedText += tagBuffer; + tagName = getTagName(tagBuffer); + + if (tagBuffer.match(/<\s*\//) && getTagName(stack[stack.length - 1]) === tagName) { + stack.pop(); + } else if (voidElements.indexOf(tagName) < 0 && !tagBuffer.match(/\/\s*>$/)) { + stack.push(tagBuffer); + } + tagBuffer = ""; + + continue; + } + + if (parseState === states.comment && text.substring(pointer - 2, pointer) === "--") { + parseState = states.unitialized; + truncatedText += tagBuffer; + tagBuffer = ""; + + continue; + } + + break; + case "-": + break; + } + + if (!parseState) { + break; + } + } + + truncatedText += tagBuffer; + + while (stack.length) { + truncatedText += closeTag(stack.pop()); + } + + return truncatedText; +} + +module.exports = downzero; \ No newline at end of file diff --git a/core/test/unit/server_helpers_index_spec.js b/core/test/unit/server_helpers_index_spec.js index 7aa61ad328..e68357c87e 100644 --- a/core/test/unit/server_helpers_index_spec.js +++ b/core/test/unit/server_helpers_index_spec.js @@ -123,13 +123,27 @@ describe('Core Helpers', function () { rendered.string.should.equal('

'); }); + it('can truncate html to 0 words, leaving image tag with attributes', function () { + var html = '

Alternative

', + rendered = ( + helpers.content + .call( + {html: html}, + {'hash': {'words': '0'}} + ) + ); + + should.exist(rendered); + rendered.string.should.equal('

Alternative

'); + }); + it('can truncate html to 0 words, leaving first image tag & if alt text has a single quote', function () { var html = '

It\'s me!Hello World! It\'s me!

', rendered = ( helpers.content .call( - { html: html }, - { 'hash': { 'words': '0' } } + {html: html}, + {'hash': { 'words': '0' } } ) ); @@ -143,8 +157,8 @@ describe('Core Helpers', function () { rendered = ( helpers.content .call( - { html: html }, - { 'hash': { 'words': '0' } } + {html: html }, + {'hash': { 'words': '0' } } ) ); @@ -152,6 +166,62 @@ describe('Core Helpers', function () { rendered.string.should.equal('

A double quote is \'

'); }); + it('can truncate html to 0 words, leaving first image tag if it contains > & <', function () { + var html = '

', + rendered = ( + helpers.content + .call( + {html: html }, + {'hash': { 'words': '0' } } + ) + ); + + should.exist(rendered); + rendered.string.should.equal('

'); + }); + + it('can truncate html to 0 words, leaving first two image tags', function () { + var html = '

Hi

', + rendered = ( + helpers.content + .call( + {html: html }, + {'hash': { 'words': '0' } } + ) + ); + + should.exist(rendered); + rendered.string.should.equal('

'); + }); + + it('can truncate html to 0 words, removing image if text comes first', function () { + var html = '

BliBlob

', + rendered = ( + helpers.content + .call( + {html: html }, + {'hash': { 'words': '0' } } + ) + ); + + should.exist(rendered); + rendered.string.should.equal('

'); + }); + + it('can truncate html to 0 words, leaving video tag', function () { + var html = '

', + rendered = ( + helpers.content + .call( + {html: html }, + {'hash': { 'words': '0' } } + ) + ); + + should.exist(rendered); + rendered.string.should.equal('

'); + }); + it('can truncate html by character', function () { var html = '

Hello World! It\'s me!

', rendered = (