<commit_before>import { createAd } from '../ad';
import { createAdVerification } from '../ad_verification';
import { parseCreatives } from './creatives_parser';
import { parseExtensions } from './extensions_parser';
import { parserUtils } from './parser_utils';
import { parserVerification } from './parser_verification';

/**
 * This module provides methods to parse a VAST Ad Element.
 */

/**
 * Parses an Ad element (can either be a Wrapper or an InLine).
 * @param  {Object} adElement - The VAST Ad element to parse.
 * @param  {Function} emit - Emit function used to trigger Warning event
 * @param  {Object} options - An optional Object of parameters to be used in the parsing process.
 * @emits  VASTParser#VAST-warning
 * @return {Object|undefined} - Object containing the ad and if it is wrapper/inline
 */
export function parseAd(
  adElement,
  emit,
  { allowMultipleAds, followAdditionalWrappers } = {}
) {
  const childNodes = adElement.childNodes;

  for (const adTypeElementKey in childNodes) {
    const adTypeElement = childNodes[adTypeElementKey];

    if (['Wrapper', 'InLine'].indexOf(adTypeElement.nodeName) === -1) {
      continue;
    }

    if (
      adTypeElement.nodeName === 'Wrapper' &&
      followAdditionalWrappers === false
    ) {
      continue;
    }

    parserUtils.copyNodeAttribute('id', adElement, adTypeElement);
    parserUtils.copyNodeAttribute('sequence', adElement, adTypeElement);
    parserUtils.copyNodeAttribute('adType', adElement, adTypeElement);
    if (adTypeElement.nodeName === 'Wrapper') {
      return { ad: parseWrapper(adTypeElement, emit), type: 'WRAPPER' };
    } else if (adTypeElement.nodeName === 'InLine') {
      return {
        ad: parseInLine(adTypeElement, emit, { allowMultipleAds }),
        type: 'INLINE'
      };
    }
  }
}

/**
 * Parses an Inline
 * @param  {Object} adElement Element - The VAST Inline element to parse.
 * @param  {Function} emit - Emit function used to trigger Warning event.
 * @param  {Object} options - An optional Object of parameters to be used in the parsing process.
 * @emits  VASTParser#VAST-warning
 * @return {Object} ad - The ad object.
 */
function parseInLine(adElement, emit, { allowMultipleAds } = {}) {
  // if allowMultipleAds is set to false by wrapper attribute
  // only the first stand-alone Ad (with no sequence values) in the
  // requested VAST response is allowed so we won't parse ads with sequence
  if (allowMultipleAds === false && adElement.getAttribute('sequence')) {
    return null;
  }

  return parseAdElement(adElement, emit);
}

/**
 * Parses an ad type (Inline or Wrapper)
 * @param  {Object} adTypeElement - The VAST Inline or Wrapper element to parse.
 * @param  {Function} emit - Emit function used to trigger Warning event.
 * @emits  VASTParser#VAST-warning
 * @return {Object} ad - The ad object.
 */
function parseAdElement(adTypeElement, emit) {
  if (emit) {
    parserVerification.verifyRequiredValues(adTypeElement, emit);
  }

  const childNodes = adTypeElement.childNodes;
  const ad = createAd(parserUtils.parseAttributes(adTypeElement));

  for (const nodeKey in childNodes) {
    const node = childNodes[nodeKey];
    switch (node.nodeName) {
      case 'Error':
        ad.errorURLTemplates.push(parserUtils.parseNodeText(node));
        break;

      case 'Impression':
        ad.impressionURLTemplates.push({
          id: node.getAttribute('id') || null,
          url: parserUtils.parseNodeText(node)
        });
        break;

      case 'Creatives':
        ad.creatives = parseCreatives(
          parserUtils.childrenByName(node, 'Creative')
        );
        break;

      case 'Extensions': {
        const extNodes = parserUtils.childrenByName(node, 'Extension');
        ad.extensions = parseExtensions(extNodes);

        /*
          OMID specify adVerifications should be in extensions for VAST < 4.0
          To avoid to put them on two different places in two different format we reparse it
          from extensions the same way than for an AdVerifications node.
        */
        if (!ad.adVerifications.length) {
          ad.adVerifications = _parseAdVerificationsFromExensions(extNodes);
        }
        break;
      }
      case 'AdVerifications':
        ad.adVerifications = _parseAdVerifications(
          parserUtils.childrenByName(node, 'Verification')
        );
        break;

      case 'AdSystem':
        ad.system = {
          value: parserUtils.parseNodeText(node),
          version: node.getAttribute('version') || null
        };
        break;

      case 'AdTitle':
        ad.title = parserUtils.parseNodeText(node);
        break;

      case 'AdServingId':
        ad.adServingId = parserUtils.parseNodeText(node);
        break;

      case 'Category':
        ad.categories.push({
          authority: node.getAttribute('authority') || null,
          value: parserUtils.parseNodeText(node)
        });
        break;

      case 'Expires':
        ad.expires = parseInt(parserUtils.parseNodeText(node), 10);
        break;

      case 'ViewableImpression':
        ad.viewableImpression = _parseViewableImpression(node);
        break;

      case 'Description':
        ad.description = parserUtils.parseNodeText(node);
        break;

      case 'Advertiser':
        ad.advertiser = {
          id: node.getAttribute('id') || null,
          value: parserUtils.parseNodeText(node)
        };
        break;

      case 'Pricing':
        ad.pricing = {
          value: parserUtils.parseNodeText(node),
          model: node.getAttribute('model') || null,
          currency: node.getAttribute('currency') || null
        };
        break;

      case 'Survey':
        ad.survey = parserUtils.parseNodeText(node);
        break;

      case 'BlockedAdCategories':
        ad.blockedAdCategories.push({
          authority: node.getAttribute('authority') || null,
          value: parserUtils.parseNodeText(node)
        });
        break;
    }
  }

  return ad;
}

/**
 * Parses a Wrapper element without resolving the wrapped urls.
 * @param  {Object} wrapperElement - The VAST Wrapper element to be parsed.
 * @param  {Function} emit - Emit function used to trigger Warning event.
 * @emits  VASTParser#VAST-warning
 * @return {Ad}
 */
function parseWrapper(wrapperElement, emit) {
  const ad = parseAdElement(wrapperElement, emit);

  const followAdditionalWrappersValue = wrapperElement.getAttribute(
    'followAdditionalWrappers'
  );
  const allowMultipleAdsValue = wrapperElement.getAttribute('allowMultipleAds');
  const fallbackOnNoAdValue = wrapperElement.getAttribute('fallbackOnNoAd');
  ad.followAdditionalWrappers = followAdditionalWrappersValue
    ? parserUtils.parseBoolean(followAdditionalWrappersValue)
    : true;

  ad.allowMultipleAds = allowMultipleAdsValue
    ? parserUtils.parseBoolean(allowMultipleAdsValue)
    : false;

  ad.fallbackOnNoAd = fallbackOnNoAdValue
    ? parserUtils.parseBoolean(fallbackOnNoAdValue)
    : null;

  let wrapperURLElement = parserUtils.childByName(
    wrapperElement,
    'VASTAdTagURI'
  );

  if (wrapperURLElement) {
    ad.nextWrapperURL = parserUtils.parseNodeText(wrapperURLElement);
  } else {
    wrapperURLElement = parserUtils.childByName(wrapperElement, 'VASTAdTagURL');

    if (wrapperURLElement) {
      ad.nextWrapperURL = parserUtils.parseNodeText(
        parserUtils.childByName(wrapperURLElement, 'URL')
      );
    }
  }

  ad.creatives.forEach(wrapperCreativeElement => {
    if (['linear', 'nonlinear'].indexOf(wrapperCreativeElement.type) !== -1) {
      // TrackingEvents Linear / NonLinear
      if (wrapperCreativeElement.trackingEvents) {
        if (!ad.trackingEvents) {
          ad.trackingEvents = {};
        }
        if (!ad.trackingEvents[wrapperCreativeElement.type]) {
          ad.trackingEvents[wrapperCreativeElement.type] = {};
        }
        for (const eventName in wrapperCreativeElement.trackingEvents) {
          const urls = wrapperCreativeElement.trackingEvents[eventName];
          if (
            !Array.isArray(
              ad.trackingEvents[wrapperCreativeElement.type][eventName]
            )
          ) {
            ad.trackingEvents[wrapperCreativeElement.type][eventName] = [];
          }
          urls.forEach(url => {
            ad.trackingEvents[wrapperCreativeElement.type][eventName].push(url);
          });
        }
      }
      // ClickTracking
      if (wrapperCreativeElement.videoClickTrackingURLTemplates) {
        if (!Array.isArray(ad.videoClickTrackingURLTemplates)) {
          ad.videoClickTrackingURLTemplates = [];
        } // tmp property to save wrapper tracking URLs until they are merged
        wrapperCreativeElement.videoClickTrackingURLTemplates.forEach(item => {
          ad.videoClickTrackingURLTemplates.push(item);
        });
      }
      // ClickThrough
      if (wrapperCreativeElement.videoClickThroughURLTemplate) {
        ad.videoClickThroughURLTemplate =
          wrapperCreativeElement.videoClickThroughURLTemplate;
      }
      // CustomClick
      if (wrapperCreativeElement.videoCustomClickURLTemplates) {
        if (!Array.isArray(ad.videoCustomClickURLTemplates)) {
          ad.videoCustomClickURLTemplates = [];
        } // tmp property to save wrapper tracking URLs until they are merged
        wrapperCreativeElement.videoCustomClickURLTemplates.forEach(item => {
          ad.videoCustomClickURLTemplates.push(item);
        });
      }
    }
  });

  if (ad.nextWrapperURL) {
    return ad;
  }
}

/**
 * Parses the AdVerifications Element.
 * @param  {Array} verifications - The array of verifications to parse.
 * @return {Array<Object>}
 */
export function _parseAdVerifications(verifications) {
  const ver = [];

  verifications.forEach(verificationNode => {
    const verification = createAdVerification();
    const childNodes = verificationNode.childNodes;

    parserUtils.assignAttributes(verificationNode.attributes, verification);
    for (const nodeKey in childNodes) {
      const node = childNodes[nodeKey];

      switch (node.nodeName) {
        case 'JavaScriptResource':
        case 'ExecutableResource':
          verification.resource = parserUtils.parseNodeText(node);
          parserUtils.assignAttributes(node.attributes, verification);
          break;
        case 'VerificationParameters':
          verification.parameters = parserUtils.parseNodeText(node);
          break;
      }
    }

    const trackingEventsElement = parserUtils.childByName(
      verificationNode,
      '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(verification.trackingEvents[eventName])) {
              verification.trackingEvents[eventName] = [];
            }
            verification.trackingEvents[eventName].push(trackingURLTemplate);
          }
        });
    }

    ver.push(verification);
  });

  return ver;
}

/**
 * Parses the AdVerifications Element from extension for versions < 4.0
 * @param  {Array<Node>} extensions - The array of extensions to parse.
 * @return {Array<Object>}
 */
export function _parseAdVerificationsFromExensions(extensions) {
  let adVerificationsNode = null,
    adVerifications = [];

  // Find the first (and only) AdVerifications node from extensions
  extensions.some(extension => {
    return (adVerificationsNode = parserUtils.childByName(
      extension,
      'AdVerifications'
    ));
  });

  // Parse it if we get it
  if (adVerificationsNode) {
    adVerifications = _parseAdVerifications(
      parserUtils.childrenByName(adVerificationsNode, 'Verification')
    );
  }

  return adVerifications;
}

