<commit_before>"use strict";

const Utils = require("./color-utils").ColorUtils;

class Color {
  /**
   * Creates an instance of ColorContractCalc from an HSL value
   * @param {Array<number,number, number>} hsl - an array of numbers that represents an HSL value
   * @returns {Color} An instance of Color
   */
  static newHslColor(hsl) {
    return this.calc.getByHexCode(Utils.hslToHexCode(hsl));
  }

  /**
   * @private
   */
  static assignColorConstants() {
    /** @property {Color} BLACK - an instance that represents #000000 */
    this.BLACK = this.calc.HEX_TO_COLOR.get("#000000");
    /** @property {Color} WHITE - an instance that represents #ffffff */
    this.WHITE = this.calc.HEX_TO_COLOR.get("#ffffff");
    /** @property {Color} GRAY - an instance that represents #808080 */
    this.GRAY = this.calc.NAME_TO_COLOR.get("gray");
    this.prototype.BLACK = this.BLACK;
    this.prototype.WHITE = this.WHITE;
    this.prototype.GRAY = this.GRAY;
  }

  /**
   * @param {string|Array<number, number, number>} rgb - RGB value represented as a string (hex code) or an array of numbers
   * @param {string} [name=null] - the value of this.name: if null, the value of this.hexCode is set to this.name instead
   */
  constructor(rgb, name = null) {
    const calc = this.constructor.calc;
    /** @property {Array<number, number, number>} rgb - RGB value repsented as an array of decimal numbers */
    this.rgb = Utils.isString(rgb) ? Utils.hexCodeToDecimal(rgb) : rgb;
    /** @property {number} relativeLuminance - The relative luminance of the color */
    this.relativeLuminance = calc.relativeLuminance(this.rgb);
    /** @property {string} name - If no name is explicitely given, the property is set to the value of this.hexCode */
    this.name = name === null ? Utils.decimalToHexCode(this.rgb) : name;
    /** @property {string} hexCode - The RGB value in hex code notation */
    this.hexCode = Utils.decimalToHexCode(this.rgb);
    this.freezeProperties();
    /** @private */
    this._hsl = null;
  }

  /**
   * @property {Array<number, number, number>} hsl - HSL value repsented as an array of decimal numbers
   */
  get hsl() {
    if (this._hsl) { return this._hsl; }
    this._hsl = Utils.rgbToHsl(this.rgb);
    return this._hsl;
  }

  /**
   * Calculate the contrast ratio against another color
   * @param {Color|string|Array<number, number, number>} color - another instance of Color or a RGB value
   * @returns {number}
   */
  contrastRatioAgainst(color) {
    const calc = this.constructor.calc;

    if (!(color instanceof Color)) {
      return calc.contrastRatio(this.rgb, color);
    }

    return calc.luminanceToContrastRatio(this.relativeLuminance,
                                         color.relativeLuminance);
  }

  /**
   * @param {number} ratio - Value in percent
   * @param {string} [name=null] - Name of color
   * @returns {Color}
   */
  newContrastColor(ratio, name = null) {
    return this.generateNewColor(Utils.ContrastCalc, ratio, name);
  }

  /**
   * @param {number} ratio - Value in percent
   * @param {string} [name=null] - Name of color
   * @returns {Color}
   */
  newBrightnessColor(ratio, name = null) {
    return this.generateNewColor(Utils.BrightnessCalc, ratio, name);
  }

  /**
   * @param {number} ratio - Value in percent
   * @param {string} [name=null] - Name of color
   * @returns {Color}
   */
  newInvertColor(ratio, name = null) {
    return this.generateNewColor(Utils.InvertCalc, ratio, name);
  }

  /**
   * @param {number} degree - Value in degree
   * @param {string} [name=null] - Name of color
   * @returns {Color}
   */
  newHueRotateColor(degree, name = null) {
    return this.generateNewColor(Utils.HueRotateCalc, degree, name);
  }

  /**
   * @param {number} ratio - Value in percent
   * @param {string} [name=null] - Name of color
   * @returns {Color}
   */
  newSaturateColor(ratio, name = null) {
    return this.generateNewColor(Utils.SaturateCalc, ratio, name);
  }

  /**
   * @param {number} ratio - Value in percent
   * @param {string} [name=null] - Name of color
   * @returns {Color}
   */
  newGrayscaleColor(ratio, name = null) {
    return this.generateNewColor(Utils.GrayscaleCalc, ratio, name);
  }

  /**
   * Tries to find a color whose contrast against the base color is close to a given level.
   *
   * The returned color is gained by modifying the brightness of otherColor.
   * Even when a color that satisfies the level is not found, it returns a new color anyway.
   * @param {Color} otherColor - The color before the modification of brightness
   * @param {string} [level="AA"] - A, AA or AAA
   * @returns {Color} A color whose contrast against the base color is close to a specified level
   */
  findBrightnessThreshold(otherColor, level = "AA") {
    const targetRatio = this.levelToContrastRatio(level);
    const criteria = this.thresholdCriteria(targetRatio, otherColor);
    const w = otherColor.calcUpperRatioLimit() / 2;
    const upperColor = otherColor.newBrightnessColor(w * 2);

    if (otherColor.isBrighterThan(this) && ! upperColor.hasSufficientContrast(this, level)) {
      return upperColor;
    }

    const [r, lastSufficentRatio] = this.calcBrightnessRatio(otherColor, targetRatio, criteria, w);

    const nearestColor = otherColor.newBrightnessColor(criteria.round(r));

    if (lastSufficentRatio && nearestColor.contrastRatioAgainst(this) < targetRatio) {
      return otherColor.newBrightnessColor(criteria.round(lastSufficentRatio));
    }

    return nearestColor;
  }

  /**
   * Tries to find a color whose contrast against the base color is close to a given level.
   *
   * The returned color is gained by modifying the lightness of otherColor.
   * Even when a color that satisfies the level is not found, it returns a new color anyway.
   * @param {Color} otherColor - The color before the modification of lightness
   * @param {string} [level="AA"] - A, AA or AAA
   * @returns {Color} A color whose contrast against the base color is close to a specified level
   */
  findLightnessThreshold(otherColor, level = "AA") {
    const targetRatio = this.levelToContrastRatio(level);
    const criteria = this.thresholdCriteria(targetRatio, otherColor);
    const [h, s, initL] = Utils.rgbToHsl(otherColor.rgb);
    const [max, min] = this.shouldScanDarkerSide(otherColor) ? [initL, 0] : [100, initL];
    const boundaryColor = this.lightnessBoundaryColor(max, min, level);

    if (boundaryColor) { return boundaryColor; }

    let l = (max + min) / 2;
    let lastSufficientLightness = null;

    for (let d of Color.calc.binarySearchWidth(max - min, 0.01)) {
      let newColor = Utils.hslToRgb([h, s, l]);
      let contrastRatio = this.contrastRatioAgainst(newColor);

      if (contrastRatio >= targetRatio) { lastSufficientLightness = l; }
      if (contrastRatio === targetRatio) { break; }
      l += criteria.incrementCondition(contrastRatio) ? d : -d;
    }

    const nearlestColor = Color.newHslColor([h, s, l]);

    if (lastSufficientLightness && nearlestColor.contrastRatioAgainst(this) < targetRatio) {
      return Color.newHslColor([h, s, lastSufficientLightness]);
    }

    return nearlestColor;
  }

  /**
   * @private
   */
  shouldScanDarkerSide(otherColor) {
    if (this.isBrighterThan(otherColor) ||
        this.isSameColor(otherColor) && this.isLightColor()) {
      return true;
    }
    return false;
  }

  /**
   * @private
   */
  lightnessBoundaryColor(max, min, level) {
    if (min === 0 && ! this.hasSufficientContrast(this.BLACK, level)) {
      return this.BLACK;
    }

    if (max === 100 && ! this.hasSufficientContrast(this.WHITE, level)) {
      return this.WHITE;
    }

    return null;
  }

  /**
   * @param {Color} otherColor
   * @returns {string} A, AA or AAA if the contrast ratio meets the criteria of WCAG 2.0, otherwise "-"
   */
  contrastLevel(otherColor) {
    const ratio = this.contrastRatioAgainst(otherColor);
    if (ratio >= 7) {
      return "AAA";
    } else if (ratio >= 4.5) {
      return "AA";
    } else if (ratio >= 3) {
      return "A";
    }

    return "-";
  }

  /**
   * Checks if the contrast ratio between the base color and otherColor meets the requirement of WCAG 2.0
   * @param {Color} otherColor
   * @param {string} [level="AA"] - A, AA or AAA
   * @returns {boolean}
   */
  hasSufficientContrast(otherColor, level = "AA") {
    const ratio = this.levelToContrastRatio(level);
    return this.contrastRatioAgainst(otherColor) >= ratio;
  }

  /**
   * Checks if the base color and otherColor have the same RGB value
   * @param {Color} otherColor
   * @returns {boolean}
   */
  isSameColor(otherColor) {
    return this.hexCode === otherColor.hexCode;
  }

  /**
   * @returns {boolean} true if each primary color of the base color is 0 or 255
   */
  isMaxContrast() {
    const limits = [0, 255];
    return this.rgb.every(primaryColor => limits.includes(primaryColor));
  }

  /**
   * @returns {boolean} true if the hex code of the color is #808080
   */
  isMinContrast() {
    return this.rgb.every((primaryColor, i) => {
      return this.GRAY.rgb[i] === primaryColor;
    });
  }

  /**
   * Returns a string representation of the color.
   * When 16 is passed, it return the hex code, and when 10 is passed, it returns the value in RGB notation
   * Otherwise, it returns the color name or the hex code
   * @param {number|null} [base=16] - 16, 10 or null
   * @returns {string}
   */
  toString(base = 16) {
    switch (base) {
    case 16:
      return this.hexCode;
    case 10:
      return `rgb(${this.rgb.join(",")})`;
    default:
      return this.name || this.hexCode;
    }
  }

  /**
   * @private
   */
  levelToContrastRatio(level) {
    if (level === "A" || level === 1) {
      return 3.0;
    } else if (level === "AA" || level === 2) {
      return 4.5;
    } else if (level === "AAA" || level === 3) {
      return 7.0;
    }
  }

  /**
   * @private
   */
  calcBrightnessRatio(otherColor, targetRatio, criteria, w) {
    const otherRgb = otherColor.rgb;
    let r = w;
    let lastSufficentRatio = null;

    for (let d of Color.calc.binarySearchWidth(w, 0.01)) {
      const newRgb = Utils.BrightnessCalc.calcRgb(otherRgb, r);
      const contrastRatio = this.calcContrastRatio(newRgb);

      if (contrastRatio >= targetRatio) { lastSufficentRatio = r; }
      if (contrastRatio === targetRatio) { break; }
      r += criteria.incrementCondition(contrastRatio) ? d : -d;
    }

    return [r, lastSufficentRatio];
  }

  /**
   * @private
   */
  calcContrastRatio(otherRgb) {
    const otherLuminance = Color.calc.relativeLuminance(otherRgb);
    return Color.calc.luminanceToContrastRatio(this.relativeLuminance,
                                               otherLuminance);
  }

  /**
   * @private
   */
  calcUpperRatioLimit() {
    if (this.isSameColor(this.BLACK)) {
      return 100;
    }

    const darkest = this.rgb
            .filter(c => c !== 0)
            .reduce((a, b) => Math.min(a, b));
    return Math.ceil((255 / darkest) * 100);
  }

  /**
   * @private
   */
  thresholdCriteria(targetRatio, otherColor) {
    const criteria = {};

    if (this.isBrighterThan(otherColor) ||
        this.hasSameLuminance(otherColor) && this.isLightColor()) {
      criteria.round = (r) => Math.floor(r * 10 ) / 10;
      criteria.incrementCondition = (contrastRatio) => contrastRatio > targetRatio;
    } else {
      criteria.round = (r) => Math.ceil(r * 10) / 10;
      criteria.incrementCondition = (contrastRatio) => targetRatio > contrastRatio;
    }

    return criteria;
  }

