<commit_before>import React from "react";
import { connect } from "react-redux";
import {
  populateMetadataStore,
  populateTreeStore,
  populateSequencesStore,
  populateFrequenciesStore,
  populateEntropyStore
} from "../actions";

import Radium from "radium";
import Flex from "./framework/flex";
import Header from "./framework/header";
import Footer from "./framework/footer";
import Background from "./framework/background";

import Controls from "./controls/controls";
import Frequencies from "./charts/frequencies";
import Entropy from "./charts/entropy";
import Map from "./map/map";
import TreeView from "./tree/treeView";
import parseParams from "../util/parseParams";
import queryString from "query-string";
import getColorScale from "../util/getColorScale";
import { parseGenotype, getGenotype } from "../util/getGenotype";
import {colorOptions} from "../util/globals";
import Sidebar from "react-sidebar";

const returnStateNeeded = (fullStateTree) => {
  return {
    tree: fullStateTree.tree,
    sequences: fullStateTree.sequences,
    sidebarOpen: false,
    sidebarDocked: false,
    metadata: fullStateTree.metadata,
    selectedLegendItem: fullStateTree.controls.selectedLegendItem
  };
};
@connect(returnStateNeeded)
@Radium
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      sidebarOpen: false,
      location: {
        pathname: window.location.pathname,
        query: queryString.parse(window.location.search)
      },
      colorScale: {
        colorBy: null,
        scale: null,
        legendBoundsMap: null,
        continuous: null
      },
      // sidebarDocked: true,
    };
  }
  static propTypes = {
    /* react */
    dispatch: React.PropTypes.func,
    params: React.PropTypes.object,
    /* component api */
    error: React.PropTypes.object,
    loading: React.PropTypes.bool,
    user: React.PropTypes.object,
    routes: React.PropTypes.array
    // foo: React.PropTypes.string
  }
  static defaultProps = {
    // foo: "bar"

  }

  /******************************************
   * LIFECYLCE METHODS
   *****************************************/

  componentWillReceiveProps(nextProps) {
  }

  componentWillMount() {
    var mql = window.matchMedia(`(min-width: 800px)`);
    mql.addListener(this.mediaQueryChanged.bind(this));
    this.setState({mql: mql, sidebarDocked: mql.matches});
  }

  componentDidMount() {
    // when the user hits the back button or forward, let us know so we can setstate again
    // all of the other intentional route changes we will manually setState
    const tmpQuery = queryString.parse(window.location.search);
    this.maybeFetchDataset();
    const cScale = this.updateColorScale(tmpQuery.colorBy || "region");
    window.addEventListener("popstate", (a, b, c) => {
      this.setState({
        location: {
          pathname: window.location.pathname.slice(1, -1),
          query: tmpQuery
        },
        colorScale: cScale
      });
    });
  }

  componentDidUpdate() {
    this.maybeFetchDataset();
    if (!this.state.nodeColor && this.props.tree.nodes) {
      const cScale = this.updateColorScale(this.state.location.query.colorBy || "region");
      this.setState(cScale);
    }
  }

  maybeFetchDataset() {
    if (this.state.latestValidParams === this.state.location.pathname) {
      return;
    }

    const parsedParams = parseParams(this.state.location.pathname);
    const tmp_levels = Object.keys(parsedParams.dataset).map((d) => parsedParams.dataset[d]);
    tmp_levels.sort((x, y) => x[0] > y[0]);
    // make prefix for data files with fields joined by _ instead of / as in URL
    const data_path = tmp_levels.map((d) => d[1]).join("_");
    if (parsedParams.incomplete) {
      this.setVirusPath(parsedParams.fullsplat);
    }
    if (parsedParams.valid && this.state.latestValidParams !== parsedParams.fullsplat) {
      this.props.dispatch(populateMetadataStore(data_path));
      this.props.dispatch(populateTreeStore(data_path));
      this.props.dispatch(populateSequencesStore(data_path));
      this.props.dispatch(populateFrequenciesStore(data_path));
      this.props.dispatch(populateEntropyStore(data_path));
      this.setState({latestValidParams: parsedParams.fullsplat});
    }
  }

  /******************************************
   * HANDLE QUERY PARAM CHANGES AND ASSOCIATED STATE UPDATES
   *****************************************/
  setVirusPath(newPath) {
    const prefix = (newPath === "" || newPath[0] === "/") ? "" : "/";
    const suffix = (newPath.length && newPath[newPath.length - 1] !== "/") ? "/?" : "?";
    const url = prefix + newPath + suffix + queryString.stringify(this.state.location.query);
    window.history.pushState({}, "", url);
    this.changeRoute(newPath, this.state.location.query);
  }

  getTipColorAttribute(node, cScale) {
    if (cScale.colorBy.slice(0,3) === "gt-") {
        return getGenotype(cScale.genotype[0][0],
                                  cScale.genotype[0][1],
                                  node, this.props.sequences.sequences);
    } else {
      return node.attr[cScale.colorBy];
    };
  }

  updateColorScale(colorBy) {
    const cScale = getColorScale(colorBy, this.props.tree, this.props.sequences);
    let gts = null;
    if (colorBy.slice(0,3) === "gt-" && this.props.sequences.geneLength) {
      gts = parseGenotype(colorBy, this.props.sequences.geneLength);
    }
    cScale.genotype = gts;
    let nodeColorAttr=null;
    let nodeColor=null;
    if (this.props.tree.nodes){
      nodeColorAttr = this.props.tree.nodes.map((n) => this.getTipColorAttribute(n, cScale));
      nodeColor = nodeColorAttr.map((n) => cScale.scale(n));
    }
    return {
      colorScale: cScale,
      nodeColor: nodeColor,
      nodeColorAttr: nodeColorAttr,
    };
  }

  parseFilterQuery(query) {
    const tmp = query.split("-").map( (d) => d.split("."));
    return {"fields": tmp.map( (d) => d[0] ), "filters": tmp.map( (d) => d[d.length-1].split(',') )};
  }

  tipVisibility(filters) {
    let upperLimit = +this.state.location.query.dmax;
    let lowerLimit = +this.state.location.query.dmin;
    if (!upperLimit) {
      upperLimit = 1000000000000;
    }
    if (!lowerLimit) {
      lowerLimit = -1000000000000;
    }
    if (this.props.tree.nodes){
      const filter_pairs = [];
      if (this.props.metadata && this.props.metadata.metadata) {
        for (const filter in this.props.metadata.metadata.controls){ // possible race condition with tree?
          const tmp = this.parseFilterQuery(this.state.location.query[filter] || "");
          for (let ii=0; ii<tmp.filters.length; ii+=1) {
            if (tmp.filters[ii] && tmp.fields[ii]){
              filter_pairs.push([tmp.fields[ii], tmp.filters[ii]]);
            }
          }
        }
      }
      if (filter_pairs.length){
        return this.props.tree.nodes.map((d) => (d.attr.num_date >= lowerLimit
                                         && d.attr.num_date < upperLimit
                                         && filter_pairs.every((x) => x[1].indexOf(d.attr[x[0]])>-1))
                                           ? "visible" : "hidden");
      } else {
        return this.props.tree.nodes.map((d) => (d.attr.num_date >= lowerLimit
                                         && d.attr.num_date < upperLimit)
                                            ? "visible" : "hidden");
      }
    } else {
      return "visible";
    }
  }

  changeRoute(pathname, query) {
    pathname = pathname.replace("!/", ""); // needed to assist with S3 redirects
    const prefix = (pathname === "" || pathname[0] === "/") ? "" : "/";
    const suffix = (pathname.length && pathname[pathname.length - 1] !== "/") ? "/?" : "?";
    const url = prefix + pathname + suffix + queryString.stringify(query);
    window.history.pushState({}, "", url);

    let new_colorData = {};
    //only update colorScales and nodeColors when changed
    if (query.colorBy && (query.colorBy !== this.state.colorScale.colorBy)) {
      new_colorData = this.updateColorScale(query.colorBy);
    }
    this.setState(Object.assign({location:{query, pathname}}, new_colorData));
  }

  currentFrequencies() {
    let freq = "";
    if (this.state.location.query.colorBy && this.state.location.query.colorBy.slice(0,3) === "gt-") {
      const gt = this.state.location.query.colorBy.slice(3).split("_");
      freq = "global_" + gt[0] + ":" + gt[1];
    }
    return freq;
  }

  /******************************************
   * HOVER EVENTS
   *****************************************/
  determineLegendMatch(selectedLegendItem, nodeAttr, legendBoundsMap) {
    let bool;
    // equates a tip and a legend element
    // exact match is required for categorical qunantities such as genotypes, regions
    // continuous variables need to fall into the interal (lower_bound[leg], leg]
    if (legendBoundsMap) {
      bool = (nodeAttr <= legendBoundsMap.upper_bound[selectedLegendItem]) &&
             (nodeAttr > legendBoundsMap.lower_bound[selectedLegendItem]);
    } else {
      bool = nodeAttr === selectedLegendItem;
    }
    return bool;
  }

  tipRadii() {
    const selItem = this.props.selectedLegendItem;
    if (selItem && this.state.nodeColorAttr){
      const legendMap = this.state.colorScale.continuous
                        ? this.state.colorScale.legendBoundsMap : false;
      return this.state.nodeColorAttr.map((d) => this.determineLegendMatch(selItem, d, legendMap) ? 6 : 3);
    } else if (this.state.nodeColorAttr) {
      return this.state.nodeColorAttr.map((d) => 3);
    } else {
      return null;
    }
  }
  /******************************************
   * SIDEBAR
   *****************************************/

  onSetSidebarOpen(open) {
    this.setState({sidebarOpen: open});
  }

  mediaQueryChanged() {
    this.setState({sidebarDocked: this.state.mql.matches});
  }

  /******************************************
   * RENDER
   *****************************************/
  render() {

    return (
      <Sidebar
        sidebar={
          <Controls changeRoute={this.changeRoute.bind(this)}
          location={this.state.location}
          colorOptions={colorOptions}
          colorScale={this.state.colorScale}/>
        }
        open={this.state.sidebarOpen}
        docked={this.state.sidebarDocked}
        onSetOpen={this.onSetSidebarOpen}>
        <Background>
          <Header/>
          <TreeView nodes={this.props.tree.nodes}
            nodeColorAttr={this.state.nodeColorAttr}
            colorScale={this.state.colorScale}
            nodeColor={this.state.nodeColor}
            tipRadii={this.tipRadii()}
            tipVisibility={this.tipVisibility()}
            layout={this.state.location.query.l || "rectangular"}
            distanceMeasure={this.state.location.query.m || "div"}
            datasetGuid={this.props.tree.datasetGuid}
          />
          <Frequencies genotype={this.currentFrequencies()}/>
          <Entropy
            changeRoute={this.changeRoute.bind(this)}
            location={this.state.location}
          />
          <Map nodes={this.props.tree.nodes} justGotNewDatasetRenderNewMap={false}/>
        </Background>
      </Sidebar>
    );
  }
}