/**
 * Parses the ViewableImpression Element.
 * @param  {Object} viewableImpressionNode - The ViewableImpression node element.
 * @return {Object} viewableImpression - The viewableImpression object
 */
export function _parseViewableImpression(viewableImpressionNode) {
  const viewableImpression = {};
  viewableImpression.id = viewableImpressionNode.getAttribute('id') || null;
  const viewableImpressionChildNodes = viewableImpressionNode.childNodes;
  for (const viewableImpressionElementKey in viewableImpressionChildNodes) {
    const viewableImpressionElement =
      viewableImpressionChildNodes[viewableImpressionElementKey];
    const viewableImpressionNodeName = viewableImpressionElement.nodeName;
    const viewableImpressionNodeValue = parserUtils.parseNodeText(
      viewableImpressionElement
    );

    if (
      (viewableImpressionNodeName !== 'Viewable' &&
        viewableImpressionNodeName !== 'NotViewable' &&
        viewableImpressionNodeName !== 'ViewUndetermined') ||
      !viewableImpressionNodeValue
    ) {
      continue;
    } else {
      const viewableImpressionNodeNameLower = viewableImpressionNodeName.toLowerCase();
      if (!Array.isArray(viewableImpression[viewableImpressionNodeNameLower])) {
        viewableImpression[viewableImpressionNodeNameLower] = [];
      }
      viewableImpression[viewableImpressionNodeNameLower].push(
        viewableImpressionNodeValue
      );
    }
  }

  return viewableImpression;
}
<commit_msg>fix problem with adVerification nodes merging (updateMergingAdVerification)<commit_after>import { createAd } from '../ad';
import { createAdVerification } from '../ad_verification';
import { parseCreatives } from './creatives_parser';
import { parseExtensions } from './extensions_parser';
import { parserUtils } from './parser_utils';
import { parserVerification } from './parser_verification';

/**
 * This module provides methods to parse a VAST Ad Element.
 */

/**
 * Parses an Ad element (can either be a Wrapper or an InLine).
 * @param  {Object} adElement - The VAST Ad element to parse.
 * @param  {Function} emit - Emit function used to trigger Warning event
 * @param  {Object} options - An optional Object of parameters to be used in the parsing process.
 * @emits  VASTParser#VAST-warning
 * @return {Object|undefined} - Object containing the ad and if it is wrapper/inline
 */
export function parseAd(
  adElement,
  emit,
  { allowMultipleAds, followAdditionalWrappers } = {}
) {
  const childNodes = adElement.childNodes;

  for (const adTypeElementKey in childNodes) {
    const adTypeElement = childNodes[adTypeElementKey];

    if (['Wrapper', 'InLine'].indexOf(adTypeElement.nodeName) === -1) {
      continue;
    }

    if (
      adTypeElement.nodeName === 'Wrapper' &&
      followAdditionalWrappers === false
    ) {
      continue;
    }

    parserUtils.copyNodeAttribute('id', adElement, adTypeElement);
    parserUtils.copyNodeAttribute('sequence', adElement, adTypeElement);
    parserUtils.copyNodeAttribute('adType', adElement, adTypeElement);
    if (adTypeElement.nodeName === 'Wrapper') {
      return { ad: parseWrapper(adTypeElement, emit), type: 'WRAPPER' };
    } else if (adTypeElement.nodeName === 'InLine') {
      return {
        ad: parseInLine(adTypeElement, emit, { allowMultipleAds }),
        type: 'INLINE',
      };
    }
  }
}

/**
 * Parses an Inline
 * @param  {Object} adElement Element - The VAST Inline element to parse.
 * @param  {Function} emit - Emit function used to trigger Warning event.
 * @param  {Object} options - An optional Object of parameters to be used in the parsing process.
 * @emits  VASTParser#VAST-warning
 * @return {Object} ad - The ad object.
 */
function parseInLine(adElement, emit, { allowMultipleAds } = {}) {
  // if allowMultipleAds is set to false by wrapper attribute
  // only the first stand-alone Ad (with no sequence values) in the
  // requested VAST response is allowed so we won't parse ads with sequence
  if (allowMultipleAds === false && adElement.getAttribute('sequence')) {
    return null;
  }

  return parseAdElement(adElement, emit);
}

/**
 * Parses an ad type (Inline or Wrapper)
 * @param  {Object} adTypeElement - The VAST Inline or Wrapper element to parse.
 * @param  {Function} emit - Emit function used to trigger Warning event.
 * @emits  VASTParser#VAST-warning
 * @return {Object} ad - The ad object.
 */
function parseAdElement(adTypeElement, emit) {
  let adVerificationFromExtensions = [];
  if (emit) {
    parserVerification.verifyRequiredValues(adTypeElement, emit);
  }

  const childNodes = adTypeElement.childNodes;
  const ad = createAd(parserUtils.parseAttributes(adTypeElement));

  for (const nodeKey in childNodes) {
    const node = childNodes[nodeKey];
    switch (node.nodeName) {
      case 'Error':
        ad.errorURLTemplates.push(parserUtils.parseNodeText(node));
        break;

      case 'Impression':
        ad.impressionURLTemplates.push({
          id: node.getAttribute('id') || null,
          url: parserUtils.parseNodeText(node),
        });
        break;

      case 'Creatives':
        ad.creatives = parseCreatives(
          parserUtils.childrenByName(node, 'Creative')
        );
        break;

      case 'Extensions': {
        const extNodes = parserUtils.childrenByName(node, 'Extension');
        ad.extensions = parseExtensions(extNodes);

        /*
          OMID specify adVerifications should be in extensions for VAST < 4.0
          To avoid to put them on two different places in two different format we reparse it
          from extensions the same way than for an AdVerifications node.
        */
        if (!ad.adVerifications.length) {
          adVerificationFromExtensions = _parseAdVerificationsFromExensions(
            extNodes
          );
        }
        break;
      }
      case 'AdVerifications':
        ad.adVerifications = _parseAdVerifications(
          parserUtils.childrenByName(node, 'Verification')
        );
        break;

      case 'AdSystem':
        ad.system = {
          value: parserUtils.parseNodeText(node),
          version: node.getAttribute('version') || null,
        };
        break;

      case 'AdTitle':
        ad.title = parserUtils.parseNodeText(node);
        break;

      case 'AdServingId':
        ad.adServingId = parserUtils.parseNodeText(node);
        break;

      case 'Category':
        ad.categories.push({
          authority: node.getAttribute('authority') || null,
          value: parserUtils.parseNodeText(node),
        });
        break;

      case 'Expires':
        ad.expires = parseInt(parserUtils.parseNodeText(node), 10);
        break;

      case 'ViewableImpression':
        ad.viewableImpression = _parseViewableImpression(node);
        break;

      case 'Description':
        ad.description = parserUtils.parseNodeText(node);
        break;

      case 'Advertiser':
        ad.advertiser = {
          id: node.getAttribute('id') || null,
          value: parserUtils.parseNodeText(node),
        };
        break;

      case 'Pricing':
        ad.pricing = {
          value: parserUtils.parseNodeText(node),
          model: node.getAttribute('model') || null,
          currency: node.getAttribute('currency') || null,
        };
        break;

      case 'Survey':
        ad.survey = parserUtils.parseNodeText(node);
        break;

      case 'BlockedAdCategories':
        ad.blockedAdCategories.push({
          authority: node.getAttribute('authority') || null,
          value: parserUtils.parseNodeText(node),
        });
        break;
    }
  }

  if (adVerificationFromExtensions.length) {
    ad.adVerifications = ad.adVerifications.concat(
      adVerificationFromExtensions
    );
  }
  return ad;
}

/**
 * Parses a Wrapper element without resolving the wrapped urls.
 * @param  {Object} wrapperElement - The VAST Wrapper element to be parsed.
 * @param  {Function} emit - Emit function used to trigger Warning event.
 * @emits  VASTParser#VAST-warning
 * @return {Ad}
 */
function parseWrapper(wrapperElement, emit) {
  const ad = parseAdElement(wrapperElement, emit);

  const followAdditionalWrappersValue = wrapperElement.getAttribute(
    'followAdditionalWrappers'
  );
  const allowMultipleAdsValue = wrapperElement.getAttribute('allowMultipleAds');
  const fallbackOnNoAdValue = wrapperElement.getAttribute('fallbackOnNoAd');
  ad.followAdditionalWrappers = followAdditionalWrappersValue
    ? parserUtils.parseBoolean(followAdditionalWrappersValue)
    : true;

  ad.allowMultipleAds = allowMultipleAdsValue
    ? parserUtils.parseBoolean(allowMultipleAdsValue)
    : false;

  ad.fallbackOnNoAd = fallbackOnNoAdValue
    ? parserUtils.parseBoolean(fallbackOnNoAdValue)
    : null;

  let wrapperURLElement = parserUtils.childByName(
    wrapperElement,
    'VASTAdTagURI'
  );

  if (wrapperURLElement) {
    ad.nextWrapperURL = parserUtils.parseNodeText(wrapperURLElement);
  } else {
    wrapperURLElement = parserUtils.childByName(wrapperElement, 'VASTAdTagURL');

    if (wrapperURLElement) {
      ad.nextWrapperURL = parserUtils.parseNodeText(
        parserUtils.childByName(wrapperURLElement, 'URL')
      );
    }
  }

  ad.creatives.forEach((wrapperCreativeElement) => {
    if (['linear', 'nonlinear'].indexOf(wrapperCreativeElement.type) !== -1) {
      // TrackingEvents Linear / NonLinear
      if (wrapperCreativeElement.trackingEvents) {
        if (!ad.trackingEvents) {
          ad.trackingEvents = {};
        }
        if (!ad.trackingEvents[wrapperCreativeElement.type]) {
          ad.trackingEvents[wrapperCreativeElement.type] = {};
        }
        for (const eventName in wrapperCreativeElement.trackingEvents) {
          const urls = wrapperCreativeElement.trackingEvents[eventName];
          if (
            !Array.isArray(
              ad.trackingEvents[wrapperCreativeElement.type][eventName]
            )
          ) {
            ad.trackingEvents[wrapperCreativeElement.type][eventName] = [];
          }
          urls.forEach((url) => {
            ad.trackingEvents[wrapperCreativeElement.type][eventName].push(url);
          });
        }
      }
      // ClickTracking
      if (wrapperCreativeElement.videoClickTrackingURLTemplates) {
        if (!Array.isArray(ad.videoClickTrackingURLTemplates)) {
          ad.videoClickTrackingURLTemplates = [];
        } // tmp property to save wrapper tracking URLs until they are merged
        wrapperCreativeElement.videoClickTrackingURLTemplates.forEach(
          (item) => {
            ad.videoClickTrackingURLTemplates.push(item);
          }
        );
      }
      // ClickThrough
      if (wrapperCreativeElement.videoClickThroughURLTemplate) {
        ad.videoClickThroughURLTemplate =
          wrapperCreativeElement.videoClickThroughURLTemplate;
      }
      // CustomClick
      if (wrapperCreativeElement.videoCustomClickURLTemplates) {
        if (!Array.isArray(ad.videoCustomClickURLTemplates)) {
          ad.videoCustomClickURLTemplates = [];
        } // tmp property to save wrapper tracking URLs until they are merged
        wrapperCreativeElement.videoCustomClickURLTemplates.forEach((item) => {
          ad.videoCustomClickURLTemplates.push(item);
        });
      }
    }
  });

  if (ad.nextWrapperURL) {
    return ad;
  }
}

