<commit_before>// flow

import {
    React,
    Component,
    ActivityIndicator,
    View,
    TouchableOpacity,
    T,
} from './Component.js';
import { observable, transaction, computed, action } from 'mobx'
import { observer } from 'mobx-react/native'

import { Cache, cache } from './Cache.js'
import { LargeButton } from './Button.js'
import { PureComponent } from './Component.js'
import { Loader } from './Page.js'
import { config } from './Config.js'
import { store } from './Store.js'
import * as _ from './Curry.js'

import type { Int, Float, String, URL } from './Types.js'

const { log, assert } = _.utils('./HTTP.js')

/*********************************************************************/

export type HTTPOptions = RequestOptions

/*********************************************************************/
var HOST : String
// HOST = 'http://192.168.0.6:5000'
// HOST = 'http://192.168.0.28:5000'
HOST = 'http://172.24.176.169:5000'
// HOST = 'http://localhost:5000/graphql'
// HOST = 'http://10.147.18.19:5000'

export class TimeoutError {
    message : string
    constructor() {
        this.message = "Downloading is taking too long, please try again later."
    }
}

export class NetworkError {
    message : string
    constructor(message : string, status : string) {
        this.message = message
        this.status = status
    }
}

export type DownloadState =
    | 'NotStarted'  // download error
    | 'InProgress'  // download error
    | 'Finished'    // download error
    | 'Error'       // download error

export class DownloadResult<T> {

    @observable state   : DownloadState = 'NotStarted'
    @observable message : ?string       = undefined
    @observable value   : ?T            = null

    static combine = (downloadResults : Array<DownloadResult<*>>) => {
        return new CombinedDownloadResult(downloadResults)
    }

    getState = () => {
        return {
            state:      this.state,
            message:    this.message,
            value:      this.value,
        }
    }

    @action setState = (downloadState) => {
        this.state   = downloadState.state
        this.message = downloadState.message
        this.value   = downloadState.value
    }

    @action from = (downloadResult : DownloadResult<T>) => {
        this.state   = downloadResult.state
        this.message = downloadResult.message
        this.value   = downloadResult.value
    }

    @action reset = () : DownloadResult<T> => {
        this.state = 'NotStarted'
        this.message = undefined
        this.value = undefined
        return this
    }

    @action downloadStarted = () : DownloadResult<T> => {
        this.state   = 'InProgress'
        this.message = undefined
        this.value   = undefined
        return this
    }

    @action downloadError = (message : string) : DownloadResult<T> => {
        this.state   = 'Error'
        this.message = message
        this.value   = undefined
        return this
    }

    @action downloadFinished = (value : T) : DownloadResult<T> => {
        this.state   = 'Finished'
        this.message = undefined
        this.value   = value
        return this
    }

    update = (f : (value : T) => T) : DownloadResult<T> => {
        if (this.state == 'Finished') {
            this.value = f(this.value)
        }
        return this
    }
}

class CombinedDownloadResult<T> {

    @observable downloadResults : Array<DownloadResult> = null

    constructor(downloadResults : Array<DownloadResult>) {
        this.downloadResults = downloadResults
        this._value = null
    }

    @computed get state() {
        if (_.any(this.downloadResults.map(result => result.state === 'Error')))
            return 'Error'
        if (_.any(this.downloadResults.map(result => result.state === 'InProgress')))
            return 'InProgress'
        if (_.all(this.downloadResults.map(result => result.state === 'Finished')))
            return 'Finished'
        return 'NotStarted'
    }

    @computed get message() {
        const results = this.downloadResults
        for (var i = 0; i < results.length; i++) {
            if (results[i].state === 'Error')
                return results[i].message
        }
        return undefined
    }

    @computed get value() {
        if (this._value)
            return this._value
        if (this.state !== 'Finished')
            return null
        return this.downloadResults.map(downloadResult => downloadResult.value)
    }

    update = (f : (value : T) => T) : DownloadResult<T> => {
        if (this.state == 'Finished') {
            this._value = f(this.value)
        }
        return this
    }
}

export const emptyResult =
    <T>() : DownloadResult<T> => new DownloadResult()

/* React Component for rendering a downloadResult in its different states */
@observer
export class DownloadResultView<T> extends PureComponent {
    /* props:
        downloadResult: DownloadResult
    */

    inProgressMessage = null
    errorMessage = null

    render = () => {
        const res = this.getDownloadResult()
        if (res.state == 'NotStarted') {
            return this.renderNotStarted()
        } else if (res.state == 'InProgress') {
            return this.renderInProgress()
        } else if (res.state == 'Finished') {
            return this.renderFinished(res.value)
        } else if (res.state == 'Error') {
            return this.renderError(res.message)
        } else {
            throw Error('unreachable')
        }
    }

    getDownloadResult = () => {
        throw Error('NotImplemented')
    }

    refreshPage = () => {
        throw Error('NotImplemented')
    }

    renderNotStarted = () => {
        throw Error('NotImplemented')
    }

    renderFinished = (value : T) => {
        throw Error('NotImplemented')
    }

    renderInProgress = () => {
        return <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
            {
                this.inProgressMessage
                    ? <T style={{fontSize: 20, color: '#000'}}>
                        {this.inProgressMessage}
                      </T>
                    : undefined
            }
            <View>
                <Loader />
            </View>
        </View>
    }

    renderError = (message : string) => {
        assert(this.errorMessage != null,
               "Expected errorMessage to be set in DownloadResultView")
        const errorMessage = this.errorMessage + (message ? ': ' : '')
        const errorTextStyle = {
            fontSize: 20,
            color: config.theme.primary.dark,
            // color: config.theme.removeColor,
            textAlign: 'center',
        }
        return <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
            <T style={errorTextStyle}>{this.errorMessage}</T>
            { message
                ? <T style={errorTextStyle}>{message}</T>
                : undefined
            }
            <LargeButton
                style={{marginTop: 20}}
                label="Refresh"
                onPress={this.refreshPage}
                />
        </View>
    }
}

export const graphQLArg = (obj) => {
    if (typeof(obj) === 'number') {
        return JSON.stringify(obj)
    } else if (typeof(obj) === 'string') {
        return JSON.stringify(obj)
    } else if (Array.isArray(obj)) {
        const items = obj.map(graphQLArg).join(', ')
        return `[ ${items} ]`
    } else if (typeof(obj) === 'object') {
        const fields = Object.keys(obj).map(key => {
            const value = graphQLArg(obj[key])
            return `${key}: ${value}`
        }).join(', ')
        return `{ ${fields} }`
    } else {
        throw Error(`Unknown type: ${typeof(obj)}`)
    }
}

class DownloadManager {

    graphQLMutate = async (query) => {
        return await this.graphQL('DO_NOT_CACHE', query, { noCache: true })
    }

    /* Execute a GraphQL query */
    graphQL = async /*<T>*/(
            /* key used for caching responses */
            key : string,
            /* GraphQL query string to execute */
            query : string,
            /* Callback that decides whether the download is still relevant */
            cacheInfo    : CacheInfo,
            ) => { //: Promise<DownloadResult<T>> => {
        const httpOptions = {
            method: 'POST',
            headers: {
                // 'Accept': 'application/json',
                'Content-Type': 'application/graphql',
            },
            body: query,
        }
        const result = await this.fetchJSON(key, HOST + '/graphql', httpOptions, cacheInfo)
        const value = result.value
        if (value && value.errors && value.errors.length > 0) {
            /* GraphQL error */
            const errorMessage = value.errors[0].message
            return result.downloadError(errorMessage)
        }

        return result.update(value => value.data)
    }

    /* HTTP GET/POST/etc to a URL */
    fetchJSON = async /*<T>*/(
            /* key used for caching responses */
            key : string,
            /* URL to fetch */
            url : URL,
            /* HTTP options to pass to fetch() */
            httpOptions : HTTPOptions,
            cacheInfo : CacheInfo,
        ) : Promise<DownloadResult<T>> => {
        assert(url)
        /* Try a fresh download... */
        return await fetchJSONWithTimeouts(key, url, httpOptions, 12000, 20000, cacheInfo)
    }

}

export const downloadManager = new DownloadManager()

const fetchJSON = async /*<T>*/(
        key : string,
        url : URL,
        httpOptions : HTTPOptions,
        cacheInfo   : CacheInfo,
        ) : Promise<DownloadResult<T>> => {
    return await fetchJSONWithTimeouts(key, url, httpOptions, 5000, 12000, cacheInfo)
}

const isNetworkError = (e : Error) : boolean =>
    e instanceof NetworkError || e instanceof TimeoutError

const fetchJSONWithTimeouts = async /*<T>*/(
        key             : string,
        url             : URL,
        httpOptions     : HTTPOptions,
        refreshTimeout  : Float,
        expiredTimeout  : Float,
        cacheInfo       : CacheInfo,
        ) : Promise<DownloadResult<T>> => {

    const refreshCallback = async () => {
        return await simpleFetchJSON(url, httpOptions, refreshTimeout)
    }
    const expiredCallback = async () => {
        return await simpleFetchJSON(url, httpOptions, expiredTimeout)
    }

    if (cacheInfo && cacheInfo.noCache) {
        /* Don't cache anything... */
        return await refreshCallback()
    }

    try {
        const result = await cache.get(key, refreshCallback, /*expiredCallback, */ cacheInfo = cacheInfo)
        return new DownloadResult().downloadFinished(result)
    } catch (e) {
        if (isNetworkError(e))
            return new DownloadResult().downloadError(e.message)
        throw e
    }
}

