<commit_before>import Change from './listener/change.js';
import Submit from './listener/submit.js';
import Input from './listener/input.js';
import Reset from './listener/reset.js';
import Click from './listener/click.js';
import State from './listener/state.js';
import Load from './listener/load.js';
import Component from './component.js';
import General from './general.js';
import Utility from './utility.js';
import Batcher from './batcher.js';
import Fetcher from './fetcher.js';
import Methods from './methods.js'
import Router from './router.js';
import Loader from './loader.js';
import Binder from './binder.js';
import Model from './model.js';

// OPTIMIZE wait until polyfill are ready then allow setup

const eStyle = document.createElement('style');
const tStyle = document.createTextNode(' \
	o-router, o-router > :first-child { \
		display: block; \
		animation: o-transition 150ms ease-in-out; \
	} \
	@keyframes o-transition { \
		0% { opacity: 0; } \
		100% { opacity: 1; } \
	} \
');

eStyle.setAttribute('type', 'text/css');
eStyle.appendChild(tStyle);
document.head.appendChild(eStyle);

const oSetup = document.querySelector('script[o-setup]');

if (oSetup) {

	let currentCount = 0;
	let requiredCount = 0;

	const loaded = function () {
		if (currentCount !== requiredCount) return;

		document.addEventListener('load', Load, true);
		document.addEventListener('input', Input, true);
		document.addEventListener('reset', Reset, true);
		document.addEventListener('click', Click, true);
		document.addEventListener('submit', Submit, true);
		document.addEventListener('change', Change, true);
		window.addEventListener('popstate', State, true);

		const args = oSetup.getAttribute('o-setup').split(/\s*,\s*/);
		const meta = document.querySelector('meta[name="oxe"]');

		if (meta && meta.hasAttribute('compiled')) {
			args[1] = 'null';
			args[2] = 'script';
			Router.compiled = true;
			General.compiled = true;
			Component.compiled = true;
		}

		if (!args[0]) {
			throw new Error('Oxe - o-setup attribute requires a url');
		}

		if (args.length > 1) {
			Loader.load({
				url: args[0],
				method: args[2],
				transformer: args[1]
			});
		} else {
			const index = document.createElement('script');
			index.setAttribute('src', args[0]);
			index.setAttribute('async', 'true');
			index.setAttribute('type', 'module');
			document.head.appendChild(index);
		}

		document.registerElement('o-router', {
			prototype: Object.create(HTMLElement.prototype)
		});

	};

	const loader = function (url, callback) {
		const polly = document.createElement('script');

		polly.setAttribute('async', 'true');
		polly.setAttribute('src', url);
		polly.addEventListener('load', function () {
			currentCount++;
			callback();
		}, true);

		document.head.appendChild(polly);
	};

	const features = [];
	const isNotFetch = !('fetch' in window);
	const isNotAssign = !('assign' in Object);
	const isNotPromise = !('Promise' in window);
	const isNotCustomElement = !('registerElement' in document) || !('content' in document.createElement('template'));

	if (isNotFetch) features.push('fetch');
	if (isNotPromise) features.push('Promise');
	if (isNotAssign) features.push('Object.assign');

	if (isNotPromise || isNotFetch || isNotAssign) {
		requiredCount++;
		loader('https://cdn.polyfill.io/v2/polyfill.min.js?features=' + features.join(','), loaded);
	}

	if (isNotCustomElement) {
		requiredCount++;
		loader('https://cdnjs.cloudflare.com/ajax/libs/document-register-element/1.7.2/document-register-element.js', loaded);
	}

	loaded();
}

class Oxe {

	constructor () {
		this.compiled = true;
	}

	get window () {
		return window;
	}

	get document () {
		return window.document;
	}

	get body () {
		return window.document.body;
	}

	get head () {
		return window.document.head;
	}

	get location () {
		return this.router.location;
	}

	get currentScript () {
		return (window.document._currentScript || window.document.currentScript);
	}

	get ownerDocument () {
		return (window.document._currentScript || window.document.currentScript).ownerDocument;
	}

	get global () {
		return {};
	}

	get methods () {
		return 	Methods;
	}

	get utility () {
		return Utility;
	}

	get general () {
		return General;
	}

	get batcher () {
		return Batcher;
	}

	get loader () {
		return Loader;
	}

	get binder () {
		return Binder;
	}

	get fetcher () {
		return Fetcher;
	}

	get component () {
		return Component;
	}

	get router () {
		return Router;
	}

	get model () {
		return Model;
	}

	async setup (data) {

		if (this._setup) {
			return;
		} else {
			this._setup = true;
		}

		data = data || {};

		if (data.listener && data.listener.before) {
			data.listener.before();
		}

		if (data.general) {
			this.general.setup(data.general);
		}

		if (data.fetcher) {
			this.fetcher.setup(data.fetcher);
		}

		if (data.loader) {
			this.loader.setup(data.loader);
		}

		if (data.component) {
			this.component.setup(data.component);
		}

		if (data.router) {
			this.router.setup(data.router);
		}

		if (data.listener && data.listener.after) {
			data.listener.after();
		}

	}

}

export default new Oxe();
<commit_msg>formating<commit_after>import Change from './listener/change.js';
import Submit from './listener/submit.js';
import Input from './listener/input.js';
import Reset from './listener/reset.js';
import Click from './listener/click.js';
import State from './listener/state.js';
import Load from './listener/load.js';
import Component from './component.js';
import General from './general.js';
import Utility from './utility.js';
import Batcher from './batcher.js';
import Fetcher from './fetcher.js';
import Methods from './methods.js'
import Router from './router.js';
import Loader from './loader.js';
import Binder from './binder.js';
import Model from './model.js';

// OPTIMIZE wait until polyfill are ready then allow setup

const eStyle = document.createElement('style');
const tStyle = document.createTextNode(`
	o-router, o-router > :first-child {
		display: block;
		animation: o-transition 150ms ease-in-out;
	}
	@keyframes o-transition {
		0% { opacity: 0; }
		100% { opacity: 1; }
	}
`);

eStyle.setAttribute('type', 'text/css');
eStyle.appendChild(tStyle);
document.head.appendChild(eStyle);

const oSetup = document.querySelector('script[o-setup]');

if (oSetup) {

	let currentCount = 0;
	let requiredCount = 0;

	const loaded = function () {
		if (currentCount !== requiredCount) return;

		document.addEventListener('load', Load, true);
		document.addEventListener('input', Input, true);
		document.addEventListener('reset', Reset, true);
		document.addEventListener('click', Click, true);
		document.addEventListener('submit', Submit, true);
		document.addEventListener('change', Change, true);
		window.addEventListener('popstate', State, true);

		const args = oSetup.getAttribute('o-setup').split(/\s*,\s*/);
		const meta = document.querySelector('meta[name="oxe"]');

		if (meta && meta.hasAttribute('compiled')) {
			args[1] = 'null';
			args[2] = 'script';
			Router.compiled = true;
			General.compiled = true;
			Component.compiled = true;
		}

		if (!args[0]) {
			throw new Error('Oxe - o-setup attribute requires a url');
		}

		if (args.length > 1) {
			Loader.load({
				url: args[0],
				method: args[2],
				transformer: args[1]
			});
		} else {
			const index = document.createElement('script');

			index.setAttribute('src', args[0]);
			index.setAttribute('async', 'true');
			index.setAttribute('type', 'module');

			document.head.appendChild(index);
		}

		document.registerElement('o-router', {
			prototype: Object.create(HTMLElement.prototype)
		});

	};

	const loader = function (url, callback) {
		const polly = document.createElement('script');

		polly.setAttribute('async', 'true');
		polly.setAttribute('src', url);
		polly.addEventListener('load', function () {
			currentCount++;
			callback();
		}, true);

		document.head.appendChild(polly);
	};

	const features = [];
	const isNotFetch = !('fetch' in window);
	const isNotAssign = !('assign' in Object);
	const isNotPromise = !('Promise' in window);
	const isNotCustomElement = !('registerElement' in document) || !('content' in document.createElement('template'));

	if (isNotFetch) features.push('fetch');
	if (isNotPromise) features.push('Promise');
	if (isNotAssign) features.push('Object.assign');

	if (isNotPromise || isNotFetch || isNotAssign) {
		requiredCount++;
		loader('https://cdn.polyfill.io/v2/polyfill.min.js?features=' + features.join(','), loaded);
	}

	if (isNotCustomElement) {
		requiredCount++;
		loader('https://cdnjs.cloudflare.com/ajax/libs/document-register-element/1.7.2/document-register-element.js', loaded);
	}

	loaded();
}

class Oxe {

	constructor () {
		this.compiled = true;
	}

	get window () {
		return window;
	}

	get document () {
		return window.document;
	}

	get body () {
		return window.document.body;
	}

	get head () {
		return window.document.head;
	}

	get location () {
		return this.router.location;
	}

	get currentScript () {
		return (window.document._currentScript || window.document.currentScript);
	}

	get ownerDocument () {
		return (window.document._currentScript || window.document.currentScript).ownerDocument;
	}

	get global () {
		return {};
	}

	get methods () {
		return 	Methods;
	}

	get utility () {
		return Utility;
	}

	get general () {
		return General;
	}

	get batcher () {
		return Batcher;
	}

	get loader () {
		return Loader;
	}

	get binder () {
		return Binder;
	}

	get fetcher () {
		return Fetcher;
	}

	get component () {
		return Component;
	}

	get router () {
		return Router;
	}

	get model () {
		return Model;
	}

	async setup (data) {

		if (this._setup) {
			return;
		} else {
			this._setup = true;
		}

		data = data || {};

		if (data.listener && data.listener.before) {
			data.listener.before();
		}

		if (data.general) {
			this.general.setup(data.general);
		}

		if (data.fetcher) {
			this.fetcher.setup(data.fetcher);
		}

		if (data.loader) {
			this.loader.setup(data.loader);
		}

		if (data.component) {
			this.component.setup(data.component);
		}

		if (data.router) {
			this.router.setup(data.router);
		}

		if (data.listener && data.listener.after) {
			data.listener.after();
		}

	}

}

export default new Oxe();
<|endoftext|>


<commit_before>/* eslint-disable no-underscore-dangle */
require('babel-polyfill');
const writeModule = require('./util/tmpFile');
const chalk = require('chalk');
const rimraf = require('rimraf');

const TMP_DIR = `${__dirname}/tmp`;
const FILE_MOCK_DEPENDENCY_PATH = `${__dirname}/mocks/fileMock`;
const STYLE_MOCK_DEPENDENCY_PATH = `${__dirname}/mocks/styleMock`;
const JS_FILE_REGEX = /\.(js|jsx)$/;
const FILE_MOCK_REGEX = /\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$/;
const STYLE_MOCK_REGEX = /\.(css|less|sass)$/;

const isReactComponent = source => source.indexOf('(_react.Component)') > -1;

const isExternalDependency = module =>
  module && module.resource && module.resource.indexOf('node_modules') > -1;

const getSourceCode = module => module._source && module._source._value;

