<commit_before>
var React = require('react/addons'),
	logger = require('./logging').getLogger(__LOGGER__),
	RequestContext = require('./context/RequestContext'),
	RequestLocalStorage = require('./util/RequestLocalStorage'),
	Q = require('q'),
	cssHelper = require('./util/ClientCssHelper'),
	EventEmitter = require("events").EventEmitter,
	ClientRequest = require("./ClientRequest"),
	History = require('./components/History'),
	PageUtil = require("./util/PageUtil"),
	TritonAgent = require('./util/TritonAgent');

// for dev tools
window.React = React;

var TRITON_DATA_ATTRIBUTE = "data-triton-root-id";

class ClientController extends EventEmitter {

	constructor ({routes}) {
		var dehydratedState = window.__tritonState;

		checkNotEmpty(dehydratedState, 'InitialContext');
		checkNotEmpty(dehydratedState, 'Config');

		RequestLocalStorage.startRequest();

		this.config = buildConfig(dehydratedState.Config);

		if (routes.onClientConfigLoaded) {
			routes.onClientConfigLoaded.call(this);
		}

		this.context = buildContext(
			dehydratedState.InitialContext,
			routes
		);
		this.mountNode = document.getElementById('content');

		var irDfd = this._initialRenderDfd = Q.defer();
		this.once('render', irDfd.resolve.bind(irDfd));

		this._setupNavigateListener();
		this._setupLateArrivalHandler();

		this._previouslyRendered = false;
	}

	terminate() {
        this._history.off(this._historyListener);
        this._historyListener = null;
        this._history = null;
	}

	_startRequest() {

		// If this is a secondary request (client transition) within a
		// session, then we'll get a fresh RequestLocalStorage
		// container.
		if (this._previouslyRendered){
			RequestLocalStorage.startRequest();
			// we need to re-register the request context as a RequestLocal.
			this.context.registerRequestLocal();
		}
	}

	_setupNavigateListener () {
		var context = this.context;

		context.onNavigateStart(this._startRequest.bind(this));

		/**
		 * type is one of
		 *    History.events.PUSHSTATE: user clicked something to go forward but browser didn't do a
		 * full page load
		 *    History.events.POPSTATE: user clicked back button but browser didn't do a full page load
		 *    History.events.PAGELOAD: full browser page load, not using History API.
		 */
		context.onNavigate( (err, page, path, type, isFragment) => {
			logger.debug('Executing navigate action');

			// if this is a History.events.PUSHSTATE navigation, we should change the URL in the bar location bar
			// before rendering.
			// note that for browsers that do not have pushState, this will result in a window.location change
			// and full browser load. It's kind of late to do that, as we may have waited for handleRoute to
			// finish asynchronously. perhaps we should have an "URLChanged" event that happens before "NavigateDone".
			if (type === History.events.PUSHSTATE && this._history) {
				this._history.pushState(null, null, path);
			}

			if (err) {
				// redirects are sent as errors, so let's handle it if that's the case.
				if (err.status && (err.status === 301 || err.status === 302)) {
					if (!err.redirectUrl) {
						console.error("A redirect status was sent without a corresponding redirect redirectUrl.", err);
					} else {
						setTimeout(() => {
							this._history.replaceState(null, null, err.redirectUrl);
							this.context.navigate(new ClientRequest(err.redirectUrl));
						}, 0);
					}
				} else {
					logger.error("onNavigate error", err);
				}
				return;
			}

			var routeName = context.navigator.getCurrentRoute().name;

			if (!this._previouslyRendered) {
				cssHelper.registerPageLoad(routeName);
			} else {
				// render the document title.
				this._renderTitle(page);

				// render the base tag.
				this._renderBase(page);

				this._renderMetaTags(page);

			}

			cssHelper.ensureCss(routeName, page);

			page.getBodyClasses().then((classes) => {
				classes.push(`route-${routeName}`);
				document.body.className = classes.join(' ');
			}).then(() => this._render(page))

		});

	}

	_renderTitle(page) {
		page.getTitle().then(newTitle => {
			if (newTitle && newTitle !== document.title) {
				document.title = newTitle;
			}
		}).catch(err => console.error("Error while setting the document title", err));
	}

	_renderBase(page) {
		page.getBase().then(base => {
			var currentBaseTag = document.head.querySelector("head base");
			if (base === null) {
				// get rid of the current base tag.
				if (currentBaseTag) currentBaseTag.parentNode.removeChild(currentBaseTag);
			} else {
				// we need a base tag. add one if it's not there yet.
				if (!currentBaseTag) {
					currentBaseTag = document.createElement("base");
					document.head.appendChild(currentBaseTag);
				}
				currentBaseTag.href = base.href;
				if (base.target) currentBaseTag.target = base.target;
			}

		});
	}

	_renderMetaTags(page) {
		// first, remove all the current meta tags.
		var currentMetaTags = document.head.querySelectorAll("meta");
		for (var i = 0; i < currentMetaTags.length; i++) {
			currentMetaTags[i].parentNode.removeChild(currentMetaTags[i]);
		}

		// now add all the meta tags for the new page.
		page.getMetaTags().forEach((metaTagPromise) => {
			metaTagPromise.then((metaTag) => {
				var parent = document.head;
				if (metaTag.noscript) {
					var noscript = document.createElement("noscript");
					parent.appendChild(noscript);
					parent = noscript;
				}

				var meta = document.createElement("meta");
				["name", "httpEquiv", "charset", "content"].forEach((name) => {
					if (metaTag[name]) meta[name] = metaTag[name];
				});

				parent.appendChild(meta);
			});
		});
	}

	_render (page) {
		var t0 = new Date;
		logger.debug('React Rendering');

		// if we were previously rendered on the client, clean up the old divs and
		// their ReactComponents.
		this._cleanupPreviousRender(this.mountNode);

		// if we are reattaching to server-generated HTML, we should find the root elements that were sent down.
		// we store them in a temp array indexed by their triton-root-id.
		var serverRenderedRoots = [];
		this._getRootElements(this.mountNode).forEach((rootElement) => {
			serverRenderedRoots[rootElement.getAttribute(TRITON_DATA_ATTRIBUTE)] = rootElement;
		});

		var elementPromises = PageUtil.standardizeElements(page.getElements());

		var newRenderedElements = [];
		var renderElement = (element, index) => {
			// if someone already rendered this element, let's not!
			if (newRenderedElements[index]) return;

			// for each ReactElement that we want to render, either use the server-rendered root element, or
			// create a new root element.
			logger.debug("Rendering root node #" + index);
			var root = serverRenderedRoots[index] || this._createTritonRootNode(this.mountNode, index);

			// TODO: get rid of context once continuation-local-storage holds our important context vars.
			element = React.addons.cloneWithProps(element, { context: this.context });
			newRenderedElements[index] = React.render(element, root);
			if (index === elementPromises.length - 1) {
				logger.debug('React Rendered');
				logger.time('render', new Date - t0);
				this.emit("render");
			}
		};

		// if and when the loader runs out of cache, we should render everything we have synchronously through EarlyPromises.
		// this lets us render when the server timed out.
		// TODO: should we ONLY do this when the server times out?
		TritonAgent.cache().whenCacheDepleted().then(() => {
			logger.debug("Loader cache depleted; rendering elements.");
			elementPromises.forEach((elementEarlyPromise, index) => {
				renderElement(elementEarlyPromise.getValue(), index);
			});
		});

		// I find the control flow for chaining promises impossibly mind-bending, but what I intended was something
		// like:
		//
		//		elementPromises.forEach((elementPromise, index) => {
		//			var element = await elementPromise;
		//			renderElement(element, index);
		//		});
		//		this._previouslyRendered = true;
		//		this.emit("render");
		//
		// if it was using ES7 async/await syntax. I learned how to chain an array of promises in ES5/6
		// from http://bahmutov.calepin.co/chaining-promises.html , and apparently it looks like the code below. But
		// if I got something wrong, what I intended was the control flow expressed in this comment.
		return elementPromises.reduce((chain, next, index) => {
			return chain.then((element) => {
		 		renderElement(element, index - 1);
				return next;
			})
		}).then((element) => {
			// reduce is called length - 1 times. we need to call one final time here to make sure we
			// chain the final promise.
	 		renderElement(element, elementPromises.length - 1);

			this._previouslyRendered = true;
		}).catch((err) => {
			logger.error("Error while rendering.", err.stack);
		});
	}

	/**
	 * Cleans up a previous React render in the document. Unmounts all the components and destoys the mounting
	 * DOM node(s) that were created.
	 */
	_cleanupPreviousRender(mountNode) {
		if (this._previouslyRendered) {
			logger.debug("Removing previous page's React components");

			this._getRootElements(mountNode).forEach((tritonRoot) => {
				// since this node has a "data-triton-root-id" attribute, we can assume that we created it and
				// should destroy it. Destruction means first unmounting from React and then destroying the DOM node.
				React.unmountComponentAtNode(tritonRoot);
				mountNode.removeChild(tritonRoot);
			});
		}
	}

	/**
	 * Returns an array of all of the root elements that are children of mountNode. The root elements
	 * should all have a "data-triton-root-id" attribute and be direct children of mountNode.
	 */
	_getRootElements(mountNode) {
		// if children returned an array instead of a NodeList, we could use .filter(), but
		// alas, it does not. We could copy it over to an array, but seems easier to just iterate
		// over the NodeList.
		var potentialRoots = mountNode.children;
		var result = [];
		for (var i = 0; i < potentialRoots.length; i++) {
			var potentialRoot = potentialRoots[i];
			if (potentialRoot.hasAttribute(TRITON_DATA_ATTRIBUTE)) {
				// since this node has a "data-triton-root-id" attribute, we can assume that we created it.
				result.push(potentialRoot);
			} else {
				// it's deeply troubling that there's a div we didn't create, but for now, just warn, and
				// don't obliterate the node.
				console.warn("Found an element inside Triton's rendering canvas that did not have data-triton-root-id " +
					"and was probably not created by Triton. Other code may be manually mucking with the DOM, which could " +
					"cause unpredictable behavior", potentialRoot);
			}
		}
		return result;
	}

	/**
	 * This method creates a new div to render a ReactElement in to at the end of the mount node.
	 */
	_createTritonRootNode(mountNode, index) {
		var root = document.createElement("div");
		root.setAttribute(TRITON_DATA_ATTRIBUTE, index);
		mountNode.appendChild(root);
		return root;
	}

	init () {

		var unloadHandler = () => {this.terminate();};

		if (window && window.addEventListener) {
		    window.addEventListener("unload", unloadHandler);
		}
		else if (window && window.attachEvent) {
		    window.attachEvent("onunload", unloadHandler);
		}

		var location = window.location;
		var path = location.pathname + location.search;
		this._initializeHistoryListener(this.context);
		this.context.navigate(new ClientRequest(path));
	}

	/**
	 * Initializes us to listen to back button events. When the user presses the back button, the history
	 * listener will be called and cause a navigate() event.
	 */
	_initializeHistoryListener(context) {

        this._historyListener = (e) => {
            if (context) {
                var path = this._history.getPath();

                // pass in "popstate" because this is when a user clicks the back button.
                context.navigate(new ClientRequest(path), History.events.POPSTATE);

            }
        };

        this._history = new History();
        this._history.on(this._historyListener);
	}