export const simpleFetchJSON = async /*<T>*/(
        url             : URL,
        httpOptions     : HTTPOptions,
        downloadTimeout : Float,
        ) : Promise<T> => {
    var response : Response
    try {
        const fetchPromise : Promise<Response> = fetch(url, httpOptions)
        response = await _.timeout(downloadTimeout, fetchPromise)
        // response = await fetch(url, httpOptions)
    } catch (err) {
        throw new NetworkError(err.message)
    }
    if (response.status !== 200) {
        throw new NetworkError("Network Error", response.status)
    }
    return await response.json()
}
<commit_msg>Properly disable cache for graphQL mutations<commit_after>// flow

import {
    React,
    Component,
    ActivityIndicator,
    View,
    TouchableOpacity,
    T,
} from './Component.js';
import { observable, transaction, computed, action } from 'mobx'
import { observer } from 'mobx-react/native'

import { Cache, cache } from './Cache.js'
import { LargeButton } from './Button.js'
import { PureComponent } from './Component.js'
import { Loader } from './Page.js'
import { config } from './Config.js'
import { store } from './Store.js'
import * as _ from './Curry.js'

import type { Int, Float, String, URL } from './Types.js'

const { log, assert } = _.utils('./HTTP.js')

/*********************************************************************/

export type HTTPOptions = RequestOptions

/*********************************************************************/
var HOST : String
// HOST = 'http://192.168.0.6:5000'
// HOST = 'http://192.168.0.28:5000'
HOST = 'http://172.24.176.169:5000'
// HOST = 'http://localhost:5000/graphql'
// HOST = 'http://10.147.18.19:5000'

export class TimeoutError {
    message : string
    constructor() {
        this.message = "Downloading is taking too long, please try again later."
    }
}

export class NetworkError {
    message : string
    constructor(message : string, status : string) {
        this.message = message
        this.status = status
    }
}

export type DownloadState =
    | 'NotStarted'  // download error
    | 'InProgress'  // download error
    | 'Finished'    // download error
    | 'Error'       // download error

export class DownloadResult<T> {

    @observable state   : DownloadState = 'NotStarted'
    @observable message : ?string       = undefined
    @observable value   : ?T            = null

    static combine = (downloadResults : Array<DownloadResult<*>>) => {
        return new CombinedDownloadResult(downloadResults)
    }

    getState = () => {
        return {
            state:      this.state,
            message:    this.message,
            value:      this.value,
        }
    }

    @action setState = (downloadState) => {
        this.state   = downloadState.state
        this.message = downloadState.message
        this.value   = downloadState.value
    }

    @action from = (downloadResult : DownloadResult<T>) => {
        this.state   = downloadResult.state
        this.message = downloadResult.message
        this.value   = downloadResult.value
    }

    @action reset = () : DownloadResult<T> => {
        this.state = 'NotStarted'
        this.message = undefined
        this.value = undefined
        return this
    }

    @action downloadStarted = () : DownloadResult<T> => {
        this.state   = 'InProgress'
        this.message = undefined
        this.value   = undefined
        return this
    }

    @action downloadError = (message : string) : DownloadResult<T> => {
        this.state   = 'Error'
        this.message = message
        this.value   = undefined
        return this
    }

    @action downloadFinished = (value : T) : DownloadResult<T> => {
        this.state   = 'Finished'
        this.message = undefined
        this.value   = value
        return this
    }

    update = (f : (value : T) => T) : DownloadResult<T> => {
        if (this.state == 'Finished') {
            this.value = f(this.value)
        }
        return this
    }
}

class CombinedDownloadResult<T> {

    @observable downloadResults : Array<DownloadResult> = null

    constructor(downloadResults : Array<DownloadResult>) {
        this.downloadResults = downloadResults
        this._value = null
    }

    @computed get state() {
        if (_.any(this.downloadResults.map(result => result.state === 'Error')))
            return 'Error'
        if (_.any(this.downloadResults.map(result => result.state === 'InProgress')))
            return 'InProgress'
        if (_.all(this.downloadResults.map(result => result.state === 'Finished')))
            return 'Finished'
        return 'NotStarted'
    }

    @computed get message() {
        const results = this.downloadResults
        for (var i = 0; i < results.length; i++) {
            if (results[i].state === 'Error')
                return results[i].message
        }
        return undefined
    }

    @computed get value() {
        if (this._value)
            return this._value
        if (this.state !== 'Finished')
            return null
        return this.downloadResults.map(downloadResult => downloadResult.value)
    }

    update = (f : (value : T) => T) : DownloadResult<T> => {
        if (this.state == 'Finished') {
            this._value = f(this.value)
        }
        return this
    }
}

export const emptyResult =
    <T>() : DownloadResult<T> => new DownloadResult()

/* React Component for rendering a downloadResult in its different states */
@observer
export class DownloadResultView<T> extends PureComponent {
    /* props:
        downloadResult: DownloadResult
    */

    inProgressMessage = null
    errorMessage = null

    render = () => {
        const res = this.getDownloadResult()
        if (res.state == 'NotStarted') {
            return this.renderNotStarted()
        } else if (res.state == 'InProgress') {
            return this.renderInProgress()
        } else if (res.state == 'Finished') {
            return this.renderFinished(res.value)
        } else if (res.state == 'Error') {
            return this.renderError(res.message)
        } else {
            throw Error('unreachable')
        }
    }

    getDownloadResult = () => {
        throw Error('NotImplemented')
    }

    refreshPage = () => {
        throw Error('NotImplemented')
    }

    renderNotStarted = () => {
        throw Error('NotImplemented')
    }

    renderFinished = (value : T) => {
        throw Error('NotImplemented')
    }

    renderInProgress = () => {
        return <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
            {
                this.inProgressMessage
                    ? <T style={{fontSize: 20, color: '#000'}}>
                        {this.inProgressMessage}
                      </T>
                    : undefined
            }
            <View>
                <Loader />
            </View>
        </View>
    }

    renderError = (message : string) => {
        assert(this.errorMessage != null,
               "Expected errorMessage to be set in DownloadResultView")
        const errorMessage = this.errorMessage + (message ? ': ' : '')
        const errorTextStyle = {
            fontSize: 20,
            color: config.theme.primary.dark,
            // color: config.theme.removeColor,
            textAlign: 'center',
        }
        return <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
            <T style={errorTextStyle}>{this.errorMessage}</T>
            { message
                ? <T style={errorTextStyle}>{message}</T>
                : undefined
            }
            <LargeButton
                style={{marginTop: 20}}
                label="Refresh"
                onPress={this.refreshPage}
                />
        </View>
    }
}

export const graphQLArg = (obj) => {
    if (typeof(obj) === 'number') {
        return JSON.stringify(obj)
    } else if (typeof(obj) === 'string') {
        return JSON.stringify(obj)
    } else if (Array.isArray(obj)) {
        const items = obj.map(graphQLArg).join(', ')
        return `[ ${items} ]`
    } else if (typeof(obj) === 'object') {
        const fields = Object.keys(obj).map(key => {
            const value = graphQLArg(obj[key])
            return `${key}: ${value}`
        }).join(', ')
        return `{ ${fields} }`
    } else {
        throw Error(`Unknown type: ${typeof(obj)}`)
    }
}

class DownloadManager {

    graphQLMutate = async (query) => {
        return await this.graphQL('DO_NOT_CACHE', query, { noCache: true })
    }

    /* Execute a GraphQL query */
    graphQL = async /*<T>*/(
            /* key used for caching responses */
            key : string,
            /* GraphQL query string to execute */
            query : string,
            /* Callback that decides whether the download is still relevant */
            cacheInfo    : CacheInfo,
            ) => { //: Promise<DownloadResult<T>> => {
        const httpOptions = {
            method: 'POST',
            headers: {
                // 'Accept': 'application/json',
                'Content-Type': 'application/graphql',
            },
            body: query,
        }
        const result = await this.fetchJSON(key, HOST + '/graphql', httpOptions, cacheInfo)
        const value = result.value
        if (value && value.errors && value.errors.length > 0) {
            /* GraphQL error */
            const errorMessage = value.errors[0].message
            return result.downloadError(errorMessage)
        }

        return result.update(value => value.data)
    }

    /* HTTP GET/POST/etc to a URL */
    fetchJSON = async /*<T>*/(
            /* key used for caching responses */
            key : string,
            /* URL to fetch */
            url : URL,
            /* HTTP options to pass to fetch() */
            httpOptions : HTTPOptions,
            cacheInfo : CacheInfo,
        ) : Promise<DownloadResult<T>> => {
        assert(url)
        /* Try a fresh download... */
        return await fetchJSONWithTimeouts(key, url, httpOptions, 12000, 20000, cacheInfo)
    }

}

export const downloadManager = new DownloadManager()

const fetchJSON = async /*<T>*/(
        key : string,
        url : URL,
        httpOptions : HTTPOptions,
        cacheInfo   : CacheInfo,
        ) : Promise<DownloadResult<T>> => {
    return await fetchJSONWithTimeouts(key, url, httpOptions, 5000, 12000, cacheInfo)
}

const isNetworkError = (e : Error) : boolean =>
    e instanceof NetworkError || e instanceof TimeoutError

const fetchJSONWithTimeouts = async /*<T>*/(
        key             : string,
        url             : URL,
        httpOptions     : HTTPOptions,
        refreshTimeout  : Float,
        expiredTimeout  : Float,
        cacheInfo       : CacheInfo,
        ) : Promise<DownloadResult<T>> => {

    const refreshCallback = async () => {
        return await simpleFetchJSON(url, httpOptions, refreshTimeout)
    }
    const expiredCallback = async () => {
        return await simpleFetchJSON(url, httpOptions, expiredTimeout)
    }

    var result
    try {
        if (cacheInfo && cacheInfo.noCache)
            result = await refreshCallback()
        else
            result = await cache.get(key, refreshCallback, /*expiredCallback, */ cacheInfo = cacheInfo)
        return new DownloadResult().downloadFinished(result)
    } catch (e) {
        if (isNetworkError(e))
            return new DownloadResult().downloadError(e.message)
        throw e
    }
}

