<commit_before>/*
 * dos2unix
 * https://github.com/JamesMGreene/node-dos2unix
 *
 * Copyright (c) 2013 James M. Greene
 * Licensed under the MIT license.
 */

'use strict';

// Built-in modules
var fs = require('fs');
var util = require('util');
var EventEmitter = require('events').EventEmitter;

// External modules
var async = require('async');
var extend = require('node.extend');
var Readable = require('stream').Readable || require('readable-stream').Readable;

// Internal modules
var encoder = require('./util/encoding');
var globber = require('./util/glob');
var validator = require('./util/validate');

// Utility
var slicer = Array.prototype.slice;

function arraysMatch(arr1, arr2) {
  if (arr1 && arr2 && arr1.length === arr2.length) {
    var matches = arr1.filter(function(e, i) {
      return e === arr2[i];
    });
    return matches.length === arr1.length;
  }
  return arr1 === arr2;
}

// A big frickin' mess... but aimed at performance by not reading the file more times than necessary
function processFile(filePath, callback, options) {
  /*jshint validthis:true */
  var processor = this;
  processor.emit('processing.start', {
    file: filePath
  });

  encoder.detectBom(filePath, function(err, bomName) {
    var status = 'good';

    if (err) {
      status = 'error';
      processor.emit('processing.error', {
        file: filePath,
        message: 'Skipping file with errors during encoding detection: ' + err
      });
      return callback(null, status);
    }
    else {
      var needsFixing = false;
      var lastByteSequenceWasCR = false;
      var bytesPerBom = encoder.getBytesPerBom(bomName);
      var bytesPerControlChar = encoder.getBytesPerControlChar(bomName);

      var transformBuffer = [];
      var tempBuffer = null;

      // Fast-forward past the BOM if present
      var onFirstReadable = function() {
        var bomBuffer = this.read(bytesPerBom);
        if (bomBuffer == null || bomBuffer.length !== bytesPerBom) {
          status = 'error';
          processor.emit('processing.error', {
            file: filePath,
            message: 'Skipping file with errors during read: Unable to read past the expected BOM'
          });
          return callback(null, status);
        }
      };
      var onReadable = function() {
        var data = this.read(),
            i = 0,
            len = data.length,
            cursor = i,
            buffer,
            isByteSequenceLF;
        for ( ; i < len; i += bytesPerControlChar) {
          buffer = slicer.call(data, i, i + bytesPerControlChar);

          // If we read less bytes than the minimum number necessary to register
          // as a complete character in this encoding, put them back on the front
          // of the read queue for the next read and then bail out.
          if (buffer.length < bytesPerControlChar) {
            if (!tempBuffer || !arraysMatch(buffer, tempBuffer)) {
              tempBuffer = buffer;
              // Save these rogue bytes to inspect on the next time around
              this.unshift(data.slice(i, i + buffer.length));
              // Break out of the loop
              break;
            }
            else {
              // This will prevent an infinite loop if the number of bytes in the file
              // is erroneous/irregular, e.g. a total of 7 bytes in a UTF-16 file.
              status = 'error';

              // Send a signal to cause early termination (i.e. trigger the 'end' event)
              this.push(null);
              // Break out of the loop
              break;
            }
          }
          tempBuffer = null;

          if (encoder.doesByteSequenceSuggestBinary(buffer, bomName)) {
            status = 'skip';
            processor.emit('processing.skip', {
              file: filePath,
              message: 'Skipping suspected binary file'
            });
            // Send a signal to cause early termination (i.e. trigger the 'end' event)
            this.push(null);
            // Break out of the loop
            break;
          }
          else {
            if (encoder.isByteSequenceCR(buffer, bomName)) {
              lastByteSequenceWasCR = true;
            }
            else {
              if (lastByteSequenceWasCR) {
                isByteSequenceLF = encoder.isByteSequenceLF(buffer, bomName);

                if (isByteSequenceLF || options.convertCrLineEndings) {
                  needsFixing = true;

                  // Push everything (except for CRs) into the outgoing buffer
                  transformBuffer.splice.apply(transformBuffer, [transformBuffer.length, 0].concat(slicer.call(data, cursor, i - bytesPerControlChar)));
                  if (!isByteSequenceLF) {
                    transformBuffer.splice.apply(transformBuffer, [transformBuffer.length, 0].concat(encoder.getLFByteSequence(bomName)));
                  }
                  cursor = i;
                }
              }

              // If this is the last byte of the buffer, push the remaining data into the outgoing buffer
              if (i + bytesPerControlChar === len && needsFixing) {
                // Push everything (except for CRs) into the outgoing buffer
                transformBuffer.splice.apply(transformBuffer, [transformBuffer.length, 0].concat(slicer.call(data, cursor)));
              }

              // Reset
              lastByteSequenceWasCR = false;
            }
          }
        }
      };
      var onError = function(err) {
        status = 'error';
        processor.emit('processing.error', {
          file: filePath,
          message: 'Skipping file with errors during read: ' + err
        });
        return callback(null, status);
      };
      var onEnd = function() {
        if (status === 'good') {
          if (needsFixing) {
            status = 'bad';
            this.on('close', function() {
              // Overwrite the existing file with the fixed data
              process.nextTick(function() {
                processor.emit('convert.start', {
                  file: filePath
                });

                //
                // TODO: Is there a more optimal way to do this?
                //       e.g. TransformStream, write to temp then move, etc.
                //
                fs.writeFile(filePath, new Buffer(transformBuffer), function(err) {
                  if (err) {
                    status = 'error';
                    processor.emit('convert.error', {
                      file: filePath,
                      message: 'Unexpected error during conversion process: ' + err
                    });
                  }
                  else {
                    status = 'fix';
                    processor.emit('convert.end', {
                      file: filePath
                    });
                  }

                  processor.emit('processing.end', {
                    file: filePath
                  });
                  return callback(null, status);
                });
              });
            });
          }
          else {
            status = 'skip';
            processor.emit('processing.skip', {
              file: filePath,
              message: 'Skipping file with all correct line endings'
            });
          }
        }

        if (status === 'skip') {
          processor.emit('processing.end', {
            file: filePath
          });
        }
        if (status !== 'bad') {
          return callback(null, status);
        }
      };

      var reader = new Readable();
      reader.wrap(fs.createReadStream(filePath));

      if (bytesPerBom > 0) {
        reader.once('readable', onFirstReadable);
      }
      reader
        .on('readable', onReadable)
        .on('error', onError)
        .on('end', onEnd);
    }
  });
}

function processAllFiles(fileList, options, callback) {
  /*jshint validthis:true */
  var processor = this;
  var processFileFn = processFile.bind(processor);

  // For reporting final results
  var defaultStats = {
    'error': 0,
    'skip': 0,
    'fix': 0
  };

  // Temporarily combining both Q and async... should reduce to just one or the other
  var taskList = fileList.map(function(filePath) {
    return function(cb) {
      processFileFn(filePath, cb, options);
    };
  });

  // If `options.maxConcurrency` is `0` or less, then default to processing the
  // whole list in parallel (a.k.a. "unlimited")
  var maxConcurrency = options.maxConcurrency > 0 ? options.maxConcurrency : taskList.length;

  // Parallelize, within reason
  async.parallelLimit(taskList, maxConcurrency, function(err, statuses) {
    // Roll up the counts of each status
    var results = (statuses || []).reduce(
      function(previousVal, currentVal) {
        if (!previousVal.hasOwnProperty(currentVal)) {
          throw new Error('Reduced object did not have the expected key "' + currentVal + '": ' + JSON.stringify(previousVal));
        }
        previousVal[currentVal] += 1;
        return previousVal;
      },
      defaultStats
    );
    callback(null, results);
  });
}

// Promise bindings
var validateGlobPatternsFn = validator.validateGlobPatterns.bind(validator);
var globFn = globber.glob.bind(globber);

function Dos2UnixConverter(defaultOptions) {
  // Enforce the constructor pattern
  if (!(this instanceof Dos2UnixConverter)) {
    return new Dos2UnixConverter(defaultOptions);
  }

  // Mixin the EventEmitter API
  EventEmitter.call(this);

  // Validate and save the default options (if any)
  var validOptions = validator.validateOptions(defaultOptions);
  this.defaultOptions = extend(true, {}, validOptions);
}
// Mark it as inherited, too
util.inherits(Dos2UnixConverter, EventEmitter);

// GO!
Dos2UnixConverter.prototype.process = function(globPatterns, options) {
  /*jshint validthis:true */
  var processor = this;
  var processAllFilesFn = processAllFiles.bind(processor);

  this.emit('start');

  var opts = extend(true, {}, this.defaultOptions, options);

  // Find the pertinent files from the glob, sorted and without duplicates
  var taskList = [
    function(callback) {
      validateGlobPatternsFn(globPatterns, callback);
    },
    function(validGlobPatterns, callback) {
      globFn(validGlobPatterns, opts.glob, callback);
    },
    function(fileList, callback) {
      processAllFilesFn(fileList, opts, callback);
    }
  ];
  async.waterfall(taskList, function(err, stats) {
    if (err) {
      processor.emit('error', err);
    }
    else {
      processor.emit('end', stats);
    }
  });
};

// Exports
module.exports = Dos2UnixConverter;<commit_msg>Fix exception for skipped files on reading .length of null<commit_after>/*
 * dos2unix
 * https://github.com/JamesMGreene/node-dos2unix
 *
 * Copyright (c) 2013 James M. Greene
 * Licensed under the MIT license.
 */

'use strict';

// Built-in modules
var fs = require('fs');
var util = require('util');
var EventEmitter = require('events').EventEmitter;

// External modules
var async = require('async');
var extend = require('node.extend');
var Readable = require('stream').Readable || require('readable-stream').Readable;

// Internal modules
var encoder = require('./util/encoding');
var globber = require('./util/glob');
var validator = require('./util/validate');

// Utility
var slicer = Array.prototype.slice;

function arraysMatch(arr1, arr2) {
  if (arr1 && arr2 && arr1.length === arr2.length) {
    var matches = arr1.filter(function(e, i) {
      return e === arr2[i];
    });
    return matches.length === arr1.length;
  }
  return arr1 === arr2;
}

