<commit_before>//= require ./main

"use strict";

var
    esPhinx;

(function ($) {

    $.extend({
        prototype: {
            removeAttr: function (attrName) {
                var
                    self = this;

                self.each(function (node) {
                    node.removeAttribute(attrName);
                });

                return self;
            },

            attr: function (attrName, value) {
                var
                    self = this,
                    first = self.first();

                if (self.length === 0) {
                    return self;
                }

                if (typeof value === "boolean" || value || value === "") {
                    self.each(function (node) {
                        node.setAttribute(attrName, value);
                    });

                    return self;
                } else {
                    if (first.attributes[attrName]) {
                        return first.attributes[attrName].value;
                    }
                }

                return undefined;
            },

            property: function (propertyName, value) {
                var
                    self = this,
                    first = self.first();

                if (self.length === 0) {
                    return self;
                }

                if (typeof value === "boolean" || value || value === "") {
                    self.each(function (node) {
                        node[propertyName] = value;
                    });

                    return self;
                } else {
                    if (first[propertyName] ||
                        typeof first[propertyName] === "boolean") {
                        return first[propertyName];
                    }
                }

                return undefined;
            },

            value: function (value) {
                var
                    self = this;

                if (value || value === "") {
                    self.property("value", value);
                    return self;
                }

                return self.property("value");
            },

            checked: function () {
                return this.property("checked");
            },

            check: function () {
                var
                    self = this;

                if (!self.checked()) {
                    self.property("checked", true);
                }

                return self;
            },

            uncheck: function () {
                var
                    self = this;

                return self.property("checked", false);
            },

            data: function (attrName, value) {
                return this.attr("data-" + attrName, value);
            },

            params: function () {
                var
                    self = this,
                    resolvedParams = {},
                    index,
                    name,
                    param,
                    key,
                    merged,
                    attr,
                    nested_attr,

                    resolveParam = function (name, value) {
                        value = value || "";

                        var
                            structure = name,
                            parsed;

                        structure = "{\"" + structure;
                        // obj[nested_attr][][attr]
                        if (/.+\[.+\]\[\]\[.+\](?!.)/.test(name)) {
                            // debugger
                            structure = structure
                                .replace(/\]\[\]\[/, "\":[{\"")
                                .replace(/\[(?![\{])/g, "\":{\"")
                                .replace(/\]$/g, "\":\"" + value + "\"}]}}");
                        // obj[nested_attr][0-9+][attr]
                        } else if (/.+\[.+\]\[[0-9]+\]\[.+\](?!.)/.test(name)) {
                            // debugger
                            index = structure.split(/\[([0-9]+)/)[1];
                            structure = structure
                                .replace(/\[(?!([0-9]+|\]))/g, "\":{\"")
                                .replace(/\]\[[0-9]+\]/g, "\":{\"" + index)
                                .replace(/\]$/g, "\":\"" + value + "\"}}}}");
                        // obj[attr][]
                        } else if (/.+\[.+\]\[\](?!.)/.test(name)) {
                            // debugger
                            structure = structure
                                .replace(/\]\[/, "\":[\"")
                                .replace(/\[(?!\")/g, "\":{\"")
                                .replace(/\]$/g, value + "\"]}}");
                        // obj[attr][0-9+]
                        // } else if (/.+\[.+\]\[[0-9]+\](?!.)/.test(name)) {
                        //     index = structure.split(/\[([0-9]+)/)[1];
                        //     structure = structure
                        //         .replace(/\[(?!([0-9]+|\]))/g, "\":{\"")
                        //         .replace(/\]\[[0-9]+\]/g, "\":{\"" + index + "\":[\"" + value + "\"]}}}");
                            // debugger
                        // obj[attr]
                        } else if (/.+\[.+\](?!.)/.test(name)) {
                            structure = structure
                                .replace(/\[/g, "\":{\"")
                                .replace(/\]$/g, "\":\"" + value + "\"}}");
                        // attr
                        } else {
                            structure += "\":\"" + value + "\"}";
                        }

                        if (parsed = JSON.parse(structure)) {
                            return parsed;
                        }
                        return false;
                    };

                self.each(function (form) {
                    if (form instanceof HTMLFormElement) {
                        $.each(form.elements, function (e, i) {
                            if (name = e.name) {
                                if (param = resolveParam(e.name, e.value)) {
                                    key = Object.keys(param)[0];
                                    if (resolvedParams[key]) {
                                        attr = Object.keys(param[key])[0];
                                        if (resolvedParams[key][attr]
                                            instanceof Array) {

                                            index = Object
                                                .keys(resolvedParams[key][attr])
                                                    .last();

                                            nested_attr = Object
                                                .keys(param[key][attr][0])[0];

                                            if (resolvedParams[key][attr][index][nested_attr]) {
                                                param[key][attr] = resolvedParams[key][attr]
                                                    .concat(param[key][attr]);
                                            } else {
                                                resolvedParams[key][attr][index] = $.merge(resolvedParams[key][attr][index], param[key][attr][0]);
                                                param[key][attr] = resolvedParams[key][attr];
                                            }
                                        } else if (resolvedParams[key][attr]
                                            instanceof Object) {
                                            param[key][attr][index] = $.merge(resolvedParams[key][attr][index], param[key][attr][index]);
                                            param[key][attr] = $.merge(resolvedParams[key][attr], param[key][attr]);
                                        }
                                        param[key] = $.merge(resolvedParams[key], param[key]);
                                    }
                                    resolvedParams = $.merge(resolvedParams, param);
                                }
                            }
                        });
                    }
                });

                return resolvedParams;
            },

        }

    });

}(esPhinx));
<commit_msg>add support to attr[]<commit_after>//= require ./main

"use strict";

var
    esPhinx;

(function ($) {

    $.extend({
        prototype: {
            removeAttr: function (attrName) {
                var
                    self = this;

                self.each(function (node) {
                    node.removeAttribute(attrName);
                });

                return self;
            },

            attr: function (attrName, value) {
                var
                    self = this,
                    first = self.first();

                if (self.length === 0) {
                    return self;
                }

                if (typeof value === "boolean" || value || value === "") {
                    self.each(function (node) {
                        node.setAttribute(attrName, value);
                    });

                    return self;
                } else {
                    if (first.attributes[attrName]) {
                        return first.attributes[attrName].value;
                    }
                }

                return undefined;
            },

            property: function (propertyName, value) {
                var
                    self = this,
                    first = self.first();

                if (self.length === 0) {
                    return self;
                }

                if (typeof value === "boolean" || value || value === "") {
                    self.each(function (node) {
                        node[propertyName] = value;
                    });

                    return self;
                } else {
                    if (first[propertyName] ||
                        typeof first[propertyName] === "boolean") {
                        return first[propertyName];
                    }
                }

                return undefined;
            },

            value: function (value) {
                var
                    self = this;

                if (value || value === "") {
                    self.property("value", value);
                    return self;
                }

                return self.property("value");
            },

            checked: function () {
                return this.property("checked");
            },

            check: function () {
                var
                    self = this;

                if (!self.checked()) {
                    self.property("checked", true);
                }

                return self;
            },

            uncheck: function () {
                var
                    self = this;

                return self.property("checked", false);
            },

            data: function (attrName, value) {
                return this.attr("data-" + attrName, value);
            },

            params: function () {
                var
                    self = this,
                    resolvedParams = {},
                    index,
                    name,
                    param,
                    key,
                    merged,
                    attr,
                    nested_attr,

                    resolveParam = function (name, value) {
                        value = value || "";

                        var
                            structure = name,
                            parsed;

                        // get keys, maybe to generical with .match()...
                        // /^[^[]+|[^[]+(?=\])/g

                        // next test. Catch attr[0-9+][]
                        // .+\[[0-9]+\]\[\](?!.)

                        structure = "{\"" + structure;
                        // obj[nested_attr][][attr]
                        if (/.+\[.+\]\[\]\[.+\](?!.)/.test(name)) {
                            structure = structure
                                .replace(/\]\[\]\[/, "\":[{\"")
                                .replace(/\[(?![\{])/g, "\":{\"")
                                .replace(/\]$/g, "\":\"" + value + "\"}]}}");
                        // obj[nested_attr][0-9+][attr]
                        } else if (/.+\[.+\]\[[0-9]+\]\[.+\](?!.)/.test(name)) {
                            index = structure.split(/\[([0-9]+)/)[1];
                            structure = structure
                                .replace(/\[(?!([0-9]+|\]))/g, "\":{\"")
                                .replace(/\]\[[0-9]+\]/g, "\":{\"" + index)
                                .replace(/\]$/g, "\":\"" + value + "\"}}}}");
                        // obj[attr][]
                        } else if (/.+\[.+\]\[\](?!.)/.test(name)) {
                            structure = structure
                                .replace(/\]\[/, "\":[\"")
                                .replace(/\[(?!\")/g, "\":{\"")
                                .replace(/\]$/g, value + "\"]}}");
                        // obj[attr]
                        } else if (/.+\[.+\](?!.)/.test(name)) {
                            structure = structure
                                .replace(/\[/, "\":{\"")
                                .replace(/\]$/, "\":\"" + value + "\"}}");
                        // attr[]
                        } else if (/.+\[\](?!.)/.test(name)) {
                            structure = structure
                                .replace(/\[\]/, "\":[\"" + value + "\"]}");
                        // attr[0-9+][]
                        // } else if (/.+\[[0-9]+\]\[\](?!.)/.test(name)) {
                        //     // structure = structure
                        //     //     .replace(/\[\]/, "\":[\"" + value + "\"]}");
                        //     debugger
                        // attr
                        } else {
                            structure += "\":\"" + value + "\"}";
                        }

                        if (parsed = JSON.parse(structure)) {
                            return parsed;
                        }
                        return false;
                    };

                self.each(function (form) {
                    if (form instanceof HTMLFormElement) {
                        $.each(form.elements, function (e, i) {
                            if (name = e.name) {
                                if (param = resolveParam(e.name, e.value)) {
                                    key = Object.keys(param)[0];
                                    if (resolvedParams[key]) {
                                        attr = Object.keys(param[key])[0];
                                        if (resolvedParams[key][attr]
                                            instanceof Array) {

                                            index = Object
                                                .keys(resolvedParams[key][attr])
                                                    .last();

                                            nested_attr = Object
                                                .keys(param[key][attr][0])[0];

                                            if (resolvedParams[key][attr][index][nested_attr]) {
                                                param[key][attr] = resolvedParams[key][attr]
                                                    .concat(param[key][attr]);
                                            } else {
                                                resolvedParams[key][attr][index] = $.merge(resolvedParams[key][attr][index], param[key][attr][0]);
                                                param[key][attr] = resolvedParams[key][attr];
                                            }
                                        } else if (resolvedParams[key][attr]
                                            instanceof Object) {
                                            param[key][attr][index] = $.merge(resolvedParams[key][attr][index], param[key][attr][index]);
                                            param[key][attr] = $.merge(resolvedParams[key][attr], param[key][attr]);
                                        }
                                        param[key] = $.merge(resolvedParams[key], param[key]);
                                    }
                                    resolvedParams = $.merge(resolvedParams, param);
                                }
                            }
                        });
                    }
                });

                return resolvedParams;
            },

        }

    });

}(esPhinx));
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import {createFragmentContainer, graphql} from 'react-relay';
import {TextBuffer} from 'atom';
import {CompositeDisposable} from 'event-kit';
import path from 'path';

import CreateDialogView from '../views/create-dialog-view';

export class BareCreateDialogController extends React.Component {
  static propTypes = {
    // Relay
    user: PropTypes.shape({
      id: PropTypes.string.isRequired,
    }),

    // Model
    request: PropTypes.shape({
      getParams: PropTypes.func.isRequired,
      accept: PropTypes.func.isRequired,
    }).isRequired,
    autofocus: PropTypes.object.isRequired,
    error: PropTypes.instanceOf(Error),
    isLoading: PropTypes.bool.isRequired,
    inProgress: PropTypes.bool.isRequired,

    // Atom environment
    currentWindow: PropTypes.object.isRequired,
    workspace: PropTypes.object.isRequired,
    commands: PropTypes.object.isRequired,
    config: PropTypes.object.isRequired,
  }

  constructor(props) {
    super(props);

    const {localDir} = this.props.request.getParams();

    this.projectHome = this.props.config.get('core.projectHome');
    this.modified = {
      repoName: false,
      localPath: false,
    };

    this.repoName = new TextBuffer({
      text: localDir ? path.basename(localDir) : '',
    });
    this.localPath = new TextBuffer({
      text: localDir || this.projectHome,
    });
    this.sourceRemoteName = new TextBuffer({
      text: this.props.config.get('github.sourceRemoteName'),
    });

    this.subs = new CompositeDisposable(
      this.repoName.onDidChange(this.didChangeRepoName),
      this.localPath.onDidChange(this.didChangeLocalPath),
      this.sourceRemoteName.onDidChange(this.didChangeSourceRemoteName),
      this.props.config.onDidChange('github.sourceRemoteName', this.readSourceRemoteNameSetting),
      this.props.config.onDidChange('github.remoteFetchProtocol', this.readRemoteFetchProtocolSetting),
    );

    this.state = {
      acceptEnabled: this.acceptIsEnabled(),
      selectedVisibility: 'PUBLIC',
      selectedProtocol: this.props.config.get('github.remoteFetchProtocol'),
      selectedOwnerID: this.props.user ? this.props.user.id : '',
    };
  }

  render() {
    return (
      <CreateDialogView
        selectedOwnerID={this.state.selectedOwnerID}
        repoName={this.repoName}
        selectedVisibility={this.state.selectedVisibility}
        localPath={this.localPath}
        sourceRemoteName={this.sourceRemoteName}
        selectedProtocol={this.state.selectedProtocol}
        didChangeOwnerID={this.didChangeOwnerID}
        didChangeVisibility={this.didChangeVisibility}
        didChangeProtocol={this.didChangeProtocol}
        acceptEnabled={this.state.acceptEnabled}
        accept={this.accept}
        {...this.props}
      />
    );
  }

  componentWillUnmount() {
    this.subs.dispose();
  }

  didChangeRepoName = () => {
    this.modified.repoName = true;
    if (!this.modified.localPath) {
      if (this.localPath.getText() === this.projectHome) {
        this.localPath.setText(path.join(this.projectHome, this.repoName.getText()));
      } else {
        const dirName = path.dirname(this.localPath.getText());
        this.localPath.setText(path.join(dirName, this.repoName.getText()));
      }
      this.modified.localPath = false;
    }
    this.recheckAcceptEnablement();
  }

  didChangeOwnerID = ownerID => new Promise(resolve => this.setState({selectedOwnerID: ownerID}, resolve))

  didChangeLocalPath = () => {
    this.modified.localPath = true;
    if (!this.modified.repoName) {
      this.repoName.setText(path.basename(this.localPath.getText()));
      this.modified.repoName = false;
    }
    this.recheckAcceptEnablement();
  }

  didChangeVisibility = visibility => {
    return new Promise(resolve => this.setState({selectedVisibility: visibility}, resolve));
  }

  didChangeSourceRemoteName = () => {
    this.writeSourceRemoteNameSetting();
    this.recheckAcceptEnablement();
  }

  didChangeProtocol = async protocol => {
    await new Promise(resolve => this.setState({selectedProtocol: protocol}, resolve));
    this.writeRemoteFetchProtocolSetting(protocol);
  }

  readSourceRemoteNameSetting = ({newValue}) => {
    if (newValue !== this.sourceRemoteName.getText()) {
      this.sourceRemoteName.setText(newValue);
    }
  }

  writeSourceRemoteNameSetting() {
    if (this.props.config.get('github.sourceRemoteName') !== this.sourceRemoteName.getText()) {
      this.props.config.set('github.sourceRemoteName', this.sourceRemoteName.getText());
    }
  }

  readRemoteFetchProtocolSetting = ({newValue}) => {
    if (newValue !== this.state.selectedProtocol) {
      this.setState({selectedProtocol: newValue});
    }
  }

  writeRemoteFetchProtocolSetting(protocol) {
    if (this.props.config.get('github.remoteFetchProtocol') !== protocol) {
      this.props.config.set('github.remoteFetchProtocol', protocol);
    }
  }

  acceptIsEnabled() {
    return !this.repoName.isEmpty() && !this.localPath.isEmpty() && !this.sourceRemoteName.isEmpty();
  }

  recheckAcceptEnablement() {
    const nextEnablement = this.acceptIsEnabled();
    if (nextEnablement !== this.state.acceptEnabled) {
      this.setState({acceptEnabled: nextEnablement});
    }
  }

  accept = () => {
    if (!this.acceptIsEnabled()) {
      return Promise.resolve();
    }

    return this.props.request.accept({
      ownerID: this.state.selectedOwnerID,
      name: this.repoName.getText(),
      visibility: this.state.selectedVisibility,
      localPath: this.localPath.getText(),
      protocol: this.state.selectedProtocol,
      sourceRemoteName: this.sourceRemoteName.getText(),
    });
  }
}

export default createFragmentContainer(BareCreateDialogController, {
  user: graphql`
    fragment createDialogController_user on User
    @argumentDefinitions(
      organizationCount: {type: "Int!"}
      organizationCursor: {type: "String"}
    ) {
      id
      ...repositoryHomeSelectionView_user @arguments(
        organizationCount: $organizationCount
        organizationCursor: $organizationCursor
      )
    }
  `,
});
<commit_msg>Always pass a real ID to ownerID<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import {createFragmentContainer, graphql} from 'react-relay';
import {TextBuffer} from 'atom';
import {CompositeDisposable} from 'event-kit';
import path from 'path';

import CreateDialogView from '../views/create-dialog-view';

export class BareCreateDialogController extends React.Component {
  static propTypes = {
    // Relay
    user: PropTypes.shape({
      id: PropTypes.string.isRequired,
    }),

    // Model
    request: PropTypes.shape({
      getParams: PropTypes.func.isRequired,
      accept: PropTypes.func.isRequired,
    }).isRequired,
    autofocus: PropTypes.object.isRequired,
    error: PropTypes.instanceOf(Error),
    isLoading: PropTypes.bool.isRequired,
    inProgress: PropTypes.bool.isRequired,

    // Atom environment
    currentWindow: PropTypes.object.isRequired,
    workspace: PropTypes.object.isRequired,
    commands: PropTypes.object.isRequired,
    config: PropTypes.object.isRequired,
  }

  constructor(props) {
    super(props);

    const {localDir} = this.props.request.getParams();

    this.projectHome = this.props.config.get('core.projectHome');
    this.modified = {
      repoName: false,
      localPath: false,
    };

    this.repoName = new TextBuffer({
      text: localDir ? path.basename(localDir) : '',
    });
    this.localPath = new TextBuffer({
      text: localDir || this.projectHome,
    });
    this.sourceRemoteName = new TextBuffer({
      text: this.props.config.get('github.sourceRemoteName'),
    });

    this.subs = new CompositeDisposable(
      this.repoName.onDidChange(this.didChangeRepoName),
      this.localPath.onDidChange(this.didChangeLocalPath),
      this.sourceRemoteName.onDidChange(this.didChangeSourceRemoteName),
      this.props.config.onDidChange('github.sourceRemoteName', this.readSourceRemoteNameSetting),
      this.props.config.onDidChange('github.remoteFetchProtocol', this.readRemoteFetchProtocolSetting),
    );

    this.state = {
      acceptEnabled: this.acceptIsEnabled(),
      selectedVisibility: 'PUBLIC',
      selectedProtocol: this.props.config.get('github.remoteFetchProtocol'),
      selectedOwnerID: this.props.user ? this.props.user.id : '',
    };
  }

  render() {
    return (
      <CreateDialogView
        selectedOwnerID={this.state.selectedOwnerID}
        repoName={this.repoName}
        selectedVisibility={this.state.selectedVisibility}
        localPath={this.localPath}
        sourceRemoteName={this.sourceRemoteName}
        selectedProtocol={this.state.selectedProtocol}
        didChangeOwnerID={this.didChangeOwnerID}
        didChangeVisibility={this.didChangeVisibility}
        didChangeProtocol={this.didChangeProtocol}
        acceptEnabled={this.state.acceptEnabled}
        accept={this.accept}
        {...this.props}
      />
    );
  }

  componentWillUnmount() {
    this.subs.dispose();
  }

  didChangeRepoName = () => {
    this.modified.repoName = true;
    if (!this.modified.localPath) {
      if (this.localPath.getText() === this.projectHome) {
        this.localPath.setText(path.join(this.projectHome, this.repoName.getText()));
      } else {
        const dirName = path.dirname(this.localPath.getText());
        this.localPath.setText(path.join(dirName, this.repoName.getText()));
      }
      this.modified.localPath = false;
    }
    this.recheckAcceptEnablement();
  }

  didChangeOwnerID = ownerID => new Promise(resolve => this.setState({selectedOwnerID: ownerID}, resolve))

  didChangeLocalPath = () => {
    this.modified.localPath = true;
    if (!this.modified.repoName) {
      this.repoName.setText(path.basename(this.localPath.getText()));
      this.modified.repoName = false;
    }
    this.recheckAcceptEnablement();
  }

  didChangeVisibility = visibility => {
    return new Promise(resolve => this.setState({selectedVisibility: visibility}, resolve));
  }

  didChangeSourceRemoteName = () => {
    this.writeSourceRemoteNameSetting();
    this.recheckAcceptEnablement();
  }

  didChangeProtocol = async protocol => {
    await new Promise(resolve => this.setState({selectedProtocol: protocol}, resolve));
    this.writeRemoteFetchProtocolSetting(protocol);
  }

  readSourceRemoteNameSetting = ({newValue}) => {
    if (newValue !== this.sourceRemoteName.getText()) {
      this.sourceRemoteName.setText(newValue);
    }
  }

  writeSourceRemoteNameSetting() {
    if (this.props.config.get('github.sourceRemoteName') !== this.sourceRemoteName.getText()) {
      this.props.config.set('github.sourceRemoteName', this.sourceRemoteName.getText());
    }
  }

  readRemoteFetchProtocolSetting = ({newValue}) => {
    if (newValue !== this.state.selectedProtocol) {
      this.setState({selectedProtocol: newValue});
    }
  }

  writeRemoteFetchProtocolSetting(protocol) {
    if (this.props.config.get('github.remoteFetchProtocol') !== protocol) {
      this.props.config.set('github.remoteFetchProtocol', protocol);
    }
  }

  acceptIsEnabled() {
    return !this.repoName.isEmpty() && !this.localPath.isEmpty() && !this.sourceRemoteName.isEmpty();
  }

  recheckAcceptEnablement() {
    const nextEnablement = this.acceptIsEnabled();
    if (nextEnablement !== this.state.acceptEnabled) {
      this.setState({acceptEnabled: nextEnablement});
    }
  }

  accept = () => {
    if (!this.acceptIsEnabled()) {
      return Promise.resolve();
    }

    const ownerID = this.state.selectedOwnerID !== '' ? this.state.selectedOwner : this.props.user.id;

    return this.props.request.accept({
      ownerID,
      name: this.repoName.getText(),
      visibility: this.state.selectedVisibility,
      localPath: this.localPath.getText(),
      protocol: this.state.selectedProtocol,
      sourceRemoteName: this.sourceRemoteName.getText(),
    });
  }
}

export default createFragmentContainer(BareCreateDialogController, {
  user: graphql`
    fragment createDialogController_user on User
    @argumentDefinitions(
      organizationCount: {type: "Int!"}
      organizationCursor: {type: "String"}
    ) {
      id
      ...repositoryHomeSelectionView_user @arguments(
        organizationCount: $organizationCount
        organizationCursor: $organizationCursor
      )
    }
  `,
});
<|endoftext|>


<commit_before>// relevant: https://github.com/jprichardson/node-fs-extra/issues/89
// come up with better file name

var assert = require('assert')
var fs = require('fs')
var path = require('path')
var os = require('os')
var fse = require(process.cwd())

/* global afterEach, beforeEach, describe, it */

describe('copy / gh #89', function () {
  var TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-gh-89')

  beforeEach(function (done) {
    fse.emptyDir(TEST_DIR, done)
  })

  afterEach(function (done) {
    fse.remove(TEST_DIR, done)
  })

  it('should...', function (done) {
    var A = path.join(TEST_DIR, 'A')
    var B = path.join(TEST_DIR, 'B')
    fs.mkdirSync(A)
    fs.mkdirSync(B)

    var one = path.join(A, 'one.txt')
    var two = path.join(A, 'two.txt')
    var three = path.join(B, 'three.txt')
    var four = path.join(B, 'four.txt')

    fs.writeFileSync(one, '1')
    fs.writeFileSync(two, '2')
    fs.writeFileSync(three, '3')
    fs.writeFileSync(four, '4')

    var C = path.join(TEST_DIR, 'C')
    fse.copy(A, C, function (err) {
      if (err) return done(err)

      fse.copy(B, C, function (err) {
        if (err) return done(err)

        assert(fs.existsSync(path.join(C, 'one.txt')))
        assert(fs.existsSync(path.join(C, 'two.txt')))
        assert(fs.existsSync(path.join(C, 'three.txt')))
        assert(fs.existsSync(path.join(C, 'four.txt')))
        done()
      })
    })
  })
})
<commit_msg>Refactor test copy/async<commit_after>'use strict'

// relevant: https://github.com/jprichardson/node-fs-extra/issues/89
// come up with better file name

const fs = require('fs')
const os = require('os')
const fse = require(process.cwd())
const path = require('path')
const assert = require('assert')

/* global afterEach, beforeEach, describe, it */

describe('copy / gh #89', () => {
  const TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-gh-89')

  beforeEach(done => {
    fse.emptyDir(TEST_DIR, done)
  })

  afterEach(done => {
    fse.remove(TEST_DIR, done)
  })

  it('should...', done => {
    const A = path.join(TEST_DIR, 'A')
    const B = path.join(TEST_DIR, 'B')
    fs.mkdirSync(A)
    fs.mkdirSync(B)

    const one = path.join(A, 'one.txt')
    const two = path.join(A, 'two.txt')
    const three = path.join(B, 'three.txt')
    const four = path.join(B, 'four.txt')

    fs.writeFileSync(one, '1')
    fs.writeFileSync(two, '2')
    fs.writeFileSync(three, '3')
    fs.writeFileSync(four, '4')

    const C = path.join(TEST_DIR, 'C')
    fse.copy(A, C, err => {
      if (err) return done(err)

      fse.copy(B, C, err => {
        if (err) return done(err)

        assert(fs.existsSync(path.join(C, 'one.txt')))
        assert(fs.existsSync(path.join(C, 'two.txt')))
        assert(fs.existsSync(path.join(C, 'three.txt')))
        assert(fs.existsSync(path.join(C, 'four.txt')))
        done()
      })
    })
  })
})
<|endoftext|>


<commit_before>'use strict';

const Pooling = require('generic-pool');
const Promise = require('../../promise');
const _ = require('lodash');
const Utils = require('../../utils');
const debug = Utils.getLogger().debugContext('pool');
const semver = require('semver');

const defaultPoolingConfig = {
  max: 5,
  min: 0,
  idle: 10000,
  acquire: 10000,
  evict: 10000,
  handleDisconnects: true
};

/**
 * Abstract Connection Manager
 *
 * Connection manager which handles pool, replication and determining database version
 * Works with generic-pool to maintain connection pool
 *
 * @private
 */
class ConnectionManager {
  constructor(dialect, sequelize) {
    const config = _.cloneDeep(sequelize.config);

    this.sequelize = sequelize;
    this.config = config;
    this.dialect = dialect;
    this.versionPromise = null;
    this.dialectName = this.sequelize.options.dialect;

    if (config.pool === false) {
      throw new Error('Support for pool:false was removed in v4.0');
    }

    config.pool = _.defaults(config.pool || {}, defaultPoolingConfig, {
      validate: this._validate.bind(this),
      Promise
    });

    // Save a reference to the bound version so we can remove it with removeListener
    this._onProcessExit = this._onProcessExit.bind(this);
    process.on('exit', this._onProcessExit);

    this.initPools();
  }

  refreshTypeParser(dataTypes) {
    _.each(dataTypes, dataType => {
      if (dataType.hasOwnProperty('parse')) {
        if (dataType.types[this.dialectName]) {
          this._refreshTypeParser(dataType);
        } else {
          throw new Error('Parse function not supported for type ' + dataType.key + ' in dialect ' + this.dialectName);
        }
      }
    });
  }

  /**
   * Handler which executes on process exit or connection manager shutdown
   *
   * @private
   * @return {Promise}
   */
  _onProcessExit() {
    if (!this.pool) {
      return Promise.resolve();
    }

    return this.pool.drain().then(() => {
      debug('connection drain due to process exit');
      return this.pool.clear();
    });
  }

  /**
   * Drain the pool and close it permanently
   *
   * @return {Promise}
   */
  close() {
    // Remove the listener, so all references to this instance can be garbage collected.
    process.removeListener('exit', this._onProcessExit);

    // Mark close of pool
    this.getConnection = function getConnection() {
      return Promise.reject(new Error('ConnectionManager.getConnection was called after the connection manager was closed!'));
    };

    return this._onProcessExit();
  }

  /**
   * Initialize connection pool. By default pool autostart is set to false, so no connection will be
   * be created unless `pool.acquire` is called.
   */
  initPools() {
    const config = this.config;

    if (!config.replication) {
      this.pool = Pooling.createPool({
        create: () => this._connect(config).catch(err => err),
        destroy: mayBeConnection => {
          if (mayBeConnection instanceof Error) {
            return Promise.resolve();
          }

          return this._disconnect(mayBeConnection)
            .tap(() => { debug('connection destroy'); });
        },
        validate: config.pool.validate
      }, {
        Promise: config.pool.Promise,
        testOnBorrow: true,
        autostart: false,
        max: config.pool.max,
        min: config.pool.min,
        acquireTimeoutMillis: config.pool.acquire,
        idleTimeoutMillis: config.pool.idle,
        evictionRunIntervalMillis: config.pool.evict
      });

      debug(`pool created with max/min: ${config.pool.max}/${config.pool.min}, no replication`);

      return;
    }

    let reads = 0;

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

    // Map main connection config
    config.replication.write = _.defaults(config.replication.write, _.omit(config, 'replication'));

    // Apply defaults to each read config
    config.replication.read = _.map(config.replication.read, readConfig =>
      _.defaults(readConfig, _.omit(this.config, 'replication'))
    );

    // custom pooling for replication (original author @janmeier)
    this.pool = {
      release: client => {
        if (client.queryType === 'read') {
          return this.pool.read.release(client);
        } else {
          return this.pool.write.release(client);
        }
      },
      acquire: (priority, queryType, useMaster) => {
        useMaster = _.isUndefined(useMaster) ? false : useMaster;
        if (queryType === 'SELECT' && !useMaster) {
          return this.pool.read.acquire(priority)
            .then(mayBeConnection => this._determineConnection(mayBeConnection));
        } else {
          return this.pool.write.acquire(priority)
            .then(mayBeConnection => this._determineConnection(mayBeConnection));
        }
      },
      destroy: mayBeConnection => {
        if (mayBeConnection instanceof Error) {
          return Promise.resolve();
        }

        return this.pool[mayBeConnection.queryType].destroy(mayBeConnection)
          .tap(() => { debug('connection destroy'); });
      },
      clear: () => {
        return Promise.join(
          this.pool.read.clear(),
          this.pool.write.clear()
        ).tap(() => { debug('all connection clear'); });
      },
      drain: () => {
        return Promise.join(
          this.pool.write.drain(),
          this.pool.read.drain()
        );
      },
      read: Pooling.createPool({
        create: () => {
          const nextRead = reads++ % config.replication.read.length; // round robin config
          return this
            ._connect(config.replication.read[nextRead])
            .tap(connection => {
              connection.queryType = 'read';
            })
            .catch(err => err);
        },
        destroy: connection => this._disconnect(connection),
        validate: config.pool.validate
      }, {
        Promise: config.pool.Promise,
        testOnBorrow: true,
        autostart: false,
        max: config.pool.max,
        min: config.pool.min,
        acquireTimeoutMillis: config.pool.acquire,
        idleTimeoutMillis: config.pool.idle,
        evictionRunIntervalMillis: config.pool.evict
      }),
      write: Pooling.createPool({
        create: () => {
          return this
            ._connect(config.replication.write)
            .tap(connection => {
              connection.queryType = 'write';
            })
            .catch(err => err);
        },
        destroy: connection => this._disconnect(connection),
        validate: config.pool.validate
      }, {
        Promise: config.pool.Promise,
        testOnBorrow: true,
        autostart: false,
        max: config.pool.max,
        min: config.pool.min,
        acquireTimeoutMillis: config.pool.acquire,
        idleTimeoutMillis: config.pool.idle,
        evictionRunIntervalMillis: config.pool.evict
      })
    };

    debug(`pool created with max/min: ${config.pool.max}/${config.pool.min}, with replication`);
  }

  /**
   * Get connection from pool. It sets database version if it's not already set.
   * Call pool.acquire to get a connection
   *
   * @param {Object}   [options]                 Pool options
   * @param {Integer}  [options.priority]        Set priority for this call. Read more at https://github.com/coopernurse/node-pool#priority-queueing
   * @param {String}   [options.type]            Set which replica to use. Available options are `read` and `write`
   * @param {Boolean}  [options.useMaster=false] Force master or write replica to get connection from
   *
   * @return {Promise<Connection>}
   */
  getConnection(options) {
    options = options || {};

    let promise;
    if (this.sequelize.options.databaseVersion === 0) {
      if (this.versionPromise) {
        promise = this.versionPromise;
      } else {
        promise = this.versionPromise = this._connect(this.config.replication.write || this.config)
          .then(connection => {
            const _options = {};

            _options.transaction = {connection}; // Cheat .query to use our private connection
            _options.logging = () => {};
            _options.logging.__testLoggingFn = true;

            return this.sequelize.databaseVersion(_options).then(version => {
              this.sequelize.options.databaseVersion = semver.valid(version) ? version : this.defaultVersion;
              this.versionPromise = null;

              return this._disconnect(connection);
            });
          }).catch(err => {
            this.versionPromise = null;
            throw err;
          });
      }
    } else {
      promise = Promise.resolve();
    }

    return promise.then(() => {
      return this.pool.acquire(options.priority, options.type, options.useMaster)
        .then(mayBeConnection => this._determineConnection(mayBeConnection))
        .tap(() => { debug('connection acquired'); });
    });
  }

  /**
   * Release a pooled connection so it can be utilized by other connection requests
   *
   * @param {Connection} connection
   *
   * @return {Promise}
   */
  releaseConnection(connection) {
    return this.pool.release(connection)
      .tap(() => { debug('connection released'); })
      .catch(/Resource not currently part of this pool/, () => {});
  }

  /**
   * Check if something acquired by pool is indeed a connection but not an Error instance
   * Why we need to do this https://github.com/sequelize/sequelize/pull/8330
   *
   * @param {Object|Error} mayBeConnection Object which can be either connection or error
   *
   * @retun {Promise<Connection>}
   */
  _determineConnection(mayBeConnection) {
    if (mayBeConnection instanceof Error) {
      return Promise.resolve(this.pool.destroy(mayBeConnection))
        .catch(/Resource not currently part of this pool/, () => {})
        .then(() => { throw mayBeConnection; });
    }

    return Promise.resolve(mayBeConnection);
  }

  /**
   * Call dialect library to get connection
   *
   * @param {*} config Connection config
   * @private
   * @return {Promise<Connection>}
   */
  _connect(config) {
    return this.sequelize.runHooks('beforeConnect', config)
      .then(() => this.dialect.connectionManager.connect(config))
      .then(connection => this.sequelize.runHooks('afterConnect', connection, config).return(connection));
  }

  /**
   * Call dialect library to disconnect a connection
   *
   * @param {Connection} connection
   * @private
   * @return {Promise}
   */
  _disconnect(connection) {
    return this.dialect.connectionManager.disconnect(connection);
  }

  /**
   * Determine if a connection is still valid or not
   *
   * @param {Connection} connection
   *
   * @return {Boolean}
   */
  _validate(connection) {
    if (!this.dialect.connectionManager.validate) {
      return true;
    }

    return this.dialect.connectionManager.validate(connection);
  }
}

module.exports = ConnectionManager;
module.exports.ConnectionManager = ConnectionManager;
module.exports.default = ConnectionManager;
<commit_msg>feat(pooling): reuse connections with returnToHead (#8567)<commit_after>'use strict';

const Pooling = require('generic-pool');
const Promise = require('../../promise');
const _ = require('lodash');
const Utils = require('../../utils');
const debug = Utils.getLogger().debugContext('pool');
const semver = require('semver');

const defaultPoolingConfig = {
  max: 5,
  min: 0,
  idle: 10000,
  acquire: 10000,
  evict: 10000,
  handleDisconnects: true
};

/**
 * Abstract Connection Manager
 *
 * Connection manager which handles pool, replication and determining database version
 * Works with generic-pool to maintain connection pool
 *
 * @private
 */
class ConnectionManager {
  constructor(dialect, sequelize) {
    const config = _.cloneDeep(sequelize.config);

    this.sequelize = sequelize;
    this.config = config;
    this.dialect = dialect;
    this.versionPromise = null;
    this.dialectName = this.sequelize.options.dialect;

    if (config.pool === false) {
      throw new Error('Support for pool:false was removed in v4.0');
    }

    config.pool = _.defaults(config.pool || {}, defaultPoolingConfig, {
      validate: this._validate.bind(this),
      Promise
    });

    // Save a reference to the bound version so we can remove it with removeListener
    this._onProcessExit = this._onProcessExit.bind(this);
    process.on('exit', this._onProcessExit);

    this.initPools();
  }

  refreshTypeParser(dataTypes) {
    _.each(dataTypes, dataType => {
      if (dataType.hasOwnProperty('parse')) {
        if (dataType.types[this.dialectName]) {
          this._refreshTypeParser(dataType);
        } else {
          throw new Error('Parse function not supported for type ' + dataType.key + ' in dialect ' + this.dialectName);
        }
      }
    });
  }

  /**
   * Handler which executes on process exit or connection manager shutdown
   *
   * @private
   * @return {Promise}
   */
  _onProcessExit() {
    if (!this.pool) {
      return Promise.resolve();
    }

    return this.pool.drain().then(() => {
      debug('connection drain due to process exit');
      return this.pool.clear();
    });
  }

  /**
   * Drain the pool and close it permanently
   *
   * @return {Promise}
   */
  close() {
    // Remove the listener, so all references to this instance can be garbage collected.
    process.removeListener('exit', this._onProcessExit);

    // Mark close of pool
    this.getConnection = function getConnection() {
      return Promise.reject(new Error('ConnectionManager.getConnection was called after the connection manager was closed!'));
    };

    return this._onProcessExit();
  }

  /**
   * Initialize connection pool. By default pool autostart is set to false, so no connection will be
   * be created unless `pool.acquire` is called.
   */
  initPools() {
    const config = this.config;

    if (!config.replication) {
      this.pool = Pooling.createPool({
        create: () => this._connect(config).catch(err => err),
        destroy: mayBeConnection => {
          if (mayBeConnection instanceof Error) {
            return Promise.resolve();
          }

          return this._disconnect(mayBeConnection)
            .tap(() => { debug('connection destroy'); });
        },
        validate: config.pool.validate
      }, {
        Promise: config.pool.Promise,
        testOnBorrow: true,
        returnToHead: true,
        autostart: false,
        max: config.pool.max,
        min: config.pool.min,
        acquireTimeoutMillis: config.pool.acquire,
        idleTimeoutMillis: config.pool.idle,
        evictionRunIntervalMillis: config.pool.evict
      });

      debug(`pool created with max/min: ${config.pool.max}/${config.pool.min}, no replication`);

      return;
    }

    let reads = 0;

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

    // Map main connection config
    config.replication.write = _.defaults(config.replication.write, _.omit(config, 'replication'));

    // Apply defaults to each read config
    config.replication.read = _.map(config.replication.read, readConfig =>
      _.defaults(readConfig, _.omit(this.config, 'replication'))
    );

    // custom pooling for replication (original author @janmeier)
    this.pool = {
      release: client => {
        if (client.queryType === 'read') {
          return this.pool.read.release(client);
        } else {
          return this.pool.write.release(client);
        }
      },
      acquire: (priority, queryType, useMaster) => {
        useMaster = _.isUndefined(useMaster) ? false : useMaster;
        if (queryType === 'SELECT' && !useMaster) {
          return this.pool.read.acquire(priority)
            .then(mayBeConnection => this._determineConnection(mayBeConnection));
        } else {
          return this.pool.write.acquire(priority)
            .then(mayBeConnection => this._determineConnection(mayBeConnection));
        }
      },
      destroy: mayBeConnection => {
        if (mayBeConnection instanceof Error) {
          return Promise.resolve();
        }

        return this.pool[mayBeConnection.queryType].destroy(mayBeConnection)
          .tap(() => { debug('connection destroy'); });
      },
      clear: () => {
        return Promise.join(
          this.pool.read.clear(),
          this.pool.write.clear()
        ).tap(() => { debug('all connection clear'); });
      },
      drain: () => {
        return Promise.join(
          this.pool.write.drain(),
          this.pool.read.drain()
        );
      },
      read: Pooling.createPool({
        create: () => {
          const nextRead = reads++ % config.replication.read.length; // round robin config
          return this
            ._connect(config.replication.read[nextRead])
            .tap(connection => {
              connection.queryType = 'read';
            })
            .catch(err => err);
        },
        destroy: connection => this._disconnect(connection),
        validate: config.pool.validate
      }, {
        Promise: config.pool.Promise,
        testOnBorrow: true,
        returnToHead: true,
        autostart: false,
        max: config.pool.max,
        min: config.pool.min,
        acquireTimeoutMillis: config.pool.acquire,
        idleTimeoutMillis: config.pool.idle,
        evictionRunIntervalMillis: config.pool.evict
      }),
      write: Pooling.createPool({
        create: () => {
          return this
            ._connect(config.replication.write)
            .tap(connection => {
              connection.queryType = 'write';
            })
            .catch(err => err);
        },
        destroy: connection => this._disconnect(connection),
        validate: config.pool.validate
      }, {
        Promise: config.pool.Promise,
        testOnBorrow: true,
        returnToHead: true,
        autostart: false,
        max: config.pool.max,
        min: config.pool.min,
        acquireTimeoutMillis: config.pool.acquire,
        idleTimeoutMillis: config.pool.idle,
        evictionRunIntervalMillis: config.pool.evict
      })
    };

    debug(`pool created with max/min: ${config.pool.max}/${config.pool.min}, with replication`);
  }

  /**
   * Get connection from pool. It sets database version if it's not already set.
   * Call pool.acquire to get a connection
   *
   * @param {Object}   [options]                 Pool options
   * @param {Integer}  [options.priority]        Set priority for this call. Read more at https://github.com/coopernurse/node-pool#priority-queueing
   * @param {String}   [options.type]            Set which replica to use. Available options are `read` and `write`
   * @param {Boolean}  [options.useMaster=false] Force master or write replica to get connection from
   *
   * @return {Promise<Connection>}
   */
  getConnection(options) {
    options = options || {};

    let promise;
    if (this.sequelize.options.databaseVersion === 0) {
      if (this.versionPromise) {
        promise = this.versionPromise;
      } else {
        promise = this.versionPromise = this._connect(this.config.replication.write || this.config)
          .then(connection => {
            const _options = {};

            _options.transaction = {connection}; // Cheat .query to use our private connection
            _options.logging = () => {};
            _options.logging.__testLoggingFn = true;

            return this.sequelize.databaseVersion(_options).then(version => {
              this.sequelize.options.databaseVersion = semver.valid(version) ? version : this.defaultVersion;
              this.versionPromise = null;

              return this._disconnect(connection);
            });
          }).catch(err => {
            this.versionPromise = null;
            throw err;
          });
      }
    } else {
      promise = Promise.resolve();
    }

    return promise.then(() => {
      return this.pool.acquire(options.priority, options.type, options.useMaster)
        .then(mayBeConnection => this._determineConnection(mayBeConnection))
        .tap(() => { debug('connection acquired'); });
    });
  }

  /**
   * Release a pooled connection so it can be utilized by other connection requests
   *
   * @param {Connection} connection
   *
   * @return {Promise}
   */
  releaseConnection(connection) {
    return this.pool.release(connection)
      .tap(() => { debug('connection released'); })
      .catch(/Resource not currently part of this pool/, () => {});
  }

  /**
   * Check if something acquired by pool is indeed a connection but not an Error instance
   * Why we need to do this https://github.com/sequelize/sequelize/pull/8330
   *
   * @param {Object|Error} mayBeConnection Object which can be either connection or error
   *
   * @retun {Promise<Connection>}
   */
  _determineConnection(mayBeConnection) {
    if (mayBeConnection instanceof Error) {
      return Promise.resolve(this.pool.destroy(mayBeConnection))
        .catch(/Resource not currently part of this pool/, () => {})
        .then(() => { throw mayBeConnection; });
    }

    return Promise.resolve(mayBeConnection);
  }

  /**
   * Call dialect library to get connection
   *
   * @param {*} config Connection config
   * @private
   * @return {Promise<Connection>}
   */
  _connect(config) {
    return this.sequelize.runHooks('beforeConnect', config)
      .then(() => this.dialect.connectionManager.connect(config))
      .then(connection => this.sequelize.runHooks('afterConnect', connection, config).return(connection));
  }

  /**
   * Call dialect library to disconnect a connection
   *
   * @param {Connection} connection
   * @private
   * @return {Promise}
   */
  _disconnect(connection) {
    return this.dialect.connectionManager.disconnect(connection);
  }

  /**
   * Determine if a connection is still valid or not
   *
   * @param {Connection} connection
   *
   * @return {Boolean}
   */
  _validate(connection) {
    if (!this.dialect.connectionManager.validate) {
      return true;
    }

    return this.dialect.connectionManager.validate(connection);
  }
}

module.exports = ConnectionManager;
module.exports.ConnectionManager = ConnectionManager;
module.exports.default = ConnectionManager;
<|endoftext|>


<commit_before>var _ = require('@sailshq/lodash');

module.exports = function(self, options) {
  self.addHelpers({

    // `apos.singleton` renders a single widget of a fixed type, standing alone
    // in the page. The `_options` object is passed to the widget.
    //
    // A singleton is just a special case of an area, so you can change your
    // mind later and call `apos.area` with the same `name`.
    //
    // The `name` property distinguishes this singleton from other areas in
    // the same `doc`.
    //
    // If `_options.addLabel` is set, that text appears on the button to
    // initially populate the singleton. If `_options.editLabel` is set, that
    // text appears on the button edit an existing widget in the singleton.
    //
    // If `_options.controls.movable` is false, the widget may not be dragged out
    // of the singleton.
    //
    // If `_options.controls.removable` is false, the widget
    // may not be removed entirely.
    //
    // If `_options.controls.position` is set to `top-left`, `top-right`,
    // `bottom-left` or `bottom-right`, the widget controls (edit, drag
    // and remove) are positioned accordingly.
    //
    // If `_options` is not specified, Apostrophe falls back to the options
    // configured for the given field `name` in the schema for this type of
    // `doc`. For ordinary pages there usually won't be any, but this is
    // very convenient when working with `apostrophe-pieces`.
    //
    // Alternate syntax: `{ area: doc.areaname, type: type, ... more options }`

    singleton: function(doc, name, type, _options) {
      var area;
      var options;

      if (arguments.length === 1) {
        options = doc;
        area = options.area;
      } else {
        options = { type: type };
        if (_.has(doc, name)) {
          area = doc[name];
        } else {
          area = {
            _docId: doc._id,
            _dotPath: name,
            _edit: doc._edit
          };
        }
      }

      if (!options.widgets) {
        // Need to make the widget definition options configured the same
        // way as in areas to support drag n drop.
        options.widgets = {};
        options.widgets[options.type] = _options || self.getSchemaOptions(doc, name);
      }

      // Don't fill the session with blessings of option sets unless
      // this user actually can edit this singleton
      if (area._edit) {
        _.each(options.widgets || {}, function(options, type) {
          self.apos.utils.bless(self.apos.templates.contextReq, options, 'widget', type);
        });
      }

      options.limit = 1;

      return self.renderArea(area, options);
    },

    // `apos.area` renders an area: a column of widgets of one or more types.
    //
    // If present The `_options` object must contain a `widgets` property, an object
    // which must at least contain a property by the name of each allowed widget. The
    // corresponding value should be an object, and is passed on as options to
    // widgets of that type appearing in this area.
    //
    // If `blockLevelControls: true` is present, the controls for this area are given
    // extra vertical space and rendered in a distinct color. These are affordances
    // to ensure the user can clearly distinguish the controls of an area used for layout, i.e.
    // an area that contains "two column" and "three column" widgets containing areas
    // of their own.
    //
    // The `name` property distinguishes this area from other areas in
    // the same `doc`.
    //
    // The `limit` option may be used to limit the number of widgets allowed.
    //
    // If `_options.controls.movable` is false, the widget may not be dragged out
    // of the singleton.
    //
    // If `_options.controls.removable` is false, the widget
    // may not be removed entirely.
    //
    // If `_options.controls.position` is set to `top-left`, `top-right`,
    // `bottom-left` or `bottom-right`, the widget controls (edit, drag
    // and remove) are positioned accordingly.
    //
    // If `_options` is not specified, Apostrophe falls back to the options
    // configured for the given field `name` in the schema for this type of
    // `doc`. For ordinary pages there usually won't be any, but this is
    // very convenient when working with `apostrophe-pieces`.
    //
    // Alternate syntax: `{ area: doc.areaname, ... more options }`

    area: function(doc, name, _options) {
      var area;
      var options;

      if (arguments.length === 1) {
        options = doc;
        area = options.area;
      } else {
        options = _options || self.getSchemaOptions(doc, name);
        if (_.has(doc, name)) {
          area = doc[name];
        } else {
          area = {
            _docId: doc._id,
            _dotPath: name,
            _edit: doc._edit
          };
        }
      }

      options.hint = options.hint || self.apos.templates.contextReq.__('Use the Add Content button to get started.');

      // Don't fill the session with blessings of option sets unless
      // this user actually can edit this area
      if (area._edit) {
        _.each(options.widgets || {}, function(options, type) {
          self.apos.utils.bless(self.apos.templates.contextReq, options, 'widget', type);
        });
      }

      return self.renderArea(area, options);
    },

    // apos.areas.widget renders one widget. Invoked by both `apos.area` and
    // `apos.singleton`. Not
    // often called directly, but see `area.html` if you are interested in
    // doing so.

    widget: function(widget, options) {
      var manager = self.getWidgetManager(widget.type);
      if (!manager) {
        // Not configured in this project
        self.warnMissingWidgetType(widget.type);
        return '';
      }
      var partial;
      if (!manager.options.wrapperTemplate) {
        partial = _.partial(self.partial, 'widget');
      } else {
        partial = _.partial(manager.partial, manager.options.wrapperTemplate);
      }
      return partial({
        // The widget minus any properties that don't
        // absolutely need to be crammed into a JSON
        // attribute for the editor's benefit. Good filtering
        // here prevents megabytes of markup. -Tom and Sam
        dataFiltered: manager.filterForDataAttribute(widget),
        manager: manager,
        widget: widget,
        options: options,
        output: function() {
          return manager.output(widget, options);
        }
      });
    },

    // Returns the rich text markup of all `apostrophe-rich-text` widgets
    // within the provided doc or area, concatenated as a single string. In future this method
    // may improve to return the content of other widgets that consider themselves primarily
    // providers of rich text, such as subclasses of `apostrophe-rich-text`,
    // which will **not** be regarded as a bc break. However it will never return images, videos, etc.
    //
    // By default the rich text contents of the widgets are joined with
    // a newline between. You may pass your own `options.delimiter` string if
    // you wish a different delimiter or the empty string. You may also pass
    // an HTML element name like `div` via `options.wrapper` to wrap each
    // one in a `<div>...</div>` block. Of course, there may already be a div
    // in the rich txt (but then again there may not).
    //
    // Content will be retrieved from any widget type that supplies a
    // `getRichText` method.

    richText: function(within, options) {
      // Use the safe filter so that the markup doesn't get double-escaped by nunjucks
      return self.apos.templates.safe(self.richText(within, options));
    },

    // Returns the plaintext contents  of all rich text widgets
    // within the provided doc or area, concatenated as a single string.
    //
    // By default the rich text contents of the various widgets are joined with
    // a newline between. You may pass your own `options.delimiter` string if
    // you wish a different delimiter or the empty string.
    //
    // Pass `options.limit` to limit the number of characters. This method will
    // return fewer characters in order to avoid cutting off in mid-word.
    //
    // By default, three periods (`...`) follow a truncated string. If you prefer,
    // set `options.ellipsis` to a different suffix, which may be the empty string
    // if you wish.
    //
    // Content will be retrieved from any widget type that supplies a
    // `getRichText` method.

    plaintext: function(within, options) {
      return self.plaintext(within, options);
    },

    // Output the widget controls. The `addWidgetControlGroups` option can
    // be used to append additional control groups. See the
    // `widgetControlGroups` method for the format. That method can also
    // be extended via the `super` pattern to make the decision dynamically.

    widgetControlGroups: function(widget, options) {
      return self.widgetControlGroups(self.apos.templates.contextReq, widget, options);
    },

    // Returns true if the named area in the given `doc` is empty.
    //
    // Alternate syntax: `{ area: doc.areaname, ... more options }`
    //
    // An area is empty if it has no widgets in it, or when
    // all of the widgets in it return true when their
    // `isEmpty()` methods are interrogated. For instance,
    // if an area only contains a rich text widget and that
    // widget. A widget with no `isEmpty()` method is never empty.

    isEmpty: function(doc, name) {
      var area;
      if (!name) {
        area = doc.area;
      } else {
        area = doc[name];
      }
      return self.isEmpty({ area: area });
    }

  });

  // These are the most frequently used helpers in A2,
  // so we allow their use without typing .areas first
  self.addHelperShortcut('area');
  self.addHelperShortcut('singleton');
};
<commit_msg>warn developers if they use bad area names<commit_after>var _ = require('@sailshq/lodash');

module.exports = function(self, options) {
  self.addHelpers({

    // `apos.singleton` renders a single widget of a fixed type, standing alone
    // in the page. The `_options` object is passed to the widget.
    //
    // A singleton is just a special case of an area, so you can change your
    // mind later and call `apos.area` with the same `name`.
    //
    // The `name` property distinguishes this singleton from other areas in
    // the same `doc`.
    //
    // If `_options.addLabel` is set, that text appears on the button to
    // initially populate the singleton. If `_options.editLabel` is set, that
    // text appears on the button edit an existing widget in the singleton.
    //
    // If `_options.controls.movable` is false, the widget may not be dragged out
    // of the singleton.
    //
    // If `_options.controls.removable` is false, the widget
    // may not be removed entirely.
    //
    // If `_options.controls.position` is set to `top-left`, `top-right`,
    // `bottom-left` or `bottom-right`, the widget controls (edit, drag
    // and remove) are positioned accordingly.
    //
    // If `_options` is not specified, Apostrophe falls back to the options
    // configured for the given field `name` in the schema for this type of
    // `doc`. For ordinary pages there usually won't be any, but this is
    // very convenient when working with `apostrophe-pieces`.
    //
    // Alternate syntax: `{ area: doc.areaname, type: type, ... more options }`

    singleton: function(doc, name, type, _options) {
      var area;
      var options;

      if (arguments.length === 1) {
        options = doc;
        area = options.area;
      } else {
        if (!name.match(/^\w+$/)) {
          self.apos.utils.warn(name + ' in ' + doc.slug + ': area names should be made up only of letters, underscores and digits. Otherwise they may not save properly.');
        }
        options = { type: type };
        if (_.has(doc, name)) {
          area = doc[name];
        } else {
          area = {
            _docId: doc._id,
            _dotPath: name,
            _edit: doc._edit
          };
        }
      }

      if (!options.widgets) {
        // Need to make the widget definition options configured the same
        // way as in areas to support drag n drop.
        options.widgets = {};
        options.widgets[options.type] = _options || self.getSchemaOptions(doc, name);
      }

      // Don't fill the session with blessings of option sets unless
      // this user actually can edit this singleton
      if (area._edit) {
        _.each(options.widgets || {}, function(options, type) {
          self.apos.utils.bless(self.apos.templates.contextReq, options, 'widget', type);
        });
      }

      options.limit = 1;

      return self.renderArea(area, options);
    },

    // `apos.area` renders an area: a column of widgets of one or more types.
    //
    // If present The `_options` object must contain a `widgets` property, an object
    // which must at least contain a property by the name of each allowed widget. The
    // corresponding value should be an object, and is passed on as options to
    // widgets of that type appearing in this area.
    //
    // If `blockLevelControls: true` is present, the controls for this area are given
    // extra vertical space and rendered in a distinct color. These are affordances
    // to ensure the user can clearly distinguish the controls of an area used for layout, i.e.
    // an area that contains "two column" and "three column" widgets containing areas
    // of their own.
    //
    // The `name` property distinguishes this area from other areas in
    // the same `doc`.
    //
    // The `limit` option may be used to limit the number of widgets allowed.
    //
    // If `_options.controls.movable` is false, the widget may not be dragged out
    // of the singleton.
    //
    // If `_options.controls.removable` is false, the widget
    // may not be removed entirely.
    //
    // If `_options.controls.position` is set to `top-left`, `top-right`,
    // `bottom-left` or `bottom-right`, the widget controls (edit, drag
    // and remove) are positioned accordingly.
    //
    // If `_options` is not specified, Apostrophe falls back to the options
    // configured for the given field `name` in the schema for this type of
    // `doc`. For ordinary pages there usually won't be any, but this is
    // very convenient when working with `apostrophe-pieces`.
    //
    // Alternate syntax: `{ area: doc.areaname, ... more options }`

    area: function(doc, name, _options) {
      var area;
      var options;

      if (arguments.length === 1) {
        options = doc;
        area = options.area;
      } else {
        if (!name.match(/^\w+$/)) {
          self.apos.utils.warn(name + ' in ' + doc.slug + ': area names should be made up only of letters, underscores and digits. Otherwise they may not save properly.');
        }
        options = _options || self.getSchemaOptions(doc, name);
        if (_.has(doc, name)) {
          area = doc[name];
        } else {
          area = {
            _docId: doc._id,
            _dotPath: name,
            _edit: doc._edit
          };
        }
      }

      options.hint = options.hint || self.apos.templates.contextReq.__('Use the Add Content button to get started.');

      // Don't fill the session with blessings of option sets unless
      // this user actually can edit this area
      if (area._edit) {
        _.each(options.widgets || {}, function(options, type) {
          self.apos.utils.bless(self.apos.templates.contextReq, options, 'widget', type);
        });
      }

      return self.renderArea(area, options);
    },

    // apos.areas.widget renders one widget. Invoked by both `apos.area` and
    // `apos.singleton`. Not
    // often called directly, but see `area.html` if you are interested in
    // doing so.

    widget: function(widget, options) {
      var manager = self.getWidgetManager(widget.type);
      if (!manager) {
        // Not configured in this project
        self.warnMissingWidgetType(widget.type);
        return '';
      }
      var partial;
      if (!manager.options.wrapperTemplate) {
        partial = _.partial(self.partial, 'widget');
      } else {
        partial = _.partial(manager.partial, manager.options.wrapperTemplate);
      }
      return partial({
        // The widget minus any properties that don't
        // absolutely need to be crammed into a JSON
        // attribute for the editor's benefit. Good filtering
        // here prevents megabytes of markup. -Tom and Sam
        dataFiltered: manager.filterForDataAttribute(widget),
        manager: manager,
        widget: widget,
        options: options,
        output: function() {
          return manager.output(widget, options);
        }
      });
    },

    // Returns the rich text markup of all `apostrophe-rich-text` widgets
    // within the provided doc or area, concatenated as a single string. In future this method
    // may improve to return the content of other widgets that consider themselves primarily
    // providers of rich text, such as subclasses of `apostrophe-rich-text`,
    // which will **not** be regarded as a bc break. However it will never return images, videos, etc.
    //
    // By default the rich text contents of the widgets are joined with
    // a newline between. You may pass your own `options.delimiter` string if
    // you wish a different delimiter or the empty string. You may also pass
    // an HTML element name like `div` via `options.wrapper` to wrap each
    // one in a `<div>...</div>` block. Of course, there may already be a div
    // in the rich txt (but then again there may not).
    //
    // Content will be retrieved from any widget type that supplies a
    // `getRichText` method.

    richText: function(within, options) {
      // Use the safe filter so that the markup doesn't get double-escaped by nunjucks
      return self.apos.templates.safe(self.richText(within, options));
    },

    // Returns the plaintext contents  of all rich text widgets
    // within the provided doc or area, concatenated as a single string.
    //
    // By default the rich text contents of the various widgets are joined with
    // a newline between. You may pass your own `options.delimiter` string if
    // you wish a different delimiter or the empty string.
    //
    // Pass `options.limit` to limit the number of characters. This method will
    // return fewer characters in order to avoid cutting off in mid-word.
    //
    // By default, three periods (`...`) follow a truncated string. If you prefer,
    // set `options.ellipsis` to a different suffix, which may be the empty string
    // if you wish.
    //
    // Content will be retrieved from any widget type that supplies a
    // `getRichText` method.

    plaintext: function(within, options) {
      return self.plaintext(within, options);
    },

    // Output the widget controls. The `addWidgetControlGroups` option can
    // be used to append additional control groups. See the
    // `widgetControlGroups` method for the format. That method can also
    // be extended via the `super` pattern to make the decision dynamically.

    widgetControlGroups: function(widget, options) {
      return self.widgetControlGroups(self.apos.templates.contextReq, widget, options);
    },

    // Returns true if the named area in the given `doc` is empty.
    //
    // Alternate syntax: `{ area: doc.areaname, ... more options }`
    //
    // An area is empty if it has no widgets in it, or when
    // all of the widgets in it return true when their
    // `isEmpty()` methods are interrogated. For instance,
    // if an area only contains a rich text widget and that
    // widget. A widget with no `isEmpty()` method is never empty.

    isEmpty: function(doc, name) {
      var area;
      if (!name) {
        area = doc.area;
      } else {
        area = doc[name];
      }
      return self.isEmpty({ area: area });
    }

  });

  // These are the most frequently used helpers in A2,
  // so we allow their use without typing .areas first
  self.addHelperShortcut('area');
  self.addHelperShortcut('singleton');
};
<|endoftext|>


<commit_before>var _ = require('lodash');
var util = require('util');

module.exports = function(self, options) {
  return {
    slugify: function(string) {
      return self.slugify(string);
    },

    // Log a message to the console from a Nunjucks template. Great for debugging.
    log: function(m) {
      console.log(util.inspect(m, { depth: 10 } ));
      return '';
    },

    // Generate a globally unique ID
    generateId: function() {
      return self.generateId();
    },

    // Test whether the specified date object refers to a date in the current year.
    // The events module utilizes this

    isCurrentYear: function(date) {
      var now = new Date();
      return date.getYear() === now.getYear();
    },

    // check if something is properly undefined
    isUndefined: function(o) {
      return (o === undefined);
    },

    // check if something is properly undefined or null
    isUndefinedOrNull: function(o) {
      return (o === undefined) || (o === null);
    },

    // check if something is a function (as opposd to property)
    isFunction: function(o) {
      return (typeof o == 'function');
    },

    // make up for lack of triple equals
    eqStrict: function(a, b) {
      return (a === b);
    },

    // Returns true if the list contains the specified value.
    // If value is an array, returns true if the list contains
    // *any of* the specified values
    contains: function(list, value) {
      if (_.isArray(value)) {
        for (var i = 0; i < value.length; i++) {
          var valueItem = value[i];
          if (_.contains(list, valueItem)){
            return true;
          }
        }
        return false;
      }
      else{
        return _.contains(list, value);
      }
    },

    // Returns true if the list contains at least one
    // object with the named property.

    // The first parameter may also be a single object, in
    // which case this function returns true if that object
    // has the named property.

    containsProperty: function(list, property) {
      if (_.isArray(list)) {
        return _.some(list, function(item){ return _.has(item, property); });
      } else {
        return _.has(list, property);
      }
    },

    // Reverses the order of the array. This MODIFIES the array
    // in addition to returning it
    reverse: function(array){
      return array.reverse();
    },

    // If the `list` argument is a string, returns true if it begins
    // with `value`. If the `list` argument is an array, returns
    // true if at least one of its elements begins with `value`.
    beginsWith: function(list, value) {
      if (_.isArray(list)) {
        for (var i = 0; i < list.length; i++) {
          var listItem = list[i];
          if (listItem.indexOf(value) === 0) {
            return true;
          }
        }
      } else {
        if (list.indexOf(value) === 0) {
          return true;
        }
      }
      return false;
    },

    // Pass as many objects as you want; they will get merged via
    // `_.merge` into a new object, without modifying any of them, and
    // the resulting object will be returned. If several objects have
    // a property, the last object wins.
    //
    // This is useful to add one more option to an options object
    // which was passed to you.
    //
    // If any argument is null, it is skipped gracefully. This allows
    // you to pass in an options object without checking if it is null.

    merge: function() {
      var result = {};
      var i;
      for (i = 0; (i < arguments.length); i++) {
        if (!arguments[i]) {
          continue;
        }
        _.merge(true, result, arguments[i]);
      }
      return result;
    },

    // Find the first array element, if any, that has the specified value for
    // the specified property.

    find: function(arr, property, value) {
      return _.find(arr, function(item) {
        return (item[property] === value);
      });
    },

    // If propertyName is _id, then the keys in the returned
    // object will be the ids of each object in arr,
    // and the values will be the corresponding objects.
    // You may index by any property name.

    indexBy: function(arr, propertyName) {
      return _.indexBy(arr, propertyName);
    },

    // Find all the array elements, if any, that have the specified value for
    // the specified property.

    filter: function(arr, property, value) {
      return _.filter(arr, function(item) {
        return (item[property] === value);
      });
    },

    // Reject array elements that have the specified value for
    // the specified property.

    reject: function(arr, property, value) {
      return _.reject(arr, function(item) {
        return (item[property] === value);
      });
    },

    // Find all the array elements, if any, for which the specified property
    // is truthy.

    filterNonempty: function(arr, property) {
      return _.filter(arr, function(item) {
        return (item[property]);
      });
    },

    // Find all the array elements, if any, for which the specified property
    // is not truthy.

    filterEmpty: function(arr, property) {
      return _.filter(arr, function(item) {
        return (!item[property]);
      });
    },

    // Returns true if the specified array or object is considered empty.
    // Objects are empty if they have no own enumerable properties.
    // Arrays are considered empty if they have a length of 0.

    isEmpty: function(item) {
      return _.isEmpty(item);
    },

    // Given an array of objects with the given property, return an array with
    // the value of that property for each object.

    pluck: function(arr, property) {
      return _.pluck(arr, property);
    },

    // Given an object, return an object without
    // the named properties or array of named
    // properties (see _.omit()).

    omit: function(object, property /* , property... */) {
      return _.omit.apply(_, arguments);
    },

    // Concatenate all of the given arrays and/or values
    // into a single array. If an argument is an array, all
    // of its elements are individually added to the
    // resulting array. If an argument is a value, it is
    // added directly to the array.

    concat: function(arrOrObj1, arrOrObj2 /* , ... */) {
      // I tried to implement this with call() but I kept
      // getting arrays in my values. We still get the
      // benefit of concat() and its built-in support for
      // treating arrays and scalar values differently. -Tom
      var result = [];
      var i;
      for (i = 0; (i < arguments.length); i++) {
        result = result.concat(arguments[i]);
      }
      return result;
    },

    // Groups by the property named by 'key' on each of the values.
    // If the property referred to by the string 'key' is found to be
    // an array property of the first object, apos.utils.groupByArray is called.
    //
    // Usage: {{ apos.utils.groupBy(people, 'age') }} or {{ apos.utils.groupBy(items, 'tags') }}
    groupBy: function(items, key){
      if(items.length && Array.isArray(items[0][key])) {
        return groupByArray(items, key);
      }
      return _.groupBy(items, key);

      function groupByArray(items, arrayName) {
        var results = {};
        _.each(items, function(item) {
          _.each(item[arrayName] || [], function(inner) {
            if (!results[inner]) {
              results[inner] = [];
            }
            results[inner].push(item);
          });
        });

        return results;
      };

    },

    // Given a series of alternating keys and values, this
    // function returns an object with those values for
    // those keys. For instance, apos.utils.object('name', 'bob')
    // returns { name: 'bob' }. This is useful because
    // Nunjucks does not allow you to create an object with
    // a property whose name is unknown at the time the
    // template is written.
    object: function(/* key, value, ... */) {
      var o = {};
      var i = 0;
      while (i < arguments.length) {
        o[arguments[i]] = arguments[i + 1];
        i += 2;
      }
      return o;
    },

    merge: function() {
      var result = {};
      var i;
      for (i = 0; (i < arguments.length); i++) {
        if (!arguments[i]) {
          continue;
        }
        var result = _.merge(result, arguments[i]);
      }
      return result;
    }

  };
};
<commit_msg>dead code removal<commit_after>var _ = require('lodash');
var util = require('util');

module.exports = function(self, options) {
  return {
    slugify: function(string) {
      return self.slugify(string);
    },

    // Log a message to the console from a Nunjucks template. Great for debugging.
    log: function(m) {
      console.log(util.inspect(m, { depth: 10 } ));
      return '';
    },

    // Generate a globally unique ID
    generateId: function() {
      return self.generateId();
    },

    // Test whether the specified date object refers to a date in the current year.
    // The events module utilizes this

    isCurrentYear: function(date) {
      var now = new Date();
      return date.getYear() === now.getYear();
    },

    // check if something is properly undefined
    isUndefined: function(o) {
      return (o === undefined);
    },

    // check if something is a function (as opposd to property)
    isFunction: function(o) {
      return (typeof o == 'function');
    },

    // make up for lack of triple equals
    eqStrict: function(a, b) {
      return (a === b);
    },

    // Returns true if the list contains the specified value.
    // If value is an array, returns true if the list contains
    // *any of* the specified values
    contains: function(list, value) {
      if (_.isArray(value)) {
        for (var i = 0; i < value.length; i++) {
          var valueItem = value[i];
          if (_.contains(list, valueItem)){
            return true;
          }
        }
        return false;
      }
      else{
        return _.contains(list, value);
      }
    },

    // Returns true if the list contains at least one
    // object with the named property.

    // The first parameter may also be a single object, in
    // which case this function returns true if that object
    // has the named property.

    containsProperty: function(list, property) {
      if (_.isArray(list)) {
        return _.some(list, function(item){ return _.has(item, property); });
      } else {
        return _.has(list, property);
      }
    },

    // Reverses the order of the array. This MODIFIES the array
    // in addition to returning it
    reverse: function(array){
      return array.reverse();
    },

    // If the `list` argument is a string, returns true if it begins
    // with `value`. If the `list` argument is an array, returns
    // true if at least one of its elements begins with `value`.
    beginsWith: function(list, value) {
      if (_.isArray(list)) {
        for (var i = 0; i < list.length; i++) {
          var listItem = list[i];
          if (listItem.indexOf(value) === 0) {
            return true;
          }
        }
      } else {
        if (list.indexOf(value) === 0) {
          return true;
        }
      }
      return false;
    },

    // Pass as many objects as you want; they will get merged via
    // `_.merge` into a new object, without modifying any of them, and
    // the resulting object will be returned. If several objects have
    // a property, the last object wins.
    //
    // This is useful to add one more option to an options object
    // which was passed to you.
    //
    // If any argument is null, it is skipped gracefully. This allows
    // you to pass in an options object without checking if it is null.

    merge: function() {
      var result = {};
      var i;
      for (i = 0; (i < arguments.length); i++) {
        if (!arguments[i]) {
          continue;
        }
        _.merge(true, result, arguments[i]);
      }
      return result;
    },

    // Find the first array element, if any, that has the specified value for
    // the specified property.

    find: function(arr, property, value) {
      return _.find(arr, function(item) {
        return (item[property] === value);
      });
    },

    // If propertyName is _id, then the keys in the returned
    // object will be the ids of each object in arr,
    // and the values will be the corresponding objects.
    // You may index by any property name.

    indexBy: function(arr, propertyName) {
      return _.indexBy(arr, propertyName);
    },

    // Find all the array elements, if any, that have the specified value for
    // the specified property.

    filter: function(arr, property, value) {
      return _.filter(arr, function(item) {
        return (item[property] === value);
      });
    },

    // Reject array elements that have the specified value for
    // the specified property.

    reject: function(arr, property, value) {
      return _.reject(arr, function(item) {
        return (item[property] === value);
      });
    },

    // Find all the array elements, if any, for which the specified property
    // is truthy.

    filterNonempty: function(arr, property) {
      return _.filter(arr, function(item) {
        return (item[property]);
      });
    },

    // Find all the array elements, if any, for which the specified property
    // is not truthy.

    filterEmpty: function(arr, property) {
      return _.filter(arr, function(item) {
        return (!item[property]);
      });
    },

    // Returns true if the specified array or object is considered empty.
    // Objects are empty if they have no own enumerable properties.
    // Arrays are considered empty if they have a length of 0.

    isEmpty: function(item) {
      return _.isEmpty(item);
    },

    // Given an array of objects with the given property, return an array with
    // the value of that property for each object.

    pluck: function(arr, property) {
      return _.pluck(arr, property);
    },

    // Given an object, return an object without
    // the named properties or array of named
    // properties (see _.omit()).

    omit: function(object, property /* , property... */) {
      return _.omit.apply(_, arguments);
    },

    // Concatenate all of the given arrays and/or values
    // into a single array. If an argument is an array, all
    // of its elements are individually added to the
    // resulting array. If an argument is a value, it is
    // added directly to the array.

    concat: function(arrOrObj1, arrOrObj2 /* , ... */) {
      // I tried to implement this with call() but I kept
      // getting arrays in my values. We still get the
      // benefit of concat() and its built-in support for
      // treating arrays and scalar values differently. -Tom
      var result = [];
      var i;
      for (i = 0; (i < arguments.length); i++) {
        result = result.concat(arguments[i]);
      }
      return result;
    },

    // Groups by the property named by 'key' on each of the values.
    // If the property referred to by the string 'key' is found to be
    // an array property of the first object, apos.utils.groupByArray is called.
    //
    // Usage: {{ apos.utils.groupBy(people, 'age') }} or {{ apos.utils.groupBy(items, 'tags') }}
    groupBy: function(items, key){
      if(items.length && Array.isArray(items[0][key])) {
        return groupByArray(items, key);
      }
      return _.groupBy(items, key);

      function groupByArray(items, arrayName) {
        var results = {};
        _.each(items, function(item) {
          _.each(item[arrayName] || [], function(inner) {
            if (!results[inner]) {
              results[inner] = [];
            }
            results[inner].push(item);
          });
        });

        return results;
      };

    },

    // Given a series of alternating keys and values, this
    // function returns an object with those values for
    // those keys. For instance, apos.utils.object('name', 'bob')
    // returns { name: 'bob' }. This is useful because
    // Nunjucks does not allow you to create an object with
    // a property whose name is unknown at the time the
    // template is written.
    object: function(/* key, value, ... */) {
      var o = {};
      var i = 0;
      while (i < arguments.length) {
        o[arguments[i]] = arguments[i + 1];
        i += 2;
      }
      return o;
    },

    merge: function() {
      var result = {};
      var i;
      for (i = 0; (i < arguments.length); i++) {
        if (!arguments[i]) {
          continue;
        }
        var result = _.merge(result, arguments[i]);
      }
      return result;
    }

  };
};
<|endoftext|>