// <Footer/>

export default App;
<commit_msg>read color options from meta data, closes #43<commit_after>import React from "react";
import { connect } from "react-redux";
import {
  populateMetadataStore,
  populateTreeStore,
  populateSequencesStore,
  populateFrequenciesStore,
  populateEntropyStore
} from "../actions";

import Radium from "radium";
import Flex from "./framework/flex";
import Header from "./framework/header";
import Footer from "./framework/footer";
import Background from "./framework/background";

import Controls from "./controls/controls";
import Frequencies from "./charts/frequencies";
import Entropy from "./charts/entropy";
import Map from "./map/map";
import TreeView from "./tree/treeView";
import parseParams from "../util/parseParams";
import queryString from "query-string";
import getColorScale from "../util/getColorScale";
import { parseGenotype, getGenotype } from "../util/getGenotype";
import {colorOptions} from "../util/globals";
import Sidebar from "react-sidebar";

const returnStateNeeded = (fullStateTree) => {
  return {
    tree: fullStateTree.tree,
    sequences: fullStateTree.sequences,
    sidebarOpen: false,
    sidebarDocked: false,
    metadata: fullStateTree.metadata,
    selectedLegendItem: fullStateTree.controls.selectedLegendItem
  };
};
@connect(returnStateNeeded)
@Radium
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      sidebarOpen: false,
      location: {
        pathname: window.location.pathname,
        query: queryString.parse(window.location.search)
      },
      colorScale: {
        colorBy: null,
        scale: null,
        legendBoundsMap: null,
        continuous: null
      },
      // sidebarDocked: true,
    };
  }
  static propTypes = {
    /* react */
    dispatch: React.PropTypes.func,
    params: React.PropTypes.object,
    /* component api */
    error: React.PropTypes.object,
    loading: React.PropTypes.bool,
    user: React.PropTypes.object,
    routes: React.PropTypes.array
    // foo: React.PropTypes.string
  }
  static defaultProps = {
    // foo: "bar"

  }

  /******************************************
   * LIFECYLCE METHODS
   *****************************************/

  componentWillReceiveProps(nextProps) {
  }

  componentWillMount() {
    var mql = window.matchMedia(`(min-width: 800px)`);
    mql.addListener(this.mediaQueryChanged.bind(this));
    this.setState({mql: mql, sidebarDocked: mql.matches});
  }

  componentDidMount() {
    // when the user hits the back button or forward, let us know so we can setstate again
    // all of the other intentional route changes we will manually setState
    const tmpQuery = queryString.parse(window.location.search);
    this.maybeFetchDataset();
    const cScale = this.updateColorScale(tmpQuery.colorBy || "region");
    window.addEventListener("popstate", (a, b, c) => {
      this.setState({
        location: {
          pathname: window.location.pathname.slice(1, -1),
          query: tmpQuery
        },
        colorScale: cScale
      });
    });
  }

  componentDidUpdate() {
    this.maybeFetchDataset();
    if (!this.state.nodeColor && this.props.tree.nodes) {
      const cScale = this.updateColorScale(this.state.location.query.colorBy || "region");
      this.setState(cScale);
    }
  }

  maybeFetchDataset() {
    if (this.state.latestValidParams === this.state.location.pathname) {
      return;
    }

    const parsedParams = parseParams(this.state.location.pathname);
    const tmp_levels = Object.keys(parsedParams.dataset).map((d) => parsedParams.dataset[d]);
    tmp_levels.sort((x, y) => x[0] > y[0]);
    // make prefix for data files with fields joined by _ instead of / as in URL
    const data_path = tmp_levels.map((d) => d[1]).join("_");
    if (parsedParams.incomplete) {
      this.setVirusPath(parsedParams.fullsplat);
    }
    if (parsedParams.valid && this.state.latestValidParams !== parsedParams.fullsplat) {
      this.props.dispatch(populateMetadataStore(data_path));
      this.props.dispatch(populateTreeStore(data_path));
      this.props.dispatch(populateSequencesStore(data_path));
      this.props.dispatch(populateFrequenciesStore(data_path));
      this.props.dispatch(populateEntropyStore(data_path));
      this.setState({latestValidParams: parsedParams.fullsplat});
    }
  }

  /******************************************
   * HANDLE QUERY PARAM CHANGES AND ASSOCIATED STATE UPDATES
   *****************************************/
  setVirusPath(newPath) {
    const prefix = (newPath === "" || newPath[0] === "/") ? "" : "/";
    const suffix = (newPath.length && newPath[newPath.length - 1] !== "/") ? "/?" : "?";
    const url = prefix + newPath + suffix + queryString.stringify(this.state.location.query);
    window.history.pushState({}, "", url);
    this.changeRoute(newPath, this.state.location.query);
  }

  getTipColorAttribute(node, cScale) {
    if (cScale.colorBy.slice(0,3) === "gt-") {
        return getGenotype(cScale.genotype[0][0],
                                  cScale.genotype[0][1],
                                  node, this.props.sequences.sequences);
    } else {
      return node.attr[cScale.colorBy];
    };
  }

  updateColorScale(colorBy) {
    const cScale = getColorScale(colorBy, this.props.tree, this.props.sequences);
    let gts = null;
    if (colorBy.slice(0,3) === "gt-" && this.props.sequences.geneLength) {
      gts = parseGenotype(colorBy, this.props.sequences.geneLength);
    }
    cScale.genotype = gts;
    let nodeColorAttr=null;
    let nodeColor=null;
    if (this.props.tree.nodes){
      nodeColorAttr = this.props.tree.nodes.map((n) => this.getTipColorAttribute(n, cScale));
      nodeColor = nodeColorAttr.map((n) => cScale.scale(n));
    }
    return {
      colorScale: cScale,
      nodeColor: nodeColor,
      nodeColorAttr: nodeColorAttr,
    };
  }

  parseFilterQuery(query) {
    const tmp = query.split("-").map( (d) => d.split("."));
    return {"fields": tmp.map( (d) => d[0] ), "filters": tmp.map( (d) => d[d.length-1].split(',') )};
  }

  tipVisibility(filters) {
    let upperLimit = +this.state.location.query.dmax;
    let lowerLimit = +this.state.location.query.dmin;
    if (!upperLimit) {
      upperLimit = 1000000000000;
    }
    if (!lowerLimit) {
      lowerLimit = -1000000000000;
    }
    if (this.props.tree.nodes){
      const filter_pairs = [];
      if (this.props.metadata && this.props.metadata.metadata) {
        for (const filter in this.props.metadata.metadata.controls){ // possible race condition with tree?
          const tmp = this.parseFilterQuery(this.state.location.query[filter] || "");
          for (let ii=0; ii<tmp.filters.length; ii+=1) {
            if (tmp.filters[ii] && tmp.fields[ii]){
              filter_pairs.push([tmp.fields[ii], tmp.filters[ii]]);
            }
          }
        }
      }
      if (filter_pairs.length){
        return this.props.tree.nodes.map((d) => (d.attr.num_date >= lowerLimit
                                         && d.attr.num_date < upperLimit
                                         && filter_pairs.every((x) => x[1].indexOf(d.attr[x[0]])>-1))
                                           ? "visible" : "hidden");
      } else {
        return this.props.tree.nodes.map((d) => (d.attr.num_date >= lowerLimit
                                         && d.attr.num_date < upperLimit)
                                            ? "visible" : "hidden");
      }
    } else {
      return "visible";
    }
  }

  changeRoute(pathname, query) {
    pathname = pathname.replace("!/", ""); // needed to assist with S3 redirects
    const prefix = (pathname === "" || pathname[0] === "/") ? "" : "/";
    const suffix = (pathname.length && pathname[pathname.length - 1] !== "/") ? "/?" : "?";
    const url = prefix + pathname + suffix + queryString.stringify(query);
    window.history.pushState({}, "", url);

    let new_colorData = {};
    //only update colorScales and nodeColors when changed
    if (query.colorBy && (query.colorBy !== this.state.colorScale.colorBy)) {
      new_colorData = this.updateColorScale(query.colorBy);
    }
    this.setState(Object.assign({location:{query, pathname}}, new_colorData));
  }

  currentFrequencies() {
    let freq = "";
    if (this.state.location.query.colorBy && this.state.location.query.colorBy.slice(0,3) === "gt-") {
      const gt = this.state.location.query.colorBy.slice(3).split("_");
      freq = "global_" + gt[0] + ":" + gt[1];
    }
    return freq;
  }

  /******************************************
   * HOVER EVENTS
   *****************************************/
  determineLegendMatch(selectedLegendItem, nodeAttr, legendBoundsMap) {
    let bool;
    // equates a tip and a legend element
    // exact match is required for categorical qunantities such as genotypes, regions
    // continuous variables need to fall into the interal (lower_bound[leg], leg]
    if (legendBoundsMap) {
      bool = (nodeAttr <= legendBoundsMap.upper_bound[selectedLegendItem]) &&
             (nodeAttr > legendBoundsMap.lower_bound[selectedLegendItem]);
    } else {
      bool = nodeAttr === selectedLegendItem;
    }
    return bool;
  }

  tipRadii() {
    const selItem = this.props.selectedLegendItem;
    if (selItem && this.state.nodeColorAttr){
      const legendMap = this.state.colorScale.continuous
                        ? this.state.colorScale.legendBoundsMap : false;
      return this.state.nodeColorAttr.map((d) => this.determineLegendMatch(selItem, d, legendMap) ? 6 : 3);
    } else if (this.state.nodeColorAttr) {
      return this.state.nodeColorAttr.map((d) => 3);
    } else {
      return null;
    }
  }
  /******************************************
   * SIDEBAR
   *****************************************/

  onSetSidebarOpen(open) {
    this.setState({sidebarOpen: open});
  }

  mediaQueryChanged() {
    this.setState({sidebarDocked: this.state.mql.matches});
  }

  /******************************************
   * RENDER
   *****************************************/
  render() {

    return (
      <Sidebar
        sidebar={
          <Controls changeRoute={this.changeRoute.bind(this)}
          location={this.state.location}
          colorOptions={this.props.metadata.color_options || colorOptions}
          colorScale={this.state.colorScale}/>
        }
        open={this.state.sidebarOpen}
        docked={this.state.sidebarDocked}
        onSetOpen={this.onSetSidebarOpen}>
        <Background>
          <Header/>
          <TreeView nodes={this.props.tree.nodes}
            nodeColorAttr={this.state.nodeColorAttr}
            colorScale={this.state.colorScale}
            nodeColor={this.state.nodeColor}
            tipRadii={this.tipRadii()}
            tipVisibility={this.tipVisibility()}
            layout={this.state.location.query.l || "rectangular"}
            distanceMeasure={this.state.location.query.m || "div"}
            datasetGuid={this.props.tree.datasetGuid}
          />
          <Frequencies genotype={this.currentFrequencies()}/>
          <Entropy
            changeRoute={this.changeRoute.bind(this)}
            location={this.state.location}
          />
          <Map nodes={this.props.tree.nodes} justGotNewDatasetRenderNewMap={false}/>
        </Background>
      </Sidebar>
    );
  }
}