export const simpleFetchJSON = async /*<T>*/(
        url             : URL,
        httpOptions     : HTTPOptions,
        downloadTimeout : Float,
        ) : Promise<T> => {
    var response : Response
    try {
        const fetchPromise : Promise<Response> = fetch(url, httpOptions)
        response = await _.timeout(downloadTimeout, fetchPromise)
        // response = await fetch(url, httpOptions)
    } catch (err) {
        throw new NetworkError(err.message)
    }
    if (response.status !== 200) {
        throw new NetworkError("Network Error", response.status)
    }
    return await response.json()
}
<|endoftext|>


<commit_before>var fs = require('fs');
var path = require('path');

var servers = function(api, next){

  api.servers = {};
  api.servers.servers = [];

  api.servers._start = function(api, next){

    var started = 0;
    if(api.utils.hashLength(api.servers.servers) == 0){ next() }
    for(var server in api.servers.servers){
      started++;
      if(api.config.servers[server].enabled === true){
        api.log('starting server: ' + server, 'notice');
        api.servers.servers[server]._start(function(){
          process.nextTick(function(){
            started--;
            if(started == 0){ next() }
          });
        });
      }else{
        process.nextTick(function(){
          started--;
          if(started == 0){ next() }
        });
      }
    }
  }

  api.servers._stop = function(api, next){
    var started = 0;
    if(api.utils.hashLength(api.servers.servers) == 0){ next() }
    for(var server in api.servers.servers){
      started++;
      (function(server){
        api.log('stopping server: ' + server, 'notice');
        api.servers.servers[server]._stop(function(){
          process.nextTick(function(){
            api.log('server stopped: ' + server, 'debug');
            started--;
            if(started == 0){ next() }
          });
        });
      })(server)
    }
  }

  // Load the servers

  var serverFolders = [
    path.resolve(__dirname + '/../servers')
  ];

  api.config.general.paths.server.forEach(function(p){
    p = path.resolve(p);
    if(serverFolders.indexOf(p) < 0){
      serverFolders.push(p);
    }
  })

  var inits = {}

  serverFolders.forEach(function(p){
    api.utils.recursiveDirectoryGlob(p).forEach(function(f){
      var parts = f.split(/[\/\\]+/)
      var server = parts[(parts.length - 1)].split('.')[0];
      if(api.config.servers[server] != null && api.config.servers[server].enabled === true){
        inits[server] = require(f)[server];
      }
      api.watchFileAndAct(f, function(){
        api.log('\r\n\r\n*** rebooting due to server ('+server+') change ***\r\n\r\n', 'info');
        api.commands.restart.call(api._self);
      });
    });
  })

  var started = 0;
  for(var server in inits){
    started++;
    (function(server){
      var options = api.config.servers[server];
      inits[server](api, options, function(serverObject){
        api.servers.servers[server] = serverObject;
        api.log('initialized server: ' + server, 'debug');
        process.nextTick(function(){
          started--;
          if(started == 0){ next() }
        });
      });
    })(server)
  }
  if(started == 0){ next() }
}

exports.servers = servers;
<commit_msg>server boot (still needs work)<commit_after>var path = require('path');

var servers = function(api, next){

  api.servers = {};
  api.servers.servers = [];

  api.servers._start = function(api, next){

    var started = 0;
    if(api.utils.hashLength(api.servers.servers) === 0){ next() }
    for(var server in api.servers.servers){
      started++;
      if(api.config.servers[server].enabled === true){
        api.log('starting server: ' + server, 'notice');
        api.servers.servers[server]._start(function(){
          process.nextTick(function(){
            started--;
            if(started === 0){ next() }
          });
        });
      }else{
        process.nextTick(function(){
          started--;
          if(started === 0){ next() }
        });
      }
    }
  }

  api.servers._stop = function(api, next){
    var started = 0;
    if(api.utils.hashLength(api.servers.servers) === 0){ next() }
    for(var server in api.servers.servers){
      started++;
      (function(server){
        api.log('stopping server: ' + server, 'notice');
        api.servers.servers[server]._stop(function(){
          process.nextTick(function(){
            api.log('server stopped: ' + server, 'debug');
            started--;
            if(started === 0){ next() }
          });
        });
      })(server)
    }
  }

  // Load the servers

  var serverFolders = [
    path.resolve(__dirname + '/../servers')
  ];

  api.config.general.paths.server.forEach(function(p){
    p = path.resolve(p);
    if(serverFolders.indexOf(p) < 0){
      serverFolders.push(p);
    }
  })

  var inits = {}

  serverFolders.forEach(function(p){
    api.utils.recursiveDirectoryGlob(p).forEach(function(f){
      var parts = f.split(/[\/\\]+/)
      var server = parts[(parts.length - 1)].split('.')[0];
      if(api.config.servers[server] && api.config.servers[server].enabled === true){
        inits[server] = require(f)[server];
      }
      api.watchFileAndAct(f, function(){
        api.log('\r\n\r\n*** rebooting due to server ('+server+') change ***\r\n\r\n', 'info');
        api.commands.restart.call(api._self);
      });
    });
  })

  var started = 0;
  for(var server in inits){
    started++;
    (function(server){
      var options = api.config.servers[server];
      inits[server](api, options, function(serverObject){
        api.servers.servers[server] = serverObject;
        api.log('initialized server: ' + server, 'debug');
        process.nextTick(function(){
          started--;
          if(started === 0){ next() }
        });
      });
    })(server)
  }
  if(started === 0){ next() }
}

exports.servers = servers;
<|endoftext|>


<commit_before>(function(d3) {
	// Example
	// <script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/inputs/select/select.js"></script>
	//
	//		isoLines = d3.ui.select({
	//			base: inputs,
	// 			onUpdate: update,
	//			data: [{text: "show lines", value: "#ccc"}, {text: "hide lines", value: "none"}]
	//		}),
	//
	//		.style("stroke", isoLines.value());
	//

	d3.ui = d3.ui || {};
	d3.ui.select = function (config) {
        // add a select element on base with options matching data
        // if the text and value is the same then data is scalar array
        // 	otherwise the data elements must have text and value fields
        // config
        //  base
        //  before
        //  style
        //  initial
        //  hook
        //  onX
        var select  = (config.base ?
                       (config.base.append ? config.base : d3.select(config.base)) :
                       d3.select("body"))
                [config.before ? "insert" : "append"]("select", config.before ? config.before : null)
                .each(hookEvents)
                .data([config.data]),
            options = select.selectAll("option").data(function(d) {return d});
        options.enter().append("option");
        options.exit().remove();
        if(config.style) select.style(config.style);
        if(config.properties) merge(config.properties, options, ["base", "before", "style", "initial", "hook", /on.+/]);
        return options
            .attr({
                value: function(d) {
                    return d.value || d;
                },
                selected: function(d){return d == config.initial ? "selected" : null}
            })
            .text(function(d) {
                return d.text || d
            })
            .call(function() { //add a custom property to the final selection
                if(config.hook) config.hook();
                this.value = function() {
                    return this[0].parentNode.value
                }
            });

        function hookEvents() {
            // store the DOM element
            var _control = this;
            // config object for the control
            // parse the keys and bind a listener to any key beginning with "on"
            Object.keys(config).filter(function(k) {
                return k.slice(0, 2) == "on";
            }).map(function(p, i, listeners) {
                // strip the event name off the listener
                var e = p.slice(2);

                if(!config.on)
                // lazily create a dispatch with the events found in config
                    config.on = d3.dispatch
                        .apply(null, listeners.map(function(l) {
                            return l.slice(2);
                        }));

                config.on.on(e, config[p]);
                // then hook the dispatch to the element event
                d3.select(_control).on(e, function() {
                    config.on[e].apply(this, arguments)
                })

            });
        }
        function merge(source, target, exclude) {
            function included(test){
                return !exclude.some(function(p,i){return test.match(p)})
            }
            for(var p in source) if(included(p) && target && !target.hasOwnProperty(p)) target[p] = source[p];
            return target;
        }
    }

    //experiment with chaining...
	d3.ui.select2 = function (base){
		var _base, _on, _onUpdate, _data;
		function select(){}
		d3_ui_select.base = function(base){
			_base = (base.append ? base : d3.select(base)).append("select");
			return this;
		}
		function on(on, listener) {
			if (arguments.length == 1) return _on;
			return this.on(_on || "input", _onUpdate)
		}
	}

})(d3)
<commit_msg>select - pass through user properties<commit_after>(function(d3) {
	// Example
	// <script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/inputs/select/select.js"></script>
	//
	//		isoLines = d3.ui.select({
	//			base: inputs,
	// 			onUpdate: update,
	//			data: [{text: "show lines", value: "#ccc"}, {text: "hide lines", value: "none"}]
	//		}),
	//
	//		.style("stroke", isoLines.value());
	//

	d3.ui = d3.ui || {};
	d3.ui.select = function (config) {
        // add a select element on base with options matching data
        // if the text and value is the same then data is scalar array
        // 	otherwise the data elements must have text and value fields
        // config
        //  base
        //  before
        //  style
        //  initial
        //  hook
        //  onX
        var select  = (config.base ?
                       (config.base.append ? config.base : d3.select(config.base)) :
                       d3.select("body"))
                [config.before ? "insert" : "append"]("select", config.before ? config.before : null)
                .each(hookEvents)
                .data([config.data]),
            options = select.selectAll("option").data(function(d) {return d});
        options.enter().append("option");
        options.exit().remove();
        if(config.style) select.style(config.style);
        merge(config, options, ["base", "before", "style", "initial", "hook", /on.+/]);
        return options
            .attr({
                value: function(d) {
                    return d.value || d;
                },
                selected: function(d){return d == config.initial ? "selected" : null}
            })
            .text(function(d) {
                return d.text || d
            })
            .call(function() { //add a custom property to the final selection
                if(config.hook) config.hook();
                this.value = function() {
                    return this[0].parentNode.value
                }
            });

        function hookEvents() {
            // store the DOM element
            var _control = this;
            // config object for the control
            // parse the keys and bind a listener to any key beginning with "on"
            Object.keys(config).filter(function(k) {
                return k.slice(0, 2) == "on";
            }).map(function(p, i, listeners) {
                // strip the event name off the listener
                var e = p.slice(2);

                if(!config.on)
                // lazily create a dispatch with the events found in config
                    config.on = d3.dispatch
                        .apply(null, listeners.map(function(l) {
                            return l.slice(2);
                        }));

                config.on.on(e, config[p]);
                // then hook the dispatch to the element event
                d3.select(_control).on(e, function() {
                    config.on[e].apply(this, arguments)
                })

            });
        }
        function merge(source, target, exclude) {
            function included(test){
                return !exclude.some(function(p,i){return test.match(p)})
            }
            for(var p in source)
                if(included(p) && target && !target.hasOwnProperty(p))
                    Object.defineProperty(target, p, Object.getOwnPropertyDescriptor(source, p));
            return target;
        }
    }

    //experiment with chaining...
	d3.ui.select2 = function (base){
		var _base, _on, _onUpdate, _data;
		function select(){}
		d3_ui_select.base = function(base){
			_base = (base.append ? base : d3.select(base)).append("select");
			return this;
		}
		function on(on, listener) {
			if (arguments.length == 1) return _on;
			return this.on(_on || "input", _onUpdate)
		}
	}

})(d3)
<|endoftext|>