// A big frickin' mess... but aimed at performance by not reading the file more times than necessary
function processFile(filePath, callback, options) {
  /*jshint validthis:true */
  var processor = this;
  processor.emit('processing.start', {
    file: filePath
  });

  encoder.detectBom(filePath, function(err, bomName) {
    var status = 'good';

    if (err) {
      status = 'error';
      processor.emit('processing.error', {
        file: filePath,
        message: 'Skipping file with errors during encoding detection: ' + err
      });
      return callback(null, status);
    }
    else {
      var needsFixing = false;
      var lastByteSequenceWasCR = false;
      var bytesPerBom = encoder.getBytesPerBom(bomName);
      var bytesPerControlChar = encoder.getBytesPerControlChar(bomName);

      var transformBuffer = [];
      var tempBuffer = null;

      // Fast-forward past the BOM if present
      var onFirstReadable = function() {
        var bomBuffer = this.read(bytesPerBom);
        if (bomBuffer == null || bomBuffer.length !== bytesPerBom) {
          status = 'error';
          processor.emit('processing.error', {
            file: filePath,
            message: 'Skipping file with errors during read: Unable to read past the expected BOM'
          });
          return callback(null, status);
        }
      };
      var onReadable = function() {
        var data = this.read(),
            i = 0,
            len = data ? data.length : 0,
            cursor = i,
            buffer,
            isByteSequenceLF;
        for ( ; i < len; i += bytesPerControlChar) {
          buffer = slicer.call(data, i, i + bytesPerControlChar);

          // If we read less bytes than the minimum number necessary to register
          // as a complete character in this encoding, put them back on the front
          // of the read queue for the next read and then bail out.
          if (buffer.length < bytesPerControlChar) {
            if (!tempBuffer || !arraysMatch(buffer, tempBuffer)) {
              tempBuffer = buffer;
              // Save these rogue bytes to inspect on the next time around
              this.unshift(data.slice(i, i + buffer.length));
              // Break out of the loop
              break;
            }
            else {
              // This will prevent an infinite loop if the number of bytes in the file
              // is erroneous/irregular, e.g. a total of 7 bytes in a UTF-16 file.
              status = 'error';

              // Send a signal to cause early termination (i.e. trigger the 'end' event)
              this.push(null);
              // Break out of the loop
              break;
            }
          }
          tempBuffer = null;

          if (encoder.doesByteSequenceSuggestBinary(buffer, bomName)) {
            status = 'skip';
            processor.emit('processing.skip', {
              file: filePath,
              message: 'Skipping suspected binary file'
            });
            // Send a signal to cause early termination (i.e. trigger the 'end' event)
            this.push(null);
            // Break out of the loop
            break;
          }
          else {
            if (encoder.isByteSequenceCR(buffer, bomName)) {
              lastByteSequenceWasCR = true;
            }
            else {
              if (lastByteSequenceWasCR) {
                isByteSequenceLF = encoder.isByteSequenceLF(buffer, bomName);

                if (isByteSequenceLF || options.convertCrLineEndings) {
                  needsFixing = true;

                  // Push everything (except for CRs) into the outgoing buffer
                  transformBuffer.splice.apply(transformBuffer, [transformBuffer.length, 0].concat(slicer.call(data, cursor, i - bytesPerControlChar)));
                  if (!isByteSequenceLF) {
                    transformBuffer.splice.apply(transformBuffer, [transformBuffer.length, 0].concat(encoder.getLFByteSequence(bomName)));
                  }
                  cursor = i;
                }
              }

              // If this is the last byte of the buffer, push the remaining data into the outgoing buffer
              if (i + bytesPerControlChar === len && needsFixing) {
                // Push everything (except for CRs) into the outgoing buffer
                transformBuffer.splice.apply(transformBuffer, [transformBuffer.length, 0].concat(slicer.call(data, cursor)));
              }

              // Reset
              lastByteSequenceWasCR = false;
            }
          }
        }
      };
      var onError = function(err) {
        status = 'error';
        processor.emit('processing.error', {
          file: filePath,
          message: 'Skipping file with errors during read: ' + err
        });
        return callback(null, status);
      };
      var onEnd = function() {
        if (status === 'good') {
          if (needsFixing) {
            status = 'bad';
            this.on('close', function() {
              // Overwrite the existing file with the fixed data
              process.nextTick(function() {
                processor.emit('convert.start', {
                  file: filePath
                });

                //
                // TODO: Is there a more optimal way to do this?
                //       e.g. TransformStream, write to temp then move, etc.
                //
                fs.writeFile(filePath, new Buffer(transformBuffer), function(err) {
                  if (err) {
                    status = 'error';
                    processor.emit('convert.error', {
                      file: filePath,
                      message: 'Unexpected error during conversion process: ' + err
                    });
                  }
                  else {
                    status = 'fix';
                    processor.emit('convert.end', {
                      file: filePath
                    });
                  }

                  processor.emit('processing.end', {
                    file: filePath
                  });
                  return callback(null, status);
                });
              });
            });
          }
          else {
            status = 'skip';
            processor.emit('processing.skip', {
              file: filePath,
              message: 'Skipping file with all correct line endings'
            });
          }
        }

        if (status === 'skip') {
          processor.emit('processing.end', {
            file: filePath
          });
        }
        if (status !== 'bad') {
          return callback(null, status);
        }
      };

      var reader = new Readable();
      reader.wrap(fs.createReadStream(filePath));

      if (bytesPerBom > 0) {
        reader.once('readable', onFirstReadable);
      }
      reader
        .on('readable', onReadable)
        .on('error', onError)
        .on('end', onEnd);
    }
  });
}

function processAllFiles(fileList, options, callback) {
  /*jshint validthis:true */
  var processor = this;
  var processFileFn = processFile.bind(processor);

  // For reporting final results
  var defaultStats = {
    'error': 0,
    'skip': 0,
    'fix': 0
  };

  // Temporarily combining both Q and async... should reduce to just one or the other
  var taskList = fileList.map(function(filePath) {
    return function(cb) {
      processFileFn(filePath, cb, options);
    };
  });

  // If `options.maxConcurrency` is `0` or less, then default to processing the
  // whole list in parallel (a.k.a. "unlimited")
  var maxConcurrency = options.maxConcurrency > 0 ? options.maxConcurrency : taskList.length;

  // Parallelize, within reason
  async.parallelLimit(taskList, maxConcurrency, function(err, statuses) {
    // Roll up the counts of each status
    var results = (statuses || []).reduce(
      function(previousVal, currentVal) {
        if (!previousVal.hasOwnProperty(currentVal)) {
          throw new Error('Reduced object did not have the expected key "' + currentVal + '": ' + JSON.stringify(previousVal));
        }
        previousVal[currentVal] += 1;
        return previousVal;
      },
      defaultStats
    );
    callback(null, results);
  });
}

// Promise bindings
var validateGlobPatternsFn = validator.validateGlobPatterns.bind(validator);
var globFn = globber.glob.bind(globber);

function Dos2UnixConverter(defaultOptions) {
  // Enforce the constructor pattern
  if (!(this instanceof Dos2UnixConverter)) {
    return new Dos2UnixConverter(defaultOptions);
  }

  // Mixin the EventEmitter API
  EventEmitter.call(this);

  // Validate and save the default options (if any)
  var validOptions = validator.validateOptions(defaultOptions);
  this.defaultOptions = extend(true, {}, validOptions);
}
// Mark it as inherited, too
util.inherits(Dos2UnixConverter, EventEmitter);

// GO!
Dos2UnixConverter.prototype.process = function(globPatterns, options) {
  /*jshint validthis:true */
  var processor = this;
  var processAllFilesFn = processAllFiles.bind(processor);

  this.emit('start');

  var opts = extend(true, {}, this.defaultOptions, options);

  // Find the pertinent files from the glob, sorted and without duplicates
  var taskList = [
    function(callback) {
      validateGlobPatternsFn(globPatterns, callback);
    },
    function(validGlobPatterns, callback) {
      globFn(validGlobPatterns, opts.glob, callback);
    },
    function(fileList, callback) {
      processAllFilesFn(fileList, opts, callback);
    }
  ];
  async.waterfall(taskList, function(err, stats) {
    if (err) {
      processor.emit('error', err);
    }
    else {
      processor.emit('end', stats);
    }
  });
};

// Exports
module.exports = Dos2UnixConverter;<|endoftext|>


