<commit_before>const DATA_ATTR = 'clicktrack';

/*
 * This module provides utilities for handling browser-based click event data,
 * including the dispatch of corresponding Redux actions
 */

// set reference to un-modified stopPropagation
const originalStopPropagation =
	typeof Event !== 'undefined' && Event.prototype.stopPropagation;

/**
 * Overriding stopPropagation for maximum click tracking
 * @param {Function} trackClick function to track clicks
 * @return {undefined} side effects only
 */
export function trackStopPropagation(trackClick) {
	/**
	 * A special Event object method that hooks the click tracking into events
	 * that would never bubble to document.body
	 */
	Event.prototype.stopPropAndTrack = function() {
		trackClick(this);
		return originalStopPropagation.call(this);
	};

	// override stopPropagation to track clicks
	Event.prototype.stopPropagation = function() {
		if (this.type === 'click') {
			trackClick(this);
		}
		return originalStopPropagation.call(this);
	};
}

/**
 * Get a descriptive string for the HTML element passed in, e.g.
 *
 * `tagName#id.class1.class2`
 *
 * @param {HTMLElement} element the element to describe
 * @param {Boolean} idOnly only use id in description
 */
function elementShorthand(element, idOnly) {
	const id = element.id ? `#${element.id}` : '';
	if (id && idOnly) {
		return id;
	}
	const tag = element.tagName.toLowerCase();
	const classes = `.${[...element.classList].join('.')}`;

	return `${tag}${id}${classes}`;
}

function _getData(e) {
	const target = e.target;
	const useChildData =
		e.type === 'change' && target.tagName.toLowerCase() === 'select';
	const el = useChildData ? target.children[target.selectedIndex] : target;

	try {
		const data = (el.dataset || {})[DATA_ATTR] || '{}';
		return JSON.parse(data);
	} catch (err) {
		// data is not JSON-formatted
		return {};
	}
}

function getTrackClick() {
	/**
	 * Event handler that emits data about each in-page click
	 *
	 * metadata: {
	 *   "lineage": <String representing 'this' location in DOM>,
	 *   "linkText": <this.text>,
	 *   "coords": [x offset from midline (elinate screen-width dependency, y (top) offset],
	 *   "data": { data on clicked element matching the tracking suffix }
	 * }
	 *
	 * @param  {Event} e the click (or other) DOM event
	 * @return {Object} a Redux action describing the click
	 */
	function trackClick(e) {
		const el = e.target;
		const tagName = el.tagName.toLowerCase();
		if (tagName !== 'a' && tagName !== 'button' && !el.dataset[DATA_ATTR]) {
			// ignore all events on non-anchor/button elements that do not have data-clicktrack
			return;
		}

		const linkText = el.textContent
			.trim()
			.substring(0, 16) // truncate if text is long
			.replace(/\s{2,}/g, ' ');
		const clickLineage = [];
		const targetPosition = el.getBoundingClientRect();
		const docMidlineOffset = document.body.clientWidth / 2;
		const x = Math.round(targetPosition.left - docMidlineOffset);
		const y = Math.round(targetPosition.top);
		const data = _getData(e);

		// 1. Build array of DOM tag lineage
		clickLineage.push(elementShorthand(el)); // full shorthand for clicked el
		var currentEl = el;
		while ((currentEl = currentEl.parentNode) && currentEl !== document.body) {
			clickLineage.push(elementShorthand(currentEl, true)); // id/tag only for parents
		}

		// 2. Create click action with metadata
		const now = new Date();
		return {
			timestamp: now.toISOString(),
			lineage: clickLineage.join('<'),
			linkText: linkText,
			coords: [x, y],
			data: data,
		};
	}

	// modify window.Event prototype
	trackStopPropagation(trackClick);

	return trackClick;
}

export default getTrackClick;
<commit_msg>try/catch around click parser<commit_after>const DATA_ATTR = 'clicktrack';

/*
 * This module provides utilities for handling browser-based click event data,
 * including the dispatch of corresponding Redux actions
 */

// set reference to un-modified stopPropagation
const originalStopPropagation =
	typeof Event !== 'undefined' && Event.prototype.stopPropagation;

/**
 * Overriding stopPropagation for maximum click tracking
 * @param {Function} trackClick function to track clicks
 * @return {undefined} side effects only
 */
export function trackStopPropagation(trackClick) {
	/**
	 * A special Event object method that hooks the click tracking into events
	 * that would never bubble to document.body
	 */
	Event.prototype.stopPropAndTrack = function() {
		trackClick(this);
		return originalStopPropagation.call(this);
	};

	// override stopPropagation to track clicks
	Event.prototype.stopPropagation = function() {
		if (this.type === 'click') {
			trackClick(this);
		}
		return originalStopPropagation.call(this);
	};
}

/**
 * Get a descriptive string for the HTML element passed in, e.g.
 *
 * `tagName#id.class1.class2`
 *
 * @param {HTMLElement} element the element to describe
 * @param {Boolean} idOnly only use id in description
 */
function elementShorthand(element, idOnly) {
	const id = element.id ? `#${element.id}` : '';
	if (id && idOnly) {
		return id;
	}
	const tag = element.tagName.toLowerCase();
	const classes = `.${[...element.classList].join('.')}`;

	return `${tag}${id}${classes}`;
}

function _getData(e) {
	const target = e.target;
	const useChildData =
		e.type === 'change' && target.tagName.toLowerCase() === 'select';
	const el = useChildData ? target.children[target.selectedIndex] : target;

	try {
		const data = (el.dataset || {})[DATA_ATTR] || '{}';
		return JSON.parse(data);
	} catch (err) {
		// data is not JSON-formatted
		return {};
	}
}

function getTrackClick() {
	/**
	 * Event handler that emits data about each in-page click
	 *
	 * metadata: {
	 *   "lineage": <String representing 'this' location in DOM>,
	 *   "linkText": <this.text>,
	 *   "coords": [x offset from midline (elinate screen-width dependency, y (top) offset],
	 *   "data": { data on clicked element matching the tracking suffix }
	 * }
	 *
	 * @param  {Event} e the click (or other) DOM event
	 * @return {Object} a Redux action describing the click
	 */
	function trackClick(e) {
		try {
			const el = e.target;
			const tagName = el.tagName.toLowerCase();
			if (tagName !== 'a' && tagName !== 'button' && !el.dataset[DATA_ATTR]) {
				// ignore all events on non-anchor/button elements that do not have data-clicktrack
				return;
			}

			const linkText = el.textContent
				.trim()
				.substring(0, 16) // truncate if text is long
				.replace(/\s{2,}/g, ' ');
			const clickLineage = [];
			const targetPosition = el.getBoundingClientRect();
			const docMidlineOffset = document.body.clientWidth / 2;
			const x = Math.round(targetPosition.left - docMidlineOffset);
			const y = Math.round(targetPosition.top);
			const data = _getData(e);

			// 1. Build array of DOM tag lineage
			clickLineage.push(elementShorthand(el)); // full shorthand for clicked el
			var currentEl = el;
			while (
				(currentEl = currentEl.parentNode) &&
				currentEl !== document.body
			) {
				clickLineage.push(elementShorthand(currentEl, true)); // id/tag only for parents
			}

			// 2. Create click action with metadata
			const now = new Date(); // TODO: make this a America/New_York timestamp
			return {
				timestamp: now.toISOString(),
				lineage: clickLineage.join('<'),
				linkText: linkText,
				coords: [x, y],
				data: data,
			};
		} catch (err) {
			console.error(err);
		}
	}

	// modify window.Event prototype
	trackStopPropagation(trackClick);

	return trackClick;
}

export default getTrackClick;
<|endoftext|>


<commit_before>import { applyMiddleware, createStore } from 'redux'
import reducer from '../../src/reducer'

export default ({ middleware, initialState = {} }) =>
  createStore(reducer, initialState, applyMiddleware(middleware))
<commit_msg>Use combine reducers in test store<commit_after>import { applyMiddleware, combineReducers, createStore } from 'redux'
import reducer from '../../src/reducer'

export default ({ middleware, initialState = {} }) =>
  createStore(
    combineReducers({ session: reducer }),
    initialState,
    applyMiddleware(middleware)
  )
<|endoftext|>


<commit_before>'use strict';

/**
 * GraphQL.js service
 *
 * @description: A set of functions similar to controller's actions to avoid code duplication.
 */

const _ = require('lodash');
const pluralize = require('pluralize');
const Aggregator = require('./Aggregator');
const Query = require('./Query.js');
const Mutation = require('./Mutation.js');
const Types = require('./Types.js');
const Schema = require('./Schema.js');