/**
 * Parses the AdVerifications Element.
 * @param  {Array} verifications - The array of verifications to parse.
 * @return {Array<Object>}
 */
export function _parseAdVerifications(verifications) {
  const ver = [];

  verifications.forEach((verificationNode) => {
    const verification = createAdVerification();
    const childNodes = verificationNode.childNodes;

    parserUtils.assignAttributes(verificationNode.attributes, verification);
    for (const nodeKey in childNodes) {
      const node = childNodes[nodeKey];

      switch (node.nodeName) {
        case 'JavaScriptResource':
        case 'ExecutableResource':
          verification.resource = parserUtils.parseNodeText(node);
          parserUtils.assignAttributes(node.attributes, verification);
          break;
        case 'VerificationParameters':
          verification.parameters = parserUtils.parseNodeText(node);
          break;
      }
    }

    const trackingEventsElement = parserUtils.childByName(
      verificationNode,
      '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(verification.trackingEvents[eventName])) {
              verification.trackingEvents[eventName] = [];
            }
            verification.trackingEvents[eventName].push(trackingURLTemplate);
          }
        });
    }

    ver.push(verification);
  });

  return ver;
}

/**
 * Parses the AdVerifications Element from extension for versions < 4.0
 * @param  {Array<Node>} extensions - The array of extensions to parse.
 * @return {Array<Object>}
 */
export function _parseAdVerificationsFromExensions(extensions) {
  let adVerificationsNode = null,
    adVerifications = [];

  // Find the first (and only) AdVerifications node from extensions
  extensions.some((extension) => {
    return (adVerificationsNode = parserUtils.childByName(
      extension,
      'AdVerifications'
    ));
  });

  // Parse it if we get it
  if (adVerificationsNode) {
    adVerifications = _parseAdVerifications(
      parserUtils.childrenByName(adVerificationsNode, 'Verification')
    );
  }
  return adVerifications;
}

/**
 * Parses the ViewableImpression Element.
 * @param  {Object} viewableImpressionNode - The ViewableImpression node element.
 * @return {Object} viewableImpression - The viewableImpression object
 */
export function _parseViewableImpression(viewableImpressionNode) {
  const viewableImpression = {};
  viewableImpression.id = viewableImpressionNode.getAttribute('id') || null;
  const viewableImpressionChildNodes = viewableImpressionNode.childNodes;
  for (const viewableImpressionElementKey in viewableImpressionChildNodes) {
    const viewableImpressionElement =
      viewableImpressionChildNodes[viewableImpressionElementKey];
    const viewableImpressionNodeName = viewableImpressionElement.nodeName;
    const viewableImpressionNodeValue = parserUtils.parseNodeText(
      viewableImpressionElement
    );

    if (
      (viewableImpressionNodeName !== 'Viewable' &&
        viewableImpressionNodeName !== 'NotViewable' &&
        viewableImpressionNodeName !== 'ViewUndetermined') ||
      !viewableImpressionNodeValue
    ) {
      continue;
    } else {
      const viewableImpressionNodeNameLower = viewableImpressionNodeName.toLowerCase();
      if (!Array.isArray(viewableImpression[viewableImpressionNodeNameLower])) {
        viewableImpression[viewableImpressionNodeNameLower] = [];
      }
      viewableImpression[viewableImpressionNodeNameLower].push(
        viewableImpressionNodeValue
      );
    }
  }

  return viewableImpression;
}
<|endoftext|>


<commit_before>import update from 'react-addons-update';

import { ADD_POINT, MODIFY_POINT } from '../actions/drawing';

const initialState = {
  points: {
    // 0: { x: 100, y: 350, prev: null, next: 1 },
  },
  lines: {

  }
};

export default function (state = initialState, action) {
  switch (action.type) {
    case ADD_POINT: {
      const current = Object.assign({}, state.points);
      const prev = action.prev;

      current[action.id] = {
        x: action.x,
        y: action.y,
        prev,
      };

      let newState = Object.assign({}, state, { points: current });

      if (prev) {
        const p = current[prev];

        if (!p.next) {
          newState = Object.assign({}, newState, {
            points: update(current, {
              [prev]: {
                next: { $set: action.id },
              },
            }),
          });
        } else if (!p.prev && p.next) {
          newState = Object.assign({}, newState, {
            points: update(current, {
              [prev]: {
                prev: { $set: action.id },
              },
            }),
          });
        }
      }

      return newState;
    }
    case MODIFY_POINT: {
      const current = Object.assign({}, state.points);
      const origin = state.points[action.id];

      let newState = Object.assign({}, {
        points: update(current, {
          [action.id]: {
            x: { $set: action.x || origin.x },
            y: { $set: action.y || origin.y },
            next: { $set: action.next || origin.next },
          },
        }),
      });

      if (action.next) {
        newState = Object.assign({}, newState, {
          points: update(newState.points, {
            [action.next]: {
              prev: { $set: action.id },
            },
          }),
        });
      }

      return newState;
    }
    default: {
      return state;
    }
  }
}
<commit_msg>Final commit before refactoring to new data structure<commit_after>import update from 'react-addons-update';

import { ADD_POINT, MODIFY_POINT } from '../actions/drawing';

const initialState = {
  points: {
    // 0: { x: 100, y: 350, prev: null, next: 1 },
  },
  lines: {

  }
};

export default function (state = initialState, action) {
  switch (action.type) {
    case ADD_POINT: {
      const current = Object.assign({}, state.points);
      const prev = action.prev;

      current[action.id] = {
        x: action.x,
        y: action.y,
        prev,
      };

      let newState = Object.assign({}, state, { points: current });

      if (prev) {
        const p = current[prev];

        if (typeof p.next === 'undefined') {
          newState = Object.assign({}, newState, {
            points: update(current, {
              [prev]: {
                next: { $set: action.id },
              },
            }),
          });
        } else if (typeof p.prev === 'undefined') {
          newState = Object.assign({}, newState, {
            points: update(current, {
              [prev]: {
                prev: { $set: action.id },
              },
            }),
          });
        }
      }

      return newState;
    }
    case MODIFY_POINT: {
      const current = Object.assign({}, state.points);
      const origin = state.points[action.id];

      let newState = Object.assign({}, {
        points: update(current, {
          [action.id]: {
            x: { $set: action.x || origin.x },
            y: { $set: action.y || origin.y },
            next: { $set: action.next || origin.next },
          },
        }),
      });

      if (action.next) {
        newState = Object.assign({}, newState, {
          points: update(newState.points, {
            [action.next]: {
              prev: { $set: action.id },
            },
          }),
        });
      }

      return newState;
    }
    default: {
      return state;
    }
  }
}
<|endoftext|>


<commit_before>import path from 'path';
import {fromRemoteWindow} from './remote-event';

import {AsyncSubject} from 'rxjs/AsyncSubject';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';

import 'rxjs/add/observable/merge';
import 'rxjs/add/observable/throw';
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/defer';

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeAll';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/multicast';
import 'rxjs/add/operator/take';
import 'rxjs/add/operator/toPromise';

import {createProxyForRemote, executeJavaScriptMethod, executeJavaScriptMethodObservable, RecursiveProxyHandler} from './execute-js-func';

import './custom-operators';

const d = require('debug')('electron-remote:renderer-require');

const BrowserWindow = process.type === 'renderer' ?
  require('electron').remote.BrowserWindow :
  require('electron').BrowserWindow;

/**
 * Creates a BrowserWindow, requires a module in it, then returns a Proxy
 * object that will call into it. You probably want to use {requireTaskPool}
 * instead.
 *
 * @param  {string} modulePath  The path of the module to include.
 * @return {Object}             Returns an Object with a `module` which is a Proxy
 *                              object, and a `unsubscribe` method that will clean up
 *                              the window.
 */
export async function rendererRequireDirect(modulePath) {
  let bw = new BrowserWindow({width: 500, height: 500, show: false});
  let fullPath = require.resolve(modulePath);

  let ready = Observable.merge(
    fromRemoteWindow(bw, 'did-finish-load', true),
    fromRemoteWindow(bw, 'did-fail-load', true)
      .mergeMap(([, , errMsg]) => Observable.throw(new Error(errMsg)))
    ).take(1).toPromise();

  /* Uncomment for debugging!
  bw.show();
  bw.openDevTools();
  */

  let preloadFile = path.join(__dirname, 'renderer-require-preload.html');
  bw.loadURL(`file:///${preloadFile}?module=${encodeURIComponent(fullPath)}`);
  await ready;

  let fail = await executeJavaScriptMethod(bw, 'window.moduleLoadFailure');
  if (fail) {
    let msg = await executeJavaScriptMethod(bw, 'window.moduleLoadFailure.message');
    throw new Error(msg);
  }

  return {
    module: createProxyForRemote(bw).requiredModule,
    executeJavaScriptMethod: (chain, ...args) => executeJavaScriptMethod(bw, chain, ...args),
    executeJavaScriptMethodObservable: (chain, ...args) => executeJavaScriptMethodObservable(bw, 240*1000, chain, ...args),
    unsubscribe: () => bw.isDestroyed() ? bw.destroy() : bw.close()
  };
}

/**
 * requires a module in BrowserWindows that are created/destroyed as-needed, and
 * returns a Proxy object that will secretly marshal invocations to other processes
 * and marshal back the result. This is the cool method in this library.
 *
 * Note that since the global context is created / destroyed, you *cannot* rely
 * on module state (i.e. global variables) to be consistent
 *
 * @param  {string} modulePath       The path to the module. You may have to
 *                                   `require.resolve` it.
 * @param  {Number} maxConcurrency   The maximum number of concurrent processes
 *                                   to run. Defaults to 4.
 * @param  {Number} idleTimeout      The amount of time to wait before closing
 *                                   a BrowserWindow as idle.
 *
 * @return {Proxy}                   An ES6 Proxy object representing the module.
 */
export function requireTaskPool(modulePath, maxConcurrency=4, idleTimeout=5*1000) {
  return new RendererTaskpoolItem(modulePath, maxConcurrency, idleTimeout).moduleProxy;
}

/**
 * This class implements the scheduling logic for queuing and dispatching method
 * invocations to various background windows. It is complicated. But in like,
 * a cool way.
 */
class RendererTaskpoolItem {
  constructor(modulePath, maxConcurrency, idleTimeout) {
    const freeWindowList = [];
    const invocationQueue = new Subject();
    const completionQueue = new Subject();

    // This method will find a window that is currently idle or if it doesn't
    // exist, create one.
    const getOrCreateWindow = () => {
      let item = freeWindowList.pop();
      if (item) return Observable.of(item);

      return Observable.fromPromise(rendererRequireDirect(modulePath));
    };

    // Here, we set up a pipeline that maps a stream of invocations (i.e.
    // something we can pass to executeJavaScriptMethod) => stream of Future
    // Results from various windows => Stream of completed results, for which we
    // throw the Window that completed the result back onto the free window stack.
    invocationQueue
      .map(({chain, args, retval}) => Observable.defer(() => {
        return getOrCreateWindow()
          .mergeMap((wnd) => {
            d(`Actually invoking ${chain.join('.')}(${JSON.stringify(args)})`);
            let ret = wnd.executeJavaScriptMethodObservable(chain, ...args);

            ret.multicast(retval).connect();
            return ret.map(() => wnd).catch(() => Observable.of(wnd));
          });
      }))
      .mergeAll(maxConcurrency)
      .subscribe((wnd) => {
        if (!wnd || !wnd.unsubscribe) throw new Error("Bogus!");
        freeWindowList.push(wnd);
        completionQueue.next(true);
      });

    // Here, we create a version of RecursiveProxyHandler that will turn method
    // invocations into something we can push onto our invocationQueue pipeline.
    // This is the object that ends up being returned to the caller of
    // requireTaskPool.
    this.moduleProxy = RecursiveProxyHandler.create('__removeme__', (methodChain, args) => {
      let chain = methodChain.splice(1);

      d(`Queuing ${chain.join('.')}(${JSON.stringify(args)})`);
      let retval = new AsyncSubject();

      invocationQueue.next({ chain: ['requiredModule'].concat(chain), args, retval });
      return retval.toPromise();
    });

    // If we haven't received any invocations within a certain idle timeout
    // period, burn all of our BrowserWindow instances
    completionQueue.guaranteedThrottle(idleTimeout).subscribe(() => {
      d(`Freeing ${freeWindowList.length} taskpool processes`);
      while (freeWindowList.length > 0) {
        let wnd = freeWindowList.pop();
        if (wnd) wnd.unsubscribe();
      }
    });
  }
}
<commit_msg>:wrench: Filter “Error: OK”<commit_after>import path from 'path';
import {fromRemoteWindow} from './remote-event';

