@dailymotion/vast-client
Version:
JavaScript VAST Client
1,376 lines (1,303 loc) • 141 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function createAd() {
let adAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return {
id: adAttributes.id || null,
sequence: adAttributes.sequence || null,
adType: adAttributes.adType || null,
adServingId: null,
categories: [],
expires: null,
viewableImpression: [],
system: null,
title: null,
description: null,
advertiser: null,
pricing: null,
survey: null,
// @deprecated in VAST 4.1
errorURLTemplates: [],
impressionURLTemplates: [],
creatives: [],
extensions: [],
adVerifications: [],
blockedAdCategories: [],
followAdditionalWrappers: true,
allowMultipleAds: false,
fallbackOnNoAd: null
};
}
function createAdVerification() {
return {
resource: null,
vendor: null,
browserOptional: false,
apiFramework: null,
type: null,
parameters: null,
trackingEvents: {}
};
}
function createCompanionAd() {
let creativeAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return {
id: creativeAttributes.id || null,
adType: 'companionAd',
width: creativeAttributes.width || 0,
height: creativeAttributes.height || 0,
assetWidth: creativeAttributes.assetWidth || null,
assetHeight: creativeAttributes.assetHeight || null,
expandedWidth: creativeAttributes.expandedWidth || null,
expandedHeight: creativeAttributes.expandedHeight || null,
apiFramework: creativeAttributes.apiFramework || null,
adSlotId: creativeAttributes.adSlotId || null,
pxratio: creativeAttributes.pxratio || '1',
renderingMode: creativeAttributes.renderingMode || 'default',
staticResources: [],
htmlResources: [],
iframeResources: [],
adParameters: null,
altText: null,
companionClickThroughURLTemplate: null,
companionClickTrackingURLTemplates: [],
trackingEvents: {}
};
}
function isCompanionAd(ad) {
return ad.adType === 'companionAd';
}
function createCreative() {
let creativeAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return {
id: creativeAttributes.id || null,
adId: creativeAttributes.adId || null,
sequence: creativeAttributes.sequence || null,
apiFramework: creativeAttributes.apiFramework || null,
universalAdIds: [],
creativeExtensions: []
};
}
function createCreativeCompanion() {
let creativeAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
const {
id,
adId,
sequence,
apiFramework
} = createCreative(creativeAttributes);
return {
id,
adId,
sequence,
apiFramework,
type: 'companion',
required: null,
variations: []
};
}
const supportedMacros = ['ADCATEGORIES', 'ADCOUNT', 'ADPLAYHEAD', 'ADSERVINGID', 'ADTYPE', 'APIFRAMEWORKS', 'APPBUNDLE', 'ASSETURI', 'BLOCKEDADCATEGORIES', 'BREAKMAXADLENGTH', 'BREAKMAXADS', 'BREAKMAXDURATION', 'BREAKMINADLENGTH', 'BREAKMINDURATION', 'BREAKPOSITION', 'CLICKPOS', 'CLICKTYPE', 'CLIENTUA', 'CONTENTID', 'CONTENTPLAYHEAD',
// @deprecated VAST 4.1
'CONTENTURI', 'DEVICEIP', 'DEVICEUA', 'DOMAIN', 'EXTENSIONS', 'GDPRCONSENT', 'IFA', 'IFATYPE', 'INVENTORYSTATE', 'LATLONG', 'LIMITADTRACKING', 'MEDIAMIME', 'MEDIAPLAYHEAD', 'OMIDPARTNER', 'PAGEURL', 'PLACEMENTTYPE', 'PLAYERCAPABILITIES', 'PLAYERSIZE', 'PLAYERSTATE', 'PODSEQUENCE', 'REGULATIONS', 'SERVERSIDE', 'SERVERUA', 'TRANSACTIONID', 'UNIVERSALADID', 'VASTVERSIONS', 'VERIFICATIONVENDORS'];
function track(URLTemplates, macros, options) {
const URLs = resolveURLTemplates(URLTemplates, macros, options);
URLs.forEach(URL => {
if (typeof window !== 'undefined' && window !== null) {
const i = new Image();
i.src = URL;
}
});
}
/**
* Replace the provided URLTemplates with the given values
*
* @param {Array} URLTemplates - An array of tracking url templates.
* @param {Object} [macros={}] - An optional Object of parameters to be used in the tracking calls.
* @param {Object} [options={}] - An optional Object of options to be used in the tracking calls.
*/
function resolveURLTemplates(URLTemplates) {
let macros = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
const resolvedURLs = [];
const URLArray = extractURLsFromTemplates(URLTemplates);
// Set default value for invalid ERRORCODE
if (macros['ERRORCODE'] && !options.isCustomCode && !/^[0-9]{3}$/.test(macros['ERRORCODE'])) {
macros['ERRORCODE'] = 900;
}
// Calc random/time based macros
macros['CACHEBUSTING'] = addLeadingZeros(Math.round(Math.random() * 1.0e8));
macros['TIMESTAMP'] = new Date().toISOString();
// RANDOM/random is not defined in VAST 3/4 as a valid macro tho it's used by some adServer (Auditude)
macros['RANDOM'] = macros['random'] = macros['CACHEBUSTING'];
for (const macro in macros) {
macros[macro] = encodeURIComponentRFC3986(macros[macro]);
}
for (const URLTemplateKey in URLArray) {
const resolveURL = URLArray[URLTemplateKey];
if (typeof resolveURL !== 'string') {
continue;
}
resolvedURLs.push(replaceUrlMacros(resolveURL, macros));
}
return resolvedURLs;
}
/**
* Replace the macros tracking url with their value.
* If no value is provided for a supported macro and it exists in the url,
* it will be replaced by -1 as described by the VAST 4.1 iab specifications
*
* @param {String} url - Tracking url.
* @param {Object} macros - Object of macros to be replaced in the tracking calls
*/
function replaceUrlMacros(url, macros) {
url = replaceMacrosValues(url, macros);
// match any macros from the url that was not replaced
const remainingMacros = url.match(/[^[\]]+(?=])/g);
if (!remainingMacros) {
return url;
}
let supportedRemainingMacros = remainingMacros.filter(macro => supportedMacros.indexOf(macro) > -1);
if (supportedRemainingMacros.length === 0) {
return url;
}
supportedRemainingMacros = supportedRemainingMacros.reduce((accumulator, macro) => {
accumulator[macro] = -1;
return accumulator;
}, {});
return replaceMacrosValues(url, supportedRemainingMacros);
}
/**
* Replace the macros tracking url with their value.
*
* @param {String} url - Tracking url.
* @param {Object} macros - Object of macros to be replaced in the tracking calls
*/
function replaceMacrosValues(url, macros) {
let replacedMacrosUrl = url;
for (const key in macros) {
const value = macros[key];
// this will match [${key}] and %%${key}%% and replace it
replacedMacrosUrl = replacedMacrosUrl.replace(new RegExp("(?:\\[|%%)(".concat(key, ")(?:\\]|%%)"), 'g'), value);
}
return replacedMacrosUrl;
}
/**
* Extract the url/s from the URLTemplates.
* If the URLTemplates is an array of urls
* If the URLTemplates object has a url property
* If the URLTemplates is a single string
*
* @param {Array|String} URLTemplates - An array|string of url templates.
*/
function extractURLsFromTemplates(URLTemplates) {
if (Array.isArray(URLTemplates)) {
return URLTemplates.map(URLTemplate => {
return URLTemplate && URLTemplate.hasOwnProperty('url') ? URLTemplate.url : URLTemplate;
});
}
return URLTemplates;
}
/**
* Filter URLTemplates elements .
* To be valid, urls should:
* - have the same protocol as the client
* or
* - be protocol-relative urls
*
* Returns an object with two arrays
* - validUrls : An array of valid URLs
* - invalidUrls: An array of invalid URLs
*
* @param {Array} URLTemplates - A Array of string/object containing urls templates.
* @returns {Object}
*
*/
function filterUrlTemplates(URLTemplates) {
return URLTemplates.reduce((acc, urlTemplate) => {
const url = urlTemplate.url || urlTemplate;
isValidUrl(url) ? acc.validUrls.push(url) : acc.invalidUrls.push(url);
return acc;
}, {
validUrls: [],
invalidUrls: []
});
}
function isValidUrl(url) {
const regex = /^(https?:\/\/|\/\/)/;
return regex.test(url);
}
/**
* Returns a boolean after checking if the object exists in the array.
* true - if the object exists, false otherwise
*
* @param {Object} obj - The object who existence is to be checked.
* @param {Array} list - List of objects.
*/
function containsTemplateObject(obj, list) {
for (let i = 0; i < list.length; i++) {
if (isTemplateObjectEqual(list[i], obj)) {
return true;
}
}
return false;
}
/**
* Returns a boolean after comparing two Template objects.
* true - if the objects are equivalent, false otherwise
*
* @param {Object} obj1
* @param {Object} obj2
*/
function isTemplateObjectEqual(obj1, obj2) {
if (obj1 && obj2) {
const obj1Properties = Object.getOwnPropertyNames(obj1);
const obj2Properties = Object.getOwnPropertyNames(obj2);
// If number of properties is different, objects are not equivalent
if (obj1Properties.length !== obj2Properties.length) {
return false;
}
if (obj1.id !== obj2.id || obj1.url !== obj2.url) {
return false;
}
return true;
}
return false;
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
function encodeURIComponentRFC3986(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, c => "%".concat(c.charCodeAt(0).toString(16)));
}
/**
* Return a string of the input number with leading zeros defined by the length param
*
* @param {Number} input - number to convert
* @param {Number} length - length of the desired string
*
* @return {String}
*/
function addLeadingZeros(input) {
let length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 8;
return input.toString().padStart(length, '0');
}
function isNumeric(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
function flatten(arr) {
return arr.reduce((flat, toFlatten) => {
return flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten);
}, []);
}
/**
* Joins two arrays of objects without duplicates
*
* @param {Array} arr1
* @param {Array} arr2
*
* @return {Array}
*/
function joinArrayOfUniqueTemplateObjs() {
let arr1 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
let arr2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
const firstArr = Array.isArray(arr1) ? arr1 : [];
const secondArr = Array.isArray(arr2) ? arr2 : [];
const arr = firstArr.concat(secondArr);
return arr.reduce((res, val) => {
if (!containsTemplateObject(val, res)) {
res.push(val);
}
return res;
}, []);
}
/**
* Check if a provided value is a valid time value according to the IAB definition
* Check if a provided value is a valid time value according to the IAB definition: Must be a positive number or -1.
* if not implemented by ad unit or -2 if value is unknown.
* @param {Number} time
*
* @return {Boolean}
*/
function isValidTimeValue(time) {
return Number.isFinite(time) && time >= -2;
}
/**
* Check if we are in a browser environment
* @returns {Boolean}
*/
function isBrowserEnvironment() {
return typeof window !== 'undefined';
}
function formatMacrosValues(macros) {
return typeof macros !== 'object' ? macros : JSON.stringify(macros);
}
const util = {
track,
resolveURLTemplates,
extractURLsFromTemplates,
filterUrlTemplates,
containsTemplateObject,
isTemplateObjectEqual,
encodeURIComponentRFC3986,
replaceUrlMacros,
isNumeric,
flatten,
joinArrayOfUniqueTemplateObjs,
isValidTimeValue,
addLeadingZeros,
isValidUrl,
isBrowserEnvironment,
formatMacrosValues
};
/**
* This module provides support methods to the parsing classes.
*/
/**
* Returns the first element of the given node which nodeName matches the given name.
* @param {Node} node - The node to use to find a match.
* @param {String} name - The name to look for.
* @return {Object|undefined}
*/
function childByName(node, name) {
const childNodes = Array.from(node.childNodes);
return childNodes.find(childNode => childNode.nodeName === name);
}
/**
* Returns all the elements of the given node which nodeName match the given name.
* @param {Node} node - The node to use to find the matches.
* @param {String} name - The name to look for.
* @return {Array}
*/
function childrenByName(node, name) {
const childNodes = Array.from(node.childNodes);
return childNodes.filter(childNode => childNode.nodeName === name);
}
/**
* Converts relative vastAdTagUri.
* @param {String} vastAdTagUrl - The url to resolve.
* @param {String} originalUrl - The original url.
* @return {String}
*/
function resolveVastAdTagURI(vastAdTagUrl, originalUrl) {
if (!originalUrl) {
return vastAdTagUrl;
}
if (vastAdTagUrl.startsWith('//')) {
const {
protocol
} = location;
return "".concat(protocol).concat(vastAdTagUrl);
}
if (!vastAdTagUrl.includes('://')) {
// Resolve relative URLs (mainly for unit testing)
const baseURL = originalUrl.slice(0, originalUrl.lastIndexOf('/'));
return "".concat(baseURL, "/").concat(vastAdTagUrl);
}
return vastAdTagUrl;
}
/**
* Converts a boolean string into a Boolean.
* @param {String} booleanString - The boolean string to convert.
* @return {Boolean}
*/
function parseBoolean(booleanString) {
return ['true', 'TRUE', 'True', '1'].includes(booleanString);
}
/**
* Parses a node text (for legacy support).
* @param {Object} node - The node to parse the text from.
* @return {String}
*/
function parseNodeText(node) {
return node && (node.textContent || node.text || '').trim();
}
/**
* Copies an attribute from a node to another.
* @param {String} attributeName - The name of the attribute to clone.
* @param {Object} nodeSource - The source node to copy the attribute from.
* @param {Object} nodeDestination - The destination node to copy the attribute at.
*/
function copyNodeAttribute(attributeName, nodeSource, nodeDestination) {
const attributeValue = nodeSource.getAttribute(attributeName);
if (attributeValue) {
nodeDestination.setAttribute(attributeName, attributeValue);
}
}
/**
* Converts element attributes into an object, where object key is attribute name
* and object value is attribute value
* @param {Element} element
* @returns {Object}
*/
function parseAttributes(element) {
const nodeAttributes = Array.from(element.attributes);
return nodeAttributes.reduce((acc, nodeAttribute) => {
acc[nodeAttribute.nodeName] = nodeAttribute.nodeValue;
return acc;
}, {});
}
/**
* Parses a String duration into a Number.
* @param {String} durationString - The dureation represented as a string.
* @return {Number}
*/
function parseDuration(durationString) {
if (durationString === null || typeof durationString === 'undefined') {
return -1;
}
// Some VAST doesn't have an HH:MM:SS duration format but instead jus the number of seconds
if (util.isNumeric(durationString)) {
return parseInt(durationString);
}
const durationComponents = durationString.split(':');
if (durationComponents.length !== 3) {
return -1;
}
const secondsAndMS = durationComponents[2].split('.');
let seconds = parseInt(secondsAndMS[0]);
if (secondsAndMS.length === 2) {
seconds += parseFloat("0.".concat(secondsAndMS[1]));
}
const minutes = parseInt(durationComponents[1] * 60);
const hours = parseInt(durationComponents[0] * 60 * 60);
if (isNaN(hours) || isNaN(minutes) || isNaN(seconds) || minutes > 60 * 60 || seconds > 60) {
return -1;
}
return hours + minutes + seconds;
}
/**
* Sorts and filters ads that are part of an Ad Pod.
* @param {Array} ads - An array of ad objects.
* @returns {Array} An array of sorted ad objects based on the sequence attribute.
*/
function getSortedAdPods() {
let ads = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
return ads.filter(ad => parseInt(ad.sequence, 10)).sort((a, b) => a.sequence - b.sequence);
}
/**
* Filters out AdPods of given ads array and returns only standalone ads without sequence attribute.
* @param {Array} ads - An array of ad objects.
* @returns {Array} An array of standalone ad.
*/
function getStandAloneAds() {
let ads = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
return ads.filter(ad => !parseInt(ad.sequence, 10));
}
/**
* Parses the attributes and assign them to object
* @param {Object} attributes attribute
* @param {Object} verificationObject with properties which can be assigned
*/
function assignAttributes(attributes, verificationObject) {
if (attributes) {
Array.from(attributes).forEach(_ref => {
let {
nodeName,
nodeValue
} = _ref;
if (nodeName && nodeValue && verificationObject.hasOwnProperty(nodeName)) {
let value = nodeValue;
if (typeof verificationObject[nodeName] === 'boolean') {
value = parseBoolean(value);
}
verificationObject[nodeName] = value;
}
});
}
}
/**
* Merges the data between an unwrapped ad and his wrapper.
* @param {Ad} unwrappedAd - The 'unwrapped' Ad.
* @param {Ad} wrapper - The wrapper Ad.
* @return {void}
*/
function mergeWrapperAdData(unwrappedAd, wrapper) {
var _wrapper$creatives;
unwrappedAd.errorURLTemplates = wrapper.errorURLTemplates.concat(unwrappedAd.errorURLTemplates);
unwrappedAd.impressionURLTemplates = wrapper.impressionURLTemplates.concat(unwrappedAd.impressionURLTemplates);
unwrappedAd.extensions = wrapper.extensions.concat(unwrappedAd.extensions);
if (wrapper.viewableImpression.length > 0) {
unwrappedAd.viewableImpression = [...unwrappedAd.viewableImpression, ...wrapper.viewableImpression];
}
// values from the child wrapper will be overridden
unwrappedAd.followAdditionalWrappers = wrapper.followAdditionalWrappers;
unwrappedAd.allowMultipleAds = wrapper.allowMultipleAds;
unwrappedAd.fallbackOnNoAd = wrapper.fallbackOnNoAd;
const wrapperCompanions = (wrapper.creatives || []).filter(creative => creative && creative.type === 'companion');
const wrapperCompanionClickTracking = wrapperCompanions.reduce((result, creative) => {
(creative.variations || []).forEach(variation => {
(variation.companionClickTrackingURLTemplates || []).forEach(companionClickTrackingURLTemplate => {
if (!util.containsTemplateObject(companionClickTrackingURLTemplate, result)) {
result.push(companionClickTrackingURLTemplate);
}
});
});
return result;
}, []);
unwrappedAd.creatives = wrapperCompanions.concat(unwrappedAd.creatives);
const wrapperHasVideoClickTracking = wrapper.videoClickTrackingURLTemplates && wrapper.videoClickTrackingURLTemplates.length;
const wrapperHasVideoCustomClick = wrapper.videoCustomClickURLTemplates && wrapper.videoCustomClickURLTemplates.length;
unwrappedAd.creatives.forEach(creative => {
// merge tracking events
if (wrapper.trackingEvents && wrapper.trackingEvents[creative.type]) {
for (const eventName in wrapper.trackingEvents[creative.type]) {
const urls = wrapper.trackingEvents[creative.type][eventName];
if (!Array.isArray(creative.trackingEvents[eventName])) {
creative.trackingEvents[eventName] = [];
}
creative.trackingEvents[eventName] = creative.trackingEvents[eventName].concat(urls);
}
}
if (creative.type === 'linear') {
// merge video click tracking url
if (wrapperHasVideoClickTracking) {
creative.videoClickTrackingURLTemplates = creative.videoClickTrackingURLTemplates.concat(wrapper.videoClickTrackingURLTemplates);
}
// merge video custom click url
if (wrapperHasVideoCustomClick) {
creative.videoCustomClickURLTemplates = creative.videoCustomClickURLTemplates.concat(wrapper.videoCustomClickURLTemplates);
}
// VAST 2.0 support - Use Wrapper/linear/clickThrough when Inline/Linear/clickThrough is null
if (wrapper.videoClickThroughURLTemplate && (creative.videoClickThroughURLTemplate === null || typeof creative.videoClickThroughURLTemplate === 'undefined')) {
creative.videoClickThroughURLTemplate = wrapper.videoClickThroughURLTemplate;
}
}
// pass wrapper companion trackers to all companions
if (creative.type === 'companion' && wrapperCompanionClickTracking.length) {
(creative.variations || []).forEach(variation => {
variation.companionClickTrackingURLTemplates = util.joinArrayOfUniqueTemplateObjs(variation.companionClickTrackingURLTemplates, wrapperCompanionClickTracking);
});
}
});
if (wrapper.adVerifications) {
// As specified by VAST specs unwrapped ads should contains wrapper adVerification script
unwrappedAd.adVerifications = unwrappedAd.adVerifications.concat(wrapper.adVerifications);
}
if (wrapper.blockedAdCategories) {
unwrappedAd.blockedAdCategories = unwrappedAd.blockedAdCategories.concat(wrapper.blockedAdCategories);
}
// Merge Wrapper's creatives containing icon elements
if ((_wrapper$creatives = wrapper.creatives) !== null && _wrapper$creatives !== void 0 && _wrapper$creatives.length) {
// As specified by VAST specs, wrapper should not contain any mediafiles
const wrapperCreativesWithIconsNode = wrapper.creatives.filter(creative => {
var _creative$icons;
return ((_creative$icons = creative.icons) === null || _creative$icons === void 0 ? void 0 : _creative$icons.length) && !creative.mediaFiles.length;
});
if (wrapperCreativesWithIconsNode.length) {
unwrappedAd.creatives = unwrappedAd.creatives.concat(wrapperCreativesWithIconsNode);
}
}
}
const parserUtils = {
childByName,
childrenByName,
resolveVastAdTagURI,
parseBoolean,
parseNodeText,
copyNodeAttribute,
parseAttributes,
parseDuration,
getStandAloneAds,
getSortedAdPods,
assignAttributes,
mergeWrapperAdData
};
/**
* This module provides methods to parse a VAST CompanionAd Element.
*/
/**
* Parses a CompanionAd.
* @param {Object} creativeElement - The VAST CompanionAd element to parse.
* @param {Object} creativeAttributes - The attributes of the CompanionAd (optional).
* @return {Object} creative - The creative object.
*/
function parseCreativeCompanion(creativeElement, creativeAttributes) {
const creative = createCreativeCompanion(creativeAttributes);
creative.required = creativeElement.getAttribute('required') || null;
creative.variations = parserUtils.childrenByName(creativeElement, 'Companion').map(companionResource => {
const companionAd = createCompanionAd(parserUtils.parseAttributes(companionResource));
companionAd.htmlResources = parserUtils.childrenByName(companionResource, 'HTMLResource').reduce((urls, resource) => {
const url = parserUtils.parseNodeText(resource);
return url ? urls.concat(url) : urls;
}, []);
companionAd.iframeResources = parserUtils.childrenByName(companionResource, 'IFrameResource').reduce((urls, resource) => {
const url = parserUtils.parseNodeText(resource);
return url ? urls.concat(url) : urls;
}, []);
companionAd.staticResources = parserUtils.childrenByName(companionResource, 'StaticResource').reduce((urls, resource) => {
const url = parserUtils.parseNodeText(resource);
return url ? urls.concat({
url,
creativeType: resource.getAttribute('creativeType') || null
}) : urls;
}, []);
companionAd.altText = parserUtils.parseNodeText(parserUtils.childByName(companionResource, 'AltText')) || null;
const trackingEventsElement = parserUtils.childByName(companionResource, 'TrackingEvents');
if (trackingEventsElement) {
parserUtils.childrenByName(trackingEventsElement, 'Tracking').forEach(trackingElement => {
const eventName = trackingElement.getAttribute('event');
const trackingURLTemplate = parserUtils.parseNodeText(trackingElement);
if (eventName && trackingURLTemplate) {
if (!Array.isArray(companionAd.trackingEvents[eventName])) {
companionAd.trackingEvents[eventName] = [];
}
companionAd.trackingEvents[eventName].push(trackingURLTemplate);
}
});
}
companionAd.companionClickTrackingURLTemplates = parserUtils.childrenByName(companionResource, 'CompanionClickTracking').map(clickTrackingElement => {
return {
id: clickTrackingElement.getAttribute('id') || null,
url: parserUtils.parseNodeText(clickTrackingElement)
};
});
companionAd.companionClickThroughURLTemplate = parserUtils.parseNodeText(parserUtils.childByName(companionResource, 'CompanionClickThrough')) || null;
const adParametersElement = parserUtils.childByName(companionResource, 'AdParameters');
if (adParametersElement) {
companionAd.adParameters = {
value: parserUtils.parseNodeText(adParametersElement),
xmlEncoded: adParametersElement.getAttribute('xmlEncoded') || null
};
}
return companionAd;
});
return creative;
}
function createCreativeLinear() {
let creativeAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
const {
id,
adId,
sequence,
apiFramework
} = createCreative(creativeAttributes);
return {
id,
adId,
sequence,
apiFramework,
type: 'linear',
duration: 0,
skipDelay: null,
mediaFiles: [],
mezzanine: null,
interactiveCreativeFile: null,
closedCaptionFiles: [],
videoClickThroughURLTemplate: null,
videoClickTrackingURLTemplates: [],
videoCustomClickURLTemplates: [],
adParameters: null,
icons: [],
trackingEvents: {}
};
}
function isCreativeLinear(ad) {
return ad.type === 'linear';
}
function createClosedCaptionFile() {
let closedCaptionAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return {
type: closedCaptionAttributes.type || null,
language: closedCaptionAttributes.language || null,
fileURL: null
};
}
function createIcon() {
return {
program: null,
height: 0,
width: 0,
xPosition: 0,
yPosition: 0,
apiFramework: null,
offset: null,
duration: 0,
type: null,
staticResource: null,
htmlResource: null,
iframeResource: null,
pxratio: '1',
iconClickThroughURLTemplate: null,
iconClickTrackingURLTemplates: [],
iconViewTrackingURLTemplate: null,
iconClickFallbackImages: [],
altText: null,
hoverText: null
};
}
function createInteractiveCreativeFile() {
let interactiveCreativeAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return {
type: interactiveCreativeAttributes.type || null,
apiFramework: interactiveCreativeAttributes.apiFramework || null,
variableDuration: parserUtils.parseBoolean(interactiveCreativeAttributes.variableDuration),
fileURL: null
};
}
function createMediaFile() {
return {
id: null,
fileURL: null,
fileSize: 0,
deliveryType: 'progressive',
mimeType: null,
mediaType: null,
codec: null,
bitrate: 0,
minBitrate: 0,
maxBitrate: 0,
width: 0,
height: 0,
apiFramework: null,
// @deprecated in VAST 4.1. <InteractiveCreativeFile> should be used instead.
scalable: null,
maintainAspectRatio: null
};
}
function createMezzanine() {
return {
id: null,
fileURL: null,
delivery: null,
codec: null,
type: null,
width: 0,
height: 0,
fileSize: 0,
mediaType: '2D'
};
}
/**
* This module provides methods to parse a VAST Linear Element.
*/
/**
* Parses a Linear element.
* @param {Object} creativeElement - The VAST Linear element to parse.
* @param {any} creativeAttributes - The attributes of the Linear (optional).
* @return {Object} creative - The creativeLinear object.
*/
function parseCreativeLinear(creativeElement, creativeAttributes) {
let offset;
const creative = createCreativeLinear(creativeAttributes);
creative.duration = parserUtils.parseDuration(parserUtils.parseNodeText(parserUtils.childByName(creativeElement, 'Duration')));
const skipOffset = creativeElement.getAttribute('skipoffset');
if (typeof skipOffset === 'undefined' || skipOffset === null) {
creative.skipDelay = null;
} else if (skipOffset.charAt(skipOffset.length - 1) === '%' && creative.duration !== -1) {
const percent = parseInt(skipOffset, 10);
creative.skipDelay = creative.duration * (percent / 100);
} else {
creative.skipDelay = parserUtils.parseDuration(skipOffset);
}
const videoClicksElement = parserUtils.childByName(creativeElement, 'VideoClicks');
if (videoClicksElement) {
const videoClickThroughElement = parserUtils.childByName(videoClicksElement, 'ClickThrough');
if (videoClickThroughElement) {
creative.videoClickThroughURLTemplate = {
id: videoClickThroughElement.getAttribute('id') || null,
url: parserUtils.parseNodeText(videoClickThroughElement)
};
} else {
creative.videoClickThroughURLTemplate = null;
}
parserUtils.childrenByName(videoClicksElement, 'ClickTracking').forEach(clickTrackingElement => {
creative.videoClickTrackingURLTemplates.push({
id: clickTrackingElement.getAttribute('id') || null,
url: parserUtils.parseNodeText(clickTrackingElement)
});
});
parserUtils.childrenByName(videoClicksElement, 'CustomClick').forEach(customClickElement => {
creative.videoCustomClickURLTemplates.push({
id: customClickElement.getAttribute('id') || null,
url: parserUtils.parseNodeText(customClickElement)
});
});
}
const adParamsElement = parserUtils.childByName(creativeElement, 'AdParameters');
if (adParamsElement) {
creative.adParameters = {
value: parserUtils.parseNodeText(adParamsElement),
xmlEncoded: adParamsElement.getAttribute('xmlEncoded') || null
};
}
parserUtils.childrenByName(creativeElement, 'TrackingEvents').forEach(trackingEventsElement => {
parserUtils.childrenByName(trackingEventsElement, 'Tracking').forEach(trackingElement => {
let eventName = trackingElement.getAttribute('event');
const trackingURLTemplate = parserUtils.parseNodeText(trackingElement);
if (eventName && trackingURLTemplate) {
if (eventName === 'progress') {
offset = trackingElement.getAttribute('offset');
if (!offset) {
return;
}
if (offset.charAt(offset.length - 1) === '%') {
eventName = "progress-".concat(offset);
} else {
eventName = "progress-".concat(parserUtils.parseDuration(offset));
}
}
if (!Array.isArray(creative.trackingEvents[eventName])) {
creative.trackingEvents[eventName] = [];
}
creative.trackingEvents[eventName].push(trackingURLTemplate);
}
});
});
parserUtils.childrenByName(creativeElement, 'MediaFiles').forEach(mediaFilesElement => {
parserUtils.childrenByName(mediaFilesElement, 'MediaFile').forEach(mediaFileElement => {
creative.mediaFiles.push(parseMediaFile(mediaFileElement));
});
const interactiveCreativeElement = parserUtils.childByName(mediaFilesElement, 'InteractiveCreativeFile');
if (interactiveCreativeElement) {
creative.interactiveCreativeFile = parseInteractiveCreativeFile(interactiveCreativeElement);
}
const closedCaptionElements = parserUtils.childByName(mediaFilesElement, 'ClosedCaptionFiles');
if (closedCaptionElements) {
parserUtils.childrenByName(closedCaptionElements, 'ClosedCaptionFile').forEach(closedCaptionElement => {
const closedCaptionFile = createClosedCaptionFile(parserUtils.parseAttributes(closedCaptionElement));
closedCaptionFile.fileURL = parserUtils.parseNodeText(closedCaptionElement);
creative.closedCaptionFiles.push(closedCaptionFile);
});
}
const mezzanineElement = parserUtils.childByName(mediaFilesElement, 'Mezzanine');
const requiredAttributes = getRequiredAttributes(mezzanineElement, ['delivery', 'type', 'width', 'height']);
if (requiredAttributes) {
const mezzanine = createMezzanine();
mezzanine.id = mezzanineElement.getAttribute('id');
mezzanine.fileURL = parserUtils.parseNodeText(mezzanineElement);
mezzanine.delivery = requiredAttributes.delivery;
mezzanine.codec = mezzanineElement.getAttribute('codec');
mezzanine.type = requiredAttributes.type;
mezzanine.width = parseInt(requiredAttributes.width, 10);
mezzanine.height = parseInt(requiredAttributes.height, 10);
mezzanine.fileSize = parseInt(mezzanineElement.getAttribute('fileSize'), 10);
mezzanine.mediaType = mezzanineElement.getAttribute('mediaType') || '2D';
creative.mezzanine = mezzanine;
}
});
const iconsElement = parserUtils.childByName(creativeElement, 'Icons');
if (iconsElement) {
parserUtils.childrenByName(iconsElement, 'Icon').forEach(iconElement => {
creative.icons.push(parseIcon(iconElement));
});
}
return creative;
}
/**
* Parses the MediaFile element from VAST.
* @param {Object} mediaFileElement - The VAST MediaFile element.
* @return {Object} - Parsed mediaFile object.
*/
function parseMediaFile(mediaFileElement) {
const mediaFile = createMediaFile();
mediaFile.id = mediaFileElement.getAttribute('id');
mediaFile.fileURL = parserUtils.parseNodeText(mediaFileElement);
mediaFile.deliveryType = mediaFileElement.getAttribute('delivery');
mediaFile.codec = mediaFileElement.getAttribute('codec');
mediaFile.mimeType = mediaFileElement.getAttribute('type');
mediaFile.mediaType = mediaFileElement.getAttribute('mediaType') || '2D';
mediaFile.apiFramework = mediaFileElement.getAttribute('apiFramework');
mediaFile.fileSize = parseInt(mediaFileElement.getAttribute('fileSize') || 0);
mediaFile.bitrate = parseInt(mediaFileElement.getAttribute('bitrate') || 0);
mediaFile.minBitrate = parseInt(mediaFileElement.getAttribute('minBitrate') || 0);
mediaFile.maxBitrate = parseInt(mediaFileElement.getAttribute('maxBitrate') || 0);
mediaFile.width = parseInt(mediaFileElement.getAttribute('width') || 0);
mediaFile.height = parseInt(mediaFileElement.getAttribute('height') || 0);
const scalable = mediaFileElement.getAttribute('scalable');
if (scalable && typeof scalable === 'string') {
mediaFile.scalable = parserUtils.parseBoolean(scalable);
}
const maintainAspectRatio = mediaFileElement.getAttribute('maintainAspectRatio');
if (maintainAspectRatio && typeof maintainAspectRatio === 'string') {
mediaFile.maintainAspectRatio = parserUtils.parseBoolean(maintainAspectRatio);
}
return mediaFile;
}
/**
* Parses the InteractiveCreativeFile element from VAST MediaFiles node.
* @param {Object} interactiveCreativeElement - The VAST InteractiveCreativeFile element.
* @return {Object} - Parsed interactiveCreativeFile object.
*/
function parseInteractiveCreativeFile(interactiveCreativeElement) {
const interactiveCreativeFile = createInteractiveCreativeFile(parserUtils.parseAttributes(interactiveCreativeElement));
interactiveCreativeFile.fileURL = parserUtils.parseNodeText(interactiveCreativeElement);
return interactiveCreativeFile;
}
/**
* Parses the Icon element from VAST.
* @param {Object} iconElement - The VAST Icon element.
* @return {Object} - Parsed icon object.
*/
function parseIcon(iconElement) {
const icon = createIcon();
icon.program = iconElement.getAttribute('program');
icon.height = parseInt(iconElement.getAttribute('height') || 0);
icon.width = parseInt(iconElement.getAttribute('width') || 0);
icon.xPosition = parseXPosition(iconElement.getAttribute('xPosition'));
icon.yPosition = parseYPosition(iconElement.getAttribute('yPosition'));
icon.apiFramework = iconElement.getAttribute('apiFramework');
icon.pxratio = iconElement.getAttribute('pxratio') || '1';
icon.offset = parserUtils.parseDuration(iconElement.getAttribute('offset'));
icon.duration = parserUtils.parseDuration(iconElement.getAttribute('duration'));
icon.altText = iconElement.getAttribute('altText');
icon.hoverText = iconElement.getAttribute('hoverText');
parserUtils.childrenByName(iconElement, 'HTMLResource').forEach(htmlElement => {
icon.type = htmlElement.getAttribute('creativeType') || 'text/html';
icon.htmlResource = parserUtils.parseNodeText(htmlElement);
});
parserUtils.childrenByName(iconElement, 'IFrameResource').forEach(iframeElement => {
icon.type = iframeElement.getAttribute('creativeType') || 0;
icon.iframeResource = parserUtils.parseNodeText(iframeElement);
});
parserUtils.childrenByName(iconElement, 'StaticResource').forEach(staticElement => {
icon.type = staticElement.getAttribute('creativeType') || 0;
icon.staticResource = parserUtils.parseNodeText(staticElement);
});
const iconClicksElement = parserUtils.childByName(iconElement, 'IconClicks');
if (iconClicksElement) {
icon.iconClickThroughURLTemplate = parserUtils.parseNodeText(parserUtils.childByName(iconClicksElement, 'IconClickThrough'));
parserUtils.childrenByName(iconClicksElement, 'IconClickTracking').forEach(iconClickTrackingElement => {
icon.iconClickTrackingURLTemplates.push({
id: iconClickTrackingElement.getAttribute('id') || null,
url: parserUtils.parseNodeText(iconClickTrackingElement)
});
});
const iconClickFallbackImagesElement = parserUtils.childByName(iconClicksElement, 'IconClickFallbackImages');
if (iconClickFallbackImagesElement) {
parserUtils.childrenByName(iconClickFallbackImagesElement, 'IconClickFallbackImage').forEach(iconClickFallbackImageElement => {
icon.iconClickFallbackImages.push({
url: parserUtils.parseNodeText(iconClickFallbackImageElement) || null,
width: iconClickFallbackImageElement.getAttribute('width') || null,
height: iconClickFallbackImageElement.getAttribute('height') || null
});
});
}
}
icon.iconViewTrackingURLTemplate = parserUtils.parseNodeText(parserUtils.childByName(iconElement, 'IconViewTracking'));
return icon;
}
/**
* Parses an horizontal position into a String ('left' or 'right') or into a Number.
* @param {String} xPosition - The x position to parse.
* @return {String|Number}
*/
function parseXPosition(xPosition) {
if (['left', 'right'].indexOf(xPosition) !== -1) {
return xPosition;
}
return parseInt(xPosition || 0);
}
/**
* Parses an vertical position into a String ('top' or 'bottom') or into a Number.
* @param {String} yPosition - The x position to parse.
* @return {String|Number}
*/
function parseYPosition(yPosition) {
if (['top', 'bottom'].indexOf(yPosition) !== -1) {
return yPosition;
}
return parseInt(yPosition || 0);
}
/**
* Getting required attributes from element
* @param {Object} element - DOM element
* @param {Array} attributes - list of attributes
* @return {Object|null} null if a least one element not present
*/
function getRequiredAttributes(element, attributes) {
const values = {};
let error = false;
attributes.forEach(name => {
if (!element || !element.getAttribute(name)) {
error = true;
} else {
values[name] = element.getAttribute(name);
}
});
return error ? null : values;
}
function createCreativeNonLinear() {
let creativeAttributes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
const {
id,
adId,
sequence,
apiFramework
} = createCreative(creativeAttributes);
return {
id,
adId,
sequence,
apiFramework,
type: 'nonlinear',
variations: [],
trackingEvents: {}
};
}
function createNonLinearAd() {
return {
id: null,
width: 0,
height: 0,
expandedWidth: 0,
expandedHeight: 0,
scalable: true,
maintainAspectRatio: true,
minSuggestedDuration: 0,
apiFramework: 'static',
adType: 'nonLinearAd',
type: null,
staticResource: null,
htmlResource: null,
iframeResource: null,
nonlinearClickThroughURLTemplate: null,
nonlinearClickTrackingURLTemplates: [],
adParameters: null
};
}
function isNonLinearAd(ad) {
return ad.adType === 'nonLinearAd';
}
/**
* This module provides methods to parse a VAST NonLinear Element.
*/
/**
* Parses a NonLinear element.
* @param {any} creativeElement - The VAST NonLinear element to parse.
* @param {any} creativeAttributes - The attributes of the NonLinear (optional).
* @return {Object} creative - The CreativeNonLinear object.
*/
function parseCreativeNonLinear(creativeElement, creativeAttributes) {
const creative = createCreativeNonLinear(creativeAttributes);
parserUtils.childrenByName(creativeElement, 'TrackingEvents').forEach(trackingEventsElement => {
let eventName, trackingURLTemplate;
parserUtils.childrenByName(trackingEventsElement, 'Tracking').forEach(trackingElement => {
eventName = trackingElement.getAttribute('event');
trackingURLTemplate = parserUtils.parseNodeText(trackingElement);
if (eventName && trackingURLTemplate) {
if (!Array.isArray(creative.trackingEvents[eventName])) {
creative.trackingEvents[eventName] = [];
}
creative.trackingEvents[eventName].push(trackingURLTemplate);
}
});
});
parserUtils.childrenByName(creativeElement, 'NonLinear').forEach(nonlinearResource => {
const nonlinearAd = createNonLinearAd();
nonlinearAd.id = nonlinearResource.getAttribute('id') || null;
nonlinearAd.width = nonlinearResource.getAttribute('width');
nonlinearAd.height = nonlinearResource.getAttribute('height');
nonlinearAd.expandedWidth = nonlinearResource.getAttribute('expandedWidth');
nonlinearAd.expandedHeight = nonlinearResource.getAttribute('expandedHeight');
nonlinearAd.scalable = parserUtils.parseBoolean(nonlinearResource.getAttribute('scalable'));
nonlinearAd.maintainAspectRatio = parserUtils.parseBoolean(nonlinearResource.getAttribute('maintainAspectRatio'));
nonlinearAd.minSuggestedDuration = parserUtils.parseDuration(nonlinearResource.getAttribute('minSuggestedDuration'));
nonlinearAd.apiFramework = nonlinearResource.getAttribute('apiFramework');
parserUtils.childrenByName(nonlinearResource, 'HTMLResource').forEach(htmlElement => {
nonlinearAd.type = htmlElement.getAttribute('creativeType') || 'text/html';
nonlinearAd.htmlResource = parserUtils.parseNodeText(htmlElement);
});
parserUtils.childrenByName(nonlinearResource, 'IFrameResource').forEach(iframeElement => {
nonlinearAd.type = iframeElement.getAttribute('creativeType') || 0;
nonlinearAd.iframeResource = parserUtils.parseNodeText(iframeElement);
});
parserUtils.childrenByName(nonlinearResource, 'StaticResource').forEach(staticElement => {
nonlinearAd.type = staticElement.getAttribute('creativeType') || 0;
nonlinearAd.staticResource = parserUtils.parseNodeText(staticElement);
});
const adParamsElement = parserUtils.childByName(nonlinearResource, 'AdParameters');
if (adParamsElement) {
nonlinearAd.adParameters = {
value: parserUtils.parseNodeText(adParamsElement),
xmlEncoded: adParamsElement.getAttribute('xmlEncoded') || null
};
}
nonlinearAd.nonlinearClickThroughURLTemplate = parserUtils.parseNodeText(parserUtils.childByName(nonlinearResource, 'NonLinearClickThrough'));
parserUtils.childrenByName(nonlinearResource, 'NonLinearClickTracking').forEach(clickTrackingElement => {
nonlinearAd.nonlinearClickTrackingURLTemplates.push({
id: clickTrackingElement.getAttribute('id') || null,
url: parserUtils.parseNodeText(clickTrackingElement)
});
});
creative.variations.push(nonlinearAd);
});
return creative;
}
function createExtension() {
return {
name: null,
value: null,
attributes: {},
children: []
};
}
function isEmptyExtension(extension) {
return extension.value === null && Object.keys(extension.attributes).length === 0 && extension.children.length === 0;
}
/**
* Parses an array of Extension elements.
* @param {Node[]} extensions - The array of extensions to parse.
* @param {String} type - The type of extensions to parse.(Ad|Creative)
* @return {AdExtension[]|CreativeExtension[]} - The nodes parsed to extensions
*/
function parseExtensions(extensions) {
const exts = [];
extensions.forEach(extNode => {
const ext = _parseExtension(extNode);
if (ext) {
exts.push(ext);
}
});
return exts;
}
/**
* Parses an extension child node
* @param {Node} extNode - The extension node to parse
* @return {AdExtension|CreativeExtension|null} - The node parsed to extension
*/
function _parseExtension(extNode) {
// Ignore comments
if (extNode.nodeName === '#comment') return null;
const ext = createExtension();
const extNodeAttrs = extNode.attributes;
const childNodes = extNode.childNodes;
ext.name = extNode.nodeName;
// Parse attributes
if (extNode.attributes) {
for (const extNodeAttrKey in extNodeAttrs) {
if (extNodeAttrs.hasOwnProperty(extNodeAttrKey)) {
const extNodeAttr = extNodeAttrs[extNodeAttrKey];
if (extNodeAttr.nodeName && extNodeAttr.nodeValue) {
ext.attributes[extNodeAttr.nodeName] = extNodeAttr.nodeValue;
}
}
}
}
// Parse all children
for (const childNodeKey in childNodes) {
if (childNodes.hasOwnProperty(childNodeKey)) {
const parsedChild = _parseExtension(childNodes[childNodeKey]);
if (parsedChild) {
ext.children.push(parsedChild);
}
}
}
/*
Only parse value of Nodes with only eather no children or only a cdata or text
to avoid useless parsing that would result to a concatenation of all children
*/
if (ext.children.length === 0 || ext.children.length === 1 && ['#cdata-section', '#text'].indexOf(ext.children[0].name) >= 0) {
const txt = parserUtils.parseNodeText(extNode);
if (txt !== '') {
ext.value = txt;
}
// Remove the children if it's a cdata or simply text to avoid useless children
ext.children = [];
}
// Only return not empty objects to not pollute extentions
return isEmptyExtension(ext) ? null : ext;
}
/**
* Parses the creatives from the Creatives Node.
* @param {any} creativeNodes - The creative nodes to parse.
* @return {Array<Creative>} - An array of Creative objects.
*/
function parseCreatives(creativeNodes) {
const creatives = [];
creativeNodes.forEach(creativeElement => {
const creativeAttributes = {
id: creativeElement.getAttribute('id') || null,
adId: parseCreativeAdIdAttribute(creativeElement),
sequence: creativeElement.getAttribute('sequence') || null,
apiFramework: creativeElement.getAttribute('apiFramework') || null
};
const universalAdIds = [];
const universalAdIdElements = parserUtils.childrenByName(creativeElement, 'UniversalAdId');
universalAdIdElements.forEach(universalAdIdElement => {
const universalAdId = {
idRegistry: universalAdIdElement.getAttribute('idRegistry') || 'unknown',
value: parserUtils.parseNodeText(universalAdIdElement)
};
universalAdIds.push(universalAdId);
});
let creativeExtensions;
const creativeExtensionsElement = parserUtils.childByName(creativeElement, 'CreativeExtensions');
if (creativeExtensionsElement) {
creativeExtensions = parseExtensions(parserUtils.childrenByName(creativeExtensionsElement, 'CreativeExtension'));
}
for (const creativeTypeElementKey in creativeElement.childNodes) {
const creativeTypeElement = creativeElement.childNodes[creativeTypeElementKey];
let parsedCreative;
switch (creativeTypeElement.nodeName) {
case 'Linear':
parsedCreative = parseCreativeLinear(creativeTypeElement, creativeAttributes);
break;
case 'NonLinearAds':
parsedCreative = parseCreativeNonLinear(creativeTypeElement, creativeAttributes);
break;
case 'CompanionAds':
parsedCreative = parseCreativeCompanion(creativeTypeElement, creativeAttributes);
break;
}
if (parsedCreative) {
if (universalAdIds) {
parsedCreative.universalAdIds = universalAdIds;
}
if (creativeExtensions) {
parsedCreative.creativeExtensions = creativeExtensions;
}
creatives.push(parsedCreative);
}
}
});
return creatives;
}
/**
* Parses the creative adId Attribute.
* @param {any} creativeElement - The creative element to retrieve the adId from.
* @return {String|null}
*/
function parseCreativeAdIdAttribute(creativeElement) {
return creativeElement.getAttribute('AdID') ||
// VAST 2 spec
creativeElement.getAttribute('adID') ||
// VAST 3 spec
creativeElement.getAttribute('adId') ||
// VAST 4 spec
null;
}
const requiredValues = {
Wrapper: {
subElements: ['VASTAdTagURI', 'Impression']
},
BlockedAdCategories: {
attributes: ['authority']
},
InLine: {
subElements: ['AdSystem', 'AdTitle', 'Impression', 'AdServingId', 'Creatives']
},
Category: {
attributes: ['authority']
},
Pricing: {
attributes: ['model', 'currency']
},
Verification: {
oneOfinLineResources: ['JavaScriptResource', 'ExecutableResource'],
attributes: ['vendor']
},
UniversalAdId: {
attributes: ['idRegistry']
},
JavaScriptResource: {
attributes: ['apiFramework', 'browserOptional']
},
ExecutableResource: {
attributes: ['apiFramework', 'type']
},
Tracking: {
attributes: ['event']
},
Creatives: {
subElements: ['Creative']
},
Creative: {
sub