module.exports = {
  /**
   * Construct the GraphQL query & definition and apply the right resolvers.
   *
   * @return Object
   */

  shadowCRUD: function(models, plugin) {
    // Retrieve generic service from the Content Manager plugin.
    const resolvers =
      strapi.plugins['content-manager'].services['contentmanager'];

    const initialState = {
      definition: '',
      query: {},
      mutation: {},
      resolver: { Query: {}, Mutation: {} },
    };

    if (_.isEmpty(models)) {
      return initialState;
    }

    return models.reduce((acc, name) => {
      const model = plugin
        ? strapi.plugins[plugin].models[name]
        : strapi.models[name];

      // Setup initial state with default attribute that should be displayed
      // but these attributes are not properly defined in the models.
      const initialState = {
        [model.primaryKey]: 'ID!',
      };

      const globalId = model.globalId;
      const _schema = _.cloneDeep(
        _.get(strapi.plugins, 'graphql.config._schema.graphql', {}),
      );

      if (!acc.resolver[globalId]) {
        acc.resolver[globalId] = {};
      }

      // Add timestamps attributes.
      if (_.get(model, 'options.timestamps') === true) {
        Object.assign(initialState, {
          createdAt: 'DateTime!',
          updatedAt: 'DateTime!',
        });

        Object.assign(acc.resolver[globalId], {
          createdAt: (obj, options, context) => {
            // eslint-disable-line no-unused-vars
            return obj.createdAt || obj.created_at;
          },
          updatedAt: (obj, options, context) => {
            // eslint-disable-line no-unused-vars
            return obj.updatedAt || obj.updated_at;
          },
        });
      }

      // Retrieve user customisation.
      const { type = {}, resolver = {} } = _schema;

      // Convert our layer Model to the GraphQL DL.
      const attributes = Object.keys(model.attributes)
        .filter(attribute => model.attributes[attribute].private !== true)
        .reduce((acc, attribute) => {
          // Convert our type to the GraphQL type.
          acc[attribute] = Types.convertType({
            definition: model.attributes[attribute],
            modelName: globalId,
            attributeName: attribute,
          });

          return acc;
        }, initialState);

      // Detect enum and generate it for the schema definition
      const enums = Object.keys(model.attributes)
        .filter(attribute => model.attributes[attribute].type === 'enumeration')
        .map(attribute => {
          const definition = model.attributes[attribute];

          return `enum ${Types.convertEnumType(
            definition,
            globalId,
            attribute,
          )} { ${definition.enum.join(' \n ')} }`;
        })
        .join(' ');

      acc.definition += enums;

      // Add parameters to optimize association query.
      (model.associations || [])
        .filter(association => association.type === 'collection')
        .forEach(association => {
          attributes[
            `${
              association.alias
            }(sort: String, limit: Int, start: Int, where: JSON)`
          ] = attributes[association.alias];

          delete attributes[association.alias];
        });

      acc.definition += `${Schema.getDescription(
        type[globalId],
        model,
      )}type ${globalId} {${Schema.formatGQL(
        attributes,
        type[globalId],
        model,
      )}}\n\n`;

      // Add definition to the schema but this type won't be "queriable" or "mutable".
      if (
        type[model.globalId] === false ||
        _.get(type, `${model.globalId}.enabled`) === false
      ) {
        return acc;
      }

      // Build resolvers.
      const queries = {
        singular:
          _.get(resolver, `Query.${pluralize.singular(name)}`) !== false
            ? Query.composeQueryResolver(_schema, plugin, name, true)
            : null,
        plural:
          _.get(resolver, `Query.${pluralize.plural(name)}`) !== false
            ? Query.composeQueryResolver(_schema, plugin, name, false)
            : null,
      };

      Object.keys(queries).forEach(type => {
        // The query cannot be built.
        if (_.isError(queries[type])) {
          console.error(queries[type]);
          strapi.stop();
        }

        // Only create query if the function is available.
        if (_.isFunction(queries[type])) {
          if (type === 'singular') {
            Object.assign(acc.query, {
              [`${pluralize.singular(name)}(id: ID!)`]: model.globalId,
            });
          } else {
            Object.assign(acc.query, {
              [`${pluralize.plural(
                name,
              )}(sort: String, limit: Int, start: Int, where: JSON)`]: `[${
                model.globalId
              }]`,
            });
          }

          _.merge(acc.resolver.Query, {
            [type === 'singular'
              ? pluralize.singular(name)
              : pluralize.plural(name)]: queries[type],
          });
        }
      });

      // TODO:
      // - Implement batch methods (need to update the content-manager as well).
      // - Implement nested transactional methods (create/update).
      const mutations = {
        create:
          _.get(resolver, `Mutation.create${_.capitalize(name)}`) !== false
            ? Mutation.composeMutationResolver(_schema, plugin, name, 'create')
            : null,
        update:
          _.get(resolver, `Mutation.update${_.capitalize(name)}`) !== false
            ? Mutation.composeMutationResolver(_schema, plugin, name, 'update')
            : null,
        delete:
          _.get(resolver, `Mutation.delete${_.capitalize(name)}`) !== false
            ? Mutation.composeMutationResolver(_schema, plugin, name, 'delete')
            : null,
      };

      // Add model Input definition.
      acc.definition += Types.generateInputModel(model, name);

      Object.keys(mutations).forEach(type => {
        if (_.isFunction(mutations[type])) {
          let mutationDefinition;
          let mutationName = `${type}${_.capitalize(name)}`;

          // Generate the Input for this specific action.
          acc.definition += Types.generateInputPayloadArguments(
            model,
            name,
            type,
          );

          switch (type) {
            case 'create':
              mutationDefinition = {
                [`${mutationName}(input: ${mutationName}Input)`]: `${mutationName}Payload`,
              };

              break;
            case 'update':
              mutationDefinition = {
                [`${mutationName}(input: ${mutationName}Input)`]: `${mutationName}Payload`,
              };

              break;
            case 'delete':
              mutationDefinition = {
                [`${mutationName}(input: ${mutationName}Input)`]: `${mutationName}Payload`,
              };
              break;
            default:
            // Nothing.
          }

          // Assign mutation definition to global definition.
          Object.assign(acc.mutation, mutationDefinition);

          // Assign resolver to this mutation and merge it with the others.
          _.merge(acc.resolver.Mutation, {
            [`${mutationName}`]: mutations[type],
          });
        }
      });

      // TODO:
      // - Add support for Graphql Aggregation in Bookshelf ORM
      if (model.orm === 'mongoose') {
        // Generation the aggregation for the given model
        const modelAggregator = Aggregator.formatModelConnectionsGQL(
          attributes,
          model,
          name,
          queries.plural,
        );
        if (modelAggregator) {
          acc.definition += modelAggregator.type;
          if (!acc.resolver[modelAggregator.globalId]) {
            acc.resolver[modelAggregator.globalId] = {};
          }

          _.merge(acc.resolver, modelAggregator.resolver);
          _.merge(acc.query, modelAggregator.query);
        }
      }

      // Build associations queries.
      (model.associations || []).forEach(association => {
        switch (association.nature) {
          case 'oneToManyMorph':
            return _.merge(acc.resolver[globalId], {
              [association.alias]: async obj => {
                const withRelated = await resolvers.fetch(
                  {
                    id: obj[model.primaryKey],
                    model: name,
                  },
                  plugin,
                  [association.alias],
                  false,
                );

                const entry =
                  withRelated && withRelated.toJSON
                    ? withRelated.toJSON()
                    : withRelated;

                // Set the _type only when the value is defined
                if (entry[association.alias]) {
                  entry[association.alias]._type = _.upperFirst(
                    association.model,
                  );
                }

                return entry[association.alias];
              },
            });
          case 'manyMorphToOne':
          case 'manyMorphToMany':
          case 'manyToManyMorph':
            return _.merge(acc.resolver[globalId], {
              [association.alias]: async (obj, options, context) => {
                // eslint-disable-line no-unused-vars
                const [withRelated, withoutRelated] = await Promise.all([
                  resolvers.fetch(
                    {
                      id: obj[model.primaryKey],
                      model: name,
                    },
                    plugin,
                    [association.alias],
                    false,
                  ),
                  resolvers.fetch(
                    {
                      id: obj[model.primaryKey],
                      model: name,
                    },
                    plugin,
                    [],
                  ),
                ]);

                const entry =
                  withRelated && withRelated.toJSON
                    ? withRelated.toJSON()
                    : withRelated;

                // TODO:
                // - Handle sort, limit and start (lodash or inside the query)
                entry[association.alias].map((entry, index) => {
                  const type =
                    _.get(
                      withoutRelated,
                      `${association.alias}.${index}.kind`,
                    ) ||
                    _.upperFirst(
                      _.camelCase(
                        _.get(
                          withoutRelated,
                          `${association.alias}.${index}.${
                            association.alias
                          }_type`,
                        ),
                      ),
                    ) ||
                    _.upperFirst(_.camelCase(association[association.type]));

                  entry._type = type;

                  return entry;
                });

                return entry[association.alias];
              },
            });
          default:
        }

        _.merge(acc.resolver[globalId], {
          [association.alias]: async (obj, options, context) => {
            // eslint-disable-line no-unused-vars
            // Construct parameters object to retrieve the correct related entries.
            const params = {
              model: association.model || association.collection,
            };

            const queryOpts = {
              source: association.plugin,
            };

            if (association.type === 'model') {
              params.id = obj[association.alias];
            } else {
              // Get refering model.
              const ref = association.plugin
                ? strapi.plugins[association.plugin].models[params.model]
                : strapi.models[params.model];

              // Apply optional arguments to make more precise nested request.
              const convertedParams = strapi.utils.models.convertParams(
                name,
                Query.convertToParams(Query.amountLimiting(options)),
              );
              const where = strapi.utils.models.convertParams(
                name,
                options.where || {},
              );

              // Limit, order, etc.
              Object.assign(queryOpts, convertedParams);

              // Skip.
              queryOpts.skip = convertedParams.start;

              switch (association.nature) {
                case 'manyToMany': {
                  if (association.dominant) {
                    const arrayOfIds = (obj[association.alias] || []).map(
                      related => {
                        return related[ref.primaryKey] || related;
                      },
                    );

                    // Where.
                    queryOpts.query = strapi.utils.models.convertParams(name, {
                      // Construct the "where" query to only retrieve entries which are
                      // related to this entry.
                      [ref.primaryKey]: arrayOfIds,
                      ...where.where,
                    }).where;
                  }
                  break;
                  // falls through
                }
                default:
                  // Where.
                  queryOpts.query = strapi.utils.models.convertParams(name, {
                    // Construct the "where" query to only retrieve entries which are
                    // related to this entry.
                    [association.via]: obj[ref.primaryKey],
                    ...where.where,
                  }).where;
              }
            }

            const value = await (association.model
              ? resolvers.fetch(params, association.plugin, [])
              : resolvers.fetchAll(params, { ...queryOpts, populate: [] }));

            return value && value.toJSON ? value.toJSON() : value;
          },
        });
      });

      return acc;
    }, initialState);
  },
};
<commit_msg>Convert GraphQL params to one level-deep object<commit_after>'use strict';

/**
 * GraphQL.js service
 *
 * @description: A set of functions similar to controller's actions to avoid code duplication.
 */

const _ = require('lodash');
const pluralize = require('pluralize');
const Aggregator = require('./Aggregator');
const Query = require('./Query.js');
const Mutation = require('./Mutation.js');
const Types = require('./Types.js');
const Schema = require('./Schema.js');