  /**
   * @param {Color} otherColor
   * @returns {boolean} true if the relative luminance of the base color is greater than that of otherColor
   */
  isBrighterThan(otherColor) {
    return this.relativeLuminance > otherColor.relativeLuminance;
  }

  /**
   * @param {Color} otherColor
   * @returns {boolean} true if the relative luminance of the base color is equal to that of otherColor
   */
  hasSameLuminance(otherColor) {
    return this.relativeLuminance === otherColor.relativeLuminance;
  }

  /**
   * @returns {boolean} true if the contrast ratio against white is qual to/ less than the ratio against black
   */
  isLightColor() {
    return this.WHITE.contrastRatioAgainst(this) <= this.BLACK.contrastRatioAgainst(this);
  }

  /**
   * @private
   */
  freezeProperties() {
    Object.freeze(this.rgb);
    Object.freeze(this.relativeLuminance);
    Object.freeze(this.name);
    Object.freeze(this.hexCode);
  }

  /**
   * @private
   */
  generateNewColor(calc, ratio, name = null) {
    const newRgb = calc.calcRgb(this.rgb, ratio);
    return new Color(newRgb, name);
  }
}

module.exports.Color = Color;
<commit_msg>use static methods of ColorChecker instead of corresponding methods of ColorContrastCalc<commit_after>"use strict";

const Utils = require("./color-utils").ColorUtils;
const Checker = require("./color-checker").ColorChecker;

class Color {
  /**
   * Creates an instance of ColorContractCalc from an HSL value
   * @param {Array<number,number, number>} hsl - an array of numbers that represents an HSL value
   * @returns {Color} An instance of Color
   */
  static newHslColor(hsl) {
    return this.calc.getByHexCode(Utils.hslToHexCode(hsl));
  }

  /**
   * @private
   */
  static assignColorConstants() {
    /** @property {Color} BLACK - an instance that represents #000000 */
    this.BLACK = this.calc.HEX_TO_COLOR.get("#000000");
    /** @property {Color} WHITE - an instance that represents #ffffff */
    this.WHITE = this.calc.HEX_TO_COLOR.get("#ffffff");
    /** @property {Color} GRAY - an instance that represents #808080 */
    this.GRAY = this.calc.NAME_TO_COLOR.get("gray");
    this.prototype.BLACK = this.BLACK;
    this.prototype.WHITE = this.WHITE;
    this.prototype.GRAY = this.GRAY;
  }

  /**
   * @param {string|Array<number, number, number>} rgb - RGB value represented as a string (hex code) or an array of numbers
   * @param {string} [name=null] - the value of this.name: if null, the value of this.hexCode is set to this.name instead
   */
  constructor(rgb, name = null) {
    /** @property {Array<number, number, number>} rgb - RGB value repsented as an array of decimal numbers */
    this.rgb = Utils.isString(rgb) ? Utils.hexCodeToDecimal(rgb) : rgb;
    /** @property {number} relativeLuminance - The relative luminance of the color */
    this.relativeLuminance = Checker.relativeLuminance(this.rgb);
    /** @property {string} name - If no name is explicitely given, the property is set to the value of this.hexCode */
    this.name = name === null ? Utils.decimalToHexCode(this.rgb) : name;
    /** @property {string} hexCode - The RGB value in hex code notation */
    this.hexCode = Utils.decimalToHexCode(this.rgb);
    this.freezeProperties();
    /** @private */
    this._hsl = null;
  }

  /**
   * @property {Array<number, number, number>} hsl - HSL value repsented as an array of decimal numbers
   */
  get hsl() {
    if (this._hsl) { return this._hsl; }
    this._hsl = Utils.rgbToHsl(this.rgb);
    return this._hsl;
  }

  /**
   * Calculate the contrast ratio against another color
   * @param {Color|string|Array<number, number, number>} color - another instance of Color or a RGB value
   * @returns {number}
   */
  contrastRatioAgainst(color) {
    if (!(color instanceof Color)) {
      return Checker.contrastRatio(this.rgb, color);
    }

    return Checker.luminanceToContrastRatio(this.relativeLuminance,
                                         color.relativeLuminance);
  }

  /**
   * @param {number} ratio - Value in percent
   * @param {string} [name=null] - Name of color
   * @returns {Color}
   */
  newContrastColor(ratio, name = null) {
    return this.generateNewColor(Utils.ContrastCalc, ratio, name);
  }

  /**
   * @param {number} ratio - Value in percent
   * @param {string} [name=null] - Name of color
   * @returns {Color}
   */
  newBrightnessColor(ratio, name = null) {
    return this.generateNewColor(Utils.BrightnessCalc, ratio, name);
  }

  /**
   * @param {number} ratio - Value in percent
   * @param {string} [name=null] - Name of color
   * @returns {Color}
   */
  newInvertColor(ratio, name = null) {
    return this.generateNewColor(Utils.InvertCalc, ratio, name);
  }

  /**
   * @param {number} degree - Value in degree
   * @param {string} [name=null] - Name of color
   * @returns {Color}
   */
  newHueRotateColor(degree, name = null) {
    return this.generateNewColor(Utils.HueRotateCalc, degree, name);
  }

  /**
   * @param {number} ratio - Value in percent
   * @param {string} [name=null] - Name of color
   * @returns {Color}
   */
  newSaturateColor(ratio, name = null) {
    return this.generateNewColor(Utils.SaturateCalc, ratio, name);
  }

  /**
   * @param {number} ratio - Value in percent
   * @param {string} [name=null] - Name of color
   * @returns {Color}
   */
  newGrayscaleColor(ratio, name = null) {
    return this.generateNewColor(Utils.GrayscaleCalc, ratio, name);
  }

  /**
   * Tries to find a color whose contrast against the base color is close to a given level.
   *
   * The returned color is gained by modifying the brightness of otherColor.
   * Even when a color that satisfies the level is not found, it returns a new color anyway.
   * @param {Color} otherColor - The color before the modification of brightness
   * @param {string} [level="AA"] - A, AA or AAA
   * @returns {Color} A color whose contrast against the base color is close to a specified level
   */
  findBrightnessThreshold(otherColor, level = "AA") {
    const targetRatio = this.levelToContrastRatio(level);
    const criteria = this.thresholdCriteria(targetRatio, otherColor);
    const w = otherColor.calcUpperRatioLimit() / 2;
    const upperColor = otherColor.newBrightnessColor(w * 2);

    if (otherColor.isBrighterThan(this) && ! upperColor.hasSufficientContrast(this, level)) {
      return upperColor;
    }

    const [r, lastSufficentRatio] = this.calcBrightnessRatio(otherColor, targetRatio, criteria, w);

    const nearestColor = otherColor.newBrightnessColor(criteria.round(r));

    if (lastSufficentRatio && nearestColor.contrastRatioAgainst(this) < targetRatio) {
      return otherColor.newBrightnessColor(criteria.round(lastSufficentRatio));
    }

    return nearestColor;
  }

  /**
   * Tries to find a color whose contrast against the base color is close to a given level.
   *
   * The returned color is gained by modifying the lightness of otherColor.
   * Even when a color that satisfies the level is not found, it returns a new color anyway.
   * @param {Color} otherColor - The color before the modification of lightness
   * @param {string} [level="AA"] - A, AA or AAA
   * @returns {Color} A color whose contrast against the base color is close to a specified level
   */
  findLightnessThreshold(otherColor, level = "AA") {
    const targetRatio = this.levelToContrastRatio(level);
    const criteria = this.thresholdCriteria(targetRatio, otherColor);
    const [h, s, initL] = Utils.rgbToHsl(otherColor.rgb);
    const [max, min] = this.shouldScanDarkerSide(otherColor) ? [initL, 0] : [100, initL];
    const boundaryColor = this.lightnessBoundaryColor(max, min, level);

    if (boundaryColor) { return boundaryColor; }

    let l = (max + min) / 2;
    let lastSufficientLightness = null;

    for (let d of Color.calc.binarySearchWidth(max - min, 0.01)) {
      let newColor = Utils.hslToRgb([h, s, l]);
      let contrastRatio = this.contrastRatioAgainst(newColor);

      if (contrastRatio >= targetRatio) { lastSufficientLightness = l; }
      if (contrastRatio === targetRatio) { break; }
      l += criteria.incrementCondition(contrastRatio) ? d : -d;
    }

    const nearlestColor = Color.newHslColor([h, s, l]);

    if (lastSufficientLightness && nearlestColor.contrastRatioAgainst(this) < targetRatio) {
      return Color.newHslColor([h, s, lastSufficientLightness]);
    }

    return nearlestColor;
  }

  /**
   * @private
   */
  shouldScanDarkerSide(otherColor) {
    if (this.isBrighterThan(otherColor) ||
        this.isSameColor(otherColor) && this.isLightColor()) {
      return true;
    }
    return false;
  }

  /**
   * @private
   */
  lightnessBoundaryColor(max, min, level) {
    if (min === 0 && ! this.hasSufficientContrast(this.BLACK, level)) {
      return this.BLACK;
    }

    if (max === 100 && ! this.hasSufficientContrast(this.WHITE, level)) {
      return this.WHITE;
    }

    return null;
  }

  /**
   * @param {Color} otherColor
   * @returns {string} A, AA or AAA if the contrast ratio meets the criteria of WCAG 2.0, otherwise "-"
   */
  contrastLevel(otherColor) {
    const ratio = this.contrastRatioAgainst(otherColor);
    if (ratio >= 7) {
      return "AAA";
    } else if (ratio >= 4.5) {
      return "AA";
    } else if (ratio >= 3) {
      return "A";
    }

    return "-";
  }

  /**
   * Checks if the contrast ratio between the base color and otherColor meets the requirement of WCAG 2.0
   * @param {Color} otherColor
   * @param {string} [level="AA"] - A, AA or AAA
   * @returns {boolean}
   */
  hasSufficientContrast(otherColor, level = "AA") {
    const ratio = this.levelToContrastRatio(level);
    return this.contrastRatioAgainst(otherColor) >= ratio;
  }

  /**
   * Checks if the base color and otherColor have the same RGB value
   * @param {Color} otherColor
   * @returns {boolean}
   */
  isSameColor(otherColor) {
    return this.hexCode === otherColor.hexCode;
  }

  /**
   * @returns {boolean} true if each primary color of the base color is 0 or 255
   */
  isMaxContrast() {
    const limits = [0, 255];
    return this.rgb.every(primaryColor => limits.includes(primaryColor));
  }

  /**
   * @returns {boolean} true if the hex code of the color is #808080
   */
  isMinContrast() {
    return this.rgb.every((primaryColor, i) => {
      return this.GRAY.rgb[i] === primaryColor;
    });
  }

  /**
   * Returns a string representation of the color.
   * When 16 is passed, it return the hex code, and when 10 is passed, it returns the value in RGB notation
   * Otherwise, it returns the color name or the hex code
   * @param {number|null} [base=16] - 16, 10 or null
   * @returns {string}
   */
  toString(base = 16) {
    switch (base) {
    case 16:
      return this.hexCode;
    case 10:
      return `rgb(${this.rgb.join(",")})`;
    default:
      return this.name || this.hexCode;
    }
  }

  /**
   * @private
   */
  levelToContrastRatio(level) {
    if (level === "A" || level === 1) {
      return 3.0;
    } else if (level === "AA" || level === 2) {
      return 4.5;
    } else if (level === "AAA" || level === 3) {
      return 7.0;
    }
  }

  /**
   * @private
   */
  calcBrightnessRatio(otherColor, targetRatio, criteria, w) {
    const otherRgb = otherColor.rgb;
    let r = w;
    let lastSufficentRatio = null;

    for (let d of Color.calc.binarySearchWidth(w, 0.01)) {
      const newRgb = Utils.BrightnessCalc.calcRgb(otherRgb, r);
      const contrastRatio = this.calcContrastRatio(newRgb);

      if (contrastRatio >= targetRatio) { lastSufficentRatio = r; }
      if (contrastRatio === targetRatio) { break; }
      r += criteria.incrementCondition(contrastRatio) ? d : -d;
    }

    return [r, lastSufficentRatio];
  }