	_setupLateArrivalHandler () {
		// used by <script> callbacks to register data sent down on the
		// initial connection after initial render
		window.__lateArrival = this.lateArrival.bind(this);
	}

	lateArrival (url, resString) {
		this._initialRenderDfd.promise.done( () => {
			TritonAgent.cache().lateArrival(url, JSON.parse(decodeURIComponent(resString)));
		});
	}

}

function checkNotEmpty(state, key) {
	if (typeof state[key] === 'undefined') {
		var msg = key + ' not defined in dehydrated state';
		logger.error(msg)
		throw new Error(msg);
	}
}

function buildConfig(dehydratedConfig) {
	// rehydrate the config object
	var config = require("./config")();
	config.rehydrate(dehydratedConfig);
	return config;
}

function buildContext(dehydratedContext, routes) {
	var context = new RequestContext.Builder()
		.setRoutes(routes)
		.create();
	context.rehydrate(dehydratedContext);
	return context;
}

module.exports = ClientController;

<commit_msg>Client-side logging of errors in `getElements()`<commit_after>
var React = require('react/addons'),
	logger = require('./logging').getLogger(__LOGGER__),
	RequestContext = require('./context/RequestContext'),
	RequestLocalStorage = require('./util/RequestLocalStorage'),
	Q = require('q'),
	cssHelper = require('./util/ClientCssHelper'),
	EventEmitter = require("events").EventEmitter,
	ClientRequest = require("./ClientRequest"),
	History = require('./components/History'),
	PageUtil = require("./util/PageUtil"),
	TritonAgent = require('./util/TritonAgent');

// for dev tools
window.React = React;

var TRITON_DATA_ATTRIBUTE = "data-triton-root-id";

class ClientController extends EventEmitter {

	constructor ({routes}) {
		var dehydratedState = window.__tritonState;

		checkNotEmpty(dehydratedState, 'InitialContext');
		checkNotEmpty(dehydratedState, 'Config');

		RequestLocalStorage.startRequest();

		this.config = buildConfig(dehydratedState.Config);

		if (routes.onClientConfigLoaded) {
			routes.onClientConfigLoaded.call(this);
		}

		this.context = buildContext(
			dehydratedState.InitialContext,
			routes
		);
		this.mountNode = document.getElementById('content');

		var irDfd = this._initialRenderDfd = Q.defer();
		this.once('render', irDfd.resolve.bind(irDfd));

		this._setupNavigateListener();
		this._setupLateArrivalHandler();

		this._previouslyRendered = false;
	}

	terminate() {
        this._history.off(this._historyListener);
        this._historyListener = null;
        this._history = null;
	}

	_startRequest() {

		// If this is a secondary request (client transition) within a
		// session, then we'll get a fresh RequestLocalStorage
		// container.
		if (this._previouslyRendered){
			RequestLocalStorage.startRequest();
			// we need to re-register the request context as a RequestLocal.
			this.context.registerRequestLocal();
		}
	}

	_setupNavigateListener () {
		var context = this.context;

		context.onNavigateStart(this._startRequest.bind(this));

		/**
		 * type is one of
		 *    History.events.PUSHSTATE: user clicked something to go forward but browser didn't do a
		 * full page load
		 *    History.events.POPSTATE: user clicked back button but browser didn't do a full page load
		 *    History.events.PAGELOAD: full browser page load, not using History API.
		 */
		context.onNavigate( (err, page, path, type, isFragment) => {
			logger.debug('Executing navigate action');

			// if this is a History.events.PUSHSTATE navigation, we should change the URL in the bar location bar
			// before rendering.
			// note that for browsers that do not have pushState, this will result in a window.location change
			// and full browser load. It's kind of late to do that, as we may have waited for handleRoute to
			// finish asynchronously. perhaps we should have an "URLChanged" event that happens before "NavigateDone".
			if (type === History.events.PUSHSTATE && this._history) {
				this._history.pushState(null, null, path);
			}

			if (err) {
				// redirects are sent as errors, so let's handle it if that's the case.
				if (err.status && (err.status === 301 || err.status === 302)) {
					if (!err.redirectUrl) {
						console.error("A redirect status was sent without a corresponding redirect redirectUrl.", err);
					} else {
						setTimeout(() => {
							this._history.replaceState(null, null, err.redirectUrl);
							this.context.navigate(new ClientRequest(err.redirectUrl));
						}, 0);
					}
				} else {
					logger.error("onNavigate error", err);
				}
				return;
			}

			var routeName = context.navigator.getCurrentRoute().name;

			if (!this._previouslyRendered) {
				cssHelper.registerPageLoad(routeName);
			} else {
				// render the document title.
				this._renderTitle(page);

				// render the base tag.
				this._renderBase(page);

				this._renderMetaTags(page);

			}

			cssHelper.ensureCss(routeName, page);

			page.getBodyClasses().then((classes) => {
				classes.push(`route-${routeName}`);
				document.body.className = classes.join(' ');
			}).then(() => this._render(page)).catch((err) => {
				logger.error("Error while rendering", err.stack);
			});

		});

	}

	_renderTitle(page) {
		page.getTitle().then(newTitle => {
			if (newTitle && newTitle !== document.title) {
				document.title = newTitle;
			}
		}).catch(err => console.error("Error while setting the document title", err));
	}

	_renderBase(page) {
		page.getBase().then(base => {
			var currentBaseTag = document.head.querySelector("head base");
			if (base === null) {
				// get rid of the current base tag.
				if (currentBaseTag) currentBaseTag.parentNode.removeChild(currentBaseTag);
			} else {
				// we need a base tag. add one if it's not there yet.
				if (!currentBaseTag) {
					currentBaseTag = document.createElement("base");
					document.head.appendChild(currentBaseTag);
				}
				currentBaseTag.href = base.href;
				if (base.target) currentBaseTag.target = base.target;
			}

		});
	}

	_renderMetaTags(page) {
		// first, remove all the current meta tags.
		var currentMetaTags = document.head.querySelectorAll("meta");
		for (var i = 0; i < currentMetaTags.length; i++) {
			currentMetaTags[i].parentNode.removeChild(currentMetaTags[i]);
		}

		// now add all the meta tags for the new page.
		page.getMetaTags().forEach((metaTagPromise) => {
			metaTagPromise.then((metaTag) => {
				var parent = document.head;
				if (metaTag.noscript) {
					var noscript = document.createElement("noscript");
					parent.appendChild(noscript);
					parent = noscript;
				}

				var meta = document.createElement("meta");
				["name", "httpEquiv", "charset", "content"].forEach((name) => {
					if (metaTag[name]) meta[name] = metaTag[name];
				});

				parent.appendChild(meta);
			});
		});
	}

	_render (page) {
		var t0 = new Date;
		logger.debug('React Rendering');

		// if we were previously rendered on the client, clean up the old divs and
		// their ReactComponents.
		this._cleanupPreviousRender(this.mountNode);

		// if we are reattaching to server-generated HTML, we should find the root elements that were sent down.
		// we store them in a temp array indexed by their triton-root-id.
		var serverRenderedRoots = [];
		this._getRootElements(this.mountNode).forEach((rootElement) => {
			serverRenderedRoots[rootElement.getAttribute(TRITON_DATA_ATTRIBUTE)] = rootElement;
		});

		var elementPromises = PageUtil.standardizeElements(page.getElements());

		var newRenderedElements = [];
		var renderElement = (element, index) => {
			// if someone already rendered this element, let's not!
			if (newRenderedElements[index]) return;

			// for each ReactElement that we want to render, either use the server-rendered root element, or
			// create a new root element.
			logger.debug("Rendering root node #" + index);
			var root = serverRenderedRoots[index] || this._createTritonRootNode(this.mountNode, index);

			// TODO: get rid of context once continuation-local-storage holds our important context vars.
			element = React.addons.cloneWithProps(element, { context: this.context });
			newRenderedElements[index] = React.render(element, root);
			if (index === elementPromises.length - 1) {
				logger.debug('React Rendered');
				logger.time('render', new Date - t0);
				this.emit("render");
			}
		};

		// if and when the loader runs out of cache, we should render everything we have synchronously through EarlyPromises.
		// this lets us render when the server timed out.
		// TODO: should we ONLY do this when the server times out?
		TritonAgent.cache().whenCacheDepleted().then(() => {
			logger.debug("Loader cache depleted; rendering elements.");
			elementPromises.forEach((elementEarlyPromise, index) => {
				renderElement(elementEarlyPromise.getValue(), index);
			});
		});

		// I find the control flow for chaining promises impossibly mind-bending, but what I intended was something
		// like:
		//
		//		elementPromises.forEach((elementPromise, index) => {
		//			var element = await elementPromise;
		//			renderElement(element, index);
		//		});
		//		this._previouslyRendered = true;
		//		this.emit("render");
		//
		// if it was using ES7 async/await syntax. I learned how to chain an array of promises in ES5/6
		// from http://bahmutov.calepin.co/chaining-promises.html , and apparently it looks like the code below. But
		// if I got something wrong, what I intended was the control flow expressed in this comment.
		return elementPromises.reduce((chain, next, index) => {
			return chain.then((element) => {
		 		renderElement(element, index - 1);
				return next;
			})
		}).then((element) => {
			// reduce is called length - 1 times. we need to call one final time here to make sure we
			// chain the final promise.
	 		renderElement(element, elementPromises.length - 1);

			this._previouslyRendered = true;
		}).catch((err) => {
			logger.error("Error while rendering.", err.stack);
		});
	}

	/**
	 * Cleans up a previous React render in the document. Unmounts all the components and destoys the mounting
	 * DOM node(s) that were created.
	 */
	_cleanupPreviousRender(mountNode) {
		if (this._previouslyRendered) {
			logger.debug("Removing previous page's React components");

			this._getRootElements(mountNode).forEach((tritonRoot) => {
				// since this node has a "data-triton-root-id" attribute, we can assume that we created it and
				// should destroy it. Destruction means first unmounting from React and then destroying the DOM node.
				React.unmountComponentAtNode(tritonRoot);
				mountNode.removeChild(tritonRoot);
			});
		}
	}

	/**
	 * Returns an array of all of the root elements that are children of mountNode. The root elements
	 * should all have a "data-triton-root-id" attribute and be direct children of mountNode.
	 */
	_getRootElements(mountNode) {
		// if children returned an array instead of a NodeList, we could use .filter(), but
		// alas, it does not. We could copy it over to an array, but seems easier to just iterate
		// over the NodeList.
		var potentialRoots = mountNode.children;
		var result = [];
		for (var i = 0; i < potentialRoots.length; i++) {
			var potentialRoot = potentialRoots[i];
			if (potentialRoot.hasAttribute(TRITON_DATA_ATTRIBUTE)) {
				// since this node has a "data-triton-root-id" attribute, we can assume that we created it.
				result.push(potentialRoot);
			} else {
				// it's deeply troubling that there's a div we didn't create, but for now, just warn, and
				// don't obliterate the node.
				console.warn("Found an element inside Triton's rendering canvas that did not have data-triton-root-id " +
					"and was probably not created by Triton. Other code may be manually mucking with the DOM, which could " +
					"cause unpredictable behavior", potentialRoot);
			}
		}
		return result;
	}