import {AsyncSubject} from 'rxjs/AsyncSubject';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';

import 'rxjs/add/observable/merge';
import 'rxjs/add/observable/throw';
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/defer';

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeAll';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/multicast';
import 'rxjs/add/operator/take';
import 'rxjs/add/operator/toPromise';

import {createProxyForRemote, executeJavaScriptMethod, executeJavaScriptMethodObservable, RecursiveProxyHandler} from './execute-js-func';

import './custom-operators';

const d = require('debug')('electron-remote:renderer-require');

const BrowserWindow = process.type === 'renderer' ?
  require('electron').remote.BrowserWindow :
  require('electron').BrowserWindow;

/**
 * Creates a BrowserWindow, requires a module in it, then returns a Proxy
 * object that will call into it. You probably want to use {requireTaskPool}
 * instead.
 *
 * @param  {string} modulePath  The path of the module to include.
 * @return {Object}             Returns an Object with a `module` which is a Proxy
 *                              object, and a `unsubscribe` method that will clean up
 *                              the window.
 */
export async function rendererRequireDirect(modulePath) {
  let bw = new BrowserWindow({width: 500, height: 500, show: false});
  let fullPath = require.resolve(modulePath);

  let ready = Observable.merge(
    fromRemoteWindow(bw, 'did-finish-load', true),
    fromRemoteWindow(bw, 'did-fail-load', true)
      .filter(([, errCode]) => errCode !== 0)
      .mergeMap(([, , errMsg]) => Observable.throw(new Error(errMsg)))
    ).take(1).toPromise();

  /* Uncomment for debugging!
  bw.show();
  bw.openDevTools();
  */

  let preloadFile = path.join(__dirname, 'renderer-require-preload.html');
  bw.loadURL(`file:///${preloadFile}?module=${encodeURIComponent(fullPath)}`);
  await ready;

  let fail = await executeJavaScriptMethod(bw, 'window.moduleLoadFailure');
  if (fail) {
    let msg = await executeJavaScriptMethod(bw, 'window.moduleLoadFailure.message');
    throw new Error(msg);
  }

  return {
    module: createProxyForRemote(bw).requiredModule,
    executeJavaScriptMethod: (chain, ...args) => executeJavaScriptMethod(bw, chain, ...args),
    executeJavaScriptMethodObservable: (chain, ...args) => executeJavaScriptMethodObservable(bw, 240*1000, chain, ...args),
    unsubscribe: () => bw.isDestroyed() ? bw.destroy() : bw.close()
  };
}

/**
 * requires a module in BrowserWindows that are created/destroyed as-needed, and
 * returns a Proxy object that will secretly marshal invocations to other processes
 * and marshal back the result. This is the cool method in this library.
 *
 * Note that since the global context is created / destroyed, you *cannot* rely
 * on module state (i.e. global variables) to be consistent
 *
 * @param  {string} modulePath       The path to the module. You may have to
 *                                   `require.resolve` it.
 * @param  {Number} maxConcurrency   The maximum number of concurrent processes
 *                                   to run. Defaults to 4.
 * @param  {Number} idleTimeout      The amount of time to wait before closing
 *                                   a BrowserWindow as idle.
 *
 * @return {Proxy}                   An ES6 Proxy object representing the module.
 */
export function requireTaskPool(modulePath, maxConcurrency=4, idleTimeout=5*1000) {
  return new RendererTaskpoolItem(modulePath, maxConcurrency, idleTimeout).moduleProxy;
}

/**
 * This class implements the scheduling logic for queuing and dispatching method
 * invocations to various background windows. It is complicated. But in like,
 * a cool way.
 */
class RendererTaskpoolItem {
  constructor(modulePath, maxConcurrency, idleTimeout) {
    const freeWindowList = [];
    const invocationQueue = new Subject();
    const completionQueue = new Subject();

    // This method will find a window that is currently idle or if it doesn't
    // exist, create one.
    const getOrCreateWindow = () => {
      let item = freeWindowList.pop();
      if (item) return Observable.of(item);

      return Observable.fromPromise(rendererRequireDirect(modulePath));
    };

    // Here, we set up a pipeline that maps a stream of invocations (i.e.
    // something we can pass to executeJavaScriptMethod) => stream of Future
    // Results from various windows => Stream of completed results, for which we
    // throw the Window that completed the result back onto the free window stack.
    invocationQueue
      .map(({chain, args, retval}) => Observable.defer(() => {
        return getOrCreateWindow()
          .mergeMap((wnd) => {
            d(`Actually invoking ${chain.join('.')}(${JSON.stringify(args)})`);
            let ret = wnd.executeJavaScriptMethodObservable(chain, ...args);

            ret.multicast(retval).connect();
            return ret.map(() => wnd).catch(() => Observable.of(wnd));
          });
      }))
      .mergeAll(maxConcurrency)
      .subscribe((wnd) => {
        if (!wnd || !wnd.unsubscribe) throw new Error("Bogus!");
        freeWindowList.push(wnd);
        completionQueue.next(true);
      });

    // Here, we create a version of RecursiveProxyHandler that will turn method
    // invocations into something we can push onto our invocationQueue pipeline.
    // This is the object that ends up being returned to the caller of
    // requireTaskPool.
    this.moduleProxy = RecursiveProxyHandler.create('__removeme__', (methodChain, args) => {
      let chain = methodChain.splice(1);

      d(`Queuing ${chain.join('.')}(${JSON.stringify(args)})`);
      let retval = new AsyncSubject();

      invocationQueue.next({ chain: ['requiredModule'].concat(chain), args, retval });
      return retval.toPromise();
    });

    // If we haven't received any invocations within a certain idle timeout
    // period, burn all of our BrowserWindow instances
    completionQueue.guaranteedThrottle(idleTimeout).subscribe(() => {
      d(`Freeing ${freeWindowList.length} taskpool processes`);
      while (freeWindowList.length > 0) {
        let wnd = freeWindowList.pop();
        if (wnd) wnd.unsubscribe();
      }
    });
  }
}
<|endoftext|>


<commit_before>// @flow

import { bind, unreachable } from "../utils/main";
import type {
  ElementWithHint,
  FromBackground,
  FromRenderer,
  HintUpdate,
  ToBackground,
} from "../data/Messages";

// It's tempting to put a random number or something in the ID, but in case
// something goes wrong and a rogue container is left behind it's always
// possible to find and remove it if the ID is known.
const CONTAINER_ID = "SynthWebExt";
const HINT_CLASS = "hint";
const HIDDEN_HINT_CLASS = "hiddenHint";
const MATCHED_CHARS_CLASS = "matchedChars";

const CONTAINER_STYLES = {
  all: "unset",
  position: "absolute",
  "z-index": "2147483647", // Maximum z-index browsers support.
  width: "100%",
  height: "100%",
};

const CSS = `
.${HINT_CLASS} {
  position: absolute;
  transform: translateY(-50%);
  box-sizing: border-box;
  padding: 2px;
  border: solid 1px rgba(0, 0, 0, 0.4);
  background-color: #ffd76e;
  color: black;
  font: menu;
  font-size: 12px;
  line-height: 1;
  font-weight: bold;
  white-space: nowrap;
  text-align: center;
  text-transform: uppercase;
}

.${MATCHED_CHARS_CLASS} {
  opacity: 0.3;
}

.${HIDDEN_HINT_CLASS} {
  opacity: 0;
}
`.trim();

export default class RendererProgram {
  css: string;

  constructor() {
    this.css = CSS;

    bind(this, ["onMessage"]);
  }

  start() {
    browser.runtime.onMessage.addListener(this.onMessage);

    this.sendMessage({
      type: "RendererScriptAdded",
    });
  }

  stop() {
    browser.runtime.onMessage.removeListener(this.onMessage);
  }

  async sendMessage(message: FromRenderer): Promise<any> {
    const wrappedMessage: ToBackground = {
      type: "FromRenderer",
      message,
    };
    try {
      return await browser.runtime.sendMessage((wrappedMessage: any));
    } catch (error) {
      console.error(
        "RendererProgram#sendMessage failed",
        wrappedMessage,
        error
      );
      throw error;
    }
  }

  onMessage(wrappedMessage: FromBackground) {
    if (wrappedMessage.type !== "ToRenderer") {
      return;
    }

    const { message } = wrappedMessage;

    switch (message.type) {
      case "Render":
        this.render(message.elements);
        break;

      case "UpdateHints":
        this.updateHints(message.updates);
        break;

      case "Unrender":
        this.unrender();
        break;

      default:
        unreachable(message.type, message);
    }
  }

  render(elements: Array<ElementWithHint>) {
    this.unrender();

    // I've tried creating the container in the constructor and re-using it for
    // all renders, but that didn't turn out to be faster.
    const container = document.createElement("div");
    container.id = CONTAINER_ID;
    setStyles(container, {
      ...CONTAINER_STYLES,
      left: `${window.scrollX}px`,
      top: `${window.scrollY}px`,
    });

    // Using `mode: "closed"` is tempting, but then Firefox does not seem to
    // allow inspecting the elements inside in its devtools. That's important
    // for people who want to customize the styling of the hints.
    const root = container.attachShadow({ mode: "open" });

    // Inserting a `<style>` element is way faster than doing
    // `element.style.setProperty()` on every element.
    const style = document.createElement("style");
    style.append(document.createTextNode(this.css));
    root.append(style);

    for (const { hintMeasurements, hint } of elements) {
      const element = document.createElement("div");
      element.className = HINT_CLASS;

      // Use `right` rather than `left` since the hints should be right-aligned
      // rather than left-aligned. This could also be done using `left` and
      // `transform: translateX(-100%)`, but that results in blurry hints in
      // Chrome due to Chrome making the widths of the hints non-integer based
      // on the font. `calc()` does not affect performance.
      element.style.right = `calc(100% - ${Math.round(hintMeasurements.x)}px)`;
      element.style.top = `${Math.round(hintMeasurements.y)}px`;

      element.append(document.createTextNode(hint));

      root.append(element);
    }

    if (document.documentElement != null) {
      document.documentElement.append(container);

      // Most hints are already correctly positioned, but some near the edges
      // might need to be moved a tiny bit to avoid being partially off-screen.
      // Do this in a separate animation frame so that the hints appear on
      // screen as quickly as possible. Adjusting positions is just a tweak –
      // that can be delayed a little bit.
      window.requestAnimationFrame(() => {
        // Using double `requestAnimationFrame` since they run before paint.
        // See: https://youtu.be/cCOL7MC4Pl0?t=20m29s
        window.requestAnimationFrame(() => {
          const { innerWidth, innerHeight } = window;
          for (const child of root.children) {
            const rect = child.getBoundingClientRect();
            if (rect.left < 0) {
              child.style.marginRight = `${Math.round(rect.left)}px`;
            }
            if (rect.top < 0) {
              child.style.marginTop = `${Math.round(-rect.top)}px`;
            }
            if (rect.right > innerWidth) {
              child.style.marginRight = `${Math.round(
                rect.right - innerWidth
              )}px`;
            }
            if (rect.bottom > innerHeight) {
              child.style.marginTop = `${Math.round(
                innerHeight - rect.bottom
              )}px`;
            }
          }
        });
      });
    }

    this.sendMessage({
      type: "Rendered",
      timestamp: performance.now(),
    });
  }