  /**
   * @private
   */
  calcContrastRatio(otherRgb) {
    const otherLuminance = Checker.relativeLuminance(otherRgb);
    return Checker.luminanceToContrastRatio(this.relativeLuminance,
                                               otherLuminance);
  }

  /**
   * @private
   */
  calcUpperRatioLimit() {
    if (this.isSameColor(this.BLACK)) {
      return 100;
    }

    const darkest = this.rgb
            .filter(c => c !== 0)
            .reduce((a, b) => Math.min(a, b));
    return Math.ceil((255 / darkest) * 100);
  }

  /**
   * @private
   */
  thresholdCriteria(targetRatio, otherColor) {
    const criteria = {};

    if (this.isBrighterThan(otherColor) ||
        this.hasSameLuminance(otherColor) && this.isLightColor()) {
      criteria.round = (r) => Math.floor(r * 10 ) / 10;
      criteria.incrementCondition = (contrastRatio) => contrastRatio > targetRatio;
    } else {
      criteria.round = (r) => Math.ceil(r * 10) / 10;
      criteria.incrementCondition = (contrastRatio) => targetRatio > contrastRatio;
    }

    return criteria;
  }

  /**
   * @param {Color} otherColor
   * @returns {boolean} true if the relative luminance of the base color is greater than that of otherColor
   */
  isBrighterThan(otherColor) {
    return this.relativeLuminance > otherColor.relativeLuminance;
  }

  /**
   * @param {Color} otherColor
   * @returns {boolean} true if the relative luminance of the base color is equal to that of otherColor
   */
  hasSameLuminance(otherColor) {
    return this.relativeLuminance === otherColor.relativeLuminance;
  }

  /**
   * @returns {boolean} true if the contrast ratio against white is qual to/ less than the ratio against black
   */
  isLightColor() {
    return this.WHITE.contrastRatioAgainst(this) <= this.BLACK.contrastRatioAgainst(this);
  }

  /**
   * @private
   */
  freezeProperties() {
    Object.freeze(this.rgb);
    Object.freeze(this.relativeLuminance);
    Object.freeze(this.name);
    Object.freeze(this.hexCode);
  }

  /**
   * @private
   */
  generateNewColor(calc, ratio, name = null) {
    const newRgb = calc.calcRgb(this.rgb, ratio);
    return new Color(newRgb, name);
  }
}

module.exports.Color = Color;
<|endoftext|>


<commit_before>'use strict';

var bin = require('./optipng').bin;
var chalk = require('chalk');

bin.check(function (w) {
    if (!w) {
        console.log(chalk.red('✗ pre-build test failed, compiling from source...'));

        if (process.platform === 'win32') {
            throw new Error('building is not supported on ' + process.platform);
        }

        return bin.build(function (err) {
            if (err) {
                return console.log(chalk.red('✗ ' + err.message));
            }

            console.log(chalk.green('✓ optipng rebuilt successfully'));
        });
    }

    console.log(chalk.green('✓ pre-build test passed successfully'));
});
<commit_msg>Don't throw error on Windows<commit_after>'use strict';

var bin = require('./optipng').bin;
var chalk = require('chalk');

bin.check(function (w) {
	if (!w) {
		console.log(chalk.red('✗ pre-build test failed, compiling from source...'));

		if (process.platform === 'win32') {
			return console.log(chalk.red('✗ building is not supported on ' + process.platform));
		}

		return bin.build(function (err) {
			if (err) {
				return console.log(chalk.red('✗ ' + err.message));
			}

			console.log(chalk.green('✓ optipng rebuilt successfully'));
		});
	}

	console.log(chalk.green('✓ pre-build test passed successfully'));
});
<|endoftext|>


<commit_before>const events = require('events')
const path = require('path')
const fs = require('fs')
const zlib = require('zlib')
const request = require('request')
const Stream = require('stream')
const CombinedStream = require('combined-stream')

const util = require('./util')
const MIMES = require('./mime-types')

const RDLMap = require('./readline-cachemap')

const confSymbol = Symbol('combo#conf')

/**
 *  Combo(opts)
 *      .static(root,prefix)
 *      .prefix(`Mixed String|Array`)
 *      .tag(`String`)
 *      .charset(`String`)
 *      .mini(`Boolean`)
 *      .gzip(`Boolean`)
 *      .maxAge(`Number`)
 *      .cacheControl(`Mixed String|REST `)
 *      .remoteMap(url,realhost)
 *      .hooks(uripath,dir,allow_ext,cat)
 *      .transform(`Mixed String|Array`)
 *      .sourcemap(`Mixed String|Array`)
 *      .middlewares()
 */

module.exports = class Combo extends events.EventEmitter {

    constructor(options) {
        super(options)
        // this.options = options || {}
        this[confSymbol] = options || {}
    }

    async _streamOfStatic(filepath, ctx) {

        let fileStat = await util.fileStatAsync(filepath)

        if (!fileStat) {
            ctx.status = 404
            return false
        }

        let lastModified = fileStat.mtime.toUTCString()

        //判断资源重复
        if (ctx.headers['if-modified-since'] && lastModified === ctx.headers['if-modified-since']) {
            ctx.status = 304
            return false
        }

        let conf = this[confSymbol]

        let mtime = fileStat.mtime.getTime().toString(16)
        let size = fileStat.size.toString(16)

        let ext = path.extname(filepath)
        ext = ext ? ext.slice(1) : 'unknown'

        ctx.type = MIMES[ext]
        ctx.lastModified = lastModified
        ctx.length = fileStat.size
        ctx.etag = size + '-' + mtime
        ctx.etag = conf.isweak ? ('W/' + ctx.etag) : ctx.etag

        let rStream = fs.createReadStream(filepath)

        ctx.set('cache-control', conf.cacheControl || ('public , max-age=' + conf.maxAge))
        ctx.set('expires', new Date(Date.now() + conf.maxAge * 1000).toUTCString())

        if (conf.gzip) {
            ctx.vary('Accept-Encoding')
            ctx.remove('content-length')
            ctx.set('content-encoding', 'gzip')
            return rStream.pipe(zlib.createGzip())
        }
        return rStream
    }

    async _streamOfCombo(ctx, waitingComboFilesPath) {

        let self = this,
            conf = self[confSymbol]

        let
            regMatches,
            filepath,
            fileStat,
            fileStats = [],
            combinedStream = CombinedStream.create(),
            remoteStream,
            transformEngine,
            rStream,
            outputStream,
            cache_combo_file_ext,
            streams = []

        let len = waitingComboFilesPath.length

        let hookItem = conf.hooks[ctx.path.slice(1)]

        if (!hookItem) {

            for (; len--;) {

                waitingComboFilesPath[len] = path.normalize(waitingComboFilesPath[len])

                filepath = path.join(conf.root, waitingComboFilesPath[len])

                fileStat = await util.fileStatAsync(filepath)

                if (!fileStat) {
                    self.emit('warn-miss-file', waitingComboFilesPath[len], filepath)
                    continue
                }

                rStream = fs.createReadStream(filepath)

                transformEngine = self.getTransform(path.extname(filepath).substring(1))

                if (transformEngine) {
                    rStream = rStream.pipe(transformEngine.factory(transformEngine.opts, transformEngine.context))
                }

                streams.push(rStream)

                fileStats.push(fileStat)
            }

        } else {

            let remoteUrl = ''

            for (; len--;) {

                waitingComboFilesPath[len] = path.normalize(waitingComboFilesPath[len])

                regMatches = waitingComboFilesPath[len].match(hookItem.filterReg)

                if (!regMatches) continue

                cache_combo_file_ext = util.judgeRealExt(regMatches[3])

                if (regMatches[1]) { //远程地址

                    filepath = hookItem.realpath(regMatches[2], '.' + regMatches[3], regMatches[1], conf.remoteMap)

                    if (!filepath) continue

                    if (util.isArray(filepath)) {
                        remoteUrl = filepath[0]
                        filepath = filepath[1]
                    } else {
                        remoteUrl = filepath
                    }

                    if (!remoteUrl) continue

                    if (filepath) {
                        fileStat = await util.fileStatAsync(filepath)
                        if (!fileStat) {
                            //如果是提供了本地缓存地址,但是没有找到的情况
                            await util.mkdir(path.dirname(filepath))
                        }
                    } else {
                        //自定义创建目录
                        await util.mkdir(path.join(conf.root, regMatches[1]))
                    }

                    remoteStream = request(remoteUrl)

                    streams.push(remoteStream)

                    if (conf.remoteCache) {

                        remoteStream.pipe(
                            new Stream.PassThrough().pipe(
                                fs.createWriteStream(filepath)
                            )
                        )
                    }

                    continue

                } else {
                    filepath = hookItem.realpath(regMatches[2], '.' + regMatches[3])
                    fileStat = await util.fileStatAsync(filepath)
                }

                if (!fileStat) continue

                rStream = fs.createReadStream(filepath)

                transformEngine = self.getTransform(regMatches[3])

                if (transformEngine) {

                    transformEngine = transformEngine.factory(transformEngine.opts, transformEngine.context)

                    transformEngine.curFilePath = filepath

                    rStream = rStream.pipe(transformEngine)
                }

                streams.push(rStream)

                fileStats.push(fileStat)
            }

        }

        if (streams.length === 1) {
            outputStream = streams[0]
        } else {
            streams.forEach(streamItem => {
                combinedStream.append(streamItem)
            })
            outputStream = combinedStream
        }

        if (conf.mini) {
            let miniEngine,
                i = 0,
                hookEvents = ['before-mini', 'on-mini', 'after-mini']

            while (i <= hookEvents.length) {
                miniEngine = conf.hooks[path.basename(ctx.path)][hookEvents[i++]]
                if (miniEngine) {
                    transformEngine = self.getTransform(miniEngine)
                    if (transformEngine) {
                        outputStream = outputStream.pipe(transformEngine[2](transformEngine[1]))
                    }
                }
            }
        }

        if (!conf.debug) {

            let diskpath = path.join(hookItem ? (hookItem.dir || conf.root) : conf.root, util.getCombodFileName(fileStats) + '.' + (cache_combo_file_ext || 'combo'))

            let cacheFileName = waitingComboFilesPath.join(',')

            RDLMap.init().storeCache(cacheFileName, diskpath)

            outputStream.pipe(
                new Stream.PassThrough().pipe(
                    fs.createWriteStream(diskpath, {
                        encoding: conf.charset
                    })
                )
            )

            outputStream.on('end', function () {
                self.__cache[cacheFileName] = diskpath
            })
        }

        ctx.type = MIMES[cache_combo_file_ext || 'unknow']

        if (conf.gzip) {
            ctx.vary('Accept-Encoding')
            ctx.remove('content-length')
            ctx.set('content-encoding', 'gzip')
            outputStream = outputStream.pipe(zlib.createGzip())
        }

        return outputStream
    }

    /**
     * 获取指定后缀名的转换刘引擎
     *
     * @param {any} ext 文件后缀名(不含.)
     * @returns {Hash} 转换流引擎
     */
    getTransform(ext) {

        let transforms = this[confSymbol].transform

        let transform = (ext === 'minjs' || ext === 'mincss' || ext === 'minhtml') ? transforms[ext] : transforms['.' + ext]

        return transform
    }

    /**
     * 自定义|重置  转换流引擎
     *
     * @param {any} options 配置项
     *
     *  * `name`: [`String`] 转换流的名称
     *  * `ext`:  [`String` | `Array`] 转换流支持的文件后缀名称
     *  * `opts`: [`Hash`] 转换流引擎需要的初始化配置项
     *  * `context`: [`Hash`] 转换流引擎运作需要的上下文数据
     *  * `factory`: [`Function`] 转换流引擎的构造工厂
     *
     * @returns {Combo} 当前方法所属作用域(链式调用)
     */
    transform(options) {

        if (!options || !options.ext || !options.name) return this

        let conf = this[confSymbol]

        let curTransforms = conf.transform || (conf.transform = {})

        let ext = Array.isArray(options.ext) ? options.ext : [options.ext]

        ext.forEach(item => {

            let curEngine = curTransforms[item]

            if (!curEngine) { //重新定义一个转换流
                //判断合法性
                if (options.factory && util.isFunction(options.factory)) {
                    curTransforms[item] = options
                }
                //[engineName, engineOptions, engineFactory]
            } else if (curEngine) {

                //如果是内置的转换引擎,则需要判断engineOptions和engineFactory的可用性
                if (options.factory && util.isFunction(options.factory)) {
                    curEngine.factory = options.factory
                }

                options.opts && (curEngine.opts = Object.assign({}, curEngine.opts, options.opts))

                options.context && (curEngine.context = options.context)
            }
        })

        return this

    }

    /**
     * 解析combo请求的文件列表时的自定义钩子
     *  用于获取每个文件的实际地址
     *
     * @param {String} comboPrefix combo合并前缀(与`prefix选项对应`)
     * @param {Object} options 此前缀对应的选项
     *  `options.dir`: [`String`] 前缀对应的物理路径
     *  `options.allow_ext`:[`Array`] 此前缀允许访问的文件后缀
     *  `options.realpath`:[`Function`] 实际地址获取的回调方法
     * @returns  {Combo} this 当前对象实例
     * @memberof Combo
     */
    hooks(comboPrefix, options) {

        let conf = this[confSymbol]

        if (!conf.prefix || !conf.prefix.length) return this

        if (!conf.prefix.includes(comboPrefix)) {
            conf.prefix.push(comboPrefix)
        }

        !conf.hooks && (conf.hooks = {})

        let hookItem = conf.hooks[comboPrefix] = options || {}

        if (!options.realpath) {
            options.realpath = (filename, ext) => {
                return path.join(options.dir, filename + (ext[0] === '.' ? ext : ('.' + ext)))
            }
        }

        if (options.allow_ext) {
            /**
             * match[1]: domain
             * match[2]: filename
             * match[3]: ext
             * match[4]: file.query --- ?v=1
             * match[5]: version
             */
            hookItem.filterReg = new RegExp('(.+\\/)?(.+)\\.(' + options.allow_ext.join('|') + ')(\\?v=(.+))?')
        }
        return this
    }
}
<commit_msg>bugs: 1. 静态文件带请求参数`?`或`#`的文件无法加载;<commit_after>const events = require('events')
const path = require('path')
const fs = require('fs')
const url = require('url')
const zlib = require('zlib')
const request = require('request')
const Stream = require('stream')
const CombinedStream = require('combined-stream')

