vastSelectors/index.js

/**
 * @memberof module:@mailonline/video-ad-sdk
 * @description Published as part of {@link module:@mailonline/video-ad-sdk}
 * @module vastSelectors
 */
import {
  get,
  getAll,
  getFirstChild,
  getText,
  getAttributes,
  getAttribute
} from '../xml';
import parseOffset from './helpers/parseOffset';
import getLinearCreative from './helpers/getLinearCreative';
import getLinearTrackingEvents from './getLinearTrackingEvents';
import getNonLinearTrackingEvents from './getNonLinearTrackingEvents';
import getIcons from './getIcons';

const getBooleanValue = (val) => {
  if (typeof val === 'string') {
    return val === 'true';
  }

  return Boolean(val);
};

const compareBySequence = (itemA, itemB) => {
  const itemASequence = parseInt(getAttribute(itemA, 'sequence'), 10);
  const itemBSequence = parseInt(getAttribute(itemB, 'sequence'), 10);

  if (itemASequence < itemBSequence) {
    return -1;
  }

  if (itemASequence > itemBSequence) {
    return 1;
  }

  return 0;
};

/**
 * Selects the ads of the passed VAST.
 *
 * @function
 * @param {ParsedVast} parsedVAST - Parsed VAST xml.
 * @returns {?Array} - Array of ads or empty array.
 * @static
 */
export const getAds = (parsedVAST) => {
  const vastElement = parsedVAST && get(parsedVAST, 'VAST');
  const ads = vastElement && getAll(vastElement, 'Ad');

  if (ads && ads.length > 0) {
    return ads;
  }

  return [];
};

/**
 * Gets the Error URI of the passed parsed VAST xml.
 *
 * @function
 * @param {ParsedVast} parsedVAST - Parsed VAST xml.
 * @returns {?VAST-macro} - Vast Error URI or `null` otherwise.
 * @static
 */
export const getVastErrorURI = (parsedVAST) => {
  const vastElement = parsedVAST && get(parsedVAST, 'VAST');

  if (vastElement) {
    const error = get(vastElement, 'Error');

    if (error) {
      return getText(error);
    }
  }

  return null;
};

/**
 * Gets the sequence of the pod ad.
 *
 * @function
 * @param {ParsedAd} ad - Parsed ad definition object.
 * @returns {?number} - The pod ad sequence number or `null`.
 * @static
 */
export const getPodAdSequence = (ad) => {
  const sequence = parseInt(getAttribute(ad, 'sequence'), 10);

  if (typeof sequence === 'number' && !isNaN(sequence)) {
    return sequence;
  }

  return null;
};

/**
 * Checks if the passed ad definition is a pod ad.
 *
 * @function
 * @param {ParsedAd} ad - Parsed ad definition object.
 * @returns {?boolean} - Returns true if there the ad is a pod ad and false otherwise.
 * @static
 */
export const isPodAd = (ad) => Boolean(getPodAdSequence(ad));

/**
 * Checks if the passed array of ads have an ad pod.
 *
 * @function
 * @param {ParsedVAST} parsedVAST - Parsed VAST xml.
 * @returns {?boolean} - Returns true if there is an ad pod in the array and false otherwise.
 * @static
 */
export const hasAdPod = (parsedVAST) => {
  const ads = getAds(parsedVAST);

  return Array.isArray(ads) && ads.filter(isPodAd).length > 1;
};

/**
 * Returns true if the passed VastChain has an ad pod or false otherwise.
 *
 * @function
 * @param {Array} VastChain - Array of VAST responses. See `load` or `requestAd` for more info.
 *
 * @returns {boolean} - True if the VastChain contains an ad pod and false otherwise.
 * @static
 */
export const isAdPod = (VastChain = []) => VastChain.map(({parsedXML}) => parsedXML).some(hasAdPod);

/**
 * Selects the first ad of the passed VAST. If the passed VAST response contains an ad pod it will return the first ad in the ad pod sequence.
 *
 * @function
 * @param {ParsedVAST} parsedVAST - Parsed VAST xml.
 * @returns {?ParsedAd} - First ad of the VAST xml or `null`.
 * @static
 */