module.exports = {
  /**
   * Construct the GraphQL query & definition and apply the right resolvers.
   *
   * @return Object
   */

  shadowCRUD: function(models, plugin) {
    // Retrieve generic service from the Content Manager plugin.
    const resolvers =
      strapi.plugins['content-manager'].services['contentmanager'];

    const initialState = {
      definition: '',
      query: {},
      mutation: {},
      resolver: { Query: {}, Mutation: {} },
    };

    if (_.isEmpty(models)) {
      return initialState;
    }

    return models.reduce((acc, name) => {
      const model = plugin
        ? strapi.plugins[plugin].models[name]
        : strapi.models[name];

      // Setup initial state with default attribute that should be displayed
      // but these attributes are not properly defined in the models.
      const initialState = {
        [model.primaryKey]: 'ID!',
      };

      const globalId = model.globalId;
      const _schema = _.cloneDeep(
        _.get(strapi.plugins, 'graphql.config._schema.graphql', {}),
      );

      if (!acc.resolver[globalId]) {
        acc.resolver[globalId] = {};
      }

      // Add timestamps attributes.
      if (_.get(model, 'options.timestamps') === true) {
        Object.assign(initialState, {
          createdAt: 'DateTime!',
          updatedAt: 'DateTime!',
        });

        Object.assign(acc.resolver[globalId], {
          createdAt: (obj, options, context) => {
            // eslint-disable-line no-unused-vars
            return obj.createdAt || obj.created_at;
          },
          updatedAt: (obj, options, context) => {
            // eslint-disable-line no-unused-vars
            return obj.updatedAt || obj.updated_at;
          },
        });
      }

      // Retrieve user customisation.
      const { type = {}, resolver = {} } = _schema;

      // Convert our layer Model to the GraphQL DL.
      const attributes = Object.keys(model.attributes)
        .filter(attribute => model.attributes[attribute].private !== true)
        .reduce((acc, attribute) => {
          // Convert our type to the GraphQL type.
          acc[attribute] = Types.convertType({
            definition: model.attributes[attribute],
            modelName: globalId,
            attributeName: attribute,
          });

          return acc;
        }, initialState);

      // Detect enum and generate it for the schema definition
      const enums = Object.keys(model.attributes)
        .filter(attribute => model.attributes[attribute].type === 'enumeration')
        .map(attribute => {
          const definition = model.attributes[attribute];

          return `enum ${Types.convertEnumType(
            definition,
            globalId,
            attribute,
          )} { ${definition.enum.join(' \n ')} }`;
        })
        .join(' ');

      acc.definition += enums;

      // Add parameters to optimize association query.
      (model.associations || [])
        .filter(association => association.type === 'collection')
        .forEach(association => {
          attributes[
            `${
              association.alias
            }(sort: String, limit: Int, start: Int, where: JSON)`
          ] = attributes[association.alias];

          delete attributes[association.alias];
        });

      acc.definition += `${Schema.getDescription(
        type[globalId],
        model,
      )}type ${globalId} {${Schema.formatGQL(
        attributes,
        type[globalId],
        model,
      )}}\n\n`;

      // Add definition to the schema but this type won't be "queriable" or "mutable".
      if (
        type[model.globalId] === false ||
        _.get(type, `${model.globalId}.enabled`) === false
      ) {
        return acc;
      }

      // Build resolvers.
      const queries = {
        singular:
          _.get(resolver, `Query.${pluralize.singular(name)}`) !== false
            ? Query.composeQueryResolver(_schema, plugin, name, true)
            : null,
        plural:
          _.get(resolver, `Query.${pluralize.plural(name)}`) !== false
            ? Query.composeQueryResolver(_schema, plugin, name, false)
            : null,
      };

      Object.keys(queries).forEach(type => {
        // The query cannot be built.
        if (_.isError(queries[type])) {
          console.error(queries[type]);
          strapi.stop();
        }

        // Only create query if the function is available.
        if (_.isFunction(queries[type])) {
          if (type === 'singular') {
            Object.assign(acc.query, {
              [`${pluralize.singular(name)}(id: ID!)`]: model.globalId,
            });
          } else {
            Object.assign(acc.query, {
              [`${pluralize.plural(
                name,
              )}(sort: String, limit: Int, start: Int, where: JSON)`]: `[${
                model.globalId
              }]`,
            });
          }

          _.merge(acc.resolver.Query, {
            [type === 'singular'
              ? pluralize.singular(name)
              : pluralize.plural(name)]: queries[type],
          });
        }
      });

      // TODO:
      // - Implement batch methods (need to update the content-manager as well).
      // - Implement nested transactional methods (create/update).
      const mutations = {
        create:
          _.get(resolver, `Mutation.create${_.capitalize(name)}`) !== false
            ? Mutation.composeMutationResolver(_schema, plugin, name, 'create')
            : null,
        update:
          _.get(resolver, `Mutation.update${_.capitalize(name)}`) !== false
            ? Mutation.composeMutationResolver(_schema, plugin, name, 'update')
            : null,
        delete:
          _.get(resolver, `Mutation.delete${_.capitalize(name)}`) !== false
            ? Mutation.composeMutationResolver(_schema, plugin, name, 'delete')
            : null,
      };

      // Add model Input definition.
      acc.definition += Types.generateInputModel(model, name);

      Object.keys(mutations).forEach(type => {
        if (_.isFunction(mutations[type])) {
          let mutationDefinition;
          let mutationName = `${type}${_.capitalize(name)}`;

          // Generate the Input for this specific action.
          acc.definition += Types.generateInputPayloadArguments(
            model,
            name,
            type,
          );

          switch (type) {
            case 'create':
              mutationDefinition = {
                [`${mutationName}(input: ${mutationName}Input)`]: `${mutationName}Payload`,
              };

              break;
            case 'update':
              mutationDefinition = {
                [`${mutationName}(input: ${mutationName}Input)`]: `${mutationName}Payload`,
              };

              break;
            case 'delete':
              mutationDefinition = {
                [`${mutationName}(input: ${mutationName}Input)`]: `${mutationName}Payload`,
              };
              break;
            default:
            // Nothing.
          }

          // Assign mutation definition to global definition.
          Object.assign(acc.mutation, mutationDefinition);

          // Assign resolver to this mutation and merge it with the others.
          _.merge(acc.resolver.Mutation, {
            [`${mutationName}`]: mutations[type],
          });
        }
      });

      // TODO:
      // - Add support for Graphql Aggregation in Bookshelf ORM
      if (model.orm === 'mongoose') {
        // Generation the aggregation for the given model
        const modelAggregator = Aggregator.formatModelConnectionsGQL(
          attributes,
          model,
          name,
          queries.plural,
        );
        if (modelAggregator) {
          acc.definition += modelAggregator.type;
          if (!acc.resolver[modelAggregator.globalId]) {
            acc.resolver[modelAggregator.globalId] = {};
          }

          _.merge(acc.resolver, modelAggregator.resolver);
          _.merge(acc.query, modelAggregator.query);
        }
      }

      // Build associations queries.
      (model.associations || []).forEach(association => {
        switch (association.nature) {
          case 'oneToManyMorph':
            return _.merge(acc.resolver[globalId], {
              [association.alias]: async obj => {
                const withRelated = await resolvers.fetch(
                  {
                    id: obj[model.primaryKey],
                    model: name,
                  },
                  plugin,
                  [association.alias],
                  false,
                );

                const entry =
                  withRelated && withRelated.toJSON
                    ? withRelated.toJSON()
                    : withRelated;

                // Set the _type only when the value is defined
                if (entry[association.alias]) {
                  entry[association.alias]._type = _.upperFirst(
                    association.model,
                  );
                }

                return entry[association.alias];
              },
            });
          case 'manyMorphToOne':
          case 'manyMorphToMany':
          case 'manyToManyMorph':
            return _.merge(acc.resolver[globalId], {
              [association.alias]: async (obj, options, context) => {
                // eslint-disable-line no-unused-vars
                const [withRelated, withoutRelated] = await Promise.all([
                  resolvers.fetch(
                    {
                      id: obj[model.primaryKey],
                      model: name,
                    },
                    plugin,
                    [association.alias],
                    false,
                  ),
                  resolvers.fetch(
                    {
                      id: obj[model.primaryKey],
                      model: name,
                    },
                    plugin,
                    [],
                  ),
                ]);

                const entry =
                  withRelated && withRelated.toJSON
                    ? withRelated.toJSON()
                    : withRelated;

                // TODO:
                // - Handle sort, limit and start (lodash or inside the query)
                entry[association.alias].map((entry, index) => {
                  const type =
                    _.get(
                      withoutRelated,
                      `${association.alias}.${index}.kind`,
                    ) ||
                    _.upperFirst(
                      _.camelCase(
                        _.get(
                          withoutRelated,
                          `${association.alias}.${index}.${
                            association.alias
                          }_type`,
                        ),
                      ),
                    ) ||
                    _.upperFirst(_.camelCase(association[association.type]));

                  entry._type = type;

                  return entry;
                });

                return entry[association.alias];
              },
            });
          default:
        }

        _.merge(acc.resolver[globalId], {
          [association.alias]: async (obj, options, context) => {
            // eslint-disable-line no-unused-vars
            // Construct parameters object to retrieve the correct related entries.
            const params = {
              model: association.model || association.collection,
            };

            const queryOpts = {
              source: association.plugin,
            };

            if (association.type === 'model') {
              params.id = obj[association.alias];
            } else {
              // Get refering model.
              const ref = association.plugin
                ? strapi.plugins[association.plugin].models[params.model]
                : strapi.models[params.model];

              // Apply optional arguments to make more precise nested request.
              const convertedParams = strapi.utils.models.convertParams(
                name,
                Query.convertToParams(Query.amountLimiting(options)),
              );
              const where = strapi.utils.models.convertParams(
                name,
                options.where || {},
              );

              // Limit, order, etc.
              Object.assign(queryOpts, convertedParams);

              // Skip.
              queryOpts.skip = convertedParams.start;

              switch (association.nature) {
                case 'manyToMany':
                  if (association.dominant) {
                    const arrayOfIds = (obj[association.alias] || []).map(
                      related => {
                        return related[ref.primaryKey] || related;
                      },
                    );

                    // Where.
                    queryOpts.query = strapi.utils.models.convertParams(name, {
                      // Construct the "where" query to only retrieve entries which are
                      // related to this entry.
                      [ref.primaryKey]: arrayOfIds,
                      ...where.where,
                    }).where;
                    break;
                  }
                  // falls through
                default:
                  // Where.
                  queryOpts.query = strapi.utils.models.convertParams(name, {
                    // Construct the "where" query to only retrieve entries which are
                    // related to this entry.
                    [association.via]: obj[ref.primaryKey],
                    ...where.where,
                  }).where;
              }
            }

            const value = await (association.model
              ? resolvers.fetch(params, association.plugin, [])
              : resolvers.fetchAll(params, { ...queryOpts, populate: [] }));

            return value && value.toJSON ? value.toJSON() : value;
          },
        });
      });

      return acc;
    }, initialState);
  },
};
<|endoftext|>


<commit_before>import {applyMiddleware, compose, createStore as reduxCreateStore, combineReducers} from 'redux'
import {intlReducer} from 'react-intl-redux'
import _difference from 'lodash/difference'
import _pick from 'lodash/pick'
import createSagaMiddleware, {takeEvery} from 'redux-saga'
import {fork} from 'redux-saga/effects'
import thunk from 'redux-thunk'

import errorLogging, {sagas as loggingSagas, logError as logErrorAction} from '../errorLogging'
import {autoRestartSaga, createGenerator} from './sagaHelpers'
import externalEvents from '../externalEvents'

import input from './input/reducer'

const sagaMiddleware = createSagaMiddleware()

function* mainSaga() {
  yield [
    fork(takeEvery, 'FIRE_EXTERNAL_EVENT', externalEvents.fireExternalEventSaga)
  ]
}

export const createStore = (initialState = {}, reducers = {}, sagas = []) => {
  let middleware = applyMiddleware(thunk, sagaMiddleware)

  if (__DEBUG__) {
    const devToolsExtension = window.devToolsExtension

    if (typeof devToolsExtension === 'function') {
      middleware = compose(middleware, devToolsExtension())
    }
  }

  const allReducers = {
    ...reducers,
    input,
    errorLogging,
    intl: intlReducer
  }

  sagas.push(mainSaga)

  reducers = combineReducers(allReducers)

  const store = reduxCreateStore(reducers, initialState, middleware)

  store.allReducers = allReducers
  store.sagas = sagas

  const rootSaga = createGenerator(sagas.map(s => fork(s)))

  sagaMiddleware.run(autoRestartSaga(rootSaga, logErrorAction))
  sagaMiddleware.run(loggingSagas, ['console', 'remote', 'toastr'])

  return store
}

export const hotReloadReducers = (store, reducers) => {
  const allReducers = {
    ...store.allReducers,
    ...reducers
  }
  const combinedReducers = combineReducers(allReducers)
  store.replaceReducer(combinedReducers)
  store.allReducers = allReducers
}

export const injectReducers = (store, reducers) => {
  const newKeys = _difference(Object.keys(reducers), Object.keys(store.allReducers))
  if (newKeys.length > 0) {
    const allReducers = {
      ...store.allReducers,
      ..._pick(reducers, newKeys)
    }
    hotReloadReducers(store, allReducers)
  }
}

export const injectSaga = (store, saga) => {
  if (!store.sagas.includes(saga)) {
    store.sagas.push(saga)
    sagaMiddleware.run(autoRestartSaga(saga, logErrorAction))
  }
}
<commit_msg>refactor(tocco-util): attach sagaMiddleware to store<commit_after>import {applyMiddleware, compose, createStore as reduxCreateStore, combineReducers} from 'redux'
import {intlReducer} from 'react-intl-redux'
import _difference from 'lodash/difference'
import _pick from 'lodash/pick'
import createSagaMiddleware, {takeEvery} from 'redux-saga'
import {fork} from 'redux-saga/effects'
import thunk from 'redux-thunk'

