<commit_before>/**
 * `core/user` data store: permission scopes.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { CORE_USER, PERMISSION_READ_SHARED_MODULE_DATA } from './constants';
import { CORE_MODULES } from '../../modules/datastore/constants';
import { getMetaCapabilityPropertyName } from '../util/permissions';
import { createFetchStore } from '../../data/create-fetch-store';
const { createRegistrySelector } = Data;

// Actions
const CLEAR_PERMISSION_SCOPE_ERROR = 'CLEAR_PERMISSION_SCOPE_ERROR';
const SET_PERMISSION_SCOPE_ERROR = 'SET_PERMISSION_SCOPE_ERROR';
const RECEIVE_CAPABILITIES = 'RECEIVE_CAPABILITIES';

const fetchRefreshCapabilitiesStore = createFetchStore( {
	baseName: 'refreshCapabilities',
	controlCallback: () => {
		return API.get( 'core', 'user', 'permissions', undefined, {
			useCache: false,
		} );
	},
} );

const baseInitialState = {
	permissionError: null,
	capabilities: undefined,
};

const baseActions = {
	/**
	 * Clears the permission scope error, if one was previously set.
	 *
	 * @since 1.9.0
	 * @private
	 *
	 * @return {Object} Redux-style action.
	 */
	clearPermissionScopeError() {
		return {
			payload: {},
			type: CLEAR_PERMISSION_SCOPE_ERROR,
		};
	},

	/**
	 * Sets a permission error encountered by an API request.
	 *
	 * @since 1.9.0
	 * @private
	 *
	 * @param {Object} permissionError Permissions error object.
	 * @return {Object} Redux-style action.
	 */
	setPermissionScopeError( permissionError ) {
		invariant( permissionError, 'permissionError is required.' );

		return {
			payload: { permissionError },
			type: SET_PERMISSION_SCOPE_ERROR,
		};
	},

	/**
	 * Sets user capabilities.
	 *
	 * @since 1.13.0
	 * @private
	 *
	 * @param {Object} capabilities User capabilities.
	 * @return {Object} Redux-style action.
	 */
	receiveCapabilities( capabilities ) {
		return {
			type: RECEIVE_CAPABILITIES,
			payload: { capabilities },
		};
	},

	/**
	 * Refreshs user capabilities.
	 *
	 * @since n.e.x.t
	 *
	 * @return {Object} Redux-style action.
	 */
	*refreshCapabilities() {
		const { dispatch } = yield Data.commonActions.getRegistry();

		const {
			response,
		} = yield fetchRefreshCapabilitiesStore.actions.fetchRefreshCapabilities();

		global._googlesitekitUserData = {
			permissions: response,
		};

		return dispatch( CORE_USER ).receiveCapabilities( response );
	},
};

const baseControls = {};

const baseReducer = ( state, { type, payload } ) => {
	switch ( type ) {
		case CLEAR_PERMISSION_SCOPE_ERROR: {
			return {
				...state,
				permissionError: null,
			};
		}

		case SET_PERMISSION_SCOPE_ERROR: {
			const { permissionError } = payload;

			return {
				...state,
				permissionError,
			};
		}

		case RECEIVE_CAPABILITIES: {
			const { capabilities } = payload;

			return {
				...state,
				capabilities,
			};
		}

		default: {
			return state;
		}
	}
};

const baseResolvers = {
	*getCapabilities() {
		const registry = yield Data.commonActions.getRegistry();

		if ( registry.select( CORE_USER ).getCapabilities() ) {
			return;
		}

		if ( ! global._googlesitekitUserData?.permissions ) {
			global.console.error( 'Could not load core/user permissions.' );
		}

		yield actions.receiveCapabilities(
			global._googlesitekitUserData?.permissions
		);
	},
};

const baseSelectors = {
	/**
	 * Gets the most recent permission error encountered by this user.
	 *
	 * @since 1.9.0
	 * @private
	 *
	 * @param {Object} state Data store's state.
	 * @return {(Object|null)} Permission scope errors. Returns `null` if no error exists.
	 */
	getPermissionScopeError( state ) {
		const { permissionError } = state;
		return permissionError;
	},

	/**
	 * Gets capabilities of the current user.
	 *
	 * @since 1.13.0
	 *
	 * @param {Object} state Data store's state.
	 * @return {(Object|undefined)} Capabilities object. Returns undefined if it is not loaded yet.
	 */
	getCapabilities( state ) {
		const { capabilities } = state;
		return capabilities;
	},

	/**
	 * Gets viewable module slugs of the current user.
	 *
	 * @since 1.72.0
	 *
	 * @return {(Array|undefined)} An array of viewable module slugs. `undefined` if `modules` are not loaded yet.
	 */
	getViewableModules: createRegistrySelector( ( select ) => () => {
		const modules = select( CORE_MODULES ).getModules();

		if ( modules === undefined ) {
			return undefined;
		}

		// Return an array of module slugs for modules that are
		// shareable and the user has the "read shared module data"
		// capability for.
		return Object.values( modules ).reduce( ( moduleSlugs, module ) => {
			const hasCapability = select( CORE_USER ).hasCapability(
				PERMISSION_READ_SHARED_MODULE_DATA,
				module.slug
			);

			if ( module.shareable && hasCapability ) {
				return [ ...moduleSlugs, module.slug ];
			}

			return moduleSlugs;
		}, [] );
	} ),

	/**
	 * Checks if the current user has the specified capability or not.
	 *
	 * @since 1.13.0
	 *
	 * @param {Object} state      Data store's state.
	 * @param {string} capability Capability name to check.
	 * @param {Array}  args       List of rest of the arguments. Specifically one or many module slugs.
	 * @return {(boolean|undefined)} TRUE if the current user has this capability, otherwise FALSE. If capabilities ain't loaded yet, returns undefined.
	 */
	hasCapability: createRegistrySelector(
		( select ) => ( state, capability, ...args ) => {
			const capabilities = select( CORE_USER ).getCapabilities();

			if ( args.length > 0 ) {
				capability = getMetaCapabilityPropertyName(
					capability,
					...args
				);
			}

			if ( capabilities ) {
				return !! capabilities[ capability ];
			}

			return undefined;
		}
	),

	/**
	 * Checks if the specified module is shareable and viewable by the current user.
	 *
	 * @since 1.77.0
	 *
	 * @param {Object} state      Data store's state.
	 * @param {string} moduleSlug Module slug to check.
	 * @return {(boolean|undefined)} `true` if the module is shareable and viewable by the current user. `false` if the module does not exist, is not shareable or not viewable by the current user. `undefined` if state is not loaded yet.
	 */
	canViewSharedModule: createRegistrySelector(
		( select ) => ( state, moduleSlug ) => {
			const module = select( CORE_MODULES ).getModule( moduleSlug );

			if ( module === undefined ) {
				return undefined;
			}

			if ( module === null || ! module.shareable ) {
				return false;
			}

			return select( CORE_USER ).hasCapability(
				PERMISSION_READ_SHARED_MODULE_DATA,
				module.slug
			);
		}
	),
};

const store = Data.combineStores( fetchRefreshCapabilitiesStore, {
	initialState: baseInitialState,
	actions: baseActions,
	controls: baseControls,
	reducer: baseReducer,
	resolvers: baseResolvers,
	selectors: baseSelectors,
} );

export const initialState = store.initialState;
export const actions = store.actions;
export const controls = store.controls;
export const reducer = store.reducer;
export const resolvers = store.resolvers;
export const selectors = store.selectors;

export default store;
<commit_msg>Add error handling to refreshCapabilities action.<commit_after>/**
 * `core/user` data store: permission scopes.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { CORE_USER, PERMISSION_READ_SHARED_MODULE_DATA } from './constants';
import { CORE_MODULES } from '../../modules/datastore/constants';
import { getMetaCapabilityPropertyName } from '../util/permissions';
import { createFetchStore } from '../../data/create-fetch-store';
const { createRegistrySelector } = Data;

// Actions
const CLEAR_PERMISSION_SCOPE_ERROR = 'CLEAR_PERMISSION_SCOPE_ERROR';
const SET_PERMISSION_SCOPE_ERROR = 'SET_PERMISSION_SCOPE_ERROR';
const RECEIVE_CAPABILITIES = 'RECEIVE_CAPABILITIES';

const fetchRefreshCapabilitiesStore = createFetchStore( {
	baseName: 'refreshCapabilities',
	controlCallback: () => {
		return API.get( 'core', 'user', 'permissions', undefined, {
			useCache: false,
		} );
	},
} );

const baseInitialState = {
	permissionError: null,
	capabilities: undefined,
};

const baseActions = {
	/**
	 * Clears the permission scope error, if one was previously set.
	 *
	 * @since 1.9.0
	 * @private
	 *
	 * @return {Object} Redux-style action.
	 */
	clearPermissionScopeError() {
		return {
			payload: {},
			type: CLEAR_PERMISSION_SCOPE_ERROR,
		};
	},

	/**
	 * Sets a permission error encountered by an API request.
	 *
	 * @since 1.9.0
	 * @private
	 *
	 * @param {Object} permissionError Permissions error object.
	 * @return {Object} Redux-style action.
	 */
	setPermissionScopeError( permissionError ) {
		invariant( permissionError, 'permissionError is required.' );

		return {
			payload: { permissionError },
			type: SET_PERMISSION_SCOPE_ERROR,
		};
	},

	/**
	 * Sets user capabilities.
	 *
	 * @since 1.13.0
	 * @private
	 *
	 * @param {Object} capabilities User capabilities.
	 * @return {Object} Redux-style action.
	 */
	receiveCapabilities( capabilities ) {
		return {
			type: RECEIVE_CAPABILITIES,
			payload: { capabilities },
		};
	},

	/**
	 * Refreshs user capabilities.
	 *
	 * @since n.e.x.t
	 *
	 * @return {Object} Redux-style action.
	 */
	*refreshCapabilities() {
		const { dispatch } = yield Data.commonActions.getRegistry();

		const {
			response,
			error,
		} = yield fetchRefreshCapabilitiesStore.actions.fetchRefreshCapabilities();

		if ( error ) {
			return dispatch( CORE_USER ).setPermissionScopeError( error );
		}

		global._googlesitekitUserData = {
			permissions: response,
		};

		return dispatch( CORE_USER ).receiveCapabilities( response );
	},
};

