UNPKG

@dailymotion/vast-client

Version:
337 lines (301 loc) 10.4 kB
import { util } from '../util/util'; /** * 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 `${protocol}${vastAdTagUrl}`; } if (!vastAdTagUrl.includes('://')) { // Resolve relative URLs (mainly for unit testing) const baseURL = originalUrl.slice(0, originalUrl.lastIndexOf('/')); return `${baseURL}/${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} */ export 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.${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(ads = []) { 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(ads = []) { 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(({ nodeName, nodeValue }) => { 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) { 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?.length) { // As specified by VAST specs, wrapper should not contain any mediafiles const wrapperCreativesWithIconsNode = wrapper.creatives.filter( (creative) => creative.icons?.length && !creative.mediaFiles.length ); if (wrapperCreativesWithIconsNode.length) { unwrappedAd.creatives = unwrappedAd.creatives.concat( wrapperCreativesWithIconsNode ); } } } export const parserUtils = { childByName, childrenByName, resolveVastAdTagURI, parseBoolean, parseNodeText, copyNodeAttribute, parseAttributes, parseDuration, getStandAloneAds, getSortedAdPods, assignAttributes, mergeWrapperAdData, };