<commit_before>export { ScaleArray };

import { ArrayInterleaver } from "./ArrayInterleaver.js";

/**
 * Describe an array of scale (i.e. multiplier) values.
 *
 * @member {number[]} scales
 *   The scale (i.e. multiplier) values.
 *
 * @member {number} length
 *   The length of this scale array. Setting it will change the length.
 */
class ScaleArray {

  constructor( length ) {
    this.scales = new Array( length );
  }

  get length() {
    return this.scales.length;
  }

  set length( newLength ) {
    this.scales.length = newLength;
  }

  /**
   * @return {ScaleArray} Return a newly created ScaleArray which is a copy of this ScaleArray.
   */
  clone() {
    let result = new ScaleArray( this.scales.length );
    result.set_all_byScaleArray( this );
    return result;
  }

  /**
   * @param {number} thisIndex  The array index of this.scales[].
   * @param {number} N          Set ( this.scales[ thisIndex ] ) by ( N ). Default are ( N = 1 ) (i.e. no scale).
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_byN( thisIndex, N = 1 ) {
    this.scales[ thisIndex ] = N;
    return this;
  }

  /**
   * @param {number} thisIndex  The array index of this.scales[].
   * @param {number[]} Ns       Set ( this.scales[ thisIndex ] ) by ( Ns[ aIndex ] ).
   * @param {number} aIndex     The array index of Ns[].
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_byNs( thisIndex, Ns, aIndex ) {
    return this.set_one_byN( thisIndex, Ns[ aIndex ] );
  }

  /**
   * @param {number} thisIndex        The array index of this.scales[].
   * @param {ScaleArray} aScaleArray  Set ( this.scales[ thisIndex ] ) by ( aScaleArray.scales[ aIndex ] ).
   * @param {number} aIndex           The array index of aScaleArray.scales[].
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_byScaleArray( thisIndex, aScaleArray, aIndex ) {
    return this.set_one_byN( thisIndex, aScaleArray.scales[ aIndex ] );
  }

  /**
   * Set this.scales[ thisIndex ] to a scale value which could let source bounds [ fromLower, fromUpper ] completely insides destination
   * bounds [ toLower, toUpper ].
   *
   * Note: this.scales[ thisIndex ] may become Number.NaN, if it is impossible to let source bounds completely insides destination
   * bounds.
   *
   * @param {number} thisIndex  The array index of this.scales[].
   * @param {number} fromLower  The source bounds [ fromLower, fromUpper ]
   * @param {number} fromUpper  The source bounds [ fromLower, fromUpper ]
   * @param {number} toLower    The destination bounds [ toLower, toUpper ]
   * @param {number} toUpper    The destination bounds [ toLower, toUpper ]
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_by_fromLowerUpper_toLowerUpper( thisIndex, fromLower, fromUpper, toLower, toUpper ) {
    let aScale = ScaleArray.calc_scale_by_fromLowerUpper_toLowerUpper( fromLower, fromUpper, toLower, toUpper );
    this.scales[ thisIndex ] = aScale;
    return this;
  }

  /**
   * @param {number} thisIndex  The array index of this.scales[].
   * @param {number} N          Set ( this.scales[ thisIndex ] ) by ( 1 / N ).
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_byUndo_N( thisIndex, N ) {
    this.scales[ thisIndex ] = ( 1 / N );  // Reciprocal will undo the scale. (Note: Not work for zero.)
    return this;
  }

  /**
   * @param {number} thisIndex  The array index of this.scales[].
   * @param {number[]} Ns       Set ( this.scales[ thisIndex ] ) by ( 1 / Ns[ aIndex ] ).
   * @param {number} aIndex     The array index of Ns[].
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_byUndo_Ns( thisIndex, Ns, aIndex ) {
    this.scales[ thisIndex ] = ( 1 / Ns[ aIndex ] );  // Reciprocal will undo the scale. (Note: Not work for zero.)
    return this;
  }

  /**
   * @param {number} thisIndex        The array index of this.scales[].
   * @param {ScaleArray} aScaleArray  Set ( this.scales[ thisIndex ] ) by ( 1 / aScaleArray.scales[ aIndex ] ).
   * @param {number} aIndex           The array index of aScaleArray.scales[].
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_byUndo_ScaleArray( thisIndex, aScaleArray, aIndex ) {
    return this.set_one_byUndo_Ns( thisIndex, aScaleArray.scales, aIndex );
  }

  /**
   * @param {number} N  Set all ( this.scales[] ) by ( N ). Default are ( N = 1 ) (i.e. no scale).
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_all_byN( N = 1 ) {
    this.scales.fill( N );
    return this;
  }

  /**
   * @param {number[]} Ns  Set all ( this.scales[] ) by ( Ns[] ).
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_all_byNs( Ns ) {
    for ( let i = 0; i < this.scales.length; ++i ) {
      this.scales[ i ] = Ns[ i ];
    }
    return this;
  }

  /**
   * @param {ScaleArray} aScaleArray  Set all ( this.scales[] ) by ( aScaleArray.scales[] ).
   *
   * @return {ScaleArray} Return this (modified) object whose values are copied from aScaleArray.
   */
  set_all_byScaleArray( aScaleArray ) {
    return this.set_all_byNs( aScaleArray.scales );
  }

  /**
   * @param {number[]} Ns  Set all ( this.scales[] ) by ( 1 / Ns[] ).
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_all_byUndo_Ns( Ns ) {
    for ( let i = 0; i < this.scales.length; ++i ) {
      this.scales[ i ] = ( 1 / Ns[ i ] );  // Reciprocal will undo the scale. (Note: Not work for zero.)
    }
    return this;
  }

  /**
   * @param {ScaleArray} aScaleArray  Set all ( this.scales[] ) by ( 1 / aScaleArray.scales[] ).
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_all_byUndo_ScaleArray( aScaleArray ) {
    return this.set_all_byUndo_Ns( aScaleArray.scales );
  }

  /**
   * The this.length will be modified.
   *
   * @param {ScaleArray} inputScaleArray0  The ScaleArray of the 1st input.
   * @param {ScaleArray} inputScaleArray1  The BoundsArray of the 2nd input.
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_all_byScaleArray_concat_input0_input1( inputScaleArray0, inputScaleArray1 ) {

    let totalLength = ( inputScaleArray0?.length ?? 0 ) + ( inputScaleArray1?.length ?? 0 );
    this.length = totalLength;

    // Concat value scale array.
    let inChannel = 0;

    if ( inputScaleArray0 ) {
      for ( let inChannel0 = 0; inChannel0 < inputScaleArray0.length; ++inChannel0, ++inChannel ) {
        this.set_one_byScaleArray( inChannel, inputScaleArray0, inChannel0 );
      }
    }

    if ( inputScaleArray1 ) {
      for ( let inChannel1 = 0; inChannel1 < inputScaleArray1.length; ++inChannel1, ++inChannel ) {
        this.set_one_byScaleArray( inChannel, inputScaleArray1, inChannel1 );
      }
    }

    return this;
  }

  /**
   * @param {ScaleArray} lowerHalfScaleArray   The ScaleArray of the 1st output. Its .length will be modified.
   * @param {ScaleArray} higherHalfScaleArray  The ScaleArray of the 2nd output. Its .length will be modified.
   *
   * @return {ScaleArray} Return this (unmodified) object.
   */
  split_to_lowerHalf_higherHalf( lowerHalfScaleArray, higherHalfScaleArray ) {

    // If not divided by 2, let lower half have one more.
    let length_lowerHalf = Math.ceil( this.length / 2 );
    let length_higherHalf = this.length - length_lowerHalf;

    lowerHalfScaleArray.length = length_lowerHalf;
    higherHalfScaleArray.length = length_higherHalf;

    // Split value bounds array.
    let inChannel = 0;

    for ( let outChannel = 0; outChannel < length_lowerHalf; ++outChannel, ++inChannel ) {
      lowerHalfScaleArray.set_one_byBoundsArray( outChannel, this, inChannel );
    }

    for ( let outChannel = 0; outChannel < length_higherHalf; ++outChannel, ++inChannel ) {
      higherHalfScaleArray.set_one_byBoundsArray( outChannel, this, inChannel );
    }

    return this;
  }

  /**
   * Rearrange scales by interleaving as ( groupCount == 2 ). This element count must be even (i.e. divisible by 2).
   *
   * @param {Array} arrayTemp
   *   A temporary array for placing the original elements temporarily. Provide this array could reduce memory re-allocation
   * and improve performance.
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  interleave_asGrouptTwo( arrayTemp ) {
    ArrayInterleaver.interleave_asGrouptTwo( this.scales, 0, this.scales.length, arrayTemp );
    return this;
  }

  /**
   *
   * @param {number} fromLower  The source bounds [ fromLower, fromUpper ]
   * @param {number} fromUpper  The source bounds [ fromLower, fromUpper ]
   * @param {number} toLower    The destination bounds [ toLower, toUpper ]
   * @param {number} toUpper    The destination bounds [ toLower, toUpper ]
   *
   * @return {number}
   *   Return a scale value which could let source bounds [ fromLower, fromUpper ] completely insides destination bounds [ toLower, toUpper ].
   * Return Number.NaN, if it is impossible to do that.
   */
  static calc_scale_by_fromLowerUpper_toLowerUpper( fromLower, fromUpper, toLower, toUpper ) {
    let srcLower = Math.min( fromLower, fromUpper ); // Confirm ( lower <= upper ).
    let srcUpper = Math.max( fromLower, fromUpper );
    let dstLower = Math.min( toLower, toUpper );
    let dstUpper = Math.max( toLower, toUpper );

    let scale;

    // 1. Try lower bound.
    //
    // Note:
    //  ( x / +Infinity ) == +0
    //  ( x / -Infinity ) == -0
    //  (  0 / x ) = 0
    //  ( -1 / 0 ) = -Infinity
    //  ( +1 / 0 ) = +Infinity
    //  (  0 / 0 ) = NaN
    //  ( +-Infinity / +-Infinity ) == NaN
    //
    scale = dstLower / srcLower;

    // 1.2 Verification.
    //   - If scale is zero or -Infinity or +Infinity or NaN, it is always failed.
    //   - If scaled source upper bound is out of destination range, it is also failed.
    let adjustedUpper = srcUpper * scale;
    if ( ( scale == 0 ) || ( !Number.isFinite( scale ) ) || ( adjustedUpper < dstLower ) || ( adjustedUpper > dstUpper ) ) {

      // 2. Try upperer bound, since it is failed to fit [ srcLower, srcUpper ] into [ dstLower, dstUpper ] by scale according to lower bound.
      scale = dstUpper / srcUpper;

      // 2.2 Verification. If scale is zero, it is always failed. Otherwise, check it by lower side.
      //   - If scale is zero or -Infinity or +Infinity or NaN, it is always failed.
      //   - If scaled source lower bound is out of destination range, it is also failed.
      let adjustedLower = srcLower * scale;
      if ( ( scale == 0 ) || ( !Number.isFinite( scale ) ) || ( adjustedLower < dstLower ) || ( adjustedLower > dstUpper ) ) {
        // 3. It is impossible to fit [ srcLower, srcUpper ] into [ dstLower, dstUpper ] only by scale because all cases are tried and failed.
        scale = Number.NaN;
      }
    }

    return scale;
  }

}