const baseControls = {};

const baseReducer = ( state, { type, payload } ) => {
	switch ( type ) {
		case CLEAR_PERMISSION_SCOPE_ERROR: {
			return {
				...state,
				permissionError: null,
			};
		}

		case SET_PERMISSION_SCOPE_ERROR: {
			const { permissionError } = payload;

			return {
				...state,
				permissionError,
			};
		}

		case RECEIVE_CAPABILITIES: {
			const { capabilities } = payload;

			return {
				...state,
				capabilities,
			};
		}

		default: {
			return state;
		}
	}
};

const baseResolvers = {
	*getCapabilities() {
		const registry = yield Data.commonActions.getRegistry();

		if ( registry.select( CORE_USER ).getCapabilities() ) {
			return;
		}

		if ( ! global._googlesitekitUserData?.permissions ) {
			global.console.error( 'Could not load core/user permissions.' );
		}

		yield actions.receiveCapabilities(
			global._googlesitekitUserData?.permissions
		);
	},
};

const baseSelectors = {
	/**
	 * Gets the most recent permission error encountered by this user.
	 *
	 * @since 1.9.0
	 * @private
	 *
	 * @param {Object} state Data store's state.
	 * @return {(Object|null)} Permission scope errors. Returns `null` if no error exists.
	 */
	getPermissionScopeError( state ) {
		const { permissionError } = state;
		return permissionError;
	},

	/**
	 * Gets capabilities of the current user.
	 *
	 * @since 1.13.0
	 *
	 * @param {Object} state Data store's state.
	 * @return {(Object|undefined)} Capabilities object. Returns undefined if it is not loaded yet.
	 */
	getCapabilities( state ) {
		const { capabilities } = state;
		return capabilities;
	},

	/**
	 * Gets viewable module slugs of the current user.
	 *
	 * @since 1.72.0
	 *
	 * @return {(Array|undefined)} An array of viewable module slugs. `undefined` if `modules` are not loaded yet.
	 */
	getViewableModules: createRegistrySelector( ( select ) => () => {
		const modules = select( CORE_MODULES ).getModules();

		if ( modules === undefined ) {
			return undefined;
		}

		// Return an array of module slugs for modules that are
		// shareable and the user has the "read shared module data"
		// capability for.
		return Object.values( modules ).reduce( ( moduleSlugs, module ) => {
			const hasCapability = select( CORE_USER ).hasCapability(
				PERMISSION_READ_SHARED_MODULE_DATA,
				module.slug
			);

			if ( module.shareable && hasCapability ) {
				return [ ...moduleSlugs, module.slug ];
			}

			return moduleSlugs;
		}, [] );
	} ),

	/**
	 * Checks if the current user has the specified capability or not.
	 *
	 * @since 1.13.0
	 *
	 * @param {Object} state      Data store's state.
	 * @param {string} capability Capability name to check.
	 * @param {Array}  args       List of rest of the arguments. Specifically one or many module slugs.
	 * @return {(boolean|undefined)} TRUE if the current user has this capability, otherwise FALSE. If capabilities ain't loaded yet, returns undefined.
	 */
	hasCapability: createRegistrySelector(
		( select ) => ( state, capability, ...args ) => {
			const capabilities = select( CORE_USER ).getCapabilities();

			if ( args.length > 0 ) {
				capability = getMetaCapabilityPropertyName(
					capability,
					...args
				);
			}

			if ( capabilities ) {
				return !! capabilities[ capability ];
			}

			return undefined;
		}
	),

	/**
	 * Checks if the specified module is shareable and viewable by the current user.
	 *
	 * @since 1.77.0
	 *
	 * @param {Object} state      Data store's state.
	 * @param {string} moduleSlug Module slug to check.
	 * @return {(boolean|undefined)} `true` if the module is shareable and viewable by the current user. `false` if the module does not exist, is not shareable or not viewable by the current user. `undefined` if state is not loaded yet.
	 */
	canViewSharedModule: createRegistrySelector(
		( select ) => ( state, moduleSlug ) => {
			const module = select( CORE_MODULES ).getModule( moduleSlug );

			if ( module === undefined ) {
				return undefined;
			}

			if ( module === null || ! module.shareable ) {
				return false;
			}

			return select( CORE_USER ).hasCapability(
				PERMISSION_READ_SHARED_MODULE_DATA,
				module.slug
			);
		}
	),
};

const store = Data.combineStores( fetchRefreshCapabilitiesStore, {
	initialState: baseInitialState,
	actions: baseActions,
	controls: baseControls,
	reducer: baseReducer,
	resolvers: baseResolvers,
	selectors: baseSelectors,
} );

export const initialState = store.initialState;
export const actions = store.actions;
export const controls = store.controls;
export const reducer = store.reducer;
export const resolvers = store.resolvers;
export const selectors = store.selectors;

export default store;
<|endoftext|>


<commit_before>const EventEmitter = require('events');
const Promise = require('bluebird');

class MiniMediator extends EventEmitter {
  constructor(apiPrefix) {
    super();

    this.components = {};
    this.registered = [];
    this.apiPrefix = apiPrefix || 'api';
  }

  register(name, component) {
    let proceed = true;
    const self = this;
    const components = this.components;
    const registered = this.registered;

    Object.keys(components).forEach((cmp) => {
      if (components.hasOwnProperty.call(components, cmp)) {
        let breakFor = false;

        if (components[cmp] === component || cmp === name) {
          proceed = false;
          breakFor = true;
        }

        if (breakFor) {
          return false;
        }
      }

      return true;
    });

    if (proceed) {
      if (typeof component.setMediator === 'function') {
        component.setMediator(this, () => {
          registered.push(name);
          components[name] = component;

          self.emit('registered', {
            component: name,
            members: registered,
            isRegistered: search => self.registered.indexOf(search) > -1,
          });
        });
      }
    }
  }

  remove(name) {
    if (name in this.components) {
      delete this.components[name];
    }

    const index = this.registered.indexOf(name);

    if (index > -1) {
      this.registered.splice(index, 1);
    }
  }

  isRegistered(name) {
    return this.registered[name] !== undefined;
  }

  hasComponent(name) {
    return this.components[name] !== undefined;
  }

  hasApi(domain, api) {
    if (this.hasComponent(domain)) {
      const component = this.getComponent(domain);
      const fullApiName = this.apiPrefix + api;

      if (typeof component[fullApiName] === 'function') {
        return true;
      }

      return false;
    }

    return false;
  }

  getComponent(name) {
    return this.components[name];
  }

  count() {
    return Object.keys(this.components).length;
  }

  callApi(name, api, args, callback) {
    if (this.hasComponent(name)) {
      const component = this.getComponent(name);
      const fullApiName = this.apiPrefix + api;

      if (typeof component[fullApiName] === 'function') {
        return component[fullApiName].call(component, args, callback);
      }

      return callback(new Error('API not found!'));
    }

    return callback(new Error('Component not found!'));
  }

  callApiPromise(name, api, args) {
    return Promise.fromCallback(this.callApi.bind(this, name, api, args));
  }

  registerListener(eventName, listener) {
    this.addListener(eventName, listener);
  }

  unregisterListener(eventName, listener) {
    this.removeListener(eventName, listener);
  }
}

module.exports = MiniMediator;
<commit_msg>Fix isRegistered() logic: should use indexOf() instead of [].<commit_after>const EventEmitter = require('events');
const Promise = require('bluebird');

class MiniMediator extends EventEmitter {
  constructor(apiPrefix) {
    super();

    this.components = {};
    this.registered = [];
    this.apiPrefix = apiPrefix || 'api';
  }

  register(name, component) {
    let proceed = true;
    const self = this;
    const components = this.components;
    const registered = this.registered;

    Object.keys(components).forEach((cmp) => {
      if (components.hasOwnProperty.call(components, cmp)) {
        let breakFor = false;

        if (components[cmp] === component || cmp === name) {
          proceed = false;
          breakFor = true;
        }

        if (breakFor) {
          return false;
        }
      }

      return true;
    });

    if (proceed) {
      if (typeof component.setMediator === 'function') {
        component.setMediator(this, () => {
          registered.push(name);
          components[name] = component;

          self.emit('registered', {
            component: name,
            members: registered,
            isRegistered: search => self.registered.indexOf(search) > -1,
          });
        });
      }
    }
  }

  remove(name) {
    if (name in this.components) {
      delete this.components[name];
    }

    const index = this.registered.indexOf(name);

    if (index > -1) {
      this.registered.splice(index, 1);
    }
  }

  isRegistered(name) {
    return this.registered.indexOf(name) !== -1;
  }

  hasComponent(name) {
    return this.components[name] !== undefined;
  }

  hasApi(domain, api) {
    if (this.hasComponent(domain)) {
      const component = this.getComponent(domain);
      const fullApiName = this.apiPrefix + api;

      if (typeof component[fullApiName] === 'function') {
        return true;
      }

      return false;
    }

    return false;
  }

  getComponent(name) {
    return this.components[name];
  }

  count() {
    return Object.keys(this.components).length;
  }

  callApi(name, api, args, callback) {
    if (this.hasComponent(name)) {
      const component = this.getComponent(name);
      const fullApiName = this.apiPrefix + api;

      if (typeof component[fullApiName] === 'function') {
        return component[fullApiName].call(component, args, callback);
      }

      return callback(new Error('API not found!'));
    }

    return callback(new Error('Component not found!'));
  }

  callApiPromise(name, api, args) {
    return Promise.fromCallback(this.callApi.bind(this, name, api, args));
  }

  registerListener(eventName, listener) {
    this.addListener(eventName, listener);
  }

  unregisterListener(eventName, listener) {
    this.removeListener(eventName, listener);
  }
}

module.exports = MiniMediator;
<|endoftext|>


<commit_before>import isString from '../util/isString'
import keys from '../util/keys'
import {setCursor} from '../model/selectionHelpers'
import EditingBehavior from '../model/EditingBehavior'
import Surface from '../packages/surface/Surface'
import IsolatedNodeComponent from '../packages/isolated-node/IsolatedNodeComponent'
import RenderingEngine from '../ui/RenderingEngine'
// import platform from '../util/platform'
// import Component from '../ui/Component'