<commit_before>import Command from './command/Command.js';
import Config from './config/Config.js';
import Converter from './converter/Converter.js';
import Filter from './filter/Filter.js';
import Observer from './observer/Observer.js';
import Tag from './config/Tag.js';
import TextCommand from './command/TextCommand.js';
import configCommand from '../cfg/command.js';
import configConverter from '../cfg/converter.js';
import configFilter from '../cfg/filter.js';
import configObserver from '../cfg/observer.js';
import configTag from '../cfg/tag.js';

/**
 * Editor
 */
export default class Editor {
    /**
     * Creates a new instance of editor with given configuration
     *
     * @param {HTMLElement} orig
     * @param {Object} [cfg = {}]
     */
    constructor(orig, cfg = {}) {
        if (!(orig instanceof HTMLElement)) {
            throw 'Invalid element';
        }

        const element = orig.ownerDocument.createElement('div');
        const content = orig.ownerDocument.createElement('div');
        const toolbarMain = orig.ownerDocument.createElement('div');
        const toolbarEditable = orig.ownerDocument.createElement('div');

        element.classList.add('editor');
        content.classList.add('editor-content');
        toolbarMain.classList.add('editor-toolbar', 'editor-toolbar-main');
        toolbarEditable.classList.add('editor-toolbar', 'editor-toolbar-editable');

        orig.hidden = true;
        orig.insertAdjacentElement('afterend', element);
        element.appendChild(toolbarMain);
        element.appendChild(content);
        element.appendChild(toolbarEditable);

        /**
         * Corresponding DOM element of the source
         *
         * @type {HTMLElement}
         * @readonly
         */
        this.orig = orig;

        /**
         * Correspondig DOM Document
         *
         * @type {Document}
         * @readonly
         */
        this.document = orig.ownerDocument;

        /**
         * Corresponding Window object
         *
         * @type {Window}
         * @readonly
         */
        this.window = this.document.defaultView;

        /**
         * Configuration
         *
         * @type {Config}
         * @readonly
         */
        this.config = new Config(cfg);

        /**
         * Corresponding DOM element of the editor
         *
         * @type {HTMLDivElement}
         * @readonly
         */
        this.element = element;

        /**
         * Corresponding DOM element of the editor content
         *
         * @type {HTMLDivElement}
         * @readonly
         */
        this.content = content;

        /**
         * Corresponding DOM element of the main toolbar
         *
         * @type {HTMLDivElement}
         * @readonly
         */
        this.toolbarMain = toolbarMain;

        /**
         * Corresponding DOM element of the editable toolbar
         *
         * @type {HTMLDivElement}
         * @readonly
         */
        this.toolbarEditable = toolbarEditable;

        /**
         * Tags
         *
         * @type {Map<String, Tag>}
         * @readonly
         */
        this.tags = this.createTags(configTag);

        /**
         * Element converters
         *
         * @type {Map<String, Converter>}
         * @readonly
         */
        this.converters = this.createConverters(configConverter);

        /**
         * Commands
         *
         * @type {Map<String, Command>}
         */
        this.commands = this.createCommands(configCommand);

        /**
         * Observers
         *
         * @type {Observer[]}
         */
        this.observers = this.create(configObserver, Observer);

        /**
         * Filters
         *
         * @type {Filter[]}
         */
        this.filters = this.create(configFilter, Filter);
    }

    /**
     * Instantiates each element in given array with this editor instance
     *
     * @private
     *
     * @param {Function[]} config
     * @param {Function} constructor
     *
     * @return {Array}
     */
    create(config, constructor) {
        return config.map(item => {
            if (typeof item !== 'function' || !(item = new item(this)) || !(item instanceof constructor)) {
                throw 'Invalid item';
            }

            return item;
        });
    }

    /**
     * Creates tags map
     *
     * @private
     *
     * @param {Object[]} config
     *
     * @return {Map<String, Tag>}
     */
    createTags(config) {
        const map = new Map();

        config.forEach(item => {
            const tag = new Tag(item);
            return map.set(tag.name, tag);
        });

        return map;
    }

    /**
     * Creates converters map
     *
     * @private
     *
     * @param {Object.<String, Converter>} config
     *
     * @return {Map<String, Converter>}
     */
    createConverters(config) {
        const map = new Map();

        Object.getOwnPropertyNames(config).forEach(key => {
            if (!(config[key] instanceof Converter)) {
                throw 'Invalid converter';
            }

            map.set(key, config[key]);
        });

        return map;
    }

    /**
     * Creates commands map
     *
     * @private
     *
     * @param {Object.<String, Function>} config
     *
     * @return {Map<String, Command>}
     */
    createCommands(config) {
        const map = new Map();

        Object.getOwnPropertyNames(config).forEach(key => {
            let command;

            if (typeof config[key] !== 'function' || !(command = config[key](this)) || !(command instanceof Command)) {
                throw 'Invalid command';
            }

            return map.set(key, command);
        });

        return map;
    }

    /**
     * Init editor
     */
    init() {
        this.observers.forEach(item => this.register(ev => item.observe(ev)));
        this.initContent();
        this.initToolbar();
    }

    /**
     * Init content
     *
     * @private
     */
    initContent() {
        if (this.orig instanceof HTMLTextAreaElement) {
            this.content.innerHTML = this.orig.value.replace('/ /g', ' ');
            this.orig.form.addEventListener('submit', () => this.save());
        } else {
            this.content.innerHTML = this.orig.innerHTML;
        }

        this.filter(this.content);
    }

    /**
     * Init Toolbar
     *
     * @private
     */
    initToolbar() {
        this.content.addEventListener('selectstart', () => {
            const active = this.document.activeElement;

            if (active.isContentEditable && this.allowedGroup('text', active.tagName)) {
                this.toolbarEditable.classList.add('editor-toolbar-active');
                this.toolbarEditable.style.top = (active.offsetTop + active.offsetParent.offsetTop - this.toolbarEditable.clientHeight) + 'px';
            }
        });
        this.document.addEventListener('selectionchange', () => {
            const active = this.document.activeElement;

            if (this.window.getSelection().isCollapsed || !active.isContentEditable || !this.content.contains(active)) {
                this.toolbarEditable.classList.remove('editor-toolbar-active');
                this.toolbarEditable.removeAttribute('style');
            }
        });

        for (let cmd of this.commands.entries()) {
            const item = this.document.createElement('button');
            item.setAttribute('type', 'button');
            item.setAttribute('data-cmd', cmd[0]);
            item.setAttribute('title', cmd[0]);
            item.innerText = cmd[0];
            item.addEventListener('click', () => {
                if (!this.window.getSelection().containsNode(this.content, true)) {
                    this.content.focus();
                }

                cmd[1].execute();
            });

            if (cmd[1] instanceof TextCommand) {
                this.toolbarEditable.appendChild(item);
            } else {
                this.toolbarMain.appendChild(item);
            }
        }
    }

    /**
     * Short-cut method to register a mutation observer
     *
     * @param {Function} call
     * @param {Object} [config = {childList: true, subtree: true}]
     */
    register(call, config = {childList: true, subtree: true}) {
        if (typeof call !== 'function') {
            throw 'Invalid observer';
        }

        const mutation = new MutationObserver(call);
        mutation.observe(this.content, config);
    }

    /**
     * Execute command
     *
     * @param {String} name
     */
    execute(name) {
        const command = this.commands.get(name);

        if (!command) {
            throw 'Invalid command';
        }

        command.execute();
    }