const util = require('./util')
const MIMES = require('./mime-types')

const RDLMap = require('./readline-cachemap')

const confSymbol = Symbol('combo#conf')

/**
 *  Combo(opts)
 *      .static(root,prefix)
 *      .prefix(`Mixed String|Array`)
 *      .tag(`String`)
 *      .charset(`String`)
 *      .mini(`Boolean`)
 *      .gzip(`Boolean`)
 *      .maxAge(`Number`)
 *      .cacheControl(`Mixed String|REST `)
 *      .remoteMap(url,realhost)
 *      .hooks(uripath,dir,allow_ext,cat)
 *      .transform(`Mixed String|Array`)
 *      .sourcemap(`Mixed String|Array`)
 *      .middlewares()
 */

module.exports = class Combo extends events.EventEmitter {

    constructor(options) {
        super(options)
        // this.options = options || {}
        this[confSymbol] = options || {}
    }

    async _streamOfStatic(filepath, ctx) {

        filepath = url.parse(filepath)

        filepath = filepath.pathname

        let fileStat = await util.fileStatAsync(filepath)

        if (!fileStat) {
            ctx.status = 404
            return false
        }

        let lastModified = fileStat.mtime.toUTCString()

        //判断资源重复
        if (ctx.headers['if-modified-since'] && lastModified === ctx.headers['if-modified-since']) {
            ctx.status = 304
            return false
        }

        let conf = this[confSymbol]

        let mtime = fileStat.mtime.getTime().toString(16)
        let size = fileStat.size.toString(16)

        let ext = path.extname(filepath)
        ext = ext ? ext.slice(1) : 'unknown'

        ctx.type = MIMES[ext]
        ctx.lastModified = lastModified
        ctx.length = fileStat.size
        ctx.etag = size + '-' + mtime
        ctx.etag = conf.isweak ? ('W/' + ctx.etag) : ctx.etag

        let rStream = fs.createReadStream(filepath)

        ctx.set('cache-control', conf.cacheControl || ('public , max-age=' + conf.maxAge))
        ctx.set('expires', new Date(Date.now() + conf.maxAge * 1000).toUTCString())

        if (conf.gzip) {
            ctx.vary('Accept-Encoding')
            ctx.remove('content-length')
            ctx.set('content-encoding', 'gzip')
            return rStream.pipe(zlib.createGzip())
        }
        return rStream
    }

    async _streamOfCombo(ctx, waitingComboFilesPath) {

        let self = this,
            conf = self[confSymbol]

        let
            regMatches,
            filepath,
            fileStat,
            fileStats = [],
            combinedStream = CombinedStream.create(),
            remoteStream,
            transformEngine,
            rStream,
            outputStream,
            cache_combo_file_ext,
            streams = []

        let len = waitingComboFilesPath.length

        let hookItem = conf.hooks[ctx.path.slice(1)]

        if (!hookItem) {

            for (; len--;) {

                waitingComboFilesPath[len] = path.normalize(waitingComboFilesPath[len])

                filepath = path.join(conf.root, waitingComboFilesPath[len])

                fileStat = await util.fileStatAsync(filepath)

                if (!fileStat) {
                    self.emit('warn-miss-file', waitingComboFilesPath[len], filepath)
                    continue
                }

                rStream = fs.createReadStream(filepath)

                transformEngine = self.getTransform(path.extname(filepath).substring(1))

                if (transformEngine) {
                    rStream = rStream.pipe(transformEngine.factory(transformEngine.opts, transformEngine.context))
                }

                streams.push(rStream)

                fileStats.push(fileStat)
            }

        } else {

            let remoteUrl = ''

            for (; len--;) {

                waitingComboFilesPath[len] = path.normalize(waitingComboFilesPath[len])

                regMatches = waitingComboFilesPath[len].match(hookItem.filterReg)

                if (!regMatches) continue

                cache_combo_file_ext = util.judgeRealExt(regMatches[3])

                if (regMatches[1]) { //远程地址

                    filepath = hookItem.realpath(regMatches[2], '.' + regMatches[3], regMatches[1], conf.remoteMap)

                    if (!filepath) continue

                    if (util.isArray(filepath)) {
                        remoteUrl = filepath[0]
                        filepath = filepath[1]
                    } else {
                        remoteUrl = filepath
                    }

                    if (!remoteUrl) continue

                    if (filepath) {
                        fileStat = await util.fileStatAsync(filepath)
                        if (!fileStat) {
                            //如果是提供了本地缓存地址,但是没有找到的情况
                            await util.mkdir(path.dirname(filepath))
                        }
                    } else {
                        //自定义创建目录
                        await util.mkdir(path.join(conf.root, regMatches[1]))
                    }

                    remoteStream = request(remoteUrl)

                    streams.push(remoteStream)

                    if (conf.remoteCache) {

                        remoteStream.pipe(
                            new Stream.PassThrough().pipe(
                                fs.createWriteStream(filepath)
                            )
                        )
                    }

                    continue

                } else {
                    filepath = hookItem.realpath(regMatches[2], '.' + regMatches[3])
                    fileStat = await util.fileStatAsync(filepath)
                }

                if (!fileStat) continue

                rStream = fs.createReadStream(filepath)

                transformEngine = self.getTransform(regMatches[3])

                if (transformEngine) {

                    transformEngine = transformEngine.factory(transformEngine.opts, transformEngine.context)

                    transformEngine.curFilePath = filepath

                    rStream = rStream.pipe(transformEngine)
                }

                streams.push(rStream)

                fileStats.push(fileStat)
            }

        }

        if (streams.length === 1) {
            outputStream = streams[0]
        } else {
            streams.forEach(streamItem => {
                combinedStream.append(streamItem)
            })
            outputStream = combinedStream
        }

        if (conf.mini) {
            let miniEngine,
                i = 0,
                hookEvents = ['before-mini', 'on-mini', 'after-mini']

            while (i <= hookEvents.length) {
                miniEngine = conf.hooks[path.basename(ctx.path)][hookEvents[i++]]
                if (miniEngine) {
                    transformEngine = self.getTransform(miniEngine)
                    if (transformEngine) {
                        outputStream = outputStream.pipe(transformEngine[2](transformEngine[1]))
                    }
                }
            }
        }

        if (!conf.debug) {

            let diskpath = path.join(hookItem ? (hookItem.dir || conf.root) : conf.root, util.getCombodFileName(fileStats) + '.' + (cache_combo_file_ext || 'combo'))

            let cacheFileName = waitingComboFilesPath.join(',')

            RDLMap.init().storeCache(cacheFileName, diskpath)

            outputStream.pipe(
                new Stream.PassThrough().pipe(
                    fs.createWriteStream(diskpath, {
                        encoding: conf.charset
                    })
                )
            )

            outputStream.on('end', function () {
                self.__cache[cacheFileName] = diskpath
            })
        }

        ctx.type = MIMES[cache_combo_file_ext || 'unknow']

        if (conf.gzip) {
            ctx.vary('Accept-Encoding')
            ctx.remove('content-length')
            ctx.set('content-encoding', 'gzip')
            outputStream = outputStream.pipe(zlib.createGzip())
        }

        return outputStream
    }

    /**
     * 获取指定后缀名的转换刘引擎
     *
     * @param {any} ext 文件后缀名(不含.)
     * @returns {Hash} 转换流引擎
     */
    getTransform(ext) {

        let transforms = this[confSymbol].transform

        let transform = (ext === 'minjs' || ext === 'mincss' || ext === 'minhtml') ? transforms[ext] : transforms['.' + ext]

        return transform
    }

    /**
     * 自定义|重置  转换流引擎
     *
     * @param {any} options 配置项
     *
     *  * `name`: [`String`] 转换流的名称
     *  * `ext`:  [`String` | `Array`] 转换流支持的文件后缀名称
     *  * `opts`: [`Hash`] 转换流引擎需要的初始化配置项
     *  * `context`: [`Hash`] 转换流引擎运作需要的上下文数据
     *  * `factory`: [`Function`] 转换流引擎的构造工厂
     *
     * @returns {Combo} 当前方法所属作用域(链式调用)
     */
    transform(options) {

        if (!options || !options.ext || !options.name) return this

        let conf = this[confSymbol]

        let curTransforms = conf.transform || (conf.transform = {})

        let ext = Array.isArray(options.ext) ? options.ext : [options.ext]

        ext.forEach(item => {

            let curEngine = curTransforms[item]

            if (!curEngine) { //重新定义一个转换流
                //判断合法性
                if (options.factory && util.isFunction(options.factory)) {
                    curTransforms[item] = options
                }
                //[engineName, engineOptions, engineFactory]
            } else if (curEngine) {

                //如果是内置的转换引擎,则需要判断engineOptions和engineFactory的可用性
                if (options.factory && util.isFunction(options.factory)) {
                    curEngine.factory = options.factory
                }

                options.opts && (curEngine.opts = Object.assign({}, curEngine.opts, options.opts))

                options.context && (curEngine.context = options.context)
            }
        })

        return this

    }

    /**
     * 解析combo请求的文件列表时的自定义钩子
     *  用于获取每个文件的实际地址
     *
     * @param {String} comboPrefix combo合并前缀(与`prefix选项对应`)
     * @param {Object} options 此前缀对应的选项
     *  `options.dir`: [`String`] 前缀对应的物理路径
     *  `options.allow_ext`:[`Array`] 此前缀允许访问的文件后缀
     *  `options.realpath`:[`Function`] 实际地址获取的回调方法
     * @returns  {Combo} this 当前对象实例
     * @memberof Combo
     */
    hooks(comboPrefix, options) {

        let conf = this[confSymbol]

        if (!conf.prefix || !conf.prefix.length) return this

        if (!conf.prefix.includes(comboPrefix)) {
            conf.prefix.push(comboPrefix)
        }

        !conf.hooks && (conf.hooks = {})

        let hookItem = conf.hooks[comboPrefix] = options || {}

        if (!options.realpath) {
            options.realpath = (filename, ext) => {
                return path.join(options.dir, filename + (ext[0] === '.' ? ext : ('.' + ext)))
            }
        }

        if (options.allow_ext) {
            /**
             * match[1]: domain
             * match[2]: filename
             * match[3]: ext
             * match[4]: file.query --- ?v=1
             * match[5]: version
             */
            hookItem.filterReg = new RegExp('(.+\\/)?(.+)\\.(' + options.allow_ext.join('|') + ')(\\?v=(.+))?')
        }
        return this
    }
}
<|endoftext|>