	/**
	 * This method creates a new div to render a ReactElement in to at the end of the mount node.
	 */
	_createTritonRootNode(mountNode, index) {
		var root = document.createElement("div");
		root.setAttribute(TRITON_DATA_ATTRIBUTE, index);
		mountNode.appendChild(root);
		return root;
	}

	init () {

		var unloadHandler = () => {this.terminate();};

		if (window && window.addEventListener) {
		    window.addEventListener("unload", unloadHandler);
		}
		else if (window && window.attachEvent) {
		    window.attachEvent("onunload", unloadHandler);
		}

		var location = window.location;
		var path = location.pathname + location.search;
		this._initializeHistoryListener(this.context);
		this.context.navigate(new ClientRequest(path));
	}

	/**
	 * Initializes us to listen to back button events. When the user presses the back button, the history
	 * listener will be called and cause a navigate() event.
	 */
	_initializeHistoryListener(context) {

        this._historyListener = (e) => {
            if (context) {
                var path = this._history.getPath();

                // pass in "popstate" because this is when a user clicks the back button.
                context.navigate(new ClientRequest(path), History.events.POPSTATE);

            }
        };

        this._history = new History();
        this._history.on(this._historyListener);
	}

	_setupLateArrivalHandler () {
		// used by <script> callbacks to register data sent down on the
		// initial connection after initial render
		window.__lateArrival = this.lateArrival.bind(this);
	}

	lateArrival (url, resString) {
		this._initialRenderDfd.promise.done( () => {
			TritonAgent.cache().lateArrival(url, JSON.parse(decodeURIComponent(resString)));
		});
	}

}

function checkNotEmpty(state, key) {
	if (typeof state[key] === 'undefined') {
		var msg = key + ' not defined in dehydrated state';
		logger.error(msg)
		throw new Error(msg);
	}
}

function buildConfig(dehydratedConfig) {
	// rehydrate the config object
	var config = require("./config")();
	config.rehydrate(dehydratedConfig);
	return config;
}

function buildContext(dehydratedContext, routes) {
	var context = new RequestContext.Builder()
		.setRoutes(routes)
		.create();
	context.rehydrate(dehydratedContext);
	return context;
}

module.exports = ClientController;

<|endoftext|>


<commit_before>#!/usr/bin/env node
;(function() {
  'use strict';

  /** The Node filesystem, path, `zlib`, and child process modules */
  var fs = require('fs'),
      gzip = require('zlib').gzip,
      path = require('path'),
      spawn = require('child_process').spawn;

  /** The build directory containing the build scripts */
  var buildPath = path.join(__dirname, 'build');

  /** The directory where the Closure Compiler is located */
  var closurePath = path.join(__dirname, 'vendor', 'closure-compiler', 'compiler.jar');

  /** The distribution directory */
  var distPath = path.join(__dirname, 'dist');

  /** Load other modules */
  var preprocess = require(path.join(buildPath, 'pre-compile')),
      postprocess = require(path.join(buildPath, 'post-compile')),
      uglifyJS = require(path.join(__dirname, 'vendor', 'uglifyjs', 'uglify-js'));

  /** Used to shares values between multiple callbacks */
  var accumulator = {
    'combined': {},
    'compiled': {},
    'uglified': {}
  };

  /** Closure Compiler command-line options */
  var closureOptions = [
    '--compilation_level=ADVANCED_OPTIMIZATIONS',
    '--language_in=ECMASCRIPT5_STRICT',
    '--warning_level=QUIET'
  ];

  /** The pre-processed Lo-Dash source */
  var source = preprocess(fs.readFileSync(path.join(__dirname, 'lodash.js'), 'utf8'));

  /*--------------------------------------------------------------------------*/

  /**
   * Compresses a `source` string using the Closure Compiler. Yields the
   * minified result, and any exceptions encountered, to a `callback` function.
   *
   * @private
   * @param {String} source The JavaScript source to minify.
   * @param {String} [message] The message to log.
   * @param {Function} callback The function to call once the process completes.
   */
  function closureCompile(source, message, callback) {
    // the standard error stream, standard output stream, and Closure Compiler process
    var error = '',
        output = '',
        compiler = spawn('java', ['-jar', closurePath].concat(closureOptions));

    // juggle arguments
    if (typeof message == 'function') {
      callback = message;
      message = null;
    }

    console.log(message == null ? 'Compressing lodash.js using the Closure Compiler...' : message);

    compiler.stdout.on('data', function(data) {
      // append the data to the output stream
      output += data;
    });

    compiler.stderr.on('data', function(data) {
      // append the error message to the error stream
      error += data;
    });

    compiler.on('exit', function(status) {
      var exception = null;

      // `status` contains the process exit code
      if (status) {
        exception = new Error(error);
        exception.status = status;
      }
      callback(exception, output);
    });

    // proxy the standard input to the Closure Compiler
    compiler.stdin.end(source);
  }

  /**
   * Compresses a `source` string using UglifyJS. Yields the result to a
   * `callback` function. This function is synchronous; the `callback` is used
   * for symmetry.
   *
   * @private
   * @param {String} source The JavaScript source to minify.
   * @param {String} [message] The message to log.
   * @param {Function} callback The function to call once the process completes.
   */
  function uglify(source, message, callback) {
    var exception,
        result,
        ugly = uglifyJS.uglify;

    // juggle arguments
    if (typeof message == 'function') {
      callback = message;
      message = null;
    }

    console.log(message == null ? 'Compressing lodash.js using UglifyJS...' : message);

    try {
      result = ugly.gen_code(
        // enable unsafe transformations
        ugly.ast_squeeze_more(
          ugly.ast_squeeze(
            // munge variable and function names, excluding the special `define`
            // function exposed by AMD loaders
            ugly.ast_mangle(uglifyJS.parser.parse(source), {
              'except': ['define']
            }
        ))), {
        'ascii_only': true
      });
    } catch(e) {
      exception = e;
    }
    // lines are restricted to 500 characters for consistency with the Closure Compiler
    callback(exception, result && ugly.split_lines(result, 500));
  }

  /*--------------------------------------------------------------------------*/

  /**
   * The `closureCompile()` callback.
   *
   * @private
   * @param {Object|Undefined} exception The error object.
   * @param {String} result The resulting minified source.
   */
  function onClosureCompile(exception, result) {
    if (exception) {
      throw exception;
    }
    // store the post-processed Closure Compiler result and gzip it
    accumulator.compiled.source = result = postprocess(result);
    gzip(result, onClosureGzip);
  }

  /**
   * The Closure Compiler `gzip` callback.
   *
   * @private
   * @param {Object|Undefined} exception The error object.
   * @param {Buffer} result The resulting gzipped source.
   */
  function onClosureGzip(exception, result) {
    if (exception) {
      throw exception;
    }
    // store the gzipped result and report the size
    accumulator.compiled.gzip = result;
    console.log('Done. Size: %d KB.', result.length);

    // next, minify the source using only UglifyJS
    uglify(source, onUglify);
  }

  /**
   * The `uglify()` callback.
   *
   * @private
   * @param {Object|Undefined} exception The error object.
   * @param {String} result The resulting minified source.
   */
  function onUglify(exception, result) {
    if (exception) {
      throw exception;
    }
    // store the post-processed Uglified result and gzip it
    accumulator.uglified.source = result = postprocess(result);
    gzip(result, onUglifyGzip);
  }

  /**
   * The UglifyJS `gzip` callback.
   *
   * @private
   * @param {Object|Undefined} exception The error object.
   * @param {Buffer} result The resulting gzipped source.
   */
  function onUglifyGzip(exception, result) {
    if (exception) {
      throw exception;
    }
    var message = 'Compressing lodash.js combining Closure Compiler and UglifyJS...';

    // store the gzipped result and report the size
    accumulator.uglified.gzip = result;
    console.log('Done. Size: %d KB.', result.length);

    // next, minify the Closure Compiler minified source using UglifyJS
    uglify(accumulator.compiled.source, message, onCombine);
  }

  /**
   * The combined `uglify()` callback.
   *
   * @private
   * @param {Object|Undefined} exception The error object.
   * @param {String} result The resulting minified source.
   */
  function onCombine(exception, result) {
    if (exception) {
      throw exception;
    }
    // store the post-processed Uglified result and gzip it
    accumulator.combined.source = result = postprocess(result);
    gzip(result, onCombineGzip);
  }

  /**
   * The combined `gzip` callback.
   *
   * @private
   * @param {Object|Undefined} exception The error object.
   * @param {Buffer} result The resulting gzipped source.
   */
  function onCombineGzip(exception, result) {
    if (exception) {
      throw exception;
    }
    // store the gzipped result and report the size
    accumulator.combined.gzip = result;
    console.log('Done. Size: %d KB.', result.length);

    // finish by choosing the smallest compressed file
    onComplete();
  }

  /**
   * The callback executed after JavaScript source is minified and gzipped.
   *
   * @private
   */
  function onComplete() {
    var combined = accumulator.combined,
        compiled = accumulator.compiled,
        uglified = accumulator.uglified;

    // save the Closure Compiled version to disk
    fs.writeFileSync(path.join(distPath, 'lodash.compiler.js'), compiled.source);
    fs.writeFileSync(path.join(distPath, 'lodash.compiler.js.gz'), compiled.gzip);

    // save the Uglified version to disk
    fs.writeFileSync(path.join(distPath, 'lodash.uglify.js'), uglified.source);
    fs.writeFileSync(path.join(distPath, 'lodash.uglify.js.gz'), uglified.gzip);

    // save the Combined minified version to disk
    fs.writeFileSync(path.join(distPath, 'lodash.combined.js'), combined.source);
    fs.writeFileSync(path.join(distPath, 'lodash.combined.js.gz'), combined.gzip);

    // select the smallest gzipped file and use its minified counterpart as the
    // official minified release (ties go to Closure Compiler)
    var min = Math.min(combined.gzip.length, compiled.gzip.length, uglified.gzip.length);

    fs.writeFileSync(path.join(__dirname, 'lodash.min.js'),
      compiled.gzip.length == min
        ? compiled.source
        : uglified.gzip.length == min
          ? uglified.source
          : combined.source
    );
  }

  /*--------------------------------------------------------------------------*/

  // create the destination directory if it doesn't exist
  if (!path.existsSync(distPath)) {
    fs.mkdirSync(distPath);
  }
  // begin the minification process
  closureCompile(source, onClosureCompile);
}());
<commit_msg>Rename `combine` to `hybrid`.<commit_after>#!/usr/bin/env node
;(function() {
  'use strict';

  /** The Node filesystem, path, `zlib`, and child process modules */
  var fs = require('fs'),
      gzip = require('zlib').gzip,
      path = require('path'),
      spawn = require('child_process').spawn;

  /** The build directory containing the build scripts */
  var buildPath = path.join(__dirname, 'build');

  /** The directory where the Closure Compiler is located */
  var closurePath = path.join(__dirname, 'vendor', 'closure-compiler', 'compiler.jar');

  /** The distribution directory */
  var distPath = path.join(__dirname, 'dist');

  /** Load other modules */
  var preprocess = require(path.join(buildPath, 'pre-compile')),
      postprocess = require(path.join(buildPath, 'post-compile')),
      uglifyJS = require(path.join(__dirname, 'vendor', 'uglifyjs', 'uglify-js'));

  /** Used to shares values between multiple callbacks */
  var accumulator = {
    'compiled': {},
    'hybrid': {},
    'uglified': {}
  };

  /** Closure Compiler command-line options */
  var closureOptions = [
    '--compilation_level=ADVANCED_OPTIMIZATIONS',
    '--language_in=ECMASCRIPT5_STRICT',
    '--warning_level=QUIET'
  ];

  /** The pre-processed Lo-Dash source */
  var source = preprocess(fs.readFileSync(path.join(__dirname, 'lodash.js'), 'utf8'));

  /*--------------------------------------------------------------------------*/

  /**
   * Compresses a `source` string using the Closure Compiler. Yields the
   * minified result, and any exceptions encountered, to a `callback` function.
   *
   * @private
   * @param {String} source The JavaScript source to minify.
   * @param {String} [message] The message to log.
   * @param {Function} callback The function to call once the process completes.
   */
  function closureCompile(source, message, callback) {
    // the standard error stream, standard output stream, and Closure Compiler process
    var error = '',
        output = '',
        compiler = spawn('java', ['-jar', closurePath].concat(closureOptions));

    // juggle arguments
    if (typeof message == 'function') {
      callback = message;
      message = null;
    }

    console.log(message == null ? 'Compressing lodash.js using the Closure Compiler...' : message);

    compiler.stdout.on('data', function(data) {
      // append the data to the output stream
      output += data;
    });

    compiler.stderr.on('data', function(data) {
      // append the error message to the error stream
      error += data;
    });

    compiler.on('exit', function(status) {
      var exception = null;

      // `status` contains the process exit code
      if (status) {
        exception = new Error(error);
        exception.status = status;
      }
      callback(exception, output);
    });

    // proxy the standard input to the Closure Compiler
    compiler.stdin.end(source);
  }

  /**
   * Compresses a `source` string using UglifyJS. Yields the result to a
   * `callback` function. This function is synchronous; the `callback` is used
   * for symmetry.
   *
   * @private
   * @param {String} source The JavaScript source to minify.
   * @param {String} [message] The message to log.
   * @param {Function} callback The function to call once the process completes.
   */
  function uglify(source, message, callback) {
    var exception,
        result,
        ugly = uglifyJS.uglify;

    // juggle arguments
    if (typeof message == 'function') {
      callback = message;
      message = null;
    }

    console.log(message == null ? 'Compressing lodash.js using UglifyJS...' : message);

    try {
      result = ugly.gen_code(
        // enable unsafe transformations
        ugly.ast_squeeze_more(
          ugly.ast_squeeze(
            // munge variable and function names, excluding the special `define`
            // function exposed by AMD loaders
            ugly.ast_mangle(uglifyJS.parser.parse(source), {
              'except': ['define']
            }
        ))), {
        'ascii_only': true
      });
    } catch(e) {
      exception = e;
    }
    // lines are restricted to 500 characters for consistency with the Closure Compiler
    callback(exception, result && ugly.split_lines(result, 500));
  }

  /*--------------------------------------------------------------------------*/

  /**
   * The `closureCompile()` callback.
   *
   * @private
   * @param {Object|Undefined} exception The error object.
   * @param {String} result The resulting minified source.
   */
  function onClosureCompile(exception, result) {
    if (exception) {
      throw exception;
    }
    // store the post-processed Closure Compiler result and gzip it
    accumulator.compiled.source = result = postprocess(result);
    gzip(result, onClosureGzip);
  }

  /**
   * The Closure Compiler `gzip` callback.
   *
   * @private
   * @param {Object|Undefined} exception The error object.
   * @param {Buffer} result The resulting gzipped source.
   */
  function onClosureGzip(exception, result) {
    if (exception) {
      throw exception;
    }
    // store the gzipped result and report the size
    accumulator.compiled.gzip = result;
    console.log('Done. Size: %d KB.', result.length);

    // next, minify the source using only UglifyJS
    uglify(source, onUglify);
  }

  /**
   * The `uglify()` callback.
   *
   * @private
   * @param {Object|Undefined} exception The error object.
   * @param {String} result The resulting minified source.
   */
  function onUglify(exception, result) {
    if (exception) {
      throw exception;
    }
    // store the post-processed Uglified result and gzip it
    accumulator.uglified.source = result = postprocess(result);
    gzip(result, onUglifyGzip);
  }

  /**
   * The UglifyJS `gzip` callback.
   *
   * @private
   * @param {Object|Undefined} exception The error object.
   * @param {Buffer} result The resulting gzipped source.
   */
  function onUglifyGzip(exception, result) {
    if (exception) {
      throw exception;
    }
    var message = 'Compressing lodash.js combining Closure Compiler and UglifyJS...';

    // store the gzipped result and report the size
    accumulator.uglified.gzip = result;
    console.log('Done. Size: %d KB.', result.length);

    // next, minify the Closure Compiler minified source using UglifyJS
    uglify(accumulator.compiled.source, message, onHybrid);
  }

  /**
   * The hybrid `uglify()` callback.
   *
   * @private
   * @param {Object|Undefined} exception The error object.
   * @param {String} result The resulting minified source.
   */
  function onHybrid(exception, result) {
    if (exception) {
      throw exception;
    }
    // store the post-processed Uglified result and gzip it
    accumulator.hybrid.source = result = postprocess(result);
    gzip(result, onHybridGzip);
  }

  /**
   * The hybrid `gzip` callback.
   *
   * @private
   * @param {Object|Undefined} exception The error object.
   * @param {Buffer} result The resulting gzipped source.
   */
  function onHybridGzip(exception, result) {
    if (exception) {
      throw exception;
    }
    // store the gzipped result and report the size
    accumulator.hybrid.gzip = result;
    console.log('Done. Size: %d KB.', result.length);

    // finish by choosing the smallest compressed file
    onComplete();
  }

  /**
   * The callback executed after JavaScript source is minified and gzipped.
   *
   * @private
   */
  function onComplete() {
    var compiled = accumulator.compiled,
        hybrid = accumulator.hybrid,
        uglified = accumulator.uglified;

    // save the Closure Compiled version to disk
    fs.writeFileSync(path.join(distPath, 'lodash.compiler.js'), compiled.source);
    fs.writeFileSync(path.join(distPath, 'lodash.compiler.js.gz'), compiled.gzip);

    // save the Uglified version to disk
    fs.writeFileSync(path.join(distPath, 'lodash.uglify.js'), uglified.source);
    fs.writeFileSync(path.join(distPath, 'lodash.uglify.js.gz'), uglified.gzip);

    // save the hybrid minified version to disk
    fs.writeFileSync(path.join(distPath, 'lodash.hybrid.js'), hybrid.source);
    fs.writeFileSync(path.join(distPath, 'lodash.hybrid.js.gz'), hybrid.gzip);

    // select the smallest gzipped file and use its minified counterpart as the
    // official minified release (ties go to Closure Compiler)
    var min = Math.min(compiled.gzip.length, hybrid.gzip.length, uglified.gzip.length);

    fs.writeFileSync(path.join(__dirname, 'lodash.min.js'),
      compiled.gzip.length == min
        ? compiled.source
        : uglified.gzip.length == min
          ? uglified.source
          : hybrid.source
    );
  }

  /*--------------------------------------------------------------------------*/

  // create the destination directory if it doesn't exist
  if (!path.existsSync(distPath)) {
    fs.mkdirSync(distPath);
  }
  // begin the minification process
  closureCompile(source, onClosureCompile);
}());
<|endoftext|>