<commit_before>
;(function(){

  'use strict';

  /**
   * Aliases
   */

  var indexOf = Array.prototype.indexOf;
  var getStyle = window.getComputedStyle;

  /**
   * CSS Classes
   */

  var overflowingChildClass = 'ellipsis-overflowing-child';
  var containerClass = 'ellipsis-set';

  /**
   * Error Messages
   */

  var errors = [
    'The ellipsis container must have line-height set on it'
  ];

  /**
   * Vendor Info
   */

  var vendor = getVendorData();

  /**
   * Initialize a new Ellipsis
   * instance with the given element.
   *
   * Options:
   *   - `container` A parent container element
   *   - `reRender` Forces a redraw after ellipsis applied
   *
   * @constructor
   * @param {Element} el
   * @param {Object} options
   * @api public
   */

  function Ellipsis(el, options) {
    if (!el) return;
    this.el = el;
    this.container = options && options.container;
    this.reRender = options && options.reRender;
  }

  /**
   * Measures the element and
   * finds the overflowing child.
   *
   * @return {Ellipsis}
   * @api public
   */

  Ellipsis.prototype.calc = function() {
    if (!this.el) return this;
    var style = getStyle(this.el);
    var size = getSize(this.el);

    this.columnHeight = size[1];
    this.columnCount = getColumnCount(style);
    this.columnGap = getColumnGap(style);
    this.columnWidth = size[0] / this.columnCount;
    this.lineHeight = getLineHeight(style);
    this.deltaHeight = size[1] % this.lineHeight;
    this.linesPerColumn = Math.floor(this.columnHeight / this.lineHeight);
    this.totalLines = this.linesPerColumn * this.columnCount;

    this.child = this.getOverflowingChild();

    return this;
  };

  /**
   * Clamps the overflowing child using
   * the information acquired from #calc().
   *
   * @return {Ellipsis}
   * @api public
   */

  Ellipsis.prototype.set = function() {
    if (!this.el || !this.child) return this;

    // COMPLEX:
    // We set the height on the container
    // explicitly to work around problem
    // with columned containers not fitting
    // all lines when the height is exactly
    // divisible by the line height
    if (this.columnCount > 1) {
      this.el.style.height = this.columnHeight + 'px';
    }

    this.clampChild();
    siblingsAfter(this.child.el, { display: 'none' });
    this.markContainer();

    return this;
  };

  /**
   * Unclamps the overflowing child.
   *
   * @return {Ellipsis}
   * @api public
   */

  Ellipsis.prototype.unset = function() {
    if (!this.el || !this.child) return this;

    this.el.style.height = '';
    this.unclampChild(this.child);
    siblingsAfter(this.child.el, { display: '' });
    this.unmarkContainer();
    this.child = null;

    return this;
  };

  /**
   * Clears any references
   *
   * @return {Ellipsis}
   * @api public
   */

  Ellipsis.prototype.destroy = function() {
    this.el = this.child = null;
    return this;
  };

  /**
   * Returns the overflowing child with some
   * extra data required for clamping.
   *
   * @param  {Ellipsis} instance
   * @return {Object}
   * @api private
   */

  Ellipsis.prototype.getOverflowingChild = function() {
    var self = this;
    var child = {};
    var lineCounter = 0;

    // Loop over each child element
    each(this.el.children, function(el) {
      var lineCount, overflow, underflow;
      var startColumnIndex = Math.floor(lineCounter / self.linesPerColumn);

      // Get the line count of the
      // child and increment the counter
      lineCounter += lineCount = self.getLineCount(el);

      // If this is the overflowing child
      if (lineCounter >= self.totalLines) {
        overflow = lineCounter - self.totalLines;
        underflow = lineCount - overflow;

        child.el = el;
        child.clampedLines = underflow;
        child.clampedHeight = child.clampedLines * self.lineHeight;
        child.visibleColumnSpan = self.columnCount - startColumnIndex;
        child.applyTopMargin = self.shouldApplyTopMargin(child);

        return child;
      }
    });

    return child;
  };

  /**
   * Returns the number
   * of lines an element has.
   *
   * If the element is larger than
   * the column width we make the
   * assumption that this is FireFox
   * and the element is broken across
   * a column boundary. In this case
   * we have to get the height using
   * `getClientRects()`.
   *
   * @param  {Element} el
   * @return {Number}
   * @api private
   */

  Ellipsis.prototype.getLineCount = function(el) {
    return (el.offsetWidth > this.columnWidth)
      ? getLinesFromRects(el, this.lineHeight)
      : lineCount(el.clientHeight, this.lineHeight);
  };

  /**
   * If a container has been
   * declared we mark it with
   * a class for styling purposes.
   *
   * @api private
   */

  Ellipsis.prototype.markContainer = function() {
    if (!this.container) return;
    this.container.classList.add(containerClass);
    if (this.reRender) reRender(this.container);
  };

  /**
   * Removes the class
   * from the container.
   *
   * @api private
   */

  Ellipsis.prototype.unmarkContainer = function() {
    if (!this.container) return;
    this.container.classList.remove(containerClass);
    if (this.reRender) reRender(this.container);
  };

  /**
   * Determines whether top margin should be
   * applied to the overflowing child.
   *
   * This is to counteract an annoying
   * column-count/-webkit-box bug, whereby the
   * flexbox element falls into the delta are under
   * the previous sibling. Top margin keeps it
   * in the correct column.
   *
   * @param  {Element} el
   * @param  {Ellipsis} instance
   * @return {Boolean}
   * @api private
   */

  Ellipsis.prototype.shouldApplyTopMargin = function(child) {
    var el = child.el;

    // Dont't if it's not webkit
    if (!vendor.webkit) return;

    // Don't if it's a single column layout
    if (this.columnCount === 1) return;

    // Don't if the delta height is minimal
    if (this.deltaHeight <= 3) return;

    // Don't if it's the first child
    if (!el.previousElementSibling) return;

    // FINAL TEST: If the element is at the top or bottom of its
    // parent container then we require top margin.
    return (el.offsetTop === 0 || el.offsetTop === this.columnHeight);
  };

  /**
   * Clamps the child element to the set
   * height and lines.
   *
   * @param  {Object} child
   * @api private
   */

  Ellipsis.prototype.clampChild = function() {
    var child = this.child;
    if (!child || !child.el) return;

    // Clamp the height
    child.el.style.height = child.clampedHeight + 'px';

    // Use webkit line clamp
    // for webkit browsers.
    if (vendor.webkit) {
      child.el.style.webkitLineClamp = child.clampedLines;
      child.el.style.display = '-webkit-box';
      child.el.style.webkitBoxOrient = 'vertical';
    }

    if (this.shouldHideOverflow()) child.el.style.overflow = 'hidden';

    // Apply a top margin to fix webkit
    // column-count mixed with flexbox bug,
    // if we have decided it is neccessary.
    if (child.applyTopMargin) child.el.style.marginTop = '2em';

    // Add the overflowing child class as
    // a style hook
    child.el.classList.add(overflowingChildClass);

    // Non webkit browsers get a helper
    // elment that is styled as an alternative
    // to the webkit-line-clamp ellipsis.
    // Must be position relative so that we can
    // position the helper element.
    if (!vendor.webkit) {
      child.el.style.position = 'relative';
      child.helper = child.el.appendChild(this.helperElement());
    }
  };

  /**
   * Removes all clamping styles from
   * the overflowing child.
   *
   * @param  {Object} child
   * @api private
   */

  Ellipsis.prototype.unclampChild = function(child) {
    if (!child || !child.el) return;
    child.el.style.display = '';
    child.el.style.height = '';
    child.el.style.webkitLineClamp = '';
    child.el.style.webkitBoxOrient = '';
    child.el.style.marginTop = '';
    child.el.style.overflow = '';
    child.el.classList.remove(overflowingChildClass);

    if (child.helper) {
      child.helper.parentNode.removeChild(child.helper);
    }
  };

  /**
   * Creates the helper element
   * for non-webkit browsers.
   *
   * @return {Element}
   * @api private
   */

  Ellipsis.prototype.helperElement = function() {
    var el = document.createElement('span');
    var columns = this.child.visibleColumnSpan - 1;
    var rightOffset, marginRight;

    el.className = 'ellipsis-helper';
    el.style.display = 'block';
    el.style.height = this.lineHeight + 'px';
    el.style.width = '5em';
    el.style.position = 'absolute';
    el.style.bottom = 0;
    el.style.right = 0;

    // HACK: This is a work around to deal with
    // the wierdness of positioning elements
    // inside an element that is broken across
    // more than one column.
    if (vendor.js === 'Moz' && columns) {
      rightOffset = -(columns * 100);
      marginRight = -(columns * this.columnGap);
      el.style.right = rightOffset + '%';
      el.style.marginRight = marginRight + 'px';
      el.style.marginBottom = this.deltaHeight + 'px';
    }

    return el;
  };

  /**
   * Determines whether overflow
   * should be hidden on clamped
   * child.
   *
   * NOTE:
   * Overflow hidden is only required
   * for single column containers as
   * multi-column containers overflow
   * to the right, so are not visible.
   * `overflow: hidden;` also messes
   * with column layout in Firefox.
   *
   * @return {Boolean}
   * @api private
   */
  Ellipsis.prototype.shouldHideOverflow = function() {
    var columns = this.columnCount > 1;

    // Hide all single column overflow
    if (!columns) return true;

    // Only hide multi-column overflow
    // if this isn't mozilla.
    return columns && !vendor.moz;
  };

  /**
   * Re-render with no setTimeout, boom!
   *
   * NOTE:
   * We have to assign the return value
   * to something global so that Closure
   * Compiler doesn't strip it out.
   *
   * @param  {Element} el
   * @return void
   */
  function reRender(el) {
    el.style.display = 'none';
    Ellipsis.r = el.offsetTop;
    el.style.display = '';
  }

  /**
   * Sets the display property on
   * all siblingsafter the given element.
   *
   * Options:
   *   - `display` the css display type to use
   *
   * @param  {Node} el
   * @param  {Options} options
   * @return void
   */

  function siblingsAfter(el, options) {
    if (!el) return;
    var display = options && options.display;
    var siblings = el.parentNode.children;
    var index = indexOf.call(siblings, el);

    for (var i = index + 1, l = siblings.length; i < l; i++) {
      siblings[i].style.display = display;
    }
  }

  /**
   * Returns total line
   * count from a rect list.
   *
   * @param  {Element} el
   * @param  {Number} lineHeight
   * @return {Number}
   */

  function getLinesFromRects(el, lineHeight) {
    var rects = el.getClientRects();
    var lines = 0;

    each(rects, function(rect) {
      lines += lineCount(rect.height, lineHeight);
    });

    return lines;
  }

  /**
   * Calculates a line count
   * from the passed height.
   *
   * @param  {Number} height
   * @param  {Number} lineHeight
   * @return {Number}
   */

  function lineCount(height, lineHeight) {
    return Math.floor(height / lineHeight);
  }

  /**
   * Returns infomation about
   * the current vendor.
   *
   * @return {Object}
   */

  function getVendorData() {
    var el = document.createElement( "div" );
    var js = ["Webkit", "Moz", "O", "ms"];
    var css = ["-webkit-", "-moz-", "-o-", "-ms-"];
    for (var i = 0; i < js.length; i++ ) {
      if (js[i] + "ColumnCount" in el.style) {
        return {
          css: css[i],
          js: js[i],
          webkit: js[i] === 'Webkit',
          moz: js[i] === 'Moz'
        };
      }
    }
  }

  /**
   * Gets the column count of an
   * element using the vendor prefix.
   *
   * @param  {CSSStyleDeclaration} style  [description]
   * @return {Number}
   */

  function getColumnCount(style) {
    return parseInt(style[vendor.js + 'ColumnCount'], 10) || 1;
  }

  /**
   * Returns the gap between columns
   *
   * @param  {CSSStyleDeclaration} style
   * @return {Number}
   */

  function getColumnGap(style) {
    return parseInt(style[vendor.js + 'ColumnGap'], 10);
  }

  /**
   * Gets the line height
   * from the style declaration.
   *
   * @param  {CSSStyleDeclaration} style
   * @return {Number|null}
   */

  function getLineHeight(style) {
    var lineHeight = parseInt(style.lineHeight, 10);
    if (!lineHeight) throw new Error(errors[0]);

    return lineHeight;
  }

  /**
   * Returns the width and
   * height of the given element.
   *
   * @param  {Element} el
   * @return {Array}
   */

  function getSize(el) {
    return [el.offsetWidth, el.offsetHeight];
  }

  /**
   * Little iterator
   *
   * @param  {Array}   list
   * @param  {Function} fn
   */

  function each(list, fn) {
    for (var i = 0, l = list.length; i < l; i++) if (fn(list[i])) break;
  }

  /**
   * Expose `Ellipsis`
   */

  if (typeof exports === 'object') {
    module.exports = function(el, options) {
      return new Ellipsis(el, options);
    };

    module.exports.Ellipsis = Ellipsis;
  } else if (typeof define === 'function' && define.amd) {
    define(Ellipsis);
  } else {
    window.Ellipsis = Ellipsis;
  }

})();<commit_msg>Fix bug webkit delta column count bug before measuring lines<commit_after>
;(function(){

  'use strict';

  /**
   * Aliases
   */

  var indexOf = Array.prototype.indexOf;
  var getStyle = window.getComputedStyle;

  /**
   * CSS Classes
   */

  var overflowingChildClass = 'ellipsis-overflowing-child';
  var containerClass = 'ellipsis-set';

  /**
   * Error Messages
   */

  var errors = [
    'The ellipsis container must have line-height set on it'
  ];

  /**
   * Vendor Info
   */

  var vendor = getVendorData();

  /**
   * Initialize a new Ellipsis
   * instance with the given element.
   *
   * Options:
   *   - `container` A parent container element
   *   - `reRender` Forces a redraw after ellipsis applied
   *
   * @constructor
   * @param {Element} el
   * @param {Object} options
   * @api public
   */

  function Ellipsis(el, options) {
    if (!el) return;
    this.el = el;
    this.container = options && options.container;
    this.reRender = options && options.reRender;
  }

  /**
   * Measures the element and
   * finds the overflowing child.
   *
   * @return {Ellipsis}
   * @api public
   */

  Ellipsis.prototype.calc = function() {
    if (!this.el) return this;
    var style = getStyle(this.el);
    var size = getSize(this.el);

    this.columnHeight = size[1];
    this.columnCount = getColumnCount(style);
    this.columnGap = getColumnGap(style);
    this.columnWidth = size[0] / this.columnCount;
    this.lineHeight = getLineHeight(style);
    this.deltaHeight = size[1] % this.lineHeight;
    this.linesPerColumn = Math.floor(this.columnHeight / this.lineHeight);
    this.totalLines = this.linesPerColumn * this.columnCount;

    // COMPLEX:
    // We set the height on the container
    // explicitly to work around problem
    // with columned containers not fitting
    // all lines when the height is exactly
    // divisible by the line height.
    if (!this.deltaHeight && this.columnCount > 1) {
      this.el.style.height = this.columnHeight + 'px';
    }

    this.child = this.getOverflowingChild();

    return this;
  };

  /**
   * Clamps the overflowing child using
   * the information acquired from #calc().
   *
   * @return {Ellipsis}
   * @api public
   */

  Ellipsis.prototype.set = function() {
    if (!this.el || !this.child) return this;

    this.clampChild();
    siblingsAfter(this.child.el, { display: 'none' });
    this.markContainer();

    return this;
  };

  /**
   * Unclamps the overflowing child.
   *
   * @return {Ellipsis}
   * @api public
   */

  Ellipsis.prototype.unset = function() {
    if (!this.el || !this.child) return this;

    this.el.style.height = '';
    this.unclampChild(this.child);
    siblingsAfter(this.child.el, { display: '' });
    this.unmarkContainer();
    this.child = null;

    return this;
  };

  /**
   * Clears any references
   *
   * @return {Ellipsis}
   * @api public
   */

  Ellipsis.prototype.destroy = function() {
    this.el = this.child = null;
    return this;
  };

  /**
   * Returns the overflowing child with some
   * extra data required for clamping.
   *
   * @param  {Ellipsis} instance
   * @return {Object}
   * @api private
   */

  Ellipsis.prototype.getOverflowingChild = function() {
    var self = this;
    var child = {};
    var lineCounter = 0;

    // Loop over each child element
    each(this.el.children, function(el) {
      var lineCount, overflow, underflow;
      var startColumnIndex = Math.floor(lineCounter / self.linesPerColumn);

      // Get the line count of the
      // child and increment the counter
      lineCounter += lineCount = self.getLineCount(el);

      // If this is the overflowing child
      if (lineCounter >= self.totalLines) {
        overflow = lineCounter - self.totalLines;
        underflow = lineCount - overflow;

        child.el = el;
        child.clampedLines = underflow;
        child.clampedHeight = child.clampedLines * self.lineHeight;
        child.visibleColumnSpan = self.columnCount - startColumnIndex;
        child.gutterSpan = child.visibleColumnSpan - 1;
        child.applyTopMargin = self.shouldApplyTopMargin(child);

        // COMPLEX:
        // In order to get the overflowing
        // child height correct we have to
        // add the delta for each gutter the
        // overflowing child crosses. This is
        // just how webkit columns work.
        if (vendor.webkit) {
          child.clampedHeight += child.gutterSpan * self.deltaHeight;
        }

        return child;
      }
    });

    return child;
  };

  /**
   * Returns the number
   * of lines an element has.
   *
   * If the element is larger than
   * the column width we make the
   * assumption that this is FireFox
   * and the element is broken across
   * a column boundary. In this case
   * we have to get the height using
   * `getClientRects()`.
   *
   * @param  {Element} el
   * @return {Number}
   * @api private
   */

  Ellipsis.prototype.getLineCount = function(el) {
    return (el.offsetWidth > this.columnWidth)
      ? getLinesFromRects(el, this.lineHeight)
      : lineCount(el.clientHeight, this.lineHeight);
  };

  /**
   * If a container has been
   * declared we mark it with
   * a class for styling purposes.
   *
   * @api private
   */

  Ellipsis.prototype.markContainer = function() {
    if (!this.container) return;
    this.container.classList.add(containerClass);
    if (this.reRender) reRender(this.container);
  };

  /**
   * Removes the class
   * from the container.
   *
   * @api private
   */

  Ellipsis.prototype.unmarkContainer = function() {
    if (!this.container) return;
    this.container.classList.remove(containerClass);
    if (this.reRender) reRender(this.container);
  };

  /**
   * Determines whether top margin should be
   * applied to the overflowing child.
   *
   * This is to counteract an annoying
   * column-count/-webkit-box bug, whereby the
   * flexbox element falls into the delta are under
   * the previous sibling. Top margin keeps it
   * in the correct column.
   *
   * @param  {Element} el
   * @param  {Ellipsis} instance
   * @return {Boolean}
   * @api private
   */

  Ellipsis.prototype.shouldApplyTopMargin = function(child) {
    var el = child.el;

    // Dont't if it's not webkit
    if (!vendor.webkit) return;

    // Don't if it's a single column layout
    if (this.columnCount === 1) return;

    // Don't if the delta height is minimal
    if (this.deltaHeight <= 3) return;

    // Don't if it's the first child
    if (!el.previousElementSibling) return;

    // FINAL TEST: If the element is at the top or bottom of its
    // parent container then we require top margin.
    return (el.offsetTop === 0 || el.offsetTop === this.columnHeight);
  };

  /**
   * Clamps the child element to the set
   * height and lines.
   *
   * @param  {Object} child
   * @api private
   */

  Ellipsis.prototype.clampChild = function() {
    var child = this.child;
    if (!child || !child.el) return;

    // Clamp the height
    child.el.style.height = child.clampedHeight + 'px';

    // Use webkit line clamp
    // for webkit browsers.
    if (vendor.webkit) {
      child.el.style.webkitLineClamp = child.clampedLines;
      child.el.style.display = '-webkit-box';
      child.el.style.webkitBoxOrient = 'vertical';
    }

    if (this.shouldHideOverflow()) child.el.style.overflow = 'hidden';

    // Apply a top margin to fix webkit
    // column-count mixed with flexbox bug,
    // if we have decided it is neccessary.
    if (child.applyTopMargin) child.el.style.marginTop = '2em';

    // Add the overflowing child class as
    // a style hook
    child.el.classList.add(overflowingChildClass);

    // Non webkit browsers get a helper
    // elment that is styled as an alternative
    // to the webkit-line-clamp ellipsis.
    // Must be position relative so that we can
    // position the helper element.
    if (!vendor.webkit) {
      child.el.style.position = 'relative';
      child.helper = child.el.appendChild(this.helperElement());
    }
  };

  /**
   * Removes all clamping styles from
   * the overflowing child.
   *
   * @param  {Object} child
   * @api private
   */

  Ellipsis.prototype.unclampChild = function(child) {
    if (!child || !child.el) return;
    child.el.style.display = '';
    child.el.style.height = '';
    child.el.style.webkitLineClamp = '';
    child.el.style.webkitBoxOrient = '';
    child.el.style.marginTop = '';
    child.el.style.overflow = '';
    child.el.classList.remove(overflowingChildClass);

    if (child.helper) {
      child.helper.parentNode.removeChild(child.helper);
    }
  };

  /**
   * Creates the helper element
   * for non-webkit browsers.
   *
   * @return {Element}
   * @api private
   */

  Ellipsis.prototype.helperElement = function() {
    var el = document.createElement('span');
    var columns = this.child.visibleColumnSpan - 1;
    var rightOffset, marginRight;

    el.className = 'ellipsis-helper';
    el.style.display = 'block';
    el.style.height = this.lineHeight + 'px';
    el.style.width = '5em';
    el.style.position = 'absolute';
    el.style.bottom = 0;
    el.style.right = 0;

    // HACK: This is a work around to deal with
    // the wierdness of positioning elements
    // inside an element that is broken across
    // more than one column.
    if (vendor.js === 'Moz' && columns) {
      rightOffset = -(columns * 100);
      marginRight = -(columns * this.columnGap);
      el.style.right = rightOffset + '%';
      el.style.marginRight = marginRight + 'px';
      el.style.marginBottom = this.deltaHeight + 'px';
    }

    return el;
  };

  /**
   * Determines whether overflow
   * should be hidden on clamped
   * child.
   *
   * NOTE:
   * Overflow hidden is only required
   * for single column containers as
   * multi-column containers overflow
   * to the right, so are not visible.
   * `overflow: hidden;` also messes
   * with column layout in Firefox.
   *
   * @return {Boolean}
   * @api private
   */
  Ellipsis.prototype.shouldHideOverflow = function() {
    var columns = this.columnCount > 1;

    if (this.columnHeight < this.lineHeight) return true;

    // Hide all single column overflow
    if (!columns) return true;

    return columns && !vendor.moz;
  };

  /**
   * Re-render with no setTimeout, boom!
   *
   * NOTE:
   * We have to assign the return value
   * to something global so that Closure
   * Compiler doesn't strip it out.
   *
   * @param  {Element} el
   * @return void
   */
  function reRender(el) {
    el.style.display = 'none';
    Ellipsis.r = el.offsetTop;
    el.style.display = '';
  }

  /**
   * Sets the display property on
   * all siblingsafter the given element.
   *
   * Options:
   *   - `display` the css display type to use
   *
   * @param  {Node} el
   * @param  {Options} options
   * @return void
   */

  function siblingsAfter(el, options) {
    if (!el) return;
    var display = options && options.display;
    var siblings = el.parentNode.children;
    var index = indexOf.call(siblings, el);

    for (var i = index + 1, l = siblings.length; i < l; i++) {
      siblings[i].style.display = display;
    }
  }

  /**
   * Returns total line
   * count from a rect list.
   *
   * @param  {Element} el
   * @param  {Number} lineHeight
   * @return {Number}
   */

  function getLinesFromRects(el, lineHeight) {
    var rects = el.getClientRects();
    var lines = 0;

    each(rects, function(rect) {
      lines += lineCount(rect.height, lineHeight);
    });

    return lines;
  }

  /**
   * Calculates a line count
   * from the passed height.
   *
   * @param  {Number} height
   * @param  {Number} lineHeight
   * @return {Number}
   */

  function lineCount(height, lineHeight) {
    return Math.floor(height / lineHeight);
  }

  /**
   * Returns infomation about
   * the current vendor.
   *
   * @return {Object}
   */

  function getVendorData() {
    var el = document.createElement( "div" );
    var js = ["Webkit", "Moz", "O", "ms"];
    var css = ["-webkit-", "-moz-", "-o-", "-ms-"];
    for (var i = 0; i < js.length; i++ ) {
      if (js[i] + "ColumnCount" in el.style) {
        return {
          css: css[i],
          js: js[i],
          webkit: js[i] === 'Webkit',
          moz: js[i] === 'Moz'
        };
      }
    }
  }

  /**
   * Gets the column count of an
   * element using the vendor prefix.
   *
   * @param  {CSSStyleDeclaration} style  [description]
   * @return {Number}
   */

  function getColumnCount(style) {
    return parseInt(style[vendor.js + 'ColumnCount'], 10) || 1;
  }

  /**
   * Returns the gap between columns
   *
   * @param  {CSSStyleDeclaration} style
   * @return {Number}
   */

  function getColumnGap(style) {
    return parseInt(style[vendor.js + 'ColumnGap'], 10);
  }

  /**
   * Gets the line height
   * from the style declaration.
   *
   * @param  {CSSStyleDeclaration} style
   * @return {Number|null}
   */

  function getLineHeight(style) {
    var lineHeight = parseInt(style.lineHeight, 10);
    if (!lineHeight) throw new Error(errors[0]);

    return lineHeight;
  }

  /**
   * Returns the width and
   * height of the given element.
   *
   * @param  {Element} el
   * @return {Array}
   */

  function getSize(el) {
    return [el.offsetWidth, el.offsetHeight];
  }

  /**
   * Little iterator
   *
   * @param  {Array}   list
   * @param  {Function} fn
   */

  function each(list, fn) {
    for (var i = 0, l = list.length; i < l; i++) if (fn(list[i])) break;
  }

  /**
   * Expose `Ellipsis`
   */

  if (typeof exports === 'object') {
    module.exports = function(el, options) {
      return new Ellipsis(el, options);
    };

    module.exports.Ellipsis = Ellipsis;
  } else if (typeof define === 'function' && define.amd) {
    define(Ellipsis);
  } else {
    window.Ellipsis = Ellipsis;
  }

})();<|endoftext|>