<commit_before>var _ = require('lodash');
var chalk = require('chalk');
var EventEmitter = require('events');
var fs = require('fs');
var glob = require('glob');
var mkdirp = require('mkdirp');
var Nightmare = require('nightmare');
var path = require('path');
var Promise = require('bluebird');
var relToAbs = require('rel-to-abs');
var rimraf = require('rimraf');
var sanitizeFilename = require('sanitize-filename');
var slash = require('slash');
var sitemap = require('sitemap');
var url = require('url');
var util = require('util');

Promise.promisifyAll(fs);
var mkdirpAsync = Promise.promisify(mkdirp);
var rimrafAsync = Promise.promisify(rimraf);

/**
 *
 * @return {Cordy}
 */
function Cordy() {
  EventEmitter.call(this);
}

util.inherits(Cordy, EventEmitter);

Cordy.prototype = {
  constructor: Cordy,
  /**
   * Builds cache.
   *
   * @param  {Object} options
   * @return {Promise}
   */
  buildCache: function (options) {
    var defaultSitemapOptions = {
      baseURL: undefined
    };

    var defaultOptions = {
      cleanDest: false,
      urls: [],
      dest: undefined,
      sitemap: defaultSitemapOptions
    };

    if (_.isNil(options)) {
      options = defaultOptions;
    }
    else {
      _.defaults(options, defaultOptions);
    }

    if (_.isNil(options.sitemap)) {
      options.sitemap = defaultSitemapOptions;
    }
    else {
      _.defaults(options.sitemap, defaultSitemapOptions);
    }

    if (_.isNil(options.dest)) {
      return Promise.reject('buildCache() `options.dest` is required');
    }

    var self = this;

    // Build array of values for mapper function.
    var values = [];

    _.forEach(options.urls, function (url) {
      values.push({
        src: url.url,
        baseURL: url.baseURL,
        auth: url.auth,
        dest: options.dest
      });
    });

    // Fetches and saves HTML of all URLs, and builds and saves sitemap.
    var alwaysFn = function () {
      // Create destination directory.
      return mkdirpAsync(options.dest)
        // Save HTML.
        .then(function () {
          // Run map function on all URLs. Note that map loop exits on first rejection.
          return Promise.map(values, function (options) {
            var auth = options.auth;
            var src = options.src;
            var baseURL = options.baseURL;
            var nightmareOptions = options.nightmare;
            var dest = options.dest;

            var saveHTMLFn = function () {
              return self.saveHTML(src, nightmareOptions, baseURL, dest);
            };

            if (_.isFunction(auth)) {
              return auth()
                .then(saveHTMLFn);
            }

            return saveHTMLFn();
          }, {
            concurrency: 1
          });
        })
        // Create sitemap.
        .then(function () {
          return self.saveSitemap(options.dest + '/**/*.html', null, options.sitemap, options.dest + '/sitemap.xml');
        });
    };

    if (options.cleanDest) {
      return this.cleanDirectory(options.dest)
        .then(alwaysFn);
    }

    return alwaysFn();
  },
  /**
   * Fetches HTML for a given URL, and writes HTML to file.
   *
   * @param  {String} src
   * @param  {Object} nightmareOptions
   * @return {Promise}
   */
  getHTML: function (src, nightmareOptions) {
    if (_.isNil(src)) {
      return Promise.reject('getHTML() `src` is required');
    }

    return this.runTasks([
      ['goto', src],
      ['evaluate', function () {
        return new XMLSerializer().serializeToString(document);
      }]
    ], nightmareOptions);
  },
  /**
   * Fetches HTML for a given URL, and writes HTML to file.
   *
   * @param  {String} src
   * @param  {Object} nightmareOptions
   * @param  {String} baseURL
   * @param  {String} dest
   * @return {Promise}
   */
  saveHTML: function (src, nightmareOptions, baseURL, dest) {
    if (_.isNil(src)) {
      return Promise.reject('saveHTML() `src` is required');
    }
    else if (_.isNil(dest)) {
      return Promise.reject('saveHTML() `dest` is required');
    }

    var self = this;
    var file = dest + '/' + this.buildFilenameFromURI(src);
    var srcParsed = url.parse(src);

    if (_.isNil(baseURL)) {
      baseURL = url.format({
        host: srcParsed.host,
        protocol: srcParsed.protocol
      });
    }

    return this.getHTML(src, nightmareOptions)
      .then(function (data) {
        data = self.relToAbs(data, baseURL);
        return self.writeFile(file, data);
      });
  },
  /**
   * Runs the given Nightmare tasks.
   *
   * @param  {Array} tasks
   * @param  {Object} options
   * @return {Promise}
   */
  runTasks: function (tasks, options) {
    var self = this;

    this.emit('runTasks', tasks);

    return new Promise(function (resolve, reject) {
      var flags = {
        error: false // Whether one or more tasks threw an error
      };
      var gotoStack = [];
      var nightmare = Nightmare(options);

      nightmare.on('did-get-response-details', function (event, status, newURL, originalURL, httpResponseCode, requestMethod, referrer, headers) {
        // Handle HTTP errors thrown by goto tasks
        var gotoIndex = _.findIndex(gotoStack, function (o) {
          return newURL === o.url && false === o.flags.run;
        });

        if (gotoIndex > -1) {
          gotoStack[gotoIndex].flags.run = true;
          gotoStack[gotoIndex].httpResponseCode = httpResponseCode;

          // Client (400) or server (500) error
          if (httpResponseCode >= 400) {
            flags.error = true;
            self.emit('httpError', newURL, httpResponseCode);
          }
        }
      });

      _.forEach(tasks, function (value, index) {
        var task = value[0];
        var args = _.slice(value, 1);

        if ('goto' === task) {
          gotoStack.push({
            url: args[0],
            flags: {
              run: false
            },
            httpResponseCode: undefined
          });
        }

        nightmare[task].apply(nightmare, args);
      });

      return nightmare.end().then(function () {
        if (flags.error) {
          reject();
        }
        else {
          resolve.apply(nightmare, arguments);
        }
      }, reject);
    });
  },
  /**
   * Returns a filename from a given URI without unsafe characters.
   *
   * @param  {type} uri URI
   * @param  {String} extension File extension (optional). Default: .html
   * @return {String}
   */
  buildFilenameFromURI: function (uri, extension) {
    if (_.isNil(extension)) {
      extension = '.html';
    }

    var uriParsed = url.parse(uri);
    var filename = _.trim(uriParsed.pathname, '/\\');

    if (!filename.length) {
      filename = 'index';
    }

    filename = sanitizeFilename(filename, {
      replacement: '-'
    });

    if (!filename.length) {
      return filename;
    }

    if (path.extname(filename).toLowerCase() === extension) {
      return filename;
    }

    return filename + extension;
  },
  /**
   * Deletes all files and directories in a given directory.
   *
   * @param  {String} directory
   * @return {Promise}
   */
  cleanDirectory: function (directory) {
    if (_.isNil(directory)) {
      return Promise.reject('cleanDirectory() `directory` is required');
    }
    else if (!directory.length) {
      return Promise.reject('cleanDirectory() `directory` is empty');
    }

    return rimrafAsync(directory + '/**/*');
  },
  /**
   * Writes data to a file.
   *
   * @param  {String} file Path to file
   * @param  {String} data
   * @return {Promise}
   */
  writeFile: function (file, data) {
    if (_.isNil(file)) {
      return Promise.reject('writeFile() `file` is required');
    }

    return fs.writeFileAsync(file, data);
  },
  /**
   *
   * @param  {String} data
   * @param  {String} baseURL
   * @return {String}
   */
  relToAbs: function (data, baseURL) {
    return relToAbs.convert(data, baseURL);
  },
  /**
   * Matches paths.
   *
   * @param  {String} pattern
   * @param  {Object} options
   * @return {Promise}
   */
  glob: function (pattern, options) {
    return new Promise(function (resolve, reject) {
      glob(pattern, options, function (err, paths) {
        return err === null ? resolve(paths) : reject(err);
      });
    });
  },
  /**
   * Matches and stats paths.
   *
   * @param  {String} pattern
   * @param  {Object} options
   * @return {Promise}
   */
  globStat: function (pattern, options) {
    return this.glob(pattern, options)
      .then(function (paths) {
        return Promise
          .map(paths, function (path) {
            return fs.statAsync(path)
              .then(function (stat) {
                stat.path = slash(path);
                return stat;
              });
          })
          .then(function (stats) {
            return stats;
          });
      });
  },
  /**
   * Builds a sitemap from matched paths.
   *
   * @param  {String} globPattern
   * @param  {Object} globOptions
   * @param  {Object} sitemapOptions
   * @return {Promise}
   */
  buildSitemap: function (globPattern, globOptions, sitemapOptions) {
    var defaultSitemapOptions = {
      baseURL: 'http://localhost',
      includePath: true
    };

    if (_.isNil(sitemapOptions)) {
      sitemapOptions = defaultSitemapOptions;
    }
    else {
      _.defaults(sitemapOptions, defaultSitemapOptions);
    }

    sitemapOptions.baseURL = _.trimEnd(sitemapOptions.baseURL, '/');

    return this.globStat(globPattern, globOptions)
      .then(function (files) {
        var urls = [];

        _.forEach(files, function (file) {
          var url;

          if (sitemapOptions.includePath) {
            url = _.trimStart(file.path, '/');
          }
          else {
            url = path.basename(file.path);
          }

          urls.push({
            url: '/' + url,
            lastmodISO: new Date(file.mtime || Date.now()).toISOString()
          });
        });

        return sitemap.createSitemap({
          hostname: sitemapOptions.baseURL,
          urls: urls
        }).toString();
      });
  },
  /**
   *
   * @param  {String} globPattern
   * @param  {Object} globOptions
   * @param  {Object} sitemapOptions
   * @param  {String} dest
   * @return {Promise}
   */
  saveSitemap: function (globPattern, globOptions, sitemapOptions, dest) {
    if (_.isNil(dest)) {
      dest = 'sitemap.xml';
    }

    var self = this;

    // Build sitemap contents.
    return this.buildSitemap(globPattern, globOptions, sitemapOptions)
      // Write sitemap contents to file.
      .then(function (data) {
        return self.writeFile(dest, data);
      });
  }
};

module.exports = Cordy;
<commit_msg>Minor cody tidy<commit_after>var _ = require('lodash');
var chalk = require('chalk');
var EventEmitter = require('events');
var fs = require('fs');
var glob = require('glob');
var mkdirp = require('mkdirp');
var Nightmare = require('nightmare');
var path = require('path');
var Promise = require('bluebird');
var relToAbs = require('rel-to-abs');
var rimraf = require('rimraf');
var sanitizeFilename = require('sanitize-filename');
var slash = require('slash');
var sitemap = require('sitemap');
var url = require('url');
var util = require('util');