const handleDependencies = (module) => {
  const { dependencies: moduleDependencies = [] } = module;

  // Gather internal dependencies.
  const dependencies = moduleDependencies
    .map(dependency => dependency.module)
    .filter(
      dependency =>
        isExternalDependency(dependency) === false && dependency.resource !== module.resource,
    );

  if (dependencies.length === 0) {
    return module;
  }

  // Return module with modified source code that changes requires to point
  // to tmp files with compiled dependencies.
  try {
    const revisedModule = dependencies.reduce(async (m = {}, dependency) => {
      // Recurse through all dependencies.
      handleDependencies(dependency);

      // Async functions return a promise.
      const modifiedModule = await m;
      const pathToReplaceInSource = dependency.rawRequest;
      const source = modifiedModule.modifiedSource || getSourceCode(modifiedModule) || '';
      const dependencyPaths = modifiedModule.dependencyPaths || [];

      if (STYLE_MOCK_REGEX.test(dependency.userRequest)) {
        return {
          ...modifiedModule,
          modifiedSource: source.replace(pathToReplaceInSource, STYLE_MOCK_DEPENDENCY_PATH),
        };
      } else if (FILE_MOCK_REGEX.test(dependency.userRequest)) {
        return {
          ...modifiedModule,
          modifiedSource: source.replace(pathToReplaceInSource, FILE_MOCK_DEPENDENCY_PATH),
        };
      }

      const newDependencyPath = await writeModule(TMP_DIR, getSourceCode(dependency));
      return {
        ...modifiedModule,
        modifiedSource: source.replace(pathToReplaceInSource, newDependencyPath),
        // Paths to clean up afterwards.
        dependencyPaths: dependencyPaths.concat([newDependencyPath]),
      };
    }, module);

    return revisedModule;
  } catch (e) {
    throw e;
  }
};

class AccessibilityWebpackPlugin {
  constructor(options) {
    this.createElement = options.createElement;
    this.renderMarkup = options.renderMarkup;
  }

  apply(compiler) {
    compiler.plugin('emit', (compilation, callback) => {
      compilation.chunks.forEach((chunk) => {
        chunk.modules
          .filter(
            module => isExternalDependency(module) === false && JS_FILE_REGEX.test(module.resource),
          )
          .filter(module => isReactComponent(getSourceCode(module)))
          .forEach(async (module) => {
            try {
              const modifiedModule = await handleDependencies(module);
              const source = modifiedModule.modifiedSource || getSourceCode(module) || '';
              var dependencyPaths = modifiedModule.dependencyPaths || [];

              var path = await writeModule(TMP_DIR, source);

              // Inject component library (React, preact, etc)
              // require(this.componentLibrary); // eslint-disable-line
              const component = require(path).default; // eslint-disable-line
              const element = this.createElement(component);
              const markup = this.renderMarkup(element);

              // Run a11y report on markup!
              console.log(chalk.green(`<${component.name}>: `), markup, '\n');
            } catch (e) {
              throw e;
            } finally {
              // Clean up tmp files.
              // TODO: create glob from list of paths to make this one rimraf call.
              dependencyPaths.concat([path]).forEach(file => rimraf(file, () => {}));
            }
          });
      });

      callback();
    });
  }
}

module.exports = AccessibilityWebpackPlugin;
/* eslint-enable no-underscore-dangle */
<commit_msg>Clean up.<commit_after>/* eslint-disable no-underscore-dangle */
require('babel-polyfill');
const writeModule = require('./util/tmpFile');
const chalk = require('chalk');
const rimraf = require('rimraf');

const TMP_DIR = `${__dirname}/tmp`;
const FILE_MOCK_DEPENDENCY_PATH = `${__dirname}/mocks/fileMock`;
const STYLE_MOCK_DEPENDENCY_PATH = `${__dirname}/mocks/styleMock`;
const JS_FILE_REGEX = /\.(js|jsx)$/;
const FILE_MOCK_REGEX = /\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$/;
const STYLE_MOCK_REGEX = /\.(css|less|sass)$/;

const isReactComponent = source => source.indexOf('(_react.Component)') > -1;

const isExternalDependency = module =>
  module && module.resource && module.resource.indexOf('node_modules') > -1;

const getSourceCode = module => module._source && module._source._value;

const handleDependencies = (module) => {
  const { dependencies: moduleDependencies = [] } = module;

  // Gather dependencies that have relative paths (in-app).
  const dependencies = moduleDependencies
    .map(dependency => dependency.module)
    .filter(
      dependency =>
        isExternalDependency(dependency) === false && dependency.resource !== module.resource,
    );

  if (dependencies.length === 0) {
    return module;
  }

  // Return module with modified source code that changes requires to point
  // to tmp files with compiled dependencies.
  try {
    const revisedModule = dependencies.reduce(async (m = {}, dependency) => {
      // Recurse through all dependencies.
      handleDependencies(dependency);

      // Async functions return a promise.
      const modifiedModule = await m;
      const pathToReplaceInSource = dependency.rawRequest;
      const source = modifiedModule.modifiedSource || getSourceCode(modifiedModule) || '';
      const dependencyPaths = modifiedModule.dependencyPaths || [];

      if (STYLE_MOCK_REGEX.test(dependency.userRequest)) {
        return {
          ...modifiedModule,
          modifiedSource: source.replace(pathToReplaceInSource, STYLE_MOCK_DEPENDENCY_PATH),
        };
      } else if (FILE_MOCK_REGEX.test(dependency.userRequest)) {
        return {
          ...modifiedModule,
          modifiedSource: source.replace(pathToReplaceInSource, FILE_MOCK_DEPENDENCY_PATH),
        };
      }

      const newDependencyPath = await writeModule(TMP_DIR, getSourceCode(dependency));
      return {
        ...modifiedModule,
        modifiedSource: source.replace(pathToReplaceInSource, newDependencyPath),
        // Paths to clean up afterwards.
        dependencyPaths: dependencyPaths.concat([newDependencyPath]),
      };
    }, module);

    return revisedModule;
  } catch (e) {
    throw e;
  }
};

class AccessibilityWebpackPlugin {
  constructor(options) {
    this.createElement = options.createElement;
    this.renderMarkup = options.renderMarkup;
  }

  apply(compiler) {
    compiler.plugin('emit', (compilation, callback) => {
      compilation.chunks.forEach((chunk) => {
        chunk.modules
          .filter(
            module => isExternalDependency(module) === false && JS_FILE_REGEX.test(module.resource),
          )
          .filter(module => isReactComponent(getSourceCode(module)))
          .forEach(async (module) => {
            try {
              const modifiedModule = await handleDependencies(module);
              const source = modifiedModule.modifiedSource || getSourceCode(module) || '';
              var dependencyPaths = modifiedModule.dependencyPaths || [];

              var path = await writeModule(TMP_DIR, source);

              // Inject component library (React, preact, etc)
              const component = require(path).default; // eslint-disable-line
              const element = this.createElement(component);
              const markup = this.renderMarkup(element);

              // Run a11y report on markup!
              console.log(chalk.green(`<${component.name}>: `), markup, '\n');
            } catch (e) {
              throw e;
            } finally {
              // Clean up tmp files.
              // TODO: create glob from list of paths to make this one rimraf call.
              // Not sure how safe this would be if the LCS is something outside the scope of the project.
              dependencyPaths.concat([path]).forEach(file => rimraf(file, () => {}));
            }
          });
      });

      callback();
    });
  }
}

module.exports = AccessibilityWebpackPlugin;
/* eslint-enable no-underscore-dangle */
<|endoftext|>


<commit_before>'use strict'

//  ▄▄ •             ▄▄▄ . ▄· ▄▌
// ▐█ ▀ ▪▪     ▪     ▀▄.▀·▐█▪██▌
// ▄█ ▀█▄ ▄█▀▄  ▄█▀▄ ▐▀▀▪▄▐█▌▐█▪
// ▐█▄▪▐█▐█▌.▐▌▐█▌.▐▌▐█▄▄▌ ▐█▀·.
// ·▀▀▀▀  ▀█▄▀▪ ▀█▄▀▪ ▀▀▀   ▀ •

import jsonPath from 'jsonpath'

import * as traversals from './traverse'
import * as topic from './topic'
import * as _util from './util'

/**
 * Flat map of all registered services, indexed by name
 */
var _services = {}

/**
 * Default service configuration object
 */
const _config = {
  strict: true,

  data: {
    matching: true,

    publish: {
      ignore: {
        falsy: true
      }
    }
  }
}

/**
 * A canonical, hierarchical, and composable data source that can publish and receive updates bi-directionally with other services
 * Forms a full k-ary tree that exists in a global forest
 */
export class Service {

  /**
   * Creates and registers a new gooey service with the current module
   *
   * @param {String} name
   * @param {?Function} model
   * @param {?Service} parent
   * @param {?Array} children
   * @param {?Object} config
   */
  constructor(name: String, model?: Function, parent?: Service, children?: Array = [], config?: Object = _config) {
    if (Object.is(name, undefined) || Service.isRegistered(name)) {
      throw `Services must have unique names: ${name}`
    }

    this.name = name
    this.model = model
    this.state = {}
    this.parent = parent instanceof Service ? parent.relateTo(this) : null
    this.children = this.relateToAll(children)
    this.subscriptions = []
    this.config = config
    this.symbol = Symbol(name)

    _services[name] = this

    if (this.model instanceof Function) {
      this.model(this.state)
    }
  }

  /**
   * Getter alias for `data`
   *
   * @returns {Object}
   */
  get data() {
    return this.state
  }

  /**
   * Setter for `data` that automatically publishes changes with default traversal settings
   *
   * @param {Object} data
   */
  set data(data: Object) {
    this.update(data)
  }

  /**
   * Traverses service tree via a conflict-free path and matches subscribers against the published data
   *
   * @param {Object} data
   * @param {?String} traversal
   * @param {?String} direction
   * @returns {Promise} deferred service tree traversal(s)
   */
  // TODO - Allow users to publish data with a certain key
  // - that way you aren't forced to always write a JsonPath or matcher function for each subscribe / publish
  publish(data, traversal: String = 'breadth', direction: String = 'down'): Promise {
    return new Promise((resolve, reject) => {
      // ensure data is pure
      data = data instanceof Object ? Object.assign({}, data) : data

      // process data against matching subscribers
      const matches = this.subscriptions.map(subscrip => subscrip.process(data, false))

      // final result is either converged conflict-resolved subscriber match or cloned source data
      const result = matches[0] || data

      // traverse service node tree and publish result on each "next" node
      return this.traverse(traversal, direction,
        next => next.publish(result, traversal, direction)
      )
    })
  }

  /**
   * Creates and registers a publish subsubscription with the Service
   *
   * @param {String} path
   * @param {Function} on
   */
  subscribe(topic: Object = '$', on: Function): Subscription {
    const subscrip = new Subscription(this, topic, on)

    this.subscriptions.push(subscrip)

    return subscrip
  }

  /**
   * Deregisters a subsubscription from the Service
   *
   * @param {Subscription} subscrip
   */
  unsubscribe(subscrip: Subscription) {
    this.subscriptions.splice(this.subscriptions.indexOf(subscrip), 1)
  }

  /**
   * Updates the Service's canonical data source with new data and publishs the change
   *
   * @param {Object} data
   * @returns {Promise}
   */
  update(data: Object, ...rest): Promise {
    this.state = data

    return this.publish(data, ...rest)
  }

  /**
   * Merges and updates the Service's canonical date source with a new (cloned) data object and publishs the change
   *
   * @param {Object} data
   * @param {?Function} error
   * @returns {Promise}
   */
  merge(data: Object, ...rest): Promise {
    const merged = data instanceof Object ? Object.assign({}, this.state, data) : this.state

    return this.update(merged, ...rest)
  }

  /**
   * Alias for subscribe
   *
   * @param {String} path
   * @param {Function} on
   * @returns {Subscription}
   */
  on(path: String, on: Function): Subscription {
    return this.subscribe(path, on)
  }