<commit_before>// @flow

var packageInfo = require('../package.json');
var chalk = require('chalk');
var Install = require('./Install.js');
var Compile = require('./Compile.js');
var Generate = require('./Generate.js');
var processTitle = 'elm-test';
var which = require('which');

process.title = processTitle;

process.on('uncaughtException', function(error) {
  if (/ an argument in Javascript/.test(error)) {
    // Handle arg mismatch between js and elm code. Expected message from Elm:
    // "You are giving module `Main` an argument in JavaScript.
    // This module does not take arguments though! You probably need to change the
    // initialization code to something like `Elm.Test.Generated.Main.fullscreen()`]"
    console.error('Error starting the node-test-runner.');
    console.error(
      "Please check your Javascript 'elm-test' and Elm 'node-test-runner' package versions are compatible"
    );
    process.exit(1);
  } else {
    console.error('Unhandled exception while running the tests:', error);
    process.exit(1);
  }
});

var compile = require('node-elm-compiler').compile,
  fs = require('fs-extra'),
  os = require('os'),
  glob = require('glob'),
  path = require('path'),
  _ = require('lodash'),
  minimist = require('minimist'),
  chokidar = require('chokidar'),
  Runner = require('./Runner.js'),
  Supervisor = require('./Supervisor.js');

// Check Node version
const nodeVersionString = process.versions.node;
const nodeVersion = _.map(_.split(nodeVersionString, '.'), _.parseInt);