<commit_msg>Update ScaleArray.js<commit_after>export { ScaleArray };

import { ArrayInterleaver } from "./ArrayInterleaver.js";

/**
 * Describe an array of scale (i.e. multiplier) values.
 *
 * @member {number[]} scales
 *   The scale (i.e. multiplier) values.
 *
 * @member {number} length
 *   The length of this scale array. Setting it will change the length.
 */
class ScaleArray {

  constructor( length ) {
    this.scales = new Array( length );
  }

  get length() {
    return this.scales.length;
  }

  set length( newLength ) {
    this.scales.length = newLength;
  }

  /**
   * @return {ScaleArray} Return a newly created ScaleArray which is a copy of this ScaleArray.
   */
  clone() {
    let result = new ScaleArray( this.scales.length );
    result.set_all_byScaleArray( this );
    return result;
  }

  /**
   * @param {number} thisIndex  The array index of this.scales[].
   * @param {number} N          Set ( this.scales[ thisIndex ] ) by ( N ). Default are ( N = 1 ) (i.e. no scale).
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_byN( thisIndex, N = 1 ) {
    this.scales[ thisIndex ] = N;
    return this;
  }

  /**
   * @param {number} thisIndex  The array index of this.scales[].
   * @param {number[]} Ns       Set ( this.scales[ thisIndex ] ) by ( Ns[ aIndex ] ).
   * @param {number} aIndex     The array index of Ns[].
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_byNs( thisIndex, Ns, aIndex ) {
    return this.set_one_byN( thisIndex, Ns[ aIndex ] );
  }

  /**
   * @param {number} thisIndex        The array index of this.scales[].
   * @param {ScaleArray} aScaleArray  Set ( this.scales[ thisIndex ] ) by ( aScaleArray.scales[ aIndex ] ).
   * @param {number} aIndex           The array index of aScaleArray.scales[].
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_byScaleArray( thisIndex, aScaleArray, aIndex ) {
    return this.set_one_byN( thisIndex, aScaleArray.scales[ aIndex ] );
  }

  /**
   * Set this.scales[ thisIndex ] to a scale value which could let source bounds [ fromLower, fromUpper ] completely insides destination
   * bounds [ toLower, toUpper ].
   *
   * Note: this.scales[ thisIndex ] may become Number.NaN, if it is impossible to let source bounds completely insides destination
   * bounds.
   *
   * @param {number} thisIndex  The array index of this.scales[].
   * @param {number} fromLower  The source bounds [ fromLower, fromUpper ]
   * @param {number} fromUpper  The source bounds [ fromLower, fromUpper ]
   * @param {number} toLower    The destination bounds [ toLower, toUpper ]
   * @param {number} toUpper    The destination bounds [ toLower, toUpper ]
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_by_fromLowerUpper_toLowerUpper( thisIndex, fromLower, fromUpper, toLower, toUpper ) {
    let aScale = ScaleArray.calc_scale_by_fromLowerUpper_toLowerUpper( fromLower, fromUpper, toLower, toUpper );
    this.scales[ thisIndex ] = aScale;
    return this;
  }

  /**
   * @param {number} thisIndex  The array index of this.scales[].
   * @param {number} N          Set ( this.scales[ thisIndex ] ) by ( 1 / N ).
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_byUndo_N( thisIndex, N ) {
    this.scales[ thisIndex ] = ( 1 / N );  // Reciprocal will undo the scale. (Note: Not work for zero.)
    return this;
  }

  /**
   * @param {number} thisIndex  The array index of this.scales[].
   * @param {number[]} Ns       Set ( this.scales[ thisIndex ] ) by ( 1 / Ns[ aIndex ] ).
   * @param {number} aIndex     The array index of Ns[].
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_byUndo_Ns( thisIndex, Ns, aIndex ) {
    this.scales[ thisIndex ] = ( 1 / Ns[ aIndex ] );  // Reciprocal will undo the scale. (Note: Not work for zero.)
    return this;
  }

  /**
   * @param {number} thisIndex        The array index of this.scales[].
   * @param {ScaleArray} aScaleArray  Set ( this.scales[ thisIndex ] ) by ( 1 / aScaleArray.scales[ aIndex ] ).
   * @param {number} aIndex           The array index of aScaleArray.scales[].
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_one_byUndo_ScaleArray( thisIndex, aScaleArray, aIndex ) {
    return this.set_one_byUndo_Ns( thisIndex, aScaleArray.scales, aIndex );
  }

  /**
   * @param {number} N  Set all ( this.scales[] ) by ( N ). Default are ( N = 1 ) (i.e. no scale).
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_all_byN( N = 1 ) {
    this.scales.fill( N );
    return this;
  }

  /**
   * @param {number[]} Ns  Set all ( this.scales[] ) by ( Ns[] ).
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_all_byNs( Ns ) {
    for ( let i = 0; i < this.scales.length; ++i ) {
      this.scales[ i ] = Ns[ i ];
    }
    return this;
  }

  /**
   * @param {ScaleArray} aScaleArray  Set all ( this.scales[] ) by ( aScaleArray.scales[] ).
   *
   * @return {ScaleArray} Return this (modified) object whose values are copied from aScaleArray.
   */
  set_all_byScaleArray( aScaleArray ) {
    return this.set_all_byNs( aScaleArray.scales );
  }

  /**
   * @param {number[]} Ns  Set all ( this.scales[] ) by ( 1 / Ns[] ).
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_all_byUndo_Ns( Ns ) {
    for ( let i = 0; i < this.scales.length; ++i ) {
      this.scales[ i ] = ( 1 / Ns[ i ] );  // Reciprocal will undo the scale. (Note: Not work for zero.)
    }
    return this;
  }

  /**
   * @param {ScaleArray} aScaleArray  Set all ( this.scales[] ) by ( 1 / aScaleArray.scales[] ).
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_all_byUndo_ScaleArray( aScaleArray ) {
    return this.set_all_byUndo_Ns( aScaleArray.scales );
  }

  /**
   * The this.length will be modified.
   *
   * @param {ScaleArray} inputScaleArray0  The ScaleArray of the 1st input.
   * @param {ScaleArray} inputScaleArray1  The BoundsArray of the 2nd input.
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_all_byScaleArray_concat_input0_input1( inputScaleArray0, inputScaleArray1 ) {

    let totalLength = ( inputScaleArray0?.length ?? 0 ) + ( inputScaleArray1?.length ?? 0 );
    this.length = totalLength;

    // Concat value scale array.
    let inChannel = 0;

    if ( inputScaleArray0 ) {
      for ( let inChannel0 = 0; inChannel0 < inputScaleArray0.length; ++inChannel0, ++inChannel ) {
        this.set_one_byScaleArray( inChannel, inputScaleArray0, inChannel0 );
      }
    }

    if ( inputScaleArray1 ) {
      for ( let inChannel1 = 0; inChannel1 < inputScaleArray1.length; ++inChannel1, ++inChannel ) {
        this.set_one_byScaleArray( inChannel, inputScaleArray1, inChannel1 );
      }
    }

    return this;
  }

  /**
   * Rearrange scales by interleaving as ( groupCount == 2 ). This element count must be even (i.e. divisible by 2).
   *
   * @param {Array} arrayTemp
   *   A temporary array for placing the original elements temporarily. Provide this array could reduce memory re-allocation
   * and improve performance.
   *
   * @return {ScaleArray} Return this (modified) object.
   */
  set_all_byInterleave_asGrouptTwo( arrayTemp ) {
    ArrayInterleaver.interleave_asGrouptTwo( this.scales, 0, this.scales.length, arrayTemp );
    return this;
  }

  /**
   * @param {ScaleArray} lowerHalfScaleArray   The ScaleArray of the 1st output. Its .length will be modified.
   * @param {ScaleArray} higherHalfScaleArray  The ScaleArray of the 2nd output. Its .length will be modified.
   *
   * @return {ScaleArray} Return this (unmodified) object.
   */
  split_to_lowerHalf_higherHalf( lowerHalfScaleArray, higherHalfScaleArray ) {

    // If not divided by 2, let lower half have one more.
    let length_lowerHalf = Math.ceil( this.length / 2 );
    let length_higherHalf = this.length - length_lowerHalf;

    lowerHalfScaleArray.length = length_lowerHalf;
    higherHalfScaleArray.length = length_higherHalf;

    // Split value bounds array.
    let inChannel = 0;

    for ( let outChannel = 0; outChannel < length_lowerHalf; ++outChannel, ++inChannel ) {
      lowerHalfScaleArray.set_one_byBoundsArray( outChannel, this, inChannel );
    }

    for ( let outChannel = 0; outChannel < length_higherHalf; ++outChannel, ++inChannel ) {
      higherHalfScaleArray.set_one_byBoundsArray( outChannel, this, inChannel );
    }

    return this;
  }

  /**
   *
   * @param {number} fromLower  The source bounds [ fromLower, fromUpper ]
   * @param {number} fromUpper  The source bounds [ fromLower, fromUpper ]
   * @param {number} toLower    The destination bounds [ toLower, toUpper ]
   * @param {number} toUpper    The destination bounds [ toLower, toUpper ]
   *
   * @return {number}
   *   Return a scale value which could let source bounds [ fromLower, fromUpper ] completely insides destination bounds [ toLower, toUpper ].
   * Return Number.NaN, if it is impossible to do that.
   */
  static calc_scale_by_fromLowerUpper_toLowerUpper( fromLower, fromUpper, toLower, toUpper ) {
    let srcLower = Math.min( fromLower, fromUpper ); // Confirm ( lower <= upper ).
    let srcUpper = Math.max( fromLower, fromUpper );
    let dstLower = Math.min( toLower, toUpper );
    let dstUpper = Math.max( toLower, toUpper );

    let scale;

    // 1. Try lower bound.
    //
    // Note:
    //  ( x / +Infinity ) == +0
    //  ( x / -Infinity ) == -0
    //  (  0 / x ) = 0
    //  ( -1 / 0 ) = -Infinity
    //  ( +1 / 0 ) = +Infinity
    //  (  0 / 0 ) = NaN
    //  ( +-Infinity / +-Infinity ) == NaN
    //
    scale = dstLower / srcLower;

    // 1.2 Verification.
    //   - If scale is zero or -Infinity or +Infinity or NaN, it is always failed.
    //   - If scaled source upper bound is out of destination range, it is also failed.
    let adjustedUpper = srcUpper * scale;
    if ( ( scale == 0 ) || ( !Number.isFinite( scale ) ) || ( adjustedUpper < dstLower ) || ( adjustedUpper > dstUpper ) ) {

      // 2. Try upperer bound, since it is failed to fit [ srcLower, srcUpper ] into [ dstLower, dstUpper ] by scale according to lower bound.
      scale = dstUpper / srcUpper;

      // 2.2 Verification. If scale is zero, it is always failed. Otherwise, check it by lower side.
      //   - If scale is zero or -Infinity or +Infinity or NaN, it is always failed.
      //   - If scaled source lower bound is out of destination range, it is also failed.
      let adjustedLower = srcLower * scale;
      if ( ( scale == 0 ) || ( !Number.isFinite( scale ) ) || ( adjustedLower < dstLower ) || ( adjustedLower > dstUpper ) ) {
        // 3. It is impossible to fit [ srcLower, srcUpper ] into [ dstLower, dstUpper ] only by scale because all cases are tried and failed.
        scale = Number.NaN;
      }
    }

    return scale;
  }

}