/**
  Represents a flow editor that manages a sequence of nodes in a container. Needs to be
  instantiated inside a {@link ui/Controller} context.

  @class ContainerEditor
  @component
  @extends ui/Surface

  @prop {String} name unique editor name
  @prop {String} containerId container id
  @prop {Object[]} textTypes array of textType definition objects
  @prop {ui/SurfaceCommand[]} commands array of command classes to be available

  @example

  Create a full-fledged `ContainerEditor` for the `body` container of a document.
  Allow Strong and Emphasis annotations and to switch text types between paragraph
  and heading at level 1.

  ```js
  $$(ContainerEditor, {
    name: 'bodyEditor',
    containerId: 'body',
    textTypes: [
      {name: 'paragraph', data: {type: 'paragraph'}},
      {name: 'heading1',  data: {type: 'heading', level: 1}}
    ],
    commands: [StrongCommand, EmphasisCommand, SwitchTextTypeCommand],
  })
  ```
*/

class ContainerEditor extends Surface {

  constructor(parent, props, el) {
    // default props derived from the given props
    props.containerId = props.containerId || props.node.id
    props.name = props.name || props.containerId || props.node.id

    super(parent, props, el)

    this.containerId = this.props.containerId
    if (!isString(this.containerId)) {
      throw new Error("Property 'containerId' is mandatory.")
    }
    let doc = this.getDocument()
    this.container = doc.get(this.containerId)
    if (!this.container) {
      throw new Error('Container with id ' + this.containerId + ' does not exist.')
    }

    this.textTypes = this.props.textTypes || []

    this.editingBehavior = this.context.editingBehavior || new EditingBehavior()

    this._deriveInternalState(this.props)
  }

  // Note: this component is self managed
  shouldRerender(newProps) {
    if (newProps.disabled !== this.props.disabled) return true
    // TODO: we should still detect when the document has changed,
    // see https://github.com/substance/substance/issues/543
    return false
  }

  willReceiveProps(newProps) {
    super.willReceiveProps.apply(this, arguments)
    this._deriveInternalState(newProps)
  }

  didMount() {
    super.didMount.apply(this, arguments)
    let editorSession = this.getEditorSession()
    editorSession.onUpdate('document', this._onContainerChanged, this, {
      path: [this.getContainerId(), 'nodes']
    })

  }

  dispose() {
    super.dispose.apply(this, arguments)
    let editorSession = this.getEditorSession()
    editorSession.off(this)
  }

  render($$) {
    let el = super.render($$)

    let doc = this.getDocument()
    let containerId = this.getContainerId()
    let containerNode = doc.get(containerId)
    if (!containerNode) {
      console.warn('No container node found for ', containerId)
    }
    el.addClass('sc-container-editor container-node ' + containerId)
      .attr("data-id", containerId)

    // native spellcheck
    el.attr('spellcheck', this.props.spellcheck === 'native')

    containerNode.getNodes().forEach(function(node) {
      el.append(this._renderNode($$, node))
    }.bind(this))

    // No editing if disabled by user or container is empty
    if (!this.props.disabled && !this.isEmpty()) {
      el.addClass('sm-enabled')
      el.setAttribute('contenteditable', true)
    }

    return el
  }

  _renderNode($$, node) {
    if (!node) throw new Error('Illegal argument')
    if (node.isText()) {
      return super.renderNode($$, node)
    } else {
      let componentRegistry = this.context.componentRegistry
      let ComponentClass = componentRegistry.get(node.type)
      if (ComponentClass.prototype._isCustomNodeComponent || ComponentClass.prototype._isIsolatedNodeComponent) {
        return $$(ComponentClass, { node: node }).ref(node.id)
      } else {
        return $$(IsolatedNodeComponent, { node: node }).ref(node.id)
      }
    }
  }

  _deriveInternalState(props) {
    let _state = this._state
    if (!props.hasOwnProperty('enabled') || props.enabled) {
      _state.enabled = true
    } else {
      _state.enabled = false
    }
  }

  _selectNextIsolatedNode(direction) {
    let selState = this.getEditorSession().getSelectionState()
    let node = (direction === 'left') ? selState.getPreviousNode() : selState.getNextNode()
    if (!node || !node.isIsolatedNode()) return false
    if (
      (direction === 'left' && selState.isFirst()) ||
      (direction === 'right' && selState.isLast())
    ) {
      this.getEditorSession().setSelection({
        type: 'node',
        nodeId: node.id,
        containerId: selState.getContainer().id,
        surfaceId: this.id
      })
      return true
    }
    return false
  }

  _handleLeftOrRightArrowKey(event) {
    event.stopPropagation()
    const doc = this.getDocument()
    const sel = this.getEditorSession().getSelection()
    const left = (event.keyCode === keys.LEFT)
    const right = !left
    const direction = left ? 'left' : 'right'

    if (sel && !sel.isNull()) {
      const container = doc.get(sel.containerId, 'strict')

      // Don't react if we are at the boundary of the document
      if (sel.isNodeSelection()) {
        let nodePos = container.getPosition(doc.get(sel.getNodeId()))
        if ((left && nodePos === 0) || (right && nodePos === container.length-1)) {
          event.preventDefault()
          return
        }
      }

      if (sel.isNodeSelection() && !event.shiftKey) {
        this.domSelection.collapse(direction)
      }
    }

    window.setTimeout(() => {
      this._updateModelSelection({ direction })
    })
  }

  _handleUpOrDownArrowKey(event) {
    event.stopPropagation()
    const doc = this.getDocument()
    const sel = this.getEditorSession().getSelection()
    const up = (event.keyCode === keys.UP)
    const down = !up
    const direction = up ? 'left' : 'right'

    if (sel && !sel.isNull()) {
      const container = doc.get(sel.containerId, 'strict')
      // Don't react if we are at the boundary of the document
      if (sel.isNodeSelection()) {
        let nodePos = container.getPosition(doc.get(sel.getNodeId()))
        if ((up && nodePos === 0) || (down && nodePos === container.length-1)) {
          event.preventDefault()
          return
        }
        // Unfortunately we need to navigate out of an isolated node
        // manually, as even Chrome on Win is not able to do it.
        let editorSession = this.getEditorSession()
        event.preventDefault()
        if (up) {
          let prev = container.getChildAt(nodePos-1)
          setCursor(editorSession, prev, sel.containerId, 'after')
          return
        } else {
          let next = container.getChildAt(nodePos+1)
          setCursor(editorSession, next, sel.containerId, 'before')
          return
        }
      }
    }

    window.setTimeout(() => {
      this._updateModelSelection({ direction })
    })
  }

  _handleSpaceKey(event) {
    let sel = this.getEditorSession().getSelection()
    if (sel.isNodeSelection() && sel.isFull()) {
      let comp = this.refs[sel.getNodeId()]
      if (comp && comp.grabFocus) {
        event.preventDefault()
        event.stopPropagation()
        comp.grabFocus()
        return
      }
    }
    super._handleSpaceKey(event)
  }

  // Used by Clipboard
  isContainerEditor() {
    return true
  }

  /**
    Returns the containerId the editor is bound to
  */
  getContainerId() {
    return this.containerId
  }

  // TODO: do we really need this in addition to getContainerId?
  getContainer() {
    return this.getDocument().get(this.getContainerId())
  }

  isEmpty() {
    let containerNode = this.getContainer()
    return (containerNode && containerNode.nodes.length === 0)
  }

  isEditable() {
    return super.isEditable.call(this) && !this.isEmpty()
  }

  getTextTypes() {
    return this.textTypes || []
  }

  // Used by SwitchTextTypeTool
  // TODO: Filter by enabled commands for this Surface
  // TODO: rethink
  getTextCommands() {
    var textCommands = {}
    this.commandRegistry.each(function(cmd) {
      if (cmd.constructor.textTypeName) {
        textCommands[cmd.getName()] = cmd
      }
    });
    return textCommands
  }

  // called by flow when subscribed resources have been updated
  _onContainerChanged(change) {
    let doc = this.getDocument()
    // first update the container
    let renderContext = RenderingEngine.createContext(this)
    let $$ = renderContext.$$
    let container = this.getContainer()
    let path = container.getContentPath()
    for (let i = 0; i < change.ops.length; i++) {
      let op = change.ops[i]
      if (op.type === "update" && op.path[0] === path[0]) {
        let diff = op.diff
        if (diff.type === "insert") {
          let nodeId = diff.getValue()
          let node = doc.get(nodeId)
          let nodeEl
          if (node) {
            nodeEl = this._renderNode($$, node)
          } else {
            // node does not exist anymore
            // so we insert a stub element, so that the number of child
            // elements is consistent
            nodeEl = $$('div')
          }
          this.insertAt(diff.getOffset(), nodeEl)
        } else if (diff.type === "delete") {
          this.removeAt(diff.getOffset())
        }
      }
    }
  }
}

ContainerEditor.prototype._isContainerEditor = true

export default ContainerEditor
<commit_msg>Fix IsolatedNode cursor navigation under Windows.<commit_after>import isString from '../util/isString'
import keys from '../util/keys'
import {setCursor} from '../model/selectionHelpers'
import EditingBehavior from '../model/EditingBehavior'
import Surface from '../packages/surface/Surface'
import IsolatedNodeComponent from '../packages/isolated-node/IsolatedNodeComponent'
import RenderingEngine from '../ui/RenderingEngine'
// import platform from '../util/platform'
// import Component from '../ui/Component'

/**
  Represents a flow editor that manages a sequence of nodes in a container. Needs to be
  instantiated inside a {@link ui/Controller} context.

  @class ContainerEditor
  @component
  @extends ui/Surface

  @prop {String} name unique editor name
  @prop {String} containerId container id
  @prop {Object[]} textTypes array of textType definition objects
  @prop {ui/SurfaceCommand[]} commands array of command classes to be available

  @example

  Create a full-fledged `ContainerEditor` for the `body` container of a document.
  Allow Strong and Emphasis annotations and to switch text types between paragraph
  and heading at level 1.

  ```js
  $$(ContainerEditor, {
    name: 'bodyEditor',
    containerId: 'body',
    textTypes: [
      {name: 'paragraph', data: {type: 'paragraph'}},
      {name: 'heading1',  data: {type: 'heading', level: 1}}
    ],
    commands: [StrongCommand, EmphasisCommand, SwitchTextTypeCommand],
  })
  ```
*/

class ContainerEditor extends Surface {