  /**
   * Alias for update
   *
   * @param {Object} data
   * @returns {Promise}
   */
  use(data, ...rest): Promise {
    return this.update(data, ...rest)
  }

  /**
   * Alias for merge
   *
   * @param {Object} data
   * @returns {Promise}
   */
  up(data, ...rest): Promise {
    return this.merge(data, ...rest)
  }

  /**
   * Determines set of data that matches the provided subsubscription's path/pattern
   *
   * @param {Object} data
   * @param {Subscription} subscrip
   * @returns {Set}
   */
  matches(data, subscrip: Subscription): Set {
    return subscrip.matches(data)
  }

  /**
   * Recursively traverses service tree via provided `next` function
   *
   * @param {String} traversal supported values defined by gooey.traverse.patterns
   * @param {String} direction up, down or bi
   * @param {Function} next
   * @returns {Promise}
   */
  traverse(traversal: String, direction: String, next: Function): Promise {
    return traversals.step.call(this, traversal, direction, next)
  }

  /**
   * Establishes strong acyclic child relationship with provided service.
   * Child services inherit publications from their parent.
   * The opposite is also supported via `up` traversals.
   * Silently fails if a cyclic relationship is proposed.
   *
   * @param {Service} child service to relate to
   * @returns {Service} modified service with new child relationship
   */
  relateTo(child: Service): Service {
    this.children.push(child)

    if (Service.cycleExists()) {
      this.children.pop() // FIXME - bleh, needs improvement to say the least
    }

    return this
  }

  /**
   * Establishes service as parent to each provided child service.
   * Child services inherit publications from their parent.
   * The opposite is also supported via `up` traversals.
   *
   * @param {Array} children services to relate to
   * @returns {Array} modified children services with new parent relationship
   */
  relateToAll(children: Array): Array {
    return children.map(c => {
      c.parent = this

      // this.relateTo(c) // TODO/FIXME

      return c
    })
  }

  /**
   * Determines a provided service's depth in the service tree
   *
   * @param {Service} node relative/starting service
   * @returns {Int}
   */
  depth(node: Service = this): Number {
    let nodeDepth = 0

    while (node.parent) {
      node = node.parent
      nodeDepth += 1
    }

    return nodeDepth
  }

  /**
   * Searches for and returns all siblings of the provided service
   *
   * @param {Service} node relative/starting service
   * @param {?Boolean} global return siblings across disjoint trees (true) or siblings in connected hierarchy (false - UNSUPPORTED)
   * @returns {Int}
   */
  siblings(node = this, global?: Boolean = false): Array {
    const roots = Service.findRoots()
    const depth = node.depth()

    return Service.findAtDepth(depth, roots).filter(svc => svc !== node)
  }

  /**
   * Determines if the service is a root node in the local service tree
   *
   * @returns {Boolean}
   */
  isRoot(): Boolean {
    return !this.parent
  }

  /**
   * Determines if the service is a leaf node in the local service tree
   *
   * @returns {Boolean}
   */
  isLeaf(): Boolean {
    return !this.children
  }

  /**
   * Determines and returns all root node services in the global service tree
   *
   * @param {Array} services service tree to search through (default is global)
   * @returns {Array}
   */
  static findRoots(services: Array = _services): Array {
    return Object.values(services).filter(svc => svc instanceof Service && svc.isRoot())
  }

  /**
   * Determines and returns all leaf node services in the global service tree
   *
   * @param {Array} services service tree to search through (default is global)
   * @returns {Array}
   */
  static findLeafs(services: Array = _services): Array {
    return Object.values(services).filter(svc => svc instanceof Service && svc.isLeaf())
  }

  /**
   * Determines and returns the nodes (out of the provided service tree) at the target depth
   *
   * @param {Int} targetDepth
   * @param {Array} nodes service tree to search through (default is global)
   * @returns {Array}
   */
  static findAtDepth(targetDepth: Number, nodes: Array = []): Array {
    const found  = []
    let curDepth = 0

    nodes.forEach(node => {
      (node.children || []).forEach(child => {
        curDepth = child.depth()

        if (curDepth < targetDepth) {
          found.push(...this.findAtDepth(targetDepth, [child]))
        } else if (curDepth === targetDepth) {
          found.push(child)
        }
      })
    })

    return found
  }

  /**
   * Determines if a cyclic relationship exists anywhere in the provided service tree
   *
   * @param {Array} services service tree to search through (default is global)
   * @returns {Boolean}
   */
  static cycleExists(services = _services): Boolean {
    const roots = Service.findRoots(services) || []
    const found = !util.isEmpty(roots) ? roots.map(r => r.name) : []

    let curNode = null
    let cyclic  = false

    roots.forEach(root => {
      curNode = root

      while (!cyclic && !util.isEmpty(curNode.children)) {
        (curNode.children || []).forEach(child => {
          if (!found.includes(child.name)) {
            found.push(child.name)

            curNode = child
          } else {
            cyclic = true
          }
        })
      }
    })

    return cyclic
  }

  /**
   * Determines if a service name is already registered in the global service tree
   *
   * @returns {Boolean}
   */
  static isRegistered(name: String): Boolean {
    return Array.from(_services).map(serv => serv.name).includes(name)
  }
}

/**
 * A topic-based data matcher that reacts to a service's publications
 */
export class Subscription {

  /**
   * A topic-based data matcher that reacts to a service's publications
   *
   * @param {Service} service
   * @param {String} topic topic/pattern to react to (* is wildcard)
   * @param {Function} on functionality to be triggered on successful match
   */
  constructor(service: Service, topic: String, on: Function) {
    // this.key = key // TODO -> will allow subscriptions to be triggered via simple keys
    this.service = service
    this.topic = topic
    this.on = on
    this.active = true
  }

  /**
   * Determines data or a subset of data that matches subscription topic
   *
   * @param {Object} data
   * @returns {Set} data matching subsubscription
   */
  matches(data): Set {
    const matchSet = new Set()

    if (this.active && this.service.config.data.matching) {
      const topicMatches = topic.identify(this.topic).matches(data)

      if (!util.isEmpty(topicMatches)) {
        matchSet.add(...topicMatches)
      }
    }

    return matchSet
  }

  /**
   * Determines if data matches the subsubscription and, if so, allows
   * the subsubscription to mutate and return the data.
   *
   * @param {Object} data
   * @param {Boolean} passive return either untouched data on mismatch (true) or null on mismatch (false)
   * @returns {Object} subsubscription modified data
   */
  process(data, passive: Boolean = true): Object {
    return this.matches(data).size ? this.on(data) : (passive ? data : null)
  }

  /**
   * Unsubscribes a subsubscription from its service and mark it as inactive.
   * Subscription will not react to any messages from service until activated again.
   */
  end() {
    service.unsubscribe(this)

    this.active = false
  }

  /**
   * Activates the subsubscription, permitting it to react to topic-based messages
   */
  activate() {
    this.active = true
  }

  toString() {
    return `[gooey.${this.name}]: ${JSON.stringify(this)}`
  }

}

/**
 * Alternative destructured alias or Service constructor
 *
 * @param {String} name
 * @param {?Function} model
 * @param {?Service} parent
 * @param {?Array} children
 * @param {?Object} config
 * @returns {Service}
 */
export var service = ({name, model, parent, children, config}) => new Service(name, model, parent, children, config)

/**
 * Exported flat map of module services - to be used with caution
 */
export var services = _services

/**
 * Convenience reference to utility module
 */
export const util = _util

/**
 * Detaches services from module
 */
export const clear = () => { _services = new Set() }

/**
 * Logger for establishing a consistent message format
 */
export const log = (msg: String, level: String) => `[gooey:${level || 'INFO'}] ${msg}`
<commit_msg>:scroll: docstring for Service.toString<commit_after>'use strict'

//  ▄▄ •             ▄▄▄ . ▄· ▄▌
// ▐█ ▀ ▪▪     ▪     ▀▄.▀·▐█▪██▌
// ▄█ ▀█▄ ▄█▀▄  ▄█▀▄ ▐▀▀▪▄▐█▌▐█▪
// ▐█▄▪▐█▐█▌.▐▌▐█▌.▐▌▐█▄▄▌ ▐█▀·.
// ·▀▀▀▀  ▀█▄▀▪ ▀█▄▀▪ ▀▀▀   ▀ •

import jsonPath from 'jsonpath'

import * as traversals from './traverse'
import * as topic from './topic'
import * as _util from './util'

/**
 * Flat map of all registered services, indexed by name
 */
var _services = {}

/**
 * Default service configuration object
 */
const _config = {
  strict: true,

  data: {
    matching: true,

    publish: {
      ignore: {
        falsy: true
      }
    }
  }
}

/**
 * A canonical, hierarchical, and composable data source that can publish and receive updates bi-directionally with other services
 * Forms a full k-ary tree that exists in a global forest
 */
export class Service {

  /**
   * Creates and registers a new gooey service with the current module
   *
   * @param {String} name
   * @param {?Function} model
   * @param {?Service} parent
   * @param {?Array} children
   * @param {?Object} config
   */
  constructor(name: String, model?: Function, parent?: Service, children?: Array = [], config?: Object = _config) {
    if (Object.is(name, undefined) || Service.isRegistered(name)) {
      throw `Services must have unique names: ${name}`
    }

    this.name = name
    this.model = model
    this.state = {}
    this.parent = parent instanceof Service ? parent.relateTo(this) : null
    this.children = this.relateToAll(children)
    this.subscriptions = []
    this.config = config
    this.symbol = Symbol(name)

    _services[name] = this

    if (this.model instanceof Function) {
      this.model(this.state)
    }
  }

  /**
   * Getter alias for `data`
   *
   * @returns {Object}
   */
  get data() {
    return this.state
  }

  /**
   * Setter for `data` that automatically publishes changes with default traversal settings
   *
   * @param {Object} data
   */
  set data(data: Object) {
    this.update(data)
  }

  /**
   * Traverses service tree via a conflict-free path and matches subscribers against the published data
   *
   * @param {Object} data
   * @param {?String} traversal
   * @param {?String} direction
   * @returns {Promise} deferred service tree traversal(s)
   */
  // TODO - Allow users to publish data with a certain key
  // - that way you aren't forced to always write a JsonPath or matcher function for each subscribe / publish
  publish(data, traversal: String = 'breadth', direction: String = 'down'): Promise {
    return new Promise((resolve, reject) => {
      // ensure data is pure
      data = data instanceof Object ? Object.assign({}, data) : data

      // process data against matching subscribers
      const matches = this.subscriptions.map(subscrip => subscrip.process(data, false))

      // final result is either converged conflict-resolved subscriber match or cloned source data
      const result = matches[0] || data

      // traverse service node tree and publish result on each "next" node
      return this.traverse(traversal, direction,
        next => next.publish(result, traversal, direction)
      )
    })
  }

  /**
   * Creates and registers a publish subsubscription with the Service
   *
   * @param {String} path
   * @param {Function} on
   */
  subscribe(topic: Object = '$', on: Function): Subscription {
    const subscrip = new Subscription(this, topic, on)

    this.subscriptions.push(subscrip)

    return subscrip
  }

  /**
   * Deregisters a subsubscription from the Service
   *
   * @param {Subscription} subscrip
   */
  unsubscribe(subscrip: Subscription) {
    this.subscriptions.splice(this.subscriptions.indexOf(subscrip), 1)
  }