  updateHints(updates: Array<HintUpdate>) {
    const container = document.getElementById(CONTAINER_ID);
    const root = container == null ? undefined : container.shadowRoot;
    if (root == null) {
      console.error("RendererProgram#updateHints: missing root", container);
      return;
    }

    const hints = root.querySelectorAll(`.${HINT_CLASS}`);

    for (const [index, update] of updates.entries()) {
      const child = hints[index];

      if (child == null) {
        console.error(
          "RendererProgram#updateHints: missing child",
          index,
          update
        );
        continue;
      }

      child.classList.toggle(HIDDEN_HINT_CLASS, update.type === "Hide");

      if (update.type === "Update") {
        emptyElement(child);
        if (update.matched !== "") {
          const matched = document.createElement("span");
          matched.className = MATCHED_CHARS_CLASS;
          matched.append(document.createTextNode(update.matched));
          child.append(matched);
        }
        child.append(document.createTextNode(update.rest));
      }
    }
  }

  unrender() {
    const container = document.getElementById(CONTAINER_ID);
    if (container != null) {
      container.remove();
    }
  }
}

function setStyles(element: HTMLElement, styles: { [string]: string }) {
  for (const [property, value] of Object.entries(styles)) {
    // $FlowIgnore: Flow thinks that `value` is `mixed` here, but it is a `string`.
    element.style.setProperty(property, value, "important");
  }
}

function emptyElement(element: HTMLElement) {
  while (element.firstChild != null) {
    element.removeChild(element.firstChild);
  }
}
<commit_msg>Remove pointer events from the hints container<commit_after>// @flow

import { bind, unreachable } from "../utils/main";
import type {
  ElementWithHint,
  FromBackground,
  FromRenderer,
  HintUpdate,
  ToBackground,
} from "../data/Messages";

// It's tempting to put a random number or something in the ID, but in case
// something goes wrong and a rogue container is left behind it's always
// possible to find and remove it if the ID is known.
const CONTAINER_ID = "SynthWebExt";
const HINT_CLASS = "hint";
const HIDDEN_HINT_CLASS = "hiddenHint";
const MATCHED_CHARS_CLASS = "matchedChars";

const CONTAINER_STYLES = {
  all: "unset",
  position: "absolute",
  "z-index": "2147483647", // Maximum z-index browsers support.
  width: "100%",
  height: "100%",
  "pointer-events": "none",
};

const CSS = `
.${HINT_CLASS} {
  position: absolute;
  transform: translateY(-50%);
  box-sizing: border-box;
  padding: 2px;
  border: solid 1px rgba(0, 0, 0, 0.4);
  background-color: #ffd76e;
  color: black;
  font: menu;
  font-size: 12px;
  line-height: 1;
  font-weight: bold;
  white-space: nowrap;
  text-align: center;
  text-transform: uppercase;
}

.${MATCHED_CHARS_CLASS} {
  opacity: 0.3;
}

.${HIDDEN_HINT_CLASS} {
  opacity: 0;
}
`.trim();

export default class RendererProgram {
  css: string;

  constructor() {
    this.css = CSS;

    bind(this, ["onMessage"]);
  }

  start() {
    browser.runtime.onMessage.addListener(this.onMessage);

    this.sendMessage({
      type: "RendererScriptAdded",
    });
  }

  stop() {
    browser.runtime.onMessage.removeListener(this.onMessage);
  }

  async sendMessage(message: FromRenderer): Promise<any> {
    const wrappedMessage: ToBackground = {
      type: "FromRenderer",
      message,
    };
    try {
      return await browser.runtime.sendMessage((wrappedMessage: any));
    } catch (error) {
      console.error(
        "RendererProgram#sendMessage failed",
        wrappedMessage,
        error
      );
      throw error;
    }
  }

  onMessage(wrappedMessage: FromBackground) {
    if (wrappedMessage.type !== "ToRenderer") {
      return;
    }

    const { message } = wrappedMessage;

    switch (message.type) {
      case "Render":
        this.render(message.elements);
        break;

      case "UpdateHints":
        this.updateHints(message.updates);
        break;

      case "Unrender":
        this.unrender();
        break;

      default:
        unreachable(message.type, message);
    }
  }

  render(elements: Array<ElementWithHint>) {
    this.unrender();

    // I've tried creating the container in the constructor and re-using it for
    // all renders, but that didn't turn out to be faster.
    const container = document.createElement("div");
    container.id = CONTAINER_ID;
    setStyles(container, {
      ...CONTAINER_STYLES,
      left: `${window.scrollX}px`,
      top: `${window.scrollY}px`,
    });

    // Using `mode: "closed"` is tempting, but then Firefox does not seem to
    // allow inspecting the elements inside in its devtools. That's important
    // for people who want to customize the styling of the hints.
    const root = container.attachShadow({ mode: "open" });

    // Inserting a `<style>` element is way faster than doing
    // `element.style.setProperty()` on every element.
    const style = document.createElement("style");
    style.append(document.createTextNode(this.css));
    root.append(style);

    for (const { hintMeasurements, hint } of elements) {
      const element = document.createElement("div");
      element.className = HINT_CLASS;

      // Use `right` rather than `left` since the hints should be right-aligned
      // rather than left-aligned. This could also be done using `left` and
      // `transform: translateX(-100%)`, but that results in blurry hints in
      // Chrome due to Chrome making the widths of the hints non-integer based
      // on the font. `calc()` does not affect performance.
      element.style.right = `calc(100% - ${Math.round(hintMeasurements.x)}px)`;
      element.style.top = `${Math.round(hintMeasurements.y)}px`;

      element.append(document.createTextNode(hint));

      root.append(element);
    }

    if (document.documentElement != null) {
      document.documentElement.append(container);

      // Most hints are already correctly positioned, but some near the edges
      // might need to be moved a tiny bit to avoid being partially off-screen.
      // Do this in a separate animation frame so that the hints appear on
      // screen as quickly as possible. Adjusting positions is just a tweak –
      // that can be delayed a little bit.
      window.requestAnimationFrame(() => {
        // Using double `requestAnimationFrame` since they run before paint.
        // See: https://youtu.be/cCOL7MC4Pl0?t=20m29s
        window.requestAnimationFrame(() => {
          const { innerWidth, innerHeight } = window;
          for (const child of root.children) {
            const rect = child.getBoundingClientRect();
            if (rect.left < 0) {
              child.style.marginRight = `${Math.round(rect.left)}px`;
            }
            if (rect.top < 0) {
              child.style.marginTop = `${Math.round(-rect.top)}px`;
            }
            if (rect.right > innerWidth) {
              child.style.marginRight = `${Math.round(
                rect.right - innerWidth
              )}px`;
            }
            if (rect.bottom > innerHeight) {
              child.style.marginTop = `${Math.round(
                innerHeight - rect.bottom
              )}px`;
            }
          }
        });
      });
    }

    this.sendMessage({
      type: "Rendered",
      timestamp: performance.now(),
    });
  }

  updateHints(updates: Array<HintUpdate>) {
    const container = document.getElementById(CONTAINER_ID);
    const root = container == null ? undefined : container.shadowRoot;
    if (root == null) {
      console.error("RendererProgram#updateHints: missing root", container);
      return;
    }

    const hints = root.querySelectorAll(`.${HINT_CLASS}`);

    for (const [index, update] of updates.entries()) {
      const child = hints[index];

      if (child == null) {
        console.error(
          "RendererProgram#updateHints: missing child",
          index,
          update
        );
        continue;
      }

      child.classList.toggle(HIDDEN_HINT_CLASS, update.type === "Hide");

      if (update.type === "Update") {
        emptyElement(child);
        if (update.matched !== "") {
          const matched = document.createElement("span");
          matched.className = MATCHED_CHARS_CLASS;
          matched.append(document.createTextNode(update.matched));
          child.append(matched);
        }
        child.append(document.createTextNode(update.rest));
      }
    }
  }

  unrender() {
    const container = document.getElementById(CONTAINER_ID);
    if (container != null) {
      container.remove();
    }
  }
}

function setStyles(element: HTMLElement, styles: { [string]: string }) {
  for (const [property, value] of Object.entries(styles)) {
    // $FlowIgnore: Flow thinks that `value` is `mixed` here, but it is a `string`.
    element.style.setProperty(property, value, "important");
  }
}

function emptyElement(element: HTMLElement) {
  while (element.firstChild != null) {
    element.removeChild(element.firstChild);
  }
}
<|endoftext|>


<commit_before>import {buildQueryString, parseQueryString} from 'aurelia-path';
import {State} from './state';
import {
  StaticSegment,
  DynamicSegment,
  StarSegment,
  EpsilonSegment
} from './segments';

/**
* Class that parses route patterns and matches path strings.
*
* @class RouteRecognizer
* @constructor
*/
export class RouteRecognizer {
  constructor() {
    this.rootState = new State();
    this.names = {};
  }

  /**
  * Parse a route pattern and add it to the collection of recognized routes.
  *
  * @param route The route to add.
  */
  add(route: ConfigurableRoute|ConfigurableRoute[]): State {
    if (Array.isArray(route)) {
      route.forEach(r => this.add(r));
      return undefined;
    }

    let currentState = this.rootState;
    let regex = '^';
    let types = { statics: 0, dynamics: 0, stars: 0 };
    let names = [];
    let routeName = route.handler.name;
    let isEmpty = true;
    let segments = parse(route.path, names, types, route.caseSensitive);

    for (let i = 0, ii = segments.length; i < ii; i++) {
      let segment = segments[i];
      if (segment instanceof EpsilonSegment) {
        continue;
      }

      isEmpty = false;

      // Add a '/' for the new segment
      currentState = currentState.put({ validChars: '/' });
      regex += '/';

      // Add a representation of the segment to the NFA and regex
      currentState = addSegment(currentState, segment);
      regex += segment.regex();
    }

    if (isEmpty) {
      currentState = currentState.put({ validChars: '/' });
      regex += '/';
    }

    let handlers = [{ handler: route.handler, names: names }];

    if (routeName) {
      let routeNames = Array.isArray(routeName) ? routeName : [routeName];
      for (let i = 0; i < routeNames.length; i++) {
        this.names[routeNames[i]] = {
          segments: segments,
          handlers: handlers
        };
      }
    }

    currentState.handlers = handlers;
    currentState.regex = new RegExp(regex + '$', route.caseSensitive ? '' : 'i');
    currentState.types = types;

    return currentState;
  }