  constructor(parent, props, el) {
    // default props derived from the given props
    props.containerId = props.containerId || props.node.id
    props.name = props.name || props.containerId || props.node.id

    super(parent, props, el)

    this.containerId = this.props.containerId
    if (!isString(this.containerId)) {
      throw new Error("Property 'containerId' is mandatory.")
    }
    let doc = this.getDocument()
    this.container = doc.get(this.containerId)
    if (!this.container) {
      throw new Error('Container with id ' + this.containerId + ' does not exist.')
    }

    this.textTypes = this.props.textTypes || []

    this.editingBehavior = this.context.editingBehavior || new EditingBehavior()

    this._deriveInternalState(this.props)
  }

  // Note: this component is self managed
  shouldRerender(newProps) {
    if (newProps.disabled !== this.props.disabled) return true
    // TODO: we should still detect when the document has changed,
    // see https://github.com/substance/substance/issues/543
    return false
  }

  willReceiveProps(newProps) {
    super.willReceiveProps.apply(this, arguments)
    this._deriveInternalState(newProps)
  }

  didMount() {
    super.didMount.apply(this, arguments)
    let editorSession = this.getEditorSession()
    editorSession.onUpdate('document', this._onContainerChanged, this, {
      path: [this.getContainerId(), 'nodes']
    })

  }

  dispose() {
    super.dispose.apply(this, arguments)
    let editorSession = this.getEditorSession()
    editorSession.off(this)
  }

  render($$) {
    let el = super.render($$)

    let doc = this.getDocument()
    let containerId = this.getContainerId()
    let containerNode = doc.get(containerId)
    if (!containerNode) {
      console.warn('No container node found for ', containerId)
    }
    el.addClass('sc-container-editor container-node ' + containerId)
      .attr("data-id", containerId)

    // native spellcheck
    el.attr('spellcheck', this.props.spellcheck === 'native')

    containerNode.getNodes().forEach(function(node) {
      el.append(this._renderNode($$, node))
    }.bind(this))

    // No editing if disabled by user or container is empty
    if (!this.props.disabled && !this.isEmpty()) {
      el.addClass('sm-enabled')
      el.setAttribute('contenteditable', true)
    }

    return el
  }

  _renderNode($$, node) {
    if (!node) throw new Error('Illegal argument')
    if (node.isText()) {
      return super.renderNode($$, node)
    } else {
      let componentRegistry = this.context.componentRegistry
      let ComponentClass = componentRegistry.get(node.type)
      if (ComponentClass.prototype._isCustomNodeComponent || ComponentClass.prototype._isIsolatedNodeComponent) {
        return $$(ComponentClass, { node: node }).ref(node.id)
      } else {
        return $$(IsolatedNodeComponent, { node: node }).ref(node.id)
      }
    }
  }

  _deriveInternalState(props) {
    let _state = this._state
    if (!props.hasOwnProperty('enabled') || props.enabled) {
      _state.enabled = true
    } else {
      _state.enabled = false
    }
  }

  _selectNextIsolatedNode(direction) {
    let selState = this.getEditorSession().getSelectionState()
    let node = (direction === 'left') ? selState.getPreviousNode() : selState.getNextNode()
    if (!node || !node.isIsolatedNode()) return false
    if (
      (direction === 'left' && selState.isFirst()) ||
      (direction === 'right' && selState.isLast())
    ) {
      this.getEditorSession().setSelection({
        type: 'node',
        nodeId: node.id,
        containerId: selState.getContainer().id,
        surfaceId: this.id
      })
      return true
    }
    return false
  }

  _handleLeftOrRightArrowKey(event) {
    event.stopPropagation()
    const doc = this.getDocument()
    const sel = this.getEditorSession().getSelection()
    const left = (event.keyCode === keys.LEFT)
    const right = !left
    const direction = left ? 'left' : 'right'

    if (sel && !sel.isNull()) {
      const container = doc.get(sel.containerId, 'strict')

      // Don't react if we are at the boundary of the document
      if (sel.isNodeSelection()) {
        let nodePos = container.getPosition(doc.get(sel.getNodeId()))
        if ((left && nodePos === 0) || (right && nodePos === container.length-1)) {
          event.preventDefault()
          return
        }
      }

      if (sel.isNodeSelection() && !event.shiftKey) {
        this.domSelection.collapse(direction)
      }
    }

    window.setTimeout(() => {
      this._updateModelSelection({ direction })
    })
  }

  _handleUpOrDownArrowKey(event) {
    event.stopPropagation()
    const doc = this.getDocument()
    const sel = this.getEditorSession().getSelection()
    const up = (event.keyCode === keys.UP)
    const down = !up
    const direction = up ? 'left' : 'right'

    if (sel && !sel.isNull()) {
      const container = doc.get(sel.containerId, 'strict')
      // Don't react if we are at the boundary of the document
      if (sel.isNodeSelection()) {
        let nodePos = container.getPosition(doc.get(sel.getNodeId()))
        if ((up && nodePos === 0) || (down && nodePos === container.length-1)) {
          event.preventDefault()
          return
        }
        // Unfortunately we need to navigate out of an isolated node
        // manually, as even Chrome on Win is not able to do it.
        let editorSession = this.getEditorSession()
        // TODO the following fixes the mentioned problem for
        // regular UP/DOWN (non expanding)
        // For SHIFT+DOWN it happens to work, and only SHIFT-UP when started as NodeSelection needs to be fixed
        if (!event.shiftKey) {
          event.preventDefault()
          if (up) {
            let prev = container.getChildAt(nodePos-1)
            setCursor(editorSession, prev, sel.containerId, 'after')
            return
          } else {
            let next = container.getChildAt(nodePos+1)
            setCursor(editorSession, next, sel.containerId, 'before')
            return
          }
        }
      }
    }

    window.setTimeout(() => {
      this._updateModelSelection({ direction })
    })
  }

  _handleSpaceKey(event) {
    let sel = this.getEditorSession().getSelection()
    if (sel.isNodeSelection() && sel.isFull()) {
      let comp = this.refs[sel.getNodeId()]
      if (comp && comp.grabFocus) {
        event.preventDefault()
        event.stopPropagation()
        comp.grabFocus()
        return
      }
    }
    super._handleSpaceKey(event)
  }

  // Used by Clipboard
  isContainerEditor() {
    return true
  }

  /**
    Returns the containerId the editor is bound to
  */
  getContainerId() {
    return this.containerId
  }

  // TODO: do we really need this in addition to getContainerId?
  getContainer() {
    return this.getDocument().get(this.getContainerId())
  }

  isEmpty() {
    let containerNode = this.getContainer()
    return (containerNode && containerNode.nodes.length === 0)
  }

  isEditable() {
    return super.isEditable.call(this) && !this.isEmpty()
  }

  getTextTypes() {
    return this.textTypes || []
  }

  // Used by SwitchTextTypeTool
  // TODO: Filter by enabled commands for this Surface
  // TODO: rethink
  getTextCommands() {
    var textCommands = {}
    this.commandRegistry.each(function(cmd) {
      if (cmd.constructor.textTypeName) {
        textCommands[cmd.getName()] = cmd
      }
    });
    return textCommands
  }

  // called by flow when subscribed resources have been updated
  _onContainerChanged(change) {
    let doc = this.getDocument()
    // first update the container
    let renderContext = RenderingEngine.createContext(this)
    let $$ = renderContext.$$
    let container = this.getContainer()
    let path = container.getContentPath()
    for (let i = 0; i < change.ops.length; i++) {
      let op = change.ops[i]
      if (op.type === "update" && op.path[0] === path[0]) {
        let diff = op.diff
        if (diff.type === "insert") {
          let nodeId = diff.getValue()
          let node = doc.get(nodeId)
          let nodeEl
          if (node) {
            nodeEl = this._renderNode($$, node)
          } else {
            // node does not exist anymore
            // so we insert a stub element, so that the number of child
            // elements is consistent
            nodeEl = $$('div')
          }
          this.insertAt(diff.getOffset(), nodeEl)
        } else if (diff.type === "delete") {
          this.removeAt(diff.getOffset())
        }
      }
    }
  }
}

ContainerEditor.prototype._isContainerEditor = true

export default ContainerEditor
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import './App.css';

import {
    SearchBox,
    NoHits,
    Hits,
    HitsStats,
    ItemCheckboxList,
    SortingSelector,
    SelectedFilters,
    MenuFilter,
    Pagination,
    RefinementListFilter,
    ResetFilters,
    SearchkitManager,
    SearchkitProvider,
    Tabs,
    } from "searchkit";

import {
  Layout, TopBar, LayoutBody, LayoutResults,
  ActionBar, ActionBarRow, SideBar
} from "searchkit"

const searchkit = new SearchkitManager("https://kinto-ota.dev.mozaws.net/v1/buckets/build-hub/collections/releases/", {searchUrlPath: "search"})

const HitsTable = ({hits}) => {
  return (

            Product
            Tree
            Revision
            Version
            Platform
            Channel
            Locale
            URL
            Mimetype
            Size
            Published on
            Build ID
            Date

          {hits.map(hit => {
            const revisionUrl = (hit._source.source.revision)
              ? ({hit._source.source.revision})
              : "";
            const filename = hit._source.download.url.split("/").reverse()[0];
            return (

                {hit._source.source.product}
                {hit._source.source.tree}
                {revisionUrl}
                {hit._source.target.version}
                {hit._source.target.platform}
                {hit._source.target.channel}
                {hit._source.target.locale}
                {filename}
                {hit._source.download.mimetype}
                {hit._source.download.size}
                {hit._source.download.date}
                {hit._source.build && hit._source.build.id}
                {hit._source.build && hit._source.build.date}

          )})}

  );
};