// <Footer/>

export default App;
<|endoftext|>


<commit_before>'use strict';

import helpers from '../lib/helpers';
import skate from '../../src/skate';

describe('attributes:', function () {
  describe('default values', function () {
    it('should set a default value using the "value" option', function () {
      var tagName = helpers.safeTagName('my-el');
      var MyEl = skate(tagName.safe, {
        attributes: {
          test: {
            value: 'true'
          }
        }
      });

      expect(new MyEl().getAttribute('test')).to.equal('true');
    });

    it('should allow a callback to return a default value', function () {
      var tagName = helpers.safeTagName('my-el');
      var MyEl = skate(tagName.safe, {
        attributes: {
          test: {
            value: function () {
              return 'true';
            }
          }
        }
      });

      expect(new MyEl().getAttribute('test')).equal('true');
    });

    it('should not override if already specified', function () {
      var tagName = helpers.safeTagName('my-el');

      skate(tagName.safe, {
        attributes: {
          test: {
            value: 'true'
          }
        }
      });

      expect(
        skate.init(helpers.fixture('<my-el test="false"></my-el>', tagName))
          .firstChild
          .test
      ).to.equal('false');
    });
  });

  describe('should define properties for all watched attributes', function () {
    var myEl;
    var created = false;
    var updated = false;
    var removed = false;

    beforeEach(function () {
      var tagName = helpers.safeTagName('my-el');
      var MyEl = skate(tagName.safe, {
        attributes: {
          camelCased: {
            value: 'true'
          },
          'not-camel-cased': {
            value: 'true'
          },
          'test-proxy': {},
          'test-created': {
            value: 'true',
            created: function () {
              created = true;
            }
          },
          'test-lifecycle': {
            created: function () {
              created = true;
            },
            updated: function () {
              updated = true;
            },
            removed: function () {
              removed = true;
            }
          },
          'override': {
            value: 'false'
          }
        },
        prototype: {
          override: 'true'
        }
      });

      myEl = new MyEl();
    });

    it('should respect attributes that are already camel-cased', function () {
      expect(myEl.camelCased).to.equal('true');
    });

    it('should camel-case the property name and leave the attribute name as is', function () {
      expect(myEl.notCamelCased).to.equal('true');
    });

    it('should set the attribute when the property is set', function () {
      myEl.setAttribute('test-proxy', false);
      expect(myEl.testProxy).to.equal('false');
    });

    it('should set the property when the attribute is set', function () {
      myEl.testProxy = true;
      expect(myEl.getAttribute('test-proxy')).to.equal('true');
    });

    it('should call created after setting the default value', function (done) {
      helpers.afterMutations(function () {
        expect(myEl.testCreated).to.equal('true');
        expect(created).to.equal(true);
        done();
      });
    });

    it('should call created when the attribute is set for the first time', function (done) {
      myEl.testLifecycle = true;
      helpers.afterMutations(function () {
        expect(myEl.testLifecycle).to.equal('true');
        expect(created).to.equal(true);
        done();
      });
    });

    it('should call updated when the attribute is subsequently set', function (done) {
      myEl.testLifecycle = true;
      myEl.testLifecycle = false;
      helpers.afterMutations(function () {
        expect(updated).to.equal(true);
        done();
      });
    });

    it('should call removed when the attribute is set to "undefined".', function (done) {
      myEl.testLifecycle = true;
      myEl.testLifecycle = undefined;
      helpers.afterMutations(function () {
        expect(removed).to.equal(true);
        done();
      });
    });

    it('should not override an existing property', function (done) {
      myEl.setAttribute('override', 'false');
      helpers.afterMutations(function () {
        expect(myEl.override).to.equal('true');
        done();
      });
    });
  });

  describe('callbacks', function () {
    describe('should listen to changes in specified attributes', function () {
      function createAttributeDefinition (done) {
        return {
          test: {
            created: function (element, data) {
              expect(data.oldValue).to.equal(null);
              expect(data.newValue).to.equal('created');
            },
            updated: function (element, data) {
              expect(data.oldValue).to.equal('created');
              expect(data.newValue).to.equal('updated');
            },
            removed: function (element, data) {
              expect(data.oldValue).to.equal('updated');
              expect(data.newValue).to.equal(null);
              done();
            }
          }
        };
      }

      function assertAttributeChanges (element) {
        helpers.afterMutations(function () {
          element.setAttribute('test', 'created');
          helpers.afterMutations(function () {
            element.setAttribute('test', 'updated');
            helpers.afterMutations(function () {
              element.removeAttribute('test');
            });
          });
        });
      }

      it('for native custom elements', function (done) {
        var Element = skate(helpers.safeTagName('my-el').safe, {
          attributes: createAttributeDefinition(done)
        });

        assertAttributeChanges(new Element());
      });

      it('for existing elements (via mutation observer)', function (done) {
        var { safe: id } = helpers.safeTagName('element');

        skate(id, {
          attributes: createAttributeDefinition(done)
        });

        assertAttributeChanges(helpers.fixture(`<${id}></${id}>`).querySelector(id));
      });

      it('for attributes (via mutation observer)', function (done) {
        var { safe: id } = helpers.safeTagName('attribute');

        skate(id, {
          type: skate.type.ATTRIBUTE,
          attributes: createAttributeDefinition(done)
        });

        assertAttributeChanges(helpers.fixture(`<div ${id}></div>`).querySelector('div'));
      });

      it('for classnames (via mutation observer)', function (done) {
        var { safe: id } = helpers.safeTagName('classname');

        skate(id, {
          type: skate.type.CLASSNAME,
          attributes: createAttributeDefinition(done)
        });

        assertAttributeChanges(helpers.fixture(`<div class="${id}"></div>`).querySelector('div'));
      });
    });

    it('should accept a function insead of an object for a particular attribute definition.', function (done) {
      var tagName = helpers.safeTagName('my-el');
      var MyEl = skate(tagName.safe, {
        attributes: {
          test: function (element, data) {
            if (data.type === 'created') {
              expect(data.oldValue).to.equal(null);
              expect(data.newValue).to.equal('created');
            } else if (data.type === 'updated') {
              expect(data.oldValue).to.equal('created');
              expect(data.newValue).to.equal('updated');
            } else if (data.type === 'removed') {
              expect(data.oldValue).to.equal('updated');
              expect(data.newValue).to.equal(null);
              done();
            }
          }
        }
      });

      var myEl = new MyEl();
      myEl.setAttribute('test', 'created');
      helpers.afterMutations(function () {
        myEl.setAttribute('test', 'updated');
        helpers.afterMutations(function () {
          myEl.removeAttribute('test');
        });
      });
    });

    it('should accept a function insead of an object for the entire attribute definition.', function (done) {
      var tagName = helpers.safeTagName('my-el');
      var MyEl = skate(tagName.safe, {
        attributes: function (element, data) {
          if (data.name !== 'test') {
            return;
          }

          if (data.type === 'created') {
            expect(data.oldValue).to.equal(null);
            expect(data.newValue).to.equal('created');
          } else if (data.type === 'updated') {
            expect(data.oldValue).to.equal('created');
            expect(data.newValue).to.equal('updated');
          } else if (data.type === 'removed') {
            expect(data.oldValue).to.equal('updated');
            expect(data.newValue).to.equal(null);
            done();
          }
        }
      });

      var myEl = new MyEl();
      myEl.setAttribute('test', 'created');
      helpers.afterMutations(function () {
        myEl.setAttribute('test', 'updated');
        helpers.afterMutations(function () {
          myEl.removeAttribute('test');
        });
      });
    });

    describe('should allow a fallback callback to be specified that catches all changes (same as passing a function instead of an object)', function () {
      function assertAttributeLifeycleCalls(expectedNumCalls, nonFallbackHandlers, done) {
        var called = 0;
        var {safe: tagName} = helpers.safeTagName('my-el');
        var testHandlers = {
          fallback: function () {
            ++called;
          }
        };
        nonFallbackHandlers.forEach(function (item) {
          testHandlers[item] = function () {};
        });

        var MyEl = skate(tagName, {
          attributes: {
            test: testHandlers
          }
        });

        var myEl = new MyEl();
        myEl.test = false;
        helpers.afterMutations(function () {
          myEl.test = true;
          helpers.afterMutations(function () {
            myEl.test = undefined;
            helpers.afterMutations(function () {
              expect(called).to.equal(expectedNumCalls);
              done();
            });
          });
        });
      }

      it('fallback only', function (done) {
        assertAttributeLifeycleCalls(3, [], done);
      });

      it('created + fallback', function (done) {
        assertAttributeLifeycleCalls(2, ['created'], done);
      });

      it('updated + fallback', function (done) {
        assertAttributeLifeycleCalls(2, ['updated'], done);
      });

      it('removed + fallback', function (done) {
        assertAttributeLifeycleCalls(2, ['removed'], done);
      });
    });
  });

  describe('Attributes added via HTML', function () {
    it('should ensure attributes are initialised', function () {
      var called = false;
      var tagName = helpers.safeTagName('my-el');

      skate(tagName.safe, {
        attributes: function () {
          called = true;
        }
      });

      skate.init(helpers.fixture('<my-el some-attr></my-el>', tagName));
      expect(called).to.equal(true);
    });
  });

  describe('side effects', function () {
    it('should ensure an attribute exists before trying to action it just in case another attribute handler removes it', function () {
      var tagName = helpers.safeTagName('my-el');

      skate(tagName.safe, {
        attributes: function (element, data) {
          if (data.name === 'first') {
            element.removeAttribute('second');
          }
        }
      });

      skate.init(helpers.fixture('<my-el first></my-el>', tagName));
      expect(document.querySelector(tagName.safe).hasAttribute('first')).to.equal(true);
      expect(document.querySelector(tagName.safe).hasAttribute('second')).to.equal(false);
    });

    it('should iterate over every attribute even if one removed while it is still being processed', function () {
      var attributesCalled = 0;
      var tagName = helpers.safeTagName('my-el');

      skate(tagName.safe, {
        attributes: {
          id: {
            created: function (element) {
              element.removeAttribute('id');
              attributesCalled++;
            }
          },
          name: {
            created: function (element) {
              element.removeAttribute('name');
              attributesCalled++;
            }
          }
        }
      });

      skate.init(helpers.fixture('<my-el id="test" name="name"></my-el>', tagName));
      expect(attributesCalled).to.equal(2);
    });
  });

  describe('camelCasing', function () {
    var Ctor;
    var triggered;

    beforeEach(function () {
      Ctor = skate(helpers.safeTagName().safe, {
        attributes: {
          myAttribute: function () {
            triggered = true;
          }
        }
      });
      triggered = false;
    });

    it('attribute name in the attribute definition should convert camelCased attribute names to dash-case', function () {
      var el = new Ctor();
      el.setAttribute('my-attribute', '');
      expect(triggered).to.equal(true);
    });
  });
});
<commit_msg>References #165. Ensure assertions are made after mutations have a chance to execute.<commit_after>'use strict';