    /**
     * Returns editor element's innerHTML
     *
     * @return {String}
     */
    getData() {
        const content = this.content.cloneNode(true);
        this.filter(content, true);

        return content.innerHTML;
    }

    /**
     * Saves editor data to source element
     */
    save() {
        if (this.orig instanceof HTMLTextAreaElement) {
            this.orig.value = this.getData();
        } else {
            this.orig.innerHTML = this.getData();
        }
    }

    /**
     * Inserts an element at the editor element or optionally after given reference element
     *
     * @param {HTMLElement} element
     * @param {?HTMLElement} [ref = null]
     */
    insert(element, ref = null) {
        if (!(element instanceof HTMLElement) || ref && !(ref instanceof HTMLElement)) {
            throw 'Invalid HTML element';
        }

        const parent = ref ? ref.parentElement : this.content;
        const parentName = this.getTagName(parent);

        if (!this.content.contains(parent) || !this.allowed(element.tagName, parentName)) {
            throw 'Element is not allowed here';
        }

        parent.insertBefore(element, ref ? ref.nextElementSibling : null)
    }

    /**
     * Insert text
     *
     * @param {String} text
     */
    insertText(text) {
        this.document.execCommand('inserttext', false, text);
    }

    /**
     * Adds or removes formatting to/from selected text
     *
     * @param {HTMLElement} element
     */
    formatText(element) {
        if (!(element instanceof HTMLElement)) {
            throw 'Invalid HTML element';
        }

        const sel = this.window.getSelection();
        const anc = sel.anchorNode instanceof Text ? sel.anchorNode.parentElement : sel.anchorNode;
        const ancEdit = this.getSelectedEditable();

        if (sel.isCollapsed || !sel.toString().trim() || !(anc instanceof HTMLElement) || !ancEdit) {
            return;
        }

        const range = sel.getRangeAt(0);
        const tag = this.getTag(anc.tagName);
        const parent = !tag || tag.group === 'text' ? anc.parentElement : anc;

        if (range.startContainer instanceof Text && !range.startContainer.parentElement.isSameNode(parent)) {
            range.setStartBefore(range.startContainer.parentElement);
        }

        if (range.endContainer instanceof Text && !range.endContainer.parentElement.isSameNode(parent)) {
            range.setEndAfter(range.endContainer.parentElement);
        }

        const selText = range.toString();
        const selNodes = range.cloneContents().childNodes;
        let same = Array.from(selNodes).every(item => {
            return item instanceof Text && !item.nodeValue.trim() || item instanceof HTMLElement && item.tagName === element.tagName;
        });

        range.deleteContents();

        if (parent.isContentEditable && this.allowed(element.tagName, parent.tagName) && selText.trim() && (!tag || !same)) {
            element.innerText = selText;
            range.insertNode(element);
        } else {
            range.insertNode(this.document.createTextNode(selText));
        }

        parent.normalize();
    }

    /**
     * Returns current selected element or null
     *
     * @return {?HTMLElement}
     */
    getSelectedElement() {
        const sel = this.window.getSelection();
        const anc = sel.anchorNode instanceof Text ? sel.anchorNode.parentElement : sel.anchorNode;
        const foc = sel.focusNode instanceof Text ? sel.focusNode.parentElement : sel.focusNode;

        return anc instanceof HTMLElement && foc instanceof HTMLElement && anc.isSameNode(foc) && this.content.contains(anc) ? anc : null;
    }

    /**
     * Returns current selected contenteditable or null
     *
     * @return {?HTMLElement}
     */
    getSelectedEditable() {
        const sel = this.window.getSelection();
        const anc = sel.anchorNode instanceof Text ? sel.anchorNode.parentElement : sel.anchorNode;
        const foc = sel.focusNode instanceof Text ? sel.focusNode.parentElement : sel.focusNode;

        if (anc instanceof HTMLElement && foc instanceof HTMLElement) {
            const ancEdit = anc.closest('[contenteditable=true]');
            const focEdit = foc.closest('[contenteditable=true]');

            if (ancEdit instanceof HTMLElement && focEdit instanceof HTMLElement && ancEdit.isSameNode(focEdit) && this.content.contains(ancEdit)) {
                return ancEdit;
            }
        }

        return null;
    }

    /**
     * Returns current selected widget or null
     *
     * @return {?HTMLElement}
     */
    getSelectedWidget() {
        const el = this.getSelectedElement();

        return el ? el.closest('div.editor-content > *') : null;
    }

    /**
     * Focus end of contents
     *
     * @param {HTMLElement} element
     */
    focusEnd(element) {
        if (!(element instanceof HTMLElement)) {
            throw 'No HTML element';
        }

        const range = this.document.createRange();
        const sel = this.window.getSelection();
        element.focus();
        range.selectNodeContents(element);
        range.collapse();
        sel.removeAllRanges();
        sel.addRange(range);
    }

    /**
     * Filters element
     *
     * @param {HTMLElement} parent
     * @param {Boolean} [forceRoot = false]
     */
    filter(parent, forceRoot = false) {
        this.filters.forEach(item => item.filter(parent, forceRoot));
    }

    /**
     * Returns tag name from element considering exception for root element
     *
     * @param {HTMLElement} element
     *
     * @return {String}
     */
    getTagName(element) {
        if (!(element instanceof HTMLElement)) {
            throw 'No HTML element';
        }

        return this.content.isSameNode(element) ? 'root' : element.tagName;
    }

    /**
     * Returns tag configuration vor given tag name
     *
     * @param {String} name
     *
     * @return {?Tag}
     */
    getTag(name) {
        return this.tags.get(name.toLowerCase()) || null;
    }

    /**
     * Checks if given element is allowed inside given parent element
     *
     * @param {String} name
     * @param {String} parentName
     *
     * @return {Boolean}
     */
    allowed(name, parentName) {
        const tag = this.getTag(name);
        const parentTag = this.getTag(parentName);

        return tag && parentTag && parentTag.children.includes(tag.group);
    }

    /**
     * Checks if given group is allowed inside given parent element
     *
     * @param {String} group
     * @param {String} parentName
     *
     * @return {Boolean}
     */
    allowedGroup(group, parentName) {
        const parentTag = this.getTag(parentName);

        return parentTag && parentTag.children.includes(group);
    }

    /**
     * Converts element
     *
     * @param {HTMLElement} element
     *
     * @return {Node}
     */
    convert(element) {
        if (!(element instanceof HTMLElement)) {
            throw 'No HTML element';
        }

        const converter = this.converters.get(element.tagName.toLowerCase());

        if (!converter) {
            return element;
        }

        const newNode = converter.convert(element);
        element.parentElement.replaceChild(newNode, element);

        return newNode;
    }

    /**
     * Factory method to create a new instance of editor with given configuration
     *
     * @param {HTMLElement} element
     * @param {Object} [config = {}]
     *
     * @return {Editor}
     */
    static create(element, config = {}) {
        const editor = new Editor(element, config);
        editor.init();

        return editor;
    }
}
<commit_msg>Add missing semicolon<commit_after>import Command from './command/Command.js';
import Config from './config/Config.js';
import Converter from './converter/Converter.js';
import Filter from './filter/Filter.js';
import Observer from './observer/Observer.js';
import Tag from './config/Tag.js';
import TextCommand from './command/TextCommand.js';
import configCommand from '../cfg/command.js';
import configConverter from '../cfg/converter.js';
import configFilter from '../cfg/filter.js';
import configObserver from '../cfg/observer.js';
import configTag from '../cfg/tag.js';

/**
 * Editor
 */
export default class Editor {
    /**
     * Creates a new instance of editor with given configuration
     *
     * @param {HTMLElement} orig
     * @param {Object} [cfg = {}]
     */
    constructor(orig, cfg = {}) {
        if (!(orig instanceof HTMLElement)) {
            throw 'Invalid element';
        }

        const element = orig.ownerDocument.createElement('div');
        const content = orig.ownerDocument.createElement('div');
        const toolbarMain = orig.ownerDocument.createElement('div');
        const toolbarEditable = orig.ownerDocument.createElement('div');

        element.classList.add('editor');
        content.classList.add('editor-content');
        toolbarMain.classList.add('editor-toolbar', 'editor-toolbar-main');
        toolbarEditable.classList.add('editor-toolbar', 'editor-toolbar-editable');

        orig.hidden = true;
        orig.insertAdjacentElement('afterend', element);
        element.appendChild(toolbarMain);
        element.appendChild(content);
        element.appendChild(toolbarEditable);

        /**
         * Corresponding DOM element of the source
         *
         * @type {HTMLElement}
         * @readonly
         */
        this.orig = orig;

        /**
         * Correspondig DOM Document
         *
         * @type {Document}
         * @readonly
         */
        this.document = orig.ownerDocument;

        /**
         * Corresponding Window object
         *
         * @type {Window}
         * @readonly
         */
        this.window = this.document.defaultView;

        /**
         * Configuration
         *
         * @type {Config}
         * @readonly
         */
        this.config = new Config(cfg);

        /**
         * Corresponding DOM element of the editor
         *
         * @type {HTMLDivElement}
         * @readonly
         */
        this.element = element;

        /**
         * Corresponding DOM element of the editor content
         *
         * @type {HTMLDivElement}
         * @readonly
         */
        this.content = content;

        /**
         * Corresponding DOM element of the main toolbar
         *
         * @type {HTMLDivElement}
         * @readonly
         */
        this.toolbarMain = toolbarMain;

        /**
         * Corresponding DOM element of the editable toolbar
         *
         * @type {HTMLDivElement}
         * @readonly
         */
        this.toolbarEditable = toolbarEditable;

        /**
         * Tags
         *
         * @type {Map<String, Tag>}
         * @readonly
         */
        this.tags = this.createTags(configTag);

        /**
         * Element converters
         *
         * @type {Map<String, Converter>}
         * @readonly
         */
        this.converters = this.createConverters(configConverter);

        /**
         * Commands
         *
         * @type {Map<String, Command>}
         */
        this.commands = this.createCommands(configCommand);

        /**
         * Observers
         *
         * @type {Observer[]}
         */
        this.observers = this.create(configObserver, Observer);

        /**
         * Filters
         *
         * @type {Filter[]}
         */
        this.filters = this.create(configFilter, Filter);
    }