<|endoftext|>


<commit_before>import Flatpickr from 'flatpickr';
import settings from '../../globals/js/settings';
import mixin from '../../globals/js/misc/mixin';
import createComponent from '../../globals/js/mixins/create-component';
import initComponentBySearch from '../../globals/js/mixins/init-component-by-search';
import handles from '../../globals/js/mixins/handles';
import on from '../../globals/js/misc/on';

/* eslint no-underscore-dangle: [2, { "allow": ["_input", "_updateClassNames", "_updateInputFields"], "allowAfterThis": true }] */

// `this.options` create-component mix-in creates prototype chain
// so that `options` given in constructor argument wins over the one defined in static `options` property
// 'Flatpickr' wants flat structure of object instead

function flattenOptions(options) {
  const o = {};
  // eslint-disable-next-line guard-for-in, no-restricted-syntax
  for (const key in options) {
    o[key] = options[key];
  }
  return o;
}

// Weekdays shorthand for english locale
Flatpickr.l10ns.en.weekdays.shorthand.forEach((day, index) => {
  const currentDay = Flatpickr.l10ns.en.weekdays.shorthand;
  if (currentDay[index] === 'Thu' || currentDay[index] === 'Th') {
    currentDay[index] = 'Th';
  } else {
    currentDay[index] = currentDay[index].charAt(0);
  }
});

class DatePicker extends mixin(createComponent, initComponentBySearch, handles) {
  /**
   * DatePicker.
   * @extends CreateComponent
   * @extends InitComponentBySearch
   * @extends Handles
   * @param {HTMLElement} element The element working as an date picker.
   */
  constructor(element, options) {
    super(element, options);
    const type = this.element.getAttribute(this.options.attribType);
    this.calendar = this._initDatePicker(type);
    this.manage(
      on(this.element, 'keydown', e => {
        if (e.which === 40) {
          this.calendar.calendarContainer.focus();
        }
      })
    );
    this.manage(
      on(this.calendar.calendarContainer, 'keydown', e => {
        if (e.which === 9 && type === 'range') {
          this._updateClassNames(this.calendar);
          this.element.querySelector(this.options.selectorDatePickerInputFrom).focus();
        }
      })
    );
  }

  /**
   * Opens the date picker dropdown when this component gets focus.
   * Used only for range mode for now.
   * @private
   */
  _handleFocus = () => {
    if (this.calendar) {
      this.calendar.open();
    }
  };

  /**
   * Closes the date picker dropdown when this component loses focus.
   * Used only for range mode for now.
   * @private
   */
  _handleBlur = event => {
    if (this.calendar) {
      const focusTo = event.relatedTarget;
      if (!focusTo || (!this.element.contains(focusTo) && !this.calendar.calendarContainer.contains(focusTo))) {
        this.calendar.close();
      }
    }
  };

  _initDatePicker = type => {
    if (type === 'range') {
      // Given FlatPickr assumes one `<input>` even in range mode,
      // use a hidden `<input>` for such purpose, separate from our from/to `<input>`s
      const doc = this.element.ownerDocument;
      const rangeInput = doc.createElement('input');
      rangeInput.className = this.options.classVisuallyHidden;
      rangeInput.setAttribute('aria-hidden', 'true');
      this.element.appendChild(rangeInput);
      this._rangeInput = rangeInput;

      // An attempt to open the date picker dropdown when this component gets focus,
      // and close the date picker dropdown when this component loses focus
      const w = doc.defaultView;
      const hasFocusin = 'onfocusin' in w;
      const hasFocusout = 'onfocusout' in w;
      const focusinEventName = hasFocusin ? 'focusin' : 'focus';
      const focusoutEventName = hasFocusout ? 'focusout' : 'blur';
      this.manage(on(this.element, focusinEventName, this._handleFocus, !hasFocusin));
      this.manage(on(this.element, focusoutEventName, this._handleBlur, !hasFocusout));
      this.manage(
        on(this.element.querySelector(this.options.selectorDatePickerIcon), focusoutEventName, this._handleBlur, !hasFocusout)
      );
    }
    const self = this;
    const date = type === 'range' ? this._rangeInput : this.element.querySelector(this.options.selectorDatePickerInput);
    const { onClose, onChange, onMonthChange, onYearChange, onOpen, onValueUpdate } = this.options;
    const calendar = new Flatpickr(
      date,
      Object.assign(flattenOptions(this.options), {
        allowInput: true,
        mode: type,
        positionElement: type === 'range' && this.element.querySelector(this.options.selectorDatePickerInputFrom),
        onClose(selectedDates, ...remainder) {
          // An attempt to disable Flatpickr's focus tracking system,
          // which has adverse effect with our old set up with two `<input>`s or our latest setup with a hidden `<input>`
          if (self.shouldForceOpen) {
            self.calendar.calendarContainer.classList.add('open');
            self.calendar.isOpen = true;
          }
          if (!onClose || onClose.call(this, selectedDates, ...remainder) !== false) {
            self._updateClassNames(calendar);
            self._updateInputFields(selectedDates, type);
          }
        },
        onChange(...args) {
          if (!onChange || onChange.call(this, ...args) !== false) {
            self._updateClassNames(calendar);
            if (type === 'range') {
              if (calendar.selectedDates.length === 1 && calendar.isOpen) {
                self.element.querySelector(self.options.selectorDatePickerInputTo).classList.add(self.options.classFocused);
              } else {
                self.element.querySelector(self.options.selectorDatePickerInputTo).classList.remove(self.options.classFocused);
              }
            }
          }
        },
        onMonthChange(...args) {
          if (!onMonthChange || onMonthChange.call(this, ...args) !== false) {
            self._updateClassNames(calendar);
          }
        },
        onYearChange(...args) {
          if (!onYearChange || onYearChange.call(this, ...args) !== false) {
            self._updateClassNames(calendar);
          }
        },
        onOpen(...args) {
          // An attempt to disable Flatpickr's focus tracking system,
          // which has adverse effect with our old set up with two `<input>`s or our latest setup with a hidden `<input>`
          self.shouldForceOpen = true;
          setTimeout(() => {
            self.shouldForceOpen = false;
          }, 0);
          if (!onOpen || onOpen.call(this, ...args) !== false) {
            self._updateClassNames(calendar);
          }
        },
        onValueUpdate(...args) {
          if ((!onValueUpdate || onValueUpdate.call(this, ...args) !== false) && type === 'range') {
            self._updateInputFields(self.calendar.selectedDates, type);
          }
        },
        nextArrow: this._rightArrowHTML(),
        prevArrow: this._leftArrowHTML(),
      })
    );
    if (type === 'range') {
      this._addInputLogic(this.element.querySelector(this.options.selectorDatePickerInputFrom), 0);
      this._addInputLogic(this.element.querySelector(this.options.selectorDatePickerInputTo), 1);
    }
    this.manage(
      on(this.element.querySelector(this.options.selectorDatePickerIcon), 'click', () => {
        calendar.open();
      })
    );
    this._updateClassNames(calendar);
    if (type !== 'range') {
      this._addInputLogic(date);
    }
    return calendar;
  };

  _rightArrowHTML() {
    return `
      <svg width="8" height="12" viewBox="0 0 8 12" fill-rule="evenodd">
        <path d="M0 10.6L4.7 6 0 1.4 1.4 0l6.1 6-6.1 6z"></path>
      </svg>`;
  }

  _leftArrowHTML() {
    return `
      <svg width="8" height="12" viewBox="0 0 8 12" fill-rule="evenodd">
        <path d="M7.5 10.6L2.8 6l4.7-4.6L6.1 0 0 6l6.1 6z"></path>
      </svg>`;
  }

  _addInputLogic = (input, index) => {
    if (!isNaN(index) && (index < 0 || index > 1)) {
      throw new RangeError(`The index of <input> (${index}) is out of range.`);
    }
    const inputField = input;
    this.manage(
      on(inputField, 'change', evt => {
        if (evt.isTrusted || (evt.detail && evt.detail.isNotFromFlatpickr)) {
          const inputDate = this.calendar.parseDate(inputField.value);
          if (inputDate && !isNaN(inputDate.valueOf())) {
            if (isNaN(index)) {
              this.calendar.setDate(inputDate);
            } else {
              const selectedDates = this.calendar.selectedDates;
              selectedDates[index] = inputDate;
              this.calendar.setDate(selectedDates);
            }
          }
        }
        this._updateClassNames(this.calendar);
      })
    );
    // An attempt to temporarily set the `<input>` being edited as the one FlatPicker manages,
    // as FlatPicker attempts to take over `keydown` event handler on `document` to run on the date picker dropdown.
    this.manage(
      on(inputField, 'keydown', evt => {
        const origInput = this.calendar._input;
        this.calendar._input = evt.target;
        setTimeout(() => {
          this.calendar._input = origInput;
        });
      })
    );
  };

  _updateClassNames = calendar => {
    const calendarContainer = calendar.calendarContainer;
    calendarContainer.classList.add(this.options.classCalendarContainer);
    calendarContainer.querySelector('.flatpickr-month').classList.add(this.options.classMonth);
    calendarContainer.querySelector('.flatpickr-weekdays').classList.add(this.options.classWeekdays);
    calendarContainer.querySelector('.flatpickr-days').classList.add(this.options.classDays);
    [...calendarContainer.querySelectorAll('.flatpickr-weekday')].forEach(item => {
      const currentItem = item;
      currentItem.innerHTML = currentItem.innerHTML.replace(/\s+/g, '');
      currentItem.classList.add(this.options.classWeekday);
    });
    [...calendarContainer.querySelectorAll('.flatpickr-day')].forEach(item => {
      item.classList.add(this.options.classDay);
      if (item.classList.contains('today') && calendar.selectedDates.length > 0) {
        item.classList.add('no-border');
      } else if (item.classList.contains('today') && calendar.selectedDates.length === 0) {
        item.classList.remove('no-border');
      }
    });
  };

  _updateInputFields = (selectedDates, type) => {
    if (type === 'range') {
      if (selectedDates.length === 2) {
        this.element.querySelector(this.options.selectorDatePickerInputFrom).value = this._formatDate(selectedDates[0]);
        this.element.querySelector(this.options.selectorDatePickerInputTo).value = this._formatDate(selectedDates[1]);
      } else if (selectedDates.length === 1) {
        this.element.querySelector(this.options.selectorDatePickerInputFrom).value = this._formatDate(selectedDates[0]);
      }
    } else if (selectedDates.length === 1) {
      this.element.querySelector(this.options.selectorDatePickerInput).value = this._formatDate(selectedDates[0]);
    }
    this._updateClassNames(this.calendar);
  };

  _formatDate = date => this.calendar.formatDate(date, this.calendar.config.dateFormat);