class App extends Component {
  render() {
    return (

        <SearchkitProvider searchkit={searchkit}>
          <Layout>

            <TopBar>
              <SearchBox
              autofocus={true}
              searchOnChange={true}
              queryFields={["build.id"]}/>
            </TopBar>

            <LayoutBody>
              <SideBar>
                <RefinementListFilter field="target.version"
                            title="Version"
                            id="versions"
                            listComponent={ItemCheckboxList}
                            size={10}
                            operator="OR"
                            translations={{"All":"All versions"}}/>
                <RefinementListFilter field="target.platform"
                            title="Platform"
                            id="platform"
                            size={10}
                            operator="OR"
                            listComponent={ItemCheckboxList}
                            translations={{"All":"All platforms"}}/>
                <RefinementListFilter field="target.channel"
                            title="Channel"
                            id="channel"
                            size={10}
                            operator="OR"
                            listComponent={ItemCheckboxList}
                            translations={{"All":"All channels"}}/>
                <RefinementListFilter field="target.locale"
                            title="Locale"
                            id="locale"
                            listComponent={ItemCheckboxList}
                            size={10}
                            operator="OR"
                            translations={{"All":"All locales"}}/>
              </SideBar>

              <LayoutResults>

                <ActionBar>
                  <ActionBarRow>
                    <HitsStats/>
                    <SortingSelector options={[
                      {label: "Latest Releases", field:"download.date", order: "desc", defaultOption: true},
                      {label: "Relevance", field: "_score", order: "desc"},
                    ]}/>
                  </ActionBarRow>

                  <ActionBarRow>
                    <SelectedFilters/>
                    <ResetFilters/>
                  </ActionBarRow>

                  <MenuFilter field="source.product"
                              title="Product"
                              id="products"
                              listComponent={Tabs}
                              translations={{"All":"All products"}}/>
                </ActionBar>

                <Hits hitsPerPage={30} listComponent={HitsTable}/>
                <NoHits translations={{
                  "NoHits.NoResultsFound":"No release found were found for {query}",
                  "NoHits.DidYouMean":"Search for {suggestion}",
                  "NoHits.SearchWithoutFilters":"Search for {query} without filters"}} suggestionsField="target.version"/>

                <Pagination showNumbers={true}/>

               </LayoutResults>
            </LayoutBody>
          </Layout>
        </SearchkitProvider>

    );
  }
}

export default App;
<commit_msg>Expand/collapse entries by clicking on them<commit_after>import React, { Component } from 'react';
import './App.css';

import {
    SearchBox,
    NoHits,
    Hits,
    HitsStats,
    ItemCheckboxList,
    SortingSelector,
    SelectedFilters,
    MenuFilter,
    Pagination,
    RefinementListFilter,
    ResetFilters,
    SearchkitManager,
    SearchkitProvider,
    Tabs,
    } from "searchkit";

import {
  Layout, TopBar, LayoutBody, LayoutResults,
  ActionBar, ActionBarRow, SideBar
} from "searchkit"

const searchkit = new SearchkitManager("https://kinto-ota.dev.mozaws.net/v1/buckets/build-hub/collections/releases/", {searchUrlPath: "search"})

const HitsTable = (toggleExpand, expandedEntry) => {
  return ({hits}) => {
    return (

              Product
              Version
              Platform
              Channel
              Locale
              Published on

            {hits.map(hit => {
              const revisionUrl = (hit._source.source.revision)
                ? ({hit._source.source.revision})
                : "";
              const filename = hit._source.download.url.split("/").reverse()[0];
              const clickHandler = (event, data) => toggleExpand(event, data, hit._id);
              if (expandedEntry === hit._id) {
                return (

Product: {hit._source.source.product}

Version: {hit._source.target.version}

Platform: {hit._source.target.platform}

Channel: {hit._source.target.channel}

Locale: {hit._source.target.locale}

Published on: {hit._source.build && hit._source.build.date}

Tree: {hit._source.source.tree}

Revision: {revisionUrl}

URL: {filename}

Mimetype: {hit._source.download.mimetype}

Size: {hit._source.download.size}

Date: {hit._source.download.date}

Id: {hit._source.build && hit._source.build.id}

                )
              } else {
                return (

                    {hit._source.source.product}
                    {hit._source.target.version}
                    {hit._source.target.platform}
                    {hit._source.target.channel}
                    {hit._source.target.locale}
                    {hit._source.build && hit._source.build.date}

                )
              }
            })}

    );
  };
};

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      expandedEntry: null
    };
  }

  toggleExpand(event, data, id) {
    if (this.state.expandedEntry === id) {
      this.setState({expandedEntry: null});
    } else {
      this.setState({expandedEntry: id});
    }
  }

  render() {
    return (

        <SearchkitProvider searchkit={searchkit}>
          <Layout>

            <TopBar>
              <SearchBox
              autofocus={true}
              searchOnChange={true}
              queryFields={["build.id"]}/>
            </TopBar>

            <LayoutBody>
              <SideBar>
                <RefinementListFilter field="target.version"
                            title="Version"
                            id="versions"
                            listComponent={ItemCheckboxList}
                            size={10}
                            operator="OR"
                            translations={{"All":"All versions"}}/>
                <RefinementListFilter field="target.platform"
                            title="Platform"
                            id="platform"
                            size={10}
                            operator="OR"
                            listComponent={ItemCheckboxList}
                            translations={{"All":"All platforms"}}/>
                <RefinementListFilter field="target.channel"
                            title="Channel"
                            id="channel"
                            size={10}
                            operator="OR"
                            listComponent={ItemCheckboxList}
                            translations={{"All":"All channels"}}/>
                <RefinementListFilter field="target.locale"
                            title="Locale"
                            id="locale"
                            listComponent={ItemCheckboxList}
                            size={10}
                            operator="OR"
                            translations={{"All":"All locales"}}/>
              </SideBar>

              <LayoutResults>

                <ActionBar>
                  <ActionBarRow>
                    <HitsStats/>
                    <SortingSelector options={[
                      {label: "Latest Releases", field:"download.date", order: "desc", defaultOption: true},
                      {label: "Relevance", field: "_score", order: "desc"},
                    ]}/>
                  </ActionBarRow>

                  <ActionBarRow>
                    <SelectedFilters/>
                    <ResetFilters/>
                  </ActionBarRow>

                  <MenuFilter field="source.product"
                              title="Product"
                              id="products"
                              listComponent={Tabs}
                              translations={{"All":"All products"}}/>
                </ActionBar>

                <Hits hitsPerPage={30} listComponent={HitsTable(this.toggleExpand.bind(this), this.state.expandedEntry)}/>
                <NoHits translations={{
                  "NoHits.NoResultsFound":"No release found were found for {query}",
                  "NoHits.DidYouMean":"Search for {suggestion}",
                  "NoHits.SearchWithoutFilters":"Search for {query} without filters"}} suggestionsField="target.version"/>

                <Pagination showNumbers={true}/>

               </LayoutResults>
            </LayoutBody>
          </Layout>
        </SearchkitProvider>

    );
  }
}

export default App;
<|endoftext|>


<commit_before>/**
 * Module dependencies
 * @type {Object}
 */
var util = require('util');
var _ = require('lodash');
var async = require('async');
var switchback = require('node-switchback');

var generate = require('./generate');
var FolderHelper = require('./helpers/folder');
var TemplateHelper = require('./helpers/template');
var JSONFileHelper = require('./helpers/jsonfile');
var CopyHelper = require('./helpers/copy');

/**
 *
 * @param  {Object}   options
 */
function generateTarget (options) {

	// Options
	var target = options.target;
	var scope = options.scope;
	var cb = options.cb;
	var parentGenerator = options.parent;
	var recursiveGenerate = options.recursiveGenerate;

	var MAX_RESOLVES = 5, _resolves = 0;
	async.until(function checkIfTargetIsValidYet () {
		return isValidTarget(target) || (++_resolves > MAX_RESOLVES);
	}, function tryToParseTarget (cb) {
		parseTarget(target, scope, function (err, resolvedTarget) {
			if (err) return cb(err);
			target = resolvedTarget;
			return cb();
		});
	}, function afterwards (err) {
		if (err) return cb(err);
		if ( !isValidTarget(target) ) {
			return cb(new Error('Generator Error: Could not resolve target "'+scope.rootPath+'" (probably a recursive loop)'));
		}

		// Interpret generator definition
		if (target.exec) {
			return target.exec(scope, cb);
		}
		if (target.copy) {
			scope = mergeSubtargetScope(scope,typeof target.copy === 'string' ? {templatePath: target.copy} : target.copy);
			return CopyHelper(scope, cb);
		}
		if (target.folder) {
			scope = mergeSubtargetScope(scope, target.folder);
			return FolderHelper(scope, cb);
		}
		if (target.template) {
			scope = mergeSubtargetScope(scope,typeof target.template === 'string' ? {templatePath: target.template} : target.template);

			// Pass down parent Generator's template directory abs path
			scope.templatesDirectory = parentGenerator.templatesDirectory;

			return TemplateHelper(scope, function (err) {
				if (err) {
					if (err.path) return cb('Cannot find specified template @ `'+e.path+'`');
					return cb(err);
				}
				return cb(err);
			});
		}
		if (target.jsonfile) {
			if (typeof target.jsonfile === 'object') {
				scope = mergeSubtargetScope(scope, target.jsonfile);
			}
			else if (typeof target.jsonfile === 'function') {
				scope = _.merge(scope, {
					data: target.jsonfile(scope)
				});
			}
			return JSONFileHelper(scope, cb);
		}

		// If we made it here, this must be a recursive generator:

		// Now that the generator definition has been resolved,
		// call this method recursively on it, passing along our
		// callback:
		if (++scope._depth > scope.options.maxDepth) {
			return cb(new Error('`maxDepth` ('+maxDepth+' exceeded!  There is probably a recursive loop in one of your generators.'));
		}
		return recursiveGenerate(target, scope, cb);
	});

}

module.exports = generateTarget;

/**
 * @param  {[type]} scope     [description]
 * @param  {[type]} subtarget [description]
 * @return {[type]}           [description]
 */
function mergeSubtargetScope (scope, subtarget) {
	return _.merge(scope, _.isObject(subtarget) ? subtarget : {});
}

/**
 * Known helpers
 * @type {Array}
 */
var KNOWN_HELPERS = ['exec', 'folder', 'template', 'jsonfile', 'file', 'copy'];
function targetIsHelper (target) {
	return _.some(target, function (subTarget, key) {
		return _.contains(KNOWN_HELPERS,key);
	});
}

/**
 *
 * @param  {[type]}   target      [description]
 * @param  {[type]}   scope [description]
 * @param  {Function} cb          [description]
 * @return {[type]}               [description]
 */