    /**
     * Instantiates each element in given array with this editor instance
     *
     * @private
     *
     * @param {Function[]} config
     * @param {Function} constructor
     *
     * @return {Array}
     */
    create(config, constructor) {
        return config.map(item => {
            if (typeof item !== 'function' || !(item = new item(this)) || !(item instanceof constructor)) {
                throw 'Invalid item';
            }

            return item;
        });
    }

    /**
     * Creates tags map
     *
     * @private
     *
     * @param {Object[]} config
     *
     * @return {Map<String, Tag>}
     */
    createTags(config) {
        const map = new Map();

        config.forEach(item => {
            const tag = new Tag(item);
            return map.set(tag.name, tag);
        });

        return map;
    }

    /**
     * Creates converters map
     *
     * @private
     *
     * @param {Object.<String, Converter>} config
     *
     * @return {Map<String, Converter>}
     */
    createConverters(config) {
        const map = new Map();

        Object.getOwnPropertyNames(config).forEach(key => {
            if (!(config[key] instanceof Converter)) {
                throw 'Invalid converter';
            }

            map.set(key, config[key]);
        });

        return map;
    }

    /**
     * Creates commands map
     *
     * @private
     *
     * @param {Object.<String, Function>} config
     *
     * @return {Map<String, Command>}
     */
    createCommands(config) {
        const map = new Map();

        Object.getOwnPropertyNames(config).forEach(key => {
            let command;

            if (typeof config[key] !== 'function' || !(command = config[key](this)) || !(command instanceof Command)) {
                throw 'Invalid command';
            }

            return map.set(key, command);
        });

        return map;
    }

    /**
     * Init editor
     */
    init() {
        this.observers.forEach(item => this.register(ev => item.observe(ev)));
        this.initContent();
        this.initToolbar();
    }

    /**
     * Init content
     *
     * @private
     */
    initContent() {
        if (this.orig instanceof HTMLTextAreaElement) {
            this.content.innerHTML = this.orig.value.replace('/ /g', ' ');
            this.orig.form.addEventListener('submit', () => this.save());
        } else {
            this.content.innerHTML = this.orig.innerHTML;
        }

        this.filter(this.content);
    }

    /**
     * Init Toolbar
     *
     * @private
     */
    initToolbar() {
        this.content.addEventListener('selectstart', () => {
            const active = this.document.activeElement;

            if (active.isContentEditable && this.allowedGroup('text', active.tagName)) {
                this.toolbarEditable.classList.add('editor-toolbar-active');
                this.toolbarEditable.style.top = (active.offsetTop + active.offsetParent.offsetTop - this.toolbarEditable.clientHeight) + 'px';
            }
        });
        this.document.addEventListener('selectionchange', () => {
            const active = this.document.activeElement;

            if (this.window.getSelection().isCollapsed || !active.isContentEditable || !this.content.contains(active)) {
                this.toolbarEditable.classList.remove('editor-toolbar-active');
                this.toolbarEditable.removeAttribute('style');
            }
        });

        for (let cmd of this.commands.entries()) {
            const item = this.document.createElement('button');
            item.setAttribute('type', 'button');
            item.setAttribute('data-cmd', cmd[0]);
            item.setAttribute('title', cmd[0]);
            item.innerText = cmd[0];
            item.addEventListener('click', () => {
                if (!this.window.getSelection().containsNode(this.content, true)) {
                    this.content.focus();
                }

                cmd[1].execute();
            });

            if (cmd[1] instanceof TextCommand) {
                this.toolbarEditable.appendChild(item);
            } else {
                this.toolbarMain.appendChild(item);
            }
        }
    }

    /**
     * Short-cut method to register a mutation observer
     *
     * @param {Function} call
     * @param {Object} [config = {childList: true, subtree: true}]
     */
    register(call, config = {childList: true, subtree: true}) {
        if (typeof call !== 'function') {
            throw 'Invalid observer';
        }

        const mutation = new MutationObserver(call);
        mutation.observe(this.content, config);
    }

    /**
     * Execute command
     *
     * @param {String} name
     */
    execute(name) {
        const command = this.commands.get(name);

        if (!command) {
            throw 'Invalid command';
        }

        command.execute();
    }

    /**
     * Returns editor element's innerHTML
     *
     * @return {String}
     */
    getData() {
        const content = this.content.cloneNode(true);
        this.filter(content, true);

        return content.innerHTML;
    }

    /**
     * Saves editor data to source element
     */
    save() {
        if (this.orig instanceof HTMLTextAreaElement) {
            this.orig.value = this.getData();
        } else {
            this.orig.innerHTML = this.getData();
        }
    }

    /**
     * Inserts an element at the editor element or optionally after given reference element
     *
     * @param {HTMLElement} element
     * @param {?HTMLElement} [ref = null]
     */
    insert(element, ref = null) {
        if (!(element instanceof HTMLElement) || ref && !(ref instanceof HTMLElement)) {
            throw 'Invalid HTML element';
        }

        const parent = ref ? ref.parentElement : this.content;
        const parentName = this.getTagName(parent);

        if (!this.content.contains(parent) || !this.allowed(element.tagName, parentName)) {
            throw 'Element is not allowed here';
        }

        parent.insertBefore(element, ref ? ref.nextElementSibling : null);
    }

    /**
     * Insert text
     *
     * @param {String} text
     */
    insertText(text) {
        this.document.execCommand('inserttext', false, text);
    }

    /**
     * Adds or removes formatting to/from selected text
     *
     * @param {HTMLElement} element
     */
    formatText(element) {
        if (!(element instanceof HTMLElement)) {
            throw 'Invalid HTML element';
        }

        const sel = this.window.getSelection();
        const anc = sel.anchorNode instanceof Text ? sel.anchorNode.parentElement : sel.anchorNode;
        const ancEdit = this.getSelectedEditable();

        if (sel.isCollapsed || !sel.toString().trim() || !(anc instanceof HTMLElement) || !ancEdit) {
            return;
        }

        const range = sel.getRangeAt(0);
        const tag = this.getTag(anc.tagName);
        const parent = !tag || tag.group === 'text' ? anc.parentElement : anc;

        if (range.startContainer instanceof Text && !range.startContainer.parentElement.isSameNode(parent)) {
            range.setStartBefore(range.startContainer.parentElement);
        }

        if (range.endContainer instanceof Text && !range.endContainer.parentElement.isSameNode(parent)) {
            range.setEndAfter(range.endContainer.parentElement);
        }

        const selText = range.toString();
        const selNodes = range.cloneContents().childNodes;
        let same = Array.from(selNodes).every(item => {
            return item instanceof Text && !item.nodeValue.trim() || item instanceof HTMLElement && item.tagName === element.tagName;
        });

        range.deleteContents();

        if (parent.isContentEditable && this.allowed(element.tagName, parent.tagName) && selText.trim() && (!tag || !same)) {
            element.innerText = selText;
            range.insertNode(element);
        } else {
            range.insertNode(this.document.createTextNode(selText));
        }

        parent.normalize();
    }

    /**
     * Returns current selected element or null
     *
     * @return {?HTMLElement}
     */
    getSelectedElement() {
        const sel = this.window.getSelection();
        const anc = sel.anchorNode instanceof Text ? sel.anchorNode.parentElement : sel.anchorNode;
        const foc = sel.focusNode instanceof Text ? sel.focusNode.parentElement : sel.focusNode;

        return anc instanceof HTMLElement && foc instanceof HTMLElement && anc.isSameNode(foc) && this.content.contains(anc) ? anc : null;
    }

    /**
     * Returns current selected contenteditable or null
     *
     * @return {?HTMLElement}
     */
    getSelectedEditable() {
        const sel = this.window.getSelection();
        const anc = sel.anchorNode instanceof Text ? sel.anchorNode.parentElement : sel.anchorNode;
        const foc = sel.focusNode instanceof Text ? sel.focusNode.parentElement : sel.focusNode;

        if (anc instanceof HTMLElement && foc instanceof HTMLElement) {
            const ancEdit = anc.closest('[contenteditable=true]');
            const focEdit = foc.closest('[contenteditable=true]');

            if (ancEdit instanceof HTMLElement && focEdit instanceof HTMLElement && ancEdit.isSameNode(focEdit) && this.content.contains(ancEdit)) {
                return ancEdit;
            }
        }

        return null;
    }

    /**
     * Returns current selected widget or null
     *
     * @return {?HTMLElement}
     */
    getSelectedWidget() {
        const el = this.getSelectedElement();

        return el ? el.closest('div.editor-content > *') : null;
    }

    /**
     * Focus end of contents
     *
     * @param {HTMLElement} element
     */
    focusEnd(element) {
        if (!(element instanceof HTMLElement)) {
            throw 'No HTML element';
        }

        const range = this.document.createRange();
        const sel = this.window.getSelection();
        element.focus();
        range.selectNodeContents(element);
        range.collapse();
        sel.removeAllRanges();
        sel.addRange(range);
    }