  /**
  * Retrieve the handlers registered for the named route.
  *
  * @param name The name of the route.
  * @returns The handlers.
  */
  handlersFor(name: string): HandlerEntry[] {
    let route = this.names[name];
    if (!route) {
      throw new Error(`There is no route named ${name}`);
    }

    return [...route.handlers];
  }

  /**
  * Check if this RouteRecognizer recognizes a named route.
  *
  * @param name The name of the route.
  * @returns True if the named route is recognized.
  */
  hasRoute(name: string): boolean {
    return !!this.names[name];
  }

  /**
  * Generate a path and query string from a route name and params object.
  *
  * @param name The name of the route.
  * @param params The route params to use when populating the pattern.
  *  Properties not required by the pattern will be appended to the query string.
  * @returns The generated absolute path and query string.
  */
  generate(name: string, params: Object): string {
    let routeParams = Object.assign({}, params);

    let route = this.names[name];
    if (!route) {
      throw new Error(`There is no route named ${name}`);
    }

    let segments = route.segments;
    let consumed = {};
    let output = '';

    for (let i = 0, l = segments.length; i < l; i++) {
      let segment = segments[i];

      if (segment instanceof EpsilonSegment) {
        continue;
      }

      output += '/';
      let segmentValue = segment.generate(routeParams, consumed);
      if (segmentValue === null || segmentValue === undefined) {
        throw new Error(`A value is required for route parameter '${segment.name}' in route '${name}'.`);
      }

      output += segmentValue;
    }

    if (output.charAt(0) !== '/') {
      output = '/' + output;
    }

    // remove params used in the path and add the rest to the querystring
    for (let param in consumed) {
      delete routeParams[param];
    }

    let queryString = buildQueryString(routeParams);
    output += queryString ? `?${queryString}` : '';

    return output;
  }

  /**
  * Match a path string against registered route patterns.
  *
  * @param path The path to attempt to match.
  * @returns Array of objects containing `handler`, `params`, and
  *  `isDynanic` values for the matched route(s), or undefined if no match
  *  was found.
  */
  recognize(path: string): RecognizedRoute[] {
    let states = [this.rootState];
    let queryParams = {};
    let isSlashDropped = false;
    let normalizedPath = path;

    let queryStart = normalizedPath.indexOf('?');
    if (queryStart !== -1) {
      let queryString = normalizedPath.substr(queryStart + 1, normalizedPath.length);
      normalizedPath = normalizedPath.substr(0, queryStart);
      queryParams = parseQueryString(queryString);
    }

    normalizedPath = decodeURI(normalizedPath);

    if (normalizedPath.charAt(0) !== '/') {
      normalizedPath = '/' + normalizedPath;
    }

    let pathLen = normalizedPath.length;
    if (pathLen > 1 && normalizedPath.charAt(pathLen - 1) === '/') {
      normalizedPath = normalizedPath.substr(0, pathLen - 1);
      isSlashDropped = true;
    }

    for (let i = 0, l = normalizedPath.length; i < l; i++) {
      states = recognizeChar(states, normalizedPath.charAt(i));
      if (!states.length) {
        break;
      }
    }

    let solutions = [];
    for (let i = 0, l = states.length; i < l; i++) {
      if (states[i].handlers) {
        solutions.push(states[i]);
      }
    }

    states = sortSolutions(solutions);

    let state = solutions[0];
    if (state && state.handlers) {
      // if a trailing slash was dropped and a star segment is the last segment
      // specified, put the trailing slash back
      if (isSlashDropped && state.regex.source.slice(-5) === '(.+)$') {
        normalizedPath = normalizedPath + '/';
      }

      return findHandler(state, normalizedPath, queryParams);
    }

    return undefined;
  }
}

class RecognizeResults {
  constructor(queryParams: Object) {
    this.splice = Array.prototype.splice;
    this.slice = Array.prototype.slice;
    this.push = Array.prototype.push;
    this.length = 0;
    this.queryParams = queryParams || {};
  }
}

function parse(route, names, types, caseSensitive) {
  // normalize route as not starting with a '/'. Recognition will
  // also normalize.
  let normalizedRoute = route;
  if (route.charAt(0) === '/') {
    normalizedRoute = route.substr(1);
  }

  let results = [];

  let splitRoute = normalizedRoute.split('/');
  for (let i = 0, ii = splitRoute.length; i < ii; ++i) {
    let segment = splitRoute[i];
    let match = segment.match(/^:([^\/]+)$/);
    if (match) {
      results.push(new DynamicSegment(match[1]));
      names.push(match[1]);
      types.dynamics++;
      continue;
    }

    match = segment.match(/^\*([^\/]+)$/);
    if (match) {
      results.push(new StarSegment(match[1]));
      names.push(match[1]);
      types.stars++;
    } else if (segment === '') {
      results.push(new EpsilonSegment());
    } else {
      results.push(new StaticSegment(segment, caseSensitive));
      types.statics++;
    }
  }

  return results;
}

// This is a somewhat naive strategy, but should work in a lot of cases
// A better strategy would properly resolve /posts/:id/new and /posts/edit/:id.
//
// This strategy generally prefers more static and less dynamic matching.
// Specifically, it
//
//  * prefers fewer stars to more, then
//  * prefers using stars for less of the match to more, then
//  * prefers fewer dynamic segments to more, then
//  * prefers more static segments to more
function sortSolutions(states) {
  return states.sort((a, b) => {
    if (a.types.stars !== b.types.stars) {
      return a.types.stars - b.types.stars;
    }

    if (a.types.stars) {
      if (a.types.statics !== b.types.statics) {
        return b.types.statics - a.types.statics;
      }
      if (a.types.dynamics !== b.types.dynamics) {
        return b.types.dynamics - a.types.dynamics;
      }
    }

    if (a.types.dynamics !== b.types.dynamics) {
      return a.types.dynamics - b.types.dynamics;
    }

    if (a.types.statics !== b.types.statics) {
      return b.types.statics - a.types.statics;
    }

    return 0;
  });
}

function recognizeChar(states, ch) {
  let nextStates = [];

  for (let i = 0, l = states.length; i < l; i++) {
    let state = states[i];
    nextStates.push(...state.match(ch));
  }

  return nextStates;
}

function findHandler(state, path, queryParams) {
  let handlers = state.handlers;
  let regex = state.regex;
  let captures = path.match(regex);
  let currentCapture = 1;
  let result = new RecognizeResults(queryParams);

  for (let i = 0, l = handlers.length; i < l; i++) {
    let handler = handlers[i];
    let names = handler.names;
    let params = {};

    for (let j = 0, m = names.length; j < m; j++) {
      params[names[j]] = captures[currentCapture++];
    }

    result.push({ handler: handler.handler, params: params, isDynamic: !!names.length });
  }

  return result;
}

function addSegment(currentState, segment) {
  let state = currentState;
  segment.eachChar(ch => {
    state = state.put(ch);
  });

  return state;
}
<commit_msg>fix(route-recognizer): respect explicit href during generation<commit_after>import {buildQueryString, parseQueryString} from 'aurelia-path';
import {State} from './state';
import {
  StaticSegment,
  DynamicSegment,
  StarSegment,
  EpsilonSegment
} from './segments';

/**
* Class that parses route patterns and matches path strings.
*
* @class RouteRecognizer
* @constructor
*/
export class RouteRecognizer {
  constructor() {
    this.rootState = new State();
    this.names = {};
  }

  /**
  * Parse a route pattern and add it to the collection of recognized routes.
  *
  * @param route The route to add.
  */
  add(route: ConfigurableRoute|ConfigurableRoute[]): State {
    if (Array.isArray(route)) {
      route.forEach(r => this.add(r));
      return undefined;
    }

    let currentState = this.rootState;
    let regex = '^';
    let types = { statics: 0, dynamics: 0, stars: 0 };
    let names = [];
    let routeName = route.handler.name;
    let isEmpty = true;
    let segments = parse(route.path, names, types, route.caseSensitive);

    for (let i = 0, ii = segments.length; i < ii; i++) {
      let segment = segments[i];
      if (segment instanceof EpsilonSegment) {
        continue;
      }

      isEmpty = false;

      // Add a '/' for the new segment
      currentState = currentState.put({ validChars: '/' });
      regex += '/';

      // Add a representation of the segment to the NFA and regex
      currentState = addSegment(currentState, segment);
      regex += segment.regex();
    }

    if (isEmpty) {
      currentState = currentState.put({ validChars: '/' });
      regex += '/';
    }

    let handlers = [{ handler: route.handler, names: names }];

    if (routeName) {
      let routeNames = Array.isArray(routeName) ? routeName : [routeName];
      for (let i = 0; i < routeNames.length; i++) {
        this.names[routeNames[i]] = {
          segments: segments,
          handlers: handlers
        };
      }
    }

    currentState.handlers = handlers;
    currentState.regex = new RegExp(regex + '$', route.caseSensitive ? '' : 'i');
    currentState.types = types;

    return currentState;
  }

  /**
  * Retrieve the handlers registered for the named route.
  *
  * @param name The name of the route.
  * @returns The handlers.
  */
  handlersFor(name: string): HandlerEntry[] {
    let route = this.names[name];
    if (!route) {
      throw new Error(`There is no route named ${name}`);
    }

    return [...route.handlers];
  }

  /**
  * Check if this RouteRecognizer recognizes a named route.
  *
  * @param name The name of the route.
  * @returns True if the named route is recognized.
  */
  hasRoute(name: string): boolean {
    return !!this.names[name];
  }

  /**
  * Generate a path and query string from a route name and params object.
  *
  * @param name The name of the route.
  * @param params The route params to use when populating the pattern.
  *  Properties not required by the pattern will be appended to the query string.
  * @returns The generated absolute path and query string.
  */
  generate(name: string, params: Object): string {
    let route = this.names[name];
    if (!route) {
      throw new Error(`There is no route named ${name}`);
    }

    let handler = route.handlers[0].handler;
    if (handler.href) {
      return handler.href;
    }

    let routeParams = Object.assign({}, params);
    let segments = route.segments;
    let consumed = {};
    let output = '';

    for (let i = 0, l = segments.length; i < l; i++) {
      let segment = segments[i];

      if (segment instanceof EpsilonSegment) {
        continue;
      }

      output += '/';
      let segmentValue = segment.generate(routeParams, consumed);
      if (segmentValue === null || segmentValue === undefined) {
        throw new Error(`A value is required for route parameter '${segment.name}' in route '${name}'.`);
      }

      output += segmentValue;
    }

    if (output.charAt(0) !== '/') {
      output = '/' + output;
    }

    // remove params used in the path and add the rest to the querystring
    for (let param in consumed) {
      delete routeParams[param];
    }

    let queryString = buildQueryString(routeParams);
    output += queryString ? `?${queryString}` : '';

    return output;
  }