<commit_before>// @flow

import * as React from 'react'
import {StyleSheet, View, Text, Platform, RefreshControl} from 'react-native'
import {SearchableAlphabetListView} from '../components/searchable-alphabet-listview'
import type {TopLevelViewPropsType} from '../types'
import LoadingView from '../components/loading'
import delay from 'delay'
import {NoticeView} from '../components/notice'
import {Row, Column} from '../components/layout'
import {
	ListRow,
	ListSectionHeader,
	ListSeparator,
	Detail,
	Title,
} from '../components/list'
import {trackOrgOpen} from '../../analytics'
import {reportNetworkProblem} from '../../lib/report-network-problem'
import size from 'lodash/size'
import sortBy from 'lodash/sortBy'
import groupBy from 'lodash/groupBy'
import uniq from 'lodash/uniq'
import words from 'lodash/words'
import deburr from 'lodash/deburr'
import filter from 'lodash/filter'
import startCase from 'lodash/startCase'
import * as c from '../components/colors'
import type {StudentOrgType} from './types'
import {
	parseHtml,
	cssSelect,
	getTrimmedTextWithSpaces as getText,
} from '../../lib/html'

const orgsUrl = 'https://apps.carleton.edu/student/orgs/'
const leftSideSpacing = 20
const ROW_HEIGHT = Platform.OS === 'ios' ? 58 : 74
const SECTION_HEADER_HEIGHT = Platform.OS === 'ios' ? 33 : 41

const splitToArray = (str: string) => words(deburr(str.toLowerCase()))

const orgToArray = (term: StudentOrgType) =>
	uniq([
		...splitToArray(term.name),
		//...splitToArray(term.category),
		...splitToArray(term.description),
	])

const styles = StyleSheet.create({
	wrapper: {
		flex: 1,
	},
	row: {
		height: ROW_HEIGHT,
		paddingRight: 2,
	},
	rowDetailText: {
		fontSize: 14,
	},
	rowSectionHeader: {
		height: SECTION_HEADER_HEIGHT,
	},
	badgeContainer: {
		alignItems: 'center',
		justifyContent: 'center',
		alignSelf: 'flex-start',
		width: leftSideSpacing,
	},
	badge: {
		fontSize: Platform.OS === 'ios' ? 24 : 28,
		color: c.transparent,
	},
})

type Props = TopLevelViewPropsType

type State = {
	orgs: Array<StudentOrgType>,
	results: {[key: string]: StudentOrgType[]},
	refreshing: boolean,
	error: boolean,
	loading: boolean,
}

export class StudentOrgsView extends React.PureComponent<Props, State> {
	static navigationOptions = {
		title: 'Student Orgs',
		headerBackTitle: 'Orgs',
	}

	searchBar: any

	state = {
		orgs: [],
		results: {},
		refreshing: false,
		loading: true,
		error: false,
	}

	componentWillMount() {
		this.fetchData().then(() => {
			this.setState(() => ({loading: false}))
		})
	}

	fetchData = async () => {
		const page: StudentOrgType[] = await fetch(orgsUrl).then(r => r.text())
		const dom = parseHtml(page)

		const orgsList = cssSelect('#orgsList', dom)
		const allOrgs = cssSelect('.orgContainer', orgsList)

		let responseData = []
		allOrgs.map(org => {
			// todo:
			//  * cleanup this whole function
			//  * remove duplicate org entries from the scraping...
			//  * remove the 'manage' hyperlink from the org name
			//  * don't strip links from 'site'
			//  * plumb-in the detail view
			//  * get categories
			//  * replace description on rows with category
			//  * harvest social links (FB, Instagram, Twitter, etc)
			let name = getText(cssSelect('h4:first-of-type', org)).trim()
			let description = getText(
				cssSelect('div.orgBody > div.orgDescription', org),
			).trim()
			let contacts = getText(cssSelect('div.orgBody > p.contacts', org)).trim()
			let site = getText(cssSelect('div.orgBody > p.site', org)).trim()

			const group = {
				name: name,
				description: description,
				contacts: contacts,
				site: site,
			}
			responseData.push(group)
		})

		const sortableRegex = /^(Carleton(?: College)?|The) +/i
		const withSortableNames = responseData.map(item => {
			const sortableName = item.name.replace(sortableRegex, '')

			return {
				...item,
				$sortableName: sortableName,
				$groupableName: startCase(sortableName)[0],
			}
		})

		const sorted = sortBy(withSortableNames, '$sortableName')
		const grouped = groupBy(sorted, '$groupableName')
		this.setState(() => ({orgs: sorted, results: grouped}))
	}