    /**
     * Filters element
     *
     * @param {HTMLElement} parent
     * @param {Boolean} [forceRoot = false]
     */
    filter(parent, forceRoot = false) {
        this.filters.forEach(item => item.filter(parent, forceRoot));
    }

    /**
     * Returns tag name from element considering exception for root element
     *
     * @param {HTMLElement} element
     *
     * @return {String}
     */
    getTagName(element) {
        if (!(element instanceof HTMLElement)) {
            throw 'No HTML element';
        }

        return this.content.isSameNode(element) ? 'root' : element.tagName;
    }

    /**
     * Returns tag configuration vor given tag name
     *
     * @param {String} name
     *
     * @return {?Tag}
     */
    getTag(name) {
        return this.tags.get(name.toLowerCase()) || null;
    }

    /**
     * Checks if given element is allowed inside given parent element
     *
     * @param {String} name
     * @param {String} parentName
     *
     * @return {Boolean}
     */
    allowed(name, parentName) {
        const tag = this.getTag(name);
        const parentTag = this.getTag(parentName);

        return tag && parentTag && parentTag.children.includes(tag.group);
    }

    /**
     * Checks if given group is allowed inside given parent element
     *
     * @param {String} group
     * @param {String} parentName
     *
     * @return {Boolean}
     */
    allowedGroup(group, parentName) {
        const parentTag = this.getTag(parentName);

        return parentTag && parentTag.children.includes(group);
    }

    /**
     * Converts element
     *
     * @param {HTMLElement} element
     *
     * @return {Node}
     */
    convert(element) {
        if (!(element instanceof HTMLElement)) {
            throw 'No HTML element';
        }

        const converter = this.converters.get(element.tagName.toLowerCase());

        if (!converter) {
            return element;
        }

        const newNode = converter.convert(element);
        element.parentElement.replaceChild(newNode, element);

        return newNode;
    }

    /**
     * Factory method to create a new instance of editor with given configuration
     *
     * @param {HTMLElement} element
     * @param {Object} [config = {}]
     *
     * @return {Editor}
     */
    static create(element, config = {}) {
        const editor = new Editor(element, config);
        editor.init();

        return editor;
    }
}
<|endoftext|>


<commit_before>define([
	'exports',
	'../errors/RequestError',
	'../errors/CancelError',
	'../Deferred',
	'../io-query',
	'../lang'
], function (exports, RequestError, CancelError, Deferred, ioQuery, lang) {
	'use strict';

	exports.deepCopy = function deepCopy(target, source) {
		for (var name in source) {
			var tval = target[name],
				sval = source[name];
			if (tval !== sval) {
				if (tval && typeof tval === 'object' && sval && typeof sval === 'object') {
					exports.deepCopy(tval, sval);
				}
				else {
					target[name] = sval;
				}
			}
		}
		return target;
	};

	exports.deepCreate = function deepCreate(source, properties) {
		properties = properties || {};
		var target = lang.delegate(source),
			name, value;

		for (name in source) {
			value = source[name];

			if (value && typeof value === 'object') {
				target[name] = exports.deepCreate(value, properties[name]);
			}
		}
		return exports.deepCopy(target, properties);
	};

	var freeze = Object.freeze || function (obj) { return obj; };
	function okHandler(response) {
		return freeze(response);
	}
	function dataHandler(response) {
		return response.data || response.text;
	}

	exports.deferred = function deferred(response, cancel, isValid, isReady, handleResponse, last) {
		var def = new Deferred(function (reason) {
			cancel && cancel(def, response);

			if (!reason || !(reason instanceof RequestError) && !(reason instanceof CancelError)) {
				return new CancelError('Request canceled', response);
			}
			return reason;
		});

		def.response = response;
		def.isValid = isValid;
		def.isReady = isReady;
		def.handleResponse = handleResponse;

		function errHandler(error) {
			error.response = response;
			throw error;
		}
		var responsePromise = def.then(okHandler).otherwise(errHandler);

		if (exports.notify) {
			responsePromise.then(
				lang.hitch(exports.notify, 'emit', 'load'),
				lang.hitch(exports.notify, 'emit', 'error')
			);
		}

		var dataPromise = responsePromise.then(dataHandler);

		// http://bugs.dojotoolkit.org/ticket/16794
		// The following works around a leak in IE9 through the
		// prototype using lang.delegate on dataPromise and
		// assigning the result a property with a reference to
		// responsePromise.
		var promise = new Promise();
		for (var prop in dataPromise) {
			if (dataPromise.hasOwnProperty(prop)) {
				promise[prop] = dataPromise[prop];
			}
		}
		promise.response = responsePromise;
		freeze(promise);
		// End leak fix

		if (last) {
			def.then(function (response) {
				last.call(def, response);
			}, function (error) {
				last.call(def, response, error);
			});
		}

		def.promise = promise;
		def.then = promise.then;

		return def;
	};

	exports.addCommonMethods = function addCommonMethods(provider, methods) {
		(methods || ['GET', 'POST', 'PUT', 'DELETE']).forEach(function (method) {
			provider[(method === 'DELETE' ? 'DEL' : method).toLowerCase()] = function (url, options) {
				options = lang.delegate(options || {});
				options.method = method;
				return provider(url, options);
			};
		});
	};

	exports.parseArgs = function parseArgs(url, options, skipData) {
		var data = options.data,
			query = options.query;

		if (data && !skipData) {
			if (typeof data === 'object') {
				options.data = ioQuery.objectToQuery(data);
			}
		}

		if (query) {
			if (typeof query === 'object') {
				query = ioQuery.objectToQuery(query);
			}
			if (options.preventCache) {
				query += (query ? '&' : '') + 'request.preventCache=' + (+(new Date()));
			}
		}
		else if (options.preventCache) {
			query = 'request.preventCache=' + (+(new Date()));
		}

		if (url && query) {
			url += (~url.indexOf('?') ? '&' : '?') + query;
		}

		return {
			url: url,
			options: options,
			getHeader: function (/*headerName*/) { return null; }
		};
	};

	exports.checkStatus = function (stat) {
		stat = stat || 0;
		return (stat >= 200 && stat < 300) || // allow any 2XX response code
			stat === 304 ||                 // or, get it out of the cache
			stat === 1223 ||                // or, Internet Explorer mangled the status code
			!stat;                         // or, we're Titanium/browser chrome/chrome extension requesting a local file
	};
});
<commit_msg>Make request work with DOM Promises<commit_after>define([
	'exports',
	'../errors/RequestError',
	'../errors/CancelError',
	'../Deferred',
	'../io-query',
	'../lang'
], function (exports, RequestError, CancelError, Deferred, ioQuery, lang) {
	'use strict';

	exports.deepCopy = function deepCopy(target, source) {
		for (var name in source) {
			var tval = target[name],
				sval = source[name];
			if (tval !== sval) {
				if (tval && typeof tval === 'object' && sval && typeof sval === 'object') {
					exports.deepCopy(tval, sval);
				}
				else {
					target[name] = sval;
				}
			}
		}
		return target;
	};

	exports.deepCreate = function deepCreate(source, properties) {
		properties = properties || {};
		var target = lang.delegate(source),
			name, value;

		for (name in source) {
			value = source[name];

			if (value && typeof value === 'object') {
				target[name] = exports.deepCreate(value, properties[name]);
			}
		}
		return exports.deepCopy(target, properties);
	};

	var freeze = Object.freeze || function (obj) { return obj; };
	function okHandler(response) {
		return freeze(response);
	}
	function dataHandler(response) {
		return response.data || response.text;
	}

	exports.deferred = function deferred(response, cancel, isValid, isReady, handleResponse, last) {
		var def = new Deferred(function (reason) {
			cancel && cancel(def, response);

			if (!reason || !(reason instanceof RequestError) && !(reason instanceof CancelError)) {
				return new CancelError('Request canceled', response);
			}
			return reason;
		});

		def.response = response;
		def.isValid = isValid;
		def.isReady = isReady;
		def.handleResponse = handleResponse;

		function errHandler(error) {
			error.response = response;
			throw error;
		}
		var responsePromise = def.then(okHandler).then(null, errHandler);

		if (exports.notify) {
			responsePromise.then(
				lang.hitch(exports.notify, 'emit', 'load'),
				lang.hitch(exports.notify, 'emit', 'error')
			);
		}

		var dataPromise = responsePromise.then(dataHandler);

		// http://bugs.dojotoolkit.org/ticket/16794
		// The following works around a leak in IE9 through the
		// prototype using lang.delegate on dataPromise and
		// assigning the result a property with a reference to
		// responsePromise.
		var promise = new Promise();
		for (var prop in dataPromise) {
			if (dataPromise.hasOwnProperty(prop)) {
				promise[prop] = dataPromise[prop];
			}
		}
		promise.response = responsePromise;
		freeze(promise);
		// End leak fix

		if (last) {
			def.then(function (response) {
				last.call(def, response);
			}, function (error) {
				last.call(def, response, error);
			});
		}

		def.promise = promise;
		def.then = promise.then;

		return def;
	};

	exports.addCommonMethods = function addCommonMethods(provider, methods) {
		(methods || ['GET', 'POST', 'PUT', 'DELETE']).forEach(function (method) {
			provider[(method === 'DELETE' ? 'DEL' : method).toLowerCase()] = function (url, options) {
				options = lang.delegate(options || {});
				options.method = method;
				return provider(url, options);
			};
		});
	};

	exports.parseArgs = function parseArgs(url, options, skipData) {
		var data = options.data,
			query = options.query;

		if (data && !skipData) {
			if (typeof data === 'object') {
				options.data = ioQuery.objectToQuery(data);
			}
		}

		if (query) {
			if (typeof query === 'object') {
				query = ioQuery.objectToQuery(query);
			}
			if (options.preventCache) {
				query += (query ? '&' : '') + 'request.preventCache=' + (+(new Date()));
			}
		}
		else if (options.preventCache) {
			query = 'request.preventCache=' + (+(new Date()));
		}

		if (url && query) {
			url += (~url.indexOf('?') ? '&' : '?') + query;
		}

		return {
			url: url,
			options: options,
			getHeader: function (/*headerName*/) { return null; }
		};
	};

	exports.checkStatus = function (stat) {
		stat = stat || 0;
		return (stat >= 200 && stat < 300) || // allow any 2XX response code
			stat === 304 ||                 // or, get it out of the cache
			stat === 1223 ||                // or, Internet Explorer mangled the status code
			!stat;                         // or, we're Titanium/browser chrome/chrome extension requesting a local file
	};
});
<|endoftext|>