  release() {
    if (this._rangeInput && this._rangeInput.parentNode) {
      this._rangeInput.parentNode.removeChild(this._rangeInput);
    }
    if (this.calendar) {
      try {
        this.calendar.destroy();
      } catch (err) {} // eslint-disable-line no-empty
      this.calendar = null;
    }
    return super.release();
  }

  /**
   * The component options.
   * If `options` is specified in the constructor,
   * {@linkcode DatePicker.create .create()}, or {@linkcode DatePicker.init .init()},
   * properties in this object are overriden for the instance being create and how {@linkcode DatePicker.init .init()} works.
   * @property {string} selectorInit The CSS selector to find date picker UIs.
   */
  static get options() {
    const { prefix } = settings;
    return {
      selectorInit: '[data-date-picker]',
      selectorDatePickerInput: '[data-date-picker-input]',
      selectorDatePickerInputFrom: '[data-date-picker-input-from]',
      selectorDatePickerInputTo: '[data-date-picker-input-to]',
      selectorDatePickerIcon: '[data-date-picker-icon]',
      classCalendarContainer: `${prefix}--date-picker__calendar`,
      classMonth: `${prefix}--date-picker__month`,
      classWeekdays: `${prefix}--date-picker__weekdays`,
      classDays: `${prefix}--date-picker__days`,
      classWeekday: `${prefix}--date-picker__weekday`,
      classDay: `${prefix}--date-picker__day`,
      classFocused: `${prefix}--focused`,
      classVisuallyHidden: `${prefix}--visually-hidden`,
      attribType: 'data-date-picker-type',
      dateFormat: 'm/d/Y',
    };
  }

  /**
   * The map associating DOM element and date picker UI instance.
   * @type {WeakMap}
   */
  static components = new WeakMap();
}