  /**
   * Updates the Service's canonical data source with new data and publishs the change
   *
   * @param {Object} data
   * @returns {Promise}
   */
  update(data: Object, ...rest): Promise {
    this.state = data

    return this.publish(data, ...rest)
  }

  /**
   * Merges and updates the Service's canonical date source with a new (cloned) data object and publishs the change
   *
   * @param {Object} data
   * @param {?Function} error
   * @returns {Promise}
   */
  merge(data: Object, ...rest): Promise {
    const merged = data instanceof Object ? Object.assign({}, this.state, data) : this.state

    return this.update(merged, ...rest)
  }

  /**
   * Alias for subscribe
   *
   * @param {String} path
   * @param {Function} on
   * @returns {Subscription}
   */
  on(path: String, on: Function): Subscription {
    return this.subscribe(path, on)
  }

  /**
   * Alias for update
   *
   * @param {Object} data
   * @returns {Promise}
   */
  use(data, ...rest): Promise {
    return this.update(data, ...rest)
  }

  /**
   * Alias for merge
   *
   * @param {Object} data
   * @returns {Promise}
   */
  up(data, ...rest): Promise {
    return this.merge(data, ...rest)
  }

  /**
   * Determines set of data that matches the provided subsubscription's path/pattern
   *
   * @param {Object} data
   * @param {Subscription} subscrip
   * @returns {Set}
   */
  matches(data, subscrip: Subscription): Set {
    return subscrip.matches(data)
  }

  /**
   * Recursively traverses service tree via provided `next` function
   *
   * @param {String} traversal supported values defined by gooey.traverse.patterns
   * @param {String} direction up, down or bi
   * @param {Function} next
   * @returns {Promise}
   */
  traverse(traversal: String, direction: String, next: Function): Promise {
    return traversals.step.call(this, traversal, direction, next)
  }

  /**
   * Establishes strong acyclic child relationship with provided service.
   * Child services inherit publications from their parent.
   * The opposite is also supported via `up` traversals.
   * Silently fails if a cyclic relationship is proposed.
   *
   * @param {Service} child service to relate to
   * @returns {Service} modified service with new child relationship
   */
  relateTo(child: Service): Service {
    this.children.push(child)

    if (Service.cycleExists()) {
      this.children.pop() // FIXME - bleh, needs improvement to say the least
    }

    return this
  }

  /**
   * Establishes service as parent to each provided child service.
   * Child services inherit publications from their parent.
   * The opposite is also supported via `up` traversals.
   *
   * @param {Array} children services to relate to
   * @returns {Array} modified children services with new parent relationship
   */
  relateToAll(children: Array): Array {
    return children.map(c => {
      c.parent = this

      // this.relateTo(c) // TODO/FIXME

      return c
    })
  }

  /**
   * Determines a provided service's depth in the service tree
   *
   * @param {Service} node relative/starting service
   * @returns {Int}
   */
  depth(node: Service = this): Number {
    let nodeDepth = 0

    while (node.parent) {
      node = node.parent
      nodeDepth += 1
    }

    return nodeDepth
  }

  /**
   * Searches for and returns all siblings of the provided service
   *
   * @param {Service} node relative/starting service
   * @param {?Boolean} global return siblings across disjoint trees (true) or siblings in connected hierarchy (false - UNSUPPORTED)
   * @returns {Int}
   */
  siblings(node = this, global?: Boolean = false): Array {
    const roots = Service.findRoots()
    const depth = node.depth()

    return Service.findAtDepth(depth, roots).filter(svc => svc !== node)
  }

  /**
   * Determines if the service is a root node in the local service tree
   *
   * @returns {Boolean}
   */
  isRoot(): Boolean {
    return !this.parent
  }

  /**
   * Determines if the service is a leaf node in the local service tree
   *
   * @returns {Boolean}
   */
  isLeaf(): Boolean {
    return !this.children
  }

  /**
   * Determines and returns all root node services in the global service tree
   *
   * @param {Array} services service tree to search through (default is global)
   * @returns {Array}
   */
  static findRoots(services: Array = _services): Array {
    return Object.values(services).filter(svc => svc instanceof Service && svc.isRoot())
  }

  /**
   * Determines and returns all leaf node services in the global service tree
   *
   * @param {Array} services service tree to search through (default is global)
   * @returns {Array}
   */
  static findLeafs(services: Array = _services): Array {
    return Object.values(services).filter(svc => svc instanceof Service && svc.isLeaf())
  }

  /**
   * Determines and returns the nodes (out of the provided service tree) at the target depth
   *
   * @param {Int} targetDepth
   * @param {Array} nodes service tree to search through (default is global)
   * @returns {Array}
   */
  static findAtDepth(targetDepth: Number, nodes: Array = []): Array {
    const found  = []
    let curDepth = 0

    nodes.forEach(node => {
      (node.children || []).forEach(child => {
        curDepth = child.depth()

        if (curDepth < targetDepth) {
          found.push(...this.findAtDepth(targetDepth, [child]))
        } else if (curDepth === targetDepth) {
          found.push(child)
        }
      })
    })

    return found
  }

  /**
   * Determines if a cyclic relationship exists anywhere in the provided service tree
   *
   * @param {Array} services service tree to search through (default is global)
   * @returns {Boolean}
   */
  static cycleExists(services = _services): Boolean {
    const roots = Service.findRoots(services) || []
    const found = !util.isEmpty(roots) ? roots.map(r => r.name) : []

    let curNode = null
    let cyclic  = false

    roots.forEach(root => {
      curNode = root

      while (!cyclic && !util.isEmpty(curNode.children)) {
        (curNode.children || []).forEach(child => {
          if (!found.includes(child.name)) {
            found.push(child.name)

            curNode = child
          } else {
            cyclic = true
          }
        })
      }
    })

    return cyclic
  }

  /**
   * Determines if a service name is already registered in the global service tree
   *
   * @returns {Boolean}
   */
  static isRegistered(name: String): Boolean {
    return Array.from(_services).map(serv => serv.name).includes(name)
  }
}

/**
 * A topic-based data matcher that reacts to a service's publications
 */
export class Subscription {

  /**
   * A topic-based data matcher that reacts to a service's publications
   *
   * @param {Service} service
   * @param {String} topic topic/pattern to react to (* is wildcard)
   * @param {Function} on functionality to be triggered on successful match
   */
  constructor(service: Service, topic: String, on: Function) {
    // this.key = key // TODO -> will allow subscriptions to be triggered via simple keys
    this.service = service
    this.topic = topic
    this.on = on
    this.active = true
  }

  /**
   * Determines data or a subset of data that matches subscription topic
   *
   * @param {Object} data
   * @returns {Set} data matching subsubscription
   */
  matches(data): Set {
    const matchSet = new Set()

    if (this.active && this.service.config.data.matching) {
      const topicMatches = topic.identify(this.topic).matches(data)

      if (!util.isEmpty(topicMatches)) {
        matchSet.add(...topicMatches)
      }
    }

    return matchSet
  }

  /**
   * Determines if data matches the subsubscription and, if so, allows
   * the subsubscription to mutate and return the data.
   *
   * @param {Object} data
   * @param {Boolean} passive return either untouched data on mismatch (true) or null on mismatch (false)
   * @returns {Object} subsubscription modified data
   */
  process(data, passive: Boolean = true): Object {
    return this.matches(data).size ? this.on(data) : (passive ? data : null)
  }

  /**
   * Unsubscribes a subsubscription from its service and mark it as inactive.
   * Subscription will not react to any messages from service until activated again.
   */
  end() {
    service.unsubscribe(this)

    this.active = false
  }

  /**
   * Activates the subsubscription, permitting it to react to topic-based messages
   */
  activate() {
    this.active = true
  }

  /**
   * For sane debugging
   */
  toString() {
    return `[gooey.${this.name}]: ${JSON.stringify(this)}`
  }

}

/**
 * Alternative destructured alias or Service constructor
 *
 * @param {String} name
 * @param {?Function} model
 * @param {?Service} parent
 * @param {?Array} children
 * @param {?Object} config
 * @returns {Service}
 */
export var service = ({name, model, parent, children, config}) => new Service(name, model, parent, children, config)

/**
 * Exported flat map of module services - to be used with caution
 */
export var services = _services

/**
 * Convenience reference to utility module
 */
export const util = _util

/**
 * Detaches services from module
 */
export const clear = () => { _services = new Set() }

/**
 * Logger for establishing a consistent message format
 */
export const log = (msg: String, level: String) => `[gooey:${level || 'INFO'}] ${msg}`
<|endoftext|>


<commit_before>'use strict';
let os_types = require('./os-types.json');
let os_type_descriptions = require('./os-type-descriptions.json');
let extraOptions = require('./extra-options.js');
let _ = require('lodash-addons');
let JTS = require('jsontableschema');
let Schema = JTS.Schema;

class TypeProcessor {

    constructor() {
        this.types = os_types;
        this.ident = x => { return x; }
    }

    getAllTypes() {
        return Object.keys(this.types);
    }

    getDataTypeExtraOptions(dataType) {
      return _.union(
        _.get(
          extraOptions,
          'dataTypes.' + this.types[dataType].dataType + '.options',
          []
        ),
        _.get(
          extraOptions,
          'osTypes.' + dataType + '.options',
          []
        )
      );
    }

    autoComplete(prefix) {
        if ( !prefix ) {
            prefix = '';
        }
        var options = _.filter(this.getAllTypes(), (typ) => {
          return _.startsWith(typ, prefix);
        });
        var prefixLen = prefix.length;
        var findNextIndex = (typ) => {
            for ( var i = prefixLen ; i < typ.length ; i++ ) {
                if ( typ[i] == ":" ) {
                    break;
                }
            }
            return i;
        }
        options = _.map(options, (typ) => {
            var nextIndex = findNextIndex(typ);
            var ret = typ.slice(0,nextIndex);
            if ( nextIndex < typ.length ) {
                ret += typ[nextIndex];
            }
            return ret;
        });
        options = _.map(_.uniq(options), (k) => {
            return _.extend({type:k}, os_type_descriptions[k]);
        });
        options = _.sortBy(options, 'type');
        options = _.sortBy(options, 'group');
        var group = null;
        _.forEach(options, (option) => {
            if (option.group == group) {
               delete option.group;
            } else {
               group = option.group;
            }
        });
        return options;
    }

    _getJTSTypeByName(name, options) {
        return _.find(
          _.map(_.toPairs(JTS), function(pair) {
            var JTSType = pair[0];
            var JTSTypeValue = pair[1];
            if (_.endsWith(JTSType, 'Type') ) {
                var ret = new JTSTypeValue(options);
                if (ret.name == name) {
                    return ret;
                }
            }
        }));
    }