import errorLogging, {sagas as loggingSagas, logError as logErrorAction} from '../errorLogging'
import {autoRestartSaga, createGenerator} from './sagaHelpers'
import externalEvents from '../externalEvents'

import input from './input/reducer'

function* mainSaga() {
  yield [
    fork(takeEvery, 'FIRE_EXTERNAL_EVENT', externalEvents.fireExternalEventSaga)
  ]
}

export const createStore = (initialState = {}, reducers = {}, sagas = []) => {
  const sagaMiddleware = createSagaMiddleware()
  let middleware = applyMiddleware(thunk, sagaMiddleware)

  if (__DEBUG__) {
    const devToolsExtension = window.devToolsExtension

    if (typeof devToolsExtension === 'function') {
      middleware = compose(middleware, devToolsExtension())
    }
  }

  const allReducers = {
    ...reducers,
    input,
    errorLogging,
    intl: intlReducer
  }

  sagas.push(mainSaga)

  reducers = combineReducers(allReducers)

  const store = reduxCreateStore(reducers, initialState, middleware)

  store.allReducers = allReducers
  store.sagas = sagas
  store.sagaMiddleware = sagaMiddleware

  const rootSaga = createGenerator(sagas.map(s => fork(s)))

  sagaMiddleware.run(autoRestartSaga(rootSaga, logErrorAction))
  sagaMiddleware.run(loggingSagas, ['console', 'remote', 'toastr'])

  return store
}

export const hotReloadReducers = (store, reducers) => {
  const allReducers = {
    ...store.allReducers,
    ...reducers
  }
  const combinedReducers = combineReducers(allReducers)
  store.replaceReducer(combinedReducers)
  store.allReducers = allReducers
}

export const injectReducers = (store, reducers) => {
  const newKeys = _difference(Object.keys(reducers), Object.keys(store.allReducers))
  if (newKeys.length > 0) {
    const allReducers = {
      ...store.allReducers,
      ..._pick(reducers, newKeys)
    }
    hotReloadReducers(store, allReducers)
  }
}

export const injectSaga = (store, saga) => {
  if (!store.sagas.includes(saga)) {
    store.sagas.push(saga)
    store.sagaMiddleware.run(autoRestartSaga(saga, logErrorAction))
  }
}
<|endoftext|>


<commit_before>import { Promise } from 'es6-promise';

let _authWindow = null;
let _authWindowListener = null;
let _OAuthToken = null;

const _POLL_TIME = 500;

const cleanupAuthWindow = () => {
    if(_authWindow && _authWindow.close) {
        _authWindow.close();
    }
    _authWindow = null;
}

const removeAuthWindowListener = () => {
    if (_authWindowListener) {
        window.removeEventListener('message', _authWindowListener);
        _authWindowListener = null;
    }
}

const createAuthWindowListener = (OAuthURI, resolve, reject) => {
    return function authWindowListener(event) {
        let uri = ParseURI(OAuthURI);
        if(event.origin === `${uri.origin}`) {
            let message = event.data;
            if(message && message.token) {
                _OAuthToken = message.token;
                resolve( {OAuthToken: _OAuthToken } );
                removeAuthWindowListener();
                cleanupAuthWindow();
            }
        }
    }
}

const askOAuthServerForToken = (OAuthURI, resolve, reject) => {
    setTimeout( () => {
        let uri = ParseURI(OAuthURI);
        // keep polling until we receive a response
        if(!_OAuthToken) {
            if(_authWindow) {
                _authWindow.postMessage("getOAuthToken", `${uri.origin}`);
            }

            // if the auth window is closed, stop polling
            if(_authWindow && !_authWindow.closed) {
                askOAuthServerForToken(OAuthURI, resolve, reject);
            } else if (_authWindow && _authWindow.closed) {
                _authWindow = null;
                reject(new Error("User cancelled OAuth flow"));
            } else {
                removeAuthWindowListener();
                cleanupAuthWindow();
            }
        }
    }, _POLL_TIME);
}