import helpers from '../lib/helpers';
import skate from '../../src/skate';

describe('attributes:', function () {
  describe('default values', function () {
    it('should set a default value using the "value" option', function () {
      var tagName = helpers.safeTagName('my-el');
      var MyEl = skate(tagName.safe, {
        attributes: {
          test: {
            value: 'true'
          }
        }
      });

      expect(new MyEl().getAttribute('test')).to.equal('true');
    });

    it('should allow a callback to return a default value', function () {
      var tagName = helpers.safeTagName('my-el');
      var MyEl = skate(tagName.safe, {
        attributes: {
          test: {
            value: function () {
              return 'true';
            }
          }
        }
      });

      expect(new MyEl().getAttribute('test')).equal('true');
    });

    it('should not override if already specified', function () {
      var tagName = helpers.safeTagName('my-el');

      skate(tagName.safe, {
        attributes: {
          test: {
            value: 'true'
          }
        }
      });

      expect(
        skate.init(helpers.fixture('<my-el test="false"></my-el>', tagName))
          .firstChild
          .test
      ).to.equal('false');
    });
  });

  describe('should define properties for all watched attributes', function () {
    var myEl;
    var created = false;
    var updated = false;
    var removed = false;

    beforeEach(function () {
      var tagName = helpers.safeTagName('my-el');
      var MyEl = skate(tagName.safe, {
        attributes: {
          camelCased: {
            value: 'true'
          },
          'not-camel-cased': {
            value: 'true'
          },
          'test-proxy': {},
          'test-created': {
            value: 'true',
            created: function () {
              created = true;
            }
          },
          'test-lifecycle': {
            created: function () {
              created = true;
            },
            updated: function () {
              updated = true;
            },
            removed: function () {
              removed = true;
            }
          },
          'override': {
            value: 'false'
          }
        },
        prototype: {
          override: 'true'
        }
      });

      myEl = new MyEl();
    });

    it('should respect attributes that are already camel-cased', function () {
      expect(myEl.camelCased).to.equal('true');
    });

    it('should camel-case the property name and leave the attribute name as is', function () {
      expect(myEl.notCamelCased).to.equal('true');
    });

    it('should set the attribute when the property is set', function () {
      myEl.setAttribute('test-proxy', false);
      expect(myEl.testProxy).to.equal('false');
    });

    it('should set the property when the attribute is set', function () {
      myEl.testProxy = true;
      expect(myEl.getAttribute('test-proxy')).to.equal('true');
    });

    it('should call created after setting the default value', function (done) {
      helpers.afterMutations(function () {
        expect(myEl.testCreated).to.equal('true');
        expect(created).to.equal(true);
        done();
      });
    });

    it('should call created when the attribute is set for the first time', function (done) {
      myEl.testLifecycle = true;
      helpers.afterMutations(function () {
        expect(myEl.testLifecycle).to.equal('true');
        expect(created).to.equal(true);
        done();
      });
    });

    it('should call updated when the attribute is subsequently set', function (done) {
      myEl.testLifecycle = true;
      myEl.testLifecycle = false;
      helpers.afterMutations(function () {
        expect(updated).to.equal(true);
        done();
      });
    });

    it('should call removed when the attribute is set to "undefined".', function (done) {
      myEl.testLifecycle = true;
      myEl.testLifecycle = undefined;
      helpers.afterMutations(function () {
        expect(removed).to.equal(true);
        done();
      });
    });

    it('should not override an existing property', function (done) {
      myEl.setAttribute('override', 'false');
      helpers.afterMutations(function () {
        expect(myEl.override).to.equal('true');
        done();
      });
    });
  });

  describe('callbacks', function () {
    describe('should listen to changes in specified attributes', function () {
      function createAttributeDefinition (done) {
        return {
          test: {
            created: function (element, data) {
              expect(data.oldValue).to.equal(null);
              expect(data.newValue).to.equal('created');
            },
            updated: function (element, data) {
              expect(data.oldValue).to.equal('created');
              expect(data.newValue).to.equal('updated');
            },
            removed: function (element, data) {
              expect(data.oldValue).to.equal('updated');
              expect(data.newValue).to.equal(null);
              done();
            }
          }
        };
      }

      function assertAttributeChanges (element) {
        helpers.afterMutations(function () {
          element.setAttribute('test', 'created');
          helpers.afterMutations(function () {
            element.setAttribute('test', 'updated');
            helpers.afterMutations(function () {
              element.removeAttribute('test');
            });
          });
        });
      }

      it('for native custom elements', function (done) {
        var Element = skate(helpers.safeTagName('my-el').safe, {
          attributes: createAttributeDefinition(done)
        });

        assertAttributeChanges(new Element());
      });

      it('for existing elements (via mutation observer)', function (done) {
        var { safe: id } = helpers.safeTagName('element');

        skate(id, {
          attributes: createAttributeDefinition(done)
        });

        assertAttributeChanges(helpers.fixture(`<${id}></${id}>`).querySelector(id));
      });

      it('for attributes (via mutation observer)', function (done) {
        var { safe: id } = helpers.safeTagName('attribute');

        skate(id, {
          type: skate.type.ATTRIBUTE,
          attributes: createAttributeDefinition(done)
        });

        assertAttributeChanges(helpers.fixture(`<div ${id}></div>`).querySelector('div'));
      });

      it('for classnames (via mutation observer)', function (done) {
        var { safe: id } = helpers.safeTagName('classname');

        skate(id, {
          type: skate.type.CLASSNAME,
          attributes: createAttributeDefinition(done)
        });

        assertAttributeChanges(helpers.fixture(`<div class="${id}"></div>`).querySelector('div'));
      });
    });

    it('should accept a function insead of an object for a particular attribute definition.', function (done) {
      var tagName = helpers.safeTagName('my-el');
      var MyEl = skate(tagName.safe, {
        attributes: {
          test: function (element, data) {
            if (data.type === 'created') {
              expect(data.oldValue).to.equal(null);
              expect(data.newValue).to.equal('created');
            } else if (data.type === 'updated') {
              expect(data.oldValue).to.equal('created');
              expect(data.newValue).to.equal('updated');
            } else if (data.type === 'removed') {
              expect(data.oldValue).to.equal('updated');
              expect(data.newValue).to.equal(null);
              done();
            }
          }
        }
      });

      var myEl = new MyEl();
      myEl.setAttribute('test', 'created');
      helpers.afterMutations(function () {
        myEl.setAttribute('test', 'updated');
        helpers.afterMutations(function () {
          myEl.removeAttribute('test');
        });
      });
    });

    it('should accept a function insead of an object for the entire attribute definition.', function (done) {
      var tagName = helpers.safeTagName('my-el');
      var MyEl = skate(tagName.safe, {
        attributes: function (element, data) {
          if (data.name !== 'test') {
            return;
          }

          if (data.type === 'created') {
            expect(data.oldValue).to.equal(null);
            expect(data.newValue).to.equal('created');
          } else if (data.type === 'updated') {
            expect(data.oldValue).to.equal('created');
            expect(data.newValue).to.equal('updated');
          } else if (data.type === 'removed') {
            expect(data.oldValue).to.equal('updated');
            expect(data.newValue).to.equal(null);
            done();
          }
        }
      });

      var myEl = new MyEl();
      myEl.setAttribute('test', 'created');
      helpers.afterMutations(function () {
        myEl.setAttribute('test', 'updated');
        helpers.afterMutations(function () {
          myEl.removeAttribute('test');
        });
      });
    });

    describe('should allow a fallback callback to be specified that catches all changes (same as passing a function instead of an object)', function () {
      function assertAttributeLifeycleCalls(expectedNumCalls, nonFallbackHandlers, done) {
        var called = 0;
        var {safe: tagName} = helpers.safeTagName('my-el');
        var testHandlers = {
          fallback: function () {
            ++called;
          }
        };
        nonFallbackHandlers.forEach(function (item) {
          testHandlers[item] = function () {};
        });

        var MyEl = skate(tagName, {
          attributes: {
            test: testHandlers
          }
        });

        var myEl = new MyEl();
        myEl.test = false;
        helpers.afterMutations(function () {
          myEl.test = true;
          helpers.afterMutations(function () {
            myEl.test = undefined;
            helpers.afterMutations(function () {
              expect(called).to.equal(expectedNumCalls);
              done();
            });
          });
        });
      }

      it('fallback only', function (done) {
        assertAttributeLifeycleCalls(3, [], done);
      });

      it('created + fallback', function (done) {
        assertAttributeLifeycleCalls(2, ['created'], done);
      });

      it('updated + fallback', function (done) {
        assertAttributeLifeycleCalls(2, ['updated'], done);
      });

      it('removed + fallback', function (done) {
        assertAttributeLifeycleCalls(2, ['removed'], done);
      });
    });
  });

  describe('Attributes added via HTML', function () {
    it('should ensure attributes are initialised', function () {
      var called = false;
      var tagName = helpers.safeTagName('my-el');

      skate(tagName.safe, {
        attributes: function () {
          called = true;
        }
      });

      skate.init(helpers.fixture('<my-el some-attr></my-el>', tagName));
      expect(called).to.equal(true);
    });
  });

  describe('side effects', function () {
    it('should ensure an attribute exists before trying to action it just in case another attribute handler removes it', function () {
      var tagName = helpers.safeTagName('my-el');

      skate(tagName.safe, {
        attributes: function (element, data) {
          if (data.name === 'first') {
            element.removeAttribute('second');
          }
        }
      });

      skate.init(helpers.fixture('<my-el first></my-el>', tagName));
      expect(document.querySelector(tagName.safe).hasAttribute('first')).to.equal(true);
      expect(document.querySelector(tagName.safe).hasAttribute('second')).to.equal(false);
    });

    it('should iterate over every attribute even if one removed while it is still being processed', function () {
      var attributesCalled = 0;
      var tagName = helpers.safeTagName('my-el');

      skate(tagName.safe, {
        attributes: {
          id: {
            created: function (element) {
              element.removeAttribute('id');
              attributesCalled++;
            }
          },
          name: {
            created: function (element) {
              element.removeAttribute('name');
              attributesCalled++;
            }
          }
        }
      });

      skate.init(helpers.fixture('<my-el id="test" name="name"></my-el>', tagName));
      expect(attributesCalled).to.equal(2);
    });
  });

  describe('camelCasing', function () {
    var Ctor;
    var triggered;

    beforeEach(function () {
      Ctor = skate(helpers.safeTagName().safe, {
        attributes: {
          myAttribute: function () {
            triggered = true;
          }
        }
      });
      triggered = false;
    });

    it('attribute name in the attribute definition should convert camelCased attribute names to dash-case', function (done) {
      var el = new Ctor();
      el.setAttribute('my-attribute', '');
      helpers.afterMutations(function () {
        expect(triggered).to.equal(true);
        done();
      });
    });
  });
});
<|endoftext|>