    _checkInput(fields) {
        // Make sure we got an array...
        var valid = _.isArray(fields) || this._generalError("Fields should be an array");
        // ... of objects ...
        valid = valid &&
            _.every(fields, (f) => {
                return _.isObject(f) || this._generalError("Field items should be objects");
            });
        // ... with all the mandatory properties ...
        valid = valid &&
            _.every(fields, (f) => {
                return (_.hasIn(f, 'name') && _.hasIn(f, 'type')) ||
                    this._generalError("Field items should have 'name' and 'type'");
            });
        // ... and no unknown properties ...
        var allowedProperties = [
            'name', 'title', 'type', 'format', 'data',
            'options', 'resource', 'description', 'constant' // common properties
        ];
        valid = valid &&
            _.every(fields, (f) => {
                var diff = _.difference(_.keys(f), allowedProperties);
                return (diff.length == 0) ||
                    this._fieldError(f.name, "Got unknown properties "+diff);
            });
        // ... and no duplicate names ...
        valid = valid &&
            _.chain(fields)
             .countBy('name')
             .toPairs()
              .filter((pair) => { return pair[1] <= 1 || this._fieldError(pair[0], "Got duplicate name " + pair[0]); })
             .value()
             .length == fields.length;
        // ... and no duplicate titles ...
        valid = valid &&
            _.chain(fields)
             .filter((field) => !!field.title)
             .countBy('title')
             .toPairs()
             .filter((pair) => { return !(pair[1] <= 1 || this._fieldError(pair[0], "Got duplicate title " + pair[0])); })
             .value()
             .length == 0;
        // ... and all types are valid ...
        valid = valid &&
            _.every(fields, (f) => {
                return !f.type || _.hasIn(this.types, f.type) ||
                    this._fieldError(f.name, "Got unknown type " + f.type);
            });
        // ... and no unknown additional options ...
        valid = valid &&
            _.every(fields, (f) => {
                if ( !f.type ) { return true; }
                var allowedOptions = _.union(
                    _.get(extraOptions, 'dataTypes.'+this.types[f.type].dataType+'.options', []),
                    _.get(extraOptions, 'osTypes.'+f.type+'.options', [])
                );
                allowedOptions = _.map(allowedOptions, 'name');
                var options = _.get(f, 'options', {});
                options = _.keys(options);
                var diff = _.difference(options, allowedOptions);
                return (diff.length == 0) ||
                    this._fieldError(f.name, "Got unknown options key "+diff);
            });
        // ... and data samples match the selected datatype ...
        if (valid) {
            _.forEach(fields, (f) => {
                if ( f.type && f.data ) {
                    var typeOptions = _.get(extraOptions, 'dataTypes.'+this.types[f.type].dataType+'.options', []);
                    typeOptions = _.keyBy(typeOptions, 'name');
                    var options = _.pick(f.options,
                      _.keys(typeOptions)
                    );
                    options = _.mapValues(options, (value, key) => {
                        return (typeOptions[key].transform || this.ident)(value);
                    });
                    let jtsSchemaObj = {fields: [{name: '_', type: this.types[f.type].dataType}]};
                    this._promise(new Schema(jtsSchemaObj).then((schema) => {
                      _.every(f.data, (datum) => {
                        try {
                          schema.castRow([datum]);
                        }
                        catch(e) {
                          this._fieldError(f.name, "Data cannot be cast to this type '" + datum + "'");
                        }
                      });
                    }));
                }
            });
        }
        return valid;
    }

    _titleToSlug(title, type) {
        var slugRe = new RegExp('[a-zA-Z0-9]+','g');
        var slugs = _.deburr(title).match(slugRe);
        if ( slugs == null || slugs.length == 0 ) {
            slugs = type.match(slugRe);
        }
        var slug = _.join(slugs, '_');
        if ( this.allNames.indexOf(slug) >= 0 ) {
            let i = 2;
            while ( true ) {
                let attempt = slug + '_' + i;
                if ( this.allNames.indexOf(attempt) < 0 ) {
                    slug = attempt;
                    break;
                }
                i+=1;
            }
        }
        this.allNames.push(slug)
        return slug;
    }

    _initErrors() {
        this.errors = { general: [], perField: {} };
    }

    _generalError(err) {
        this.errors.general.push(err);
        return false;
    }

    _fieldError(field, err) {
        var fieldErrors = this.errors.perField[field];
        if (!fieldErrors) {
            fieldErrors = [];
            this.errors.perField[field] = fieldErrors;
        }
        fieldErrors.push(err);
        return false;
    }

    _promise(promise) {
        if (!!this.promise) {
            this.promise.then(() => {
                return promise;
            });
        } else {
            this.promise = promise;
        }
    }

    _embedOptions(target, options, availableOptions) {
        _.forEach(availableOptions, (availableOption) => {
            var n = availableOption.name;
            var transform = availableOption.transform || this.ident;
            if (_.hasIn(options, n) && options[n]) {
                target[n] = transform(options[n]);
            } else if (_.hasIn(availableOption, 'defaultValue')) {
                target[n] = transform(availableOption.defaultValue);
            }
        });
    }

    fieldsToModel(fields) {
        // Prepare errors
        this._initErrors();
        // Detect invalid data
        if ( !this._checkInput(fields) ) {
            var ret = {errors: this.errors};
            //console.log(JSON.stringify(ret,null,2));
            return ret;
        }
        // Modelling
        var dimensions = {};
        var measures = {};
        var model = { dimensions, measures };
        var schema = {fields:{}, primaryKey:[]};
        this.allNames = [];
        _.forEach(_.filter(fields, (f) => { return !!f.type; }), (f) => {
            var osType = this.types[f.type];
            if (!f.title) {
                f.title = f.name;
            }
            f.slug = this._titleToSlug(f.name, f.type);
            var conceptType = _.split(f.type,':')[0];
            schema.fields[f.name] = {
                title: f.title,
                name: f.name,
                slug: f.slug,
                type: osType.dataType,
                format: osType.format || f.format || 'default',
                osType: f.type,
                conceptType: conceptType,
                resource: f.resource,
                options: _.union(
                    _.get(extraOptions, 'dataTypes.'+osType.dataType+'.options', []),
                    _.get(extraOptions, 'osTypes.'+f.type+'.options', [])
                )
            };
            this._embedOptions(schema.fields[f.name], f.options, _.get(extraOptions, 'dataTypes.'+osType.dataType+'.options', []));

            if ( conceptType == 'value' ) {
                // Measure
                var measure = {
                    source: f.name,
                    title: f.title
                }
                // Extra properties
                if (f.resource)          { measure.resource = f.resource; }
                this._embedOptions(measure, f.options, _.get(extraOptions, 'osTypes.value.options', []));
                measures[f.slug] = measure;
            } else {
                let dimension;
                if ( _.hasIn(dimensions, conceptType) ) {
                    dimension = dimensions[conceptType];
                } else {
                    dimension = {
                        dimensionType: osType.dimensionType,
                        primaryKey: [],
                        attributes: {},
                    };
                    if ( osType.classificationType ) {
                        dimension.classificationType = osType.classificationType;
                    }
                    dimensions[conceptType] = dimension;
                }
                var attribute = {
                    source: f.name,
                    title: f.title
                };
                if ( f.resource ) {
                    attribute.resource = f.resource;
                }
                if ( f.constant ) {
                    attribute.constant = f.constant;
                }
                dimension.attributes[f.slug] = attribute;
                if (osType.uniqueIdentifier) {
                    dimension.primaryKey.push(f.slug);
                    schema.primaryKey.push(f.name);
                }
            }
        });
        // Process parent, labelfor
        var findAttribute = (field, osType) => {
            if ( field ) {
                return {key:field.slug, attr:dimensions[field.conceptType].attributes[field.slug]};
            }
            if ( osType ) {
                var field = _.find(_.values(schema.fields), (i) => {
                    return _.startsWith(i.osType, osType);
                });
                return findAttribute(field);
            }
        };
        _.forEach(_.values(schema.fields), (field) => {
            var osType = this.types[field.osType];
            var labelfor = osType.labelfor;
            var parent = osType.parent;
            if ( labelfor || parent ) {
                var attribute = findAttribute(field).attr;
                if ( labelfor ) {
                    var targetAttribute = findAttribute(null, labelfor);
                    if ( targetAttribute ) {
                        attribute.labelfor = targetAttribute.key;
                    } else {
                        this._fieldError(field.name, "Couldn't find a column mapped to the matching 'code' ("+labelfor+")");
                    }
                }
                while ( parent ) {
                    var targetAttribute = findAttribute(null, parent);
                    var osTypeName = _.find(_.keys(this.types), (i) => {
                        return _.startsWith(i, parent);
                    });
                    osType = this.types[osTypeName];
                    parent = osType.parent;
                    if ( targetAttribute ) {
                        attribute.parent = targetAttribute.key;
                        break;
                    } else if (!parent) {
                        this._fieldError(field.name, "Couldn't find a column mapped to the parent of this type ("+
                                                     (this.types[field.osType].parent)+")");
                    }
                }
            }
        });
        // Fix primary keys in case they're missing
        _.forEach(model.dimensions, (dimension) => {
           if (dimension.primaryKey.length == 0) {
               dimension.primaryKey = _.keys(dimension.attributes);
           }
        });
        // Reorder primary keys based on parents
        _.forEach(model.dimensions, (dimension, name) => {
            while (true) {
                var swaps = 0;
                var primaryKey = dimension.primaryKey;
                for (let i = 0 ; i < primaryKey.length ; i++) {
                    var attrib = dimension.attributes[primaryKey[i]];
                    if (!attrib.parent) {
                        continue;
                    }
                    for (let j = i+1 ; j < primaryKey.length ; j++) {
                        if (primaryKey[j] == attrib.parent) {
                            var temp = primaryKey[i];
                            primaryKey[i] = primaryKey[j];
                            primaryKey[j] = temp;
                            swaps++;
                            break;
                        }
                    }
                }
                if (swaps == 0) {
                    break;
                }
            }
        });

        var fdp = {model, schema};
        if (this.errors.general.length || _.keys(this.errors.perField).length) {
            fdp.errors = this.errors;
        }
        if (!!this.promise) {
            fdp.promise = this.promise;
        }
        //console.log(JSON.stringify(fdp,null,2));
        return fdp;
    }
}

module.exports = TypeProcessor;
<commit_msg>feat(extra-types): Add more types for unknown dataset mapping<commit_after>'use strict';
let os_types = require('./os-types.json');
let os_type_descriptions = require('./os-type-descriptions.json');
let extraOptions = require('./extra-options.js');
let _ = require('lodash-addons');
let JTS = require('jsontableschema');
let Schema = JTS.Schema;

class TypeProcessor {

    constructor() {
        this.types = os_types;
        this.ident = x => { return x; }
        this._addExtraTypes();
    }

    _addExtraTypes() {
        for (let i = 1 ; i <= 20 ; i++) {
            this.types[`unknown:string-${i}:code`] = {
                dimensionType: "other",
                dataType: "string",
                uniqueIdentifier: true
            };
            this.types[`unknown:string-${i}:label`] = {
                dimensionType: "other",
                dataType: "string",
                labelfor: `unknown:string-${i}:code`
            };
            os_type_descriptions[`unknown:string-${i}:`] = {
                displayName: `unknown dimension #${i}`,
                description: `Placeholder for an unknown dimension #${i}`
            };
            os_type_descriptions[`unknown:string-${i}:code`] = {
                displayName: `unknown dimension #${i} code`,
                description: `Unknown dimension #${i}'s code`
            };
            os_type_descriptions[`unknown:string-${i}:label`] = {
                displayName: `unknown dimension #${i} label`,
                description: `Unknown dimension #${i}'s label`
            };
        }
    }

    getAllTypes() {
        return Object.keys(this.types);
    }

    getDataTypeExtraOptions(dataType) {
      return _.union(
        _.get(
          extraOptions,
          'dataTypes.' + this.types[dataType].dataType + '.options',
          []
        ),
        _.get(
          extraOptions,
          'osTypes.' + dataType + '.options',
          []
        )
      );
    }