	refresh = async () => {
		const start = Date.now()
		this.setState(() => ({refreshing: true}))

		await this.fetchData()

		// wait 0.5 seconds – if we let it go at normal speed, it feels broken.
		const elapsed = Date.now() - start
		if (elapsed < 500) {
			await delay(500 - elapsed)
		}
		this.setState(() => ({refreshing: false}))
	}

	renderSectionHeader = ({title}: {title: string}) => (
		<ListSectionHeader
			spacing={{left: leftSideSpacing}}
			style={styles.rowSectionHeader}
			title={title}
		/>
	)

	renderRow = ({item}: {item: StudentOrgType}) => (
		<ListRow
			arrowPosition="none"
			contentContainerStyle={[styles.row]}
			fullWidth={true}
			onPress={() => this.onPressRow(item)}
		>
			<Row alignItems="flex-start">
				<View style={styles.badgeContainer}>
					<Text style={styles.badge}>•</Text>
				</View>

				<Column>
					<Title lines={1}>{item.name}</Title>
					<Detail lines={1} style={styles.rowDetailText}>
						{item.description}
					</Detail>
				</Column>
			</Row>
		</ListRow>
	)

	renderSeparator = (sectionId: string, rowId: string) => (
		<ListSeparator
			key={`${sectionId}-${rowId}`}
			spacing={{left: leftSideSpacing}}
		/>
	)

	onPressRow = (data: StudentOrgType) => {
		trackOrgOpen(data.name)
		this.props.navigation.navigate('StudentOrgsDetailView', {org: data})
	}

	performSearch = (text: ?string) => {
		if (!text) {
			this.setState(state => ({
				results: groupBy(state.orgs, '$groupableName'),
			}))
			return
		}

		const query = text.toLowerCase()
		this.setState(state => {
			const filteredResults = filter(state.orgs, org =>
				orgToArray(org).some(word => word.startsWith(query)),
			)
			return {results: groupBy(filteredResults, '$groupableName')}
		})
	}

	render() {
		if (this.state.loading) {
			return <LoadingView />
		}

		if (!size(this.state.orgs)) {
			return <NoticeView text="No organizations found." />
		}

		const refreshControl = (
			<RefreshControl
				onRefresh={this.refresh}
				refreshing={this.state.refreshing}
			/>
		)

		return (
			<SearchableAlphabetListView
				cell={this.renderRow}
				cellHeight={
					ROW_HEIGHT +
					(Platform.OS === 'ios' ? 11 / 12 * StyleSheet.hairlineWidth : 0)
				}
				data={this.state.results}
				onSearch={this.performSearch}
				refreshControl={refreshControl}
				renderSeparator={this.renderSeparator}
				sectionHeader={this.renderSectionHeader}
				sectionHeaderHeight={SECTION_HEADER_HEIGHT}
				style={styles.wrapper}
			/>
		)
	}
}
<commit_msg>Changes according to review<commit_after>// @flow

import * as React from 'react'
import {StyleSheet, View, Text, Platform, RefreshControl} from 'react-native'
import {SearchableAlphabetListView} from '../components/searchable-alphabet-listview'
import type {TopLevelViewPropsType} from '../types'
import LoadingView from '../components/loading'
import delay from 'delay'
import {NoticeView} from '../components/notice'
import {Row, Column} from '../components/layout'
import {
	ListRow,
	ListSectionHeader,
	ListSeparator,
	Detail,
	Title,
} from '../components/list'
import {trackOrgOpen} from '../../analytics'
import {reportNetworkProblem} from '../../lib/report-network-problem'
import size from 'lodash/size'
import sortBy from 'lodash/sortBy'
import groupBy from 'lodash/groupBy'
import uniq from 'lodash/uniq'
import words from 'lodash/words'
import deburr from 'lodash/deburr'
import filter from 'lodash/filter'
import startCase from 'lodash/startCase'
import * as c from '../components/colors'
import type {StudentOrgType} from './types'
import {
	parseHtml,
	cssSelect,
	getTrimmedTextWithSpaces as getText,
} from '../../lib/html'

const orgsUrl = 'https://apps.carleton.edu/student/orgs/'
const leftSideSpacing = 20
const ROW_HEIGHT = Platform.OS === 'ios' ? 58 : 74
const SECTION_HEADER_HEIGHT = Platform.OS === 'ios' ? 33 : 41

const splitToArray = (str: string) => words(deburr(str.toLowerCase()))

const orgToArray = (term: StudentOrgType) =>
	uniq([
		...splitToArray(term.name),
		//...splitToArray(term.category),
		...splitToArray(term.description),
	])

const styles = StyleSheet.create({
	wrapper: {
		flex: 1,
	},
	row: {
		height: ROW_HEIGHT,
		paddingRight: 2,
	},
	rowDetailText: {
		fontSize: 14,
	},
	rowSectionHeader: {
		height: SECTION_HEADER_HEIGHT,
	},
	badgeContainer: {
		alignItems: 'center',
		justifyContent: 'center',
		alignSelf: 'flex-start',
		width: leftSideSpacing,
	},
	badge: {
		fontSize: Platform.OS === 'ios' ? 24 : 28,
		color: c.transparent,
	},
})

type Props = TopLevelViewPropsType

type State = {
	orgs: Array<StudentOrgType>,
	results: {[key: string]: StudentOrgType[]},
	refreshing: boolean,
	error: boolean,
	loading: boolean,
}

export class StudentOrgsView extends React.PureComponent<Props, State> {
	static navigationOptions = {
		title: 'Student Orgs',
		headerBackTitle: 'Orgs',
	}

	searchBar: any

	state = {
		orgs: [],
		results: {},
		refreshing: false,
		loading: true,
		error: false,
	}

	componentWillMount() {
		this.fetchData().then(() => {
			this.setState(() => ({loading: false}))
		})
	}

	fetchData = async () => {
		const page: StudentOrgType[] = await fetch(orgsUrl).then(r => r.text())
		const dom = parseHtml(page)

		const orgsList = cssSelect('#orgsList', dom)
		const allOrgs = cssSelect('.orgContainer', orgsList)

		let responseData = []
		allOrgs.forEach(org => {
			// todo:
			//  * cleanup this whole function
			//  * remove duplicate org entries from the scraping...
			//  * remove the 'manage' hyperlink from the org name
			//  * don't strip links from 'site'
			//  * plumb-in the detail view
			//  * get categories
			//  * replace description on rows with category
			//  * harvest social links (FB, Instagram, Twitter, etc)
			let name = getText(cssSelect('h4', org)).trim()
			let description = getText(
				cssSelect('.orgDescription', org),
			).trim()
			let contacts = getText(cssSelect('.contacts', org)).trim()
			let site = getText(cssSelect('.site', org)).trim()

			const group = {
				name: name,
				description: description,
				contacts: contacts,
				site: site,
			}
			responseData.push(group)
		})

		const sortableRegex = /^(Carleton(?: College)?|The) +/i
		const withSortableNames = responseData.map(item => {
			const sortableName = item.name.replace(sortableRegex, '')

			return {
				...item,
				$sortableName: sortableName,
				$groupableName: startCase(sortableName)[0],
			}
		})

		const sorted = sortBy(withSortableNames, '$sortableName')
		const grouped = groupBy(sorted, '$groupableName')
		this.setState(() => ({orgs: sorted, results: grouped}))
	}

	refresh = async () => {
		const start = Date.now()
		this.setState(() => ({refreshing: true}))

		await this.fetchData()

		// wait 0.5 seconds – if we let it go at normal speed, it feels broken.
		const elapsed = Date.now() - start
		if (elapsed < 500) {
			await delay(500 - elapsed)
		}
		this.setState(() => ({refreshing: false}))
	}

	renderSectionHeader = ({title}: {title: string}) => (
		<ListSectionHeader
			spacing={{left: leftSideSpacing}}
			style={styles.rowSectionHeader}
			title={title}
		/>
	)

	renderRow = ({item}: {item: StudentOrgType}) => (
		<ListRow
			arrowPosition="none"
			contentContainerStyle={[styles.row]}
			fullWidth={true}
			onPress={() => this.onPressRow(item)}
		>
			<Row alignItems="flex-start">
				<View style={styles.badgeContainer}>
					<Text style={styles.badge}>•</Text>
				</View>

				<Column>
					<Title lines={1}>{item.name}</Title>
					<Detail lines={1} style={styles.rowDetailText}>
						{item.description}
					</Detail>
				</Column>
			</Row>
		</ListRow>
	)

	renderSeparator = (sectionId: string, rowId: string) => (
		<ListSeparator
			key={`${sectionId}-${rowId}`}
			spacing={{left: leftSideSpacing}}
		/>
	)

	onPressRow = (data: StudentOrgType) => {
		trackOrgOpen(data.name)
		this.props.navigation.navigate('StudentOrgsDetailView', {org: data})
	}

	performSearch = (text: ?string) => {
		if (!text) {
			this.setState(state => ({
				results: groupBy(state.orgs, '$groupableName'),
			}))
			return
		}

		const query = text.toLowerCase()
		this.setState(state => {
			const filteredResults = filter(state.orgs, org =>
				orgToArray(org).some(word => word.startsWith(query)),
			)
			return {results: groupBy(filteredResults, '$groupableName')}
		})
	}

	render() {
		if (this.state.loading) {
			return <LoadingView />
		}

		if (!size(this.state.orgs)) {
			return <NoticeView text="No organizations found." />
		}

		const refreshControl = (
			<RefreshControl
				onRefresh={this.refresh}
				refreshing={this.state.refreshing}
			/>
		)

		return (
			<SearchableAlphabetListView
				cell={this.renderRow}
				cellHeight={
					ROW_HEIGHT +
					(Platform.OS === 'ios' ? 11 / 12 * StyleSheet.hairlineWidth : 0)
				}
				data={this.state.results}
				onSearch={this.performSearch}
				refreshControl={refreshControl}
				renderSeparator={this.renderSeparator}
				sectionHeader={this.renderSectionHeader}
				sectionHeaderHeight={SECTION_HEADER_HEIGHT}
				style={styles.wrapper}
			/>
		)
	}
}
<|endoftext|>


<commit_before>console.log("Starting ficlet.");

var urlSplit = document.URL.split('/');
urlSplit.length = 5;
if ((urlSplit[2] != "www.fanfiction.net" && urlSplit[2] != "m.fanfiction.net") && (urlSplit[2] != "www.fictionpress.com" && urlSplit[2] != "m.fictionpress.com") || urlSplit[3] != "s") {
    alert("Please run ficlet on a story on fanfiction.net or fictionpress.com");
    throw("Incompatible webpage.");
}
var baseUrl = urlSplit.join('/') + '/';