Promise.promisifyAll(fs);
var mkdirpAsync = Promise.promisify(mkdirp);
var rimrafAsync = Promise.promisify(rimraf);

/**
 *
 * @return {Cordy}
 */
function Cordy() {
  EventEmitter.call(this);
}

Cordy.prototype = _.create(EventEmitter.prototype, {
  constructor: Cordy,
  /**
   * Builds cache.
   *
   * @param  {Object} options
   * @return {Promise}
   */
  buildCache: function (options) {
    var defaultSitemapOptions = {
      baseURL: undefined
    };

    var defaultOptions = {
      cleanDest: false,
      urls: [],
      dest: undefined,
      sitemap: defaultSitemapOptions
    };

    if (_.isNil(options)) {
      options = defaultOptions;
    }
    else {
      _.defaults(options, defaultOptions);
    }

    if (_.isNil(options.sitemap)) {
      options.sitemap = defaultSitemapOptions;
    }
    else {
      _.defaults(options.sitemap, defaultSitemapOptions);
    }

    if (_.isNil(options.dest)) {
      return Promise.reject('buildCache() `options.dest` is required');
    }

    var self = this;

    // Build array of values for mapper function.
    var tasks = [];

    _.forEach(options.urls, function (url) {
      tasks.push({
        src: url.url,
        baseURL: url.baseURL,
        auth: url.auth,
        dest: options.dest
      });
    });

    // Fetches and saves HTML of all URLs, and builds and saves sitemap.
    var alwaysFn = function () {
      // Create destination directory.
      return mkdirpAsync(options.dest)
        // Save HTML.
        .then(function () {
          // Run map function on all URLs. Note that map loop exits on first rejection.
          return Promise.mapSeries(tasks, function (task) {
            var auth = task.auth;
            var src = task.src;
            var baseURL = task.baseURL;
            var nightmareOptions = task.nightmare;
            var dest = task.dest;

            var saveHTMLFn = function () {
              return self.saveHTML(src, nightmareOptions, baseURL, dest);
            };

            if (_.isFunction(auth)) {
              return auth()
                .then(saveHTMLFn);
            }

            return saveHTMLFn();
          });
        })
        // Create sitemap.
        .then(function () {
          return self.saveSitemap(options.dest + '/**/*.html', null, options.sitemap, options.dest + '/sitemap.xml');
        });
    };

    if (options.cleanDest) {
      return this.cleanDirectory(options.dest)
        .then(alwaysFn);
    }

    return alwaysFn();
  },
  /**
   * Fetches HTML for a given URL, and writes HTML to file.
   *
   * @param  {String} src
   * @param  {Object} nightmareOptions
   * @return {Promise}
   */
  getHTML: function (src, nightmareOptions) {
    if (_.isNil(src)) {
      return Promise.reject('getHTML() `src` is required');
    }

    return this.runTasks([
      ['goto', src],
      ['evaluate', function () {
        return new XMLSerializer().serializeToString(document);
      }]
    ], nightmareOptions);
  },
  /**
   * Fetches HTML for a given URL, and writes HTML to file.
   *
   * @param  {String} src
   * @param  {Object} nightmareOptions
   * @param  {String} baseURL
   * @param  {String} dest
   * @return {Promise}
   */
  saveHTML: function (src, nightmareOptions, baseURL, dest) {
    if (_.isNil(src)) {
      return Promise.reject('saveHTML() `src` is required');
    }
    else if (_.isNil(dest)) {
      return Promise.reject('saveHTML() `dest` is required');
    }

    var self = this;
    var file = dest + '/' + this.buildFilenameFromURI(src);
    var srcParsed = url.parse(src);

    if (_.isNil(baseURL)) {
      baseURL = url.format({
        host: srcParsed.host,
        protocol: srcParsed.protocol
      });
    }

    return this.getHTML(src, nightmareOptions)
      .then(function (data) {
        data = self.relToAbs(data, baseURL);
        return self.writeFile(file, data);
      });
  },
  /**
   * Runs the given Nightmare tasks.
   *
   * @param  {Array} tasks
   * @param  {Object} options
   * @return {Promise}
   */
  runTasks: function (tasks, options) {
    var self = this;

    this.emit('runTasks', tasks);

    return new Promise(function (resolve, reject) {
      var flags = {
        error: false // Whether one or more tasks threw an error
      };
      var gotoStack = [];
      var nightmare = Nightmare(options);

      nightmare.on('did-get-response-details', function (event, status, newURL, originalURL, httpResponseCode, requestMethod, referrer, headers) {
        // Handle HTTP errors thrown by goto tasks
        var gotoIndex = _.findIndex(gotoStack, function (o) {
          return newURL === o.url && false === o.flags.run;
        });

        if (gotoIndex > -1) {
          gotoStack[gotoIndex].flags.run = true;
          gotoStack[gotoIndex].httpResponseCode = httpResponseCode;

          // Client (400) or server (500) error
          if (httpResponseCode >= 400) {
            flags.error = true;
            self.emit('httpError', newURL, httpResponseCode);
          }
        }
      });

      _.forEach(tasks, function (value, index) {
        var task = value[0];
        var args = _.slice(value, 1);

        if ('goto' === task) {
          gotoStack.push({
            url: args[0],
            flags: {
              run: false
            },
            httpResponseCode: undefined
          });
        }

        nightmare[task].apply(nightmare, args);
      });

      return nightmare.end().then(function () {
        if (flags.error) {
          reject();
        }
        else {
          resolve.apply(nightmare, arguments);
        }
      }, reject);
    });
  },
  /**
   * Returns a filename from a given URI without unsafe characters.
   *
   * @param  {type} uri URI
   * @param  {String} extension File extension (optional). Default: .html
   * @return {String}
   */
  buildFilenameFromURI: function (uri, extension) {
    if (_.isNil(extension)) {
      extension = '.html';
    }

    var uriParsed = url.parse(uri);
    var filename = _.trim(uriParsed.pathname, '/\\');

    if (!filename.length) {
      filename = 'index';
    }

    filename = sanitizeFilename(filename, {
      replacement: '-'
    });

    if (!filename.length) {
      return filename;
    }

    if (path.extname(filename).toLowerCase() === extension) {
      return filename;
    }

    return filename + extension;
  },
  /**
   * Deletes all files and directories in a given directory.
   *
   * @param  {String} directory
   * @return {Promise}
   */
  cleanDirectory: function (directory) {
    if (_.isNil(directory)) {
      return Promise.reject('cleanDirectory() `directory` is required');
    }
    else if (!directory.length) {
      return Promise.reject('cleanDirectory() `directory` is empty');
    }

    return rimrafAsync(directory + '/**/*');
  },
  /**
   * Writes data to a file.
   *
   * @param  {String} file Path to file
   * @param  {String} data
   * @return {Promise}
   */
  writeFile: function (file, data) {
    if (_.isNil(file)) {
      return Promise.reject('writeFile() `file` is required');
    }

    return fs.writeFileAsync(file, data);
  },
  /**
   *
   * @param  {String} data
   * @param  {String} baseURL
   * @return {String}
   */
  relToAbs: function (data, baseURL) {
    return relToAbs.convert(data, baseURL);
  },
  /**
   * Matches paths.
   *
   * @param  {String} pattern
   * @param  {Object} options
   * @return {Promise}
   */
  glob: function (pattern, options) {
    return new Promise(function (resolve, reject) {
      glob(pattern, options, function (err, paths) {
        return err === null ? resolve(paths) : reject(err);
      });
    });
  },
  /**
   * Matches and stats paths.
   *
   * @param  {String} pattern
   * @param  {Object} options
   * @return {Promise}
   */
  globStat: function (pattern, options) {
    return this.glob(pattern, options)
      .then(function (paths) {
        return Promise
          .map(paths, function (path) {
            return fs.statAsync(path)
              .then(function (stat) {
                stat.path = slash(path);
                return stat;
              });
          })
          .then(function (stats) {
            return stats;
          });
      });
  },
  /**
   * Builds a sitemap from matched paths.
   *
   * @param  {String} globPattern
   * @param  {Object} globOptions
   * @param  {Object} sitemapOptions
   * @return {Promise}
   */
  buildSitemap: function (globPattern, globOptions, sitemapOptions) {
    var defaultSitemapOptions = {
      baseURL: 'http://localhost',
      includePath: true
    };

    if (_.isNil(sitemapOptions)) {
      sitemapOptions = defaultSitemapOptions;
    }
    else {
      _.defaults(sitemapOptions, defaultSitemapOptions);
    }

    sitemapOptions.baseURL = _.trimEnd(sitemapOptions.baseURL, '/');

    return this.globStat(globPattern, globOptions)
      .then(function (files) {
        var urls = [];

        _.forEach(files, function (file) {
          var url;

          if (sitemapOptions.includePath) {
            url = _.trimStart(file.path, '/');
          }
          else {
            url = path.basename(file.path);
          }

          urls.push({
            url: '/' + url,
            lastmodISO: new Date(file.mtime || Date.now()).toISOString()
          });
        });

        return sitemap.createSitemap({
          hostname: sitemapOptions.baseURL,
          urls: urls
        }).toString();
      });
  },
  /**
   *
   * @param  {String} globPattern
   * @param  {Object} globOptions
   * @param  {Object} sitemapOptions
   * @param  {String} dest
   * @return {Promise}
   */
  saveSitemap: function (globPattern, globOptions, sitemapOptions, dest) {
    if (_.isNil(dest)) {
      dest = 'sitemap.xml';
    }

    var self = this;

    // Build sitemap contents.
    return this.buildSitemap(globPattern, globOptions, sitemapOptions)
      // Write sitemap contents to file.
      .then(function (data) {
        return self.writeFile(dest, data);
      });
  }
});

module.exports = Cordy;
<|endoftext|>


<commit_before>var mqtt = require('mqtt'),
    msg = require('./cloud-message').init(),
	registrationCompleted = false,
	sensorsList = {};

function IoTKitCloud(conf, logger, deviceId, sensorsStore){
  var me = this;

  me.logger = logger;
  me.sensorsStore = sensorsStore;

  var host = conf.host || 'broker.enableiot.com';
  var port = conf.port || 8884;
  var args = {
     keyPath: conf.key || './certs/client.key',
     certPath: conf.crt || './certs/client.crt',
     keepalive: 59000
  };

  me.deviceId = deviceId;
  me.accountId = "anon";
  me.pubArgs = {
    qos: conf.qos || 1,
    retain: conf.retain || true
  };

  me.client = mqtt.createSecureClient(port, host, args);
  me.logger.info('Cloud client created');

  me.topics = {
    reg: conf.reg_topic || '/server/registration',
    status: conf.status_topic || '/server/registration_status',
    metric: conf.metric_topic || '/server/metric/'
  };

  // add the state specifics
  me.topics.metric += me.accountId + '/' + me.deviceId;

  me.registrationCompleted = false;

  me.client
    .subscribe(me.topics.status)
    .on('message', function(topic, message) {
      me.logger.info('STATUS: %s', topic, message);
      var regStatus = JSON.parse(message);
      // Check registration status is true and device_id is ours
      if(me.registrationCompleted = regStatus !== undefined
      		&& regStatus.response !== undefined
      		&& regStatus.response.device_id == me.deviceId
  				&& regStatus.response.status){
  	    me.logger.info('Devices registered successfully');
        // populate accountId
        me.accountId = regStatus.response.account_id;
      	// save registered sensors
  	    me.sensorsStore.saveSensorsList(me.sensorsList);
      }
    });
};

IoTKitCloud.prototype.reg = function(sensorsListArg) {
  var me = this;
  me.sensorsList = sensorsListArg || me.sensorsList;
  var doc = msg.getRegMsg(me.deviceId, undefined, undefined, me.sensorsList);
  me.logger.debug("Reg doc: %j", doc, {});
  me.pub(me.topics.reg, doc);
};

IoTKitCloud.prototype.metric = function(message){
  var me = this;
  var doc = msg.getMetricMsg(me.deviceId, me.accountId, message);
  me.logger.debug("Metric doc: %j", doc, {});
  me.pub(me.topics.metric, doc);
};