    autoComplete(prefix) {
        if ( !prefix ) {
            prefix = '';
        }
        var options = _.filter(this.getAllTypes(), (typ) => {
          return _.startsWith(typ, prefix);
        });
        var prefixLen = prefix.length;
        var findNextIndex = (typ) => {
            for ( var i = prefixLen ; i < typ.length ; i++ ) {
                if ( typ[i] == ":" ) {
                    break;
                }
            }
            return i;
        }
        options = _.map(options, (typ) => {
            var nextIndex = findNextIndex(typ);
            var ret = typ.slice(0,nextIndex);
            if ( nextIndex < typ.length ) {
                ret += typ[nextIndex];
            }
            return ret;
        });
        options = _.map(_.uniq(options), (k) => {
            return _.extend({type:k}, os_type_descriptions[k]);
        });
        options = _.sortBy(options, 'type');
        options = _.sortBy(options, 'group');
        var group = null;
        _.forEach(options, (option) => {
            if (option.group == group) {
               delete option.group;
            } else {
               group = option.group;
            }
        });
        return options;
    }

    _getJTSTypeByName(name, options) {
        return _.find(
          _.map(_.toPairs(JTS), function(pair) {
            var JTSType = pair[0];
            var JTSTypeValue = pair[1];
            if (_.endsWith(JTSType, 'Type') ) {
                var ret = new JTSTypeValue(options);
                if (ret.name == name) {
                    return ret;
                }
            }
        }));
    }

    _checkInput(fields) {
        // Make sure we got an array...
        var valid = _.isArray(fields) || this._generalError("Fields should be an array");
        // ... of objects ...
        valid = valid &&
            _.every(fields, (f) => {
                return _.isObject(f) || this._generalError("Field items should be objects");
            });
        // ... with all the mandatory properties ...
        valid = valid &&
            _.every(fields, (f) => {
                return (_.hasIn(f, 'name') && _.hasIn(f, 'type')) ||
                    this._generalError("Field items should have 'name' and 'type'");
            });
        // ... and no unknown properties ...
        var allowedProperties = [
            'name', 'title', 'type', 'format', 'data',
            'options', 'resource', 'description', 'constant' // common properties
        ];
        valid = valid &&
            _.every(fields, (f) => {
                var diff = _.difference(_.keys(f), allowedProperties);
                return (diff.length == 0) ||
                    this._fieldError(f.name, "Got unknown properties "+diff);
            });
        // ... and no duplicate names ...
        valid = valid &&
            _.chain(fields)
             .countBy('name')
             .toPairs()
              .filter((pair) => { return pair[1] <= 1 || this._fieldError(pair[0], "Got duplicate name " + pair[0]); })
             .value()
             .length == fields.length;
        // ... and no duplicate titles ...
        valid = valid &&
            _.chain(fields)
             .filter((field) => !!field.title)
             .countBy('title')
             .toPairs()
             .filter((pair) => { return !(pair[1] <= 1 || this._fieldError(pair[0], "Got duplicate title " + pair[0])); })
             .value()
             .length == 0;
        // ... and all types are valid ...
        valid = valid &&
            _.every(fields, (f) => {
                return !f.type || _.hasIn(this.types, f.type) ||
                    this._fieldError(f.name, "Got unknown type " + f.type);
            });
        // ... and no unknown additional options ...
        valid = valid &&
            _.every(fields, (f) => {
                if ( !f.type ) { return true; }
                var allowedOptions = _.union(
                    _.get(extraOptions, 'dataTypes.'+this.types[f.type].dataType+'.options', []),
                    _.get(extraOptions, 'osTypes.'+f.type+'.options', [])
                );
                allowedOptions = _.map(allowedOptions, 'name');
                var options = _.get(f, 'options', {});
                options = _.keys(options);
                var diff = _.difference(options, allowedOptions);
                return (diff.length == 0) ||
                    this._fieldError(f.name, "Got unknown options key "+diff);
            });
        // ... and data samples match the selected datatype ...
        if (valid) {
            _.forEach(fields, (f) => {
                if ( f.type && f.data ) {
                    var typeOptions = _.get(extraOptions, 'dataTypes.'+this.types[f.type].dataType+'.options', []);
                    typeOptions = _.keyBy(typeOptions, 'name');
                    var options = _.pick(f.options,
                      _.keys(typeOptions)
                    );
                    options = _.mapValues(options, (value, key) => {
                        return (typeOptions[key].transform || this.ident)(value);
                    });
                    let jtsSchemaObj = {fields: [{name: '_', type: this.types[f.type].dataType}]};
                    this._promise(new Schema(jtsSchemaObj).then((schema) => {
                      _.every(f.data, (datum) => {
                        try {
                          schema.castRow([datum]);
                        }
                        catch(e) {
                          this._fieldError(f.name, "Data cannot be cast to this type '" + datum + "'");
                        }
                      });
                    }));
                }
            });
        }
        return valid;
    }

    _titleToSlug(title, type) {
        var slugRe = new RegExp('[a-zA-Z0-9]+','g');
        var slugs = _.deburr(title).match(slugRe);
        if ( slugs == null || slugs.length == 0 ) {
            slugs = type.match(slugRe);
        }
        var slug = _.join(slugs, '_');
        if ( this.allNames.indexOf(slug) >= 0 ) {
            let i = 2;
            while ( true ) {
                let attempt = slug + '_' + i;
                if ( this.allNames.indexOf(attempt) < 0 ) {
                    slug = attempt;
                    break;
                }
                i+=1;
            }
        }
        this.allNames.push(slug)
        return slug;
    }

    _initErrors() {
        this.errors = { general: [], perField: {} };
    }

    _generalError(err) {
        this.errors.general.push(err);
        return false;
    }

    _fieldError(field, err) {
        var fieldErrors = this.errors.perField[field];
        if (!fieldErrors) {
            fieldErrors = [];
            this.errors.perField[field] = fieldErrors;
        }
        fieldErrors.push(err);
        return false;
    }

    _promise(promise) {
        if (!!this.promise) {
            this.promise.then(() => {
                return promise;
            });
        } else {
            this.promise = promise;
        }
    }

    _embedOptions(target, options, availableOptions) {
        _.forEach(availableOptions, (availableOption) => {
            var n = availableOption.name;
            var transform = availableOption.transform || this.ident;
            if (_.hasIn(options, n) && options[n]) {
                target[n] = transform(options[n]);
            } else if (_.hasIn(availableOption, 'defaultValue')) {
                target[n] = transform(availableOption.defaultValue);
            }
        });
    }

    fieldsToModel(fields) {
        // Prepare errors
        this._initErrors();
        // Detect invalid data
        if ( !this._checkInput(fields) ) {
            var ret = {errors: this.errors};
            //console.log(JSON.stringify(ret,null,2));
            return ret;
        }
        // Modelling
        var dimensions = {};
        var measures = {};
        var model = { dimensions, measures };
        var schema = {fields:{}, primaryKey:[]};
        this.allNames = [];
        _.forEach(_.filter(fields, (f) => { return !!f.type; }), (f) => {
            var osType = this.types[f.type];
            if (!f.title) {
                f.title = f.name;
            }
            f.slug = this._titleToSlug(f.name, f.type);
            var conceptType = _.split(f.type,':')[0];
            schema.fields[f.name] = {
                title: f.title,
                name: f.name,
                slug: f.slug,
                type: osType.dataType,
                format: osType.format || f.format || 'default',
                osType: f.type,
                conceptType: conceptType,
                resource: f.resource,
                options: _.union(
                    _.get(extraOptions, 'dataTypes.'+osType.dataType+'.options', []),
                    _.get(extraOptions, 'osTypes.'+f.type+'.options', [])
                )
            };
            this._embedOptions(schema.fields[f.name], f.options, _.get(extraOptions, 'dataTypes.'+osType.dataType+'.options', []));

            if ( conceptType == 'value' ) {
                // Measure
                var measure = {
                    source: f.name,
                    title: f.title
                }
                // Extra properties
                if (f.resource)          { measure.resource = f.resource; }
                this._embedOptions(measure, f.options, _.get(extraOptions, 'osTypes.value.options', []));
                measures[f.slug] = measure;
            } else {
                let dimension;
                if ( _.hasIn(dimensions, conceptType) ) {
                    dimension = dimensions[conceptType];
                } else {
                    dimension = {
                        dimensionType: osType.dimensionType,
                        primaryKey: [],
                        attributes: {},
                    };
                    if ( osType.classificationType ) {
                        dimension.classificationType = osType.classificationType;
                    }
                    dimensions[conceptType] = dimension;
                }
                var attribute = {
                    source: f.name,
                    title: f.title
                };
                if ( f.resource ) {
                    attribute.resource = f.resource;
                }
                if ( f.constant ) {
                    attribute.constant = f.constant;
                }
                dimension.attributes[f.slug] = attribute;
                if (osType.uniqueIdentifier) {
                    dimension.primaryKey.push(f.slug);
                    schema.primaryKey.push(f.name);
                }
            }
        });
        // Process parent, labelfor
        var findAttribute = (field, osType) => {
            if ( field ) {
                return {key:field.slug, attr:dimensions[field.conceptType].attributes[field.slug]};
            }
            if ( osType ) {
                var field = _.find(_.values(schema.fields), (i) => {
                    return _.startsWith(i.osType, osType);
                });
                return findAttribute(field);
            }
        };
        _.forEach(_.values(schema.fields), (field) => {
            var osType = this.types[field.osType];
            var labelfor = osType.labelfor;
            var parent = osType.parent;
            if ( labelfor || parent ) {
                var attribute = findAttribute(field).attr;
                if ( labelfor ) {
                    var targetAttribute = findAttribute(null, labelfor);
                    if ( targetAttribute ) {
                        attribute.labelfor = targetAttribute.key;
                    } else {
                        this._fieldError(field.name, "Couldn't find a column mapped to the matching 'code' ("+labelfor+")");
                    }
                }
                while ( parent ) {
                    var targetAttribute = findAttribute(null, parent);
                    var osTypeName = _.find(_.keys(this.types), (i) => {
                        return _.startsWith(i, parent);
                    });
                    osType = this.types[osTypeName];
                    parent = osType.parent;
                    if ( targetAttribute ) {
                        attribute.parent = targetAttribute.key;
                        break;
                    } else if (!parent) {
                        this._fieldError(field.name, "Couldn't find a column mapped to the parent of this type ("+
                                                     (this.types[field.osType].parent)+")");
                    }
                }
            }
        });
        // Fix primary keys in case they're missing
        _.forEach(model.dimensions, (dimension) => {
           if (dimension.primaryKey.length == 0) {
               dimension.primaryKey = _.keys(dimension.attributes);
           }
        });
        // Reorder primary keys based on parents
        _.forEach(model.dimensions, (dimension, name) => {
            while (true) {
                var swaps = 0;
                var primaryKey = dimension.primaryKey;
                for (let i = 0 ; i < primaryKey.length ; i++) {
                    var attrib = dimension.attributes[primaryKey[i]];
                    if (!attrib.parent) {
                        continue;
                    }
                    for (let j = i+1 ; j < primaryKey.length ; j++) {
                        if (primaryKey[j] == attrib.parent) {
                            var temp = primaryKey[i];
                            primaryKey[i] = primaryKey[j];
                            primaryKey[j] = temp;
                            swaps++;
                            break;
                        }
                    }
                }
                if (swaps == 0) {
                    break;
                }
            }
        });

        var fdp = {model, schema};
        if (this.errors.general.length || _.keys(this.errors.perField).length) {
            fdp.errors = this.errors;
        }
        if (!!this.promise) {
            fdp.promise = this.promise;
        }
        //console.log(JSON.stringify(fdp,null,2));
        return fdp;
    }
}