export const getFirstAd = (parsedVAST) => {
  const ads = getAds(parsedVAST);

  if (Array.isArray(ads) && ads.length > 0) {
    if (hasAdPod(parsedVAST)) {
      return ads.filter(isPodAd)
        .sort(compareBySequence)[0];
    }

    return ads[0];
  }

  return null;
};

/**
 * Checks if the passed ad is a Wrapper.
 *
 * @function
 * @param {ParsedAd} ad - VAST ad object.
 * @returns {boolean} - `true` if the ad contains a wrapper and `false` otherwise.
 * @static
 */
export const isWrapper = (ad = {}) => Boolean(get(ad || {}, 'Wrapper'));

/**
 * Checks if the passed ad is an Inline.
 *
 * @function
 * @param {ParsedAd} ad - VAST ad object.
 * @returns {boolean} - Returns `true` if the ad contains an Inline or `false` otherwise.
 * @static
 */
export const isInline = (ad) => Boolean(get(ad || {}, 'Inline'));

/**
 * Returns the VASTAdTagURI from the wrapper ad.
 *
 * @function
 * @param {ParsedAd} ad - VAST ad object.
 * @returns {?string} - Returns the VASTAdTagURI from the wrapper ad or `null` otherwise.
 * @static
 */
export const getVASTAdTagURI = (ad) => {
  const wrapperElement = get(ad, 'Wrapper');
  const vastAdTagURIElement = wrapperElement && get(wrapperElement, 'VastAdTagUri');

  if (vastAdTagURIElement) {
    return getText(vastAdTagURIElement) || null;
  }

  return null;
};

/**
 * Returns the options from the wrapper ad.
 *
 * @function
 * @param {Object} ad - VAST ad object.
 * @returns {WrapperOptions} - Returns the options from the wrapper ad.
 * @static
 */
export const getWrapperOptions = (ad) => {
  const {
    allowMultipleAds,
    fallbackOnNoAd,
    followAdditionalWrappers
  } = getAttributes(get(ad, 'Wrapper'));

  const opts = {};

  if (allowMultipleAds) {
    opts.allowMultipleAds = getBooleanValue(allowMultipleAds);
  }

  if (fallbackOnNoAd) {
    opts.fallbackOnNoAd = getBooleanValue(fallbackOnNoAd);
  }

  if (followAdditionalWrappers) {
    opts.followAdditionalWrappers = getBooleanValue(followAdditionalWrappers);
  }

  return opts;
};

/**
 * Gets the Error URI of the passed ad.
 *
 * @function
 * @param {ParsedAd} ad - VAST ad object.
 * @returns {?string} - Vast ad Error URI or `null` otherwise.
 * @static
 */
export const getAdErrorURI = (ad) => {
  const adTypeElement = ad && getFirstChild(ad);

  if (adTypeElement) {
    const error = get(adTypeElement, 'Error');

    if (error) {
      return getText(error);
    }
  }

  return null;
};

/**
 * Gets the Impression URI of the passed ad.
 *
 * @function
 * @param {ParsedAd} ad - VAST ad object.
 * @returns {?string} - Vast ad Impression URI or `null` otherwise.
 * @static
 */
export const getImpressionUri = (ad) => {
  const adTypeElement = ad && getFirstChild(ad);

  if (adTypeElement) {
    const impression = get(adTypeElement, 'Impression');

    if (impression) {
      return getText(impression);
    }
  }

  return null;
};

/**
 * Gets the ad's MediaFiles.
 *
 * @function
 * @param {ParsedAd} ad - VAST ad object.
 * @returns {?Array.<MediaFile>} - array of media files or null
 * @static
 */