IoTKitCloud.prototype.pub = function(topic, doc){
  var me = this;

  // Validate the input arg
  if (!doc || !topic) {
    logger.error('send: null args');
    return;
  }

  // Send to broker
  me.logger.debug('SEND: %s', topic, doc);
  me.client.publish(topic, JSON.stringify(doc), me.pubArgs);

};

exports.init = function(conf, logger, deviceId, sensorsStore) {
  return new IoTKitCloud(conf, logger, deviceId, sensorsStore);
};  <commit_msg>Fix metric topic initialization, set actual account_id from registration_status<commit_after>var mqtt = require('mqtt'),
    msg = require('./cloud-message').init(),
	registrationCompleted = false,
	sensorsList = {};

function IoTKitCloud(conf, logger, deviceId, sensorsStore){
  var me = this;

  me.logger = logger;
  me.sensorsStore = sensorsStore;

  var host = conf.host || 'broker.enableiot.com';
  var port = conf.port || 8884;
  var args = {
     keyPath: conf.key || './certs/client.key',
     certPath: conf.crt || './certs/client.crt',
     keepalive: 59000
  };

  me.deviceId = deviceId;
  me.accountId = "anon";
  me.pubArgs = {
    qos: conf.qos || 1,
    retain: conf.retain || true
  };

  me.client = mqtt.createSecureClient(port, host, args);
  me.logger.info('Cloud client created');

  me.topics = {
    reg: conf.reg_topic || '/server/registration',
    status: conf.status_topic || '/server/registration_status',
    metric: conf.metric_topic || '/server/metric/'
  };

  me.registrationCompleted = false;

  me.client
    .subscribe(me.topics.status)
    .on('message', function(topic, message) {
      me.logger.info('STATUS: %s', topic, message);
      var regStatus = JSON.parse(message);
      // Check registration status is true and device_id is ours
      if(me.registrationCompleted = regStatus !== undefined
      		&& regStatus.response !== undefined
      		&& regStatus.response.device_id == me.deviceId
  				&& regStatus.response.status){
  	    me.logger.info('Devices registered successfully');
        // populate accountId
        me.accountId = regStatus.response.account_id;
        // add the state specifics
        me.topics.metric += me.accountId + '/' + me.deviceId;
      	// save registered sensors
  	    me.sensorsStore.saveSensorsList(me.sensorsList);
      }
    });
};

IoTKitCloud.prototype.reg = function(sensorsListArg) {
  var me = this;
  me.sensorsList = sensorsListArg || me.sensorsList;
  var doc = msg.getRegMsg(me.deviceId, undefined, undefined, me.sensorsList);
  me.logger.debug("Reg doc: %j", doc, {});
  me.pub(me.topics.reg, doc);
};

IoTKitCloud.prototype.metric = function(message){
  var me = this;
  var doc = msg.getMetricMsg(me.deviceId, me.accountId, message);
  me.logger.debug("Metric doc: %j", doc, {});
  me.pub(me.topics.metric, doc);
};

IoTKitCloud.prototype.pub = function(topic, doc){
  var me = this;

  // Validate the input arg
  if (!doc || !topic) {
    logger.error('send: null args');
    return;
  }

  // Send to broker
  me.logger.debug('SEND: %s', topic, doc);
  me.client.publish(topic, JSON.stringify(doc), me.pubArgs);

};

exports.init = function(conf, logger, deviceId, sensorsStore) {
  return new IoTKitCloud(conf, logger, deviceId, sensorsStore);
};  <|endoftext|>


<commit_before>/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/*
 * Copyright (c) 2014, Joyent, Inc.
 */

/*
 * CNAPI: workflow shared functions
 */

// These must match the names available in the workflow VM:
var async = require('async');
var restify = require('restify');
var sdcClients = require('sdc-clients');

// --- Globals

// Make jslint happy:
var cnapiUrl;

// --- Exports

/**
 * Start a provisioner task with CNAPI on each of the servers to update
 * the firewall data.
 *
 * Inputs:
 * - job.params.fwapiServers {Array} : server UUIDs to send update tasks to
 *
 * Outputs:
 * - job.params.taskIDs {Array}: task objects to poll in a later task, where
 *   each task object looks like:
 *     { server_uuid: uuid, task_id: id }
 */
function fwUpdate(job, callback) {
    if (!job.params.fwapiServers || job.params.fwapiServers.length === 0) {
        return callback(null, 'No remote servers to update');
    }

    var serversSent = [];
    var cnapi = new sdcClients.CNAPI({ url: cnapiUrl });
    var matchingVMs = job.params.fwapiMatchingVMs || [];
    job.params.taskIDs = [];

    return async.forEachSeries(job.params.fwapiServers, function (uuid, cb) {
        var endpoint = '/servers/' + uuid + '/fw/update';
        var firewall = {
            jobid: job.uuid
        };

        if (job.params.hasOwnProperty('rule')) {
            firewall.rules = [ job.params.rule ];
        }

        var remoteVMs = matchingVMs.filter(function (rvm) {
            return (rvm.server_uuid != uuid);
        });

        if (remoteVMs.length) {
            firewall.remoteVMs = remoteVMs;
        }

        job.log.debug(firewall, 'Updating rules on server "%s"', uuid);
        return cnapi.post(endpoint, firewall, function (err, task) {
            if (err) {
                if (err.restCode == 'InvalidVersion') {
                    // This is a 6.5 CN - no firewall data was sent to it
                    return cb(null);
                }

                return cb(err);
            }

            serversSent.push(uuid);
            job.log.debug(task, 'Server "%s": task', uuid);

            job.params.taskIDs.push({ server_uuid: uuid, task_id: task.id});
            return cb(null);
        });

    }, function (err) {
        if (err) {
            return callback(err);
        }

        if (serversSent.length === 0) {
            return callback(null,
                    'Only 6.5 servers available: did not send firewall data');
        }

        return callback(null, 'Started update on servers: '
            + serversSent.join(', '));
    });
}

/**
 * Get the server sysinfo objects for the given UUIDs
 *
 * Inputs:
 *
 * - job.params {Object} :
 *   - serverUUIDs {Array of UUIDs} : UUIDS of servers to get sysinfos for
 *
 * - job.servers {Array} : (optional) If present, as many sysinfo objects as
 *   possible will be pulled from here in an attempt to avoid calling out to
 *   CNAPI
 *
 * Outputs:
 *
 * Once function is complete, the following will be stored in job.params:
 * - cnapiSysinfos {Object} : CNAPI server objects containing a sysinfo
 *   subobject
 */
function sysinfoList(job, callback) {
        if (!job.params.serverUUIDs) {
            return callback(null, 'No server UUIDs to fetch');
        }

        var cnapi = new sdcClients.CNAPI({ url: cnapiUrl });
        var params = {
            extras: 'sysinfo',
            uuids: job.params.serverUUIDs.join(',')
        };
        job.params.cnapiSysinfos = [];

        // If we have job.servers from an earlier task, use as many of those
        // as we can to try and avoid hitting CNAPI again
        if (job.servers) {
            var uuids = {};
            job.params.serverUUIDs.forEach(function (uuid) {
                uuids[uuid] = 1;
            });

            job.servers.forEach(function (s) {
                if (uuids.hasOwnProperty(s.uuid)) {
                    delete uuids[s.uuid];
                    job.params.cnapiSysinfos.push(s);
                }
            });
            params.uuids = Object.keys(uuids);
        }

        if (params.uuids.length === 0) {
            return callback(null,
                'Server info cached from get_servers: not fetching');
        }

        return cnapi.listServers(params, function (err, servers) {
            if (err) {
                return callback(err);
            }

            if (!Array.isArray(servers) || servers.length === 0) {
                return callback(new Error('No servers found'));
            }

            job.params.cnapiSysinfos = job.params.cnapiSysinfos.concat(servers);
            job.log.debug(servers.map(function (s) { return s.UUID; }),
                'getServers: servers found');

            return callback(null, 'Got servers');
        });
}

/**
 * Start a provisioner task with CNAPI on a servers to update nics.
 *
 * Inputs:
 * - job.params.server_uuid {UUID} : server UUIDs to send update task to
 * - job.params.nics {Array}: nic objects to update
 *
 * Outputs:
 * - job.params.taskIDs {Array}: task objects to poll in a later task, where
 *   each task object looks like:
 *     { server_uuid: uuid, task_id: id }
 */
function nicUpdate(job, callback) {
        if (!job.params.nics || job.params.nics.length === 0) {
            return callback(null, 'No nics to update');
        }

        var cnapi = new sdcClients.CNAPI({ url: cnapiUrl });
        var endpoint = '/servers/' + job.params.server_uuid + '/nics/update';
        job.params.taskIDs = [];

        job.log.debug(job.params.nics, 'Updating rules on server "%s"',
            job.params.server_uuid);

        return cnapi.post(endpoint, { nics: job.params.nics },
            function (err, task) {
            if (err) {
                return callback(err);
            }

            job.log.debug(task, 'Server "%s": nic update task "%s"',
                job.params.server_uuid, task.id);
            job.params.taskIDs.push({ server_uuid: job.params.server_uuid,
                task_id: task.id});
            return callback(null, 'Started nic update task on server');
        });
}

/**
 * Poll CNAPI for each of the tasks started in a previous workflow task
 *
 * Inputs:
 * - job.params.taskIDs {Array} : task ID objects as per fwUpdate()
 *
 * Outputs:
 * - job.params.taskSuccesses {Array} : task ID objects of successes
 * - job.params.taskFailures {Array} : task ID objects of failures
 */
function pollTasks(job, callback) {
    if (!job.params.taskIDs || job.params.taskIDs.length === 0) {
        return callback(null, 'No tasks to poll');
    }

    var cnapi = new sdcClients.CNAPI({ url: cnapiUrl });

    job.params.taskSuccesses = [];
    job.params.taskFailures = [];

    return async.forEach(job.params.taskIDs, function (detail, cb) {
        var timeout = null;

        function pollOne() {
            cnapi.getTask(detail.task_id, function onCnapi(err, task) {
                if (timeout) {
                    clearTimeout(timeout);
                }

                if (err) {
                    return cb(err);
                }

                job.log.debug(task, 'retrieved task for server "%s"',
                    detail.server_uuid);
                if (task.status == 'failure') {
                    job.params.taskFailures.push(detail);
                    return cb(new Error('CNAPI task "' + detail.task_id
                        + '" failed for server "' + detail.server_uuid + '"'));
                }

                if (task.status == 'complete') {
                    job.params.taskSuccesses.push(detail);
                    return cb(null);
                }

                timeout = setTimeout(pollOne, 1000);
            });
        }

        pollOne();
    }, function (err) {
        if (err) {
            return callback(err);
        }

        return callback(null, 'All server tasks returned successfully');
    });
}

/**
 * Trigger a sysinfo refresh in CNAPI
 *
 * Inputs:
 * - job.params.taskIDs {Array} : task ID objects as per fwUpdate()
 *
 * Outputs:
 * - job.params.taskSuccesses {Array} : task ID objects of successes
 * - job.params.taskFailures {Array} : task ID objects of failures
 */
function refreshServerSysinfo(job, callback) {
    var cnapi = restify.createJsonClient({ url: cnapiUrl});
    var serverUrl = '/servers/' + job.params.server_uuid + '/sysinfo-refresh';

    cnapi.post(serverUrl, {}, function (error, req, res) {
        if (error) {
            job.log.info('Error refreshing server sysinfo');
            job.log.info(error.stack.toString());
            callback(error);
            return;
        }
        callback();
    });
}

module.exports = {
    fwUpdate: fwUpdate,
    sysinfoList: sysinfoList,
    nicUpdate: nicUpdate,
    pollTasks: pollTasks,
    refreshServerSysinfo: refreshServerSysinfo
};
<commit_msg>CNAPI-658 CNAPI and cn-agent shouldn't try to manage firewall rules Reviewed by: Orlando Vazquez <orlando@joyent.com> Approved by: Orlando Vazquez <orlando@joyent.com><commit_after>/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/*
 * Copyright (c) 2014, Joyent, Inc.
 */