<commit_before>// Native
const { existsSync } = require('fs');
const { join, resolve } = require('path');

// External
const importLazy = require('import-lazy')(require);
const getContext = importLazy('@abcaustralia/postcss-config/getContext');
const CopyPlugin = importLazy('copy-webpack-plugin');
const ForkTsCheckerWebpackPlugin = importLazy('fork-ts-checker-webpack-plugin');
const MiniCssExtractPlugin = importLazy('mini-css-extract-plugin');
const OptimizeCssAssetsPlugin = importLazy('optimize-css-assets-webpack-plugin');
const sveltePreprocess = importLazy('svelte-preprocess');
const TerserPlugin = importLazy('terser-webpack-plugin');
const { VueLoaderPlugin } = importLazy('vue-loader');
const EnvironmentPlugin = importLazy('webpack/lib/EnvironmentPlugin');

// Ours
const { combine, merge } = require('../utils/structures');
const { getBabelConfig } = require('./babel');
const { getBuildConfig } = require('./build');
const { getProjectConfig } = require('./project');

const URL_LOADER_LIMIT = 10000;
const JSX_RESOLVE_EXTENSIONS = ['.jsx', '.tsx'];
const PROJECT_TYPES_CONFIG = {
  preact: {
    resolve: {
      alias: {
        react: 'preact',
        'react-dom': 'preact/compat'
      },

      extensions: JSX_RESOLVE_EXTENSIONS
    }
  },
  react: {
    resolve: {
      extensions: JSX_RESOLVE_EXTENSIONS
    }
  },
  svelte: config => {
    config.resolve.extensions.push('.svelte');

    const projectRoot = config.entry.index[0].split('src/')[0];
    const { include, loader, options } = getHintedRule(config, 'scripts');

    include.push(/(node_modules\/svelte)/);

    config.module.rules.push({
      test: /\.svelte$/,
      include,
      use: [
        {
          loader,
          options
        },
        {
          loader: require.resolve('svelte-loader'),
          options: {
            dev: config.mode === 'development',
            emitCss: getHintedRule(config, 'styles').use[0] === MiniCssExtractPlugin.loader,
            preprocess: sveltePreprocess()
          }
        }
      ]
    });

    return config;
  },
  vue: config => {
    // vue-style-loader is an internal dependency of vue-loader, and is a fork of style-loader
    getHintedRule(config, 'styles').use[0] = { loader: require.resolve('vue-style-loader') };

    config.module.rules.push({
      test: /\.vue$/,
      loader: require.resolve('vue-loader')
    });

    config.plugins.push(new VueLoaderPlugin());

    return config;
  }
};
const WEBPACK_CONFIG_FILE = 'webpack.config.js';

module.exports.getWebpackConfig = () => {
  const { root } = getProjectConfig();
  const { addModernJS, showDeprecations } = getBuildConfig();
  const customWebpackConfigFilePath = join(root, WEBPACK_CONFIG_FILE);

  if (showDeprecations) {
    process.traceDeprecation = true;
  } else {
    process.noDeprecation = true;
  }

  let config;

  // If the project has a webpack config file, use it, otherwise create our own
  if (existsSync(customWebpackConfigFilePath)) {
    config = require(customWebpackConfigFilePath);

    if (!Array.isArray(config)) {
      config = [config];
    }

    // Ensure functions are resolved to objects
    config = config.map(combine);
  } else {
    config = [createWebpackConfig()];

    // Duplicate the config if the project expects an additional modern JS build
    if (addModernJS) {
      config.push(createWebpackConfig({ isModernJS: true }));
    }
  }

  return config;
};

