diff --git a/lib/amperize.js b/lib/amperize.js index 1651831..9778594 100644 --- a/lib/amperize.js +++ b/lib/amperize.js @@ -1,16 +1,16 @@ 'use strict'; -var merge = require('lodash.merge') - , EventEmitter = require('events').EventEmitter +var EventEmitter = require('events').EventEmitter , emits = require('emits') , html = require('htmlparser2') , util = require('util') , uuid = require('uuid') , async = require('async') , url = require('url') - , http = require('http') - , https = require('https') + , got = require('got') + , _ = require('lodash') , sizeOf = require('image-size') + , validator = require('validator') , helpers = require('./helpers'); var DEFAULTS = { @@ -32,8 +32,6 @@ var DEFAULTS = { } }; -var called; - /** * Amperizer constructor. Borrows from Minimize. * @@ -44,7 +42,7 @@ var called; * @api public */ function Amperize(options) { - this.config = merge({}, DEFAULTS, options || {}); + this.config = _.merge({}, DEFAULTS, options || {}); this.emits = emits; this.htmlParser = new html.Parser( @@ -105,19 +103,11 @@ Amperize.prototype.traverse = function traverse(data, html, done) { var children; function close(error, html) { - if (error) { - return step(error); - } - html += helpers.close(element); step(null, html); } function enter(error) { - if (error) { - return step(error); - } - children = element.children; html += helpers[element.type](element); @@ -129,21 +119,21 @@ Amperize.prototype.traverse = function traverse(data, html, done) { } function useSecureSchema(element) { - if (element.attribs && element.attribs.src) { - // Every src attribute must be with 'https' protocol otherwise it will not get validated by AMP. - // If we're unable to replace it, we will deal with the valitation error, but at least - // we tried. - if (element.attribs.src.indexOf('https://') === -1) { - if (element.attribs.src.indexOf('http://') === 0) { - // Replace 'http' with 'https', so the validation passes - element.attribs.src = element.attribs.src.replace(/^http:\/\//i, 'https://'); - } else if (element.attribs.src.indexOf('//') === 0) { - // Giphy embedded iFrames are without protocol and start with '//', so at least - // we can fix those cases. - element.attribs.src = 'https:' + element.attribs.src; - } - } + if (element.attribs && element.attribs.src) { + // Every src attribute must be with 'https' protocol otherwise it will not get validated by AMP. + // If we're unable to replace it, we will deal with the valitation error, but at least + // we tried. + if (element.attribs.src.indexOf('https://') === -1) { + if (element.attribs.src.indexOf('http://') === 0) { + // Replace 'http' with 'https', so the validation passes + element.attribs.src = element.attribs.src.replace(/^http:\/\//i, 'https://'); + } else if (element.attribs.src.indexOf('//') === 0) { + // Giphy embedded iFrames are without protocol and start with '//', so at least + // we can fix those cases. + element.attribs.src = 'https:' + element.attribs.src; + } } + } return; } @@ -170,49 +160,52 @@ Amperize.prototype.traverse = function traverse(data, html, done) { * @return {Object} element incl. width and height */ function getImageSize(element) { - var options = url.parse(element.attribs.src), - timeout = 5000, - request = element.attribs.src.indexOf('https') === 0 ? https : http; + var imageObj = url.parse(element.attribs.src), + requestOptions, + timeout = 3000; - called = false; + if (!validator.isURL(imageObj.href)) { + // revert this element, do not show + element.name = 'img'; - // We need the user-agent, otherwise some https request may fail (e. g. cloudfare) - options.headers = { 'User-Agent': 'Mozilla/5.0' }; - - return request.get(options, function (response) { - var chunks = []; - response.on('data', function (chunk) { - chunks.push(chunk); - }).on('end', function () { - try { - var dimensions = sizeOf(Buffer.concat(chunks)); - element.attribs.width = dimensions.width; - element.attribs.height = dimensions.height; - - return getLayoutAttribute(element); - } catch (err) { - if (called) return; - called = true; - - // revert this element, do not show - element.name = 'img'; - return enter(); - } - }); - }).on('socket', function (socket) { - socket.setTimeout(timeout); - socket.on('timeout', function () { - if (called) return; - called = true; - - // revert this element, do not show - element.name = 'img'; - return enter(); - }); - }).on('error', function () { - if (called) return; - called = true; + return enter(); + } + // We need the user-agent, otherwise some https request may fail (e. g. cloudfare) + requestOptions = { + headers: { + 'User-Agent': 'Mozilla/5.0' + }, + timeout: timeout, + encoding: null + }; + + return got ( + imageObj.href, + requestOptions + ).then(function (response) { + try { + // Using the Buffer rather than an URL requires to use sizeOf synchronously. + // See https://github.com/image-size/image-size#asynchronous + var dimensions = sizeOf(response.body); + + // CASE: `.ico` files might have multiple images and therefore multiple sizes. + // We return the largest size found (image-size default is the first size found) + if (dimensions.images) { + dimensions.width = _.maxBy(dimensions.images, function (w) {return w.width;}).width; + dimensions.height = _.maxBy(dimensions.images, function (h) {return h.height;}).height; + } + + element.attribs.width = dimensions.width; + element.attribs.height = dimensions.height; + + return getLayoutAttribute(element); + } catch (err) { + // revert this element, do not show + element.name = 'img'; + return enter(); + } + }).catch(function (err) { // revert this element, do not show element.name = 'img'; return enter(); @@ -229,7 +222,7 @@ Amperize.prototype.traverse = function traverse(data, html, done) { if (!element.attribs.width || !element.attribs.height || !element.attribs.layout) { if (element.attribs.src.indexOf('http') === 0) { - return getImageSize(element); + return getImageSize(element); } } // Fallback to default values for a local image @@ -254,7 +247,7 @@ Amperize.prototype.traverse = function traverse(data, html, done) { } if (element.name === 'audio') { - element.name = 'amp-audio'; + element.name = 'amp-audio'; } useSecureSchema(element); diff --git a/package.json b/package.json index 819e970..48b0c78 100644 --- a/package.json +++ b/package.json @@ -25,23 +25,25 @@ }, "homepage": "https://github.com/jbhannah/amperize#readme", "dependencies": { - "async": "2.1.4", - "emits": "3.0.0", - "htmlparser2": "3.9.2", + "async": "^2.1.4", + "emits": "^3.0.0", + "got": "^7.1.0", + "htmlparser2": "^3.9.2", "image-size": "0.5.1", - "lodash.merge": "4.6.0", + "lodash": "^4.17.4", "nock": "^9.0.2", "rewire": "^2.5.2", - "uuid": "^3.0.0" + "uuid": "^3.0.0", + "validator": "^8.2.0" }, "devDependencies": { - "chai": "3.5.0", + "chai": "^3.5.0", "cz-conventional-changelog": "1.2.0", - "istanbul": "0.4.5", - "mocha": "3.2.0", + "istanbul": "^0.4.5", + "mocha": "^3.2.0", "semantic-release": "6.3.2", - "sinon": "1.17.7", - "sinon-chai": "2.8.0" + "sinon": "^1.17.7", + "sinon-chai": "^2.8.0" }, "config": { "commitizen": { diff --git a/test/amperize.test.js b/test/amperize.test.js index b50cb63..80187d0 100644 --- a/test/amperize.test.js +++ b/test/amperize.test.js @@ -95,7 +95,7 @@ describe('Amperize', function () { sizeOfMock = nock('http://static.wixstatic.com') .get('/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256') .reply(200, { - data: '' + body: '' }); sizeOfStub.returns({width: 50, height: 50, type: 'jpg'}); @@ -103,7 +103,6 @@ describe('Amperize', function () { amperize.parse('', function (error, result) { expect(result).to.exist; - expect(Amperize.__get__('called')).to.be.equal(false); expect(result).to.contain('' + body: '' }); sizeOfStub.returns({width: 350, height: 200, type: 'jpg'}); @@ -126,7 +125,6 @@ describe('Amperize', function () { amperize.parse('', function (error, result) { expect(result).to.exist; - expect(Amperize.__get__('called')).to.be.equal(false); expect(result).to.contain(' into when width and height is set and overwrites it', function (done) { + sizeOfMock = nock('http://somestockwebsite.com') + .get('/image.jpg') + .reply(200, { + body: '' + }); + + sizeOfStub.returns({width: 350, height: 200, type: 'jpg'}); + Amperize.__set__('sizeOf', sizeOfStub); + + amperize.parse('', function (error, result) { + expect(result).to.exist; + expect(result).to.contain(''); + done(); + }); + }); + + it('transforms into does not overwrite layout attribute', function (done) { + sizeOfMock = nock('http://somestockwebsite.com') + .get('/image.jpg') + .reply(200, { + body: '' + }); + + sizeOfStub.returns({width: 350, height: 200, type: 'jpg'}); + Amperize.__set__('sizeOf', sizeOfStub); + + amperize.parse('', function (error, result) { + expect(result).to.exist; + expect(result).to.contain(''); + done(); + }); + }); + + it('returns largest image value for .ico files', function (done) { + sizeOfMock = nock('https://somewebsite.com') + .get('/favicon.ico') + .reply(200, { + body: '' + }); + + sizeOfStub.returns({ + width: 32, + height: 32, + type: 'ico', + images: [ + {width: 48, height: 48}, + {width: 32, height: 32}, + {width: 16, height: 16} + ] + }); + Amperize.__set__('sizeOf', sizeOfStub); + + amperize.parse('', function (error, result) { + expect(result).to.exist; + expect(result).to.contain(''); + done(); + }); + }); + + it('transforms .gif with only height property into with full dimensions by overriding them', function (done) { sizeOfMock = nock('https://media.giphy.com') .get('/media/l46CtzgjhTm29Cbjq/giphy.gif') .reply(200, { - data: '' + body: '' }); sizeOfStub.returns({width: 800, height: 600, type: 'gif'}); @@ -149,7 +223,6 @@ describe('Amperize', function () { amperize.parse('', function (error, result) { expect(result).to.exist; - expect(Amperize.__get__('called')).to.be.equal(false); expect(result).to.contain(' with only width property into with full dimensions withour overriding them', function (done) { + it('transforms ', function (error, result) { expect(result).to.exist; - expect(Amperize.__get__('called')).to.be.equal(false); expect(result).to.contain(' with only height property into with full dimensions without overriding them', function (done) { + amperize.parse('', function (error, result) { + expect(result).to.exist; + expect(result).to.contain(''); + expect(result).to.contain('sandbox="allow-scripts allow-same-origin"') + done(); + }); + }); + + it('transforms ', function (error, result) { + expect(result).to.exist; + expect(result).to.contain(''); + expect(result).to.contain('sandbox="allow-scripts"') + done(); + }); + }); + it('adds \'https\' protocol to '; amperize.parse(url, function (error, result) { expect(result).to.exist; - expect(Amperize.__get__('called')).to.be.equal(false); expect(result).to.contain(' into with default image dimensions', function (done) { amperize.parse('', function (error, result) { expect(result).to.exist; - expect(Amperize.__get__('called')).to.be.equal(false); expect(result).to.contain(' tag without src and does not transform it', function (done) { amperize.parse('

some text here

', function (error, result) { expect(result).to.exist; - expect(Amperize.__get__('called')).to.be.equal(false); expect(result).to.be.equal('

some text here

'); done(); }); }); + it('can handle invalid URLs', function (done) { + amperize.parse('', function (error, result) { + expect(result).to.exist; + expect(result).to.be.equal(''); + done(); + }); + }); + it('can handle '); done(); }); @@ -242,7 +345,6 @@ describe('Amperize', function () { it('transforms