if (
  (nodeVersion[0] === 0 && nodeVersion[1] < 11) ||
  (nodeVersion[0] === 0 && nodeVersion[1] === 11 && nodeVersion[2] < 13)
) {
  console.log('using node v' + nodeVersionString);
  console.error(
    'elm-test requires node v4.7.0 or greater - upgrade the installed version of node and try again'
  );
  process.exit(1);
}

var args = minimist(process.argv.slice(2), {
  boolean: ['warn', 'version', 'help', 'watch'],
  string: ['compiler', 'seed', 'report', 'fuzz'],
});
var processes = Math.max(1, os.cpus().length);

// Recursively search directories for *.elm files, excluding elm-stuff/
function resolveFilePath(filename) {
  var candidates;

  if (!fs.existsSync(filename)) {
    candidates = [];
  } else if (fs.lstatSync(filename).isDirectory()) {
    candidates = _.flatMap(
      glob.sync('/**/*.elm', {
        root: filename,
        nocase: true,
        ignore: '/**/elm-stuff/**',
        nodir: true,
      }),
      resolveFilePath
    );
  } else {
    candidates = [path.resolve(filename)];
  }

  // Exclude everything having anything to do with elm-stuff
  return candidates.filter(function(candidate) {
    return candidate.split(path.sep).indexOf('elm-stuff') === -1;
  });
}

let pathToElmBinary;

if (args.compiler === undefined) {
  try {
    pathToElmBinary = which.sync('elm');
  } catch (error) {
    throw new Error(
      `Cannot find elm executable, make sure it is installed.
(If elm is not on your path or is called something different the --compiler flag might help.)`
    );
  }
} else {
  try {
    pathToElmBinary = which.sync(path.resolve(args.compiler));
  } catch (error) {
    throw new Error(
      'The --compiler option must be given a path to an elm executable.'
    );
  }
}

function printUsage(str) {
  console.log('Usage: elm-test ' + str + '\n');
}

if (args.help) {
  var exampleGlob = path.join('tests', '**', '*.elm');

  [
    'init # Create example tests',
    'install PACKAGE # Like `elm install PACKAGE`, except it installs to "test-dependencies" in your elm.json',
    'TESTFILES # Run TESTFILES, for example ' + exampleGlob,
    '[--compiler /path/to/compiler] # Run tests',
    '[--seed integer] # Run with initial fuzzer seed',
    '[--fuzz integer] # Run with each fuzz test performing this many iterations',
    '[--report json, junit, or console (default)] # Print results to stdout in given format',
    '[--version] # Print version string and exit',
    '[--watch] # Run tests on file changes',
  ].forEach(printUsage);

  process.exit(0);
}

if (args.version) {
  console.log(require(path.join(__dirname, '..', 'package.json')).version);
  process.exit(0);
}

if (args._[0] === 'install') {
  var packageName = args._[1];

  if (typeof packageName === 'string') {
    if (!fs.existsSync('elm.json')) {
      console.error(
        '`elm-test install` must be run in the same directory as an existing elm.json file!'
      );
      process.exit(1);
    }

    Install.install(pathToElmBinary, packageName);

    process.exit(0);
  } else {
    console.error(
      'What package should I install? I was expecting something like this:\n\n    elm-test install elm/regex\n'
    );
    process.exit(1);
  }
} else if (args._[0] == 'init') {
  if (!fs.existsSync('elm.json')) {
    console.error(
      '`elm-test init` must be run in the same directory as an existing elm.json file! You can run `elm init` to initialize one.'
    );
    process.exit(1);
  }

  Install.install(pathToElmBinary, 'elm-explorations/test');
  fs.mkdirpSync('tests');
  fs.copySync(
    path.join(__dirname, '..', 'templates', 'tests', 'Example.elm'),
    'tests/Example.elm'
  );

  console.log(
    '\nCheck out the documentation for getting started at https://package.elm-lang.org/packages/elm-explorations/test/latest'
  );

  process.exit(0);
}

let runsExecuted = 0;

function runTests(generatedCodeDir /*: string */, testFile /*: string */) {
  const dest = path.resolve(path.join(generatedCodeDir, 'elmTestOutput.js'));

  // Incorporate the process PID into the socket name, so elm-test processes can
  // be run parallel without accidentally sharing each others' sockets.
  //
  // See https://github.com/rtfeldman/node-test-runner/pull/231
  // Also incorporate a salt number into it on Windows, to avoid EADDRINUSE -
  // see https://github.com/rtfeldman/node-test-runner/issues/275 - because the
  // alternative approach of deleting the file before creating a new one doesn't
  // work on Windows. We have to let Windows clean up the named pipe. This is
  // essentially a band-aid fix. The alternative is to rewrite a ton of stuff.
  runsExecuted++;
  const pipeFilename =
    process.platform === 'win32'
      ? '\\\\.\\pipe\\elm_test-' + process.pid + '-' + runsExecuted
      : '/tmp/elm_test-' + process.pid + '.sock';

  return Compile.compile(
    testFile,
    dest,
    args.verbose,
    pathToElmBinary,
    args.report
  )
    .then(function() {
      return Generate.prepareCompiledJsFile(pipeFilename, dest).then(
        function() {
          return Supervisor.run(
            packageInfo.version,
            pipeFilename,
            report,
            processes,
            dest,
            args.watch,
            Compile.isMachineReadableReporter(report)
          );
        }
      );
    })
    .catch(function(exitCode) {
      console.error('Compilation failed for', testFile);
      return Promise.reject(exitCode);
    });
}

function globify(filename) {
  return glob.sync(filename, {
    nocase: true,
    ignore: '**/elm-stuff/**',
    nodir: false,
    absolute: true,
  });
}

function globifyWithRoot(root, filename) {
  return glob.sync(filename, {
    root: root,
    nocase: true,
    ignore: '**/elm-stuff/**',
    nodir: false,
    absolute: true,
  });
}

function resolveGlobs(fileGlobs) {
  let globs;

  if (fileGlobs.length > 0) {
    globs = _.flatMap(fileGlobs, globify);
  } else {
    var root = path.join(
      path.resolve(Runner.findNearestElmPackageDir([process.cwd()]))
    );

    globs = globifyWithRoot(root, 'test?(s)/**/*.elm');
  }

  return _.flatMap(globs, resolveFilePath);
}