export const getMediaFiles = (ad) => {
  const creativeElement = ad && getLinearCreative(ad);

  if (creativeElement) {
    const universalAdIdElement = get(creativeElement, 'UniversalAdId');
    const universalAdId = universalAdIdElement && getText(universalAdIdElement) || null;
    const linearElement = get(creativeElement, 'Linear');
    const mediaFilesElement = get(linearElement, 'MediaFiles');
    const mediaFileElements = mediaFilesElement && getAll(mediaFilesElement, 'MediaFile');

    if (mediaFileElements && mediaFileElements.length > 0) {
      return mediaFileElements.map((mediaFileElement) => {
        const src = getText(mediaFileElement);
        const {
          apiFramework,
          bitrate,
          codec,
          delivery,
          height,
          id,
          maintainAspectRatio,
          maxBitrate,
          minBitrate,
          scalable,
          type,
          width
        } = getAttributes(mediaFileElement);

        return {
          apiFramework,
          bitrate,
          codec,
          delivery,
          height,
          id,
          maintainAspectRatio,
          maxBitrate,
          minBitrate,
          scalable,
          src,
          type,
          universalAdId,
          width
        };
      });
    }
  }

  return null;
};

/**
 * Gets the ad's InteractiveFiles. That were added with the `InteractiveCreativeFile` tag.
 *
 * @function
 * @param {ParsedAd} ad - VAST ad object.
 * @returns {?Array.<InteractiveFile>} - array of media files or null
 * @static
 */
export const getInteractiveCreativeFiles = (ad) => {
  const creativeElement = ad && getLinearCreative(ad);

  if (creativeElement) {
    const linearElement = get(creativeElement, 'Linear');
    const mediaFilesElement = get(linearElement, 'MediaFiles');
    const interactiveElements = mediaFilesElement && getAll(mediaFilesElement, 'InteractiveCreativeFile');

    if (interactiveElements && interactiveElements.length > 0) {
      return interactiveElements.map((interactiveElement) => {
        const {
          apiFramework,
          type
        } = getAttributes(interactiveElement);
        const src = getText(interactiveElement);

        return {
          apiFramework,
          src,
          type
        };
      });
    }
  }

  return null;
};

/**
 * Gets all the ad's InteractiveFiles.
 *
 * @function
 * @param {ParsedAd} ad - VAST ad object.
 * @returns {?Array.<InteractiveFile>} - array of media files or null
 * @static
 */
export const getInteractiveFiles = (ad) => {
  let interactiveFiles = getInteractiveCreativeFiles(ad);

  if (interactiveFiles) {
    return interactiveFiles;
  }

  const mediaFiles = getMediaFiles(ad);

  if (mediaFiles) {
    interactiveFiles = mediaFiles
      .filter(({apiFramework = ''}) => apiFramework.toLowerCase() === 'vpaid')
      .map(({apiFramework, src, type}) => ({
        apiFramework,
        src,
        type
      }));

    if (interactiveFiles.length > 0) {
      return interactiveFiles;
    }
  }

  return null;
};

const getVideoClicksElement = (ad) => {
  const creativeElement = ad && getLinearCreative(ad);
  const linearElement = creativeElement && get(creativeElement, 'Linear');
  const videoClicksElement = linearElement && get(linearElement, 'VideoClicks');

  if (videoClicksElement) {
    return videoClicksElement;
  }

  return null;
};

/**
 * Gets the click through {@link VAST-macro}.
 *
 * @function
 * @param {ParsedAd} ad - VAST ad object.
 * @returns {?VAST-macro} - clickthrough macro
 * @static
 */
export const getClickThrough = (ad) => {
  const videoClicksElement = getVideoClicksElement(ad);
  const clickThroughElement = videoClicksElement && get(videoClicksElement, 'ClickThrough');

  if (clickThroughElement) {
    return getText(clickThroughElement);
  }

  return null;
};

/**
 * Gets the click through {@link VAST-macro}.
 *
 * @function
 * @param {ParsedAd} ad - VAST ad object.
 * @returns {?Array.<VAST-macro>} - click tracking macro
 * @static
 */