  /**
  * Match a path string against registered route patterns.
  *
  * @param path The path to attempt to match.
  * @returns Array of objects containing `handler`, `params`, and
  *  `isDynanic` values for the matched route(s), or undefined if no match
  *  was found.
  */
  recognize(path: string): RecognizedRoute[] {
    let states = [this.rootState];
    let queryParams = {};
    let isSlashDropped = false;
    let normalizedPath = path;

    let queryStart = normalizedPath.indexOf('?');
    if (queryStart !== -1) {
      let queryString = normalizedPath.substr(queryStart + 1, normalizedPath.length);
      normalizedPath = normalizedPath.substr(0, queryStart);
      queryParams = parseQueryString(queryString);
    }

    normalizedPath = decodeURI(normalizedPath);

    if (normalizedPath.charAt(0) !== '/') {
      normalizedPath = '/' + normalizedPath;
    }

    let pathLen = normalizedPath.length;
    if (pathLen > 1 && normalizedPath.charAt(pathLen - 1) === '/') {
      normalizedPath = normalizedPath.substr(0, pathLen - 1);
      isSlashDropped = true;
    }

    for (let i = 0, l = normalizedPath.length; i < l; i++) {
      states = recognizeChar(states, normalizedPath.charAt(i));
      if (!states.length) {
        break;
      }
    }

    let solutions = [];
    for (let i = 0, l = states.length; i < l; i++) {
      if (states[i].handlers) {
        solutions.push(states[i]);
      }
    }

    states = sortSolutions(solutions);

    let state = solutions[0];
    if (state && state.handlers) {
      // if a trailing slash was dropped and a star segment is the last segment
      // specified, put the trailing slash back
      if (isSlashDropped && state.regex.source.slice(-5) === '(.+)$') {
        normalizedPath = normalizedPath + '/';
      }

      return findHandler(state, normalizedPath, queryParams);
    }

    return undefined;
  }
}

class RecognizeResults {
  constructor(queryParams: Object) {
    this.splice = Array.prototype.splice;
    this.slice = Array.prototype.slice;
    this.push = Array.prototype.push;
    this.length = 0;
    this.queryParams = queryParams || {};
  }
}

function parse(route, names, types, caseSensitive) {
  // normalize route as not starting with a '/'. Recognition will
  // also normalize.
  let normalizedRoute = route;
  if (route.charAt(0) === '/') {
    normalizedRoute = route.substr(1);
  }

  let results = [];

  let splitRoute = normalizedRoute.split('/');
  for (let i = 0, ii = splitRoute.length; i < ii; ++i) {
    let segment = splitRoute[i];
    let match = segment.match(/^:([^\/]+)$/);
    if (match) {
      results.push(new DynamicSegment(match[1]));
      names.push(match[1]);
      types.dynamics++;
      continue;
    }

    match = segment.match(/^\*([^\/]+)$/);
    if (match) {
      results.push(new StarSegment(match[1]));
      names.push(match[1]);
      types.stars++;
    } else if (segment === '') {
      results.push(new EpsilonSegment());
    } else {
      results.push(new StaticSegment(segment, caseSensitive));
      types.statics++;
    }
  }

  return results;
}

// This is a somewhat naive strategy, but should work in a lot of cases
// A better strategy would properly resolve /posts/:id/new and /posts/edit/:id.
//
// This strategy generally prefers more static and less dynamic matching.
// Specifically, it
//
//  * prefers fewer stars to more, then
//  * prefers using stars for less of the match to more, then
//  * prefers fewer dynamic segments to more, then
//  * prefers more static segments to more
function sortSolutions(states) {
  return states.sort((a, b) => {
    if (a.types.stars !== b.types.stars) {
      return a.types.stars - b.types.stars;
    }

    if (a.types.stars) {
      if (a.types.statics !== b.types.statics) {
        return b.types.statics - a.types.statics;
      }
      if (a.types.dynamics !== b.types.dynamics) {
        return b.types.dynamics - a.types.dynamics;
      }
    }

    if (a.types.dynamics !== b.types.dynamics) {
      return a.types.dynamics - b.types.dynamics;
    }

    if (a.types.statics !== b.types.statics) {
      return b.types.statics - a.types.statics;
    }

    return 0;
  });
}

function recognizeChar(states, ch) {
  let nextStates = [];

  for (let i = 0, l = states.length; i < l; i++) {
    let state = states[i];
    nextStates.push(...state.match(ch));
  }

  return nextStates;
}

function findHandler(state, path, queryParams) {
  let handlers = state.handlers;
  let regex = state.regex;
  let captures = path.match(regex);
  let currentCapture = 1;
  let result = new RecognizeResults(queryParams);

  for (let i = 0, l = handlers.length; i < l; i++) {
    let handler = handlers[i];
    let names = handler.names;
    let params = {};

    for (let j = 0, m = names.length; j < m; j++) {
      params[names[j]] = captures[currentCapture++];
    }

    result.push({ handler: handler.handler, params: params, isDynamic: !!names.length });
  }

  return result;
}

function addSegment(currentState, segment) {
  let state = currentState;
  segment.eachChar(ch => {
    state = state.put(ch);
  });

  return state;
}
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Button, DropdownButton, MenuItem } from 'react-bootstrap';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import { connect } from 'react-redux';
import MarksTable from '../../components/MarksTable';
import UnitView from '../../components/UnitView';
import { setAnswer, setAnswerBody } from '../../actions/units';
import { setSecondMenu } from '../../actions/menu';
import updateAnswer from '../../gql/updateAnswer.gql';
import createAnswer from '../../gql/createAnswer.gql';
import retrieveAnswerQuery from '../../gql/retrieveAnswer.gql';
import s from './Unit.css';
import Link from '../../components/Link/Link';
import ModalUnitEdit from '../../components/ModalUnitEdit';
import { showModal } from '../../actions/modals';
import IconButton from '../../components/IconButton';

class Unit extends React.Component {
  static propTypes = {
    dispatch: PropTypes.func.isRequired,
    course: PropTypes.shape({
      id: PropTypes.string.isRequired,
      title: PropTypes.string.isRequired,
    }).isRequired,
    role: PropTypes.string.isRequired,
    user: PropTypes.shape({
      id: PropTypes.string,
      email: PropTypes.string,
    }),
    unit: PropTypes.shape({
      id: PropTypes.string,
      title: PropTypes.string,
      body: PropTypes.string,
    }).isRequired,
    answer: PropTypes.shape({
      id: PropTypes.string,
      body: PropTypes.shape,
    }).isRequired,
  };

  static contextTypes = {
    fetch: PropTypes.func.isRequired,
  };

  static defaultProps = {
    user: PropTypes.shape({
      id: PropTypes.string,
      email: PropTypes.string,
    }),
  };

  static getAnswersByUser(answers, userId) {
    const data = {};
    answers.forEach(answer => {
      const { id } = answer.user;
      const ua = data[id] || { user: answer.user, answers: [] };
      data[id] = ua;
      ua.answers.push(answer);
    });
    return {
      users: Object.values(data).map(d => ({
        ...d.user,
        needMark: d.answers.some(ans => !ans.marks.length),
      })),
      answers: (data[userId] || { answers: [] }).answers.map(ans => ({
        ...ans,
        needMark: !ans.marks.length,
      })),
    };
  }

  static sortUsers(a, b) {
    const name1 = a.profile.displayName;
    const name2 = b.profile.displayName;
    return name1.localeCompare(name2);
  }

  constructor(props) {
    super(props);
    this.state = {
      answerCur: 0,
      answers: [],
      isSaving: false,
      saveStatus: '',
      saveMassage: '',
    };
  }

  async componentDidMount() {
    const { user } = this.props;
    if (user) {
      await this.retrieveAnswer();
    }
  }

  componentWillUnmount() {
    const { dispatch } = this.props;
    dispatch(setSecondMenu('unit', []));
  }

  handleChange = name => ({ target: { value } }) =>
    this.setState({
      [name]: value,
    });

  handleUserSelect = id => {
    const userCur = id;
    this.setState({ userCur, answerCur: undefined });
  };

  handleAnswerSelect = id => {
    const answerCur = id;
    this.setState({ answerCur });
    this.props.dispatch(
      setAnswer(this.state.answers.find(ans => ans.id === answerCur)),
    );
  };

  /**
   * Prepare data for sending
   * @returns {Object} object with body and data fields, where body - Object
   * with answer, data - additional FormData to be sent
   */
  prepareAnswer() {
    const { body } = this.props.answer;
    const files = Object.entries(body).filter(
      ans => ans[1] instanceof window.File,
    );
    const uploadOrder = [];
    const data = [];
    files.forEach(file => {
      uploadOrder.push(file[0]);
      data.push(['upload', file[1]]);
      delete body[file[0]];
    });
    data.unshift(['upload_order', JSON.stringify(uploadOrder)]);
    return { body, data };
  }

  /**
   * Sends prepared data
   * @param {string} query - query for GraphQL (create or update)
   * @param {Object} answer - body of the answer
   * @param {Object} variables - additional varibales for query
   */
  sendAnswer(query, answer, variables) {
    const formData = new FormData();
    formData.append('query', query);
    formData.append(
      'variables',
      JSON.stringify({ ...variables, body: JSON.stringify(answer.body) }),
    );
    answer.data.forEach(d => formData.append(d[0], d[1]));
    return this.context.fetch('/graphql', {
      body: formData,
    });
  }

  /**
   * Generates and sends FormData to the server. Expects that
   * this.props.answers contains Object with fields, some of them may be File
   */
  saveAnswer = async () => {
    const { course, unit } = this.props;
    this.setState({ isSaving: true });
    const answerId = this.props.answer.id;
    const answer = this.prepareAnswer();
    try {
      await this.sendAnswer(
        answerId ? updateAnswer : createAnswer,
        answer,
        answerId ? { id: answerId } : { courseId: course.id, unitId: unit.id },
      );
      this.setState({
        saveStatus: 'success',
        saveMassage: 'save completed successfully',
      });
    } catch (e) {
      this.setState({ saveStatus: 'danger', saveMassage: 'save error' });
    } finally {
      this.setState({ isSaving: false });
    }
  };

  addEmptyAnswer = async () => {
    const { course, unit } = this.props;
    const { userCur } = this.state;
    await this.sendAnswer(
      createAnswer,
      { body: {}, data: [['upload_order', '[]']] },
      { courseId: course.id, unitId: unit.id, userId: userCur },
    );
  };

  async retrieveAnswer() {
    const { user, course, unit } = this.props;
    const resp = await this.context.fetch('/graphql', {
      body: JSON.stringify({
        query: retrieveAnswerQuery,
        variables: {
          userIds: (!user.isAdmin && [user.id]) || null,
          unitIds: [unit.id],
          courseIds: [course.id],
        },
      }),
    });
    const { data } = await resp.json();
    const { answers = [] } = data.courses[0].units[0];
    if (answers && answers.length) {
      this.setState({
        answers: answers.map(answer => {
          const ans = answer;
          ans.body = JSON.parse(answer.body);
          return ans;
        }),
      });
    }
    this.props.dispatch(setAnswer(answers[0] || { body: {} }));
  }