<commit_before>var pull    = require('pull-stream')
var pt      = require('pull-traverse')
var semver  = require('semver')
var cat     = require('pull-cat')
var clean    = require('./clean')
var paramap  = require('pull-paramap')

module.exports = createResolve

function check(pkg, name, range) {
  if(!pkg) return false
  if(pkg.tree[name] && semver.satisfies(pkg.tree[name].version, range, true))
    return true
  return check(pkg.parent, name, range)
}

function fixModule (module) {
  if('string' === typeof module) {
  var parts = module.split('@')
    return {name: parts.shift(), version: parts.shift() || '*'}
  }
  return module
}

function niceError(err, parent, name, range) {
  if(!err) return
  err.message = 'package:'
  + parent.name + '@' + parent.version
  + ' could not resolve '
  + name + '@' + range
  + '\n' + err.message

  return err
}

function merge (a, b) {
  var c = {}
  for(var k in a)
    c[k] = a[k]
  for(var k in b)
    c[k] = b[k]
  return c
}

function isPackage(pkg) {
  return pkg.name && pkg.version && (pkg.dependencies || pkg.devDependencies)
}

var unresolved = {}

function createResolve (resolvePackage) {

  function resolveTree (module, opts, cb) {

    module = fixModule(module)

    var filter = opts.filter || function (pkg, root) {
      if(!pkg) return
      pkg.parent.tree[pkg.name] = pkg
    }

    if (! module) return cb(null, {})

    //this is the root package - if we are resolving from a package.json
    //we should just use that, instead of calling resolve pkg.
    var root

    if(isPackage(module)) tree(module)
    else
      resolvePackage(module.name, module.version, opts, function (err, pkg) {
        if(err) return cb(err)
        tree(pkg)
      })

    function tree (root) {
      if(opts.available) {
        root.parent = {tree: opts.available}
      }
      pull(cat([
        pull.values([root]),
        pt.depthFirst(root, function (pkg) {
          var deps = (
            opts.dev && pkg === root
          ? merge(pkg.devDependencies, pkg.dependencies)
          : pkg.dependencies
          ) || {}

          pkg.tree = {}
          return pull(
            pull.values(Object.keys(deps)),
            //this could be parallel,
            //but it's not the bottle neck.
            paramap(function (name, cb) {
              //check if there is already a module that resolves this...

              //filter out versions that we already have.
              //if(opts.check !== false && check(pkg, name, deps[name]))
              //  return cb()
              unresolved[name + '@' + deps[name]] = true
              resolvePackage(name, deps[name], opts, function (err, _pkg) {
                delete unresolved[name + '@' + deps[name]]

                cb(niceError(err, pkg, name, deps[name]), _pkg)
              })
            }),
            pull.filter(),
            pull.through(function (_pkg) {
              _pkg.parent = pkg
              filter(_pkg, root)
            })
          )
        })
      ]),
      pull.drain(null, function (err) {
        cb(err, clean(root))
      }))
    }
  }

  //install non-conflicting modules as low in the tree as possible.
  function resolveTreeGreedy (module, opts, cb) {
    if(!cb) cb = opts, opts = null

    opts = opts || {}
    opts.filter = function (pkg, root) {
      if(!pkg) return
      if(!root.tree[pkg.name]) {
        root.tree[pkg.name] = pkg
        pkg.parent = root
      }
      else {
        pkg.parent.tree[pkg.name] = pkg
      }
      return pkg
    }

    resolveTree(module, opts, cb)
  }

  return function (module, opts, cb) {
    if(!cb)
      cb = opts, opts = {}

    function resolve(module, cb) {
      if(opts && opts.greedy)
        resolveTreeGreedy(module, opts, cb)
      else
        resolveTree(module, opts, cb)
    }

    if(Array.isArray(module) && module.length > 1) {

      var n = module.length, a = {}

      module.forEach(function (m) {
        resolve(m, next)
      })

      function next (err, tree) {
        if(err) return n = 0, cb(err)
        a[tree.name] = tree
        if(--n) return
        cb(null, a)
      }

    } else {
      if(Array.isArray(module))
        module = module.shift()

      resolve(module, cb)
    }
  }
}

//This Should Never Happen.
process.on('exit', function () {
  var keys = Object.keys(unresolved)
  if(keys.length)
  throw new Error('resolve did not callback: ' + keys.join(', '))
})

<commit_msg>ensure that from field is valid for dev deps<commit_after>var pull    = require('pull-stream')
var pt      = require('pull-traverse')
var semver  = require('semver')
var cat     = require('pull-cat')
var clean    = require('./clean')
var paramap  = require('pull-paramap')

module.exports = createResolve

function check(pkg, name, range) {
  if(!pkg) return false
  if(pkg.tree[name] && semver.satisfies(pkg.tree[name].version, range, true))
    return true
  return check(pkg.parent, name, range)
}

function fixModule (module) {
  if('string' === typeof module) {
  var parts = module.split('@')
    return {name: parts.shift(), version: parts.shift() || '*'}
  }
  return module
}

function niceError(err, parent, name, range) {
  if(!err) return
  err.message = 'package:'
  + parent.name + '@' + parent.version
  + ' could not resolve '
  + name + '@' + range
  + '\n' + err.message

  return err
}

function merge (a, b) {
  var c = {}
  a = a || {}
  b = b || {}
  for(var k in a)
    c[k] = a[k]
  for(var k in b)
    c[k] = b[k]
  return c
}

function isPackage(pkg) {
  return pkg.name && pkg.version && (pkg.dependencies || pkg.devDependencies)
}

var unresolved = {}

function createResolve (resolvePackage) {

  function resolveTree (module, opts, cb) {

    module = fixModule(module)

    var filter = opts.filter || function (pkg, root) {
      if(!pkg) return
      pkg.parent.tree[pkg.name] = pkg
    }

    if(!module) return cb(null, {})

    //this is the root package - if we are resolving from a package.json
    //we should just use that, instead of calling resolve pkg.
    var root

    if(isPackage(module)) tree(module)
    else
      resolvePackage(module.name, module.version, opts, function (err, pkg) {
        if(err) return cb(err)
        tree(pkg)
      })

    function tree (root) {
      if(opts.available) {
        root.parent = {tree: opts.available}
      }
      pull(cat([
        pull.values([root]),
        pt.depthFirst(root, function (pkg) {
          var deps = pkg.dependencies || {}

          //merge deps and dev deps if this is the root module and we are in --dev mode
          if(opts.dev && pkg === root)
            deps = pkg.dependencies = merge(pkg.devDependencies, deps)

          pkg.tree = {}
          return pull(
            pull.values(Object.keys(deps)),
            //this could be parallel,
            //but it's not the bottle neck.
            paramap(function (name, cb) {
              //check if there is already a module that resolves this...

              //filter out versions that we already have.
              //if(opts.check !== false && check(pkg, name, deps[name]))
              //  return cb()
              unresolved[name + '@' + deps[name]] = true
              resolvePackage(name, deps[name], opts, function (err, _pkg) {
                delete unresolved[name + '@' + deps[name]]

                cb(niceError(err, pkg, name, deps[name]), _pkg)
              })
            }),
            pull.filter(),
            pull.through(function (_pkg) {
              _pkg.parent = pkg
              filter(_pkg, root)
            })
          )
        })
      ]),
      pull.drain(null, function (err) {
        cb(err, clean(root))
      }))
    }
  }

  //install non-conflicting modules as low in the tree as possible.
  function resolveTreeGreedy (module, opts, cb) {
    if(!cb) cb = opts, opts = null

    opts = opts || {}
    opts.filter = function (pkg, root) {
      if(!pkg) return
      if(!root.tree[pkg.name]) {
        root.tree[pkg.name] = pkg
        pkg.parent = root
      }
      else {
        pkg.parent.tree[pkg.name] = pkg
      }
      return pkg
    }

    resolveTree(module, opts, cb)
  }

  return function (module, opts, cb) {
    if(!cb)
      cb = opts, opts = {}

    function resolve(module, cb) {
      if(opts && opts.greedy)
        resolveTreeGreedy(module, opts, cb)
      else
        resolveTree(module, opts, cb)
    }

    if(Array.isArray(module) && module.length > 1) {

      var n = module.length, a = {}

      module.forEach(function (m) {
        resolve(m, next)
      })

      function next (err, tree) {
        if(err) return n = 0, cb(err)
        a[tree.name] = tree
        if(--n) return
        cb(null, a)
      }

    } else {
      if(Array.isArray(module))
        module = module.shift()

      resolve(module, cb)
    }
  }
}

//This Should Never Happen.
process.on('exit', function () {
  var keys = Object.keys(unresolved)
  if(keys.length)
  throw new Error('resolve did not callback: ' + keys.join(', '))
})

<|endoftext|>