diff --git a/3p/integration.js b/3p/integration.js index 149213a020a0..ab39988915d0 100644 --- a/3p/integration.js +++ b/3p/integration.js @@ -36,13 +36,14 @@ import {twitter} from './twitter'; import {register, run} from '../src/3p'; import {parseUrl} from '../src/url'; import {assert} from '../src/asserts'; +import {taboola} from '../ads/taboola'; /** * Whether the embed type may be used with amp-embed tag. * @const {!Object} */ const AMP_EMBED_ALLOWED = { - /* embed type: true */ + taboola: true }; register('a9', a9); @@ -51,6 +52,7 @@ register('adsense', adsense); register('adtech', adtech); register('plista', plista); register('doubleclick', doubleclick); +register('taboola', taboola); register('_ping_', function(win, data) { win.document.getElementById('c').textContent = data.ping; }); diff --git a/ads/_config.js b/ads/_config.js index 1f3c8eef1074..5fa606c3c567 100644 --- a/ads/_config.js +++ b/ads/_config.js @@ -37,6 +37,7 @@ export const adPrefetch = { export const adPreconnect = { adreactor: 'https://adserver.adreactor.com', adsense: 'https://googleads.g.doubleclick.net', + taboola: 'https://cdn.taboola.com', doubleclick: [ 'https://partner.googleadservices.com', 'https://securepubads.g.doubleclick.net', diff --git a/ads/taboola.js b/ads/taboola.js new file mode 100644 index 000000000000..333a1829f485 --- /dev/null +++ b/ads/taboola.js @@ -0,0 +1,74 @@ +/** + * Copyright 2015 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {loadScript, validateDataExists, validateExactlyOne} from '../src/3p'; + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function taboola(global, data) { + // do not copy the following attributes from the 'data' object + // to _tablloa global object + const blackList = ['height', 'initialWindowHeight', 'initialWindowWidth', + 'type', 'width', 'placement', 'mode']; + + // ensure we have vlid publisher, placement and mode + // and exactly one page-type + validateDataExists(data, ['publisher', 'placement', 'mode']); + validateExactlyOne(data, ['article', 'video', 'photo', 'search', 'category', + 'homepage', 'others']); + + // setup default values for referrer and url + const params = { + referrer: data.referrer || global.context.referrer, + url: data.url || global.context.canonicalUrl + }; + + // copy none blacklisted attribute to the 'params' map + Object.keys(data).forEach(k => { + if (blackList.indexOf(k) === -1) { + params[k] = data[k]; + } + }); + + // push the two object into the '_taboola' global + (global._taboola = global._taboola || []).push([{ + viewId: global.context.pageViewId, + publisher: data.publisher, + placement: data.placement, + mode: data.mode, + framework: 'amp', + container: 'c' + }, + params]); + + // install observation on entering/leaving the view + global.context.observeIntersection(function(changes) { + changes.forEach(function(c) { + if (c.intersectionRect.height) { + global._taboola.push({ + visible: true, + rects: c, + placement: data.placement + }); + } + }); + }); + + // load the taboola loader asynchronously + loadScript(global, `https://cdn.taboola.com/libtrc/${encodeURIComponent(data.publisher)}/loader.js`); +} diff --git a/ads/taboola.md b/ads/taboola.md new file mode 100644 index 000000000000..c325381b00d0 --- /dev/null +++ b/ads/taboola.md @@ -0,0 +1,51 @@ + + +# Taboola + +## Example + +### Basic + +```html + + +``` + +## Configuration + +For semantics of configuration, please see ad network documentation. + +Supported parameters: + +- data-publisher +- data-placement +- data-mode +- data-article +- data-video +- data-photo +- data-home +- data-category +- data-others +- data-url +- data-referrer diff --git a/examples/ads.amp.html b/examples/ads.amp.html index daec6c95d66b..618545f6c7e0 100644 --- a/examples/ads.amp.html +++ b/examples/ads.amp.html @@ -129,9 +129,19 @@

plista

data-widgetname="iAMP_2" data-geo="de" data-urlprefix="" - data-categories="politik" - > + data-categories="politik"> +

Taboola responsive widget

+ + + diff --git a/examples/responsive.amp.html b/examples/responsive.amp.html index fe462275d3d4..2ddf1e3a8545 100644 --- a/examples/responsive.amp.html +++ b/examples/responsive.amp.html @@ -312,6 +312,18 @@

Lorem Ipsum

felis aliquet maximus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.

+ + + + + diff --git a/src/3p.js b/src/3p.js index 3246ae1e90a9..01dffd3cd1cd 100644 --- a/src/3p.js +++ b/src/3p.js @@ -143,6 +143,41 @@ export function checkData(data, allowedFields) { } } +/** + * Throws an exception if data does not contains a mandatory field. + * @param {!Object} data + * @param {!Array} mandatoryFields + */ +export function validateDataExists(data, mandatoryFields) { + for (let i = 0; i < mandatoryFields.length; i++) { + const field = mandatoryFields[i]; + assert(data[field], + 'Missing attribute for %s: %s.', data.type, field); + } +} + +/** + * Throws an exception if data does not contains exactly one field + * mentioned in the alternativeField array. + * @param {!Object} data + * @param {!Array} alternativeFields + */ +export function validateExactlyOne(data, alternativeFields) { + let countFileds = 0; + + for (let i = 0; i < alternativeFields.length; i++) { + const field = alternativeFields[i]; + if (data[field]) { + countFileds += 1; + } + } + + assert(countFileds === 1, + '%s must contain exactly one of attributes: %s.', + data.type, + alternativeFields.join(', ')); +} + /** * Throws an exception if data contains a field not supported * by this embed type. diff --git a/test/functional/test-3p.js b/test/functional/test-3p.js index aa48f54dd41b..eee5de8e91e4 100644 --- a/test/functional/test-3p.js +++ b/test/functional/test-3p.js @@ -14,8 +14,8 @@ * limitations under the License. */ -import {validateSrcPrefix, validateSrcContains, checkData, validateData} - from '../../src/3p'; +import {validateSrcPrefix, validateSrcContains, checkData, validateData, + validateDataExists, validateExactlyOne} from '../../src/3p'; import * as sinon from 'sinon'; describe('3p', () => { @@ -76,6 +76,40 @@ describe('3p', () => { clock.tick(1); }); + it('should accept supplied data', () => { + validateDataExists({ + width: '', + height: false, + initialWindowWidth: 1, + initialWindowHeight: 2, + type: "taboola", + referrer: true, + canonicalUrl: true, + pageViewId: true, + location: true, + mode: true, + }, []); + clock.tick(1); + + validateDataExists({ + width: "", + type: "taboola", + foo: true, + bar: true, + }, ['foo', 'bar']); + clock.tick(1); + }); + + it('should accept supplied data', () => { + validateExactlyOne({ + width: "", + type: "taboola", + foo: true, + bar: true, + }, ['foo', 'day', 'night']); + clock.tick(1); + }); + it('should complain about unexpected args', () => { checkData({ type: 'TEST', @@ -96,5 +130,29 @@ describe('3p', () => { }, ['not-whitelisted', 'foo']); }).to.throw(/Unknown attribute for TEST: not-whitelisted2./); }); + + it('should complain about missing args', () => { + + expect(() => { + validateDataExists({ + width: "", + type: "xxxxxx", + foo: true, + bar: true, + }, ['foo', 'bar', 'persika']); + }).to.throw(/Missing attribute for xxxxxx: persika./); + + expect(() => { + validateExactlyOne({ + width: "", + type: "xxxxxx", + foo: true, + bar: true, + }, ['red', 'green', 'blue']); + }).to.throw( + /xxxxxx must contain exactly one of attributes: red, green, blue./); + }); + + });