module.exports = TypeProcessor;
<|endoftext|>


<commit_before>(function() {

  var ROW = 13
  var COL = 60
  var TOTAL = ROW * COL
  var COLOR_DEAD = '#eeeeee'
  var COLOR_ALIVE = '#8cc665'
  var CONTAINER = 'data-gol-container'
  var EMPTY_ROW = repeat(COL, 0).split('').map(function(n) { return +n })

  function repeat(times, str) {
    if (str.repeat) str.repeat(times)
    return new Array(times + 1).join(str)
  }

  function forEachIndex(fn) {
    for (var x = 0; x < ROW; ++x) {
      for (var y = 0; y < COL; ++y) {
        fn(x, y)
      }
    }
  }

  function forEachList(list, childSelector, fn) {
    var x, y
    if (typeof childSelector === 'function') {
      fn = childSelector
      for (x = 0; x < list.length; ++x) {
          fn(x, list[x])
      }
    } else {
      for (x = 0; x < list.length; ++x) {
        var sublist = list[x].querySelectorAll(childSelector)
        for (y = 0; y < sublist.length; ++y) {
          fn(y, x, sublist[y])
        }
      }
    }
  }

  function createStatusBoard(fn) {
    var board = []
    for (var i = 0; i < ROW; ++i) {
      board.push(EMPTY_ROW.slice(0))
    }
    forEachIndex(function(x, y) {
      board[x][y] = fn ? fn(x, y) : 0
    })
    return board
  }

  function countNeighbors(board, x, y) {
    var xa = x ? (x - 1) : (ROW - 1),
      xc = (x == (ROW - 1)) ? 0 : (x + 1),
      ya = y ? (y - 1) : (COL - 1),
      yc = ( y == (COL - 1)) ? 0 : (y + 1)
    return (
      board[xa][ya] + board[x][ya] + board[xc][ya] +
      board[xa][y]  +              + board[xc][y]  +
      board[xa][yc] + board[x][yc] + board[xc][yc]
    )
  }

  function next(board) {
    return createStatusBoard(function(x, y) {
      var neighbors = countNeighbors(board, x, y)
      var status = board[x][y]
      switch(status) {
        case 1: if (neighbors <= 1 || neighbors >= 4) status = 0
        case 0: if (neighbors === 3) status = 1
      }
      return status
    })
  }

  function isTagName(name, el) {
    if (el) {
      return el.tagName.toLowerCase() === name
    }
  }

  function setTransitonDelay(el, delay) {
    if (el) {
      el.style.transitionDelay = el.style.webkitTransitionDelay = delay
    }
  }

  function fillColor(cell, color) {
    if (!cell) return null
    if (isTagName('rect', cell)) {
      return cell.getAttribute('fill')
    }
    return color
      ? (cell.style.backgroundColor = color)
      : cell.style.backgroundColor
  }

  function getKey(x, y) {
    return x + '-' + y
  }

  var getPosition = (function() {
    var position = {}
    forEachIndex(function(x, y) {
      position[getKey(x, y)] = { x: x, y: y }
    })
    return function(key) {
      return position[key] || {}
    }
  }())

  var GameBoard = (function() {
    var id = 'gol-contribution-board'
    var cells = {}
    var board = null
    var emptyCells = repeat(COL,
      '
' + repeat(ROW, '

') + '
'
    )
    function toggle(e) {
      var cell = e.target
      if (isTagName('li', cell)) {
        GameBoard.ontoggle && GameBoard.ontoggle(cell)
      }
    }
    return {
      build: function() {
        if (board) return board
        board = document.createElement('div')
        board.id = id
        board.innerHTML = emptyCells
        board.addEventListener('click', toggle)
        forEachList(board.querySelectorAll('ul'), 'li', function(x, y, elem) {
          var key = getKey(x, y)
          cells[key] = elem
          elem.setAttribute('data-key', key)
        })
        return board
      },
      render: function(statusBoard, colors) {
        forEachIndex(function(x, y) {
          var key = getKey(x, y)
          var status = statusBoard[x][y]
          fillColor(cells[key], status
            ? (colors ? colors[key] : COLOR_ALIVE)
            : COLOR_DEAD
          )
        })
      },
      isEmpty: function() {
        return TOTAL === board.querySelectorAll('li[style$="238);"]').length
      },
      animateBackground: function() {
        var cells = board.querySelectorAll('li:not([style$="238);"])')
        forEachList(cells, function(_, cell) {
          setTransitonDelay(cell, (600 * Math.random()) + 'ms')
        })
        setTimeout(function() {
          forEachList(cells, function(_, cell) {
            cell.style.backgroundColor = COLOR_ALIVE
          })
        })
        setTimeout(function() {
          forEachList(cells, function(_, cell) {
            setTransitonDelay(cell, '')
          })
        }, 600)
      },
      remove: function() {
        cells = {}
        if (board) {
          board.removeEventListener('click', toggle)
          board.parentNode.removeChild(board)
          board = null
        }
      }
    }
  }())

  var Controls = (function() {
    var id = 'gol-controls'
    var controls = null
    var generation = null
    var buttons = {}
    var actions = ['run', 'pause', 'next', 'clear', 'reset', 'close']
    var content = '0' + (
      actions.map(function(name) { return '' }).join('')
    )
    function action(e) {
      var btn = e.target
      if (btn.hasAttribute('data-action') && !btn.hasAttribute('disabled')) {
        Controls.onclick && Controls.onclick(btn)
      }
    }
    return {
      build: function() {
        if (controls) return controls
        controls = document.createElement('div')
        controls.id = id
        controls.innerHTML = content
        generation = controls.querySelector('#gol-generation')
        actions.forEach(function(name) {
          buttons[name] = controls.querySelector('[data-action="' + name + '"]')
        })
        controls.addEventListener('click', action)
        return controls
      },
      apply: function(operation, names) {
        if (!Array.isArray(names)) names = [names]
        names.forEach(function(name) {
          var btn = buttons[name]
          switch (operation) {
            case 'disable': btn.setAttribute('disabled', true); break
            case 'enable': btn.removeAttribute('disabled'); break
            case 'show': btn.style.display = ''; break
            case 'hide': btn.style.display = 'none'
          }
        })
        return this
      },
      remove: function() {
        buttons = {}
        if (controls) {
          controls.removeEventListener('click', action)
          controls.parentNode.removeChild(controls)
          generation = null
          controls = null
        }
      },
      generation: function(count) {
        generation.innerHTML = count
      }
    }
  }())

  var Game = {
    generation: 0,
    running: false,
    timer: null,
    container: null,
    board: null
  }

  Game._getIntialStatus = function() {
    var board = createStatusBoard()
    var gs = Game.container.querySelectorAll('.js-calendar-graph-svg > g > g')
    var colors = {}
    forEachList(gs, 'rect', function(x, y, rect) {
      var color = fillColor(rect)
      var dx = x + 3, dy = y + 4
      board[dx][dy] = (color === COLOR_DEAD) ? 0 : 1
      colors[getKey(dx, dy)] = color
    })
    return { board: board, colors: colors }
  }

  Game._updateControls = function() {
    Game.running
      ? Controls
        .apply('disable', ['next', 'clear', 'reset'])
        .apply('show', 'pause')
        .apply('hide', 'run')
      : Controls
        .apply('enable', 'reset')
        .apply('show', 'run')
        .apply('hide', 'pause')
        .apply(
          (GameBoard.isEmpty() ? 'disable' : 'enable'),
          ['run', 'next', 'clear']
        )
  }

  Game._updateGeneration = function(count) {
    Controls.generation(Game.generation = count)
  }

  Game._ontoggle = function(cell) {
    if (Game.running) return false
    var mapping = {
      color: [COLOR_ALIVE, COLOR_DEAD], status: [1, 0]
    }
    var p = getPosition(cell.getAttribute('data-key'))
    var status = Game.board[p.x][p.y]
    fillColor(cell, mapping.color[status])
    Game.board[p.x][p.y] = mapping.status[status]
    Game._updateGeneration(0)
    Game._updateControls()
  }

  Game._loop = function() {
    if (!Game.running) return false
    Game.next()
    Game.timer = setTimeout(function() {
      Game._loop()
    }, 80)
  }

  Game.init = function() {
    var graph = document.querySelector('.js-contribution-graph')
    if (!graph) return false
    var id = 'gol-button-play'
    if (document.getElementById(id)) return false
    var play = document.createElement('a')
    play.id = id
    play.title = "Play Conway's Game of Life"
    play.innerHTML = 'Play'
    play.addEventListener('click', function(e) {
      e.preventDefault()
      Game.play()
    })
    var legend = document.querySelector('.contrib-legend')
    legend.insertBefore(play, legend.firstChild)
  }

  Game.play = function() {
    var gc = Game.container = document.querySelector('.js-calendar-graph').parentNode
    gc.setAttribute(CONTAINER, '')
    gc.appendChild(GameBoard.build())
    gc.appendChild(Controls.build())

    Game.reset()
    Controls
      .apply('hide', ['pause'])
      .onclick = function(btn) {
        var action = Game[btn.getAttribute('data-action')]
        action && action()
      }
  }

  Game.run = function() {
    Game.running = true
    Game.container.setAttribute(CONTAINER, 'running')
    Game._loop()
    Game._updateControls()
  }

  Game.next = function() {
    GameBoard.render(Game.board = next(Game.board))
    Game._updateGeneration(++Game.generation)
    if (GameBoard.isEmpty()) {
      Game._updateControls()
      return Game.pause()
    }
  }

  Game.pause = function() {
    clearTimeout(Game.timer)
    Game.running = false
    if (Game.container) {
      Game.container.setAttribute(CONTAINER, '')
    }
    Game._updateControls()
  }

  Game.clear = function() {
    GameBoard.render(Game.board = createStatusBoard())
    Game._updateGeneration(0)
    Game._updateControls()
  }

  Game.reset = function() {
    Game.pause()
    var intial = Game._getIntialStatus()
    GameBoard.render(Game.board = intial.board, intial.colors)
    GameBoard.ontoggle = Game._ontoggle
    GameBoard.animateBackground()
    Game._updateGeneration(0)
    Game._updateControls()
  }

  Game.close = function() {
    if (Game.container) {
      Game.pause()
      Game.container.removeAttribute(CONTAINER)
      GameBoard.remove()
      Controls.remove()
      Game.container = null
    }
  }

  Game.init()

  var container = document.querySelector('#js-pjax-container')
  var loaderBar = document.querySelector('#js-pjax-loader-bar')
  var maxAttempt = 100
  var isDetecting = false

  if (!container) return false
  container.addEventListener('click', function(e) {
    if (!isDetecting) {
      var link = e.target
      if (isTagName('span', link)) {
        link = e.target.parentNode
      }
      if (isTagName('a', link)
        && /underline\-nav\-item|js\-year\-link/.test(link.className)) {
        Game.close()
        detectPjaxEnd(function() {
          Game.init()
        }, maxAttempt)
      }
    }
  })

  function detectPjaxEnd(fn, attempt) {
    if (!attempt) {
      return isDetecting = false
    }
    isDetecting = true
    setTimeout(function() {
      if (/is\-loading/.test(loaderBar.className)
        || document.getElementById('gol-button-play')) {
        detectPjaxEnd(fn, --attempt)
      } else {
          setTimeout(function() {
            isDetecting = false
            fn()
          }, 500)
      }
    }, 500)
  }

}())
<commit_msg>cleanup<commit_after>(function() {

  var ROW = 13
  var COL = 60
  var TOTAL = ROW * COL
  var COLOR_DEAD = '#eeeeee'
  var COLOR_ALIVE = '#8cc665'
  var CONTAINER = 'data-gol-container'
  var EMPTY_ROW = repeat(COL, 0).split('').map(function(n) { return +n })

  function repeat(times, str) {
    if (str.repeat) return str.repeat(times)
    return new Array(times + 1).join(str)
  }

  function forEachIndex(fn) {
    for (var x = 0; x < ROW; ++x) {
      for (var y = 0; y < COL; ++y) {
        fn(x, y)
      }
    }
  }

  function forEachList(list, childSelector, fn) {
    if (typeof childSelector === 'function') {
      for (var i = 0; i < list.length; ++i) {
          childSelector(i, list[i])
      }
    } else {
      for (var x = 0; x < list.length; ++x) {
        var sublist = list[x].querySelectorAll(childSelector)
        for (var y = 0; y < sublist.length; ++y) {
          fn(y, x, sublist[y])
        }
      }
    }
  }

  function createStatusBoard(fn) {
    var board = []
    for (var i = 0; i < ROW; ++i) {
      board.push(EMPTY_ROW.slice(0))
    }
    forEachIndex(function(x, y) {
      board[x][y] = fn ? fn(x, y) : 0
    })
    return board
  }

  function countNeighbors(board, x, y) {
    var xa = x ? (x - 1) : (ROW - 1),
      xc = (x == (ROW - 1)) ? 0 : (x + 1),
      ya = y ? (y - 1) : (COL - 1),
      yc = ( y == (COL - 1)) ? 0 : (y + 1)
    return (
      board[xa][ya] + board[x][ya] + board[xc][ya] +
      board[xa][y]  +              + board[xc][y]  +
      board[xa][yc] + board[x][yc] + board[xc][yc]
    )
  }

  function next(board) {
    return createStatusBoard(function(x, y) {
      var neighbors = countNeighbors(board, x, y)
      var status = board[x][y]
      switch(status) {
        case 1: if (neighbors <= 1 || neighbors >= 4) status = 0
        case 0: if (neighbors === 3) status = 1
      }
      return status
    })
  }

  function isTagName(name, el) {
    if (el) {
      return el.tagName.toLowerCase() === name
    }
  }

  function setTransitonDelay(el, delay) {
    if (el) {
      el.style.transitionDelay = el.style.webkitTransitionDelay = delay
    }
  }

  function fillColor(cell, color) {
    if (cell) {
      if (isTagName('rect', cell)) {
        return cell.getAttribute('fill')
      }
      return color
        ? (cell.style.backgroundColor = color)
        : cell.style.backgroundColor
    }
  }

  function getKey(x, y) {
    return x + '-' + y
  }

  var getPosition = (function() {
    var position = {}
    forEachIndex(function(x, y) {
      position[getKey(x, y)] = { x: x, y: y }
    })
    return function(key) {
      return position[key] || {}
    }
  }())

  var GameBoard = (function() {
    var id = 'gol-contribution-board'
    var cells = {}
    var board = null
    var emptyCells = repeat(COL,
      '
' + repeat(ROW, '

') + '
'
    )
    function toggle(e) {
      var cell = e.target
      if (isTagName('li', cell)) {
        GameBoard.ontoggle && GameBoard.ontoggle(cell)
      }
    }
    return {
      build: function() {
        if (board) return board
        board = document.createElement('div')
        board.id = id
        board.innerHTML = emptyCells
        board.addEventListener('click', toggle)
        forEachList(board.querySelectorAll('ul'), 'li', function(x, y, elem) {
          var key = getKey(x, y)
          cells[key] = elem
          elem.setAttribute('data-key', key)
        })
        return board
      },
      render: function(statusBoard, colors) {
        forEachIndex(function(x, y) {
          var key = getKey(x, y)
          var status = statusBoard[x][y]
          fillColor(cells[key], status
            ? (colors ? colors[key] : COLOR_ALIVE)
            : COLOR_DEAD
          )
        })
      },
      isEmpty: function() {
        return TOTAL === board.querySelectorAll('li[style$="238);"]').length
      },
      animateBackground: function() {
        var cells = board.querySelectorAll('li:not([style$="238);"])')
        forEachList(cells, function(_, cell) {
          setTransitonDelay(cell, (600 * Math.random()) + 'ms')
        })
        setTimeout(function() {
          forEachList(cells, function(_, cell) {
            cell.style.backgroundColor = COLOR_ALIVE
          })
        })
        setTimeout(function() {
          forEachList(cells, function(_, cell) {
            setTransitonDelay(cell, '')
          })
        }, 600)
      },
      remove: function() {
        cells = {}
        if (board) {
          board.removeEventListener('click', toggle)
          board.parentNode.removeChild(board)
          board = null
        }
      }
    }
  }())

  var Controls = (function() {
    var id = 'gol-controls'
    var controls = null
    var generation = null
    var buttons = {}
    var actions = ['run', 'pause', 'next', 'clear', 'reset', 'close']
    var content = '0' + (
      actions.map(function(name) { return '' }).join('')
    )
    function action(e) {
      var btn = e.target
      if (btn.hasAttribute('data-action') && !btn.hasAttribute('disabled')) {
        Controls.onclick && Controls.onclick(btn)
      }
    }
    return {
      build: function() {
        if (controls) return controls
        controls = document.createElement('div')
        controls.id = id
        controls.innerHTML = content
        generation = controls.querySelector('#gol-generation')
        actions.forEach(function(name) {
          buttons[name] = controls.querySelector('[data-action="' + name + '"]')
        })
        controls.addEventListener('click', action)
        return controls
      },
      apply: function(operation, names) {
        if (!Array.isArray(names)) names = [names]
        names.forEach(function(name) {
          var btn = buttons[name]
          switch (operation) {
            case 'disable': btn.setAttribute('disabled', true); break
            case 'enable': btn.removeAttribute('disabled'); break
            case 'show': btn.style.display = ''; break
            case 'hide': btn.style.display = 'none'
          }
        })
        return this
      },
      remove: function() {
        buttons = {}
        if (controls) {
          controls.removeEventListener('click', action)
          controls.parentNode.removeChild(controls)
          generation = null
          controls = null
        }
      },
      generation: function(count) {
        generation.innerHTML = count
      }
    }
  }())

  var Game = {
    generation: 0,
    running: false,
    timer: null,
    container: null,
    board: null
  }

  Game._getIntialStatus = function() {
    var board = createStatusBoard()
    var gs = Game.container.querySelectorAll('.js-calendar-graph-svg > g > g')
    var colors = {}
    forEachList(gs, 'rect', function(x, y, rect) {
      var color = fillColor(rect)
      var dx = x + 3, dy = y + 4
      board[dx][dy] = (color === COLOR_DEAD) ? 0 : 1
      colors[getKey(dx, dy)] = color
    })
    return { board: board, colors: colors }
  }

  Game._updateControls = function() {
    Game.running
      ? Controls
        .apply('disable', ['next', 'clear', 'reset'])
        .apply('show', 'pause')
        .apply('hide', 'run')
      : Controls
        .apply('enable', 'reset')
        .apply('show', 'run')
        .apply('hide', 'pause')
        .apply(
          (GameBoard.isEmpty() ? 'disable' : 'enable'),
          ['run', 'next', 'clear']
        )
  }

  Game._updateGeneration = function(count) {
    Controls.generation(Game.generation = count)
  }

  Game._ontoggle = function(cell) {
    if (Game.running) return false
    var mapping = {
      color: [COLOR_ALIVE, COLOR_DEAD], status: [1, 0]
    }
    var p = getPosition(cell.getAttribute('data-key'))
    var status = Game.board[p.x][p.y]
    fillColor(cell, mapping.color[status])
    Game.board[p.x][p.y] = mapping.status[status]
    Game._updateGeneration(0)
    Game._updateControls()
  }

  Game._loop = function() {
    if (!Game.running) return false
    Game.next()
    Game.timer = setTimeout(function() {
      Game._loop()
    }, 80)
  }

  Game.init = function() {
    var graph = document.querySelector('.js-contribution-graph')
    if (!graph) return false
    var id = 'gol-button-play'
    if (document.getElementById(id)) return false
    var play = document.createElement('a')
    play.id = id
    play.title = "Play Conway's Game of Life"
    play.innerHTML = 'Play'
    play.addEventListener('click', function(e) {
      e.preventDefault()
      Game.play()
    })
    var legend = document.querySelector('.contrib-legend')
    legend.insertBefore(play, legend.firstChild)
  }

  Game.play = function() {
    var gc = Game.container = document.querySelector('.js-calendar-graph').parentNode
    gc.setAttribute(CONTAINER, '')
    gc.appendChild(GameBoard.build())
    gc.appendChild(Controls.build())

    Game.reset()
    Controls
      .apply('hide', ['pause'])
      .onclick = function(btn) {
        var action = Game[btn.getAttribute('data-action')]
        action && action()
      }
  }

  Game.run = function() {
    Game.running = true
    Game.container.setAttribute(CONTAINER, 'running')
    Game._loop()
    Game._updateControls()
  }

  Game.next = function() {
    GameBoard.render(Game.board = next(Game.board))
    Game._updateGeneration(++Game.generation)
    if (GameBoard.isEmpty()) {
      Game._updateControls()
      return Game.pause()
    }
  }

  Game.pause = function() {
    clearTimeout(Game.timer)
    Game.running = false
    if (Game.container) {
      Game.container.setAttribute(CONTAINER, '')
    }
    Game._updateControls()
  }

  Game.clear = function() {
    GameBoard.render(Game.board = createStatusBoard())
    Game._updateGeneration(0)
    Game._updateControls()
  }

  Game.reset = function() {
    Game.pause()
    var intial = Game._getIntialStatus()
    GameBoard.render(Game.board = intial.board, intial.colors)
    GameBoard.ontoggle = Game._ontoggle
    GameBoard.animateBackground()
    Game._updateGeneration(0)
    Game._updateControls()
  }

  Game.close = function() {
    if (Game.container) {
      Game.pause()
      Game.container.removeAttribute(CONTAINER)
      GameBoard.remove()
      Controls.remove()
      Game.container = null
    }
  }

  Game.init()

  var container = document.querySelector('#js-pjax-container')
  var loaderBar = document.querySelector('#js-pjax-loader-bar')
  var maxAttempt = 100
  var isDetecting = false

  if (!container) return false
  container.addEventListener('click', function(e) {
    if (!isDetecting) {
      var link = e.target
      if (isTagName('span', link)) {
        link = e.target.parentNode
      }
      if (isTagName('a', link)
        && /underline\-nav\-item|js\-year\-link/.test(link.className)) {
        Game.close()
        detectPjaxEnd(function() {
          Game.init()
        }, maxAttempt)
      }
    }
  })

  function detectPjaxEnd(fn, attempt) {
    if (!attempt) {
      return isDetecting = false
    }
    isDetecting = true
    setTimeout(function() {
      if (/is\-loading/.test(loaderBar.className)
        || document.getElementById('gol-button-play')) {
        detectPjaxEnd(fn, --attempt)
      } else {
          setTimeout(function() {
            isDetecting = false
            fn()
          }, 500)
      }
    }, 500)
  }

}())
<|endoftext|>