function getGlobsToWatch(elmJson) {
  let sourceDirectories;
  if (elmJson['type'] === 'package') {
    sourceDirectories = ['src'];
  } else {
    sourceDirectories = elmJson['source-directories'];
  }
  return [...sourceDirectories, 'tests'].map(function(sourceDirectory) {
    return path.posix.join(sourceDirectory, '**', '*.elm');
  });
}

var report;

if (
  args.report === 'console' ||
  args.report === 'json' ||
  args.report === 'junit'
) {
  report = args.report;
} else if (args.report !== undefined) {
  console.error(
    "The --report option must be given either 'console', 'junit', or 'json'"
  );
  process.exit(1);
} else {
  report = 'console';
}

function infoLog(msg) {
  if (report === 'console') {
    console.log(msg);
  }
}

function getProjectRootDir(filePaths) {
  return path.resolve(Runner.findNearestElmPackageDir(filePaths));
}

if (args._[0] === 'make') {
  const files = args._.slice(1);

  // It's important to globify all the arguments.
  // On Bash 4.x (or zsh), if you give it a glob as its last argument, Bash
  // translates that into a list of file paths. On bash 3.x it's just a string.
  // Ergo, globify all the arguments we receive.
  const fileGlobs = files.length > 0 ? files : [];
  const testFilePaths = resolveGlobs(files);
  const projectRootDir = getProjectRootDir(testFilePaths);

  // TODO this probably now needs to use generated dir
  Compile.compileAll(
    testFilePaths,
    projectRootDir,
    args.verbose,
    pathToElmBinary,
    args.report
  )
    .then(function() {
      process.exit(0);
    })
    .catch(function(err) {
      process.exit(1);
    });
} else {
  // It's important to globify all the arguments.
  // On Bash 4.x (or zsh), if you give it a glob as its last argument, Bash
  // translates that into a list of file paths. On bash 3.x it's just a string.
  // Ergo, globify all the arguments we receive.
  const fileGlobs = args._.length > 0 ? args._ : [];
  const testFilePaths = resolveGlobs(fileGlobs);
  const projectRootDir = getProjectRootDir(testFilePaths);

  const elmJsonPath = path.resolve(path.join(projectRootDir, 'elm.json'));
  var projectElmJson = {};

  try {
    projectElmJson = fs.readJsonSync(elmJsonPath);
  } catch (err) {
    console.error('Error reading elm.json: ' + err);
    process.exit(1);
  }

  var isPackageProject = projectElmJson.type === 'package';

  if (testFilePaths.length === 0) {
    var extraAppError =
      "\n\nAlternatively, if your application has tests in a different directory, try calling elm-test with a glob: elm-test 'frontend-app/**/*Tests.elm'.";

    var errorMessage =
      fileGlobs.length > 0
        ? 'No tests found for the file pattern "' +
          fileGlobs.toString() +
          '"\n\nMaybe try running elm-test with no arguments?'
        : "No tests found in the tests/ directory.\n\nNOTE: Make sure you're running elm-test from your project's root directory, where its elm.json lives.\n\nTo generate some initial tests to get things going, run elm-test init." +
          (isPackageProject ? '' : extraAppError);

    console.error(errorMessage);
    process.exit(1);
  }

  function clearConsole() {
    process.stdout.write(
      process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'
    );
  }

  const hasBeenGivenCustomGlobs = fileGlobs.length > 0;

  if (isPackageProject) {
    // Run `elm make` on the project root, to generate the .elmi files.
    Compile.compileSources([], projectRootDir, args.verbose, pathToElmBinary, args.report).then(() => {
      Runner.getIndirectDeps(projectRootDir).then((packageIndirectDeps) => {
        return generateAndRun(projectRootDir, testFilePaths, packageIndirectDeps, hasBeenGivenCustomGlobs);
      });
    });
  } else {
    generateAndRun(projectRootDir, testFilePaths, [], hasBeenGivenCustomGlobs);
  }
}

function generateAndRun(
  projectRootDir /*: string */,
  testFilePaths /*: Array<string> */,
  packageIndirectDeps /*: Object */,
  hasBeenGivenCustomGlobs /*: boolean */
) {
  const generatedCodeDir = Compile.getGeneratedCodeDir(projectRootDir);

  const returnValues = Generate.generateElmJson(
    projectRootDir,
    pathToElmBinary,
    generatedCodeDir,
    testFilePaths,
    packageIndirectDeps,
    hasBeenGivenCustomGlobs
  );
  const generatedSrc = returnValues[1];
  const sourceDirs = returnValues[2];

  function run() {
    // This compiles all the tests so that we generate *.elmi files for them,
    // which we can then read to determine which tests need to be run.
    return Compile.compileAll(
      testFilePaths,
      generatedCodeDir,
      args.verbose,
      pathToElmBinary,
      args.report,
    )
      .then(function() {
        return Runner.findTests(
          generatedCodeDir,
          testFilePaths,
          sourceDirs,
          !Compile.isMachineReadableReporter(report)
        )
          .then(function(testModules) {
            process.chdir(generatedCodeDir);

            const mainFile = Generate.generateMainModule(
              parseInt(args.fuzz),
              parseInt(args.seed),
              args.report,
              testFilePaths,
              testModules,
              generatedSrc,
              processes
            );

            return runTests(generatedCodeDir, mainFile);
          })
          .catch(function(err) {
            console.error(err);
            process.exit(1);
          });
      })
      .catch(function(err) {
        console.error(err);
        if (!args.watch) {
          process.exit(1);
        }
      })
      .then(function() {
        console.log(chalk.blue('Watching for changes...'));
      });
  }

  var currentRun = run();

  if (args.watch) {
    clearConsole();
    infoLog('Running in watch mode');

    var globsToWatch = getGlobsToWatch(projectElmJson);
    var watcher = chokidar.watch(globsToWatch, {
      awaitWriteFinish: {
        stabilityThreshold: 500,
      },
      ignoreInitial: true,
      ignored: /(\/|^)elm-stuff(\/|$)/,
      cwd: projectRootDir,
    });

    var eventNameMap = {
      add: 'added',
      addDir: 'added',
      change: 'changed',
      unlink: 'removed',
      unlinkDir: 'removed',
    };

    watcher.on('all', function(event, filePath) {
      var testRootDir = Compile.getTestRootDir(projectRootDir);
      var relativePath = path.relative(testRootDir, filePath);
      var eventName = eventNameMap[event] || event;
      clearConsole();
      infoLog('\n' + filePath + ' ' + eventName + '. Rebuilding!');

      // TODO if a previous run is in progress, wait until it's done.
      currentRun = currentRun.then(run);
    });
  }
}
<commit_msg>Move clearConsole definition up.<commit_after>// @flow

var packageInfo = require('../package.json');
var chalk = require('chalk');
var Install = require('./Install.js');
var Compile = require('./Compile.js');
var Generate = require('./Generate.js');
var processTitle = 'elm-test';
var which = require('which');

process.title = processTitle;

function clearConsole() {
  process.stdout.write(
    process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'
  );
}

process.on('uncaughtException', function(error) {
  if (/ an argument in Javascript/.test(error)) {
    // Handle arg mismatch between js and elm code. Expected message from Elm:
    // "You are giving module `Main` an argument in JavaScript.
    // This module does not take arguments though! You probably need to change the
    // initialization code to something like `Elm.Test.Generated.Main.fullscreen()`]"
    console.error('Error starting the node-test-runner.');
    console.error(
      "Please check your Javascript 'elm-test' and Elm 'node-test-runner' package versions are compatible"
    );
    process.exit(1);
  } else {
    console.error('Unhandled exception while running the tests:', error);
    process.exit(1);
  }
});

var compile = require('node-elm-compiler').compile,
  fs = require('fs-extra'),
  os = require('os'),
  glob = require('glob'),
  path = require('path'),
  _ = require('lodash'),
  minimist = require('minimist'),
  chokidar = require('chokidar'),
  Runner = require('./Runner.js'),
  Supervisor = require('./Supervisor.js');

// Check Node version
const nodeVersionString = process.versions.node;
const nodeVersion = _.map(_.split(nodeVersionString, '.'), _.parseInt);

if (
  (nodeVersion[0] === 0 && nodeVersion[1] < 11) ||
  (nodeVersion[0] === 0 && nodeVersion[1] === 11 && nodeVersion[2] < 13)
) {
  console.log('using node v' + nodeVersionString);
  console.error(
    'elm-test requires node v4.7.0 or greater - upgrade the installed version of node and try again'
  );
  process.exit(1);
}

var args = minimist(process.argv.slice(2), {
  boolean: ['warn', 'version', 'help', 'watch'],
  string: ['compiler', 'seed', 'report', 'fuzz'],
});
var processes = Math.max(1, os.cpus().length);

// Recursively search directories for *.elm files, excluding elm-stuff/
function resolveFilePath(filename) {
  var candidates;

  if (!fs.existsSync(filename)) {
    candidates = [];
  } else if (fs.lstatSync(filename).isDirectory()) {
    candidates = _.flatMap(
      glob.sync('/**/*.elm', {
        root: filename,
        nocase: true,
        ignore: '/**/elm-stuff/**',
        nodir: true,
      }),
      resolveFilePath
    );
  } else {
    candidates = [path.resolve(filename)];
  }

  // Exclude everything having anything to do with elm-stuff
  return candidates.filter(function(candidate) {
    return candidate.split(path.sep).indexOf('elm-stuff') === -1;
  });
}

let pathToElmBinary;

if (args.compiler === undefined) {
  try {
    pathToElmBinary = which.sync('elm');
  } catch (error) {
    throw new Error(
      `Cannot find elm executable, make sure it is installed.
(If elm is not on your path or is called something different the --compiler flag might help.)`
    );
  }
} else {
  try {
    pathToElmBinary = which.sync(path.resolve(args.compiler));
  } catch (error) {
    throw new Error(
      'The --compiler option must be given a path to an elm executable.'
    );
  }
}

function printUsage(str) {
  console.log('Usage: elm-test ' + str + '\n');
}

if (args.help) {
  var exampleGlob = path.join('tests', '**', '*.elm');

  [
    'init # Create example tests',
    'install PACKAGE # Like `elm install PACKAGE`, except it installs to "test-dependencies" in your elm.json',
    'TESTFILES # Run TESTFILES, for example ' + exampleGlob,
    '[--compiler /path/to/compiler] # Run tests',
    '[--seed integer] # Run with initial fuzzer seed',
    '[--fuzz integer] # Run with each fuzz test performing this many iterations',
    '[--report json, junit, or console (default)] # Print results to stdout in given format',
    '[--version] # Print version string and exit',
    '[--watch] # Run tests on file changes',
  ].forEach(printUsage);

  process.exit(0);
}