/*
 * CNAPI: workflow shared functions
 */

// These must match the names available in the workflow VM:
var async = require('async');
var restify = require('restify');
var sdcClients = require('sdc-clients');

// --- Globals

// Make jslint happy:
var cnapiUrl;

// --- Exports

/**
 * Get the server sysinfo objects for the given UUIDs
 *
 * Inputs:
 *
 * - job.params {Object} :
 *   - serverUUIDs {Array of UUIDs} : UUIDS of servers to get sysinfos for
 *
 * - job.servers {Array} : (optional) If present, as many sysinfo objects as
 *   possible will be pulled from here in an attempt to avoid calling out to
 *   CNAPI
 *
 * Outputs:
 *
 * Once function is complete, the following will be stored in job.params:
 * - cnapiSysinfos {Object} : CNAPI server objects containing a sysinfo
 *   subobject
 */
function sysinfoList(job, callback) {
        if (!job.params.serverUUIDs) {
            return callback(null, 'No server UUIDs to fetch');
        }

        var cnapi = new sdcClients.CNAPI({ url: cnapiUrl });
        var params = {
            extras: 'sysinfo',
            uuids: job.params.serverUUIDs.join(',')
        };
        job.params.cnapiSysinfos = [];

        // If we have job.servers from an earlier task, use as many of those
        // as we can to try and avoid hitting CNAPI again
        if (job.servers) {
            var uuids = {};
            job.params.serverUUIDs.forEach(function (uuid) {
                uuids[uuid] = 1;
            });

            job.servers.forEach(function (s) {
                if (uuids.hasOwnProperty(s.uuid)) {
                    delete uuids[s.uuid];
                    job.params.cnapiSysinfos.push(s);
                }
            });
            params.uuids = Object.keys(uuids);
        }

        if (params.uuids.length === 0) {
            return callback(null,
                'Server info cached from get_servers: not fetching');
        }

        return cnapi.listServers(params, function (err, servers) {
            if (err) {
                return callback(err);
            }

            if (!Array.isArray(servers) || servers.length === 0) {
                return callback(new Error('No servers found'));
            }

            job.params.cnapiSysinfos = job.params.cnapiSysinfos.concat(servers);
            job.log.debug(servers.map(function (s) { return s.UUID; }),
                'getServers: servers found');

            return callback(null, 'Got servers');
        });
}

/**
 * Start a provisioner task with CNAPI on a servers to update nics.
 *
 * Inputs:
 * - job.params.server_uuid {UUID} : server UUIDs to send update task to
 * - job.params.nics {Array}: nic objects to update
 *
 * Outputs:
 * - job.params.taskIDs {Array}: task objects to poll in a later task, where
 *   each task object looks like:
 *     { server_uuid: uuid, task_id: id }
 */
function nicUpdate(job, callback) {
        if (!job.params.nics || job.params.nics.length === 0) {
            return callback(null, 'No nics to update');
        }

        var cnapi = new sdcClients.CNAPI({ url: cnapiUrl });
        var endpoint = '/servers/' + job.params.server_uuid + '/nics/update';
        job.params.taskIDs = [];

        job.log.debug(job.params.nics, 'Updating rules on server "%s"',
            job.params.server_uuid);

        return cnapi.post(endpoint, { nics: job.params.nics },
            function (err, task) {
            if (err) {
                return callback(err);
            }

            job.log.debug(task, 'Server "%s": nic update task "%s"',
                job.params.server_uuid, task.id);
            job.params.taskIDs.push({ server_uuid: job.params.server_uuid,
                task_id: task.id});
            return callback(null, 'Started nic update task on server');
        });
}

/**
 * Poll CNAPI for each of the tasks started in a previous workflow task
 *
 * Inputs:
 * - job.params.taskIDs {Array}: task objects to poll, where each task object
 *   looks like:
 *     { server_uuid: uuid, task_id: id }
 *
 * Outputs:
 * - job.params.taskSuccesses {Array} : task ID objects of successes
 * - job.params.taskFailures {Array} : task ID objects of failures
 */
function pollTasks(job, callback) {
    if (!job.params.taskIDs || job.params.taskIDs.length === 0) {
        return callback(null, 'No tasks to poll');
    }

    var cnapi = new sdcClients.CNAPI({ url: cnapiUrl });

    job.params.taskSuccesses = [];
    job.params.taskFailures = [];

    return async.forEach(job.params.taskIDs, function (detail, cb) {
        var timeout = null;

        function pollOne() {
            cnapi.getTask(detail.task_id, function onCnapi(err, task) {
                if (timeout) {
                    clearTimeout(timeout);
                }

                if (err) {
                    return cb(err);
                }

                job.log.debug(task, 'retrieved task for server "%s"',
                    detail.server_uuid);
                if (task.status == 'failure') {
                    job.params.taskFailures.push(detail);
                    return cb(new Error('CNAPI task "' + detail.task_id
                        + '" failed for server "' + detail.server_uuid + '"'));
                }

                if (task.status == 'complete') {
                    job.params.taskSuccesses.push(detail);
                    return cb(null);
                }

                timeout = setTimeout(pollOne, 1000);
            });
        }

        pollOne();
    }, function (err) {
        if (err) {
            return callback(err);
        }

        return callback(null, 'All server tasks returned successfully');
    });
}

/**
 * Trigger a sysinfo refresh in CNAPI
 *
 * Inputs:
 * - job.params.server_uuid {String}: UUID of the server whose sysinfo should be
 *   refreshed.
 *
 * Outputs:
 * - job.params.taskSuccesses {Array} : task ID objects of successes
 * - job.params.taskFailures {Array} : task ID objects of failures
 */
function refreshServerSysinfo(job, callback) {
    var cnapi = restify.createJsonClient({ url: cnapiUrl});
    var serverUrl = '/servers/' + job.params.server_uuid + '/sysinfo-refresh';

    cnapi.post(serverUrl, {}, function (error, req, res) {
        if (error) {
            job.log.info('Error refreshing server sysinfo');
            job.log.info(error.stack.toString());
            callback(error);
            return;
        }
        callback();
    });
}

module.exports = {
    sysinfoList: sysinfoList,
    nicUpdate: nicUpdate,
    pollTasks: pollTasks,
    refreshServerSysinfo: refreshServerSysinfo
};
<|endoftext|>


<commit_before>var Spline = require('./spline');

var Cubic = Spline.extend({
  constructor: function() {
    Spline.apply(this, arguments);
  },
  coeff: function coeff(segment, n, dir) {
    var curr = segment,
        next = segment + 1,
        r0 = this.pos(curr, dir),
        r1 = this.pos(next, dir),
        v0 = this.vel(curr, dir),
        v1 = this.vel(next, dir, true),
        t0 = this.par(curr),
        t1 = this.par(next),
        td = t1 - t0;
    switch(n) {
      case 0: return r0;
      case 1: return v0;
      case 2: return 3*(r1-r0)/td/td - (v1+2*v0)/td;
      case 3: return 2*(r0-r1)/td/td/td + (v1+v0)/td/td;
    }
  },
  pos: function(n, dir) {
    return this.points[n][dir];
  },
  par: function(n) {
    return this.points[n].t;
  },
  seg: function(t) {
    for(var i=0; i < this.points.length; i++) {
      if(this.points[i].t > t) return i - 1;
    }
    return this.points.length - 1;
  },
  compile: function(dir, deg) {
    deg = deg || 0;
    switch(deg) {
      case 0:
        return new Function('t', this.points.slice(0, this.points.length - 1).map(function(seg, i) {
          return 'if(t <= ' + this.points[i+1].t + ') return ' +
          [3,2,1,0].map(function(n) {
            return this.coeff(i, n, dir) + '*Math.pow(t - ' + seg.t + ', ' + n + ')'
          }, this).join(' + ') + ';';
        }, this).join('\n'));
      case 1:
        return new Function('t', this.points.slice(0, this.points.length - 1).map(function(seg, i) {
          return 'if(t <= ' + this.points[i+1].t + ') return ' +
          [3,2,1].map(function(n) {
            return n * this.coeff(i, n, dir) + '*Math.pow(t - ' + seg.t + ', ' + (n-1) + ')'
          }, this).join(' + ') + ';';
        }, this).join('\n'));
      case 2:
        return new Function('t', this.points.slice(0, this.points.length - 1).map(function(seg, i) {
          return 'if(t <= ' + this.points[i+1].t + ') return ' +
          [3,2].map(function(n) {
            return (n * (n-1)) * this.coeff(i, n, dir) + '*Math.pow(t - ' + seg.t + ', ' + (n-2) + ')'
          }, this).join(' + ') + ';';
        }, this).join('\n'));
    }
  },
  compileLaTeX: function(dir) {
    return 'f_' + dir + '(t)=\\left\\{\\begin{matrix}' +
      this.points.slice(0, this.points.length - 1).map(function(seg, i) {
      return [3,2,1,0].map(function(n) {
        return this.coeff(i, n, dir).toFixed(2) + (n ? '(t-t{' + i + '})' + (n > 1 ? '^' + n : '') : '')
      }, this).join('+') + '&\\text{if}&' + seg.t.toFixed(2) + '\\leq t<' + this.points[i+1].t.toFixed(2);
    }, this).join('\\\\') + '\\end{matrix}\\right.';
  }
});

module.exports = Cubic;
<commit_msg>Refactor cubic spline derivation<commit_after>var Spline = require('./spline');

function revrange(s, e) {
  return Array(s-e+1).join(0).split(0).map(function(n, i) { return s-i; });
}

function derive(deg, n) {
  return n ? deg * derive(deg-1, n-1) : 1;
}

var Cubic = Spline.extend({
  constructor: function() {
    Spline.apply(this, arguments);
  },
  coeff: function coeff(segment, n, dir) {
    var curr = segment,
        next = segment + 1,
        r0 = this.pos(curr, dir),
        r1 = this.pos(next, dir),
        v0 = this.vel(curr, dir),
        v1 = this.vel(next, dir, true),
        t0 = this.par(curr),
        t1 = this.par(next),
        td = t1 - t0;
    switch(n) {
      case 0: return r0;
      case 1: return v0;
      case 2: return 3*(r1-r0)/td/td - (v1+2*v0)/td;
      case 3: return 2*(r0-r1)/td/td/td + (v1+v0)/td/td;
    }
  },
  pos: function(n, dir) {
    return this.points[n][dir];
  },
  par: function(n) {
    return this.points[n].t;
  },
  seg: function(t) {
    for(var i=0; i < this.points.length; i++) {
      if(this.points[i].t > t) return i - 1;
    }
    return this.points.length - 1;
  },
  compile: function(dir, deg) {
    deg = deg || 0;
    return new Function('t', this.points.slice(0, this.points.length - 1).map(function(seg, i) {
      return 'if(t <= ' + this.points[i+1].t + ') return ' +
      revrange(3, deg).map(function(n) {
        return derive(n, deg) * this.coeff(i, n, dir) + '*Math.pow(t - ' + seg.t + ', ' + (n-deg) + ')'
      }, this).join(' + ') + ';';
    }, this).join('\n'));
  },
  compileLaTeX: function(dir) {
    return 'f_' + dir + '(t)=\\left\\{\\begin{matrix}' +
      this.points.slice(0, this.points.length - 1).map(function(seg, i) {
      return [3,2,1,0].map(function(n) {
        return this.coeff(i, n, dir).toFixed(2) + (n ? '(t-t{' + i + '})' + (n > 1 ? '^' + n : '') : '')
      }, this).join('+') + '&\\text{if}&' + seg.t.toFixed(2) + '\\leq t<' + this.points[i+1].t.toFixed(2);
    }, this).join('\\\\') + '\\end{matrix}\\right.';
  }
});

module.exports = Cubic;
<|endoftext|>