function parseTarget (target, scope, cb) {

	if (typeof target === 'string') {
		target = { generator: target };
	}

	// Interpret generator definition
	if (targetIsHelper(target)) {
		return cb(null, target);
	}

	if (target.generator) {

		// Normalize the subgenerator reference
		var subGeneratorRef;
		if (typeof target.generator === 'string') {
			subGeneratorRef = { module: target.generator };
		}
		else if (typeof target.generator === 'object') {
			subGeneratorRef = target.generator;
		}
		if (!subGeneratorRef) {
			return cb(new Error('Generator Error: Invalid subgenerator referenced for target "'+ scope.rootPath +'"'));
		}

		// Now normalize the sub-generator
		var subGenerator;
		if (!subGeneratorRef.module) {
			subGenerator = subGeneratorRef;
		}
		else {

			if (typeof subGeneratorRef.module === 'string') {
				// Lookup the generator by name if a `module` was specified
				// (this allows the module for a given generator to be
				//  overridden.)
				var configuredReference = scope.modules[subGeneratorRef.module] || 'sails-generate-'+subGeneratorRef.module;
				if (configuredReference) {
					return cb(null, configuredReference);
				}
			}

			// At this point, subGeneratorRef.module should be a string,
			// and the best guess at the generator module we're going
			// to get.
			var requireError;
			try {
				subGenerator = require(subGeneratorRef.module);
			}
			catch(e) { requireError = e; }

			// If we still can't find it, give up
			if (!subGenerator) {
				// TODO: look for subGeneratorRef on npm
				// TODO: emit a message to scope.output letting user know what's going on
				return cb(
					new Error('Generator Error: Failed to load "' +
					subGeneratorRef.module + '"...' +
					(requireError ? ' ('+requireError+')' : '')+
					'')
				);
			}

			return cb(null, subGenerator);
		}
	}

	return cb( new Error(
		'Unrecognized generator syntax in `targets["'+scope.keyPath+'"]` ::\n'+
		util.inspect(target))
	);
}

/**
 *
 * @param  {[type]}  target [description]
 * @return {Boolean}        [description]
 */
function isValidTarget (target) {
	var ok = true;

	ok = ok && typeof target === 'object';

	// Is using a helper
	// Or is another generator def.
	ok = ok && ( targetIsHelper(target) || _.has(target, 'targets') );

	return ok;
}
<commit_msg>Fixed issue with determining default submodules.<commit_after>/**
 * Module dependencies
 * @type {Object}
 */
var util = require('util');
var _ = require('lodash');
var async = require('async');
var switchback = require('node-switchback');

var generate = require('./generate');
var FolderHelper = require('./helpers/folder');
var TemplateHelper = require('./helpers/template');
var JSONFileHelper = require('./helpers/jsonfile');
var CopyHelper = require('./helpers/copy');

/**
 *
 * @param  {Object}   options
 */
function generateTarget (options) {

	// Options
	var target = options.target;
	var scope = options.scope;
	var cb = options.cb;
	var parentGenerator = options.parent;
	var recursiveGenerate = options.recursiveGenerate;

	var MAX_RESOLVES = 5, _resolves = 0;
	async.until(function checkIfTargetIsValidYet () {
		return isValidTarget(target) || (++_resolves > MAX_RESOLVES);
	}, function tryToParseTarget (cb) {
		parseTarget(target, scope, function (err, resolvedTarget) {
			if (err) return cb(err);
			target = resolvedTarget;
			return cb();
		});
	}, function afterwards (err) {
		if (err) return cb(err);
		if ( !isValidTarget(target) ) {
			return cb(new Error('Generator Error: Could not resolve target "'+scope.rootPath+'" (probably a recursive loop)'));
		}

		// Interpret generator definition
		if (target.exec) {
			return target.exec(scope, cb);
		}
		if (target.copy) {
			scope = mergeSubtargetScope(scope,typeof target.copy === 'string' ? {templatePath: target.copy} : target.copy);
			return CopyHelper(scope, cb);
		}
		if (target.folder) {
			scope = mergeSubtargetScope(scope, target.folder);
			return FolderHelper(scope, cb);
		}
		if (target.template) {
			scope = mergeSubtargetScope(scope,typeof target.template === 'string' ? {templatePath: target.template} : target.template);

			// Pass down parent Generator's template directory abs path
			scope.templatesDirectory = parentGenerator.templatesDirectory;

			return TemplateHelper(scope, function (err) {
				if (err) {
					if (err.path) return cb('Cannot find specified template @ `'+e.path+'`');
					return cb(err);
				}
				return cb(err);
			});
		}
		if (target.jsonfile) {
			if (typeof target.jsonfile === 'object') {
				scope = mergeSubtargetScope(scope, target.jsonfile);
			}
			else if (typeof target.jsonfile === 'function') {
				scope = _.merge(scope, {
					data: target.jsonfile(scope)
				});
			}
			return JSONFileHelper(scope, cb);
		}

		// If we made it here, this must be a recursive generator:

		// Now that the generator definition has been resolved,
		// call this method recursively on it, passing along our
		// callback:
		if (++scope._depth > scope.options.maxDepth) {
			return cb(new Error('`maxDepth` ('+maxDepth+' exceeded!  There is probably a recursive loop in one of your generators.'));
		}
		return recursiveGenerate(target, scope, cb);
	});

}

module.exports = generateTarget;

/**
 * @param  {[type]} scope     [description]
 * @param  {[type]} subtarget [description]
 * @return {[type]}           [description]
 */
function mergeSubtargetScope (scope, subtarget) {
	return _.merge(scope, _.isObject(subtarget) ? subtarget : {});
}

/**
 * Known helpers
 * @type {Array}
 */
var KNOWN_HELPERS = ['exec', 'folder', 'template', 'jsonfile', 'file', 'copy'];
function targetIsHelper (target) {
	return _.some(target, function (subTarget, key) {
		return _.contains(KNOWN_HELPERS,key);
	});
}

/**
 *
 * @param  {[type]}   target      [description]
 * @param  {[type]}   scope [description]
 * @param  {Function} cb          [description]
 * @return {[type]}               [description]
 */
function parseTarget (target, scope, cb) {

	if (typeof target === 'string') {
		target = { generator: target };
	}

	// Interpret generator definition
	if (targetIsHelper(target)) {
		return cb(null, target);
	}

	if (target.generator) {

		// Normalize the subgenerator reference
		var subGeneratorRef;
		if (typeof target.generator === 'string') {
			subGeneratorRef = { module: target.generator };
		}
		else if (typeof target.generator === 'object') {
			subGeneratorRef = target.generator;
		}
		if (!subGeneratorRef) {
			return cb(new Error('Generator Error: Invalid subgenerator referenced for target "'+ scope.rootPath +'"'));
		}

		// Now normalize the sub-generator
		var subGenerator;
		if (!subGeneratorRef.module) {
			subGenerator = subGeneratorRef;
		}
		else {

			if (typeof subGeneratorRef.module === 'string') {
				// Lookup the generator by name if a `module` was specified
				// (this allows the module for a given generator to be
				//  overridden.)
				var configuredReference = scope.modules && scope.modules[subGeneratorRef.module];
				if (configuredReference) {
					return cb(null, configuredReference);
				}
			}

			// At this point, subGeneratorRef.module should be a string,
			// and the best guess at the generator module we're going
			// to get.
			var requireError;
			try {
				subGenerator = require(subGeneratorRef.module);
			}
			catch(e) {

				// If we didn't find that submodule, try "sails-generate-<module>"
				try {
					subGenerator = require('sails-generate-'+subGeneratorRef.module);
				}

				// Ok, we give up.
				catch(e) {
					requireError = e;
				}

			}

			// If we still can't find it, give up
			if (!subGenerator) {
				// TODO: look for subGeneratorRef on npm
				// TODO: emit a message to scope.output letting user know what's going on
				return cb(
					new Error('Generator Error: Failed to load "' +
					subGeneratorRef.module + '"...' +
					(requireError ? ' ('+requireError+')' : '')+
					'')
				);
			}

			return cb(null, subGenerator);
		}
	}

	return cb( new Error(
		'Unrecognized generator syntax in `targets["'+scope.keyPath+'"]` ::\n'+
		util.inspect(target))
	);
}

/**
 *
 * @param  {[type]}  target [description]
 * @return {Boolean}        [description]
 */
function isValidTarget (target) {
	var ok = true;

	ok = ok && typeof target === 'object';

	// Is using a helper
	// Or is another generator def.
	ok = ok && ( targetIsHelper(target) || _.has(target, 'targets') );

	return ok;
}
<|endoftext|>


<commit_before>const $ = require('jquery');
const { AppSetting } = require('../../core/app_setting');
const { Context } = require('../../core/context');
const { Field } = require('./field');
const { Form } = require('./form');
const { InternalSetting } = require('../../core/internal_setting');
const osLocale = require('os-locale');
const { Util } = require('../../core/util');

beforeEach(() => {
    Context.y18n.setLocale(osLocale.sync());
});

afterAll(() => {
    Context.y18n.setLocale(osLocale.sync());
});

test('Constructor initializes form fields', () => {
    let appSetting = new AppSetting({ name: 'fruit', value: 'apple' });
    appSetting.help = 'Help text';
    appSetting.errors['value'] = 'Value must be cherry.';
    let form = new Form('AppSetting', appSetting);
    expect(form.formId).toEqual('AppSettingForm');
    expect(form.inlineForms).toEqual([]);
    expect(Object.keys(form.fields).length).toEqual(4);

    // These are on the default exclude list.
    for (let name of ['errors', 'help', 'required']) {
        expect(form.fields[name]).not.toBeDefined();
    }

    for (let name of ['id', 'name', 'value', 'userCanDelete']) {
        let field = form.fields[name];
        let value = appSetting[name];
        expect(field).toBeDefined();
        expect(field.id).toEqual('appSettingForm_' + name);
        expect(field.name).toEqual(name);
        expect(field.label).toEqual(Util.camelToTitle(name));
        expect(field.value).toEqual(value);
        if (name === 'value') {
            expect(field.error).toEqual('Value must be cherry.');
        } else {
            expect(field.error).not.toBeDefined();
        }
    }
});

test('Constructor initializes form fields with custom exclude', () => {
    let appSetting = new AppSetting({ name: 'fruit', value: 'apple' });
    appSetting.help = 'Help text';
    appSetting.errors['value'] = 'Value must be cherry.';
    let form = new Form('AppSetting', appSetting, []);

    // We said exclude nothing this time, so we should
    // get fields for all properties.
    expect(Object.keys(form.fields).length).toEqual(7);
    for (let name of ['errors', 'help', 'required']) {
        expect(form.fields[name]).toBeDefined();
    }
});