// URI Parser from https://stackoverflow.com/a/39308026 to
// extract the origin from _widgetServerUri to prevent XSS
const ParseURI = (url) => {
    var m = url.match(/^(([^:\/?#]+:)?(?:\/\/(([^\/?#:]*)(?::([^\/?#:]*))?)))?([^?#]*)(\?[^#]*)?(#.*)?$/),
        r = {
            hash: m[8] || "",                    // #asd
            host: m[3] || "",                    // localhost:257
            hostname: m[4] || "",                // localhost
            href: m[0] || "",                    // http://localhost:257/deploy/?asd=asd#asd
            origin: m[1] || "",                  // http://localhost:257
            pathname: m[6] || (m[1] ? "/" : ""), // /deploy/
            port: m[5] || "",                    // 257
            protocol: m[2] || "",                // http:
            search: m[7] || ""                   // ?asd=asd
        };
    if (r.protocol.length == 2) {
        r.protocol = "file:///" + r.protocol.toUpperCase();
        r.origin = r.protocol + "//" + r.host;
    }
    r.href = r.origin + r.pathname + r.search + r.hash;
    return m && r;
};

const login = (OAuthURI) => {
    if(!OAuthURI) {
        throw new Exception("Missing parameter: Need to include the backend OAuth2 URI");
    }

    removeAuthWindowListener();
    _authWindow = window.open(OAuthURI, "_auth", "width=550px,height=650px");
    return new Promise( (resolve, reject) => {
        window.addEventListener('message', createAuthWindowListener(OAuthURI, resolve, reject));
        askOAuthServerForToken(OAuthURI, resolve, reject);
    });
}

const logout = () => {
    return new Promise( (resolve, reject) => {
        removeAuthWindowListener();
        cleanupAuthWindow();

        // TO DO: Make a backend call to destroy the token
        _OAuthToken = null;
        resolve(true);
    });
}

export { login, logout };
<commit_msg>Error not exception.<commit_after>import { Promise } from 'es6-promise';

let _authWindow = null;
let _authWindowListener = null;
let _OAuthToken = null;

const _POLL_TIME = 500;

const cleanupAuthWindow = () => {
    if(_authWindow && _authWindow.close) {
        _authWindow.close();
    }
    _authWindow = null;
}

const removeAuthWindowListener = () => {
    if (_authWindowListener) {
        window.removeEventListener('message', _authWindowListener);
        _authWindowListener = null;
    }
}

const createAuthWindowListener = (OAuthURI, resolve, reject) => {
    return function authWindowListener(event) {
        let uri = ParseURI(OAuthURI);
        if(event.origin === `${uri.origin}`) {
            let message = event.data;
            if(message && message.token) {
                _OAuthToken = message.token;
                resolve( {OAuthToken: _OAuthToken } );
                removeAuthWindowListener();
                cleanupAuthWindow();
            }
        }
    }
}

const askOAuthServerForToken = (OAuthURI, resolve, reject) => {
    setTimeout( () => {
        let uri = ParseURI(OAuthURI);
        // keep polling until we receive a response
        if(!_OAuthToken) {
            if(_authWindow) {
                _authWindow.postMessage("getOAuthToken", `${uri.origin}`);
            }

            // if the auth window is closed, stop polling
            if(_authWindow && !_authWindow.closed) {
                askOAuthServerForToken(OAuthURI, resolve, reject);
            } else if (_authWindow && _authWindow.closed) {
                _authWindow = null;
                reject(new Error("User cancelled OAuth flow"));
            } else {
                removeAuthWindowListener();
                cleanupAuthWindow();
            }
        }
    }, _POLL_TIME);
}

// URI Parser from https://stackoverflow.com/a/39308026 to
// extract the origin from _widgetServerUri to prevent XSS
const ParseURI = (url) => {
    var m = url.match(/^(([^:\/?#]+:)?(?:\/\/(([^\/?#:]*)(?::([^\/?#:]*))?)))?([^?#]*)(\?[^#]*)?(#.*)?$/),
        r = {
            hash: m[8] || "",                    // #asd
            host: m[3] || "",                    // localhost:257
            hostname: m[4] || "",                // localhost
            href: m[0] || "",                    // http://localhost:257/deploy/?asd=asd#asd
            origin: m[1] || "",                  // http://localhost:257
            pathname: m[6] || (m[1] ? "/" : ""), // /deploy/
            port: m[5] || "",                    // 257
            protocol: m[2] || "",                // http:
            search: m[7] || ""                   // ?asd=asd
        };
    if (r.protocol.length == 2) {
        r.protocol = "file:///" + r.protocol.toUpperCase();
        r.origin = r.protocol + "//" + r.host;
    }
    r.href = r.origin + r.pathname + r.search + r.hash;
    return m && r;
};

const login = (OAuthURI) => {
    if(!OAuthURI) {
        throw new Error("Missing parameter: Need to include the backend OAuth2 URI");
    }

    removeAuthWindowListener();
    _authWindow = window.open(OAuthURI, "_auth", "width=550px,height=650px");
    return new Promise( (resolve, reject) => {
        window.addEventListener('message', createAuthWindowListener(OAuthURI, resolve, reject));
        askOAuthServerForToken(OAuthURI, resolve, reject);
    });
}

const logout = () => {
    return new Promise( (resolve, reject) => {
        removeAuthWindowListener();
        cleanupAuthWindow();

        // TO DO: Make a backend call to destroy the token
        _OAuthToken = null;
        resolve(true);
    });
}

export { login, logout };
<|endoftext|>


<commit_before>import { mount } from '@vue/test-utils'
import Nav from '@/components/Nav'
import { wait } from '../../../utils'

const fullPathMatches = (route, item) => {
  return route.fullPath === item.path + item.to
}

const options = {
  components: {
    'veui-nav': Nav
  },
  data () {
    return {
      active: 'Input',
      width: '180px',
      fullPathMatches,
      items: [
        {
          label: 'Group One',
          name: 'group-one',
          position: 'card',
          children: [
            {
              label: 'Sub One',
              name: 'sub-one',
              children: [
                {
                  label: 'Input',
                  name: 'Input'
                }
              ]
            }
          ]
        },
        {
          label: 'Button',
          name: 'Button',
          to: '#button',
          children: [
            {
              label: 'OptionGroup',
              name: 'OptionGroup',
              children: [
                {
                  label: 'Link',
                  name: 'Link'
                }
              ]
            }
          ]
        },
        {
          label: 'Loading',
          name: 'Loading'
        }
      ]
    }
  }
}

describe('components/Nav', () => {
  it('should handle ui prop correctly', async () => {
    let wrapper = mount(
      {
        ...options,
        template: `

          <veui-nav class="small-nav" ui="s" :items="items"/>
          <veui-nav class="large-nav" ui="l" :items="items"/>

`
      },
      {
        sync: false,
        attachToDocument: true
      }
    )
    let { vm } = wrapper
    let navUI = wrapper.find('.small-nav').attributes().ui
    let navLinkUI = wrapper.find('.small-nav .veui-nav-item').attributes().ui
    expect(navUI).to.include('s')
    expect(navLinkUI).to.include('s')

    navUI = wrapper.find('.large-nav').attributes().ui
    navLinkUI = wrapper.find('.large-nav .veui-nav-item').attributes().ui
    expect(navUI).to.include('l')
    expect(navLinkUI).to.include('l')

    wrapper.find('.small-nav .veui-nav-item').trigger('mouseenter')
    await vm.$nextTick()
    let overlay = document.querySelector('.veui-nav-overlay').getAttribute('ui')
    expect(overlay).to.include('s')
    wrapper.destroy()
  })

  it('should highlight item on hovering correctly', async () => {
    let wrapper = mount(
      {
        ...options,
        template: `<veui-nav class="small-nav" :items="items"/>`
      },
      {
        sync: false,
        attachToDocument: true
      }
    )
    let { vm } = wrapper

    let items = wrapper.findAll('.small-nav .veui-nav-item')
    let last = items.at(items.length - 1)
    last.trigger('mouseenter')
    await vm.$nextTick()
    expect(last.classes()).to.include('veui-nav-item-open')

    last.trigger('mouseleave')
    await vm.$nextTick()
    expect(last.classes()).to.not.include('veui-nav-item-open')

    let second = items.at(1)
    second.trigger('mouseenter')
    await vm.$nextTick()
    second.trigger('mouseout', {
      relatedTarget: wrapper.find('.veui-nav-dropdown').element
    })
    // 等 outside 触发之后
    await wait(300)
    expect(second.classes()).to.include('veui-nav-item-open')
    wrapper.destroy()
  })

  it('should handle controlled active prop correctly', async () => {
    let wrapper = mount(
      {
        ...options,
        template: '<veui-nav :active.sync="active" :items="items"/>'
      },
      {
        sync: false,
        attachToDocument: true
      }
    )

    let { vm } = wrapper
    let item = wrapper.findAll('.veui-nav-body .veui-nav-item').at(0)
    expect(item.classes('veui-nav-item-active')).to.equal(true)

    vm.active = null
    await vm.$nextTick()
    expect(item.classes('veui-nav-item-active')).to.equal(false)
    wrapper
      .findAll('.veui-nav-body .veui-nav-item')
      .at(1)
      .trigger('click')
    await vm.$nextTick()
    expect(vm.active).to.equal('Button')
    wrapper.destroy()
  })

  it('should handle uncontrolled active prop correctly', async () => {
    let wrapper = mount(
      {
        ...options,
        template:
          '<veui-nav ref="nav" :items="items" :matches="fullPathMatches"/>'
      },
      {
        sync: false,
        attachToDocument: true
      }
    )

    let { vm } = wrapper
    wrapper
      .findAll('.veui-nav-body .veui-nav-item')
      .at(1)
      .trigger('click')
    await vm.$nextTick()
    expect(vm.$refs.nav.realActive).to.equal('Button')
    wrapper.destroy()
  })

  it('should collapse correctly on resizing', async () => {
    let wrapper = mount(
      {
        ...options,
        template: '<veui-nav :style="{width}" ref="nav" :items="items"/>'
      },
      {
        sync: false,
        attachToDocument: true
      }
    )

    let { vm } = wrapper
    await vm.$nextTick()
    let length = wrapper.findAll(
      '.veui-nav-body .veui-nav-item:not(.veui-nav-more)'
    ).length
    expect(length).to.equal(1)

    vm.width = 'auto'
    vm.$refs.nav.renderAllThenUpdateLayout()
    await vm.$nextTick()
    length = wrapper.findAll('.veui-nav-body .veui-nav-item').length
    expect(length).to.equal(3)
    wrapper.destroy()
  })

  it('should alway emit `click` event on clicking nav item', async () => {
    let wrapper = mount(
      {
        ...options,
        template: '<veui-nav ref="nav" :items="items"/>'
      },
      {
        sync: false,
        attachToDocument: true
      }
    )

    let { vm } = wrapper
    let count = 0
    vm.$refs.nav.$on('click', () => {
      count++
    })

    let items = wrapper.findAll('.veui-nav-body .veui-nav-item')
    items.at(0).trigger('click')
    expect(count).to.equal(1)
    items.at(1).trigger('mouseenter')
    await wait(300)
    wrapper.find('.veui-nav-overlay .veui-nav-item').trigger('click')
    expect(count).to.equal(2)
    wrapper.destroy()
  })

  it('should handle keyboard navigation correctly', async () => {
    let wrapper = mount(
      {
        ...options,
        template:
          '<veui-nav :active.sync="active" :items="items" :matches="fullPathMatches"/>'
      },
      {
        sync: false,
        attachToDocument: true
      }
    )

    let { vm } = wrapper
    let items = wrapper.findAll('.veui-nav-body .veui-nav-item')
    let endIndex = items.length - 1
    items.at(0).trigger('keydown', { key: 'Right' })
    await vm.$nextTick()
    expect(items.at(1).attributes().tabindex).to.include('0')

    items.at(endIndex).trigger('keydown', { key: 'Right' })
    await vm.$nextTick()
    expect(items.at(0).attributes().tabindex).to.include('0')

    items.at(0).trigger('keydown', { key: 'Left' })
    await vm.$nextTick()
    expect(items.at(endIndex).attributes().tabindex).to.include('0')

    items.at(0).trigger('keydown', { key: 'End' })
    await vm.$nextTick()
    expect(items.at(endIndex).attributes().tabindex).to.include('0')
    items.at(endIndex).trigger('keydown', { key: 'Home' })
    await vm.$nextTick()
    expect(items.at(0).attributes().tabindex).to.include('0')

    items.at(0).trigger('keydown', { key: 'Down' })
    await vm.$nextTick()
    expect(items.at(0).classes()).to.include('veui-nav-item-open')

    items.at(0).trigger('keydown', { key: 'Esc' })
    await vm.$nextTick()
    expect(items.at(0).classes()).to.not.include('veui-nav-item-open')

    items.at(1).trigger('keydown', { key: 'Enter' })
    await vm.$nextTick()
    expect(vm.active).to.equal('Button')

    wrapper.destroy()
  })
})
<commit_msg>test: update test cases for Nav<commit_after>import { mount } from '@vue/test-utils'
import Nav from '@/components/Nav'
import { wait } from '../../../utils'

const fullPathMatches = (route, item) => {
  return route.fullPath === item.path + item.to
}

const options = {
  components: {
    'veui-nav': Nav
  },
  data () {
    return {
      active: 'Input',
      width: '180px',
      fullPathMatches,
      items: [
        {
          label: 'Group One',
          name: 'group-one',
          position: 'card',
          children: [
            {
              label: 'Sub One',
              name: 'sub-one',
              children: [
                {
                  label: 'Input',
                  name: 'Input'
                }
              ]
            }
          ]
        },
        {
          label: 'Button',
          name: 'Button',
          to: '#button',
          children: [
            {
              label: 'OptionGroup',
              name: 'OptionGroup',
              children: [
                {
                  label: 'Link',
                  name: 'Link'
                }
              ]
            }
          ]
        },
        {
          label: 'Loading',
          name: 'Loading'
        }
      ]
    }
  }
}

describe('components/Nav', () => {
  it('should handle ui prop correctly', async () => {
    let wrapper = mount(
      {
        ...options,
        template: `

          <veui-nav class="small-nav" ui="s" :items="items"/>
          <veui-nav class="large-nav" ui="l" :items="items"/>

`
      },
      {
        sync: false,
        attachToDocument: true
      }
    )
    let { vm } = wrapper
    let navUI = wrapper.find('.small-nav').attributes().ui
    let navLinkUI = wrapper.find('.small-nav .veui-nav-item').attributes().ui
    expect(navUI).to.include('s')
    expect(navLinkUI).to.include('s')

    navUI = wrapper.find('.large-nav').attributes().ui
    navLinkUI = wrapper.find('.large-nav .veui-nav-item').attributes().ui
    expect(navUI).to.include('l')
    expect(navLinkUI).to.include('l')

    wrapper.find('.small-nav .veui-nav-item').trigger('mouseenter')
    await vm.$nextTick()
    let overlay = document.querySelector('.veui-nav-overlay').getAttribute('ui')
    expect(overlay).to.include('s')
    wrapper.destroy()
  })

  it('should highlight item on hovering correctly', async () => {
    let wrapper = mount(
      {
        ...options,
        template: `<veui-nav class="small-nav" :items="items"/>`
      },
      {
        sync: false,
        attachToDocument: true
      }
    )
    let { vm } = wrapper

    let items = wrapper.findAll('.small-nav .veui-nav-item')
    let last = items.at(items.length - 1)
    last.trigger('mouseenter')
    await vm.$nextTick()
    expect(last.classes()).to.include('veui-nav-item-open')

    last.trigger('mouseleave')
    await vm.$nextTick()
    expect(last.classes()).to.not.include('veui-nav-item-open')

    let second = items.at(1)
    second.trigger('mouseenter')
    await vm.$nextTick()
    second.trigger('mouseleave')
    await vm.$nextTick()
    expect(second.classes(), '有下拉的不会监听mouseleave').to.include(
      'veui-nav-item-open'
    )
    wrapper.destroy()
  })

  it('should handle controlled active prop correctly', async () => {
    let wrapper = mount(
      {
        ...options,
        template: '<veui-nav :active.sync="active" :items="items"/>'
      },
      {
        sync: false,
        attachToDocument: true
      }
    )

    let { vm } = wrapper
    let item = wrapper.findAll('.veui-nav-body .veui-nav-item').at(0)
    expect(item.classes('veui-nav-item-active')).to.equal(true)

    vm.active = null
    await vm.$nextTick()
    expect(item.classes('veui-nav-item-active')).to.equal(false)
    wrapper
      .findAll('.veui-nav-body .veui-nav-item')
      .at(1)
      .trigger('click')
    await vm.$nextTick()
    expect(vm.active).to.equal('Button')
    wrapper.destroy()
  })

  it('should handle uncontrolled active prop correctly', async () => {
    let wrapper = mount(
      {
        ...options,
        template:
          '<veui-nav ref="nav" :items="items" :matches="fullPathMatches"/>'
      },
      {
        sync: false,
        attachToDocument: true
      }
    )

    let { vm } = wrapper
    wrapper
      .findAll('.veui-nav-body .veui-nav-item')
      .at(1)
      .trigger('click')
    await vm.$nextTick()
    expect(vm.$refs.nav.realActive).to.equal('Button')
    wrapper.destroy()
  })

  it('should collapse correctly on resizing', async () => {
    let wrapper = mount(
      {
        ...options,
        template: '<veui-nav :style="{width}" ref="nav" :items="items"/>'
      },
      {
        sync: false,
        attachToDocument: true
      }
    )

    let { vm } = wrapper
    await vm.$nextTick()
    let length = wrapper.findAll(
      '.veui-nav-body .veui-nav-item:not(.veui-nav-more)'
    ).length
    expect(length).to.equal(1)

    vm.width = 'auto'
    vm.$refs.nav.renderAllThenUpdateLayout()
    await vm.$nextTick()
    length = wrapper.findAll('.veui-nav-body .veui-nav-item').length
    expect(length).to.equal(3)
    wrapper.destroy()
  })

  it('should alway emit `click` event on clicking nav item', async () => {
    let wrapper = mount(
      {
        ...options,
        template: '<veui-nav ref="nav" :items="items"/>'
      },
      {
        sync: false,
        attachToDocument: true
      }
    )

    let { vm } = wrapper
    let count = 0
    vm.$refs.nav.$on('click', () => {
      count++
    })

    let items = wrapper.findAll('.veui-nav-body .veui-nav-item')
    items.at(0).trigger('click')
    expect(count).to.equal(1)
    items.at(1).trigger('mouseenter')
    await wait(300)
    wrapper.find('.veui-nav-overlay .veui-nav-item').trigger('click')
    expect(count).to.equal(2)
    wrapper.destroy()
  })

  it('should handle keyboard navigation correctly', async () => {
    let wrapper = mount(
      {
        ...options,
        template:
          '<veui-nav :active.sync="active" :items="items" :matches="fullPathMatches"/>'
      },
      {
        sync: false,
        attachToDocument: true
      }
    )

    let { vm } = wrapper
    let items = wrapper.findAll('.veui-nav-body .veui-nav-item')
    let endIndex = items.length - 1
    items.at(0).trigger('keydown', { key: 'Right' })
    await vm.$nextTick()
    expect(items.at(1).attributes().tabindex).to.include('0')

    items.at(endIndex).trigger('keydown', { key: 'Right' })
    await vm.$nextTick()
    expect(items.at(0).attributes().tabindex).to.include('0')

    items.at(0).trigger('keydown', { key: 'Left' })
    await vm.$nextTick()
    expect(items.at(endIndex).attributes().tabindex).to.include('0')

    items.at(0).trigger('keydown', { key: 'End' })
    await vm.$nextTick()
    expect(items.at(endIndex).attributes().tabindex).to.include('0')
    items.at(endIndex).trigger('keydown', { key: 'Home' })
    await vm.$nextTick()
    expect(items.at(0).attributes().tabindex).to.include('0')

    items.at(0).trigger('keydown', { key: 'Down' })
    await vm.$nextTick()
    expect(items.at(0).classes()).to.include('veui-nav-item-open')

    items.at(0).trigger('keydown', { key: 'Esc' })
    await vm.$nextTick()
    expect(items.at(0).classes()).to.not.include('veui-nav-item-open')

    items.at(1).trigger('keydown', { key: 'Enter' })
    await vm.$nextTick()
    expect(vm.active).to.equal('Button')

    wrapper.destroy()
  })
})
<|endoftext|>


<commit_before>import * as DS    from './delaney';
import * as props from './properties';
import * as comb  from '../common/combinatorics';

export const dual = ds => {
  const d = DS.dim(ds);
  const n = DS.size(ds);

  return DS.build(
    d, n,
    (_, i) => ds.elements().map(D => [D, ds.s(d - i, D)]),
    (_, i) => ds.elements().map(D => [D, ds.v(d-i-1, d-i, D)]));
};

export const cover = (ds, nrSheets, fn) => {
  const d = DS.dim(ds);
  const n = DS.size(ds);

  const pairingsFn = (_, i) => {
    const out = [];
    for (const D of ds.elements()) {
      for (let k = 0; k < nrSheets; ++k)
        out.push([k * n + D, fn(k, i, D) * n + ds.s(i, D)]);
    }
    return out;
  };

  const branchingsFn = (tmp, i) => {
    const out = [];
    for (const D of DS.orbitReps2(tmp, i, i+1)) {
      const m = DS.m(ds, i, i+1, (D - 1) % n + 1) || 0;
      out.push([D, m / DS.r(tmp, i, i+1, D)]);
    }
    return out;
  };

  return DS.build(d, n * nrSheets, pairingsFn, branchingsFn);
};

export const orientedCover = ds => {
  if (props.isOriented(ds))
    return ds;
  else {
    const ori = props.partialOrientation(ds);
    return cover(ds, 2, (k, i, D) => ori[D] == ori[ds.s(i, D)] ? 1 - k : k);
  }
};

export const minimal = ds => {
  if (props.isMinimal(ds))
    return ds;
  else {
    const p = props.typePartition(ds);

    const reps = [];
    const seen = {};
    for (const D of ds.elements()) {
      const r = p.find(D);
      if (seen[r] == null) {
        seen[r] = true;
        reps.push(r);
      }
    }

    return DS.build(
      DS.dim(ds), reps.length,
      (_, i) => reps.map(
        (D, k) => [k+1, reps.indexOf(p.find(ds.s(i, D))) + 1]),
      (tmp, i) => reps.map(
        (D, k) => [k+1, (DS.m(ds, i, i+1, D) || 0) / DS.r(tmp, i, i+1, k+1)]));
  }
};

const _apply = (p, i) => (i < 0 || i >= p.length) ? i : p[i] - 1;

export const barycentricSubdivision = (ds, splitDim) => {
  if (splitDim == null)
    splitDim = DS.dim(ds);

  if (splitDim == 0)
    return ds;
  else {
    const n = DS.size(ds);

    const perms = [];
    const elements = [];
    let j = 0;
    for (const p of comb.permutations(splitDim + 1)) {
      perms.push(p);
      for (const D of ds.elements())
        elements.push([p, j, D]);
      ++j;
    }

    const sigma = (p, i) => {
      const q = p.slice();
      [q[i], q[i+1]] = [p[i+1], p[i]];
      return perms.findIndex(p => p <= q && p >= q);
    };

    const pairingsFn = (_, i) => elements.map(([p, j, D]) => [
      n * j + D,
      (i < splitDim) ?
        n * sigma(p, i) + D :
        n * j + ds.s(_apply(p, i), D)
    ]);

    const branchingsFn = (tmp, i) => elements.map(([p, j, D]) => [
      n * j + D,
      (i < splitDim - 1) ?
        1 :
        ds.v(_apply(p, i), _apply(p, i+1), D)
    ]);

    return DS.build(DS.dim(ds), n * perms.length, pairingsFn, branchingsFn);
  }
};

export const canonical = ds => {
  const inv = props.invariant(ds);
  const dim = DS.dim(ds);
  const size = DS.size(ds);

  const ops = new Array(dim + 1).fill(0).map(_ => []);
  const brs = new Array(dim).fill(0).map(_ => []);

  let n = 0;
  let k = -1;

  while (k+1 < inv.length) {
    const i = inv[++k];
    const D = inv[++k];
    const E = (i >= 0) ? inv[++k] : D;
    if (E > n) {
      for (let j = 0; j < dim; ++j)
        brs[j].push([E, inv[++k]]);
      n = E;
    }
    if (i >= 0)
      ops[i].push([D, E]);
  }

  return DS.build(dim, size, (_, i) => ops[i], (_, i) => brs[i]);
};

if (require.main == module) {
  const test = ds => {
    console.log(`ds = ${ds}`);
    console.log();

    console.log(`    dual          : ${dual(ds)}`);
    console.log(`    minimal image : ${minimal(ds)}`);
    console.log(`    oriented cover: ${orientedCover(ds)}`);
    console.log(`    canonical     : ${canonical(ds)}`);
    console.log();

    for (const i of ds.indices())
      console.log(`    ${i}-subdivision : ${barycentricSubdivision(ds, i)}`);
    console.log();
    console.log();
  };

  test(DS.parse(
    '<1.1:24:' +
      '2 4 6 8 10 12 14 16 18 20 22 24,' +
      '16 3 5 7 9 11 13 15 24 19 21 23,' +
      '10 9 20 19 14 13 22 21 24 23 18 17:' +
      '8 4,3 3 3 3>'));

  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 8,3>'));
  test(DS.parse('<1.1:2 3:2,1 2,1 2,2:6,3 2,6>'));
}
<commit_msg>derived: add a subsymbol() function<commit_after>import * as DS    from './delaney';
import * as props from './properties';
import * as comb  from '../common/combinatorics';

export const dual = ds => {
  const d = DS.dim(ds);
  const n = DS.size(ds);

  return DS.build(
    d, n,
    (_, i) => ds.elements().map(D => [D, ds.s(d - i, D)]),
    (_, i) => ds.elements().map(D => [D, ds.v(d-i-1, d-i, D)]));
};

export const cover = (ds, nrSheets, fn) => {
  const d = DS.dim(ds);
  const n = DS.size(ds);

  const pairingsFn = (_, i) => {
    const out = [];
    for (const D of ds.elements()) {
      for (let k = 0; k < nrSheets; ++k)
        out.push([k * n + D, fn(k, i, D) * n + ds.s(i, D)]);
    }
    return out;
  };

  const branchingsFn = (tmp, i) => {
    const out = [];
    for (const D of DS.orbitReps2(tmp, i, i+1)) {
      const m = DS.m(ds, i, i+1, (D - 1) % n + 1) || 0;
      out.push([D, m / DS.r(tmp, i, i+1, D)]);
    }
    return out;
  };

  return DS.build(d, n * nrSheets, pairingsFn, branchingsFn);
};

export const orientedCover = ds => {
  if (props.isOriented(ds))
    return ds;
  else {
    const ori = props.partialOrientation(ds);
    return cover(ds, 2, (k, i, D) => ori[D] == ori[ds.s(i, D)] ? 1 - k : k);
  }
};

export const minimal = ds => {
  if (props.isMinimal(ds))
    return ds;
  else {
    const p = props.typePartition(ds);

    const reps = [];
    const seen = {};
    for (const D of ds.elements()) {
      const r = p.find(D);
      if (seen[r] == null) {
        seen[r] = true;
        reps.push(r);
      }
    }

    return DS.build(
      DS.dim(ds), reps.length,
      (_, i) => reps.map(
        (D, k) => [k+1, reps.indexOf(p.find(ds.s(i, D))) + 1]),
      (tmp, i) => reps.map(
        (D, k) => [k+1, (DS.m(ds, i, i+1, D) || 0) / DS.r(tmp, i, i+1, k+1)]));
  }
};

const _apply = (p, i) => (i < 0 || i >= p.length) ? i : p[i] - 1;

export const barycentricSubdivision = (ds, splitDim) => {
  if (splitDim == null)
    splitDim = DS.dim(ds);

  if (splitDim == 0)
    return ds;
  else {
    const n = DS.size(ds);

    const perms = [];
    const elements = [];
    let j = 0;
    for (const p of comb.permutations(splitDim + 1)) {
      perms.push(p);
      for (const D of ds.elements())
        elements.push([p, j, D]);
      ++j;
    }

    const sigma = (p, i) => {
      const q = p.slice();
      [q[i], q[i+1]] = [p[i+1], p[i]];
      return perms.findIndex(p => p <= q && p >= q);
    };

    const pairingsFn = (_, i) => elements.map(([p, j, D]) => [
      n * j + D,
      (i < splitDim) ?
        n * sigma(p, i) + D :
        n * j + ds.s(_apply(p, i), D)
    ]);

    const branchingsFn = (tmp, i) => elements.map(([p, j, D]) => [
      n * j + D,
      (i < splitDim - 1) ?
        1 :
        ds.v(_apply(p, i), _apply(p, i+1), D)
    ]);

    return DS.build(DS.dim(ds), n * perms.length, pairingsFn, branchingsFn);
  }
};

export const canonical = ds => {
  const inv = props.invariant(ds);
  const dim = DS.dim(ds);
  const size = DS.size(ds);

  const ops = new Array(dim + 1).fill(0).map(_ => []);
  const brs = new Array(dim).fill(0).map(_ => []);

  let n = 0;
  let k = -1;

  while (k+1 < inv.length) {
    const i = inv[++k];
    const D = inv[++k];
    const E = (i >= 0) ? inv[++k] : D;
    if (E > n) {
      for (let j = 0; j < dim; ++j)
        brs[j].push([E, inv[++k]]);
      n = E;
    }
    if (i >= 0)
      ops[i].push([D, E]);
  }

  return DS.build(dim, size, (_, i) => ops[i], (_, i) => brs[i]);
};

export const subsymbol = (ds, indices, seed) => {
  const elements = props.orbit(ds, indices, seed);
  const dim = indices.length - 1;
  const size = elements.length;

  const mappedElement = {};
  for (let D = 1; D <= size; ++D)
    mappedElement[elements[D - 1]] = D;

  const ops = new Array(dim + 1).fill(0).map(_ => []);
  const brs = new Array(dim).fill(0).map(_ => []);

  for (const D of elements) {
    for (let i = 0; i <= dim; ++i)
      ops[i].push([mappedElement[D], mappedElement[ds.s(indices[i], D)]]);

    for (let i = 0; i < dim; ++i)
      brs[i].push([mappedElement[D], ds.v(indices[i], indices[i + 1], D)])
  }

  return DS.build(dim, size, (_, i) => ops[i], (_, i) => brs[i]);
};

if (require.main == module) {
  const test = ds => {
    console.log(`ds = ${ds}`);
    console.log();

    console.log(`    dual          : ${dual(ds)}`);
    console.log(`    minimal image : ${minimal(ds)}`);
    console.log(`    oriented cover: ${orientedCover(ds)}`);
    console.log(`    canonical     : ${canonical(ds)}`);
    console.log();

    for (const i of ds.indices()) {
      const idcs = ds.indices().filter(k => k != i);
      const sub = subsymbol(ds, idcs, 1);
      console.log(`    ${JSON.stringify(idcs)}-subsymbol at 1: ${sub}`);
    }
    console.log();

    for (const i of ds.indices())
      console.log(`    ${i}-subdivision : ${barycentricSubdivision(ds, i)}`);
    console.log();
    console.log();
  };

  test(DS.parse(
    '<1.1:24:' +
      '2 4 6 8 10 12 14 16 18 20 22 24,' +
      '16 3 5 7 9 11 13 15 24 19 21 23,' +
      '10 9 20 19 14 13 22 21 24 23 18 17:' +
      '8 4,3 3 3 3>'));

  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 8,3>'));
  test(DS.parse('<1.1:2 3:2,1 2,1 2,2:6,3 2,6>'));
}
<|endoftext|>


<commit_before>import calculation from './calculation';
import {rock_paper_scissors, rock_paper_scissors_lizard_spock} from './configuration';
import * as CONSTANTS from './constants';
import _ from 'lodash';

describe(`Calculation for ${CONSTANTS.ROCK_PAPER_SCISSORS}`, () => {

    const data_drive_calculation = [
        {
            description: 'Using default calcuation configuration, Player Rock should win against opponents Scissors.',
            configuration: undefined,
            player_symbol: 'rock',
            opponent_symbol: 'scissors',
            expect: CONSTANTS.OUTCOME_WIN
        },
        {
            description: 'Player Rock should win against opponents Scissors.',
            configuration: rock_paper_scissors,
            player_symbol: 'rock',
            opponent_symbol: 'scissors',
            expect: CONSTANTS.OUTCOME_WIN
        },
        {
            description: 'Player Rock should draw with opponents Rock.',
            configuration: rock_paper_scissors,
            player_symbol: 'rock',
            opponent_symbol: 'rock',
            expect: CONSTANTS.OUTCOME_DRAW
        },
        {
            description: 'Players paper should lose to opponents Paper.',
            configuration: rock_paper_scissors,
            player_symbol: 'paper',
            opponent_symbol: 'scissors',
            expect: CONSTANTS.OUTCOME_LOSE
        }
    ];

    _.each(data_drive_calculation, (unit_test) => {
        it(unit_test.description, () => {
            // Arrange
            const player_symbol = unit_test.player_symbol;
            const opponent_symbol = unit_test.opponent_symbol;

            // Act
            let outcome = calculation(player_symbol, opponent_symbol, unit_test.configuration);

            // Assert
            expect(outcome).toEqual(unit_test.expect);
        });
    })
});<commit_msg>removed redundent test<commit_after>import calculation from './calculation';
import {rock_paper_scissors, rock_paper_scissors_lizard_spock} from './configuration';
import * as CONSTANTS from './constants';
import _ from 'lodash';

describe(`Calculation for ${CONSTANTS.ROCK_PAPER_SCISSORS}`, () => {

    const data_drive_calculation = [
        {
            description: 'Player Rock should win against opponents Scissors.',
            configuration: rock_paper_scissors,
            player_symbol: 'rock',
            opponent_symbol: 'scissors',
            expect: CONSTANTS.OUTCOME_WIN
        },
        {
            description: 'Player Rock should draw with opponents Rock.',
            configuration: rock_paper_scissors,
            player_symbol: 'rock',
            opponent_symbol: 'rock',
            expect: CONSTANTS.OUTCOME_DRAW
        },
        {
            description: 'Players paper should lose to opponents Paper.',
            configuration: rock_paper_scissors,
            player_symbol: 'paper',
            opponent_symbol: 'scissors',
            expect: CONSTANTS.OUTCOME_LOSE
        }
    ];

    _.each(data_drive_calculation, (unit_test) => {
        it(unit_test.description, () => {
            // Arrange
            const player_symbol = unit_test.player_symbol;
            const opponent_symbol = unit_test.opponent_symbol;

            // Act
            let outcome = calculation(player_symbol, opponent_symbol, unit_test.configuration);

            // Assert
            expect(outcome).toEqual(unit_test.expect);
        });
    })
});<|endoftext|>


<commit_before>/**
 * The MIT License (MIT)
 * Copyright (c) 2015-present Dmitry Soshnikov <dmitry.soshnikov@gmail.com>
 */

import CanonicalCollection from './canonical-collection';
import Grammar from '../grammar/grammar';
import LRParsingTable from './lr-parsing-table';
import {EOF} from '../special-symbols';

import fs from 'fs';
import path from 'path';
import vm from 'vm';

/**
 * Generic template for all LR parsers.
 */
const PARSER_TEMPLATE = fs.readFileSync(
`${__dirname}/../templates/lr.template`,
  'utf-8'
);

/**
 * LR parser generator. Creates a parser module for a given grammar, and
 * saves it to the `outputFile`.
 *
 * By default also generates code for a
 * tokenizer, unless `customTokenizer` is `false` (in which case the parser
 * will expect a custom tokenizer instance, that should implement its
 * interface.
 */
export default class LRParserGenerator {

  /**
   * Instance constructor.
   */
  constructor({
    grammar,
    outputFile,
    customTokenizer = null,
    resolveConflicts = false,
  }) {
    this._grammar = grammar;
    this._outputFile = outputFile;
    this._customTokenizer = customTokenizer;

    if (!grammar.getMode().isLR()) {
      throw new Error(`Only LR parsers are supported at the moment.`);
    }

    this._table = new LRParsingTable({
      canonicalCollection: new CanonicalCollection({
        grammar: this._grammar,
      }),
      grammar: this._grammar,
      resolveConflicts,
    });

    // Init the result data to the template, parts of which
    // are further generated (productions, lex-rules, table, etc).
    this._resultData = PARSER_TEMPLATE;

    this._nonTerminals = this._grammar
      .getNonTerminals()
      .map(symbol => symbol.getSymbol());

    this._tokens = this._grammar
      .getTokens()
      .map(symbol => symbol.getSymbol());

    this._tokens.push(EOF);
  }

  /**
   * Generates parser code and writes it to disk as a reusable module.
   */
  generate() {
    fs.writeFileSync(
      this._outputFile,
      this._generateParserData(),
      'utf-8'
    );
    return require(this._outputFile);
  }

  /**
   * Generates parser parts. If default tokenizer is used, generates code
   * for it as well, otherwise, users are supposed to provide custom
   * tokenizer.
   */
  _generateParserData() {
    // Arbitrary code included to the module.
    this._generateModuleInclude();

    // Parse begin hook.
    this._generateOnParseBegin();

    // Lexical grammar.
    if (this._customTokenizer === null) {
      // Built-in tokinizer.
      this._generateTokenizer();
      this._generateLexRules();
    } else {
      // Require custom tokenizer if was provided.
      this._resultData = this._resultData.replace(
        '<<TOKENIZER>>',
        () => `tokenizer = require('${this._customTokenizer}');`,
      );
    }

    // Syntactic grammar.
    this._generateProductions();
    this._generateTable();

    // Parse end hook.
    this._generateOnParseEnd();

    return this._resultData;
  }

  /**
   * Injects the code passed in the module include directive.
   */
  _generateModuleInclude() {
    this._resultData = this._resultData.replace(
      '<<MODULE_INCLUDE>>',
      () => this._grammar.getModuleInclude(),
    );
  }

  /**
   * Injects the code executed on parse begin.
   */
  _generateOnParseBegin() {
    this._resultData = this._resultData.replace(
      '<<ON_PARSE_BEGIN>>',
      () => this._grammar.getOnParseBegin(),
    );
  }

  /**
   * Injects the code executed on parse end.
   */
  _generateOnParseEnd() {
    this._resultData = this._resultData.replace(
      '<<ON_PARSE_END>>',
      () => this._grammar.getOnParseEnd(),
    );
  }

  /**
   * Generates built-in tokenizer instance.
   */
  _generateTokenizer() {
    let tokenizerCode = fs.readFileSync(
      `${__dirname}/../templates/tokenizer.template`,
      'utf-8'
    );

    this._resultData = this._resultData.replace(
      '<<TOKENIZER>>',
      () => tokenizerCode,
    );
  }

  /**
   * Generates rules for tokenizer.
   */
  _generateLexRules() {
    let lexRules = this._grammar.getLexRules().map(lexRule => {
      return `[${lexRule.getMatcher()}, () => { ${lexRule.getRawHandler()} }]`;
    });

    this._resultData = this._resultData.replace(
      '<<LEX_RULES>>',
      () => `[${lexRules.join(',\n')}]`,
    );
  }

  /**
   * Format of the production is:
   * [Non-terminal index, RHS.length, semanticAction]
   */
  _generateProductions() {
    let productions = this._grammar.getProductions().map(production => {

      let LHS = production.getLHS().getSymbol().replace(/'/g, "\\'");
      let RHSLength = production.isEpsilon() ? 0 : production.getRHS().length;
      let rawSemanticAction = production.getRawSemanticAction();
      let semanticAction = null;

      if (rawSemanticAction) {
        // Builds a string of args: '$1, $2, $3...'
        let args = [...Array(RHSLength)]
          .map((_, i) => `$${i + 1}`)
          .join(',');

        semanticAction = `(${args}) => { ${rawSemanticAction} }`;
      }

      return `[${this._nonTerminals.indexOf(LHS)}, ${RHSLength}` +
        (semanticAction ? `, ${semanticAction}` : '') + ']';
    });

    this._resultData = this._resultData.replace(
      '<<PRODUCTIONS>>',
      () => `[${productions.join(',\n')}]`,
    );
  }

  /**
   * Actual parsing table.
   */
  _generateTable() {
    let originalTable = this._table.get();
    let table = {};

    // Encode tokens table: token indices start after non-terminal indices.
    let tokens = {};
    for (let k = 0; k < this._tokens.length; k++) {
      tokens[this._tokens[k]] = this._nonTerminals.length + k;
    }
    this._resultData = this._resultData.replace(
      '<<TOKENS>>',
      () => JSON.stringify(tokens),
    );

    for (let state in originalTable) {
      let row = {};
      let originalRow = originalTable[state];

      for (let symbol in originalRow) {
        let entry = originalRow[symbol];
        let nonTerminalIndex = this._nonTerminals.indexOf(symbol);
        let tokenIndex = tokens[symbol];
        // Format of a row: {
        //  <nonTerminalIndex from the production table> |
        //  <tokenIndex> :
        //    table-entry
        // }
        row[
          nonTerminalIndex !== -1
            ? nonTerminalIndex
            : tokenIndex
        ] = entry;
      }

      table[state] = row;
    }

    this._resultData = this._resultData.replace(
      '<<TABLE>>',
      () => JSON.stringify(table),
    );
  }

  /**
   * Creates a generator instance from a grammar file,
   * for the specific parsing mode.
   */
  static fromGrammarFile({
    grammarFile,
    mode,
    outputFile,
    customTokenizer = null,
  }) {
    return new LRParserGenerator({
      grammar: this.loadGrammar(grammarFile, mode),
      outputFile: outputFile || `${grammarFile}.parser.js`,
      customTokenizer,
    });
  }

  /**
   * Loads a grammar object from a grammar file,
   * for the specific parsing mode.
   */
  static loadGrammar(grammarFile, mode) {
    let grammarData = this.loadGrammarData(grammarFile);
    grammarData.mode = mode;
    return new Grammar(grammarData);
  }

  /**
   * Reads grammar file data.
   */
  static loadGrammarData(grammarFile) {
    let rawGrammarData = fs.readFileSync(grammarFile, 'utf-8');
    let grammarData;

    try {
      // An object with `lex`, and `bnf`, valid JSON.
      grammarData = JSON.parse(rawGrammarData);
    } catch (e) {
      // JS code.
      try {
        grammarData = vm.runInNewContext(`
          (function() { return (${rawGrammarData});})()
        `);
      } catch (e) {
        // Just a bnf as a string.
        grammarData = {
          bnf: rawGrammarData,
        };
      }
    }

    return grammarData;
  }

};
<commit_msg>Fix terminals in parser generator<commit_after>/**
 * The MIT License (MIT)
 * Copyright (c) 2015-present Dmitry Soshnikov <dmitry.soshnikov@gmail.com>
 */

import CanonicalCollection from './canonical-collection';
import Grammar from '../grammar/grammar';
import LRParsingTable from './lr-parsing-table';
import {EOF} from '../special-symbols';

import fs from 'fs';
import path from 'path';
import vm from 'vm';

/**
 * Generic template for all LR parsers.
 */
const PARSER_TEMPLATE = fs.readFileSync(
`${__dirname}/../templates/lr.template`,
  'utf-8'
);

/**
 * LR parser generator. Creates a parser module for a given grammar, and
 * saves it to the `outputFile`.
 *
 * By default also generates code for a
 * tokenizer, unless `customTokenizer` is `false` (in which case the parser
 * will expect a custom tokenizer instance, that should implement its
 * interface.
 */
export default class LRParserGenerator {

  /**
   * Instance constructor.
   */
  constructor({
    grammar,
    outputFile,
    customTokenizer = null,
    resolveConflicts = false,
  }) {
    this._grammar = grammar;
    this._outputFile = outputFile;
    this._customTokenizer = customTokenizer;

    if (!grammar.getMode().isLR()) {
      throw new Error(`Only LR parsers are supported at the moment.`);
    }

    this._table = new LRParsingTable({
      canonicalCollection: new CanonicalCollection({
        grammar: this._grammar,
      }),
      grammar: this._grammar,
      resolveConflicts,
    });

    // Init the result data to the template, parts of which
    // are further generated (productions, lex-rules, table, etc).
    this._resultData = PARSER_TEMPLATE;

    this._nonTerminals = this._grammar
      .getNonTerminals()
      .map(symbol => symbol.getSymbol());

    this._tokens = this._grammar
      .getTokens()
      .concat(this._grammar.getTerminals())
      .map(symbol => symbol.getSymbol());

    this._tokens.push(EOF);
  }

  /**
   * Generates parser code and writes it to disk as a reusable module.
   */
  generate() {
    fs.writeFileSync(
      this._outputFile,
      this._generateParserData(),
      'utf-8'
    );
    return require(this._outputFile);
  }

  /**
   * Generates parser parts. If default tokenizer is used, generates code
   * for it as well, otherwise, users are supposed to provide custom
   * tokenizer.
   */
  _generateParserData() {
    // Arbitrary code included to the module.
    this._generateModuleInclude();

    // Parse begin hook.
    this._generateOnParseBegin();

    // Lexical grammar.
    if (this._customTokenizer === null) {
      // Built-in tokinizer.
      this._generateTokenizer();
      this._generateLexRules();
    } else {
      // Require custom tokenizer if was provided.
      this._resultData = this._resultData.replace(
        '<<TOKENIZER>>',
        () => `tokenizer = require('${this._customTokenizer}');`,
      );
    }

    // Syntactic grammar.
    this._generateProductions();
    this._generateTable();

    // Parse end hook.
    this._generateOnParseEnd();

    return this._resultData;
  }

  /**
   * Injects the code passed in the module include directive.
   */
  _generateModuleInclude() {
    this._resultData = this._resultData.replace(
      '<<MODULE_INCLUDE>>',
      () => this._grammar.getModuleInclude(),
    );
  }

  /**
   * Injects the code executed on parse begin.
   */
  _generateOnParseBegin() {
    this._resultData = this._resultData.replace(
      '<<ON_PARSE_BEGIN>>',
      () => this._grammar.getOnParseBegin(),
    );
  }

  /**
   * Injects the code executed on parse end.
   */
  _generateOnParseEnd() {
    this._resultData = this._resultData.replace(
      '<<ON_PARSE_END>>',
      () => this._grammar.getOnParseEnd(),
    );
  }

  /**
   * Generates built-in tokenizer instance.
   */
  _generateTokenizer() {
    let tokenizerCode = fs.readFileSync(
      `${__dirname}/../templates/tokenizer.template`,
      'utf-8'
    );

    this._resultData = this._resultData.replace(
      '<<TOKENIZER>>',
      () => tokenizerCode,
    );
  }

  /**
   * Generates rules for tokenizer.
   */
  _generateLexRules() {
    let lexRules = this._grammar.getLexRules().map(lexRule => {
      return `[${lexRule.getMatcher()}, () => { ${lexRule.getRawHandler()} }]`;
    });

    this._resultData = this._resultData.replace(
      '<<LEX_RULES>>',
      () => `[${lexRules.join(',\n')}]`,
    );
  }

  /**
   * Format of the production is:
   * [Non-terminal index, RHS.length, semanticAction]
   */
  _generateProductions() {
    let productions = this._grammar.getProductions().map(production => {

      let LHS = production.getLHS().getSymbol().replace(/'/g, "\\'");
      let RHSLength = production.isEpsilon() ? 0 : production.getRHS().length;
      let rawSemanticAction = production.getRawSemanticAction();
      let semanticAction = null;

      if (rawSemanticAction) {
        // Builds a string of args: '$1, $2, $3...'
        let args = [...Array(RHSLength)]
          .map((_, i) => `$${i + 1}`)
          .join(',');

        semanticAction = `(${args}) => { ${rawSemanticAction} }`;
      }

      return `[${this._nonTerminals.indexOf(LHS)}, ${RHSLength}` +
        (semanticAction ? `, ${semanticAction}` : '') + ']';
    });

    this._resultData = this._resultData.replace(
      '<<PRODUCTIONS>>',
      () => `[${productions.join(',\n')}]`,
    );
  }

  /**
   * Actual parsing table.
   */
  _generateTable() {
    let originalTable = this._table.get();
    let table = {};

    // Encode tokens table: token indices start after non-terminal indices.
    let tokens = {};
    for (let k = 0; k < this._tokens.length; k++) {
      tokens[this._tokens[k]] = this._nonTerminals.length + k;
    }
    this._resultData = this._resultData.replace(
      '<<TOKENS>>',
      () => JSON.stringify(tokens),
    );

    for (let state in originalTable) {
      let row = {};
      let originalRow = originalTable[state];

      for (let symbol in originalRow) {
        let entry = originalRow[symbol];
        let nonTerminalIndex = this._nonTerminals.indexOf(symbol);
        let tokenIndex = tokens[symbol];
        // Format of a row: {
        //  <nonTerminalIndex from the production table> |
        //  <tokenIndex> :
        //    table-entry
        // }
        row[
          nonTerminalIndex !== -1
            ? nonTerminalIndex
            : tokenIndex
        ] = entry;
      }

      table[state] = row;
    }

    this._resultData = this._resultData.replace(
      '<<TABLE>>',
      () => JSON.stringify(table),
    );
  }

  /**
   * Creates a generator instance from a grammar file,
   * for the specific parsing mode.
   */
  static fromGrammarFile({
    grammarFile,
    mode,
    outputFile,
    customTokenizer = null,
  }) {
    return new LRParserGenerator({
      grammar: this.loadGrammar(grammarFile, mode),
      outputFile: outputFile || `${grammarFile}.parser.js`,
      customTokenizer,
    });
  }

  /**
   * Loads a grammar object from a grammar file,
   * for the specific parsing mode.
   */
  static loadGrammar(grammarFile, mode) {
    let grammarData = this.loadGrammarData(grammarFile);
    grammarData.mode = mode;
    return new Grammar(grammarData);
  }

  /**
   * Reads grammar file data.
   */
  static loadGrammarData(grammarFile) {
    let rawGrammarData = fs.readFileSync(grammarFile, 'utf-8');
    let grammarData;

    try {
      // An object with `lex`, and `bnf`, valid JSON.
      grammarData = JSON.parse(rawGrammarData);
    } catch (e) {
      // JS code.
      try {
        grammarData = vm.runInNewContext(`
          (function() { return (${rawGrammarData});})()
        `);
      } catch (e) {
        // Just a bnf as a string.
        grammarData = {
          bnf: rawGrammarData,
        };
      }
    }

    return grammarData;
  }

};
<|endoftext|>


<commit_before>var marked   = require('marked');
var nunjucks = require('nunjucks');
var path     = require('path');

module.exports = function(options) {

  options || (options = {});
  options.path || (options.path = 'template');

  var env = new nunjucks.Environment(
    new nunjucks.FileSystemLoader(options.path),
    {noCache: true}
  );
  env.addFilter('markdown', function(str) {
    return new nunjucks.runtime.SafeString(marked(str));
  });

  return function(tmplName, tmplData) {
    return env.render(tmplName, tmplData);
  };
};
<commit_msg>Make markdown filter not to throw error for null or undefined<commit_after>var marked   = require('marked');
var nunjucks = require('nunjucks');
var path     = require('path');

module.exports = function(options) {

  options || (options = {});
  options.path || (options.path = 'template');

  var env = new nunjucks.Environment(
    new nunjucks.FileSystemLoader(options.path),
    {noCache: true}
  );
  env.addFilter('markdown', function(str) {
    if (!str) {
      return str;
    }
    return new nunjucks.runtime.SafeString(marked(str));
  });

  return function(tmplName, tmplData) {
    return env.render(tmplName, tmplData);
  };
};
<|endoftext|>