  render() {
    const { user = {}, role, unit, course, dispatch } = this.props;
    const { answers = [], userCur, saveStatus, saveMassage } = this.state;
    const { answerCur } = this.state;
    const ua = Unit.getAnswersByUser(answers, userCur);
    const uids = ua.users.map(u => u.id);
    ua.users.sort(Unit.sortUsers);
    const users = ua.users.concat(
      course.users.filter(u => !uids.includes(u.id)).sort(Unit.sortUsers),
    );
    // TODO: select latest answer
    // if (!answerCur) answerCur = (ua.answers[ua.answers.length - 1] || {}).id;
    const answerUser = users.find(u => u.id === userCur);
    const answer = answers.find(ans => ans.id === answerCur);
    return (

        <ModalUnitEdit modalId="modalUnitEdit" />

            <Link to={`/courses/${course.id}`}>{course.title}</Link>
            {`/${unit.title}`}
            {role === 'teacher' && (
              <IconButton
                onClick={() => dispatch(showModal('modalUnitEdit'))}
                glyph="pencil"
              />
            )}
            {(role === 'teacher' || user.isAdmin) &&
              unit.answerable && (
                <React.Fragment>
                  <DropdownButton
                    id="user_chooser"
                    title={
                      (answerUser && answerUser.profile.displayName) || 'User'
                    }
                    onSelect={this.handleUserSelect}
                  >
                    {users.map(u => (
                      <MenuItem
                        key={u.id}
                        eventKey={u.id}
                        active={u.id === userCur}
                        className={u.needMark && s['need-mark']}
                      >
                        {u.profile.displayName}
                      </MenuItem>
                    ))}
                  </DropdownButton>
                  <DropdownButton
                    id="answer_chooser"
                    title={(answer && answer.createdAt) || 'Answer'}
                    onSelect={this.handleAnswerSelect}
                  >
                    {ua.answers.map(ans => (
                      <MenuItem
                        key={ans.id}
                        eventKey={ans.id}
                        active={ans.id === answerCur}
                        className={ans.needMark && s['need-mark']}
                      >
                        {ans.createdAt}
                      </MenuItem>
                    ))}
                  </DropdownButton>
                </React.Fragment>
              )}

          <UnitView
            answerId={this.props.answer.id}
            value={this.props.answer.body}
            body={unit.body}
            onChange={val => this.props.dispatch(setAnswerBody(val))}
            onHeadersChange={headers =>
              dispatch(
                setSecondMenu('unit', headers.filter(item => item.level === 2)),
              )
            }
          />
          {unit.answerable &&
            user && (
              <Button onClick={this.saveAnswer} disabled={this.state.isSaving}>
                Save
              </Button>
            )}
          {unit.answerable &&
            saveStatus && <Alert variant={saveStatus}>{saveMassage}</Alert>}
          {unit.answerable &&
            (answer ? (
              <MarksTable />
            ) : (

                This unit has no answers yet
                {role === 'teacher' &&
                  answerUser && (
                    <Button size="sm" onClick={this.addEmptyAnswer}>
                      Add empty answer
                    </Button>
                  )}

            ))}

    );
  }
}

const mapStateToProps = state => ({
  unit: state.unit,
  user: state.user,
  answer: state.answer,
});

export default connect(mapStateToProps)(withStyles(s)(Unit));
<commit_msg>Show marks to user<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Button, DropdownButton, MenuItem } from 'react-bootstrap';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import { connect } from 'react-redux';
import MarksTable from '../../components/MarksTable';
import UnitView from '../../components/UnitView';
import { setAnswer, setAnswerBody } from '../../actions/units';
import { setSecondMenu } from '../../actions/menu';
import updateAnswer from '../../gql/updateAnswer.gql';
import createAnswer from '../../gql/createAnswer.gql';
import retrieveAnswerQuery from '../../gql/retrieveAnswer.gql';
import s from './Unit.css';
import Link from '../../components/Link/Link';
import ModalUnitEdit from '../../components/ModalUnitEdit';
import { showModal } from '../../actions/modals';
import IconButton from '../../components/IconButton';

class Unit extends React.Component {
  static propTypes = {
    dispatch: PropTypes.func.isRequired,
    course: PropTypes.shape({
      id: PropTypes.string.isRequired,
      title: PropTypes.string.isRequired,
    }).isRequired,
    role: PropTypes.string.isRequired,
    user: PropTypes.shape({
      id: PropTypes.string,
      email: PropTypes.string,
    }),
    unit: PropTypes.shape({
      id: PropTypes.string,
      title: PropTypes.string,
      body: PropTypes.string,
    }).isRequired,
    answer: PropTypes.shape({
      id: PropTypes.string,
      body: PropTypes.shape,
    }).isRequired,
  };

  static contextTypes = {
    fetch: PropTypes.func.isRequired,
  };

  static defaultProps = {
    user: PropTypes.shape({
      id: PropTypes.string,
      email: PropTypes.string,
    }),
  };

  static getAnswersByUser(answers, userId) {
    const data = {};
    answers.forEach(answer => {
      const { id } = answer.user;
      const ua = data[id] || { user: answer.user, answers: [] };
      data[id] = ua;
      ua.answers.push(answer);
    });
    return {
      users: Object.values(data).map(d => ({
        ...d.user,
        needMark: d.answers.some(ans => !ans.marks.length),
      })),
      answers: (data[userId] || { answers: [] }).answers.map(ans => ({
        ...ans,
        needMark: !ans.marks.length,
      })),
    };
  }

  static sortUsers(a, b) {
    const name1 = a.profile.displayName;
    const name2 = b.profile.displayName;
    return name1.localeCompare(name2);
  }

  constructor(props) {
    super(props);
    this.state = {
      answerCur: 0,
      answers: [],
      isSaving: false,
      saveStatus: '',
      saveMassage: '',
    };
  }

  async componentDidMount() {
    const { user } = this.props;
    if (user) {
      await this.retrieveAnswer();
    }
  }

  componentWillUnmount() {
    const { dispatch } = this.props;
    dispatch(setSecondMenu('unit', []));
  }

  handleChange = name => ({ target: { value } }) =>
    this.setState({
      [name]: value,
    });

  handleUserSelect = id => {
    const userCur = id;
    this.setState({ userCur, answerCur: undefined });
  };

  handleAnswerSelect = id => {
    const answerCur = id;
    this.setState({ answerCur });
    this.props.dispatch(
      setAnswer(this.state.answers.find(ans => ans.id === answerCur)),
    );
  };

  /**
   * Prepare data for sending
   * @returns {Object} object with body and data fields, where body - Object
   * with answer, data - additional FormData to be sent
   */
  prepareAnswer() {
    const { body } = this.props.answer;
    const files = Object.entries(body).filter(
      ans => ans[1] instanceof window.File,
    );
    const uploadOrder = [];
    const data = [];
    files.forEach(file => {
      uploadOrder.push(file[0]);
      data.push(['upload', file[1]]);
      delete body[file[0]];
    });
    data.unshift(['upload_order', JSON.stringify(uploadOrder)]);
    return { body, data };
  }

  /**
   * Sends prepared data
   * @param {string} query - query for GraphQL (create or update)
   * @param {Object} answer - body of the answer
   * @param {Object} variables - additional varibales for query
   */
  sendAnswer(query, answer, variables) {
    const formData = new FormData();
    formData.append('query', query);
    formData.append(
      'variables',
      JSON.stringify({ ...variables, body: JSON.stringify(answer.body) }),
    );
    answer.data.forEach(d => formData.append(d[0], d[1]));
    return this.context.fetch('/graphql', {
      body: formData,
    });
  }

  /**
   * Generates and sends FormData to the server. Expects that
   * this.props.answers contains Object with fields, some of them may be File
   */
  saveAnswer = async () => {
    const { course, unit } = this.props;
    this.setState({ isSaving: true });
    const answerId = this.props.answer.id;
    const answer = this.prepareAnswer();
    try {
      await this.sendAnswer(
        answerId ? updateAnswer : createAnswer,
        answer,
        answerId ? { id: answerId } : { courseId: course.id, unitId: unit.id },
      );
      this.setState({
        saveStatus: 'success',
        saveMassage: 'save completed successfully',
      });
    } catch (e) {
      this.setState({ saveStatus: 'danger', saveMassage: 'save error' });
    } finally {
      this.setState({ isSaving: false });
    }
  };

  addEmptyAnswer = async () => {
    const { course, unit } = this.props;
    const { userCur } = this.state;
    await this.sendAnswer(
      createAnswer,
      { body: {}, data: [['upload_order', '[]']] },
      { courseId: course.id, unitId: unit.id, userId: userCur },
    );
  };

  async retrieveAnswer() {
    const { user, course, unit } = this.props;
    const resp = await this.context.fetch('/graphql', {
      body: JSON.stringify({
        query: retrieveAnswerQuery,
        variables: {
          userIds: (!user.isAdmin && [user.id]) || null,
          unitIds: [unit.id],
          courseIds: [course.id],
        },
      }),
    });
    const { data } = await resp.json();
    const { answers = [] } = data.courses[0].units[0];
    if (answers && answers.length) {
      this.setState({
        answers: answers.map(answer => {
          const ans = answer;
          ans.body = JSON.parse(answer.body);
          return ans;
        }),
      });
    }
    this.props.dispatch(setAnswer(answers[0] || { body: {} }));
  }

  render() {
    const { user = {}, role, unit, course, dispatch } = this.props;
    const {
      answers = [],
      userCur = user.id,
      saveStatus,
      saveMassage,
    } = this.state;
    let { answerCur } = this.state;
    const ua = Unit.getAnswersByUser(answers, userCur);
    const uids = ua.users.map(u => u.id);
    ua.users.sort(Unit.sortUsers);
    const users = ua.users.concat(
      course.users.filter(u => !uids.includes(u.id)).sort(Unit.sortUsers),
    );
    if (!answerCur) answerCur = (ua.answers[ua.answers.length - 1] || {}).id;
    const answerUser = users.find(u => u.id === userCur);
    const answer = answers.find(ans => ans.id === answerCur);
    return (

        <ModalUnitEdit modalId="modalUnitEdit" />

            <Link to={`/courses/${course.id}`}>{course.title}</Link>
            {`/${unit.title}`}
            {role === 'teacher' && (
              <IconButton
                onClick={() => dispatch(showModal('modalUnitEdit'))}
                glyph="pencil"
              />
            )}
            {(role === 'teacher' || user.isAdmin) &&
              unit.answerable && (
                <DropdownButton
                  id="user_chooser"
                  title={
                    (answerUser && answerUser.profile.displayName) || 'User'
                  }
                  onSelect={this.handleUserSelect}
                >
                  {users.map(u => (
                    <MenuItem
                      key={u.id}
                      eventKey={u.id}
                      active={u.id === userCur}
                      className={u.needMark && s['need-mark']}
                    >
                      {u.profile.displayName}
                    </MenuItem>
                  ))}
                </DropdownButton>
              )}
            {unit.answerable && (
              <DropdownButton
                id="answer_chooser"
                title={(answer && answer.createdAt) || 'Answer'}
                onSelect={this.handleAnswerSelect}
              >
                {ua.answers.map(ans => (
                  <MenuItem
                    key={ans.id}
                    eventKey={ans.id}
                    active={ans.id === answerCur}
                    className={ans.needMark && s['need-mark']}
                  >
                    {ans.createdAt}
                  </MenuItem>
                ))}
              </DropdownButton>
            )}

          <UnitView
            answerId={this.props.answer.id}
            value={this.props.answer.body}
            body={unit.body}
            onChange={val => this.props.dispatch(setAnswerBody(val))}
            onHeadersChange={headers =>
              dispatch(
                setSecondMenu('unit', headers.filter(item => item.level === 2)),
              )
            }
          />
          {unit.answerable &&
            user && (
              <Button onClick={this.saveAnswer} disabled={this.state.isSaving}>
                Save
              </Button>
            )}
          {unit.answerable &&
            saveStatus && <Alert variant={saveStatus}>{saveMassage}</Alert>}
          {unit.answerable &&
            (answer ? (
              <MarksTable />
            ) : (

                This unit has no answers yet
                {role === 'teacher' &&
                  answerUser && (
                    <Button size="sm" onClick={this.addEmptyAnswer}>
                      Add empty answer
                    </Button>
                  )}

            ))}

    );
  }
}

const mapStateToProps = state => ({
  unit: state.unit,
  user: state.user,
  answer: state.answer,
});

export default connect(mapStateToProps)(withStyles(s)(Unit));
<|endoftext|>