if (args.version) {
  console.log(require(path.join(__dirname, '..', 'package.json')).version);
  process.exit(0);
}

if (args._[0] === 'install') {
  var packageName = args._[1];

  if (typeof packageName === 'string') {
    if (!fs.existsSync('elm.json')) {
      console.error(
        '`elm-test install` must be run in the same directory as an existing elm.json file!'
      );
      process.exit(1);
    }

    Install.install(pathToElmBinary, packageName);

    process.exit(0);
  } else {
    console.error(
      'What package should I install? I was expecting something like this:\n\n    elm-test install elm/regex\n'
    );
    process.exit(1);
  }
} else if (args._[0] == 'init') {
  if (!fs.existsSync('elm.json')) {
    console.error(
      '`elm-test init` must be run in the same directory as an existing elm.json file! You can run `elm init` to initialize one.'
    );
    process.exit(1);
  }

  Install.install(pathToElmBinary, 'elm-explorations/test');
  fs.mkdirpSync('tests');
  fs.copySync(
    path.join(__dirname, '..', 'templates', 'tests', 'Example.elm'),
    'tests/Example.elm'
  );

  console.log(
    '\nCheck out the documentation for getting started at https://package.elm-lang.org/packages/elm-explorations/test/latest'
  );

  process.exit(0);
}

let runsExecuted = 0;

function runTests(generatedCodeDir /*: string */, testFile /*: string */) {
  const dest = path.resolve(path.join(generatedCodeDir, 'elmTestOutput.js'));

  // Incorporate the process PID into the socket name, so elm-test processes can
  // be run parallel without accidentally sharing each others' sockets.
  //
  // See https://github.com/rtfeldman/node-test-runner/pull/231
  // Also incorporate a salt number into it on Windows, to avoid EADDRINUSE -
  // see https://github.com/rtfeldman/node-test-runner/issues/275 - because the
  // alternative approach of deleting the file before creating a new one doesn't
  // work on Windows. We have to let Windows clean up the named pipe. This is
  // essentially a band-aid fix. The alternative is to rewrite a ton of stuff.
  runsExecuted++;
  const pipeFilename =
    process.platform === 'win32'
      ? '\\\\.\\pipe\\elm_test-' + process.pid + '-' + runsExecuted
      : '/tmp/elm_test-' + process.pid + '.sock';

  return Compile.compile(
    testFile,
    dest,
    args.verbose,
    pathToElmBinary,
    args.report
  )
    .then(function() {
      return Generate.prepareCompiledJsFile(pipeFilename, dest).then(
        function() {
          return Supervisor.run(
            packageInfo.version,
            pipeFilename,
            report,
            processes,
            dest,
            args.watch,
            Compile.isMachineReadableReporter(report)
          );
        }
      );
    })
    .catch(function(exitCode) {
      console.error('Compilation failed for', testFile);
      return Promise.reject(exitCode);
    });
}

function globify(filename) {
  return glob.sync(filename, {
    nocase: true,
    ignore: '**/elm-stuff/**',
    nodir: false,
    absolute: true,
  });
}

function globifyWithRoot(root, filename) {
  return glob.sync(filename, {
    root: root,
    nocase: true,
    ignore: '**/elm-stuff/**',
    nodir: false,
    absolute: true,
  });
}

function resolveGlobs(fileGlobs) {
  let globs;

  if (fileGlobs.length > 0) {
    globs = _.flatMap(fileGlobs, globify);
  } else {
    var root = path.join(
      path.resolve(Runner.findNearestElmPackageDir([process.cwd()]))
    );

    globs = globifyWithRoot(root, 'test?(s)/**/*.elm');
  }

  return _.flatMap(globs, resolveFilePath);
}

function getGlobsToWatch(elmJson) {
  let sourceDirectories;
  if (elmJson['type'] === 'package') {
    sourceDirectories = ['src'];
  } else {
    sourceDirectories = elmJson['source-directories'];
  }
  return [...sourceDirectories, 'tests'].map(function(sourceDirectory) {
    return path.posix.join(sourceDirectory, '**', '*.elm');
  });
}

var report;

if (
  args.report === 'console' ||
  args.report === 'json' ||
  args.report === 'junit'
) {
  report = args.report;
} else if (args.report !== undefined) {
  console.error(
    "The --report option must be given either 'console', 'junit', or 'json'"
  );
  process.exit(1);
} else {
  report = 'console';
}

function infoLog(msg) {
  if (report === 'console') {
    console.log(msg);
  }
}

function getProjectRootDir(filePaths) {
  return path.resolve(Runner.findNearestElmPackageDir(filePaths));
}

if (args._[0] === 'make') {
  const files = args._.slice(1);

  // It's important to globify all the arguments.
  // On Bash 4.x (or zsh), if you give it a glob as its last argument, Bash
  // translates that into a list of file paths. On bash 3.x it's just a string.
  // Ergo, globify all the arguments we receive.
  const fileGlobs = files.length > 0 ? files : [];
  const testFilePaths = resolveGlobs(files);
  const projectRootDir = getProjectRootDir(testFilePaths);

  // TODO this probably now needs to use generated dir
  Compile.compileAll(
    testFilePaths,
    projectRootDir,
    args.verbose,
    pathToElmBinary,
    args.report
  )
    .then(function() {
      process.exit(0);
    })
    .catch(function(err) {
      process.exit(1);
    });
} else {
  // It's important to globify all the arguments.
  // On Bash 4.x (or zsh), if you give it a glob as its last argument, Bash
  // translates that into a list of file paths. On bash 3.x it's just a string.
  // Ergo, globify all the arguments we receive.
  const fileGlobs = args._.length > 0 ? args._ : [];
  const testFilePaths = resolveGlobs(fileGlobs);
  const projectRootDir = getProjectRootDir(testFilePaths);

  const elmJsonPath = path.resolve(path.join(projectRootDir, 'elm.json'));
  var projectElmJson = {};

  try {
    projectElmJson = fs.readJsonSync(elmJsonPath);
  } catch (err) {
    console.error('Error reading elm.json: ' + err);
    process.exit(1);
  }

  var isPackageProject = projectElmJson.type === 'package';

  if (testFilePaths.length === 0) {
    var extraAppError =
      "\n\nAlternatively, if your application has tests in a different directory, try calling elm-test with a glob: elm-test 'frontend-app/**/*Tests.elm'.";

    var errorMessage =
      fileGlobs.length > 0
        ? 'No tests found for the file pattern "' +
          fileGlobs.toString() +
          '"\n\nMaybe try running elm-test with no arguments?'
        : "No tests found in the tests/ directory.\n\nNOTE: Make sure you're running elm-test from your project's root directory, where its elm.json lives.\n\nTo generate some initial tests to get things going, run elm-test init." +
          (isPackageProject ? '' : extraAppError);

    console.error(errorMessage);
    process.exit(1);
  }

  const hasBeenGivenCustomGlobs = fileGlobs.length > 0;

  if (isPackageProject) {
    // Run `elm make` on the project root, to generate the .elmi files.
    Compile.compileSources([], projectRootDir, args.verbose, pathToElmBinary, args.report).then(() => {
      Runner.getIndirectDeps(projectRootDir).then((packageIndirectDeps) => {
        return generateAndRun(projectRootDir, testFilePaths, packageIndirectDeps, hasBeenGivenCustomGlobs);
      });
    });
  } else {
    generateAndRun(projectRootDir, testFilePaths, [], hasBeenGivenCustomGlobs);
  }
}

function generateAndRun(
  projectRootDir /*: string */,
  testFilePaths /*: Array<string> */,
  packageIndirectDeps /*: Object */,
  hasBeenGivenCustomGlobs /*: boolean */
) {
  const generatedCodeDir = Compile.getGeneratedCodeDir(projectRootDir);

  const returnValues = Generate.generateElmJson(
    projectRootDir,
    pathToElmBinary,
    generatedCodeDir,
    testFilePaths,
    packageIndirectDeps,
    hasBeenGivenCustomGlobs
  );
  const generatedSrc = returnValues[1];
  const sourceDirs = returnValues[2];

  function run() {
    // This compiles all the tests so that we generate *.elmi files for them,
    // which we can then read to determine which tests need to be run.
    return Compile.compileAll(
      testFilePaths,
      generatedCodeDir,
      args.verbose,
      pathToElmBinary,
      args.report,
    )
      .then(function() {
        return Runner.findTests(
          generatedCodeDir,
          testFilePaths,
          sourceDirs,
          !Compile.isMachineReadableReporter(report)
        )
          .then(function(testModules) {
            process.chdir(generatedCodeDir);

            const mainFile = Generate.generateMainModule(
              parseInt(args.fuzz),
              parseInt(args.seed),
              args.report,
              testFilePaths,
              testModules,
              generatedSrc,
              processes
            );

            return runTests(generatedCodeDir, mainFile);
          })
          .catch(function(err) {
            console.error(err);
            process.exit(1);
          });
      })
      .catch(function(err) {
        console.error(err);
        if (!args.watch) {
          process.exit(1);
        }
      })
      .then(function() {
        console.log(chalk.blue('Watching for changes...'));
      });
  }

  var currentRun = run();

  if (args.watch) {
    clearConsole();
    infoLog('Running in watch mode');

    var globsToWatch = getGlobsToWatch(projectElmJson);
    var watcher = chokidar.watch(globsToWatch, {
      awaitWriteFinish: {
        stabilityThreshold: 500,
      },
      ignoreInitial: true,
      ignored: /(\/|^)elm-stuff(\/|$)/,
      cwd: projectRootDir,
    });

    var eventNameMap = {
      add: 'added',
      addDir: 'added',
      change: 'changed',
      unlink: 'removed',
      unlinkDir: 'removed',
    };

    watcher.on('all', function(event, filePath) {
      var testRootDir = Compile.getTestRootDir(projectRootDir);
      var relativePath = path.relative(testRootDir, filePath);
      var eventName = eventNameMap[event] || event;
      clearConsole();
      infoLog('\n' + filePath + ' ' + eventName + '. Rebuilding!');

      // TODO if a previous run is in progress, wait until it's done.
      currentRun = currentRun.then(run);
    });
  }
}
<|endoftext|>


<commit_before>var logging      = require('./logging');
var Serializer   = require('./framer').Serializer;
var Deserializer = require('./framer').Deserializer;
var Compressor   = require('./compressor').Compressor;
var Decompressor = require('./compressor').Decompressor;
var Connection   = require('./connection').Connection;
var Duplex       = require('stream').Duplex;

exports.Endpoint = Endpoint;

// Endpoint
// ========