export default DatePicker;
<commit_msg>fix(date-picker): fix JavaScript error for mobile (#1468)<commit_after>import Flatpickr from 'flatpickr';
import settings from '../../globals/js/settings';
import mixin from '../../globals/js/misc/mixin';
import createComponent from '../../globals/js/mixins/create-component';
import initComponentBySearch from '../../globals/js/mixins/init-component-by-search';
import handles from '../../globals/js/mixins/handles';
import on from '../../globals/js/misc/on';

/* eslint no-underscore-dangle: [2, { "allow": ["_input", "_updateClassNames", "_updateInputFields"], "allowAfterThis": true }] */

// `this.options` create-component mix-in creates prototype chain
// so that `options` given in constructor argument wins over the one defined in static `options` property
// 'Flatpickr' wants flat structure of object instead

function flattenOptions(options) {
  const o = {};
  // eslint-disable-next-line guard-for-in, no-restricted-syntax
  for (const key in options) {
    o[key] = options[key];
  }
  return o;
}

// Weekdays shorthand for english locale
Flatpickr.l10ns.en.weekdays.shorthand.forEach((day, index) => {
  const currentDay = Flatpickr.l10ns.en.weekdays.shorthand;
  if (currentDay[index] === 'Thu' || currentDay[index] === 'Th') {
    currentDay[index] = 'Th';
  } else {
    currentDay[index] = currentDay[index].charAt(0);
  }
});

class DatePicker extends mixin(createComponent, initComponentBySearch, handles) {
  /**
   * DatePicker.
   * @extends CreateComponent
   * @extends InitComponentBySearch
   * @extends Handles
   * @param {HTMLElement} element The element working as an date picker.
   */
  constructor(element, options) {
    super(element, options);
    const type = this.element.getAttribute(this.options.attribType);
    this.calendar = this._initDatePicker(type);
    if (this.calendar.calendarContainer) {
      this.manage(
        on(this.element, 'keydown', e => {
          if (e.which === 40) {
            this.calendar.calendarContainer.focus();
          }
        })
      );
      this.manage(
        on(this.calendar.calendarContainer, 'keydown', e => {
          if (e.which === 9 && type === 'range') {
            this._updateClassNames(this.calendar);
            this.element.querySelector(this.options.selectorDatePickerInputFrom).focus();
          }
        })
      );
    }
  }

  /**
   * Opens the date picker dropdown when this component gets focus.
   * Used only for range mode for now.
   * @private
   */
  _handleFocus = () => {
    if (this.calendar) {
      this.calendar.open();
    }
  };

  /**
   * Closes the date picker dropdown when this component loses focus.
   * Used only for range mode for now.
   * @private
   */
  _handleBlur = event => {
    if (this.calendar) {
      const focusTo = event.relatedTarget;
      if (
        !focusTo ||
        (!this.element.contains(focusTo) &&
          (!this.calendar.calendarContainer || !this.calendar.calendarContainer.contains(focusTo)))
      ) {
        this.calendar.close();
      }
    }
  };

  _initDatePicker = type => {
    if (type === 'range') {
      // Given FlatPickr assumes one `<input>` even in range mode,
      // use a hidden `<input>` for such purpose, separate from our from/to `<input>`s
      const doc = this.element.ownerDocument;
      const rangeInput = doc.createElement('input');
      rangeInput.className = this.options.classVisuallyHidden;
      rangeInput.setAttribute('aria-hidden', 'true');
      this.element.appendChild(rangeInput);
      this._rangeInput = rangeInput;

      // An attempt to open the date picker dropdown when this component gets focus,
      // and close the date picker dropdown when this component loses focus
      const w = doc.defaultView;
      const hasFocusin = 'onfocusin' in w;
      const hasFocusout = 'onfocusout' in w;
      const focusinEventName = hasFocusin ? 'focusin' : 'focus';
      const focusoutEventName = hasFocusout ? 'focusout' : 'blur';
      this.manage(on(this.element, focusinEventName, this._handleFocus, !hasFocusin));
      this.manage(on(this.element, focusoutEventName, this._handleBlur, !hasFocusout));
      this.manage(
        on(this.element.querySelector(this.options.selectorDatePickerIcon), focusoutEventName, this._handleBlur, !hasFocusout)
      );
    }
    const self = this;
    const date = type === 'range' ? this._rangeInput : this.element.querySelector(this.options.selectorDatePickerInput);
    const { onClose, onChange, onMonthChange, onYearChange, onOpen, onValueUpdate } = this.options;
    const calendar = new Flatpickr(
      date,
      Object.assign(flattenOptions(this.options), {
        allowInput: true,
        mode: type,
        positionElement: type === 'range' && this.element.querySelector(this.options.selectorDatePickerInputFrom),
        onClose(selectedDates, ...remainder) {
          // An attempt to disable Flatpickr's focus tracking system,
          // which has adverse effect with our old set up with two `<input>`s or our latest setup with a hidden `<input>`
          if (self.shouldForceOpen) {
            if (self.calendar.calendarContainer) {
              self.calendar.calendarContainer.classList.add('open');
            }
            self.calendar.isOpen = true;
          }
          if (!onClose || onClose.call(this, selectedDates, ...remainder) !== false) {
            self._updateClassNames(calendar);
            self._updateInputFields(selectedDates, type);
          }
        },
        onChange(...args) {
          if (!onChange || onChange.call(this, ...args) !== false) {
            self._updateClassNames(calendar);
            if (type === 'range') {
              if (calendar.selectedDates.length === 1 && calendar.isOpen) {
                self.element.querySelector(self.options.selectorDatePickerInputTo).classList.add(self.options.classFocused);
              } else {
                self.element.querySelector(self.options.selectorDatePickerInputTo).classList.remove(self.options.classFocused);
              }
            }
          }
        },
        onMonthChange(...args) {
          if (!onMonthChange || onMonthChange.call(this, ...args) !== false) {
            self._updateClassNames(calendar);
          }
        },
        onYearChange(...args) {
          if (!onYearChange || onYearChange.call(this, ...args) !== false) {
            self._updateClassNames(calendar);
          }
        },
        onOpen(...args) {
          // An attempt to disable Flatpickr's focus tracking system,
          // which has adverse effect with our old set up with two `<input>`s or our latest setup with a hidden `<input>`
          self.shouldForceOpen = true;
          setTimeout(() => {
            self.shouldForceOpen = false;
          }, 0);
          if (!onOpen || onOpen.call(this, ...args) !== false) {
            self._updateClassNames(calendar);
          }
        },
        onValueUpdate(...args) {
          if ((!onValueUpdate || onValueUpdate.call(this, ...args) !== false) && type === 'range') {
            self._updateInputFields(self.calendar.selectedDates, type);
          }
        },
        nextArrow: this._rightArrowHTML(),
        prevArrow: this._leftArrowHTML(),
      })
    );
    if (type === 'range') {
      this._addInputLogic(this.element.querySelector(this.options.selectorDatePickerInputFrom), 0);
      this._addInputLogic(this.element.querySelector(this.options.selectorDatePickerInputTo), 1);
    }
    this.manage(
      on(this.element.querySelector(this.options.selectorDatePickerIcon), 'click', () => {
        calendar.open();
      })
    );
    this._updateClassNames(calendar);
    if (type !== 'range') {
      this._addInputLogic(date);
    }
    return calendar;
  };

  _rightArrowHTML() {
    return `
      <svg width="8" height="12" viewBox="0 0 8 12" fill-rule="evenodd">
        <path d="M0 10.6L4.7 6 0 1.4 1.4 0l6.1 6-6.1 6z"></path>
      </svg>`;
  }

  _leftArrowHTML() {
    return `
      <svg width="8" height="12" viewBox="0 0 8 12" fill-rule="evenodd">
        <path d="M7.5 10.6L2.8 6l4.7-4.6L6.1 0 0 6l6.1 6z"></path>
      </svg>`;
  }

  _addInputLogic = (input, index) => {
    if (!isNaN(index) && (index < 0 || index > 1)) {
      throw new RangeError(`The index of <input> (${index}) is out of range.`);
    }
    const inputField = input;
    this.manage(
      on(inputField, 'change', evt => {
        if (evt.isTrusted || (evt.detail && evt.detail.isNotFromFlatpickr)) {
          const inputDate = this.calendar.parseDate(inputField.value);
          if (inputDate && !isNaN(inputDate.valueOf())) {
            if (isNaN(index)) {
              this.calendar.setDate(inputDate);
            } else {
              const selectedDates = this.calendar.selectedDates;
              selectedDates[index] = inputDate;
              this.calendar.setDate(selectedDates);
            }
          }
        }
        this._updateClassNames(this.calendar);
      })
    );
    // An attempt to temporarily set the `<input>` being edited as the one FlatPicker manages,
    // as FlatPicker attempts to take over `keydown` event handler on `document` to run on the date picker dropdown.
    this.manage(
      on(inputField, 'keydown', evt => {
        const origInput = this.calendar._input;
        this.calendar._input = evt.target;
        setTimeout(() => {
          this.calendar._input = origInput;
        });
      })
    );
  };

  _updateClassNames = calendar => {
    const calendarContainer = calendar.calendarContainer;
    if (calendarContainer) {
      calendarContainer.classList.add(this.options.classCalendarContainer);
      calendarContainer.querySelector('.flatpickr-month').classList.add(this.options.classMonth);
      calendarContainer.querySelector('.flatpickr-weekdays').classList.add(this.options.classWeekdays);
      calendarContainer.querySelector('.flatpickr-days').classList.add(this.options.classDays);
      [...calendarContainer.querySelectorAll('.flatpickr-weekday')].forEach(item => {
        const currentItem = item;
        currentItem.innerHTML = currentItem.innerHTML.replace(/\s+/g, '');
        currentItem.classList.add(this.options.classWeekday);
      });
      [...calendarContainer.querySelectorAll('.flatpickr-day')].forEach(item => {
        item.classList.add(this.options.classDay);
        if (item.classList.contains('today') && calendar.selectedDates.length > 0) {
          item.classList.add('no-border');
        } else if (item.classList.contains('today') && calendar.selectedDates.length === 0) {
          item.classList.remove('no-border');
        }
      });
    }
  };

  _updateInputFields = (selectedDates, type) => {
    if (type === 'range') {
      if (selectedDates.length === 2) {
        this.element.querySelector(this.options.selectorDatePickerInputFrom).value = this._formatDate(selectedDates[0]);
        this.element.querySelector(this.options.selectorDatePickerInputTo).value = this._formatDate(selectedDates[1]);
      } else if (selectedDates.length === 1) {
        this.element.querySelector(this.options.selectorDatePickerInputFrom).value = this._formatDate(selectedDates[0]);
      }
    } else if (selectedDates.length === 1) {
      this.element.querySelector(this.options.selectorDatePickerInput).value = this._formatDate(selectedDates[0]);
    }
    this._updateClassNames(this.calendar);
  };

  _formatDate = date => this.calendar.formatDate(date, this.calendar.config.dateFormat);

  release() {
    if (this._rangeInput && this._rangeInput.parentNode) {
      this._rangeInput.parentNode.removeChild(this._rangeInput);
    }
    if (this.calendar) {
      try {
        this.calendar.destroy();
      } catch (err) {} // eslint-disable-line no-empty
      this.calendar = null;
    }
    return super.release();
  }

  /**
   * The component options.
   * If `options` is specified in the constructor,
   * {@linkcode DatePicker.create .create()}, or {@linkcode DatePicker.init .init()},
   * properties in this object are overriden for the instance being create and how {@linkcode DatePicker.init .init()} works.
   * @property {string} selectorInit The CSS selector to find date picker UIs.
   */
  static get options() {
    const { prefix } = settings;
    return {
      selectorInit: '[data-date-picker]',
      selectorDatePickerInput: '[data-date-picker-input]',
      selectorDatePickerInputFrom: '[data-date-picker-input-from]',
      selectorDatePickerInputTo: '[data-date-picker-input-to]',
      selectorDatePickerIcon: '[data-date-picker-icon]',
      classCalendarContainer: `${prefix}--date-picker__calendar`,
      classMonth: `${prefix}--date-picker__month`,
      classWeekdays: `${prefix}--date-picker__weekdays`,
      classDays: `${prefix}--date-picker__days`,
      classWeekday: `${prefix}--date-picker__weekday`,
      classDay: `${prefix}--date-picker__day`,
      classFocused: `${prefix}--focused`,
      classVisuallyHidden: `${prefix}--visually-hidden`,
      attribType: 'data-date-picker-type',
      dateFormat: 'm/d/Y',
    };
  }

  /**
   * The map associating DOM element and date picker UI instance.
   * @type {WeakMap}
   */
  static components = new WeakMap();
}

export default DatePicker;
<|endoftext|>


<commit_before>'use strict';

var path = require('path');

var hopsConfig = require('hops-config');

exports.default = {
  exclude: /\.(?:m?jsx?|html|json)$/,
  loader: require.resolve('file-loader'),
  options: {
    name: path.join(hopsConfig.assetPath, '[name]-[hash:16].[ext]'),
  },
};
<commit_msg>refactor(build-config): do not emit files during server build<commit_after>'use strict';

var path = require('path');

var hopsConfig = require('hops-config');

exports.default = {
  exclude: /\.(?:m?jsx?|html|json)$/,
  loader: require.resolve('file-loader'),
  options: {
    name: path.join(hopsConfig.assetPath, '[name]-[hash:16].[ext]'),
  },
};

exports.node = {
  exclude: /\.(?:m?jsx?|html|json)$/,
  loader: require.resolve('file-loader'),
  options: {
    name: path.join(hopsConfig.assetPath, '[name]-[hash:16].[ext]'),
    emitFile: false,
  },
};
<|endoftext|>


<commit_before>var assert = require('assert');
var should = require('should');
var fs = require('fs');
var mosto = require('../mosto');
var melted = require('melted-node');
var _ = require('underscore');
var seed = require('seed-random');
var mbc = require('mbc-common');
var Media = require('mbc-common/models/Media');
var Q = require('q');

_.mixin({
    shuffleSeed: function(list, s) {
        seed(s, true);
        var ret = _.shuffle(list);
        seed(undefined, true);
    },
    randint: function(x, y, s) {
        var from = 0;
        var to = 0;
        if( y === undefined )
            to = x;
        else {
            from = x;
            to = y;
        }
        var width = to - from;
        return (parseInt(seed(s)() * width) + from)
    },
    randelem: function(list) {
        return list[_.randint(list.length)];
    },
    draw: function(list, n) {
        var ret = [];
        for(var i = 0 ; i < n ; i++) {
            ret.push(_.randelem(list));
        }
        return ret;
    },
});

describe.only("Mosto functional test", function() {
    /*
     * arrancar sin playlists
     ** ver negro
     */
    this.timeout(5000);
    var self = this;
    // use a random seed
    self.start_seed = seed()();
    self.rand = seed(self.start_seed);

    self.create_playlist = function(medias) {
        var mediamodels = [];
        medias.forEach(function(media) {
            mediamodels.push( Media.Piece(media) );
        });
        return Media.List({
            collection: mediamodels,
        });
    };

    self.setup_playlists = function(start_time) {
        /*
         * sets up playlists to be consecutive starting from start_time
         * and annotates the medias to keep track of start and end times
         *
         * returns a promise
         */
        var timewalk = moment(start_time);
        self.occurrences = [];
        var defer = Q.defer();
        var done = _.after(self.playlists.length, defer.resolve);
        var occcol = self.db.collection('scheds');
        var listcol = self.db.collection('lists');
        for( var i = 0 ; i < self.playlists.length ; i++ ) {
            var playlist = self.playlists[i];
            var occurrence = {
                start: timewalk.unix(),
            };
            for( var j = 0 ; j < playlist.medias.length ; j++ ) {
                var media = playlist.medias[j];
                media.start_time = timewalk.valueOf();
                timewalk.add(playlist.medias[j].length);
                media.end_time = timewalk.valueOf();
            }
            occurrence.end = timewalk.unix();
            listcol.insert(playlist, function(err, obj) {
                obj = obj[0];
                // update self.playlists
                _.extend(playlist, obj);
                occurrence = Media.Ocurrence({
                    list: obj._id,
                    start: occurrence.start,
                    end: occurrence.end
                });
                occcol.insert(occurrence, function(err, obj) {
                    self.occurrences.push(obj[0]);
                    done();
                });
            });
        }
        return defer.promise;
    };
    self.clean_playlists = function(done) {
        // Cleans the playlists and occurrences from the database
        var ready = _.after(2, function(){ done(); });
        self.db.collection('scheds').drop(ready);
        self.db.collection('lists').drop(ready);
    }

    self.get_playlist = function(time) {
        time = time || moment();
        return _.find(self.playlists, function(pl) {
            return pl.start <= time.unix() && pl.end >= time.unix();
        });
    };

    self.get_media = function(time) {
        time = time || moment;
        var playlist = self.get_playlist(time);

        return _.find(playlist.medias, function(me) {
            return me.start_time <= time && me.end_time >= time;
        });
    };

    self.publisher = mbc.pubsub();
    self.listener = mbc.pubsub();
    self.db = mbc.db();

    before(function() {
        self.melted = melted();
    });
    after(function() {
        delete self.melted;
    });
    describe('start without playlists', function() {
        before(function(done) {
            self.mosto = new mosto();
            self.mosto.once('playing', function() {
                done()});
            self.mosto.init();
        });
        after(function(done) {
            self.mosto.finish(function() {
                delete self.mosto;
                done();
            });
        });
        it('should show black', function(done) {
            var promise = self.melted.sendPromisedCommand('USTA U0', '202 OK');
            promise.then(function(result) {
                console.log(result);
                result = result.split('\r\n')[1].split(' ');
                var file = result[2];
                file.should.include('black_id');
            }).then(done, done);
        });
    });
    /*
     * arrancar con playlists
     ** ver que haya el play correspondiente
     ** ver que este en el frame correcto
     */
    describe('start with playlists', function() {
        self.playlist_count = _.randint(5, 10, self.rand());
        before(function() {
            // I assume all non-hidden files are videos
            self.medias = _.reject(fs.readdirSync('./test/videos'), function(el){ return el[0] == '.' });
            // self.medias = parseXMLs(self.medias);
            self.medias = [];
            self.playlists = [];
            for(var i = 0 ; i < self.playlist_count ; i++) {
                var playlist_length = _.randint(1, 10, self.rand());
                self.playlists.push(self.create_playlist(
                    _.draw(self.medias, playlist_length)));
            }
        });
        describe('starting now', function() {
            before(function(done) {
                self.db.dropDatabase(function(err, success) {
                    // let playlists start somewhere between now and 30
                    //  seconds ago
                    var setup = self.setup_playlists(moment(
                        moment() + _.randint(0, -30000)));
                    setup.then(function() {
                        self.mosto = new mosto();
                        self.mosto.once('playing', function() {
                            // send pubsub messages with new playlists
                            done();
                        });
                        self.mosto.init();
                    });
                });
            });
            after(function(done) {
                self.mosto.finish(function() {
                    delete self.mosto;
                    self.clean_playlists(done);
                });
            });
            it('should start the right clip');
            it('should start on the right frame');
            /*
            ** borrar la playlist
            *** ver que no se rompa nada
            *** ver que se este pasando negro
            */
            describe('delete currently playing playlist', function() {
                it('should not break');
                it('should be playing blank clip');
            });
        });
        /*
         * arrancar un playlist empezado hace 5m
         */
        describe('starting 5m ago', function() {
            /*
            ** ver que el frame se mantenga sincronizado durante 10 segundos
            */
            it("should keep sync'ed for at least 10 secs");
            /*
            ** mover la playlist hacia atras 1m
            *** ver que el frame se sincronice correctamente
            */
            describe('move playlist back', function() {
                it("should stay sync'ed");
            });
            /*
            ** mover la playlist hacia adelante 2m
            *** ver que el frame se sincronice correctamente
            */
            describe('move playlist forward', function() {
                it("should stay sync'ed");
            });
        });
    });
    /*
     * pruebas de timespan
     */
    describe('timespan tests', function() {
        /*
        ** levantar mosto con una ventana de 1m de ancho
        ** que haya una playlist de 2m empezando now
        ** que haya otra playlist de 30s empezando t+2m
        ** que haya otra playlist de 30s empezando en t+2m30s
        ** al start chequear que solo este la 1er playlist
        ** al 1m05 chequear que la 2da playlist este cargada
        */
        describe("start mosto with a 2m timeSpan", function() {
            it('should have a 2m timeSpan');
            describe('playlists longer than timespan', function() {
                describe('start with these [playlists:[clips]]: [3m:[1m, 1m, 1m], 1m:[30s, 30s], 1m:[1m]]', function() {
                    it('should start with a 3m playlist starting now');
                    it('should start with a playlist 1m long starting in t+3m');
                    it('should start with a playlist 1m long starting in t+4m');
                    describe('at startup', function() {
                        it('should load only the first playlist at start');
                    });
                    describe('after 1m05s', function() {
                        it('should have loaded the 2nd playlist');
                    });
                });
            });
            describe('clips longer than timespan', function() {
                describe('start with these [playlists:[clips]]: [3m:[3m], 1m[30s, 30s], 1m:[1m]]', function() {
                    it('should preload at least one clip');
                });
            });
        });
    });
    /*
     * idem anterior con una playlist de un solo clip
     ** si tengo un clip (playlist?) mas largo que la ventana, deberia haber n+1 cargados
     */
    describe("modify melted test", function() {
        it("should rectify things");
    });
});
<commit_msg>test that mosto starts up the right clip<commit_after>var assert = require('assert');
var should = require('should');
var fs = require('fs');
var mosto = require('../mosto');
var melted = require('melted-node');
var _ = require('underscore');
var seed = require('seed-random');
var mbc = require('mbc-common');
var Media = require('mbc-common/models/Media');
var Q = require('q');

_.mixin({
    shuffleSeed: function(list, s) {
        seed(s, true);
        var ret = _.shuffle(list);
        seed(undefined, true);
    },
    randint: function(x, y, s) {
        var from = 0;
        var to = 0;
        if( y === undefined )
            to = x;
        else {
            from = x;
            to = y;
        }
        var width = to - from;
        return (parseInt(seed(s)() * width) + from)
    },
    randelem: function(list) {
        return list[_.randint(list.length)];
    },
    draw: function(list, n) {
        var ret = [];
        for(var i = 0 ; i < n ; i++) {
            ret.push(_.randelem(list));
        }
        return ret;
    },
});

describe.only("Mosto functional test", function() {
    /*
     * arrancar sin playlists
     ** ver negro
     */
    this.timeout(5000);
    var self = this;
    // use a random seed
    self.start_seed = seed()();
    self.rand = seed(self.start_seed);

    self.create_playlist = function(medias) {
        var mediamodels = [];
        medias.forEach(function(media) {
            mediamodels.push( Media.Piece(media) );
        });
        return Media.List({
            collection: mediamodels,
        });
    };

    self.setup_playlists = function(start_time) {
        /*
         * sets up playlists to be consecutive starting from start_time
         * and annotates the medias to keep track of start and end times
         *
         * returns a promise
         */
        var timewalk = moment(start_time);
        self.occurrences = [];
        var defer = Q.defer();
        var done = _.after(self.playlists.length, defer.resolve);
        var occcol = self.db.collection('scheds');
        var listcol = self.db.collection('lists');
        for( var i = 0 ; i < self.playlists.length ; i++ ) {
            var playlist = self.playlists[i];
            var occurrence = {
                start: timewalk.unix(),
            };
            for( var j = 0 ; j < playlist.medias.length ; j++ ) {
                var media = playlist.medias[j];
                media.start_time = timewalk.valueOf();
                timewalk.add(playlist.medias[j].length);
                media.end_time = timewalk.valueOf();
            }
            occurrence.end = timewalk.unix();
            listcol.insert(playlist, function(err, obj) {
                obj = obj[0];
                // update self.playlists
                _.extend(playlist, obj);
                occurrence = Media.Ocurrence({
                    list: obj._id,
                    start: occurrence.start,
                    end: occurrence.end
                });
                occcol.insert(occurrence, function(err, obj) {
                    self.occurrences.push(obj[0]);
                    done();
                });
            });
        }
        return defer.promise;
    };
    self.clean_playlists = function(done) {
        // Cleans the playlists and occurrences from the database
        var ready = _.after(2, function(){ done(); });
        self.db.collection('scheds').drop(ready);
        self.db.collection('lists').drop(ready);
    }

    self.get_playlist = function(time) {
        time = time || moment();
        return _.find(self.playlists, function(pl) {
            return pl.start <= time.unix() && pl.end >= time.unix();
        });
    };

    self.get_media = function(time) {
        time = time || moment;
        var playlist = self.get_playlist(time);

        return _.find(playlist.medias, function(me) {
            return me.start_time <= time && me.end_time >= time;
        });
    };

    self.publisher = mbc.pubsub();
    self.listener = mbc.pubsub();
    self.db = mbc.db();

    before(function() {
        self.melted = melted();
    });
    after(function() {
        delete self.melted;
    });
    describe('start without playlists', function() {
        before(function(done) {
            self.mosto = new mosto();
            self.mosto.once('playing', function() {
                done()});
            self.mosto.init();
        });
        after(function(done) {
            self.mosto.finish(function() {
                delete self.mosto;
                done();
            });
        });
        it('should show black', function(done) {
            var promise = self.melted.sendPromisedCommand('USTA U0', '202 OK');
            promise.then(function(result) {
                console.log(result);
                result = result.split('\r\n')[1].split(' ');
                var file = result[2];
                file.should.include('black_id');
            }).then(done, done);
        });
    });
    /*
     * arrancar con playlists
     ** ver que haya el play correspondiente
     ** ver que este en el frame correcto
     */
    describe('start with playlists', function() {
        self.playlist_count = _.randint(5, 10, self.rand());
        before(function() {
            // I assume all non-hidden files are videos
            self.medias = _.reject(fs.readdirSync('./test/videos'), function(el){ return el[0] == '.' });
            // self.medias = parseXMLs(self.medias);
            self.medias = [];
            self.playlists = [];
            for(var i = 0 ; i < self.playlist_count ; i++) {
                var playlist_length = _.randint(1, 10, self.rand());
                self.playlists.push(self.create_playlist(
                    _.draw(self.medias, playlist_length)));
            }
        });
        describe('starting now', function() {
            before(function(done) {
                self.db.dropDatabase(function(err, success) {
                    // let playlists start somewhere between now and 30
                    //  seconds ago
                    var setup = self.setup_playlists(moment(
                        moment() + _.randint(0, -30000)));
                    setup.then(function() {
                        self.mosto = new mosto();
                        self.mosto.once('playing', function() {
                            // send pubsub messages with new playlists
                            done();
                        });
                        self.mosto.init();
                    });
                });
            });
            after(function(done) {
                self.mosto.finish(function() {
                    delete self.mosto;
                    self.clean_playlists(done);
                });
            });
            it('should start the right clip', function(done) {
                var result = self.melted.sendPromisedCommand('USTA U0', '202 OK');
                var expected_playlist = self.get_playlist();
                var expected_media = self.get_media();
                result.then(function(val) {
                    var lines = val.split("\r\n");
                    lines[0].should.eql('202 OK');
                    var filename = lines[1].split(' ')[2];
                    filename.match(expected_playlist._id + '-' + expected_media._id).should.be.ok;
                }).then(done, done);
            });
            it('should start on the right frame');
            /*
            ** borrar la playlist
            *** ver que no se rompa nada
            *** ver que se este pasando negro
            */
            describe('delete currently playing playlist', function() {
                it('should not break');
                it('should be playing blank clip');
            });
        });
        /*
         * arrancar un playlist empezado hace 5m
         */
        describe('starting 5m ago', function() {
            /*
            ** ver que el frame se mantenga sincronizado durante 10 segundos
            */
            it("should keep sync'ed for at least 10 secs");
            /*
            ** mover la playlist hacia atras 1m
            *** ver que el frame se sincronice correctamente
            */
            describe('move playlist back', function() {
                it("should stay sync'ed");
            });
            /*
            ** mover la playlist hacia adelante 2m
            *** ver que el frame se sincronice correctamente
            */
            describe('move playlist forward', function() {
                it("should stay sync'ed");
            });
        });
    });
    /*
     * pruebas de timespan
     */
    describe('timespan tests', function() {
        /*
        ** levantar mosto con una ventana de 1m de ancho
        ** que haya una playlist de 2m empezando now
        ** que haya otra playlist de 30s empezando t+2m
        ** que haya otra playlist de 30s empezando en t+2m30s
        ** al start chequear que solo este la 1er playlist
        ** al 1m05 chequear que la 2da playlist este cargada
        */
        describe("start mosto with a 2m timeSpan", function() {
            it('should have a 2m timeSpan');
            describe('playlists longer than timespan', function() {
                describe('start with these [playlists:[clips]]: [3m:[1m, 1m, 1m], 1m:[30s, 30s], 1m:[1m]]', function() {
                    it('should start with a 3m playlist starting now');
                    it('should start with a playlist 1m long starting in t+3m');
                    it('should start with a playlist 1m long starting in t+4m');
                    describe('at startup', function() {
                        it('should load only the first playlist at start');
                    });
                    describe('after 1m05s', function() {
                        it('should have loaded the 2nd playlist');
                    });
                });
            });
            describe('clips longer than timespan', function() {
                describe('start with these [playlists:[clips]]: [3m:[3m], 1m[30s, 30s], 1m:[1m]]', function() {
                    it('should preload at least one clip');
                });
            });
        });
    });
    /*
     * idem anterior con una playlist de un solo clip
     ** si tengo un clip (playlist?) mas largo que la ventana, deberia haber n+1 cargados
     */
    describe("modify melted test", function() {
        it("should rectify things");
    });
});
<|endoftext|>


<commit_before>import "../types/patch";

import * as t from "@babel/types";
import traverse, { NodePath, Scope } from "@babel/traverse";
import { MARKO_TYPES, MARKO_ALIAS_TYPES } from "../types/definitions";

MARKO_TYPES.forEach(typeName => {
  const checkKey = `is${typeName}`;
  const assertKey = `assert${typeName}`;
  const checkFn = t[checkKey];
  const assertFn = t[assertKey];
  NodePath.prototype[checkKey] = function (opts) {
    return checkFn(this.node, opts);
  };
  NodePath.prototype[assertKey] = function (opts) {
    assertFn(this.node, opts);
  };
});

MARKO_ALIAS_TYPES.forEach(aliasName => {
  const checkKey = `is${aliasName}`;
  const originalProtoCheck = NodePath.prototype[checkKey];
  NodePath.prototype[checkKey] = function (opts) {
    return (
      t.is(aliasName, this.node, opts) ||
      originalProtoCheck.call(this, this.node, opts)
    );
  };
});

// Adds a one time patch to the scope collector visitors to include
// Marko bindings for params and tag vars.
const originalCrawl = Scope.prototype.crawl;
const patchedVisitors = new WeakSet();

Scope.prototype.crawl = function () {
  const path = this.path;
  const originalTraverse = path.traverse;
  path.traverse = function (visitor, state) {
    path.traverse = originalTraverse;

    if (!patchedVisitors.has(visitor)) {
      patchedVisitors.add(visitor);
      Object.assign(
        traverse.explode(visitor),
        traverse.explode({
          MarkoTagBody(body) {
            for (const param of body.get("params")) {
              body.scope.registerBinding("param", param);
            }
          },
          MarkoTag(tag) {
            const tagVar = tag.get("var");
            if (tagVar.node) {
              tag.scope.registerBinding("local", tagVar, tag);
              for (const name in tagVar.getBindingIdentifiers()) {
                let curScope = tag.scope;
                const binding = curScope.getBinding(name);

                while ((curScope = curScope.parent)) {
                  curScope.hoistableTagVars =
                    curScope.hoistableTagVars ||
                    (curScope.hoistableTagVars = {});
                  const existingBinding = curScope.hoistableTagVars[name];

                  if (existingBinding) {
                    if (existingBinding !== binding) {
                      curScope.hoistableTagVars[name] = true;
                    }
                  } else {
                    curScope.hoistableTagVars[name] = binding;
                  }
                }
              }
            }
          }
        })
      );
    }

    this.traverse(visitor, state);

    if (state.references.length) {
      const movedBindings = new Map();
      for (const ref of state.references) {
        const { name } = ref.node;
        let curScope = ref.scope;
        if (curScope.hasBinding(name)) continue;

        do {
          const hoistableBinding =
            curScope.hoistableTagVars && curScope.hoistableTagVars[name];
          if (hoistableBinding) {
            if (hoistableBinding === true) {
              throw ref.buildCodeFrameError(
                "Ambiguous reference, variable was defined in multiple places and was not shadowed."
              );
            }

            const movedBinding = movedBindings.get(hoistableBinding);
            if (
              !movedBinding ||
              getScopeDepth(movedBinding) < getScopeDepth(curScope)
            ) {
              movedBindings.set(hoistableBinding, curScope);
            }
          }
        } while ((curScope = curScope.parent));
      }

      for (const [binding, scope] of movedBindings) {
        binding.scope.moveBindingTo(binding.identifier.name, scope);
      }
    }
  };

  originalCrawl.call(this);
  path.traverse = originalTraverse;
};

function getScopeDepth(scope) {
  let depth = 0;
  let cur = scope;
  while ((cur = cur.parent)) depth++;
  return depth;
}
<commit_msg>fix: hoisted variable scopes clean up<commit_after>import "../types/patch";

import * as t from "@babel/types";
import traverse, { NodePath, Scope } from "@babel/traverse";
import { MARKO_TYPES, MARKO_ALIAS_TYPES } from "../types/definitions";

MARKO_TYPES.forEach(typeName => {
  const checkKey = `is${typeName}`;
  const assertKey = `assert${typeName}`;
  const checkFn = t[checkKey];
  const assertFn = t[assertKey];
  NodePath.prototype[checkKey] = function (opts) {
    return checkFn(this.node, opts);
  };
  NodePath.prototype[assertKey] = function (opts) {
    assertFn(this.node, opts);
  };
});

MARKO_ALIAS_TYPES.forEach(aliasName => {
  const checkKey = `is${aliasName}`;
  const originalProtoCheck = NodePath.prototype[checkKey];
  NodePath.prototype[checkKey] = function (opts) {
    return (
      t.is(aliasName, this.node, opts) ||
      originalProtoCheck.call(this, this.node, opts)
    );
  };
});

// Adds a one time patch to the scope collector visitors to include
// Marko bindings for params and tag vars.
const originalCrawl = Scope.prototype.crawl;
const patchedVisitors = new WeakSet();

Scope.prototype.crawl = function () {
  const path = this.path;
  const originalTraverse = path.traverse;
  path.traverse = function (visitor, state) {
    state.hoistableTagVarsByScope = new Map();
    path.traverse = originalTraverse;

    if (!patchedVisitors.has(visitor)) {
      patchedVisitors.add(visitor);
      Object.assign(
        traverse.explode(visitor),
        traverse.explode({
          MarkoTagBody(body) {
            for (const param of body.get("params")) {
              body.scope.registerBinding("param", param);
            }
          },
          MarkoTag(tag, state) {
            const tagVar = tag.get("var");
            if (tagVar.node) {
              tag.scope.registerBinding("local", tagVar, tag);
              for (const name in tagVar.getBindingIdentifiers()) {
                let curScope = tag.scope;
                const binding = curScope.getBinding(name);

                while ((curScope = curScope.parent)) {
                  const hoistableTagVars =
                    state.hoistableTagVarsByScope.get(curScope);

                  if (hoistableTagVars) {
                    hoistableTagVars[name] = hoistableTagVars[name]
                      ? true
                      : binding;
                  } else {
                    state.hoistableTagVarsByScope.set(curScope, {
                      [name]: binding
                    });
                  }
                }
              }
            }
          }
        })
      );
    }

    this.traverse(visitor, state);

    if (state.references.length) {
      const movedBindings = new Map();
      for (const ref of state.references) {
        const { name } = ref.node;
        let curScope = ref.scope;
        if (curScope.hasBinding(name)) continue;

        do {
          const hoistableBinding =
            state.hoistableTagVarsByScope.get(curScope)?.[name];

          if (hoistableBinding) {
            if (hoistableBinding === true) {
              throw ref.buildCodeFrameError(
                "Ambiguous reference, variable was defined in multiple places and was not shadowed."
              );
            }

            const movedBinding = movedBindings.get(hoistableBinding);
            if (
              !movedBinding ||
              getScopeDepth(movedBinding) < getScopeDepth(curScope)
            ) {
              movedBindings.set(hoistableBinding, curScope);
            }
          }
        } while ((curScope = curScope.parent));
      }

      for (const [binding, scope] of movedBindings) {
        binding.scope.moveBindingTo(binding.identifier.name, scope);
      }
    }
  };

  originalCrawl.call(this);
  path.traverse = originalTraverse;
};

function getScopeDepth(scope) {
  let depth = 0;
  let cur = scope;
  while ((cur = cur.parent)) depth++;
  return depth;
}
<|endoftext|>


<commit_before>/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

import FeatureParser from "./featureParsers.js";
import SegParser from "./segParser.js";
import VcfParser from "../variant/vcfParser.js";
import GCNVParser from "../gcnv/gcnvParser.js";
import loadBamIndex from "../bam/bamIndex.js";
import loadTribbleIndex from "./tribble.js"
import igvxhr from "../igvxhr.js";
import {bgzBlockSize, unbgzf} from '../bam/bgzf.js';
import {isFilePath} from '../util/fileUtils.js'
import {isString} from "../util/stringUtils.js";
import {decodeDataURI, parseUri} from "../util/uriUtils.js";
import {buildOptions} from "../util/igvUtils.js";

const MAX_GZIP_BLOCK_SIZE = (1 << 16);

/**
 * Reader for "bed like" files (tab delimited files with 1 feature per line: bed, gff, vcf, etc)
 *
 * @param config
 * @constructor
 */
const FeatureFileReader = function (config, genome) {

    var uriParts;

    this.config = config || {};
    this.genome = genome;
    this.indexURL = config.indexURL;
    this.indexed = config.indexed;

    if (isFilePath(this.config.url)) {
        this.filename = this.config.url.name;
    } else if (isString(this.config.url) && this.config.url.startsWith('data:')) {
        this.indexed = false;  // by definition
        this.dataURI = config.url;
    } else {
        uriParts = parseUri(this.config.url);
        this.filename = config.filename || uriParts.file;
    }

    this.format = this.config.format;

    this.parser = this.getParser(this.format, this.config.decode, this.config);
};

/**
 * Return a promise to load features for the genomic interval
 * @param chr
 * @param start
 * @param end
 */
FeatureFileReader.prototype.readFeatures = async function (chr, start, end) {

    const index = await this.getIndex()
    if (index) {
        return this.loadFeaturesWithIndex(chr, start, end);
    } else if (this.dataURI) {
        return this.loadFeaturesFromDataURI();
    } else {
        return this.loadFeaturesNoIndex()
    }

};

FeatureFileReader.prototype.readHeader = async function () {

    if (!this.header) {

        let header

        if (this.dataURI) {

            const features = await this.loadFeaturesFromDataURI(this.dataURI)
            header = this.header || {};
            header.features = features;

        } else {
            let index;
            if (this.config.indexURL || this.config.indexed) {
                index = await this.getIndex();

                if (!index) {
                    // Note - it should be impossible to get here
                    const iurl = this.config.indexURL || this.config.url;
                    throw new Error("Unable to load index: " + iurl);
                }

                // Load the file header (not HTTP header) for an indexed file.
                let maxSize = "vcf" === this.config.format ? 65000 : 1000
                if (index.tabix) {
                    const bsizeOptions = buildOptions(this.config, {
                        range: {
                            start: index.firstAlignmentBlock,
                            size: 26
                        }
                    });
                    const abuffer = await igvxhr.loadArrayBuffer(this.config.url, bsizeOptions)
                    const bsize = bgzBlockSize(abuffer)
                    maxSize = index.firstAlignmentBlock + bsize;
                }
                const options = buildOptions(this.config, {bgz: index.tabix, range: {start: 0, size: maxSize}});
                const data = await igvxhr.loadString(this.config.url, options)
                header = this.parser.parseHeader(data);

            } else {
                // If this is a non-indexed file we will load all features in advance
                const features = await this.loadFeaturesNoIndex()
                header = this.header || {};
                header.features = features;
            }

            if (header && this.parser) {
                this.parser.header = header;
            }

            this.header = header;
            return header;
        }
    }
    return this.header;

};

FeatureFileReader.prototype.getParser = function (format, decode, config) {

    switch (format) {
        case "vcf":
            return new VcfParser(config);
        case "seg" :
            return new SegParser();
        case "gcnv" :
            return new GCNVParser();
        default:
            return new FeatureParser(format, decode, this.config);
    }

};

FeatureFileReader.prototype.loadFeaturesNoIndex = async function () {

    const options = buildOptions(this.config);    // Add oauth token, if any
    const data = await igvxhr.loadString(this.config.url, options)

    this.header = this.parser.parseHeader(data);
    if (this.header instanceof String && this.header.startsWith("##gff-version 3")) {
        this.format = 'gff3';
    }
    return this.parser.parseFeatures(data);   // <= PARSING DONE HERE

};

FeatureFileReader.prototype.loadFeaturesWithIndex = async function (chr, start, end) {

    //console.log("Using index");
    const config = this.config
    const parser = this.parser
    const tabix = this.index.tabix
    const refId = tabix ? this.index.sequenceIndexMap[chr] : chr
    const allFeatures = [];
    const genome = this.genome;

    const blocks = this.index.blocksForRange(refId, start, end);

    if (!blocks || blocks.length === 0) {
        return [];
    } else {

        for (let block of blocks) {

            const startPos = block.minv.block
            const startOffset = block.minv.offset
            const endOffset = block.maxv.offset
            let endPos

            if (tabix) {
                let lastBlockSize = 0
                if (endOffset > 0) {
                    const bsizeOptions = buildOptions(config, {
                        range: {
                            start: block.maxv.block,
                            size: 26
                        }
                    });
                    const abuffer = await igvxhr.loadArrayBuffer(config.url, bsizeOptions)
                    lastBlockSize = bgzBlockSize(abuffer)
                }
                endPos = block.maxv.block + lastBlockSize;
            } else {
                endPos = block.maxv.block;
            }

            const options = buildOptions(config, {
                range: {
                    start: startPos,
                    size: endPos - startPos + 1
                }
            });

            if (tabix) {
                const data = await igvxhr.loadArrayBuffer(config.url, options);
                const inflated = unbgzf(data);
                parse(inflated);

            } else {
                const inflated = await igvxhr.loadString(config.url, options);
                parse(inflated);
            }

            function parse(inflated) {
                const slicedData = startOffset ? inflated.slice(startOffset) : inflated;
                const slicedFeatures = parser.parseFeatures(slicedData);

                // Filter features not in requested range.
                for (let f of slicedFeatures) {
                    if (genome.getChromosomeName(f.chr) !== chr) {
                        if (allFeatures.length === 0) {
                            continue;  //adjacent chr to the left
                        } else {
                            break; //adjacent chr to the right
                        }
                    }
                    if (f.start > end) break;
                    if (f.end >= start && f.start <= end) {
                        allFeatures.push(f);
                    }
                }
            }
        }
    }

    allFeatures.sort(function (a, b) {
        return a.start - b.start;
    });

    return allFeatures;
}

FeatureFileReader.prototype.getIndex = async function () {

    if (this.index !== undefined || this.indexed === false) {
        return this.index;
    }
    const indexOrUndefined = await this.loadIndex()
    if (indexOrUndefined) {
        this.index = indexOrUndefined;
        this.indexed = true;
    } else {
        this.indexed = false;
    }
    return this.index;
};

/**
 * Return a Promise for the async loaded index
 */
FeatureFileReader.prototype.loadIndex = async function () {

    let idxFile = this.config.indexURL;
    try {
        let index;
        if (this.filename.endsWith('.gz') || this.filename.endsWith('.bgz')) {
            if (!idxFile) {
                idxFile = this.config.url + '.tbi';
            }
            index = await loadBamIndex(idxFile, this.config, true, this.genome);

        } else {
            if (!idxFile) {
                idxFile = this.config.url + '.idx';
            }
            index = await loadTribbleIndex(idxFile, this.config, this.genome);
        }
        return index;
    } catch (e) {
        if (this.config.indexURL || this.config.indexed) {
            throw e;
        } else {
            this.indexed = false;
            console.error(e);
        }
    }
};

FeatureFileReader.prototype.loadFeaturesFromDataURI = async function () {

    const plain = decodeDataURI(this.dataURI)
    this.header = this.parser.parseHeader(plain);
    if (this.header instanceof String && this.header.startsWith("##gff-version 3")) {
        this.format = 'gff3';
    }
    const features = this.parser.parseFeatures(plain);
    return features;
};

export default FeatureFileReader;

<commit_msg>pad queried features with 1 feature before and after interval<commit_after>/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

import FeatureParser from "./featureParsers.js";
import SegParser from "./segParser.js";
import VcfParser from "../variant/vcfParser.js";
import GCNVParser from "../gcnv/gcnvParser.js";
import loadBamIndex from "../bam/bamIndex.js";
import loadTribbleIndex from "./tribble.js"
import igvxhr from "../igvxhr.js";
import {bgzBlockSize, unbgzf} from '../bam/bgzf.js';
import {isFilePath} from '../util/fileUtils.js'
import {isString} from "../util/stringUtils.js";
import {decodeDataURI, parseUri} from "../util/uriUtils.js";
import {buildOptions} from "../util/igvUtils.js";

const MAX_GZIP_BLOCK_SIZE = (1 << 16);

/**
 * Reader for "bed like" files (tab delimited files with 1 feature per line: bed, gff, vcf, etc)
 *
 * @param config
 * @constructor
 */
const FeatureFileReader = function (config, genome) {

    var uriParts;

    this.config = config || {};
    this.genome = genome;
    this.indexURL = config.indexURL;
    this.indexed = config.indexed;

    if (isFilePath(this.config.url)) {
        this.filename = this.config.url.name;
    } else if (isString(this.config.url) && this.config.url.startsWith('data:')) {
        this.indexed = false;  // by definition
        this.dataURI = config.url;
    } else {
        uriParts = parseUri(this.config.url);
        this.filename = config.filename || uriParts.file;
    }

    this.format = this.config.format;

    this.parser = this.getParser(this.format, this.config.decode, this.config);
};

/**
 * Return a promise to load features for the genomic interval
 * @param chr
 * @param start
 * @param end
 */
FeatureFileReader.prototype.readFeatures = async function (chr, start, end) {

    const index = await this.getIndex()
    if (index) {
        return this.loadFeaturesWithIndex(chr, start, end);
    } else if (this.dataURI) {
        return this.loadFeaturesFromDataURI();
    } else {
        return this.loadFeaturesNoIndex()
    }

};

FeatureFileReader.prototype.readHeader = async function () {

    if (!this.header) {

        let header

        if (this.dataURI) {

            const features = await this.loadFeaturesFromDataURI(this.dataURI)
            header = this.header || {};
            header.features = features;

        } else {
            let index;
            if (this.config.indexURL || this.config.indexed) {
                index = await this.getIndex();

                if (!index) {
                    // Note - it should be impossible to get here
                    const iurl = this.config.indexURL || this.config.url;
                    throw new Error("Unable to load index: " + iurl);
                }

                // Load the file header (not HTTP header) for an indexed file.
                let maxSize = "vcf" === this.config.format ? 65000 : 1000
                if (index.tabix) {
                    const bsizeOptions = buildOptions(this.config, {
                        range: {
                            start: index.firstAlignmentBlock,
                            size: 26
                        }
                    });
                    const abuffer = await igvxhr.loadArrayBuffer(this.config.url, bsizeOptions)
                    const bsize = bgzBlockSize(abuffer)
                    maxSize = index.firstAlignmentBlock + bsize;
                }
                const options = buildOptions(this.config, {bgz: index.tabix, range: {start: 0, size: maxSize}});
                const data = await igvxhr.loadString(this.config.url, options)
                header = this.parser.parseHeader(data);

            } else {
                // If this is a non-indexed file we will load all features in advance
                const features = await this.loadFeaturesNoIndex()
                header = this.header || {};
                header.features = features;
            }

            if (header && this.parser) {
                this.parser.header = header;
            }

            this.header = header;
            return header;
        }
    }
    return this.header;

};

FeatureFileReader.prototype.getParser = function (format, decode, config) {

    switch (format) {
        case "vcf":
            return new VcfParser(config);
        case "seg" :
            return new SegParser();
        case "gcnv" :
            return new GCNVParser();
        default:
            return new FeatureParser(format, decode, this.config);
    }

};

FeatureFileReader.prototype.loadFeaturesNoIndex = async function () {

    const options = buildOptions(this.config);    // Add oauth token, if any
    const data = await igvxhr.loadString(this.config.url, options)

    this.header = this.parser.parseHeader(data);
    if (this.header instanceof String && this.header.startsWith("##gff-version 3")) {
        this.format = 'gff3';
    }
    return this.parser.parseFeatures(data);   // <= PARSING DONE HERE

};

FeatureFileReader.prototype.loadFeaturesWithIndex = async function (chr, start, end) {

    //console.log("Using index");
    const config = this.config
    const parser = this.parser
    const tabix = this.index.tabix
    const refId = tabix ? this.index.sequenceIndexMap[chr] : chr
    const allFeatures = [];
    const genome = this.genome;

    const blocks = this.index.blocksForRange(refId, start, end);

    if (!blocks || blocks.length === 0) {
        return [];
    } else {

        for (let block of blocks) {

            const startPos = block.minv.block
            const startOffset = block.minv.offset
            const endOffset = block.maxv.offset
            let endPos

            if (tabix) {
                let lastBlockSize = 0
                if (endOffset > 0) {
                    const bsizeOptions = buildOptions(config, {
                        range: {
                            start: block.maxv.block,
                            size: 26
                        }
                    });
                    const abuffer = await igvxhr.loadArrayBuffer(config.url, bsizeOptions)
                    lastBlockSize = bgzBlockSize(abuffer)
                }
                endPos = block.maxv.block + lastBlockSize;
            } else {
                endPos = block.maxv.block;
            }

            const options = buildOptions(config, {
                range: {
                    start: startPos,
                    size: endPos - startPos + 1
                }
            });

            if (tabix) {
                const data = await igvxhr.loadArrayBuffer(config.url, options);
                const inflated = unbgzf(data);
                parse(inflated);

            } else {
                const inflated = await igvxhr.loadString(config.url, options);
                parse(inflated);
            }

            function parse(inflated) {
                const slicedData = startOffset ? inflated.slice(startOffset) : inflated;
                const slicedFeatures = parser.parseFeatures(slicedData);

                // Filter features not in requested range.
                let inInterval = false;
                for (let i=0; i< slicedFeatures.length; i++) {
                    const f = slicedFeatures[i];
                    if (genome.getChromosomeName(f.chr) !== chr) {
                        if (allFeatures.length === 0) {
                            continue;  //adjacent chr to the left
                        } else {
                            break; //adjacent chr to the right
                        }
                    }
                    if (f.start > end) {
                        allFeatures.push(f);  // First feature beyond interval
                        break;
                    }
                    if (f.end >= start && f.start <= end) {
                        if(!inInterval) {
                            inInterval = true;
                            if(i > 0) {
                                allFeatures.push(slicedFeatures[i - 1]);
                            } else {
                                // TODO -- get block before this one for first feature;
                            }
                        }
                        allFeatures.push(f);
                    }
                }
            }
        }
    }

    allFeatures.sort(function (a, b) {
        return a.start - b.start;
    });

    return allFeatures;
}

FeatureFileReader.prototype.getIndex = async function () {

    if (this.index !== undefined || this.indexed === false) {
        return this.index;
    }
    const indexOrUndefined = await this.loadIndex()
    if (indexOrUndefined) {
        this.index = indexOrUndefined;
        this.indexed = true;
    } else {
        this.indexed = false;
    }
    return this.index;
};

/**
 * Return a Promise for the async loaded index
 */
FeatureFileReader.prototype.loadIndex = async function () {

    let idxFile = this.config.indexURL;
    try {
        let index;
        if (this.filename.endsWith('.gz') || this.filename.endsWith('.bgz')) {
            if (!idxFile) {
                idxFile = this.config.url + '.tbi';
            }
            index = await loadBamIndex(idxFile, this.config, true, this.genome);

        } else {
            if (!idxFile) {
                idxFile = this.config.url + '.idx';
            }
            index = await loadTribbleIndex(idxFile, this.config, this.genome);
        }
        return index;
    } catch (e) {
        if (this.config.indexURL || this.config.indexed) {
            throw e;
        } else {
            this.indexed = false;
            console.error(e);
        }
    }
};

FeatureFileReader.prototype.loadFeaturesFromDataURI = async function () {

    const plain = decodeDataURI(this.dataURI)
    this.header = this.parser.parseHeader(plain);
    if (this.header instanceof String && this.header.startsWith("##gff-version 3")) {
        this.format = 'gff3';
    }
    const features = this.parser.parseFeatures(plain);
    return features;
};

export default FeatureFileReader;

<|endoftext|>