var mobile = (urlSplit[2] == "m.fanfiction.net" || urlSplit[2] == "m.fictionpress.com") ? true : false;

var documentTitle = document.title;

if (mobile) {
    if (document.getElementById("jump") != null) {
        var numChapters = document.getElementById("jump").options.length - 1;
    }
    else if (isNaN(parseInt(document.getElementById("top").getElementsByTagName("center")[2].getElementsByTagName("a")[0].innerHTML))) {
        var numChapters = 1;
    }
    else var numChapters = 2;
    var titleStr = document.getElementById("content").getElementsByTagName("b")[0].innerHTML;
    var authorStr = document.getElementById("content").getElementsByTagName("a")[0].text;
}
else {
    if (document.getElementById("chap_select") == null) {
        var numChapters = 1;
    }
    else {
        var numChapters = document.getElementById("chap_select").options.length;
    }
    var titleStr = document.getElementsByClassName("xcontrast_txt")[4].innerHTML;
    var authorStr = document.getElementsByClassName("xcontrast_txt")[6].innerHTML;
}

var chapterIdx = 1;
var chapterText = new Array();
var chapterHTML = new Array();
var chapterTitle = new Array();

var downloadFrame = document.createElement('iframe');
downloadFrame.width = 0;
downloadFrame.height = 0;
downloadFrame.style.visibility="hidden";
document.body.appendChild(downloadFrame);
downloadFrame.onload = function() {
    if (mobile) {
        chapterText[chapterIdx] = downloadFrame.contentDocument.getElementById("storycontent").innerHTML;
        if (numChapters == 1) {
            chapterTitle[chapterIdx] = titleStr;
        }
        else {
            chapterTitle[chapterIdx] = downloadFrame.contentDocument.getElementById("content").innerHTML;
            chapterTitle[chapterIdx] = chapterTitle[chapterIdx].substring(chapterTitle[chapterIdx].indexOf("") + 7);
            chapterTitle[chapterIdx] = chapterTitle[chapterIdx].substring(1, chapterTitle[chapterIdx].indexOf("
"));
            chapterTitle[chapterIdx] = String(chapterIdx) + ". " + chapterTitle[chapterIdx];
        }
    }
    else {
        chapterText[chapterIdx] = downloadFrame.contentDocument.getElementById("storytext").innerHTML;
        if (numChapters == 1) chapterTitle[chapterIdx] = titleStr;
        else chapterTitle[chapterIdx] = document.getElementById("chap_select").options.item(chapterIdx - 1).innerHTML;
    }
    chapterHTML[chapterIdx] = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8"/><link rel="stylesheet" type="text/css" href="../style.css"/><title>' + chapterTitle[chapterIdx] + '</title>\n
' + chapterTitle[chapterIdx] + '

' + chapterText[chapterIdx] + '
';
    console.log("Downloaded chapter " + String(chapterIdx) + "/" + String(numChapters) + ".");
    document.title = '[Downloading][' + String(chapterIdx) + "/" + String(numChapters) + ']' + documentTitle;
    //console.log(chapterHTML[chapterIdx]);
    if (chapterIdx < numChapters) {
        chapterIdx++;
        downloadFrame.src = baseUrl + String(chapterIdx);
    }
    else {
        document.title = "[Compressing]" + documentTitle;
        makeEpub();
    }

}
downloadFrame.src = baseUrl + '1';

function makeEpub() {
console.log("making epub");
var zipWriter;
var zippedBlob;

async.series([
    function(callback) {
        zip.createWriter(new zip.BlobWriter("application/zip"), function(writer) {
            zipWriter = writer;
            callback();
        });
    },
    function(callback) {
        zipWriter.add("mimetype", new zip.TextReader("application/epub+zip"), function(writer) {
            callback();
        });
    },
    function(callback) {
        zipWriter.add("META-INF", null, function(writer) {
            callback();
        }, null, {directory:true});
    },
    function(callback) {
        zipWriter.add("META-INF/container.xml", new zip.TextReader('<?xml version="1.0" encoding="UTF-8" ?>\n<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">\n <rootfiles>\n <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml" />\n </rootfiles>\n</container>'),
        function(writer) {
            callback();
        });
    },
    function(callback) {
        zipWriter.add("OEBPS", null, function(writer) {
            callback();
        }, null, {directory:true});
    },
    function(callback) {
        zipWriter.add("OEBPS/style.css", new zip.TextReader('.copyrightpage {\n margin: 12%;\n}\n.copyrightpage, .copyrightpage p {\n font-size: 8pt;\n text-align: center;\n text-indent: 0;\n}\nh1 {\n font-family: sans serif;\n font-size: 1.25em;\n line-height: 18pt;\n margin: 5% 7% 5% 7%;\n text-align: center;\n}\n.chapterText p {\n clear: both;\n margin: 0;\n margin-bottom: 0.65em;\n text-align: justify;\n text-indent: 1.2em;\n}\n.chapterText p.firstParagraph {\n clear: none;\n text-indent: -1.0em;\n}\n.chapterText p.dropcap {\n border-bottom: 1px solid #000;\n display: block;\n float: left;\n font-size: 3.6em;\n font-weight: bold;\n line-height: 0.80em;\n height: 0.8em;\n margin: 0;\n margin-right: 0.3em;\n margin-top: 0.09em;\n text-indent: 0;\n}\nul.toc, ul.toc li {\n list-style: none;\n margin: 0;\n}\nhr {\n background-color: #000;\n border: none;\n height: 1px;\n margin-left: auto;\n margin-right: auto;\n width: 50%;\n}'),
        function(writer) {
            callback();
        });
    },
    function(callback) {
        var contentStr = '<?xml version="1.0" encoding="UTF-8"?>\n<package xmlns="http://www.idpf.org/2007/opf" version="2.0" unique-identifier="storySourceURI">\n <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n <dc:contributor opf:role="bkp">Ficlet</dc:contributor>\n <meta name="cover" content="cover"/>\n <dc:identifier opf:scheme="URI" id="storySourceURI">' + document.URL + '</dc:identifier>\n <dc:creator>' + authorStr + '</dc:creator>\n <dc:title>' + titleStr + '</dc:title>\n </metadata>\n <manifest>\n <item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>\n <item id="style_main" href="style.css" media-type="text/css"/>\n <item id="cover_p" href="content/cover.html" media-type="application/xhtml+xml"/>\n <item id="cover" href="images/cover.jpg" media-type="image/jpeg"/>';
        for (var i = 1; i <= numChapters; i++) {
            contentStr += '\n <item id="chapter' + String(i) + '" href="content/chapter' + String(i) + '.html" media-type="application/xhtml+xml"/>';
        }
        contentStr += ' </manifest>\n <spine toc="ncx">\n <itemref idref="cover_p"/>';
        for (var i = 1; i <= numChapters; i++) {
            contentStr += '\n <itemref idref="chapter' + String(i) + '"/>';
        }
        contentStr += ' </spine>\n <guide>\n <reference href="content/cover.html" type="cover" title="Cover"/>\n </guide>\n</package>';
        zipWriter.add("OEBPS/content.opf", new zip.TextReader(contentStr),
        function(writer) {
            callback();
        });
    },
    function(callback) {
        var tocStr = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">\n<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="en-US">\n <head\n <meta name="dtb:PrimaryID" content=""/>\n \n <docTitle>\n <text>' + titleStr + '</text>\n </docTitle>\n <docAuthor>\n <text>' + authorStr + '</text>\n </docAuthor>\n <navMap><navPoint id="cover" playOrder="1"><navLabel><text>Title Page</text></navLabel><content src="content/cover.html"/></navPoint>';
        for (var i = 1; i <= numChapters; i++) {
            tocStr += '\n <navPoint id="chapter' + String(i) + '" playOrder="' + String(i + 1) + '">\n         <navLabel>\n         <text>' + chapterTitle[i] + '</text>\n         </navLabel>\n         <content src="content/part_' + String(i) + '.html"/>\n        </navPoint>';
        }
        tocStr += ' </navMap>\n</ncx>';
        zipWriter.add("OEBPS/toc.ncx", new zip.TextReader(tocStr),
        function(writer) {
            callback();
        });
    },
    function(callback) {
        zipWriter.add("OEBPS/images", null, function(writer) {
            callback();
        }, null, {directory:true});
    },
    /*function(callback) {
zipWriter.add("OEBPS/images/cover.jpg", new zip.HttpReader('http://localhost/js/trollface.jpg'),
function(writer) {
callback();
});
},*/
    function(callback) {
        zipWriter.add("OEBPS/content", null, function(writer) {
            callback();
        }, null, {directory:true});
    },
    function(callback) {
        zipWriter.add("OEBPS/content/cover.html", new zip.TextReader('\n \n \n \n \n \n \n'),
        function(writer) {
            callback();
        });
    },
    function(callback) {
        chapterIdx = 1;
        async.whilst(
            function() { return chapterIdx <= numChapters; },
            function (callback) {
                zipWriter.add("OEBPS/content/chapter" + String(chapterIdx) + ".html", new zip.TextReader(chapterHTML[chapterIdx]),
                function(writer) {
                    console.log("Added chapter " + String(chapterIdx) + "/" + String(numChapters) + " to archive.");
                    document.title = '[Compressing][' + String(chapterIdx) + "/" + String(numChapters) + ']' + documentTitle;
                    chapterIdx++;
                    callback();
                });
            },
            function() {callback();}
        );
    },
    function(callback) {
        zipWriter.close(function(blob) {
            zippedBlob = blob;
            callback();
        });
    },
    function(callback) {
        saveAs(zippedBlob, authorStr + ' - ' + titleStr + '.epub');
        document.title = "[Finished]" + documentTitle;
        downloadFrame.parentNode.removeChild(downloadFrame);
        callback();
    },
]);
};
<commit_msg>Trying to fix display of
 in some readers.<commit_after>console.log("Starting ficlet.");

var urlSplit = document.URL.split('/');
urlSplit.length = 5;
if ((urlSplit[2] != "www.fanfiction.net" && urlSplit[2] != "m.fanfiction.net") && (urlSplit[2] != "www.fictionpress.com" && urlSplit[2] != "m.fictionpress.com") || urlSplit[3] != "s") {
    alert("Please run ficlet on a story on fanfiction.net or fictionpress.com");
    throw("Incompatible webpage.");
}
var baseUrl = urlSplit.join('/') + '/';

var mobile = (urlSplit[2] == "m.fanfiction.net" || urlSplit[2] == "m.fictionpress.com") ? true : false;

var documentTitle = document.title;

if (mobile) {
    if (document.getElementById("jump") != null) {
        var numChapters = document.getElementById("jump").options.length - 1;
    }
    else if (isNaN(parseInt(document.getElementById("top").getElementsByTagName("center")[2].getElementsByTagName("a")[0].innerHTML))) {
        var numChapters = 1;
    }
    else var numChapters = 2;
    var titleStr = document.getElementById("content").getElementsByTagName("b")[0].innerHTML;
    var authorStr = document.getElementById("content").getElementsByTagName("a")[0].text;
}
else {
    if (document.getElementById("chap_select") == null) {
        var numChapters = 1;
    }
    else {
        var numChapters = document.getElementById("chap_select").options.length;
    }
    var titleStr = document.getElementsByClassName("xcontrast_txt")[4].innerHTML;
    var authorStr = document.getElementsByClassName("xcontrast_txt")[6].innerHTML;
}

var chapterIdx = 1;
var chapterText = new Array();
var chapterHTML = new Array();
var chapterTitle = new Array();

var downloadFrame = document.createElement('iframe');
downloadFrame.width = 0;
downloadFrame.height = 0;
downloadFrame.style.visibility="hidden";
document.body.appendChild(downloadFrame);
downloadFrame.onload = function() {
    if (mobile) {
        chapterText[chapterIdx] = downloadFrame.contentDocument.getElementById("storycontent").innerHTML;
        if (numChapters == 1) {
            chapterTitle[chapterIdx] = titleStr;
        }
        else {
            chapterTitle[chapterIdx] = downloadFrame.contentDocument.getElementById("content").innerHTML;
            chapterTitle[chapterIdx] = chapterTitle[chapterIdx].substring(chapterTitle[chapterIdx].indexOf("") + 7);
            chapterTitle[chapterIdx] = chapterTitle[chapterIdx].substring(1, chapterTitle[chapterIdx].indexOf("
"));
            chapterTitle[chapterIdx] = String(chapterIdx) + ". " + chapterTitle[chapterIdx];
        }
    }
    else {
        chapterText[chapterIdx] = downloadFrame.contentDocument.getElementById("storytext").innerHTML;
        if (numChapters == 1) chapterTitle[chapterIdx] = titleStr;
        else chapterTitle[chapterIdx] = document.getElementById("chap_select").options.item(chapterIdx - 1).innerHTML;
    }
    chapterText[chapterIdx] = chapterText[chapterIdx].replace(/\
/g, "
");
    chapterHTML[chapterIdx] = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8"/><link rel="stylesheet" type="text/css" href="../style.css"/><title>' + chapterTitle[chapterIdx] + '</title>\n
' + chapterTitle[chapterIdx] + '

' + chapterText[chapterIdx] + '
';
    console.log("Downloaded chapter " + String(chapterIdx) + "/" + String(numChapters) + ".");
    document.title = '[Downloading][' + String(chapterIdx) + "/" + String(numChapters) + ']' + documentTitle;
    //console.log(chapterHTML[chapterIdx]);
    if (chapterIdx < numChapters) {
        chapterIdx++;
        downloadFrame.src = baseUrl + String(chapterIdx);
    }
    else {
        document.title = "[Compressing]" + documentTitle;
        makeEpub();
    }

}
downloadFrame.src = baseUrl + '1';

function makeEpub() {
console.log("making epub");
var zipWriter;
var zippedBlob;

async.series([
    function(callback) {
        zip.createWriter(new zip.BlobWriter("application/zip"), function(writer) {
            zipWriter = writer;
            callback();
        });
    },
    function(callback) {
        zipWriter.add("mimetype", new zip.TextReader("application/epub+zip"), function(writer) {
            callback();
        });
    },
    function(callback) {
        zipWriter.add("META-INF", null, function(writer) {
            callback();
        }, null, {directory:true});
    },
    function(callback) {
        zipWriter.add("META-INF/container.xml", new zip.TextReader('<?xml version="1.0" encoding="UTF-8" ?>\n<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">\n <rootfiles>\n <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml" />\n </rootfiles>\n</container>'),
        function(writer) {
            callback();
        });
    },
    function(callback) {
        zipWriter.add("OEBPS", null, function(writer) {
            callback();
        }, null, {directory:true});
    },
    function(callback) {
        zipWriter.add("OEBPS/style.css", new zip.TextReader('.copyrightpage {\n margin: 12%;\n}\n.copyrightpage, .copyrightpage p {\n font-size: 8pt;\n text-align: center;\n text-indent: 0;\n}\nh1 {\n font-family: sans serif;\n font-size: 1.25em;\n line-height: 18pt;\n margin: 5% 7% 5% 7%;\n text-align: center;\n}\n.chapterText p {\n clear: both;\n margin: 0;\n margin-bottom: 0.65em;\n text-align: justify;\n text-indent: 1.2em;\n}\n.chapterText p.firstParagraph {\n clear: none;\n text-indent: -1.0em;\n}\n.chapterText p.dropcap {\n border-bottom: 1px solid #000;\n display: block;\n float: left;\n font-size: 3.6em;\n font-weight: bold;\n line-height: 0.80em;\n height: 0.8em;\n margin: 0;\n margin-right: 0.3em;\n margin-top: 0.09em;\n text-indent: 0;\n}\nul.toc, ul.toc li {\n list-style: none;\n margin: 0;\n}\nhr {\n background-color: #000;\n border: none;\n height: 1px;\n margin-left: auto;\n margin-right: auto;\n width: 50%;\n}'),
        function(writer) {
            callback();
        });
    },
    function(callback) {
        var contentStr = '<?xml version="1.0" encoding="UTF-8"?>\n<package xmlns="http://www.idpf.org/2007/opf" version="2.0" unique-identifier="storySourceURI">\n <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n <dc:contributor opf:role="bkp">Ficlet</dc:contributor>\n <meta name="cover" content="cover"/>\n <dc:identifier opf:scheme="URI" id="storySourceURI">' + document.URL + '</dc:identifier>\n <dc:creator>' + authorStr + '</dc:creator>\n <dc:title>' + titleStr + '</dc:title>\n </metadata>\n <manifest>\n <item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>\n <item id="style_main" href="style.css" media-type="text/css"/>\n <item id="cover_p" href="content/cover.html" media-type="application/xhtml+xml"/>\n <item id="cover" href="images/cover.jpg" media-type="image/jpeg"/>';
        for (var i = 1; i <= numChapters; i++) {
            contentStr += '\n <item id="chapter' + String(i) + '" href="content/chapter' + String(i) + '.html" media-type="application/xhtml+xml"/>';
        }
        contentStr += ' </manifest>\n <spine toc="ncx">\n <itemref idref="cover_p"/>';
        for (var i = 1; i <= numChapters; i++) {
            contentStr += '\n <itemref idref="chapter' + String(i) + '"/>';
        }
        contentStr += ' </spine>\n <guide>\n <reference href="content/cover.html" type="cover" title="Cover"/>\n </guide>\n</package>';
        zipWriter.add("OEBPS/content.opf", new zip.TextReader(contentStr),
        function(writer) {
            callback();
        });
    },
    function(callback) {
        var tocStr = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">\n<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="en-US">\n <head\n <meta name="dtb:PrimaryID" content=""/>\n \n <docTitle>\n <text>' + titleStr + '</text>\n </docTitle>\n <docAuthor>\n <text>' + authorStr + '</text>\n </docAuthor>\n <navMap><navPoint id="cover" playOrder="1"><navLabel><text>Title Page</text></navLabel><content src="content/cover.html"/></navPoint>';
        for (var i = 1; i <= numChapters; i++) {
            tocStr += '\n <navPoint id="chapter' + String(i) + '" playOrder="' + String(i + 1) + '">\n         <navLabel>\n         <text>' + chapterTitle[i] + '</text>\n         </navLabel>\n         <content src="content/part_' + String(i) + '.html"/>\n        </navPoint>';
        }
        tocStr += ' </navMap>\n</ncx>';
        zipWriter.add("OEBPS/toc.ncx", new zip.TextReader(tocStr),
        function(writer) {
            callback();
        });
    },
    function(callback) {
        zipWriter.add("OEBPS/images", null, function(writer) {
            callback();
        }, null, {directory:true});
    },
    /*function(callback) {
zipWriter.add("OEBPS/images/cover.jpg", new zip.HttpReader('http://localhost/js/trollface.jpg'),
function(writer) {
callback();
});
},*/
    function(callback) {
        zipWriter.add("OEBPS/content", null, function(writer) {
            callback();
        }, null, {directory:true});
    },
    function(callback) {
        zipWriter.add("OEBPS/content/cover.html", new zip.TextReader('\n \n \n \n \n \n \n'),
        function(writer) {
            callback();
        });
    },
    function(callback) {
        chapterIdx = 1;
        async.whilst(
            function() { return chapterIdx <= numChapters; },
            function (callback) {
                zipWriter.add("OEBPS/content/chapter" + String(chapterIdx) + ".html", new zip.TextReader(chapterHTML[chapterIdx]),
                function(writer) {
                    console.log("Added chapter " + String(chapterIdx) + "/" + String(numChapters) + " to archive.");
                    document.title = '[Compressing][' + String(chapterIdx) + "/" + String(numChapters) + ']' + documentTitle;
                    chapterIdx++;
                    callback();
                });
            },
            function() {callback();}
        );
    },
    function(callback) {
        zipWriter.close(function(blob) {
            zippedBlob = blob;
            callback();
        });
    },
    function(callback) {
        saveAs(zippedBlob, authorStr + ' - ' + titleStr + '.epub');
        document.title = "[Finished]" + documentTitle;
        downloadFrame.parentNode.removeChild(downloadFrame);
        callback();
    },
]);
};
<|endoftext|>


<commit_before>/* connectedvoice FE route handler */

"use strict";

import Request from 'request-promise';

const voice_server_url = 'http://localhost:8000';
const sms_server_url = 'http://localhost:8001';

export async function voiceCallHandler(req, reply, next) {
	try {
		let body;
		if (req != undefined) {
			body = await Request({
				url: `${voice_server_url}${req.url}`,
				method: 'POST',
				json: true,
				body: req.params
			});
		} else throw new Error('Missing request parameters')
		if (body === undefined) reply.send(200);
		else {
			//may need to: unescape(body)
			reply.send(200, body, {'content-type': 'application/xml'});
			reply.end();
			return next();
		}
	} catch(e) {
		console.log('voiceCallHandler Error ', e.message);
		reply.send(402, 'An error occured', {'content-type': 'application/xml'});
		reply.end();
		return;
	}
}

export async function smsCallHandler(req, reply, next) {
	try {
		let body;
		if (req != undefined) {
			body = await Request({
				url: `${sms_server_url}${req.url}`,
				method: 'POST',
				json: true,
				body: req.params
			});
		} else throw new Error('Missing request parameters')
		if (body === undefined) reply.send(200);
		else reply.send(200, body, {'content-type': 'application/xml'});
		reply.end();
		return next();
	} catch(e) {
		console.log('smsCallHandler Error ', e);
		reply.send(402, 'An error occured', {'content-type': 'application/xml'});
		reply.end();
		return;
	}
}
<commit_msg>minor tweak<commit_after>/* connectedvoice FE route handler */

"use strict";

import Request from 'request-promise';

const voice_server_url = 'http://localhost:8000';
const sms_server_url = 'http://localhost:8001';

export async function voiceCallHandler(req, reply, next) {
	try {
		let body;
		if (req != undefined) {
			body = await Request({
				url: `${voice_server_url}${req.url}`,
				method: 'POST',
				json: true,
				body: req.params
			});
		} else throw new Error('Missing request parameters')
		if (body === undefined) reply.send(200);
		else reply.send(200, body, {'content-type': 'application/xml'});  //may need to: unescape(body)
		reply.end();
		return next();
	} catch(e) {
		console.log('voiceCallHandler Error ', e.message);
		reply.send(402, 'An error occured', {'content-type': 'application/xml'});
		reply.end();
		return;
	}
}

export async function smsCallHandler(req, reply, next) {
	try {
		let body;
		if (req != undefined) {
			body = await Request({
				url: `${sms_server_url}${req.url}`,
				method: 'POST',
				json: true,
				body: req.params
			});
		} else throw new Error('Missing request parameters')
		if (body === undefined) reply.send(200);
		else reply.send(200, body, {'content-type': 'application/xml'});
		reply.end();
		return next();
	} catch(e) {
		console.log('smsCallHandler Error ', e);
		reply.send(402, 'An error occured', {'content-type': 'application/xml'});
		reply.end();
		return;
	}
}
<|endoftext|>


<commit_before>// Global variable to hold the current record
var rec;

var VcfUtils = Java.type("com.rtg.vcf.VcfUtils");
var FilterField = Java.type("com.rtg.vcf.header.FilterField");
var InfoField = Java.type("com.rtg.vcf.header.InfoField");
var FormatField = Java.type("com.rtg.vcf.header.FormatField");

(function (global) {
    function listToArray(list) {
        if (list == null) {
            return null;
        }
        var arr = [];
        for (var i = 0; i < list.size(); i++) {
            arr.push(list.get(i));
        }
        return arr;
    }

    // First we need to know the sample names
    var sampleNames = listToArray(RTG_VCF_HEADER.getSampleNames());
    var sampleLookup = {};
    sampleNames.forEach(function (name, index) {
        sampleLookup[name] = index;
    });

    /** List of format IDs */
    var formatIds = listToArray(RTG_VCF_HEADER.getFormatLines()).map(function (field) {
        return field.getId()
    });

    /**
     * String prototype function for fetching a format field from a sample
     * @param field format field
     * @returns {Function} that extracts a format field
     */
    function stringGetFunction(field) {
        return function () {
            var index = sampleLookup[this];
            var fields = RTG_VCF_RECORD.getFormatAndSample().get(field);
            return fields == null ? "." : fields.get(index);
        }
    }

    /**
     * String prototype function for setting a format field in a sample
     * @param field format field
     * @returns {Function} that sets a format field
     */
    function stringSetFunction(field) {
        return function (value) {
            var index = sampleLookup[this];
            RTG_VCF_RECORD.setFormatAndSample(field, value.toString(), index);
        }
    }

    /**
     * Add get/set to string prototype to allow treating them as sample references
     */
    var stringProps = {};
    formatIds.forEach(function (field) {
        stringProps[field] = {get: stringGetFunction(field), set: stringSetFunction(field)};
    });
    // Note that Object.defineProperty doesn't seem to work here... bug defineProperties does
    // No idea why.
    Object.defineProperties(String.prototype, stringProps);

    /**
     * Returns a function which will fetch the info field with the given name from the current record
     */
    function infoGet(field) {
        return function () {
            var infoList = listToArray(RTG_VCF_RECORD.getInfo().get(field));
            if (infoList == null) {
                return ".";
            }
            return infoList.join(",");
        }
    }
    /**
     * Returns a function which will fetch the info field with the given name from the current record
     */
    function infoSet(field) {
        return function (value) {
            var values = value.toString().split(',');
            var ArrayList = Java.type("java.util.ArrayList");
            var list = new ArrayList();
            if (Array.isArray(values)) {
                values.forEach(function (val) {
                    list.add(val.toString());
                });
            }
            RTG_VCF_RECORD.getInfo().put(field, list);
        }
    }

    // Map declared info fields to the INFO object
    var infoIds = listToArray(RTG_VCF_HEADER.getInfoLines()).map(function (info) {
        return info.getId()
    });
    var INFO = {};
    infoIds.forEach(function(id) {
        Object.defineProperty(INFO, id, {get: infoGet(id), set: infoSet(id)});
    });
    /**
     * Fetch the pos from the current record
     */
    function pos() {
        return RTG_VCF_RECORD.getOneBasedStart();
    }

    /**
     * Fetch the CHROM from the current record
     */
    function chrom() {
        return RTG_VCF_RECORD.getSequenceName();
    }

    /**
     * Fetch the REF from the current record
     */
    function ref() {
        return RTG_VCF_RECORD.getRefCall();
    }

    /**
     * Fetch the Alts as an array
     */
    function alt() {
        return listToArray(RTG_VCF_RECORD.getAltCalls());
    }

    /**
     * Fetch the QUAL from the current record
     */
    function qual() {
        return RTG_VCF_RECORD.getQuality();
    }

    /**
     * Fetch the FILTER from the current record
     */
    function filter() {
        return listToArray(RTG_VCF_RECORD.getFilters());
    }

    /**
     * Fetch the ID from the current record
     */
    function id() {
        return RTG_VCF_RECORD.getId();
    }

    var props = {
        CHROM: {get: chrom},
        POS: {get: pos},
        ID: {get: id},
        REF: {get: ref},
        ALT: {get: alt},
        QUAL: {get: qual},
        FILTER: {get: filter},
    };
    // Expose an API on the global scope
    global.INFO = INFO;
    Object.defineProperties(global, props);
    // Declare globally accessible objects for the sample names. If the name is javascript safe then
    // that sample will be accessible by it's raw name without needing to explicitly call sample()
    sampleNames.forEach(function (name) {
        global[name] = name;
    });
    global.SAMPLES = sampleNames;

    global.ensureFormatHeader = function (format) {
        var formatField = new FormatField(format);
        RTG_VCF_HEADER.ensureContains(formatField);
        var id = formatField.getId();
        if (!(id in String.prototype)) {
            stringProps[id] = {get: stringGetFunction(id), set: stringSetFunction(id)};
            Object.defineProperties(String.prototype, stringProps);
        }
    };
    global.ensureInfoHeader = function (info) {
        var infoField = new InfoField(info);
        RTG_VCF_HEADER.ensureContains(infoField);
        var id = infoField.getId();
        if (!(id in global.INFO)) {
            Object.defineProperty(global.INFO, id, {get: infoGet(id), set: infoSet(id)});
        }
    };
})(this);
<commit_msg>Add has() helper function<commit_after>// Global variable to hold the current record
var rec;

var VcfUtils = Java.type("com.rtg.vcf.VcfUtils");
var FilterField = Java.type("com.rtg.vcf.header.FilterField");
var InfoField = Java.type("com.rtg.vcf.header.InfoField");
var FormatField = Java.type("com.rtg.vcf.header.FormatField");

(function (global) {
    function listToArray(list) {
        if (list == null) {
            return null;
        }
        var arr = [];
        for (var i = 0; i < list.size(); i++) {
            arr.push(list.get(i));
        }
        return arr;
    }

    // First we need to know the sample names
    var sampleNames = listToArray(RTG_VCF_HEADER.getSampleNames());
    var sampleLookup = {};
    sampleNames.forEach(function (name, index) {
        sampleLookup[name] = index;
    });

    /** List of format IDs */
    var formatIds = listToArray(RTG_VCF_HEADER.getFormatLines()).map(function (field) {
        return field.getId()
    });

    /**
     * String prototype function for fetching a format field from a sample
     * @param field format field
     * @returns {Function} that extracts a format field
     */
    function stringGetFunction(field) {
        return function () {
            var index = sampleLookup[this];
            var fields = RTG_VCF_RECORD.getFormatAndSample().get(field);
            return fields == null ? "." : fields.get(index);
        }
    }

    /**
     * String prototype function for setting a format field in a sample
     * @param field format field
     * @returns {Function} that sets a format field
     */
    function stringSetFunction(field) {
        return function (value) {
            var index = sampleLookup[this];
            RTG_VCF_RECORD.setFormatAndSample(field, value.toString(), index);
        }
    }

    /**
     * Add get/set to string prototype to allow treating them as sample references
     */
    var stringProps = {};
    formatIds.forEach(function (field) {
        stringProps[field] = {get: stringGetFunction(field), set: stringSetFunction(field)};
    });
    // Note that Object.defineProperty doesn't seem to work here... bug defineProperties does
    // No idea why.
    Object.defineProperties(String.prototype, stringProps);

    /**
     * Returns a function which will fetch the info field with the given name from the current record
     */
    function infoGet(field) {
        return function () {
            var infoList = listToArray(RTG_VCF_RECORD.getInfo().get(field));
            if (infoList == null) {
                return ".";
            }
            return infoList.join(",");
        }
    }
    /**
     * Returns a function which will fetch the info field with the given name from the current record
     */
    function infoSet(field) {
        return function (value) {
            var values = value.toString().split(',');
            var ArrayList = Java.type("java.util.ArrayList");
            var list = new ArrayList();
            if (Array.isArray(values)) {
                values.forEach(function (val) {
                    list.add(val.toString());
                });
            }
            RTG_VCF_RECORD.getInfo().put(field, list);
        }
    }

    // Map declared info fields to the INFO object
    var infoIds = listToArray(RTG_VCF_HEADER.getInfoLines()).map(function (info) {
        return info.getId()
    });
    var INFO = {};
    infoIds.forEach(function(id) {
        Object.defineProperty(INFO, id, {get: infoGet(id), set: infoSet(id)});
    });
    /**
     * Fetch the pos from the current record
     */
    function pos() {
        return RTG_VCF_RECORD.getOneBasedStart();
    }

    /**
     * Fetch the CHROM from the current record
     */
    function chrom() {
        return RTG_VCF_RECORD.getSequenceName();
    }

    /**
     * Fetch the REF from the current record
     */
    function ref() {
        return RTG_VCF_RECORD.getRefCall();
    }

    /**
     * Fetch the Alts as an array
     */
    function alt() {
        return listToArray(RTG_VCF_RECORD.getAltCalls());
    }

    /**
     * Fetch the QUAL from the current record
     */
    function qual() {
        return RTG_VCF_RECORD.getQuality();
    }

    /**
     * Fetch the FILTER from the current record
     */
    function filter() {
        return listToArray(RTG_VCF_RECORD.getFilters());
    }

    /**
     * Fetch the ID from the current record
     */
    function id() {
        return RTG_VCF_RECORD.getId();
    }

    var props = {
        CHROM: {get: chrom},
        POS: {get: pos},
        ID: {get: id},
        REF: {get: ref},
        ALT: {get: alt},
        QUAL: {get: qual},
        FILTER: {get: filter},
    };
    // Expose an API on the global scope
    global.has = function (thing) {
        return typeof thing != "undefined" && thing != ".";
    }
    global.INFO = INFO;
    Object.defineProperties(global, props);
    // Declare globally accessible objects for the sample names. If the name is javascript safe then
    // that sample will be accessible by it's raw name without needing to explicitly call sample()
    sampleNames.forEach(function (name) {
        global[name] = name;
    });
    global.SAMPLES = sampleNames;

    global.ensureFormatHeader = function (format) {
        var formatField = new FormatField(format);
        RTG_VCF_HEADER.ensureContains(formatField);
        var id = formatField.getId();
        if (!(id in String.prototype)) {
            stringProps[id] = {get: stringGetFunction(id), set: stringSetFunction(id)};
            Object.defineProperties(String.prototype, stringProps);
        }
    };
    global.ensureInfoHeader = function (info) {
        var infoField = new InfoField(info);
        RTG_VCF_HEADER.ensureContains(infoField);
        var id = infoField.getId();
        if (!(id in global.INFO)) {
            Object.defineProperty(global.INFO, id, {get: infoGet(id), set: infoSet(id)});
        }
    };
})(this);
<|endoftext|>