function createWebpackConfig({ isModernJS } = {}) {
  const { pkg, root, hasTS, type, webpack: projectWebpackConfig } = getProjectConfig();
  const { entry, extractCSS, from, staticDir, to, useCSSModules } = getBuildConfig();
  const isProd = process.env.NODE_ENV === 'production';

  const stylesDestination = extractCSS
    ? {
        loader: MiniCssExtractPlugin.loader,
        options: {
          hmr: !isProd
        }
      }
    : {
        loader: require.resolve('style-loader')
      };

  const config = merge(
    {
      mode: isProd ? 'production' : 'development',
      cache: true,
      entry: {
        index: [join(root, from, entry)]
      },
      devtool: 'source-map',
      output: {
        path: join(root, to),
        publicPath: '/',
        filename: isModernJS ? '[name]_modern.js' : '[name].js',
        jsonpFunction: `webpackJsonp__${pkg.name.replace(/[^a-z]/g, '_')}`,
        // The update file hash was causing 404s and full page reloads.
        // This will make the file name more predictable.
        // See https://github.com/webpack/webpack-dev-server/issues/79#issuecomment-244596129
        hotUpdateChunkFilename: 'hot/hot-update.js',
        hotUpdateMainFilename: 'hot/hot-update.json'
      },
      resolve: {
        extensions: ['.mjs', '.js', '.json', '.ts']
      },
      module: {
        rules: [
          {
            __hint__: 'scripts',
            test: hasTS ? /\.m?[jt]sx?$/ : /\.m?jsx?$/,
            include: [resolve(root, from)],
            loader: require.resolve('babel-loader'),
            options: getBabelConfig({ isModernJS })
          },
          {
            __hint__: 'styles/@abcaustralia',
            include: /(node_modules\/@abcaustralia\/*)/,
            test: /\.css$/,
            use: [
              stylesDestination,
              {
                loader: 'css-loader',
                options: {
                  importLoaders: 1,
                  modules: {
                    exportLocalsConvention: 'camelCase',
                    localIdentName: `${isProd ? '' : '[name]__[local]--'}[hash:base64:5]`
                  },
                  url: false
                }
              },
              {
                loader: 'postcss-loader',
                options: {
                  config: {
                    path: require.resolve('@abcaustralia/postcss-config'),
                    ctx: getContext(!isProd)
                  }
                }
              }
            ]
          },
          {
            __hint__: 'styles', // non-@abcaustralia
            test: /\.(css|scss)$/,
            use: [
              stylesDestination,
              {
                loader: require.resolve('css-loader'),
                options: {
                  modules: useCSSModules && {
                    exportLocalsConvention: 'camelCase',
                    localIdentContext: __dirname,
                    //  ^^^ https://github.com/webpack-contrib/css-loader/issues/413#issuecomment-299578180
                    localIdentName: `${isProd ? '' : '[folder]-[name]__[local]-'}[hash:base64:6]`,
                    localIdentHashPrefix: `${pkg.name}@${pkg.version}`
                  },
                  sourceMap: !isProd
                }
              },
              {
                loader: require.resolve('sass-loader')
              }
            ]
          },
          {
            test: /\.(jpg|png|gif|mp4|m4v|flv|mp3|wav|m4a)$/,
            loader: require.resolve('file-loader'),
            options: {
              name: '[name]-[hash].[ext]'
            }
          },
          {
            test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
            loader: require.resolve('url-loader'),
            options: {
              limit: URL_LOADER_LIMIT,
              mimetype: 'application/font-woff'
            }
          },
          {
            test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
            loader: require.resolve('url-loader'),
            options: {
              limit: URL_LOADER_LIMIT,
              mimetype: 'application/octet-stream'
            }
          },
          {
            test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
            loader: require.resolve('file-loader')
          },
          {
            test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
            loader: require.resolve('url-loader'),
            options: {
              limit: URL_LOADER_LIMIT,
              mimetype: 'image/svg+xml'
            }
          },
          {
            test: /\.html$/,
            loader: require.resolve('html-loader')
          }
        ]
      },
      plugins: [
        new EnvironmentPlugin(Object.keys(process.env)),
        hasTS
          ? new ForkTsCheckerWebpackPlugin({
              logger: { infrastructure: 'silent', issues: 'silent' },
              typescript: {
                diagnosticOptions: {
                  semantic: true,
                  syntactic: true
                }
              }
            })
          : null,
        extractCSS
          ? new MiniCssExtractPlugin({
              filename: `[name].css`
            })
          : null,
        extractCSS
          ? new OptimizeCssAssetsPlugin({
              assetNameRegExp: /\.css$/,
              cssProcessorOptions: {
                safe: true,
                discardComments: { removeAll: isProd },
                normalizeUrl: { stripWWW: false },
                canPrint: false
              }
            })
          : null,
        new CopyPlugin({
          patterns: [
            {
              from: join(root, staticDir)
            }
          ]
        })
      ].filter(x => x),
      optimization: {
        minimizer: [],
        namedModules: !isProd
      }
    },
    PROJECT_TYPES_CONFIG[type],
    projectWebpackConfig
  );

  if (isProd) {
    config.optimization.minimize = true;
    config.optimization.minimizer.push(
      new TerserPlugin({
        extractComments: false,
        terserOptions: {
          output: {
            comments: /@license/i
          }
        }
      })
    );
  }

  // Cleanup hints
  config.module.rules.forEach(rule => {
    if (rule.__hint__) {
      delete rule.__hint__;
    }
  });

  return config;
}

function getHintedRule(config, hint) {
  return config.module.rules.find(rule => rule.__hint__ === hint);
}
<commit_msg>Optionally add style loading for @abcaustralia components when we depend on them (excluding them from the standard loader stack). Warn about missing ABC package.json key if missing.<commit_after>// Native
const { existsSync } = require('fs');
const { join, resolve } = require('path');

// External
const importLazy = require('import-lazy')(require);
const getContext = importLazy('@abcaustralia/postcss-config/getContext');
const CopyPlugin = importLazy('copy-webpack-plugin');
const ForkTsCheckerWebpackPlugin = importLazy('fork-ts-checker-webpack-plugin');
const MiniCssExtractPlugin = importLazy('mini-css-extract-plugin');
const OptimizeCssAssetsPlugin = importLazy('optimize-css-assets-webpack-plugin');
const sveltePreprocess = importLazy('svelte-preprocess');
const TerserPlugin = importLazy('terser-webpack-plugin');
const { VueLoaderPlugin } = importLazy('vue-loader');
const EnvironmentPlugin = importLazy('webpack/lib/EnvironmentPlugin');

// Ours
const { combine, merge } = require('../utils/structures');
const { getBabelConfig } = require('./babel');
const { getBuildConfig } = require('./build');
const { getProjectConfig } = require('./project');

const URL_LOADER_LIMIT = 10000;
const JSX_RESOLVE_EXTENSIONS = ['.jsx', '.tsx'];
const PROJECT_TYPES_CONFIG = {
  preact: {
    resolve: {
      alias: {
        react: 'preact',
        'react-dom': 'preact/compat'
      },

      extensions: JSX_RESOLVE_EXTENSIONS
    }
  },
  react: {
    resolve: {
      extensions: JSX_RESOLVE_EXTENSIONS
    }
  },
  svelte: config => {
    config.resolve.extensions.push('.svelte');

    const projectRoot = config.entry.index[0].split('src/')[0];
    const { include, loader, options } = getHintedRule(config, 'scripts');

    include.push(/(node_modules\/svelte)/);

    config.module.rules.push({
      test: /\.svelte$/,
      include,
      use: [
        {
          loader,
          options
        },
        {
          loader: require.resolve('svelte-loader'),
          options: {
            dev: config.mode === 'development',
            emitCss: getHintedRule(config, 'styles').use[0] === MiniCssExtractPlugin.loader,
            preprocess: sveltePreprocess()
          }
        }
      ]
    });

    return config;
  },
  vue: config => {
    // vue-style-loader is an internal dependency of vue-loader, and is a fork of style-loader
    getHintedRule(config, 'styles').use[0] = { loader: require.resolve('vue-style-loader') };

    config.module.rules.push({
      test: /\.vue$/,
      loader: require.resolve('vue-loader')
    });

    config.plugins.push(new VueLoaderPlugin());

    return config;
  }
};
const WEBPACK_CONFIG_FILE = 'webpack.config.js';

module.exports.getWebpackConfig = () => {
  const { root } = getProjectConfig();
  const { addModernJS, showDeprecations } = getBuildConfig();
  const customWebpackConfigFilePath = join(root, WEBPACK_CONFIG_FILE);

  if (showDeprecations) {
    process.traceDeprecation = true;
  } else {
    process.noDeprecation = true;
  }

  let config;

  // If the project has a webpack config file, use it, otherwise create our own
  if (existsSync(customWebpackConfigFilePath)) {
    config = require(customWebpackConfigFilePath);

    if (!Array.isArray(config)) {
      config = [config];
    }

    // Ensure functions are resolved to objects
    config = config.map(combine);
  } else {
    config = [createWebpackConfig()];

    // Duplicate the config if the project expects an additional modern JS build
    if (addModernJS) {
      config.push(createWebpackConfig({ isModernJS: true }));
    }
  }

  return config;
};

function createWebpackConfig({ isModernJS } = {}) {
  const { pkg, root, hasTS, type, webpack: projectWebpackConfig } = getProjectConfig();
  const { entry, extractCSS, from, staticDir, to, useCSSModules } = getBuildConfig();
  const isProd = process.env.NODE_ENV === 'production';

  const config = merge(
    {
      mode: isProd ? 'production' : 'development',
      cache: true,
      entry: {
        index: [join(root, from, entry)]
      },
      devtool: 'source-map',
      output: {
        path: join(root, to),
        publicPath: '/',
        filename: isModernJS ? '[name]_modern.js' : '[name].js',
        jsonpFunction: `webpackJsonp__${pkg.name.replace(/[^a-z]/g, '_')}`,
        // The update file hash was causing 404s and full page reloads.
        // This will make the file name more predictable.
        // See https://github.com/webpack/webpack-dev-server/issues/79#issuecomment-244596129
        hotUpdateChunkFilename: 'hot/hot-update.js',
        hotUpdateMainFilename: 'hot/hot-update.json'
      },
      resolve: {
        extensions: ['.mjs', '.js', '.json', '.ts']
      },
      module: {
        rules: [
          {
            __hint__: 'scripts',
            test: hasTS ? /\.m?[jt]sx?$/ : /\.m?jsx?$/,
            include: [resolve(root, from)],
            loader: require.resolve('babel-loader'),
            options: getBabelConfig({ isModernJS })
          },
          {
            __hint__: 'styles',
            test: /\.(css|scss)$/,
            use: [
              extractCSS
                ? {
                    loader: MiniCssExtractPlugin.loader,
                    options: {
                      hmr: !isProd
                    }
                  }
                : {
                    loader: require.resolve('style-loader')
                  },
              {
                loader: require.resolve('css-loader'),
                options: {
                  modules: useCSSModules && {
                    exportLocalsConvention: 'camelCase',
                    localIdentContext: __dirname,
                    //  ^^^ https://github.com/webpack-contrib/css-loader/issues/413#issuecomment-299578180
                    localIdentName: `${isProd ? '' : '[folder]-[name]__[local]-'}[hash:base64:6]`,
                    localIdentHashPrefix: `${pkg.name}@${pkg.version}`
                  },
                  sourceMap: !isProd
                }
              },
              {
                loader: require.resolve('sass-loader')
              }
            ]
          },
          {
            test: /\.(jpg|png|gif|mp4|m4v|flv|mp3|wav|m4a)$/,
            loader: require.resolve('file-loader'),
            options: {
              name: '[name]-[hash].[ext]'
            }
          },
          {
            test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
            loader: require.resolve('url-loader'),
            options: {
              limit: URL_LOADER_LIMIT,
              mimetype: 'application/font-woff'
            }
          },
          {
            test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
            loader: require.resolve('url-loader'),
            options: {
              limit: URL_LOADER_LIMIT,
              mimetype: 'application/octet-stream'
            }
          },
          {
            test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
            loader: require.resolve('file-loader')
          },
          {
            test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
            loader: require.resolve('url-loader'),
            options: {
              limit: URL_LOADER_LIMIT,
              mimetype: 'image/svg+xml'
            }
          },
          {
            test: /\.html$/,
            loader: require.resolve('html-loader')
          }
        ]
      },
      plugins: [
        new EnvironmentPlugin(Object.keys(process.env)),
        hasTS
          ? new ForkTsCheckerWebpackPlugin({
              logger: { infrastructure: 'silent', issues: 'silent' },
              typescript: {
                diagnosticOptions: {
                  semantic: true,
                  syntactic: true
                }
              }
            })
          : null,
        extractCSS
          ? new MiniCssExtractPlugin({
              filename: `[name].css`
            })
          : null,
        extractCSS
          ? new OptimizeCssAssetsPlugin({
              assetNameRegExp: /\.css$/,
              cssProcessorOptions: {
                safe: true,
                discardComments: { removeAll: isProd },
                normalizeUrl: { stripWWW: false },
                canPrint: false
              }
            })
          : null,
        new CopyPlugin({
          patterns: [
            {
              from: join(root, staticDir)
            }
          ]
        })
      ].filter(x => x),
      optimization: {
        minimizer: [],
        namedModules: !isProd
      }
    },
    conditionallyEnableABCAustraliaStyles,
    PROJECT_TYPES_CONFIG[type],
    projectWebpackConfig
  );

  if (isProd) {
    config.optimization.minimize = true;
    config.optimization.minimizer.push(
      new TerserPlugin({
        extractComments: false,
        terserOptions: {
          output: {
            comments: /@license/i
          }
        }
      })
    );
  }

  // Cleanup hints
  config.module.rules.forEach(rule => {
    if (rule.__hint__) {
      delete rule.__hint__;
    }
  });

  return config;
}

function getHintedRule(config, hint) {
  return config.module.rules.find(rule => rule.__hint__ === hint);
}

const ABC_POSTCSS_CONTEXT_SUGGESTION = `

To compile @abcaustralia/* styles, you need to add the following to your package.json:

"abc": {
  "css": {
    "libraryDir": "./node_modules/@abcaustralia/nucleus/css",
    "logVariables": "false"
  }
}
`;

function getABCAustraliaPostCSSContext(isDev) {
  try {
    return getContext(isDev);
  } catch (err) {
    if (err.message.indexOf('css') > -1) {
      err.message += ABC_POSTCSS_CONTEXT_SUGGESTION;
    }

    throw err;
  }
}

const ABC_PACKAGE_PATTERN = /(node_modules\/@abcaustralia\/*)/;

function conditionallyEnableABCAustraliaStyles(config) {
  const { pkg } = getProjectConfig();

  // Only enable if we have an @abcaustralia/* dependency
  if (!Object.keys(pkg.dependencies || {}).find(x => x.indexOf('@abcaustralia/') === 0)) {
    return config;
  }

  const isProd = config.mode === 'production';
  const stylesRule = getHintedRule(config, 'styles');

  stylesRule.exclude = ABC_PACKAGE_PATTERN;

  config.module.rules.push({
    __hint__: 'styles/@abcaustralia',
    test: /\.css$/,
    include: ABC_PACKAGE_PATTERN,
    use: [
      stylesRule.use[0],
      {
        loader: require.resolve('css-loader'),
        options: {
          importLoaders: 1,
          modules: {
            exportLocalsConvention: 'camelCase',
            localIdentName: `${isProd ? '' : '[name]__[local]--'}[hash:base64:5]`
          },
          url: false
        }
      },
      {
        loader: require.resolve('postcss-loader'),
        options: {
          config: {
            path: require.resolve('@abcaustralia/postcss-config'),
            ctx: getABCAustraliaPostCSSContext(!isProd)
          }
        }
      }
    ]
  });

  return config;
}
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import { gql, withApollo } from 'react-apollo'

import { GC_USER, GC_USER_ID } from '../constants'
import { debounce } from '../common/utils'

class App extends Component {

  constructor(props) {
    super(props);

    this.lookupUser = debounce(this.lookupUser, 100).bind(this);
  }

  state = {
    friendName: '',
    userList: [],
    contactRequestSent: false
  }

  async lookupUser() {
    const friendName = this.state.friendName;
    if (friendName && friendName.length > 3) {
      // Send query to find friend
      const result = await this.props.client.query({
        query: ALL_USERS_SEARCH_QUERY,
        variables: {
          uid: localStorage.getItem(GC_USER_ID),
          searchText: friendName
        }
      })
      const users = result.data.allUsers
      this.setState({ userList: users});
    }
  }

  async sendContactRequest(user) {
    const result = await this.props.client.mutate({
      mutation: CREATE_CONTACT_MUTATION,
      variables: {
        pursuerId: localStorage.getItem(GC_USER_ID),
        pursuedId: user.id
      }
    })
    const data = result.data
    this.setState({ contactRequestSent: true });

    setTimeout(() => {
      this.setState({ contactRequestSent: false });
    }, 3000);
  }

  render() {

    const user = JSON.parse(localStorage.getItem(GC_USER));
    const { friendName, userList, contactRequestSent } = this.state;

    return (

          { contactRequestSent &&

              Contact request sent!

          }

Welcome to the app!

          { !user.pursued.length &&

              To get started, look up your friends or invite them to the app so you can share or give things away to each other.

          }

Find a friend

            <form>
              <input
                value={this.state.friendName}
                onChange={ (e) => {
                  this.setState({ friendName: e.target.value });
                  this.lookupUser();
                } }
                type='text'
                placeholder="Your Friend's Name"
              />
            </form>
            { friendName && friendName.length > 3 && userList.length > 0 &&
              userList.map((user, index) => (

                  {user.name}
                  <button onClick={ () => this.sendContactRequest(user) }>Connect</button>

              ))
            }

    )
  }
}

const ALL_USERS_SEARCH_QUERY = gql`
  query AllUsersSearchQuery($uid: ID!, $searchText: String!) {
    allUsers(filter: {
      AND: [{
        id_not: $uid
      }, {
        name_contains: $searchText
      }]
    }) {
      id
      name
    }
  }
`

const CREATE_CONTACT_MUTATION = gql`
  mutation CreateContactMutation ($pursuerId: ID!, $pursuedId: ID!) {
    createContact(
      pursuerId: $pursuerId,
      pursuedId: $pursuedId,
      status: PENDING
    ) {
      id
    }
  }
`

export default withApollo(App)
<commit_msg>User sees contact requests when he signs in<commit_after>import React, { Component } from 'react'
import { gql, withApollo } from 'react-apollo'

import { GC_USER, GC_USER_ID } from '../constants'
import { debounce } from '../common/utils'

class App extends Component {

  constructor(props) {
    super(props);

    this.lookupUser = debounce(this.lookupUser, 100).bind(this);
  }

  componentDidMount() {
    this.getContactRequests();
  }

  state = {
    friendName: '',
    userList: [],
    contactRequestSent: false,
    contactRequestsPending: []
  }

  async lookupUser() {
    const friendName = this.state.friendName;
    if (friendName && friendName.length > 3) {
      // Send query to find friend
      const result = await this.props.client.query({
        query: ALL_USERS_SEARCH_QUERY,
        variables: {
          uid: localStorage.getItem(GC_USER_ID),
          searchText: friendName
        }
      })
      const users = result.data.allUsers
      this.setState({ userList: users});
    }
  }

  async sendContactRequest(user) {
    const result = await this.props.client.mutate({
      mutation: CREATE_CONTACT_MUTATION,
      variables: {
        pursuerId: localStorage.getItem(GC_USER_ID),
        pursuedId: user.id
      }
    })
    const data = result.data
    this.setState({ contactRequestSent: true });

    setTimeout(() => {
      this.setState({ contactRequestSent: false });
    }, 3000);
  }

  async getContactRequests() {
    // Send query to find friend
    const result = await this.props.client.query({
      query: ALL_CONTACTS_SEARCH_QUERY,
      variables: {
        uid: localStorage.getItem(GC_USER_ID),
      }
    })
    const contactRequests = result.data.allContacts
    this.setState({ contactRequestsPending: contactRequests });
  }

  render() {

    const user = JSON.parse(localStorage.getItem(GC_USER));
    const { friendName, userList, contactRequestSent, contactRequestsPending } = this.state;

    return (

          { contactRequestsPending.length > 0 &&
            contactRequestsPending.map((contactRequest, index) => (

                {contactRequest.pursuer.name} would like to exchange things with you
                <button onClick={ () => this.acceptContactRequest() }>Accept</button>
                <button onClick={ () => this.refuseContactRequest() }>Reject</button>

            ))
          }
          { contactRequestSent &&

              Contact request sent!

          }

Welcome to the app!

          { !user.pursued.length &&

              To get started, look up your friends or invite them to the app so you can share or give things away to each other.

          }

Find a friend

            <form>
              <input
                value={this.state.friendName}
                onChange={ (e) => {
                  this.setState({ friendName: e.target.value });
                  this.lookupUser();
                } }
                type='text'
                placeholder="Your Friend's Name"
              />
            </form>
            { friendName && friendName.length > 3 && userList.length > 0 &&
              userList.map((user, index) => (

                  {user.name}
                  <button onClick={ () => this.sendContactRequest(user) }>Connect</button>

              ))
            }

    )
  }
}

const ALL_USERS_SEARCH_QUERY = gql`
  query AllUsersSearchQuery($uid: ID!, $searchText: String!) {
    allUsers(filter: {
      AND: [{
        id_not: $uid
      }, {
        name_contains: $searchText
      }]
    }) {
      id
      name
    }
  }
`

const ALL_CONTACTS_SEARCH_QUERY = gql`
  query AllContactsSearchQuery($uid: ID!) {
    allContacts(filter: {
      AND: [{
        pursued: {
          id: $uid
        }
      }, {
        status: PENDING
      }]
    }) {
      id
      pursuer {
        id
        name
      }
    }
  }
`

const CREATE_CONTACT_MUTATION = gql`
  mutation CreateContactMutation ($pursuerId: ID!, $pursuedId: ID!) {
    createContact(
      pursuerId: $pursuerId,
      pursuedId: $pursuedId,
      status: PENDING
    ) {
      id
    }
  }
`

export default withApollo(App)
<|endoftext|>


<commit_before>"use strict";

var _   = require("../utils");
var FFT = require("../dsp/fft");

function NeuBuffer(context, buffer) {
  this.$context = context;
  this._buffer = buffer;

  Object.defineProperties(this, {
    sampleRate: {
      value: this._buffer.sampleRate,
      enumerable: true
    },
    length: {
      value: this._buffer.length,
      enumerable: true
    },
    duration: {
      value: this._buffer.duration,
      enumerable: true
    },
    numberOfChannels: {
      value: this._buffer.numberOfChannels,
      enumerable: true
    },
  });

  for (var i = 0; i < this._buffer.numberOfChannels; i++) {
    Object.defineProperty(this, i, {
      value: this._buffer.getChannelData(i)
    });
  }
}
NeuBuffer.$name = "NeuBuffer";

NeuBuffer.create = function(context, channels, length, sampleRate) {
  channels   = _.int(_.defaults(channels, 1));
  length     = _.int(_.defaults(length, 0));
  sampleRate = _.int(_.defaults(sampleRate, context.sampleRate));

  return new NeuBuffer(context, context.createBuffer(channels, length, sampleRate));
};

NeuBuffer.from = function(context, data) {
  var buffer = context.createBuffer(1, data.length, context.sampleRate);
  var chData = buffer.getChannelData(0);

  for (var i = 0, imax = data.length; i < imax; i++) {
    chData[i] = data[i];
  }

  return new NeuBuffer(context, buffer);
};

NeuBuffer.load = function(context, url) {
  return new window.Promise(function(resolve, reject) {
    loadWithXHR(url).then(function(audioData) {
      return decodeAudioData(context, audioData);
    }).then(function(decodedData) {
      resolve(new NeuBuffer(context, decodedData));
    }).catch(function(e) {
      reject(e);
    });
  });
};

function loadWithXHR(url) {
  return new window.Promise(function(resolve, reject) {
    var xhr = new window.XMLHttpRequest();

    xhr.open("GET", url);
    xhr.responseType = "arraybuffer";

    xhr.onload = function() {
      resolve(xhr.response);
    };

    xhr.onerror = function() {
      reject({/* TODO: error object */});
    };

    xhr.send();
  });
}

function decodeAudioData(context, audioData) {
  return new window.Promise(function(resolve, reject) {
    context.decodeAudioData(audioData, function(decodedData) {
      resolve(decodedData);
    }, function() {
      reject({/* TODO: error object */});
    });
  });
}

NeuBuffer.prototype.getChannelData = function(ch) {
  ch = _.clip(_.int(ch), 0, this.numberOfChannels - 1);

  return this._buffer.getChannelData(ch);
};

NeuBuffer.prototype.concat = function() {
  var args = _.toArray(arguments).filter(function(elem) {
    return (elem instanceof NeuBuffer) && (this.numberOfChannels === elem.numberOfChannels);
  }, this);
  var channels = this.numberOfChannels;
  var length = args.reduce(function(a, b) {
    return a + b.length;
  }, this.length);
  var sampleRate = this.sampleRate;
  var buffer = this.$context.createBuffer(channels, length, sampleRate);

  args.unshift(this);

  var argslen = args.length;

  for (var i = 0; i < channels; i++) {
    var data = buffer.getChannelData(i);
    var pos  = 0;
    for (var j = 0; j < argslen; j++) {
      data.set(args[j][i], pos);
      pos += args[j].length;
    }
  }

  return new NeuBuffer(this.$context, buffer);
};

NeuBuffer.prototype.reverse = function() {
  var channels = this.numberOfChannels;
  var buffer = this.$context.createBuffer(channels, this.length, this.sampleRate);

  for (var i = 0; i < channels; i++) {
    buffer.getChannelData(i).set(_.toArray(this[i]).reverse());
  }

  return new NeuBuffer(this.$context, buffer);
};

NeuBuffer.prototype.slice = function(start, end) {
  start = _.int(_.defaults(start, 0));
  end   = _.int(_.defaults(end  , this.length));

  if (start < 0) {
    start += this.length;
  } else {
    start = Math.min(start, this.length);
  }
  if (end < 0) {
    end += this.length;
  } else {
    end = Math.min(end, this.length);
  }

  var channels = this.numberOfChannels;
  var length = end - start;
  var sampleRate = this.sampleRate;
  var buffer = null;

  if (length <= 0) {
    buffer = this.$context.createBuffer(channels, 1, sampleRate);
  } else {
    buffer = this.$context.createBuffer(channels, length, sampleRate);
    for (var i = 0; i < channels; i++) {
      buffer.getChannelData(i).set(this[i].subarray(start, end));
    }
  }

  return new NeuBuffer(this.$context, buffer);
};

NeuBuffer.prototype.split = function(n) {
  n = _.int(_.defaults(n, 2));

  if (n <= 0) {
    return [];
  }

  var result = new Array(n);
  var len = this.length / n;
  var start = 0;
  var end   = 0;

  for (var i = 0; i < n; i++) {
    end = Math.round(start + len);
    result[i] = this.slice(start, end);
    start = end;
  }

  return result;
};

NeuBuffer.prototype.normalize = function() {
  var channels = this.numberOfChannels;
  var buffer = this.$context.createBuffer(channels, this.length, this.sampleRate);

  for (var i = 0; i < channels; i++) {
    buffer.getChannelData(i).set(normalize(this[i]));
  }

  return new NeuBuffer(this.$context, buffer);
};

NeuBuffer.prototype.resample = function(size, interpolation) {
  size = Math.max(0, _.int(_.defaults(size, this.length)));
  interpolation = !!_.defaults(interpolation, true);

  var channels = this.numberOfChannels;
  var buffer = this.$context.createBuffer(channels, size, this.sampleRate);

  for (var i = 0; i < channels; i++) {
    buffer.getChannelData(i).set(resample(this[i], size, interpolation));
  }

  return new NeuBuffer(this.$context, buffer);
};

NeuBuffer.prototype.toAudioBuffer = function() {
  return this._buffer;
};

NeuBuffer.prototype.toPeriodicWave = function(ch) {
  ch = _.clip(_.int(ch), 0, this.numberOfChannels - 1);

  var buffer = this._buffer.getChannelData(ch);

  if (4096 < buffer.length) {
    buffer = buffer.subarray(0, 4096);
  }

  var fft = FFT.forward(buffer);

  return this.$context.createPeriodicWave(fft.real, fft.imag);
};

function normalize(data) {
  var maxamp = peak(data);

  /* istanbul ignore else */
  if (maxamp !== 0) {
    var ampfac = 1 / maxamp;
    for (var i = 0, imax = data.length; i < imax; ++i) {
      data[i] *= ampfac;
    }
  }

  return data;
}

function peak(data) {
  var maxamp = 0;

  for (var i = 0, imax = data.length; i < imax; ++i) {
    var absamp = Math.abs(data[i]);
    if (maxamp < absamp) {
      maxamp = absamp;
    }
  }

  return maxamp;
}

function resample(data, size, interpolation) {
  if (data.length === size) {
    return new Float32Array(data);
  }

  if (interpolation) {
    return resample1(data, size);
  }

  return resample0(data, size);
}

function resample0(data, size) {
  var factor = (data.length - 1) / (size - 1);
  var result = new Float32Array(size);

  for (var i = 0; i < size; i++) {
    result[i] = data[Math.round(i * factor)];
  }

  return result;
}

function resample1(data, size) {
  var factor = (data.length - 1) / (size - 1);
  var result = new Float32Array(size);
  var len = data.length - 1;

  for (var i = 0; i < size; i++) {
    var x  = i * factor;
    var x0 = x|0;
    var x1 = Math.min(x0 + 1, len);
    result[i] = data[x0] + Math.abs(x - x0) * (data[x1] - data[x0]);
  }

  return result;
}

module.exports = NeuBuffer;
<commit_msg>fix NeuBuffer.from<commit_after>"use strict";

var _   = require("../utils");
var FFT = require("../dsp/fft");

function NeuBuffer(context, buffer) {
  this.$context = context;
  this._buffer = buffer;

  Object.defineProperties(this, {
    sampleRate: {
      value: this._buffer.sampleRate,
      enumerable: true
    },
    length: {
      value: this._buffer.length,
      enumerable: true
    },
    duration: {
      value: this._buffer.duration,
      enumerable: true
    },
    numberOfChannels: {
      value: this._buffer.numberOfChannels,
      enumerable: true
    },
  });

  for (var i = 0; i < this._buffer.numberOfChannels; i++) {
    Object.defineProperty(this, i, {
      value: this._buffer.getChannelData(i)
    });
  }
}
NeuBuffer.$name = "NeuBuffer";

NeuBuffer.create = function(context, channels, length, sampleRate) {
  channels   = _.int(_.defaults(channels, 1));
  length     = _.int(_.defaults(length, 0));
  sampleRate = _.int(_.defaults(sampleRate, context.sampleRate));

  return new NeuBuffer(context, context.createBuffer(channels, length, sampleRate));
};

NeuBuffer.from = function(context, data) {
  var buffer = context.createBuffer(1, data.length, context.sampleRate);

  buffer.getChannelData(0).set(data);

  return new NeuBuffer(context, buffer);
};

NeuBuffer.load = function(context, url) {
  return new window.Promise(function(resolve, reject) {
    loadWithXHR(url).then(function(audioData) {
      return decodeAudioData(context, audioData);
    }).then(function(decodedData) {
      resolve(new NeuBuffer(context, decodedData));
    }).catch(function(e) {
      reject(e);
    });
  });
};

function loadWithXHR(url) {
  return new window.Promise(function(resolve, reject) {
    var xhr = new window.XMLHttpRequest();

    xhr.open("GET", url);
    xhr.responseType = "arraybuffer";

    xhr.onload = function() {
      resolve(xhr.response);
    };

    xhr.onerror = function() {
      reject({/* TODO: error object */});
    };

    xhr.send();
  });
}

function decodeAudioData(context, audioData) {
  return new window.Promise(function(resolve, reject) {
    context.decodeAudioData(audioData, function(decodedData) {
      resolve(decodedData);
    }, function() {
      reject({/* TODO: error object */});
    });
  });
}

NeuBuffer.prototype.getChannelData = function(ch) {
  ch = _.clip(_.int(ch), 0, this.numberOfChannels - 1);

  return this._buffer.getChannelData(ch);
};

NeuBuffer.prototype.concat = function() {
  var args = _.toArray(arguments).filter(function(elem) {
    return (elem instanceof NeuBuffer) && (this.numberOfChannels === elem.numberOfChannels);
  }, this);
  var channels = this.numberOfChannels;
  var length = args.reduce(function(a, b) {
    return a + b.length;
  }, this.length);
  var sampleRate = this.sampleRate;
  var buffer = this.$context.createBuffer(channels, length, sampleRate);

  args.unshift(this);

  var argslen = args.length;

  for (var i = 0; i < channels; i++) {
    var data = buffer.getChannelData(i);
    var pos  = 0;
    for (var j = 0; j < argslen; j++) {
      data.set(args[j][i], pos);
      pos += args[j].length;
    }
  }

  return new NeuBuffer(this.$context, buffer);
};

NeuBuffer.prototype.reverse = function() {
  var channels = this.numberOfChannels;
  var buffer = this.$context.createBuffer(channels, this.length, this.sampleRate);

  for (var i = 0; i < channels; i++) {
    buffer.getChannelData(i).set(_.toArray(this[i]).reverse());
  }

  return new NeuBuffer(this.$context, buffer);
};

NeuBuffer.prototype.slice = function(start, end) {
  start = _.int(_.defaults(start, 0));
  end   = _.int(_.defaults(end  , this.length));

  if (start < 0) {
    start += this.length;
  } else {
    start = Math.min(start, this.length);
  }
  if (end < 0) {
    end += this.length;
  } else {
    end = Math.min(end, this.length);
  }

  var channels = this.numberOfChannels;
  var length = end - start;
  var sampleRate = this.sampleRate;
  var buffer = null;

  if (length <= 0) {
    buffer = this.$context.createBuffer(channels, 1, sampleRate);
  } else {
    buffer = this.$context.createBuffer(channels, length, sampleRate);
    for (var i = 0; i < channels; i++) {
      buffer.getChannelData(i).set(this[i].subarray(start, end));
    }
  }

  return new NeuBuffer(this.$context, buffer);
};

NeuBuffer.prototype.split = function(n) {
  n = _.int(_.defaults(n, 2));

  if (n <= 0) {
    return [];
  }

  var result = new Array(n);
  var len = this.length / n;
  var start = 0;
  var end   = 0;

  for (var i = 0; i < n; i++) {
    end = Math.round(start + len);
    result[i] = this.slice(start, end);
    start = end;
  }

  return result;
};

NeuBuffer.prototype.normalize = function() {
  var channels = this.numberOfChannels;
  var buffer = this.$context.createBuffer(channels, this.length, this.sampleRate);

  for (var i = 0; i < channels; i++) {
    buffer.getChannelData(i).set(normalize(this[i]));
  }

  return new NeuBuffer(this.$context, buffer);
};

NeuBuffer.prototype.resample = function(size, interpolation) {
  size = Math.max(0, _.int(_.defaults(size, this.length)));
  interpolation = !!_.defaults(interpolation, true);

  var channels = this.numberOfChannels;
  var buffer = this.$context.createBuffer(channels, size, this.sampleRate);

  for (var i = 0; i < channels; i++) {
    buffer.getChannelData(i).set(resample(this[i], size, interpolation));
  }

  return new NeuBuffer(this.$context, buffer);
};

NeuBuffer.prototype.toAudioBuffer = function() {
  return this._buffer;
};

NeuBuffer.prototype.toPeriodicWave = function(ch) {
  ch = _.clip(_.int(ch), 0, this.numberOfChannels - 1);

  var buffer = this._buffer.getChannelData(ch);

  if (4096 < buffer.length) {
    buffer = buffer.subarray(0, 4096);
  }

  var fft = FFT.forward(buffer);

  return this.$context.createPeriodicWave(fft.real, fft.imag);
};

function normalize(data) {
  var maxamp = peak(data);

  /* istanbul ignore else */
  if (maxamp !== 0) {
    var ampfac = 1 / maxamp;
    for (var i = 0, imax = data.length; i < imax; ++i) {
      data[i] *= ampfac;
    }
  }

  return data;
}

function peak(data) {
  var maxamp = 0;

  for (var i = 0, imax = data.length; i < imax; ++i) {
    var absamp = Math.abs(data[i]);
    if (maxamp < absamp) {
      maxamp = absamp;
    }
  }

  return maxamp;
}

function resample(data, size, interpolation) {
  if (data.length === size) {
    return new Float32Array(data);
  }

  if (interpolation) {
    return resample1(data, size);
  }

  return resample0(data, size);
}

function resample0(data, size) {
  var factor = (data.length - 1) / (size - 1);
  var result = new Float32Array(size);

  for (var i = 0; i < size; i++) {
    result[i] = data[Math.round(i * factor)];
  }

  return result;
}

function resample1(data, size) {
  var factor = (data.length - 1) / (size - 1);
  var result = new Float32Array(size);
  var len = data.length - 1;

  for (var i = 0; i < size; i++) {
    var x  = i * factor;
    var x0 = x|0;
    var x1 = Math.min(x0 + 1, len);
    result[i] = data[x0] + Math.abs(x - x0) * (data[x1] - data[x0]);
  }

  return result;
}

module.exports = NeuBuffer;
<|endoftext|>