// The process of initialization:
function Endpoint(role, settings, log) {
  Duplex.call(this);

  // * Initializing logging infrastructure
  this._log = (log || logging.root).child({ component: 'endpoint' });

  // * First part of the handshake process: sending and receiving the client connection header
  //   prelude and determining the id of the first stream to be created.
  var firstStreamId;
  switch(role) {
    case 'CLIENT':
      this._writePrelude();
      firstStreamId = 1;
      break;
    case 'SERVER':
      this._readPrelude();
      firstStreamId = 2;
      break;
    default:
      throw new Error('Invalid role: ' + role);
  }

  // * Second part of the handshake process: sending the first SETTINGS frame.
  //   This is done by the connection class right after initialization.
  this._initializeDataFlow(firstStreamId, settings);

  // * Initializing error handling.
  this._initializeErrorHandling();
}
Endpoint.prototype = Object.create(Duplex.prototype, { constructor: { value: Endpoint } });

// Handshake
// ---------

var CLIENT_PRELUDE = new Buffer('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n');

// Writing the client header is simple and synchronous.
Endpoint.prototype._writePrelude = function _writePrelude() {
  this._log.info('Sending the client connection header prelude.');
  this.push(CLIENT_PRELUDE);
};

// The asynchronous process of reading the client header:
Endpoint.prototype._readPrelude = function _readPrelude() {
  // * progress in the header is tracker using a `cursor`
  var cursor = 0;

  // * `_write` is temporarily replaced by the comparator function
  this._write = function _temporalWrite(chunk, encoding, done) {
    // * which compares the stored header with the current `chunk` byte by byte and emits the
    //   'error' event if there's a byte that doesn't match
    var offset = cursor;
    while(cursor < CLIENT_PRELUDE.length && (cursor - offset) < chunk.length) {
      if (CLIENT_PRELUDE[cursor] !== chunk[cursor - offset]) {
        this._log.fatal({ cursor: cursor, offset: offset, chunk: chunk },
                        'Client connection header prelude does not match.');
        return this._error('handshake', 'Client connection header prelude does not match.');
      }
      cursor += 1;
    }

    // * if the whole header is over, and there were no error then restore the original `_write`
    //   and call it with the remaining part of the current chunk
    if (cursor === CLIENT_PRELUDE.length) {
      this._log.info('Successfully received the client connection header prelude.');
      delete this._write;
      chunk = chunk.slice(cursor - offset);
      this._write(chunk, encoding, done);
    }
  };
};

// Data flow
// ---------

//     +---------------------------------------------+
//     |                                             |
//     |   +-------------------------------------+   |
//     |   | +---------+ +---------+ +---------+ |   |
//     |   | | stream1 | | stream2 | |   ...   | |   |
//     |   | +---------+ +---------+ +---------+ |   |
//     |   |             connection              |   |
//     |   +-------------------------------------+   |
//     |             |                 ^             |
//     |        pipe |                 | pipe        |
//     |             v                 |             |
//     |   +------------------+------------------+   |
//     |   |    compressor    |   decompressor   |   |
//     |   +------------------+------------------+   |
//     |             |                 ^             |
//     |        pipe |                 | pipe        |
//     |             v                 |             |
//     |   +------------------+------------------+   |
//     |   |    serializer    |   deserializer   |   |
//     |   +------------------+------------------+   |
//     |             |                 ^             |
//     |     _read() |                 | _write()    |
//     |             v                 |             |
//     |      +------------+     +-----------+       |
//     |      |output queue|     |input queue|       |
//     +------+------------+-----+-----------+-------+
//                   |                 ^
//            read() |                 | write()
//                   v                 |

Endpoint.prototype._initializeDataFlow = function _initializeDataFlow(firstStreamId, settings) {
  this._serializer   = new Serializer(this._log);
  this._deserializer = new Deserializer(this._log);
  this._compressor   = new Compressor(this._log);
  this._decompressor = new Decompressor(this._log);
  this._connection   = new Connection(firstStreamId, settings, this._log);

  this._connection.pipe(this._compressor).pipe(this._serializer);
  this._deserializer.pipe(this._decompressor).pipe(this._connection);
};

Endpoint.prototype._read = function _read(size) {
  var more_needed = true, chunk;
  while (more_needed && (chunk = this._serializer.read(size))) {
    more_needed = this.push(chunk);
  }

  if (more_needed) {
    this._serializer.once('readable', this._read.bind(this));
  }
};

Endpoint.prototype._write = function _write(chunk, encoding, done) {
  this._deserializer.write(chunk, encoding, done);
};

// Error handling
// --------------

Endpoint.prototype._initializeErrorHandling = function _initializeErrorHandling() {
  this._serializer.on('error', this._error.bind(this, 'serializer'));
  this._deserializer.on('error', this._error.bind(this, 'deserializer'));
  this._compressor.on('error', this._error.bind(this, 'compressor'));
  this._decompressor.on('error', this._error.bind(this, 'decompressor'));
  this._connection.on('error', this._error.bind(this, 'connection'));
};

Endpoint.prototype._error = function _error(component, message) {
  this._log.fatal({ component: component, message: message }, 'Fatal error, closing connection');
  this.push(null);
  this.emit('error', component, message);
};
<commit_msg>Fixing a linter warning.<commit_after>var logging      = require('./logging');
var Serializer   = require('./framer').Serializer;
var Deserializer = require('./framer').Deserializer;
var Compressor   = require('./compressor').Compressor;
var Decompressor = require('./compressor').Decompressor;
var Connection   = require('./connection').Connection;
var Duplex       = require('stream').Duplex;

exports.Endpoint = Endpoint;

// Endpoint
// ========

// The process of initialization:
function Endpoint(role, settings, log) {
  Duplex.call(this);

  // * Initializing logging infrastructure
  this._log = (log || logging.root).child({ component: 'endpoint' });

  // * First part of the handshake process: sending and receiving the client connection header
  //   prelude and determining the id of the first stream to be created.
  var firstStreamId;
  switch(role) {
    case 'CLIENT':
      this._writePrelude();
      firstStreamId = 1;
      break;
    case 'SERVER':
      this._readPrelude();
      firstStreamId = 2;
      break;
    default:
      throw new Error('Invalid role: ' + role);
  }

  // * Second part of the handshake process: sending the first SETTINGS frame.
  //   This is done by the connection class right after initialization.
  this._initializeDataFlow(firstStreamId, settings);

  // * Initializing error handling.
  this._initializeErrorHandling();
}
Endpoint.prototype = Object.create(Duplex.prototype, { constructor: { value: Endpoint } });

// Handshake
// ---------

var CLIENT_PRELUDE = new Buffer('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n');

// Writing the client header is simple and synchronous.
Endpoint.prototype._writePrelude = function _writePrelude() {
  this._log.info('Sending the client connection header prelude.');
  this.push(CLIENT_PRELUDE);
};

// The asynchronous process of reading the client header:
Endpoint.prototype._readPrelude = function _readPrelude() {
  // * progress in the header is tracker using a `cursor`
  var cursor = 0;

  // * `_write` is temporarily replaced by the comparator function
  this._write = function _temporalWrite(chunk, encoding, done) {
    // * which compares the stored header with the current `chunk` byte by byte and emits the
    //   'error' event if there's a byte that doesn't match
    var offset = cursor;
    while(cursor < CLIENT_PRELUDE.length && (cursor - offset) < chunk.length) {
      if (CLIENT_PRELUDE[cursor] !== chunk[cursor - offset]) {
        this._log.fatal({ cursor: cursor, offset: offset, chunk: chunk },
                        'Client connection header prelude does not match.');
        this._error('handshake', 'Client connection header prelude does not match.');
        return;
      }
      cursor += 1;
    }

    // * if the whole header is over, and there were no error then restore the original `_write`
    //   and call it with the remaining part of the current chunk
    if (cursor === CLIENT_PRELUDE.length) {
      this._log.info('Successfully received the client connection header prelude.');
      delete this._write;
      chunk = chunk.slice(cursor - offset);
      this._write(chunk, encoding, done);
    }
  };
};

// Data flow
// ---------

//     +---------------------------------------------+
//     |                                             |
//     |   +-------------------------------------+   |
//     |   | +---------+ +---------+ +---------+ |   |
//     |   | | stream1 | | stream2 | |   ...   | |   |
//     |   | +---------+ +---------+ +---------+ |   |
//     |   |             connection              |   |
//     |   +-------------------------------------+   |
//     |             |                 ^             |
//     |        pipe |                 | pipe        |
//     |             v                 |             |
//     |   +------------------+------------------+   |
//     |   |    compressor    |   decompressor   |   |
//     |   +------------------+------------------+   |
//     |             |                 ^             |
//     |        pipe |                 | pipe        |
//     |             v                 |             |
//     |   +------------------+------------------+   |
//     |   |    serializer    |   deserializer   |   |
//     |   +------------------+------------------+   |
//     |             |                 ^             |
//     |     _read() |                 | _write()    |
//     |             v                 |             |
//     |      +------------+     +-----------+       |
//     |      |output queue|     |input queue|       |
//     +------+------------+-----+-----------+-------+
//                   |                 ^
//            read() |                 | write()
//                   v                 |

Endpoint.prototype._initializeDataFlow = function _initializeDataFlow(firstStreamId, settings) {
  this._serializer   = new Serializer(this._log);
  this._deserializer = new Deserializer(this._log);
  this._compressor   = new Compressor(this._log);
  this._decompressor = new Decompressor(this._log);
  this._connection   = new Connection(firstStreamId, settings, this._log);

  this._connection.pipe(this._compressor).pipe(this._serializer);
  this._deserializer.pipe(this._decompressor).pipe(this._connection);
};

Endpoint.prototype._read = function _read(size) {
  var more_needed = true, chunk;
  while (more_needed && (chunk = this._serializer.read(size))) {
    more_needed = this.push(chunk);
  }

  if (more_needed) {
    this._serializer.once('readable', this._read.bind(this));
  }
};

Endpoint.prototype._write = function _write(chunk, encoding, done) {
  this._deserializer.write(chunk, encoding, done);
};

// Error handling
// --------------

Endpoint.prototype._initializeErrorHandling = function _initializeErrorHandling() {
  this._serializer.on('error', this._error.bind(this, 'serializer'));
  this._deserializer.on('error', this._error.bind(this, 'deserializer'));
  this._compressor.on('error', this._error.bind(this, 'compressor'));
  this._decompressor.on('error', this._error.bind(this, 'decompressor'));
  this._connection.on('error', this._error.bind(this, 'connection'));
};

Endpoint.prototype._error = function _error(component, message) {
  this._log.fatal({ component: component, message: message }, 'Fatal error, closing connection');
  this.push(null);
  this.emit('error', component, message);
};
<|endoftext|>