export const getClickTracking = (ad) => {
  const videoClicksElement = getVideoClicksElement(ad);
  const clickTrackingElements = videoClicksElement && getAll(videoClicksElement, 'ClickTracking');

  if (clickTrackingElements && clickTrackingElements.length > 0) {
    return clickTrackingElements.map((element) => getText(element));
  }

  return null;
};

/**
 * Gets the custom click {@link VAST-macro}.
 *
 * @function
 * @param {ParsedAd} ad - VAST ad object.
 * @returns {?Array.<VAST-macro>} - click tracking macro
 * @static
 */
export const getCustomClick = (ad) => {
  const videoClicksElement = getVideoClicksElement(ad);
  const customClickElements = videoClicksElement && getAll(videoClicksElement, 'CustomClick');

  if (customClickElements && customClickElements.length > 0) {
    return customClickElements.map((element) => getText(element));
  }

  return null;
};

/**
 * Gets the skipoffset.
 *
 * @function
 * @param {Object} ad - VAST ad object.
 * @returns {?ParsedOffset} - the time offset in milliseconds or a string with the percentage or null
 * @static
 */
export const getSkipOffset = (ad) => {
  const creativeElement = ad && getLinearCreative(ad);
  const linearElement = creativeElement && get(creativeElement, 'Linear');
  const skipoffset = linearElement && getAttribute(linearElement, 'skipoffset');

  if (skipoffset) {
    return parseOffset(skipoffset);
  }

  return null;
};

const getLinearContent = (xml) => {
  const linearRegex = /<Linear([\s\S]*)<\/Linear/gm;
  const result = linearRegex.exec(xml);

  return result && result[1];
};

const getAdParametersContent = (xml) => {
  const paramsRegex = /<AdParameters[\s\w="]*>([\s\S]*)<\/AdParameters>/gm;
  const result = paramsRegex.exec(xml);

  return result && result[1].replace(/[\n\s]*<!\[CDATA\[[\n\s]*/, '')
    .replace(/[\n\s]*\]\]>[\n\s]*$/, '')

    // unescape nested CDATA
    .replace(/\]\]\]\]><!\[CDATA\[>/, ']]>')
    .trim();
};

const getXmlEncodedValue = (xml) => {
  const xmlEncodedRegex = /<AdParameters[\s]*xmlEncoded="(.*?)">/gmi;
  const result = xmlEncodedRegex.exec(xml);

  return Boolean(result) && result[1] === 'true';
};

/**
 * Gets the creative data.
 *
 * @function
 * @param {string} xml - VAST XML text.
 * @returns {Object} - with `AdParameters` as they come in the XML and a flag `xmlEncoded` to indicate if the ad parameters are xml encoded.
 * @static
 */
export const getCreativeData = (xml) => {
  const linearContent = getLinearContent(xml);
  const AdParameters = linearContent && getAdParametersContent(linearContent);
  const xmlEncoded = linearContent && getXmlEncodedValue(linearContent);

  return {
    AdParameters,
    xmlEncoded
  };
};

export {

  /**
   * Gets the Vast Icon definitions from the Vast Ad.
   *
   * @function
   * @param {ParsedAd} ad - VAST ad object.
   * @returns {?Array.<VastIcon>} - Array of VAST icon definitions
   * @static
   */
  getIcons,

  /**
   * Gets the Linear tracking events from the Vast Ad
   *
   * @function
   * @param {ParsedAd} ad - VAST ad object.
   * @param {string} [eventName] - If provided it will filter-out the array events against it.
   * @returns {?Array.<VastTrackingEvent>} - Array of Tracking event definitions
   * @static
   */
  getLinearTrackingEvents,

  /**
   * Gets the Non Linear tracking events from the Vast Ad
   *
   * @function
   * @param {ParsedAd} ad - VAST ad object.
   * @param {string} [eventName] - If provided it will filter-out the array events against it.
   * @returns {?Array.<VastTrackingEvent>} - Array of Tracking event definitions
   * @static
   */
  getNonLinearTrackingEvents
};