test('setErrors()', () => {
    let appSetting = new AppSetting({ name: 'fruit', value: 'apple' });
    let form = new Form('AppSetting', appSetting);
    for (let name of ['id', 'name', 'value', 'userCanDelete']) {
        let field = form.fields[name];
        expect(field.error).not.toBeDefined();
    }

    form.obj.errors['id'] = 'id error'
    form.obj.errors['name'] = 'name error'
    form.obj.errors['value'] = 'value error';
    form.obj.errors['userCanDelete'] = 'userCanDelete error'

    form.setErrors();

    for (let name of ['id', 'name', 'value', 'userCanDelete']) {
        let field = form.fields[name];
        expect(field.error).toEqual(`${name} error`);
    }
});

test('castNewValueToType()', () => {
    let obj = {
        str: 'string 1',
        number: 1,
        bool: false,
        errors: {}   // required to build form
    };
    let form = new Form('objForm', obj);

    // Use toBe() for exact type matching.
    expect(form.castNewValueToType(obj.str, 'string 2')).toBe('string 2');
    expect(form.castNewValueToType(obj.number, '2')).toBe(2);
    expect(form.castNewValueToType(obj.bool, 'true')).toBe(true);
});

test('parseFromDOM()', () => {
    let appSetting = new AppSetting();
    let form = new Form('AppSetting', appSetting);

    document.body.innerHTML =
    '<form>' +
    '  <input type="text" id="AppSetting_name" value="Homer" />' +
    '  <input type="text" id="AppSetting_value" value="Simpson" />' +
    '  <input type="hidden" id="AppSetting_id" value="1234" />' +
    '  <input type="hidden" id="AppSetting_userCanDelete" value="false" />' +
    '</form>';

    form.parseFromDOM();

    // Use toBe() for exact type matching.
    expect(form.obj.name).toBe('Homer');
    expect(form.obj.value).toBe('Simpson');
    expect(form.obj.id).toBe('1234');
    expect(form.obj.userCanDelete).toBe(false);

    expect(form.changed.name.old).toEqual('');
    expect(form.changed.name.new).toEqual('Homer');
    expect(form.changed.value.old).toEqual('');
    expect(form.changed.value.new).toEqual('Simpson');
    expect(Util.looksLikeUUID(form.changed.id.old)).toBe(true);
    expect(form.changed.id.new).toEqual('1234');
    expect(form.changed.userCanDelete.old).toBe(true);
    expect(form.changed.userCanDelete.new).toBe(false);
});

test('_getLocalizedLabel()', () => {
    Context.y18n.setLocale('test_TEST');
    let internalSetting = new InternalSetting();
    let form = new Form('internalSettingForm', internalSetting);
    expect(form.fields.name.label).toEqual('Test Label');
});

test('_setRequired()', () => {
    let appSetting = new AppSetting();
    let form = new Form('AppSetting', appSetting);
    expect(form.fields.name.attrs.required).toEqual(true);
    expect(form.fields.value.attrs.required).not.toBeDefined();

    // Sanity check on our own tests
    expect(Context.y18n.locale).not.toEqual('test_TEST');
});

test('_setFieldHelpText()', () => {
    Context.y18n.setLocale('test_TEST');
    let internalSetting = new InternalSetting();
    let form = new Form('internalSettingForm', internalSetting);
    expect(form.fields.name.help).toEqual('Test help entry');

    // Special case for AppSetting
    let appSetting = new AppSetting();
    appSetting.help = 'I am what I am';
    form = new Form('AppSetting', appSetting);
    expect(form.fields.name.help).toEqual('I am what I am');

});
<commit_msg>Fixed forms test<commit_after>const $ = require('jquery');
const { AppSetting } = require('../../core/app_setting');
const { Context } = require('../../core/context');
const { Field } = require('./field');
const { Form } = require('./form');
const { InternalSetting } = require('../../core/internal_setting');
const osLocale = require('os-locale');
const { Util } = require('../../core/util');

beforeEach(() => {
    Context.y18n.setLocale(osLocale.sync());
});

afterAll(() => {
    Context.y18n.setLocale(osLocale.sync());
});

test('Constructor initializes form fields', () => {
    let appSetting = new AppSetting({ name: 'fruit', value: 'apple' });
    appSetting.help = 'Help text';
    appSetting.errors['value'] = 'Value must be cherry.';
    let form = new Form('AppSetting', appSetting);
    expect(form.formId).toEqual('appSettingForm');
    expect(form.inlineForms).toEqual([]);
    expect(Object.keys(form.fields).length).toEqual(4);

    // These are on the default exclude list.
    for (let name of ['errors', 'help', 'required']) {
        expect(form.fields[name]).not.toBeDefined();
    }

    for (let name of ['id', 'name', 'value', 'userCanDelete']) {
        let field = form.fields[name];
        let value = appSetting[name];
        expect(field).toBeDefined();
        expect(field.id).toEqual('appSettingForm_' + name);
        expect(field.name).toEqual(name);
        expect(field.label).toEqual(Util.camelToTitle(name));
        expect(field.value).toEqual(value);
        if (name === 'value') {
            expect(field.error).toEqual('Value must be cherry.');
        } else {
            expect(field.error).not.toBeDefined();
        }
    }
});

test('Constructor initializes form fields with custom exclude', () => {
    let appSetting = new AppSetting({ name: 'fruit', value: 'apple' });
    appSetting.help = 'Help text';
    appSetting.errors['value'] = 'Value must be cherry.';
    let form = new Form('AppSetting', appSetting, []);

    // We said exclude nothing this time, so we should
    // get fields for all properties.
    expect(Object.keys(form.fields).length).toEqual(7);
    for (let name of ['errors', 'help', 'required']) {
        expect(form.fields[name]).toBeDefined();
    }
});

test('setErrors()', () => {
    let appSetting = new AppSetting({ name: 'fruit', value: 'apple' });
    let form = new Form('AppSetting', appSetting);
    for (let name of ['id', 'name', 'value', 'userCanDelete']) {
        let field = form.fields[name];
        expect(field.error).not.toBeDefined();
    }

    form.obj.errors['id'] = 'id error'
    form.obj.errors['name'] = 'name error'
    form.obj.errors['value'] = 'value error';
    form.obj.errors['userCanDelete'] = 'userCanDelete error'

    form.setErrors();

    for (let name of ['id', 'name', 'value', 'userCanDelete']) {
        let field = form.fields[name];
        expect(field.error).toEqual(`${name} error`);
    }
});

test('castNewValueToType()', () => {
    let obj = {
        str: 'string 1',
        number: 1,
        bool: false,
        errors: {}   // required to build form
    };
    let form = new Form('objForm', obj);

    // Use toBe() for exact type matching.
    expect(form.castNewValueToType(obj.str, 'string 2')).toBe('string 2');
    expect(form.castNewValueToType(obj.number, '2')).toBe(2);
    expect(form.castNewValueToType(obj.bool, 'true')).toBe(true);
});

test('parseFromDOM()', () => {
    let appSetting = new AppSetting();
    let form = new Form('AppSetting', appSetting);

    document.body.innerHTML =
    '<form>' +
    '  <input type="text" id="appSettingForm_name" value="Homer" />' +
    '  <input type="text" id="appSettingForm_value" value="Simpson" />' +
    '  <input type="hidden" id="appSettingForm_id" value="1234" />' +
    '  <input type="hidden" id="appSettingForm_userCanDelete" value="false" />' +
    '</form>';

    form.parseFromDOM();

    // Use toBe() for exact type matching.
    expect(form.obj.name).toBe('Homer');
    expect(form.obj.value).toBe('Simpson');
    expect(form.obj.id).toBe('1234');
    expect(form.obj.userCanDelete).toBe(false);

    expect(form.changed.name.old).toEqual('');
    expect(form.changed.name.new).toEqual('Homer');
    expect(form.changed.value.old).toEqual('');
    expect(form.changed.value.new).toEqual('Simpson');
    expect(Util.looksLikeUUID(form.changed.id.old)).toBe(true);
    expect(form.changed.id.new).toEqual('1234');
    expect(form.changed.userCanDelete.old).toBe(true);
    expect(form.changed.userCanDelete.new).toBe(false);
});

test('_getLocalizedLabel()', () => {
    Context.y18n.setLocale('test_TEST');
    let internalSetting = new InternalSetting();
    let form = new Form('internalSettingForm', internalSetting);
    expect(form.fields.name.label).toEqual('Test Label');
});

test('_setRequired()', () => {
    let appSetting = new AppSetting();
    let form = new Form('AppSetting', appSetting);
    expect(form.fields.name.attrs.required).toEqual(true);
    expect(form.fields.value.attrs.required).not.toBeDefined();

    // Sanity check on our own tests
    expect(Context.y18n.locale).not.toEqual('test_TEST');
});

test('_setFieldHelpText()', () => {
    Context.y18n.setLocale('test_TEST');
    let internalSetting = new InternalSetting();
    let form = new Form('internalSettingForm', internalSetting);
    expect(form.fields.name.help).toEqual('Test help entry');

    // Special case for AppSetting
    let appSetting = new AppSetting();
    appSetting.help = 'I am what I am';
    form = new Form('AppSetting', appSetting);
    expect(form.fields.name.help).toEqual('I am what I am');

});
<|endoftext|>


<commit_before>'use strict';

var debug = require('diagnostics')('fever')
  , EventEmitter = require('eventemitter3')
  , TickTock = require('tick-tock')
  , destroy = require('demolish')
  , HotPath = require('hotpath')
  , parse = require('url').parse
  , fuse = require('fusing')
  , File = require('./file');

/**
 * Our file factory.
 *
 * Options:
 *
 * - hotpath, {Object}, Configuration for our hotpath cache.
 * - engine, {FileSystem}, The file system where we store and get our files.
 *
 * @constructor
 * @param {Object} options Fever configuration.
 * @api private
 */
function Fever(options) {
  if (!this) return new Fever(options);

  var selfie = this;
  options = options || {};

  this.fs = options.engine || require('supreme');   // File system like API.
  this.hotpath = new HotPath(options.hotpath);      // Internal cache system.
  this.timers = new TickTock(this);                 // Timer management.
  this.options = options;                           // Backup of the options.
  this.files = [];                                  // Active files.

  //
  // Expose the File constructor which now contains a reference to the newly
  // created Fever.
  //
  this.File = function File(path, options) {
    this.fuse([selfie, path, options]);
  };

  fuse(this.File, File);
  this.initialize();
}

//
// Supply provides our middleware and plugin system, so we're going to inherit
// from it.
//
Fever.prototype.__proto__ = require('supply').prototype;
Object.keys(EventEmitter.prototype).forEach(function each(key) {
  Fever.prototype[key] = EventEmitter.prototype[key];
});

/**
 * Initialize the Fever instance and initialize all the things.
 *
 * @api private
 */
Fever.prototype.initialize = function initialize() {
  this.on('add', function add(file) {
    this.files.push(file);
  });

  this.on('remove', function remove(file) {
    var index = this.files.indexOf(file);

    if (index === -1) return;
    this.files.splice(index, 0);
  });
};

/**
 * Handy helper function for creating optional callbacks.
 *
 * @param {Function} fn Optional callback.
 * @param {String} msg Optional failure message.
 * @returns {Function}
 * @api private
 */
Fever.prototype.optional = function optional(fn, msg) {
  return fn || function nope(err) {
    if (err) debug(msg || 'Missing callback for failed operation', err);
  };
};

/**
 * Replace the internal file system.
 *
 * @param {FileSystem} fs The file system that we should use for the files.
 * @returns {Fever}
 * @api public
 */
Fever.prototype.engine = function engine(fs) {
  this.fs = fs;

  return this;
};

/**
 * Get the files that use this path.
 *
 * @param {String} path location of file.
 * @returns {Array}
 * @api private
 */
Fever.prototype.get = function get(path) {
  return [];
};

/**
 * Add a new file to our collection, only if it doesn't exist before.
 *
 * @param {String} path Location of the file.
 * @param {Object} options File configuration.
 * @returns {Fever}
 * @api public
 */
Fever.prototype.add = function add(path, options) {
  if (this.files.some(function some(file) {
    return file.contains(path);
  })) return this;

  new this.File(path, options);

  return this;
};

/**
 * Re-cache the hot cache with the most requested files from this system.
 * Every time a file is requested we increment the requested count.
 *
 * @param {Function} fn Optional completion callback.
 * @returns {Fever}
 * @api public
 */
Fever.prototype.cache = function cache(fn) {
  fn = this.optional(fn, 'Failed to update the cache');

  var sorted = this.files.sort(function sort(a, b) {
    return a.requested - b.requested;
  });

  //
  // Clear the hot cache as we're about to refill it's contents with new and
  // potentially hotter code paths.
  //
  this.hotpath.reset();

  sorted.forEach(function forEach(file) {
    return this.hotpath.set(file.uuid, file.buffer());
  }, this);

  return this;
};

/**
 * Attempt to mount the factory to a given HTTP server instance. If no server is
 * given we will return a middleware layer that is compatible with connect.
 *
 * @param {HTTPServer} server A HTTP server which understands middleware.
 * @returns {Function} The middleware layer we've generated.
 * @api public
 */
Fever.prototype.mount = function mount(server, options) {
  var selfie = this;

  function fever(req, res, next) {
    req.uri = req.uri || parse(req.url);

    var file = selfie.alias(req.uri.pathname);
    if (!file || 'GET' !== req.method) return next();

    file.forward(res, { gzip: req.zipline, headers: req.headers });
  }

  if (server && 'function' === typeof server.use) {
    server.use('zipline', require('zipline'), options);
    server.use('fever', fever);
  }

  return fever;
};

/**
 * Destroy and clean-up things.
 *
 * @TODO cleanup Supply based data.
 * @TODO cleanup EventEmitter properties.
 *
 * @type {Function}
 * @returns {Boolean}
 * @api public
 */
Fever.prototype.destroy = destroy('fs, hotpath, timers, options, files');

//
// Expose the module.
//
Fever.File = File;
module.exports = Fever;
<commit_msg>[fix] Correctly inherit from Supply, it was missing .layer properties. [minor] Track amount of requests handled so we can trigger a re-cache. [minor] Ignore files that don't have a raw length. [minor] Adding headers to the outgoing response [minor] Renamed `gzip` property to `encoding`<commit_after>'use strict';

var debug = require('diagnostics')('fever')
  , EventEmitter = require('eventemitter3')
  , TickTock = require('tick-tock')
  , destroy = require('demolish')
  , HotPath = require('hotpath')
  , parse = require('url').parse
  , Supply = require('supply')
  , fuse = require('fusing')
  , File = require('./file');

/**
 * Our file factory.
 *
 * Options:
 *
 * - hotpath, {Object}, Configuration for our hotpath cache.
 * - engine, {FileSystem}, The file system where we store and get our files.
 * - recache, {Number} Every x amount of requests we should try to re-cache files.
 *
 * @constructor
 * @param {Object} options Fever configuration.
 * @api private
 */
function Fever(options) {
  if (!this) return new Fever(options);
  Supply.call(this);

  var selfie = this;
  options = options || {};

  this.fs = options.engine || require('supreme');   // File system like API.
  this.dir = options.directory || __dirname;        // Asset compile directory.
  this.hotpath = new HotPath(options.hotpath);      // Internal cache system.
  this.recache = options.recache || 10000;          // Amount of requests to trigger re-cache.
  this.timers = new TickTock(this);                 // Timer management.
  this.options = options;                           // Backup of the options.
  this.requested = 0;                               // Amount of requests we handled.
  this.files = [];                                  // Active files.

  //
  // Expose the File constructor which now contains a reference to the newly
  // created Fever.
  //
  this.File = function File(path, options) {
    this.fuse([selfie, path, options]);
  };

  fuse(this.File, File);
  this.initialize();
}

//
// Supply provides our middleware and plugin system, so we're going to inherit
// from it.
//
Fever.prototype.__proto__ = Supply.prototype;
Fever.prototype.emits = require('emits');
Object.keys(EventEmitter.prototype).forEach(function each(key) {
  Fever.prototype[key] = EventEmitter.prototype[key];
});

/**
 * Initialize the Fever instance and initialize all the things.
 *
 * @api private
 */
Fever.prototype.initialize = function initialize() {
  this.on('add', function add(file) {
    this.files.push(file);
  });

  this.on('remove', function remove(file) {
    var index = this.files.indexOf(file);

    if (index === -1) return;
    this.files.splice(index, 0);
  });
};

/**
 * Handy helper function for creating optional callbacks.
 *
 * @param {Function} fn Optional callback.
 * @param {String} msg Optional failure message.
 * @returns {Function}
 * @api private
 */
Fever.prototype.optional = function optional(fn, msg) {
  return fn || function nope(err) {
    if (err) debug(msg || 'Missing callback for failed operation', err);
  };
};

/**
 * Replace the internal file system.
 *
 * @param {FileSystem} fs The file system that we should use for the files.
 * @returns {Fever}
 * @api public
 */
Fever.prototype.engine = function engine(fs) {
  this.fs = fs;

  return this;
};

/**
 * Get the files that use this path.
 *
 * @param {String} path location of file.
 * @returns {Array}
 * @api private
 */
Fever.prototype.get = function get(path) {
  return [];
};

/**
 * Add a new file to our collection, only if it doesn't exist before.
 *
 * @param {String} path Location of the file.
 * @param {Object} options File configuration.
 * @returns {Fever}
 * @api public
 */
Fever.prototype.add = function add(path, options) {
  if (this.files.some(function some(file) {
    return file.contains(path);
  })) return this;

  new this.File(path, options);

  return this;
};

/**
 * Re-cache the hot cache with the most requested files from this system.
 * Every time a file is requested we increment the requested count.
 *
 * @param {Function} fn Optional completion callback.
 * @returns {Fever}
 * @api public
 */
Fever.prototype.cache = function cache(fn) {
  fn = this.optional(fn, 'Failed to update the cache');

  var sorted = this.files.sort(function sort(a, b) {
    return a.requested - b.requested;
  });

  //
  // Clear the hot cache as we're about to refill it's contents with new and
  // potentially hotter code paths.
  //
  this.hotpath.reset();

  sorted.forEach(function forEach(file) {
    return this.hotpath.set(file.uuid, file.buffer());
  }, this);

  return this;
};

/**
 * Attempt to mount the factory to a given HTTP server instance. If no server is
 * given we will return a middleware layer that is compatible with connect.
 *
 * @param {HTTPServer} server A HTTP server which understands middleware.
 * @returns {Function} The middleware layer we've generated.
 * @api public
 */
Fever.prototype.mount = function mount(server, options) {
  var selfie = this;

  /**
   * Handle the incoming files.
   *
   * @param {Request} req Incoming HTTP request.
   * @param {Response} res Outgoing HTTP response.
   * @param {Function} next Continuation of middleware.
   * @api private
   */
  function fever(req, res, next) {
    req.uri = req.uri || parse(req.url);

    var file = selfie.alias(req.uri.pathname)
      , cache;

    if (
         !file
      || 'GET' !== req.method
      || !file.length.raw
    ) return next();

    //
    // As we can handle the file we need to check if we should start a re-cache
    // procedure so our most requested files can float around in memory. And we
    // want to make sure that we're serving up to date assets from our hot cache.
    //
    selfie.requested++;

    if (selfie.request % selfie.recache === 0) {
      selfie.cache();
    }

    file.setHeader(req, res);
    cache = selfie.hotpath.get(req.uri.pathname);
    if (cache) return res.end(cache);

    file.forward(res, {
      encoding: req.zipline,
      headers: req.headers
    });
  }

  if (server && 'function' === typeof server.use) {
    server.use('zipline', require('zipline'), options);
    server.use('fever', fever);
  }

  return fever;
};

/**
 * Destroy and clean-up things.
 *
 * @TODO cleanup Supply based data.
 * @TODO cleanup EventEmitter properties.
 * @TODO clean up all files.
 *
 * @type {Function}
 * @returns {Boolean}
 * @api public
 */
Fever.prototype.destroy = destroy('fs, hotpath, timers, options, files');

//
// Expose the module.
//
Fever.File = File;
module.exports = Fever;
<|endoftext|>