<commit_before>import React, {useState, useEffect, createRef} from 'react';
import classnames from 'classnames';
import {Link, navigate} from 'gatsby';
import elasticlunr, {Index} from 'elasticlunr';
import {Input} from '../../../src/react/inputs';
import '../../stylesheets/search.scss';

elasticlunr.clearStopWords();

const inputRef = createRef();
let searchIndex;

const onWindowKeyDown = evt => {
  if (evt.key !== '/' || !inputRef.current) return;
  const {activeElement: {nodeName, type}} = document;
  if (nodeName === 'INPUT' && type === 'text') return;
  if (nodeName === 'TEXTAREA') return;

  evt.preventDefault();
  inputRef.current.focus();
};

const Search = props => {
  const [focused, setFocused] = useState(false);
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [cursor, setCursor] = useState(0);

  const showResults = focused && query.length > 0 && results.length > 0;

  useEffect(() => {
    window.addEventListener('keydown', onWindowKeyDown);
    return () => window.removeEventListener('keydown', onWindowKeyDown);
  }, []);

  const onInputChange = ({target: {value}}) => {
    searchIndex = searchIndex || Index.load(props.searchIndex);

    setCursor(0);
    setQuery(value);
    setResults(
      searchIndex
        .search(value, {expand: true})
        .map(({ ref }) => searchIndex.documentStore.getDoc(ref))
    );
  };

  const onInputKeyDown = evt => {
    if (!showResults) return;

    if (evt.key === 'ArrowDown') {
      evt.preventDefault();
      setCursor(cursor + 1 >= results.length ? 0 : cursor + 1);
      return;
    }

    if (evt.key === 'ArrowUp') {
      evt.preventDefault();
      setCursor(cursor - 1 < 0 ? results.length - 1 : cursor - 1);
      return;
    }

    if (evt.key === 'Enter') {
      evt.preventDefault();
      setQuery('');
      navigate(results[cursor].route);
      return;
    }
  };

  const onResultClick = () => {
    setQuery('');
  };

  return (

      <Input
        className="sg-search__input"
        innerRef={inputRef}
        type="search"
        icon="search"
        placeholder="Search..."
        aria-label="Search style guide content"
        value={query}
        onChange={onInputChange}
        onFocus={() => setFocused(true)}
        onBlur={() => setTimeout(() => setFocused(false), 0)}
        onKeyDown={onInputKeyDown}
      />

      {showResults && (

          {results.map((result, index) => (

              <Link
                to={result.route}
                className={classnames(
                  'sg-search__link',
                  {'sg-search__link--active': index === cursor}
                )}
                onClick={onResultClick}
                tabIndex="-1">
                {result.group} / {result.title}
              </Link>

          ))}

      )}

  );
};

export default Search;
<commit_msg>styleguide: use aria attributes to make search accessible, close search results on 'Escape' key<commit_after>import React, {useState, useEffect, createRef} from 'react';
import classnames from 'classnames';
import {Link, navigate} from 'gatsby';
import elasticlunr, {Index} from 'elasticlunr';
import {Input} from '../../../src/react/inputs';
import '../../stylesheets/search.scss';

elasticlunr.clearStopWords();

const inputRef = createRef();
let searchIndex;

const onWindowKeyDown = evt => {
  if (evt.key !== '/' || !inputRef.current) return;
  const {activeElement: {nodeName, type}} = document;
  if (nodeName === 'INPUT' && type === 'text') return;
  if (nodeName === 'TEXTAREA') return;

  evt.preventDefault();
  inputRef.current.focus();
};

const Search = props => {
  const [focused, setFocused] = useState(false);
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [cursor, setCursor] = useState(0);

  const showResults = focused && query.length > 0 && results.length > 0;

  useEffect(() => {
    window.addEventListener('keydown', onWindowKeyDown);
    return () => window.removeEventListener('keydown', onWindowKeyDown);
  }, []);

  const onInputChange = ({target: {value}}) => {
    searchIndex = searchIndex || Index.load(props.searchIndex);

    setCursor(0);
    setQuery(value);
    setResults(
      searchIndex
        .search(value, {expand: true})
        .map(({ ref }) => searchIndex.documentStore.getDoc(ref))
    );
  };

  const onInputKeyDown = evt => {
    if (!showResults) return;

    switch (evt.key) {
      case 'ArrowDown':
      case 'Down':
        evt.preventDefault();
        setCursor(cursor + 1 >= results.length ? 0 : cursor + 1);
        return;

      case 'ArrowUp':
      case 'Up':
        evt.preventDefault();
        setCursor(cursor - 1 < 0 ? results.length - 1 : cursor - 1);
        return;

      case 'Enter':
        evt.preventDefault();
        setQuery('');
        navigate(results[cursor].route);
        return;

      case 'Escape':
      case 'Esc':
        evt.preventDefault();
        setQuery('');
        return;

      default:
        return;
    }
  };

  const onResultClick = () => {
    setQuery('');
  };

  return (

      <Input
        className="sg-search__input"
        innerRef={inputRef}
        type="search"
        icon="search"
        placeholder="Search..."
        aria-autocomplete="list"
        aria-label="Search style guide pages. Press up and down arrows to choose results, and enter to visit page."
        aria-owns="sg-search__results"
        aria-activedescendant={`sg-search__result-${cursor}`}
        value={query}
        onChange={onInputChange}
        onFocus={() => setFocused(true)}
        onBlur={() => setTimeout(() => setFocused(false), 0)}
        onKeyDown={onInputKeyDown}
      />

      {showResults && (

          {results.map((result, index) => (
            <li
              key={result.id}
              id={`sg-search__result-${index}`}
              className="sg-search__result"
              role="option"
              aria-selected={index === cursor}
            >
              <Link
                to={result.route}
                className={classnames(
                  'sg-search__link',
                  {'sg-search__link--active': index === cursor}
                )}
                onClick={onResultClick}
                tabIndex="-1">
                {result.group} / {result.title}
              </Link>

          ))}

      )}

  );
};

export default Search;
<|endoftext|>


<commit_before>import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('ExampleApp', () => App);
<commit_msg>Minor formatting.<commit_after>import { AppRegistry } from "react-native";
import App             from "./App";

AppRegistry.registerComponent( "ExampleApp", () => App );
<|endoftext|>


<commit_before>/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

'use strict';

(function () {
    describe("tracker_web: Testing Modules", function () {

        // describe("App Module: ",function (){

        // var module;

        beforeEach(module('trackerApp'));

        var $controller;

  beforeEach(inject(function(_$controller_){
    // The injector unwraps the underscores (_) from around the parameter names when matching
    $controller = _$controller_;
  }));

        it("should be registered", function () {

            expect(module).not.toEqual(null);
        });

        it('should have a LoginCtrl controller', function () {
             var $scope = {};
            var NotesController=$controller('NotesController', { $scope: $scope });
            expect(NotesController).toBeDefined();
        });

        // });

    });

})();

<commit_msg>Add appSpec to test app and controllers<commit_after>/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

'use strict';

(function () {
    describe("tracker_web: Testing Modules", function () {

        // describe("App Module: ",function (){

        // var module;

        beforeEach(module('trackerApp'));

        var $controller;

        beforeEach(inject(function (_$controller_) {
            // The injector unwraps the underscores (_) from around the parameter names when matching
            $controller = _$controller_;
        }));

        it("should be registered", function () {

            expect(module).not.toEqual(null);
        });

        //Test Notes Controller is defined
        it('should have a NotesController controller', function () {
            var $scope = {};
            var NotesController = $controller('NotesController', {$scope: $scope});
            expect(NotesController).toBeDefined();
        });

        //Test Notes Controller is defined
        it('should have a ScriptController controller', function () {
            var $scope = {};
            var scriptsController = $controller('scriptsController', {$scope: $scope});
            expect(scriptsController).toBeDefined();
        });

        //Test NotesTab Controller is defined
        it('should have a ScriptController controller', function () {
            var $scope = {};
            var NotesTabController = $controller('NotesTabController', {$scope: $scope});
            expect(NotesTabController).toBeDefined();
        });

        //Test DeviceSettings Controller is defined
        it('should have a DeviceSettingsController controller', function () {
            var $scope = {};
            var DeviceSettingsController = $controller('DeviceSettingsController', {$scope: $scope});
            expect(DeviceSettingsController).toBeDefined();
        });

        //Test VoiceMailTabController Controller is defined
        it('should have a VoiceMailTabController controller', function () {
            var $scope = {};
            var VoiceMailTabController = $controller('VoiceMailTabController', {$scope: $scope});
            expect(VoiceMailTabController).toBeDefined();
        });

        //Test ChatPerHourController Controller is defined
        it('should have a ChatPerHourController controller', function () {
            var $scope = {};
            var ChatPerHourController = $controller('ChatPerHourController', {$scope: $scope});
            expect(ChatPerHourController).toBeDefined();
        });

    });

})();

<|endoftext|>


<commit_before>import {
	Platform,
	Linking,
	Dimensions,
	Keyboard,
	Alert,
	AsyncStorage
} from 'react-native'
import RNRestart from 'react-native-restart'
import RNExitApp from 'react-native-exit-app'
import dateFormat from 'dateformat'

import { COLOR_PRIMARY, COLOR_SECONDARY } from '../styles/ColorConstants'
import logger from './logger'

/**
 * A module containing general helper functions
 * @module general
 */
module.exports = {

	/**
	 * Gets whether or not the current platform the app is running on is IOS
	 * @returns {boolean} True if the platform is IOS, false otherwise
	 */
	platformIOS() {
		return Platform.OS === 'ios'
	},

	/**
	 * Gets whether or not the current platform the app is running on is Android
	 * @memberOf util/general
	 * @returns {boolean} True if the platform is Android, false otherwise
	 */
	platformAndroid() {
		return Platform.OS === 'android'
	},

	/**
	 * Gets the current platform ths app is running on
	 * @returns {string} The platform name
	 */
	getPlatform() {
		return Platform.OS
	},

	/**
	 * Checks to see if the device model is iPhone X based on the viewport height
	 * @returns {boolean} True if the device model is iPhone X, false otherwise
	 */
	deviceIphoneX() {
		return Dimensions.get('window').height === 812
	},

	/**
	 * Converts a numerical quantity from meters to miles
	 * @memberOf util/general
	 * @param {number} meters The quantity to convert
	 * @returns {number} The quantity now converted into miles
	 */
	convertMetersToMiles(meters) {
		return (meters / 1609.344)
	},

	/**
	 * Gets a string representation of a given quantity of miles (up to and including a single decimal place)
	 * @param {number} miles The quantity to convert to a string
	 * @returns {number} The miles quantity now as a string
	 */
	getDistanceMilesStr(miles) {
		return (miles.toFixed(1) + ' mi')
	},

	/**
	 * Attempts to open the provided URL for displaying to the user
	 * @param {string} url The URL to open
	 * @returns {boolean} If the URL cannot be opened, this logs to console and returns false. Otherwise, returns true.
	 */
	openURL(url) {
		Linking.canOpenURL(url).then((supported) => {
			if (!supported) {
				logger.log('ERR: openURL: Unable to handle url: ' + url)
				return false
			} else {
				return Linking.openURL(url)
			}
		}).catch((err) => {
			logger.log('ERR: openURL: ' + err)
			return false
		})
	},

	/**
	 * Gets the URL for obtaining directions to a given location via a given transportation method
	 * @param {string} method Can be "walk" or anything else. Anything else will result in a URL for driving.
	 * @param {string|number} stopLat The latitude of the destination
	 * @param {string|number} stopLon The longitude of the destination
	 * @param {string} address The destination's address
	 * @return {string} A platform-specific URL for obtaining the directions
	 */
	getDirectionsURL(method, stopLat, stopLon, address) {
		let directionsURL
		let directionsAddr

		if (stopLat && stopLon) directionsAddr = `${stopLat},${stopLon}`
		if (address) directionsAddr = address

		if (this.platformIOS()) {
			if (method === 'walk') {
				directionsURL = `http://maps.apple.com/?daddr=${directionsAddr}&dirflg=w`
			} else {
				// Default to driving directions
				directionsURL = `http://maps.apple.com/?daddr=${directionsAddr}&dirflg=d`
			}
		} else {
			if (method === 'walk') {
				// directionsURL = 'https://www.google.com/maps/dir/' + startLat + ',' + startLon + '/' + stopLat + ',' + stopLon + '/@' + startLat + ',' + startLon + ',18z/data=!4m2!4m1!3e1'
				directionsURL = 'https://maps.google.com/maps?saddr=Current+Location&daddr=' + directionsAddr + '&dirflg=w'
			} else {
				// Default to driving directions
				directionsURL = 'https://maps.google.com/maps?saddr=Current+Location&daddr=' + directionsAddr + '&dirflg=d'
			}
		}

		return directionsURL
	},

	/**
	 * Attempts to redirect the device to its navigation app
	 * @param {string|number} destinationLat The destination's latitude
	 * @param {string|number} destinationLon The destination's longitude
	 * @param {string} destinationAddress The destination's address
	 * @returns {boolean} true/false depending if navigation app is opened
	 */
	gotoNavigationApp(destinationLat, destinationLon, destinationAddress) {
		if (destinationLat && destinationLon) {
			const destinationURL = module.exports.getDirectionsURL('walk', destinationLat, destinationLon )
			return module.exports.openURL(destinationURL)
		}
		else if (destinationAddress) {
			const destinationURL = module.exports.getDirectionsURL('walk', null, null, destinationAddress )
			return module.exports.openURL(destinationURL)
		}
	},

	/**
	 * Rounds a number to the nearest integer
	 * As examples: 2.49 will be rounded down to 2, while 2.5 will be rounded up to 3
	 * @param {number} number The number to round
	 * @returns {number} The rounded number
	 */
	round(number) {
		return Math.round(number)
	},

	/**
	 * Gets the pixel-ratio-modifier needed for this device window
	 * The modifier is a ratio to be used for scaling GUI elements based on the default sizes
	 * @returns {number} The ratio of current window width vs the app's default width
	 * @todo The variable windowHeight is unused
	 */
	getPRM() {
		const windowWidth = Dimensions.get('window').width
		const appDefaultWidth = 414
		return (windowWidth / appDefaultWidth)
	},

	/**
	 * Modifies the input value by the pixel-ration-modifier
	 * This can be used to scale a GUI element's size for the current device
	 * @param {number} num The number/size to scale
	 * @returns {number} The now-scaled number/size
	 * @todo This method could call this.getPRM() to prevent redundant code
	 */
	doPRM(num) {
		const windowWidth = Dimensions.get('window').width
		const appDefaultWidth = 414
		const prm = (windowWidth / appDefaultWidth)

		return Math.round(num * prm)
	},

	/**
	 * Gets the maximum width for a card occupying a screen/window
	 * @returns {number} The maximum width in points
	 */
	getMaxCardWidth() {
		return Dimensions.get('window').width - 2 - 12
	},

	/**
	 * Gets the screen width of the users device
	 * @returns {number} The screen width in points
	 */
	getScreenWidth() {
		return Dimensions.get('window').width
	},

	/**
	 * Gets the screen height of the users device
	 * @returns {number} The screen height in points
	 */
	getScreenHeight() {
		return Dimensions.get('window').height
	},

	/**
	 * Gets the UCSD campus primary color in hexidecimal form
	 * @returns {string}
	 */
	getCampusPrimary() {
		return COLOR_PRIMARY
	},

	/**
	 * Gets the UCSD campus secondary color in hexidecimal form
	 * @returns {string}
	 */
	getCampusSecondary() {
		return COLOR_SECONDARY
	},

	/**
	 * Gets the current timestamp in seconds
	 * @returns {number} The number of seconds since midnight Jan 1, 1970
	 */
	getCurrentTimestamp() {
		return Math.round(Date.now() / 1000)
	},

	/**
	 * Gets the current timestamp formatted as a string
	 * @returns {string} The current time formatted to "yyyymmdd"
	 */
	getTimestampNumeric() {
		return (dateFormat(Date.now(), 'yyyymmdd'))
	},

	/**
	 * Gets the current timestamp in a specified format
	 * @param {string} format The format to use (e.g. "yyyymmdd")
	 * @returns {string} The formatted current time
	 */
	getTimestamp(format) {
		return (dateFormat(Date.now(), format))
	},

	/**
	 * Gets the current date
	 * @returns {number} The number of milliseconds since midnight Jan 1, 1970
	 */
	getDateNow() {
		return (Date.now())
	},

	/**
	 * Gets the humanized duration in hours and minutes from two unix timestamps
	 * @param {string|number} startTime Unix time of the start period
	 * @param {string|number} endTime Unix time of the end period
	 * @returns {string} A humanized string indicating an hours & minutes duration in time (i.e. '3 hours 15 mins')
	 */
	getHumanizedDuration(startTime, endTime) {
		let durationStr = '',
			durationHours = 0,
			durationMinutes = (Number(endTime) - Number(startTime)) / (60 * 1000)
		while (durationMinutes >= 60) {
			durationMinutes -= 60
			durationHours += 1
		}
		if (durationHours) {
			durationStr += durationHours + ' hour' + (durationHours > 1 ? 's ' : ' ')
		}
		if (durationMinutes) {
			durationStr += durationMinutes + ' min' + (durationMinutes > 1 ? 's' : '')
		}
		return (durationStr.trim())
	},

	/**
	 * Converts a time string provided in military form to AM-PM form
	 * @param {string} time The time in military form
	 * @returns {string} The time in AM-PM form
	 */
	militaryToAMPM(time) {
		if (time) {
			let militaryTime = time.substring(0, 5).replace(':','')
			let militaryTimeHH,
				militaryTimeMM,
				militaryTimeAMPM

			militaryTime = militaryTime.replace(/^0/,'')

			if (militaryTime.length === 3) {
				militaryTimeHH = militaryTime.substring(0,1)
				militaryTimeMM = militaryTime.substring(1,3)
			} else if (militaryTime.length === 4) {
				militaryTimeHH = militaryTime.substring(0,2)
				militaryTimeMM = militaryTime.substring(2,4)
			}

			if (militaryTimeHH < 12) {
				militaryTimeAMPM = 'am'
			} else {
				militaryTimeAMPM = 'pm'
			}

			if (militaryTimeHH > 12) {
				militaryTimeHH -= 12
			}

			if (militaryTimeHH === '0') {
				militaryTimeHH = '12'
			}

			if (militaryTimeMM === '00') {
				militaryTimeMM = ''
			}

			if (militaryTimeMM.length > 0) {
				return (militaryTimeHH + ':' + militaryTimeMM + militaryTimeAMPM)
			} else {
				return (militaryTimeHH + militaryTimeAMPM)
			}
		} else {
			return ''
		}
	},

	/**
	 * Gets an array containing random colors
	 * @param {number} length The length of the array to return
	 * @returns {number[]} An array with each element containing a randomly generated hex color code
	 */
	getRandomColorArray(length) {
		const randomColors = []
		for (let i = 0; i < length; ++i) {
			randomColors.push(this.getRandomColor())
		}
		return randomColors
	},

	/**
	 * Generates random color hex
	 * @returns {string} A randomly generated hex color code
	 */
	getRandomColor() {
		return '#' + ('000000' + Math.random().toString(16).slice(2, 8).toUpperCase()).slice(-6)
	},

	/**
	 * Gets a linear sorting function based on a given property of the input objects
	 * @param {string} property The property the sorting function should concern itself with
	 * @returns {function} A sorting function that compares inputs based on one of their properties
	 */
	dynamicSort(property) {
		return function (a, b) {
			return (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0
		}
	},

	/**
	 * This is a comparison function used for sorting markers
	 * @param {Object} a The first marker to compare
	 * @param {number} a.distance The first distance
	 * @param {Object} b The second marker to compare
	 * @param {number} b.distance The second distance
	 * @return {number} -1 if a is less-than b, 0 if they are equal, 1 if a is greater-then b
	 * @todo This function is already handled by the one returned by dynamicSort()
	 */
	sortNearbyMarkers(a, b) {
		if (a.distance < b.distance) {
			return -1
		} else if (a.distance > b.distance) {
			return 1
		} else {
			return 0
		}
	},

	/**
	 * @returns {function} Hides the keyboard
	 */
	hideKeyboard() {
		Keyboard.dismiss()
	},

	/**
	 * @param {Object} e Error object with details about the fatal error.
	 * @returns {function} Resets the app in case of a fatal error.
	 */
	gracefulFatalReset(e) {
		logger.trackException(e.toString(), true)
		AsyncStorage.clear()
		Alert.alert(
			'Oops! Something went wrong!',
			'An error report has been automatically sent to the technical staff. Try restarting. If the problem still occurs try again later.',
			[{
				text: 'Reload',
				onPress: () => {
					RNRestart.Restart()
				}
			},
			{
				text: 'Quit',
				style: 'cancel',
				onPress: () => {
					RNExitApp.exitApp()
				}
			}
			],
			{ cancelable: false }
		)
	}
}
<commit_msg>Update error handling p2<commit_after>import {
	Platform,
	Linking,
	Dimensions,
	Keyboard,
	Alert,
	AsyncStorage
} from 'react-native'
import RNRestart from 'react-native-restart'
import RNExitApp from 'react-native-exit-app'
import dateFormat from 'dateformat'

import { COLOR_PRIMARY, COLOR_SECONDARY } from '../styles/ColorConstants'
import logger from './logger'

/**
 * A module containing general helper functions
 * @module general
 */
module.exports = {

	/**
	 * Gets whether or not the current platform the app is running on is IOS
	 * @returns {boolean} True if the platform is IOS, false otherwise
	 */
	platformIOS() {
		return Platform.OS === 'ios'
	},

	/**
	 * Gets whether or not the current platform the app is running on is Android
	 * @memberOf util/general
	 * @returns {boolean} True if the platform is Android, false otherwise
	 */
	platformAndroid() {
		return Platform.OS === 'android'
	},

	/**
	 * Gets the current platform ths app is running on
	 * @returns {string} The platform name
	 */
	getPlatform() {
		return Platform.OS
	},

	/**
	 * Checks to see if the device model is iPhone X based on the viewport height
	 * @returns {boolean} True if the device model is iPhone X, false otherwise
	 */
	deviceIphoneX() {
		return Dimensions.get('window').height === 812
	},

	/**
	 * Converts a numerical quantity from meters to miles
	 * @memberOf util/general
	 * @param {number} meters The quantity to convert
	 * @returns {number} The quantity now converted into miles
	 */
	convertMetersToMiles(meters) {
		return (meters / 1609.344)
	},

	/**
	 * Gets a string representation of a given quantity of miles (up to and including a single decimal place)
	 * @param {number} miles The quantity to convert to a string
	 * @returns {number} The miles quantity now as a string
	 */
	getDistanceMilesStr(miles) {
		return (miles.toFixed(1) + ' mi')
	},

	/**
	 * Attempts to open the provided URL for displaying to the user
	 * @param {string} url The URL to open
	 * @returns {boolean} If the URL cannot be opened, this logs to console and returns false. Otherwise, returns true.
	 */
	openURL(url) {
		Linking.canOpenURL(url).then((supported) => {
			if (!supported) {
				logger.log('ERR: openURL: Unable to handle url: ' + url)
				return false
			} else {
				return Linking.openURL(url)
			}
		}).catch((err) => {
			logger.log('ERR: openURL: ' + err)
			return false
		})
	},

	/**
	 * Gets the URL for obtaining directions to a given location via a given transportation method
	 * @param {string} method Can be "walk" or anything else. Anything else will result in a URL for driving.
	 * @param {string|number} stopLat The latitude of the destination
	 * @param {string|number} stopLon The longitude of the destination
	 * @param {string} address The destination's address
	 * @return {string} A platform-specific URL for obtaining the directions
	 */
	getDirectionsURL(method, stopLat, stopLon, address) {
		let directionsURL
		let directionsAddr

		if (stopLat && stopLon) directionsAddr = `${stopLat},${stopLon}`
		if (address) directionsAddr = address

		if (this.platformIOS()) {
			if (method === 'walk') {
				directionsURL = `http://maps.apple.com/?daddr=${directionsAddr}&dirflg=w`
			} else {
				// Default to driving directions
				directionsURL = `http://maps.apple.com/?daddr=${directionsAddr}&dirflg=d`
			}
		} else {
			if (method === 'walk') {
				// directionsURL = 'https://www.google.com/maps/dir/' + startLat + ',' + startLon + '/' + stopLat + ',' + stopLon + '/@' + startLat + ',' + startLon + ',18z/data=!4m2!4m1!3e1'
				directionsURL = 'https://maps.google.com/maps?saddr=Current+Location&daddr=' + directionsAddr + '&dirflg=w'
			} else {
				// Default to driving directions
				directionsURL = 'https://maps.google.com/maps?saddr=Current+Location&daddr=' + directionsAddr + '&dirflg=d'
			}
		}

		return directionsURL
	},

	/**
	 * Attempts to redirect the device to its navigation app
	 * @param {string|number} destinationLat The destination's latitude
	 * @param {string|number} destinationLon The destination's longitude
	 * @param {string} destinationAddress The destination's address
	 * @returns {boolean} true/false depending if navigation app is opened
	 */
	gotoNavigationApp(destinationLat, destinationLon, destinationAddress) {
		if (destinationLat && destinationLon) {
			const destinationURL = module.exports.getDirectionsURL('walk', destinationLat, destinationLon )
			return module.exports.openURL(destinationURL)
		}
		else if (destinationAddress) {
			const destinationURL = module.exports.getDirectionsURL('walk', null, null, destinationAddress )
			return module.exports.openURL(destinationURL)
		}
	},

	/**
	 * Rounds a number to the nearest integer
	 * As examples: 2.49 will be rounded down to 2, while 2.5 will be rounded up to 3
	 * @param {number} number The number to round
	 * @returns {number} The rounded number
	 */
	round(number) {
		return Math.round(number)
	},

	/**
	 * Gets the pixel-ratio-modifier needed for this device window
	 * The modifier is a ratio to be used for scaling GUI elements based on the default sizes
	 * @returns {number} The ratio of current window width vs the app's default width
	 * @todo The variable windowHeight is unused
	 */
	getPRM() {
		const windowWidth = Dimensions.get('window').width
		const appDefaultWidth = 414
		return (windowWidth / appDefaultWidth)
	},

	/**
	 * Modifies the input value by the pixel-ration-modifier
	 * This can be used to scale a GUI element's size for the current device
	 * @param {number} num The number/size to scale
	 * @returns {number} The now-scaled number/size
	 * @todo This method could call this.getPRM() to prevent redundant code
	 */
	doPRM(num) {
		const windowWidth = Dimensions.get('window').width
		const appDefaultWidth = 414
		const prm = (windowWidth / appDefaultWidth)

		return Math.round(num * prm)
	},

	/**
	 * Gets the maximum width for a card occupying a screen/window
	 * @returns {number} The maximum width in points
	 */
	getMaxCardWidth() {
		return Dimensions.get('window').width - 2 - 12
	},

	/**
	 * Gets the screen width of the users device
	 * @returns {number} The screen width in points
	 */
	getScreenWidth() {
		return Dimensions.get('window').width
	},

	/**
	 * Gets the screen height of the users device
	 * @returns {number} The screen height in points
	 */
	getScreenHeight() {
		return Dimensions.get('window').height
	},

	/**
	 * Gets the UCSD campus primary color in hexidecimal form
	 * @returns {string}
	 */
	getCampusPrimary() {
		return COLOR_PRIMARY
	},

	/**
	 * Gets the UCSD campus secondary color in hexidecimal form
	 * @returns {string}
	 */
	getCampusSecondary() {
		return COLOR_SECONDARY
	},

	/**
	 * Gets the current timestamp in seconds
	 * @returns {number} The number of seconds since midnight Jan 1, 1970
	 */
	getCurrentTimestamp() {
		return Math.round(Date.now() / 1000)
	},

	/**
	 * Gets the current timestamp formatted as a string
	 * @returns {string} The current time formatted to "yyyymmdd"
	 */
	getTimestampNumeric() {
		return (dateFormat(Date.now(), 'yyyymmdd'))
	},

	/**
	 * Gets the current timestamp in a specified format
	 * @param {string} format The format to use (e.g. "yyyymmdd")
	 * @returns {string} The formatted current time
	 */
	getTimestamp(format) {
		return (dateFormat(Date.now(), format))
	},

	/**
	 * Gets the current date
	 * @returns {number} The number of milliseconds since midnight Jan 1, 1970
	 */
	getDateNow() {
		return (Date.now())
	},

	/**
	 * Gets the humanized duration in hours and minutes from two unix timestamps
	 * @param {string|number} startTime Unix time of the start period
	 * @param {string|number} endTime Unix time of the end period
	 * @returns {string} A humanized string indicating an hours & minutes duration in time (i.e. '3 hours 15 mins')
	 */
	getHumanizedDuration(startTime, endTime) {
		let durationStr = '',
			durationHours = 0,
			durationMinutes = (Number(endTime) - Number(startTime)) / (60 * 1000)
		while (durationMinutes >= 60) {
			durationMinutes -= 60
			durationHours += 1
		}
		if (durationHours) {
			durationStr += durationHours + ' hour' + (durationHours > 1 ? 's ' : ' ')
		}
		if (durationMinutes) {
			durationStr += durationMinutes + ' min' + (durationMinutes > 1 ? 's' : '')
		}
		return (durationStr.trim())
	},

	/**
	 * Converts a time string provided in military form to AM-PM form
	 * @param {string} time The time in military form
	 * @returns {string} The time in AM-PM form
	 */
	militaryToAMPM(time) {
		if (time) {
			let militaryTime = time.substring(0, 5).replace(':','')
			let militaryTimeHH,
				militaryTimeMM,
				militaryTimeAMPM

			militaryTime = militaryTime.replace(/^0/,'')

			if (militaryTime.length === 3) {
				militaryTimeHH = militaryTime.substring(0,1)
				militaryTimeMM = militaryTime.substring(1,3)
			} else if (militaryTime.length === 4) {
				militaryTimeHH = militaryTime.substring(0,2)
				militaryTimeMM = militaryTime.substring(2,4)
			}

			if (militaryTimeHH < 12) {
				militaryTimeAMPM = 'am'
			} else {
				militaryTimeAMPM = 'pm'
			}

			if (militaryTimeHH > 12) {
				militaryTimeHH -= 12
			}

			if (militaryTimeHH === '0') {
				militaryTimeHH = '12'
			}

			if (militaryTimeMM === '00') {
				militaryTimeMM = ''
			}

			if (militaryTimeMM.length > 0) {
				return (militaryTimeHH + ':' + militaryTimeMM + militaryTimeAMPM)
			} else {
				return (militaryTimeHH + militaryTimeAMPM)
			}
		} else {
			return ''
		}
	},

	/**
	 * Gets an array containing random colors
	 * @param {number} length The length of the array to return
	 * @returns {number[]} An array with each element containing a randomly generated hex color code
	 */
	getRandomColorArray(length) {
		const randomColors = []
		for (let i = 0; i < length; ++i) {
			randomColors.push(this.getRandomColor())
		}
		return randomColors
	},

	/**
	 * Generates random color hex
	 * @returns {string} A randomly generated hex color code
	 */
	getRandomColor() {
		return '#' + ('000000' + Math.random().toString(16).slice(2, 8).toUpperCase()).slice(-6)
	},

	/**
	 * Gets a linear sorting function based on a given property of the input objects
	 * @param {string} property The property the sorting function should concern itself with
	 * @returns {function} A sorting function that compares inputs based on one of their properties
	 */
	dynamicSort(property) {
		return function (a, b) {
			return (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0
		}
	},

	/**
	 * This is a comparison function used for sorting markers
	 * @param {Object} a The first marker to compare
	 * @param {number} a.distance The first distance
	 * @param {Object} b The second marker to compare
	 * @param {number} b.distance The second distance
	 * @return {number} -1 if a is less-than b, 0 if they are equal, 1 if a is greater-then b
	 * @todo This function is already handled by the one returned by dynamicSort()
	 */
	sortNearbyMarkers(a, b) {
		if (a.distance < b.distance) {
			return -1
		} else if (a.distance > b.distance) {
			return 1
		} else {
			return 0
		}
	},

	/**
	 * @returns {function} Hides the keyboard
	 */
	hideKeyboard() {
		Keyboard.dismiss()
	},

	/**
	 * @param {Object} e Error object with details about the fatal error.
	 * @returns {function} Resets the app in case of a fatal error.
	 */
	gracefulFatalReset(e) {
		logger.trackException(e.toString(), true)
		Alert.alert(
			'Oops! Something went wrong!',
			'An error report has been automatically sent to the technical staff. Try restarting. If the problem still occurs try again later.',
			[
				{
					text: 'Quit',
					style: 'cancel',
					onPress: () => {
						AsyncStorage.clear(() => RNExitApp.exitApp())
					}
				}
			],
			{ cancelable: false }
		)
	}
}
<|endoftext|>


<commit_before>/**
 * @file Reads calendar feed to determine schedule / day and other important events
 * @module portal
 */

var config = require(__dirname + '/config.js');

var _           = require('underscore');
var ical        = require('ical');
var querystring = require('querystring');
var request     = require('request');
var url         = require('url');

// URL Calendars come from
var urlPrefix = 'https://micds.myschoolapp.com/podium/feed/iCal.aspx?q=';
// RegEx to test if calendar summary is a valid Day Rotation
var validDayRotation = /^Day [1-6] \((US|MS)\)$/;

/**
 * Makes sure a given url is valid and it points to a Canvas calendar feed
 * @function verifyURL
 *
 * @param {string} url - URI to iCal feed
 * @param {verifyURLCallback} callback - Callback
 */

/**
 * Returns whether url is valid or not
 * @callback verifyURLCallback
 *
 * @param {Object} err - Null if success, error object if failure.
 * @param {Boolean|string} isValid - True if valid URL, string describing problem if not valid. Null if error.
 * @param {string} url - Valid and formatted URL to our likings. Null if error or invalid url.
 */

function verifyURL(portalURL, callback) {

    if(typeof callback !== 'function') return;

    if(typeof portalURL !== 'string') {
        callback(new Error('Invalid URL!'), null, null);
        return;
    }

    // Parse URL first
    var parsedURL = url.parse(portalURL);
    var queries = querystring.parse(parsedURL.query);

    if(typeof queries.q !== 'string') {
        callback(null, 'URL does not contain calendar ID!', null);
        return;
    }

    var validURL = urlPrefix + queries.q;

    // Not lets see if we can actually get any data from here
    request(validURL, function(err, response, body) {
        if(err) {
            callback(new Error('There was a problem fetching portal data from the URL!'), null, null);
            return;
        }
        if(response.statusCode !== 200) {
            callback(null, 'Invalid URL!', null);
            return;
        }

        var data = ical.parseICS(body);

        // School Portal does not give a 404 if calendar is invalid. Instead, it gives an empty calendar.
        // Unlike Canvas, the portal is guaranteed to contain some sort of data within a span of a year.
        if(_.isEmpty(data)) {
            callback(null, 'Invalid URL!', null);
            return;
        }

        // Put calendar into array
        var calEvents = [];
        for(var eventUid in data) {
            var calEvent = data[eventUid];
            delete calEvent.uid;
            calEvents.push(calEvent);
        }

        // Look through every 'Day # (US/MS)' andd see how many events there are
        var dayDates = {};
        for(var i = 0; i < calEvents.length; i++) {
            var calEvent = calEvents[i];
            // If event doesn't have a summary, skip
            if(typeof calEvent.summary !== 'string') continue;

            // See if valid day
            if(validDayRotation.test(calEvent.summary)) {
                // Get actual day
                var day = calEvent.summary.match(/[1-6]/)[0];
                // Get date
                var start = new Date(calEvent.start);

                // Add to dayDates object
                if(typeof dayDates[day] === 'undefined') {
                    dayDates[day] = [];
                }
                dayDates[day].push({
                    year : start.getFullYear(),
                    month: start.getMonth() + 1,
                    day  : start.getDate()
                });
            }
        }

        if(_.isEmpty(data)) {
            callback(null, 'The calendar does not contain the information we need! Make sure you\'re copying your personal calendar!', null);
            return;
        }

        callback(null, true, validURL);

    });
}

/**
 * Queries a person's Portal RSS feed to get schedule and stuff
 * @function scheduleFeed
 *
 * @param {string} url - URL to iCal feed
 * @param {scheduleFeedCallback} callback - Callback after schedule is parsed
 */

/**
 * Callback after schedule feed is parsed
 * @callback scheduleFeedCallback
 *
 * @param {Object} that - Object reference to the schedule feed
 */

function scheduleFeed(url, callback) {
    var that = this;
    that.success = null;

    // Parses the schedule for given date and returns arrays
    this.getSchedule = function(day, month, year) {
        if(this.success) {
            // Default date
            var current = new Date();
            var schedule = [];
            var events = [];
            var scheduleDay;

            day = day || current.getDate();
            month = month || current.getMonth();
            year = year || current.getFullYear();

            // Get schedule from feed
            _.each(this.parsed, function(event, uid) {
                var eventDate = new Date(event.start);
                if(eventDate.getDate() === day && eventDate.getMonth() === month && eventDate.getFullYear() === year) {

                    // Check if it's an all-day event
                    if(eventDate.getSeconds() === 0 && eventDate.getMinutes() === 0 && eventDate.getHours() === 0) {
                        var period = {
                            'name': event.summary,
                            'description': event.description,
                            'location': event.location
                        };

                        if(/^Day [1-6]/.test(period.name)) {
                            scheduleDay = parseInt(period.name.match(/[1-6]/)[0]);
                        } else {
                            events.push(period);
                        }

                    } else {
                        var startPeriod = new Date(event.start);
                        var endPeriod   = new Date(event.end);

                        var period = {
                            'start': startPeriod,
                            'end'  : endPeriod,
                            'class': event.summary,
                            'description': event.description,
                            'location': event.location
                        };

                        // Overlap existing events
                        var startTime = startPeriod.getTime();
                        var endTime   = endPeriod.getTime();

                        schedule.forEach(function(block, index) {

                            var startBlock = block.start.getTime();
                            var endBlock   = block.end.getTime();

                            // If start is inside period but end is not
                            if((startBlock < startTime && startTime < endBlock) && endBlock < endTime) {
                                schedule[index].start = endPeriod;
                            }

                            // If end is inside period but start is not
                            if((startBlock < endTime && endTime < endBlock) && startTime < startBlock) {
                                schedule[index].end = startPeriod;
                            }

                            // If event is completely inside the event we're trying to add
                            if((startBlock < startTime && startTime < endBlock) && (startBlock < endTime && endTime < endBlock)) {
                                schedule.splice(index, 1);
                            }

                            // If the event is the exact same
                            if(startBlock === startTime && endBlock === endTime) {
                                schedule.splice(index, 1);
                            }

                            // If the event we're trying to add is completely inside the event
                            if(startBlock < startTime && endTime < endBlock) {
                                // Split this old event into two
                                var oldEnd = schedule[index].end;
                                schedule[index].end = startPeriod;

                                // Create second block and push it to the schedule; We will order later
                                var newBlock = schedule[index];
                                newBlock.start = endPeriod;
                                newBlock.end   = oldEnd;

                                schedule.push(newBlock);
                            }
                        });
                        schedule.push(period);
                    }
                }
            });

            // Order schedule
            schedule.sort(function(a, b) {
                var aStart = new Date(a.start).getTime();
                var bStart = new Date(b.start).getTime();
                return aStart - bStart;
            });

            return {
                'day': scheduleDay,
                'schedule': schedule,
                'events'  : events
            };
        } else {
            return {};
        }
    }

    verifyFeed(url, function(success, message, raw) {
        if(success) {
            that.success = true;
            that.message = message;
            that.parsed = ical.parseICS(raw);
        } else {
            that.success = false;
            that.message = message;
            that.parsed = null;
        }
        if(typeof callback === 'function') callback(that);
    });

}

module.exports.verifyURL    = verifyURL;
module.exports.scheduleFeed = scheduleFeed;
<commit_msg>Finish verifyURL for portal.js<commit_after>/**
 * @file Reads calendar feed to determine schedule / day and other important events
 * @module portal
 */

var config = require(__dirname + '/config.js');

var _           = require('underscore');
var ical        = require('ical');
var querystring = require('querystring');
var request     = require('request');
var url         = require('url');

// URL Calendars come from
var urlPrefix = 'https://micds.myschoolapp.com/podium/feed/iCal.aspx?q=';
// RegEx to test if calendar summary is a valid Day Rotation
var validDayRotation = /^Day [1-6] \((US|MS)\)$/;

/**
 * Makes sure a given url is valid and it points to a Canvas calendar feed
 * @function verifyURL
 *
 * @param {string} url - URI to iCal feed
 * @param {verifyURLCallback} callback - Callback
 */

/**
 * Returns whether url is valid or not
 * @callback verifyURLCallback
 *
 * @param {Object} err - Null if success, error object if failure.
 * @param {Boolean|string} isValid - True if valid URL, string describing problem if not valid. Null if error.
 * @param {string} url - Valid and formatted URL to our likings. Null if error or invalid url.
 */

function verifyURL(portalURL, callback) {

    if(typeof callback !== 'function') return;

    if(typeof portalURL !== 'string') {
        callback(new Error('Invalid URL!'), null, null);
        return;
    }

    // Parse URL first
    var parsedURL = url.parse(portalURL);
    var queries = querystring.parse(parsedURL.query);

    if(typeof queries.q !== 'string') {
        callback(null, 'URL does not contain calendar ID!', null);
        return;
    }

    var validURL = urlPrefix + queries.q;

    // Not lets see if we can actually get any data from here
    request(validURL, function(err, response, body) {
        if(err) {
            callback(new Error('There was a problem fetching portal data from the URL!'), null, null);
            return;
        }
        if(response.statusCode !== 200) {
            callback(null, 'Invalid URL!', null);
            return;
        }

        var data = ical.parseICS(body);

        // School Portal does not give a 404 if calendar is invalid. Instead, it gives an empty calendar.
        // Unlike Canvas, the portal is guaranteed to contain some sort of data within a span of a year.
        if(_.isEmpty(data)) {
            callback(null, 'Invalid URL!', null);
            return;
        }

        // Look through every 'Day # (US/MS)' andd see how many events there are
        var dayDates = {};
        for(var eventUid in data) {
            var calEvent = data[eventUid];
            // If event doesn't have a summary, skip
            if(typeof calEvent.summary !== 'string') continue;

            // See if valid day
            if(validDayRotation.test(calEvent.summary)) {
                // Get actual day
                var day = calEvent.summary.match(/[1-6]/)[0];
                // Get date
                var start = new Date(calEvent.start);

                // Add to dayDates object
                if(typeof dayDates[day] === 'undefined') {
                    dayDates[day] = [];
                }
                dayDates[day].push({
                    year : start.getFullYear(),
                    month: start.getMonth() + 1,
                    day  : start.getDate()
                });
            }
        }

        if(_.isEmpty(dayDates)) {
            callback(null, 'The calendar does not contain the information we need! Make sure you\'re copying your personal calendar!', null);
            return;
        }

        callback(null, true, validURL);

    });
}

/**
 * Queries a person's Portal RSS feed to get schedule and stuff
 * @function scheduleFeed
 *
 * @param {string} url - URL to iCal feed
 * @param {scheduleFeedCallback} callback - Callback after schedule is parsed
 */

/**
 * Callback after schedule feed is parsed
 * @callback scheduleFeedCallback
 *
 * @param {Object} that - Object reference to the schedule feed
 */

function scheduleFeed(url, callback) {
    var that = this;
    that.success = null;

    // Parses the schedule for given date and returns arrays
    this.getSchedule = function(day, month, year) {
        if(this.success) {
            // Default date
            var current = new Date();
            var schedule = [];
            var events = [];
            var scheduleDay;

            day = day || current.getDate();
            month = month || current.getMonth();
            year = year || current.getFullYear();

            // Get schedule from feed
            _.each(this.parsed, function(event, uid) {
                var eventDate = new Date(event.start);
                if(eventDate.getDate() === day && eventDate.getMonth() === month && eventDate.getFullYear() === year) {

                    // Check if it's an all-day event
                    if(eventDate.getSeconds() === 0 && eventDate.getMinutes() === 0 && eventDate.getHours() === 0) {
                        var period = {
                            'name': event.summary,
                            'description': event.description,
                            'location': event.location
                        };

                        if(/^Day [1-6]/.test(period.name)) {
                            scheduleDay = parseInt(period.name.match(/[1-6]/)[0]);
                        } else {
                            events.push(period);
                        }

                    } else {
                        var startPeriod = new Date(event.start);
                        var endPeriod   = new Date(event.end);

                        var period = {
                            'start': startPeriod,
                            'end'  : endPeriod,
                            'class': event.summary,
                            'description': event.description,
                            'location': event.location
                        };

                        // Overlap existing events
                        var startTime = startPeriod.getTime();
                        var endTime   = endPeriod.getTime();

                        schedule.forEach(function(block, index) {

                            var startBlock = block.start.getTime();
                            var endBlock   = block.end.getTime();

                            // If start is inside period but end is not
                            if((startBlock < startTime && startTime < endBlock) && endBlock < endTime) {
                                schedule[index].start = endPeriod;
                            }

                            // If end is inside period but start is not
                            if((startBlock < endTime && endTime < endBlock) && startTime < startBlock) {
                                schedule[index].end = startPeriod;
                            }

                            // If event is completely inside the event we're trying to add
                            if((startBlock < startTime && startTime < endBlock) && (startBlock < endTime && endTime < endBlock)) {
                                schedule.splice(index, 1);
                            }

                            // If the event is the exact same
                            if(startBlock === startTime && endBlock === endTime) {
                                schedule.splice(index, 1);
                            }

                            // If the event we're trying to add is completely inside the event
                            if(startBlock < startTime && endTime < endBlock) {
                                // Split this old event into two
                                var oldEnd = schedule[index].end;
                                schedule[index].end = startPeriod;

                                // Create second block and push it to the schedule; We will order later
                                var newBlock = schedule[index];
                                newBlock.start = endPeriod;
                                newBlock.end   = oldEnd;

                                schedule.push(newBlock);
                            }
                        });
                        schedule.push(period);
                    }
                }
            });

            // Order schedule
            schedule.sort(function(a, b) {
                var aStart = new Date(a.start).getTime();
                var bStart = new Date(b.start).getTime();
                return aStart - bStart;
            });

            return {
                'day': scheduleDay,
                'schedule': schedule,
                'events'  : events
            };
        } else {
            return {};
        }
    }

    verifyFeed(url, function(success, message, raw) {
        if(success) {
            that.success = true;
            that.message = message;
            that.parsed = ical.parseICS(raw);
        } else {
            that.success = false;
            that.message = message;
            that.parsed = null;
        }
        if(typeof callback === 'function') callback(that);
    });

}

module.exports.verifyURL    = verifyURL;
module.exports.scheduleFeed = scheduleFeed;
<|endoftext|>


<commit_before>const html = require('choo/html')
const css = require('csjs-inject')
const C = require('../../lib/constants')

const isString = require('lodash/isString')
const intersection = require('lodash/intersection')

const height = 200
const padding = 5

module.exports = (state, prev, send) => {
  const style = css`

  .detail {
    position: absolute;
    bottom: 70px;
    height: ${state.detailshown ? height : 0}px;
    width: 100%;
    padding: ${state.detailshown ? padding : 0}px;
    margin: 0;
    flex-direction: row;
    overflow-y: hidden;
    background: ${C.GREYBLUE};
    font-family: Aleo-Regular;
    font-size: 16px;
    color: ${C.LIGHTGREY};
  }

  .column {
    width: 50%;
    max-width: 50%;
    overflow: hidden;
    flex-direction: column;
    justify-content: space-between;
  }

  .paper {
    width: 70%;
    overflow: auto;
    flex-direction: column;
    justify-content: space-between;
    padding: 10px;
  }

  .row {
    flex-direction: row;
    justify-content: space-between;
  }

  .datum {
    position: relative;
    padding: 5px;
    margin-bottom: 16px;
  }

  .title {
    display: block;
    font-size: 130%;
    width: 100%;
  }

  .abstract {
    padding: 0;
    margin: 5px;
    font-family: CooperHewitt-Light;
    line-height: 18px;
  }

  .author {
    font-family: CooperHewitt-MediumItalic;
    width: 100%;
  }

  .date {
    width: 100%;
    justify-content: flex-end;
    font-family: CooperHewitt-Medium;
  }

  .wrapper {
    flex-direction: column;
    justify-content: space-between;
    width: 100%;
    height: calc(100% - ${padding}px);
    position: relative;
  }

  .empty {
    margin: 50px;
    font-size: 2em;
    font-family: Aleo-Light;
  }

  .quart {
    max-width 24%;
    position: relative;
  }

  `

  function getcontent (state) {
    const hasresults = state.results.length > 0
    if (!hasresults) return blank()

    if (state.selection.papers.length === 1) {
      const id = state.selection.papers[0]
      const paper = state.results.find((result) => {
        return result.document.identifier[0].id === id
      })

      return singlepaper(paper, style, state, prev, send)
    } else if (state.selection.papers.length > 1) {
      const ids = state.selection.papers
      const papers = state.results.filter((result) => {
        const id = result.document.identifier[0].id
        return ids.indexOf(id) > -1
      })

      return multipaper(papers, style, state, prev, send)
    } else {
      return blank()
    }
  }

  function blank () {
    return html`

No paper selected.

    `
  }

  return html`<div class="${style.detail}">${getcontent(state)}</div>`
}

function renderDate (date) {
  return `${date.day}/${date.month}/${date.year}`
}

function renderAuthor (author) {
  const authors = isString(author)
    ? author.split(/,\s?/)
    : author.map(a => a.surname)
  if (authors.length === 1) {
    return html`<span>${authors[0]}`
  } else if (authors.length < 4) {
    return html`

        ${authors.slice(0, -1).join(', ') + ' and ' + authors.slice(-1)[0]}

    `
  } else {
    return html`<span>${authors[0]} et al.</span>`
  }
}

function renderAbstract (abstract) {
  return html('' + abstract + '')
}

function singlepaper (paper, style, state, prev, send) {
  if (!paper) return null
  const doc = paper.document

  return html`

          ${doc.title}

${renderAuthor(doc.author)}

            Published:
            ${doc.date ? renderDate(doc.date) : 'unknown'}

          ${renderAbstract(doc.abstract)}

        ${require('./detail_actions')(state, prev, send)}
        ${require('./detail_tags')(doc.tags, state, prev, send)}

  `
}

function tags (papers) {
  return intersection(...(papers.map((paper) => paper.document.tags))) || []
}

function multipaper (papers, style, state, prev, send) {
  return html`

        ${require('./detail_multi_terms')(papers, state, prev, send)}

        ${require('./detail_multi_authors')(papers, state, prev, send)}

        ${require('./detail_multi_dates')(papers, state, prev, send)}

        ${require('./detail_tags')(tags(papers), state, prev, send)}

  `
}
<commit_msg>Fix article detail metadata layout<commit_after>const html = require('choo/html')
const css = require('csjs-inject')
const C = require('../../lib/constants')

const isString = require('lodash/isString')
const intersection = require('lodash/intersection')

const height = 200
const padding = 5

module.exports = (state, prev, send) => {
  const style = css`

  .detail {
    position: absolute;
    bottom: 70px;
    height: ${state.detailshown ? height : 0}px;
    width: 100%;
    padding: ${state.detailshown ? padding : 0}px;
    margin: 0;
    flex-direction: row;
    overflow-y: hidden;
    background: ${C.GREYBLUE};
    font-family: Aleo-Regular;
    font-size: 16px;
    color: ${C.LIGHTGREY};
  }

  .column {
    width: 50%;
    max-width: 50%;
    overflow: hidden;
    flex-direction: column;
    justify-content: space-between;
  }

  .paper {
    width: 70%;
    overflow: auto;
    flex-direction: column;
    justify-content: space-between;
    padding: 10px;
  }

  .row {
    flex-direction: row;
    justify-content: space-between;
  }

  .datum {
    position: relative;
    padding: 5px;
    margin-bottom: 16px;
  }

  .title {
    display: block;
    font-size: 130%;
    width: 100%;
  }

  .meta {
    display: block;
    width: 100%;
  }

  .abstract {
    padding: 0;
    margin: 5px;
    font-family: CooperHewitt-Light;
    line-height: 18px;
  }

  .author {
    float: left;
    font-family: CooperHewitt-MediumItalic;
  }

  .date {
    float: right;
    font-family: CooperHewitt-Medium;
  }

  .wrapper {
    flex-direction: column;
    justify-content: space-between;
    width: 100%;
    height: calc(100% - ${padding}px);
    position: relative;
  }

  .empty {
    margin: 50px;
    font-size: 2em;
    font-family: Aleo-Light;
  }

  .quart {
    max-width 24%;
    position: relative;
  }

  `

  function getcontent (state) {
    const hasresults = state.results.length > 0
    if (!hasresults) return blank()

    if (state.selection.papers.length === 1) {
      const id = state.selection.papers[0]
      const paper = state.results.find((result) => {
        return result.document.identifier[0].id === id
      })

      return singlepaper(paper, style, state, prev, send)
    } else if (state.selection.papers.length > 1) {
      const ids = state.selection.papers
      const papers = state.results.filter((result) => {
        const id = result.document.identifier[0].id
        return ids.indexOf(id) > -1
      })

      return multipaper(papers, style, state, prev, send)
    } else {
      return blank()
    }
  }

  function blank () {
    return html`

No paper selected.

    `
  }

  return html`<div class="${style.detail}">${getcontent(state)}</div>`
}

function renderDate (date) {
  return `${date.day}/${date.month}/${date.year}`
}

function renderAuthor (author) {
  const authors = isString(author)
    ? author.split(/,\s?/)
    : author.map(a => a.surname)
  if (authors.length === 1) {
    return html`<span>${authors[0]}`
  } else if (authors.length < 4) {
    return html`

        ${authors.slice(0, -1).join(', ') + ' and ' + authors.slice(-1)[0]}

    `
  } else {
    return html`<span>${authors[0]} et al.</span>`
  }
}

function renderAbstract (abstract) {
  return html('' + abstract + '')
}

function singlepaper (paper, style, state, prev, send) {
  if (!paper) return null
  const doc = paper.document

  return html`

          ${doc.title}

            ${renderAuthor(doc.author)}

            Published: ${doc.date ? renderDate(doc.date) : 'unknown'}

          ${renderAbstract(doc.abstract)}

        ${require('./detail_actions')(state, prev, send)}
        ${require('./detail_tags')(doc.tags, state, prev, send)}

  `
}

function tags (papers) {
  return intersection(...(papers.map((paper) => paper.document.tags))) || []
}

function multipaper (papers, style, state, prev, send) {
  return html`

        ${require('./detail_multi_terms')(papers, state, prev, send)}

        ${require('./detail_multi_authors')(papers, state, prev, send)}

        ${require('./detail_multi_dates')(papers, state, prev, send)}

        ${require('./detail_tags')(tags(papers), state, prev, send)}

  `
}
<|endoftext|>


<commit_before>/* global on:false, generateRowID:false */

import { ABILITIES } from './constants';
import { getSetItems, getIntValue, isUndefinedOrEmpty, addArithmeticOperator, getAbilityMod, numberWithCommas, exists, updateHD } from './utilities';

export class Npc {
  updateType() {
    getSetItems('npc.updateType', {
      collectionArray: ['type'],
      callback: (v, finalSetAttrs) => {
        if (v.type) {
          finalSetAttrs.type = v.type.toLowerCase();
        }
      },
    });
  }
  updateSize() {
    getSetItems('npc.updateSize', {
      collectionArray: ['size'],
      callback: (v, finalSetAttrs) => {
        const creatureSize = v.size || 'Large';
        const sizeToHdSize = {
          Tiny: 4,
          Small: 6,
          Medium: 8,
          Large: 10,
          Huge: 12,
          Gargantuan: 20,
        };
        finalSetAttrs.hit_die = `d${sizeToHdSize[creatureSize]}`;
      },
    });
  }
  updateAC() {
    getSetItems('npc.updateAC', {
      collectionArray: ['ac_srd', 'ac', 'ac_note', 'dexterity_mod'],
      callback: (v, finalSetAttrs) => {
        if (exists(v.ac_srd)) {
          const match = v.ac_srd.match(/(\d+)\s?(.*)/);
          if (match && match[1]) {
            finalSetAttrs.AC = match[1];
          }
          if (match && match[2]) {
            finalSetAttrs.ac_note = match[2].replace(/\(|\)/g, '');
          }
        }
      },
    });
  }
  switchTo() {
    getSetItems('npc.switchTo', {
      collectionArray: ['is_npc', 'size'],
      callback: (v, finalSetAttrs) => {
        const isNPC = getIntValue(v.is_npc) === 1;

        if (isNPC && isUndefinedOrEmpty(v.size)) {
          finalSetAttrs.size = 'Large';
        }

        if (isNPC) {
          finalSetAttrs.hit_dice_output_option = '/w GM';
        } else {
          finalSetAttrs.hit_dice_output_option = '';
        }
      },
    });
  }
  updateChallenge() {
    const xpPerChallenge = {
      0: 0,
      '1/8': 25,
      '1/4': 50,
      '1/2': 100,
      1: 200,
      2: 450,
      3: 700,
      4: 1100,
      5: 1800,
      6: 2300,
      7: 2900,
      8: 3900,
      9: 5000,
      10: 5900,
      11: 7200,
      12: 8400,
      13: 10000,
      14: 11500,
      15: 13000,
      16: 15000,
      17: 18000,
      18: 20000,
      19: 22000,
      20: 25000,
      21: 33000,
      22: 41000,
      23: 50000,
      24: 62000,
      25: 75000,
      26: 90000,
      27: 105000,
      28: 120000,
      29: 135000,
      30: 155000,
    };

    getSetItems('npc.updateChallenge', {
      collectionArray: ['challenge', 'xp'],
      callback: (v, finalSetAttrs) => {
        const challenge = v.challenge;

        finalSetAttrs.xp = xpPerChallenge[challenge];
        finalSetAttrs.xp_readable = numberWithCommas(finalSetAttrs.xp);

        finalSetAttrs.level = challenge;
        if (finalSetAttrs.level < 1) {
          finalSetAttrs.level = 1;
        }
      },
    });
  }
  updateHPFromSRD() {
    getSetItems('npc.updateHPFromSRD', {
      collectionArray: ['hp_srd', 'constitution', 'constitution_mod', 'constitution_bonus', 'global_ability_bonus'],
      callback: (v, finalSetAttrs) => {
        if (exists(v.hp_srd)) {
          const match = v.hp_srd.match(/\((\d+)d(\d+)(?:\s?(\+|\-)\s?(\d+))?\)/i);
          if (!match || !match[1] || !match[2]) {
            console.warn('Character doesn\'t have valid HP/HD format');
          } else {
            const hdNum = getIntValue(match[1]);

            let conMod = getIntValue(v.constitution_mod);
            if (!conMod) {
              const conScore = getIntValue(v.constitution);
              const conBonus = getIntValue(v.constitution_bonus);
              const globalAbilityBonus = getIntValue(v.global_ability_bonus);

              conMod = getAbilityMod((conScore + conBonus + globalAbilityBonus));
            }

            finalSetAttrs.hit_dice = hdNum;
            finalSetAttrs.hit_die = `d${getIntValue(match[2])}`;

            const hpExpectedBonus = hdNum * conMod;
            const hpBonusSign = match[3];
            const hpBonus = getIntValue(match[4]);
            if (hpBonus !== hpExpectedBonus) {
              if (hpBonusSign === '-') {
                finalSetAttrs.hp_extra = hpBonus + hpExpectedBonus;
              } else {
                finalSetAttrs.hp_extra = hpBonus - hpExpectedBonus;
              }
            }
          }
        }
      },
    });
  }
  updateHP() {
    getSetItems('npc.updateHP', {
      collectionArray: ['HP', 'HP_max', 'hp_formula', 'hit_dice', 'hit_die', 'hp_extra', 'constitution_mod'],
      callback: (v, finalSetAttrs) => {
        const hdNum = getIntValue(v.hit_dice);
        const hdSize = getIntValue(v.hit_die.replace('d', ''));
        const hdAverage = (hdSize / 2) + 0.5;
        let hpFormula = `${hdNum}d${hdSize}`;
        const conMod = getIntValue(v.constitution_mod);
        let totalHP = Math.floor(hdNum * hdAverage);
        let amount;

        if (conMod !== 0) {
          const bonusHP = hdNum * conMod;
          totalHP += bonusHP;
          hpFormula += addArithmeticOperator(hpFormula, bonusHP);
        }

        if (exists(v.hp_extra)) {
          const regex = (/(?:(\+|\-)\s?)?(\d+)(?:d(\d+))?/gi);
          let splitFormula;

          while ((splitFormula = regex.exec(v.hp_extra)) !== null) {
            if (!splitFormula || !splitFormula[2]) {
              console.warn('Character doesn\'t have valid hp formula');
            } else {
              amount = 0;

              if (!splitFormula[3]) {
                amount = getIntValue(splitFormula[2]);
              } else {
                const extraHdNum = getIntValue(splitFormula[2]);
                const extraHdSize = getIntValue(splitFormula[3]);
                const extraHdAverage = (extraHdSize / 2) + 0.5;
                amount = Math.floor(extraHdNum * extraHdAverage);
              }

              if (!splitFormula[1] || splitFormula[1] === '+') {
                totalHP += amount;
                hpFormula += ` + ${amount}`;
              } else if (splitFormula[1] === '-') {
                totalHP -= amount;
                hpFormula += ` - ${amount}`;
              }
            }
            v.hp_extra.toString().replace(splitFormula[0], '');
          }
        }

        if (totalHP) {
          finalSetAttrs.HP = totalHP;
          finalSetAttrs.HP_max = totalHP;
          finalSetAttrs.hp_formula = hpFormula;
        }
      },
    });
  }
  updateHD() {
    const hd = {
      d20: 0,
      d12: 0,
      d10: 0,
      d8: 0,
      d6: 0,
      d4: 0,
      d2: 0,
      d0: 0,
    };
    const collectionArray = ['hit_dice', 'hit_die'];
    for (const key in hd) {
      if (hd.hasOwnProperty(key)) {
        collectionArray.push(`hd_${key}_max`);
        collectionArray.push(`hd_${key}_query`);
        collectionArray.push(`hd_${key}_toggle`);
      }
    }
    getSetItems('npc.updateHD', {
      collectionArray,
      callback: (v, finalSetAttrs) => {
        const hdNum = getIntValue(v.hit_dice);
        const hdSize = v.hit_die;

        if (hdNum && hdSize) {
          hd[hdSize] = hdNum;

          updateHD(v, finalSetAttrs, hd);
        }
      },
    });
  }
  parseSRDContentSection(content, finalSetAttrs, title, name) {
    const re = /@(.*)@:\s([^@]+)/gi;
    let match;
    let section;

    if (content.indexOf(title) !== -1) {
      const contentSplit = content.split(`${title}\n`);
      section = contentSplit[1];
      content = contentSplit[0];
    }
    if (exists(section)) {
      if (name === 'legendaryaction') {
        const legendaryActionsMatch = section.match(/Can take (\d+) Legendary Actions/i);
        if (legendaryActionsMatch && legendaryActionsMatch[1]) {
          finalSetAttrs.legendary_action_amount = legendaryActionsMatch[1];
        }
      }

      while ((match = re.exec(section.replace(/\*\*/g, '@'))) !== null) {
        if (match && match[1] && match[2]) {
          const repeatingString = `repeating_${name}_${generateRowID()}_`;
          finalSetAttrs[`${repeatingString}name`] = match[1];
          const text = match[2].trim();
          if (name === 'trait') {
            finalSetAttrs[`${repeatingString}display_text`] = text;
          }
          finalSetAttrs[`${repeatingString}freetext`] = text;
        } else {
          console.warn(`Character doesn\'t have a valid ${name} format`);
        }
      }
    }
    return content;
  }
  setDefaultAbility(v, finalSetAttrs) {
    const abilityScores = [getIntValue(v.intelligence), getIntValue(v.wisdom), getIntValue(v.charisma)];
    const highestAbilityScore = Math.max.apply(Math, abilityScores);
    let highestAbilityName = 'intelligence';

    if (highestAbilityScore === abilityScores[1]) {
      highestAbilityName = 'wisdom';
    } else if (highestAbilityScore === abilityScores[2]) {
      highestAbilityName = 'charisma';
    }

    finalSetAttrs.default_ability = highestAbilityName;
  }
  updateContent() {
    const collectionArray = ['content_srd'];
    for (const ability of ABILITIES) {
      collectionArray.push(ability);
    }

    getSetItems('npc.updateContent', {
      collectionArray,
      callback: (v, finalSetAttrs) => {
        let content = v.content_srd;
        let regionalEffects;
        let lairActions;
        let repeatingString;

        this.setDefaultAbility(v, finalSetAttrs);

        if (exists(content)) {
          if (content.indexOf('Regional Effects') !== -1) {
            const regionalEffectsSplit = content.split(/Regional Effects\n/);
            regionalEffects = regionalEffectsSplit[1];
            content = regionalEffectsSplit[0];
          }
          if (exists(regionalEffects)) {
            const regionalEffectsList = regionalEffects.split(/\*\*/);
            regionalEffectsList.slice(1, -1).forEach((regionalEffect) => {
              repeatingString = `repeating_regionaleffect_${generateRowID()}_`;
              finalSetAttrs[`${repeatingString}freetext`] = regionalEffect.trim();
            });
            finalSetAttrs.regional_effects_fade = regionalEffectsList.slice(-1)[0];
          }
          if (content.indexOf('Lair Actions') !== -1) {
            const lairActionsSplit = content.split(/Lair Actions\n/);
            lairActions = lairActionsSplit[1];
            content = lairActionsSplit[0];
          }
          if (exists(lairActions)) {
            lairActions.split(/\*\*/).slice(1).forEach((lairAction) => {
              repeatingString = `repeating_lairaction_${generateRowID()}_`;
              finalSetAttrs[`${repeatingString}freetext`] = lairAction.trim();
            });
          }

          content = this.parseSRDContentSection(content, finalSetAttrs, 'Legendary Actions', 'legendaryaction');
          content = this.parseSRDContentSection(content, finalSetAttrs, 'Reactions', 'reaction');
          content = this.parseSRDContentSection(content, finalSetAttrs, 'Actions', 'action');
          this.parseSRDContentSection(content, finalSetAttrs, 'Traits', 'trait');
        }
      },
    });
  }
  updateSenses() {
    getSetItems('npc.updateSenses', {
      collectionArray: ['senses'],
      callback: (v, finalSetAttrs) => {
        if (v.senses) {
          finalSetAttrs.senses_exist = 1;
          finalSetAttrs.senses = v.senses.toLowerCase();
        } else {
          finalSetAttrs.senses_exist = 0;
        }
      },
    });
  }
  updateLanguages() {
    getSetItems('npc.updateLanguages', {
      collectionArray: ['languages', 'languages_exist', 'languages_chat_var'],
      callback: (v, finalSetAttrs) => {
        finalSetAttrs.languages_exist = 0;
        finalSetAttrs.languages_chat_var = '';

        if (v.languages) {
          finalSetAttrs.languages_exist = 1;
          finalSetAttrs.languages_chat_var = '{{Languages=@{languages}}}';
        }
      },
    });
  }
  updateSpeed() {
    getSetItems('npc.updateSpeed', {
      collectionArray: ['npc_speed'],
      callback: (v, finalSetAttrs) => {
        finalSetAttrs.npc_speed = v.npc_speed.toLowerCase();
        const match = finalSetAttrs.npc_speed.match(/^\s*(\d+)\s*ft/);
        if (match && match[1]) {
          finalSetAttrs.speed = match[1];
        }
      },
    });
  }
  updateACNote() {
    getSetItems('npc.updateACNote', {
      collectionArray: ['ac_note'],
      callback: (v, finalSetAttrs) => {
        finalSetAttrs.ac_note = v.ac_note.toLowerCase();
      },
    });
  }
  setup() {
    on('change:type', () => {
      this.updateType();
    });
    on('change:size', () => {
      this.updateSize();
    });
    on('change:ac_srd', () => {
      this.updateAC();
    });
    on('change:is_npc', () => {
      this.switchTo();
    });
    on('change:challenge', () => {
      this.updateChallenge();
    });
    on('change:hp_srd', () => {
      this.updateHPFromSRD();
    });
    on('change:hit_dice change:hit_die change:hp_extra change:constitution_mod', () => {
      this.updateHP();
    });
    on('change:hit_dice change:hit_die', () => {
      this.updateHD();
    });
    on('change:content_srd', () => {
      this.updateContent();
    });
    on('change:senses', () => {
      this.updateSenses();
    });
    on('change:languages', () => {
      this.updateLanguages();
    });
    on('change:npc_speed', () => {
      this.updateSpeed();
    });
    on('change:ac_note', () => {
      this.updateACNote();
    });
  }
}
<commit_msg>remove unnecessary fade<commit_after>/* global on:false, generateRowID:false */

import { ABILITIES } from './constants';
import { getSetItems, getIntValue, isUndefinedOrEmpty, addArithmeticOperator, getAbilityMod, numberWithCommas, exists, updateHD } from './utilities';

export class Npc {
  updateType() {
    getSetItems('npc.updateType', {
      collectionArray: ['type'],
      callback: (v, finalSetAttrs) => {
        if (v.type) {
          finalSetAttrs.type = v.type.toLowerCase();
        }
      },
    });
  }
  updateSize() {
    getSetItems('npc.updateSize', {
      collectionArray: ['size'],
      callback: (v, finalSetAttrs) => {
        const creatureSize = v.size || 'Large';
        const sizeToHdSize = {
          Tiny: 4,
          Small: 6,
          Medium: 8,
          Large: 10,
          Huge: 12,
          Gargantuan: 20,
        };
        finalSetAttrs.hit_die = `d${sizeToHdSize[creatureSize]}`;
      },
    });
  }
  updateAC() {
    getSetItems('npc.updateAC', {
      collectionArray: ['ac_srd', 'ac', 'ac_note', 'dexterity_mod'],
      callback: (v, finalSetAttrs) => {
        if (exists(v.ac_srd)) {
          const match = v.ac_srd.match(/(\d+)\s?(.*)/);
          if (match && match[1]) {
            finalSetAttrs.AC = match[1];
          }
          if (match && match[2]) {
            finalSetAttrs.ac_note = match[2].replace(/\(|\)/g, '');
          }
        }
      },
    });
  }
  switchTo() {
    getSetItems('npc.switchTo', {
      collectionArray: ['is_npc', 'size'],
      callback: (v, finalSetAttrs) => {
        const isNPC = getIntValue(v.is_npc) === 1;

        if (isNPC && isUndefinedOrEmpty(v.size)) {
          finalSetAttrs.size = 'Large';
        }

        if (isNPC) {
          finalSetAttrs.hit_dice_output_option = '/w GM';
        } else {
          finalSetAttrs.hit_dice_output_option = '';
        }
      },
    });
  }
  updateChallenge() {
    const xpPerChallenge = {
      0: 0,
      '1/8': 25,
      '1/4': 50,
      '1/2': 100,
      1: 200,
      2: 450,
      3: 700,
      4: 1100,
      5: 1800,
      6: 2300,
      7: 2900,
      8: 3900,
      9: 5000,
      10: 5900,
      11: 7200,
      12: 8400,
      13: 10000,
      14: 11500,
      15: 13000,
      16: 15000,
      17: 18000,
      18: 20000,
      19: 22000,
      20: 25000,
      21: 33000,
      22: 41000,
      23: 50000,
      24: 62000,
      25: 75000,
      26: 90000,
      27: 105000,
      28: 120000,
      29: 135000,
      30: 155000,
    };

    getSetItems('npc.updateChallenge', {
      collectionArray: ['challenge', 'xp'],
      callback: (v, finalSetAttrs) => {
        const challenge = v.challenge;

        finalSetAttrs.xp = xpPerChallenge[challenge];
        finalSetAttrs.xp_readable = numberWithCommas(finalSetAttrs.xp);

        finalSetAttrs.level = challenge;
        if (finalSetAttrs.level < 1) {
          finalSetAttrs.level = 1;
        }
      },
    });
  }
  updateHPFromSRD() {
    getSetItems('npc.updateHPFromSRD', {
      collectionArray: ['hp_srd', 'constitution', 'constitution_mod', 'constitution_bonus', 'global_ability_bonus'],
      callback: (v, finalSetAttrs) => {
        if (exists(v.hp_srd)) {
          const match = v.hp_srd.match(/\((\d+)d(\d+)(?:\s?(\+|\-)\s?(\d+))?\)/i);
          if (!match || !match[1] || !match[2]) {
            console.warn('Character doesn\'t have valid HP/HD format');
          } else {
            const hdNum = getIntValue(match[1]);

            let conMod = getIntValue(v.constitution_mod);
            if (!conMod) {
              const conScore = getIntValue(v.constitution);
              const conBonus = getIntValue(v.constitution_bonus);
              const globalAbilityBonus = getIntValue(v.global_ability_bonus);

              conMod = getAbilityMod((conScore + conBonus + globalAbilityBonus));
            }

            finalSetAttrs.hit_dice = hdNum;
            finalSetAttrs.hit_die = `d${getIntValue(match[2])}`;

            const hpExpectedBonus = hdNum * conMod;
            const hpBonusSign = match[3];
            const hpBonus = getIntValue(match[4]);
            if (hpBonus !== hpExpectedBonus) {
              if (hpBonusSign === '-') {
                finalSetAttrs.hp_extra = hpBonus + hpExpectedBonus;
              } else {
                finalSetAttrs.hp_extra = hpBonus - hpExpectedBonus;
              }
            }
          }
        }
      },
    });
  }
  updateHP() {
    getSetItems('npc.updateHP', {
      collectionArray: ['HP', 'HP_max', 'hp_formula', 'hit_dice', 'hit_die', 'hp_extra', 'constitution_mod'],
      callback: (v, finalSetAttrs) => {
        const hdNum = getIntValue(v.hit_dice);
        const hdSize = getIntValue(v.hit_die.replace('d', ''));
        const hdAverage = (hdSize / 2) + 0.5;
        let hpFormula = `${hdNum}d${hdSize}`;
        const conMod = getIntValue(v.constitution_mod);
        let totalHP = Math.floor(hdNum * hdAverage);
        let amount;

        if (conMod !== 0) {
          const bonusHP = hdNum * conMod;
          totalHP += bonusHP;
          hpFormula += addArithmeticOperator(hpFormula, bonusHP);
        }

        if (exists(v.hp_extra)) {
          const regex = (/(?:(\+|\-)\s?)?(\d+)(?:d(\d+))?/gi);
          let splitFormula;

          while ((splitFormula = regex.exec(v.hp_extra)) !== null) {
            if (!splitFormula || !splitFormula[2]) {
              console.warn('Character doesn\'t have valid hp formula');
            } else {
              amount = 0;

              if (!splitFormula[3]) {
                amount = getIntValue(splitFormula[2]);
              } else {
                const extraHdNum = getIntValue(splitFormula[2]);
                const extraHdSize = getIntValue(splitFormula[3]);
                const extraHdAverage = (extraHdSize / 2) + 0.5;
                amount = Math.floor(extraHdNum * extraHdAverage);
              }

              if (!splitFormula[1] || splitFormula[1] === '+') {
                totalHP += amount;
                hpFormula += ` + ${amount}`;
              } else if (splitFormula[1] === '-') {
                totalHP -= amount;
                hpFormula += ` - ${amount}`;
              }
            }
            v.hp_extra.toString().replace(splitFormula[0], '');
          }
        }

        if (totalHP) {
          finalSetAttrs.HP = totalHP;
          finalSetAttrs.HP_max = totalHP;
          finalSetAttrs.hp_formula = hpFormula;
        }
      },
    });
  }
  updateHD() {
    const hd = {
      d20: 0,
      d12: 0,
      d10: 0,
      d8: 0,
      d6: 0,
      d4: 0,
      d2: 0,
      d0: 0,
    };
    const collectionArray = ['hit_dice', 'hit_die'];
    for (const key in hd) {
      if (hd.hasOwnProperty(key)) {
        collectionArray.push(`hd_${key}_max`);
        collectionArray.push(`hd_${key}_query`);
        collectionArray.push(`hd_${key}_toggle`);
      }
    }
    getSetItems('npc.updateHD', {
      collectionArray,
      callback: (v, finalSetAttrs) => {
        const hdNum = getIntValue(v.hit_dice);
        const hdSize = v.hit_die;

        if (hdNum && hdSize) {
          hd[hdSize] = hdNum;

          updateHD(v, finalSetAttrs, hd);
        }
      },
    });
  }
  parseSRDContentSection(content, finalSetAttrs, title, name) {
    const re = /@(.*)@:\s([^@]+)/gi;
    let match;
    let section;

    if (content.indexOf(title) !== -1) {
      const contentSplit = content.split(`${title}\n`);
      section = contentSplit[1];
      content = contentSplit[0];
    }
    if (exists(section)) {
      if (name === 'legendaryaction') {
        const legendaryActionsMatch = section.match(/Can take (\d+) Legendary Actions/i);
        if (legendaryActionsMatch && legendaryActionsMatch[1]) {
          finalSetAttrs.legendary_action_amount = legendaryActionsMatch[1];
        }
      }

      while ((match = re.exec(section.replace(/\*\*/g, '@'))) !== null) {
        if (match && match[1] && match[2]) {
          const repeatingString = `repeating_${name}_${generateRowID()}_`;
          finalSetAttrs[`${repeatingString}name`] = match[1];
          const text = match[2].trim();
          if (name === 'trait') {
            finalSetAttrs[`${repeatingString}display_text`] = text;
          }
          finalSetAttrs[`${repeatingString}freetext`] = text;
        } else {
          console.warn(`Character doesn\'t have a valid ${name} format`);
        }
      }
    }
    return content;
  }
  setDefaultAbility(v, finalSetAttrs) {
    const abilityScores = [getIntValue(v.intelligence), getIntValue(v.wisdom), getIntValue(v.charisma)];
    const highestAbilityScore = Math.max.apply(Math, abilityScores);
    let highestAbilityName = 'intelligence';

    if (highestAbilityScore === abilityScores[1]) {
      highestAbilityName = 'wisdom';
    } else if (highestAbilityScore === abilityScores[2]) {
      highestAbilityName = 'charisma';
    }

    finalSetAttrs.default_ability = highestAbilityName;
  }
  updateContent() {
    const collectionArray = ['content_srd'];
    for (const ability of ABILITIES) {
      collectionArray.push(ability);
    }

    getSetItems('npc.updateContent', {
      collectionArray,
      callback: (v, finalSetAttrs) => {
        let content = v.content_srd;
        let regionalEffects;
        let lairActions;
        let repeatingString;

        this.setDefaultAbility(v, finalSetAttrs);

        if (exists(content)) {
          if (content.indexOf('Regional Effects') !== -1) {
            const regionalEffectsSplit = content.split(/Regional Effects\n/);
            regionalEffects = regionalEffectsSplit[1];
            content = regionalEffectsSplit[0];
          }
          if (exists(regionalEffects)) {
            const regionalEffectsList = regionalEffects.split(/\*\*/);
            regionalEffectsList.slice(1, -1).forEach((regionalEffect) => {
              repeatingString = `repeating_regionaleffect_${generateRowID()}_`;
              finalSetAttrs[`${repeatingString}freetext`] = regionalEffect.trim();
            });
          }
          if (content.indexOf('Lair Actions') !== -1) {
            const lairActionsSplit = content.split(/Lair Actions\n/);
            lairActions = lairActionsSplit[1];
            content = lairActionsSplit[0];
          }
          if (exists(lairActions)) {
            lairActions.split(/\*\*/).slice(1).forEach((lairAction) => {
              repeatingString = `repeating_lairaction_${generateRowID()}_`;
              finalSetAttrs[`${repeatingString}freetext`] = lairAction.trim();
            });
          }

          content = this.parseSRDContentSection(content, finalSetAttrs, 'Legendary Actions', 'legendaryaction');
          content = this.parseSRDContentSection(content, finalSetAttrs, 'Reactions', 'reaction');
          content = this.parseSRDContentSection(content, finalSetAttrs, 'Actions', 'action');
          this.parseSRDContentSection(content, finalSetAttrs, 'Traits', 'trait');
        }
      },
    });
  }
  updateSenses() {
    getSetItems('npc.updateSenses', {
      collectionArray: ['senses'],
      callback: (v, finalSetAttrs) => {
        if (v.senses) {
          finalSetAttrs.senses_exist = 1;
          finalSetAttrs.senses = v.senses.toLowerCase();
        } else {
          finalSetAttrs.senses_exist = 0;
        }
      },
    });
  }
  updateLanguages() {
    getSetItems('npc.updateLanguages', {
      collectionArray: ['languages', 'languages_exist', 'languages_chat_var'],
      callback: (v, finalSetAttrs) => {
        finalSetAttrs.languages_exist = 0;
        finalSetAttrs.languages_chat_var = '';

        if (v.languages) {
          finalSetAttrs.languages_exist = 1;
          finalSetAttrs.languages_chat_var = '{{Languages=@{languages}}}';
        }
      },
    });
  }
  updateSpeed() {
    getSetItems('npc.updateSpeed', {
      collectionArray: ['npc_speed'],
      callback: (v, finalSetAttrs) => {
        finalSetAttrs.npc_speed = v.npc_speed.toLowerCase();
        const match = finalSetAttrs.npc_speed.match(/^\s*(\d+)\s*ft/);
        if (match && match[1]) {
          finalSetAttrs.speed = match[1];
        }
      },
    });
  }
  updateACNote() {
    getSetItems('npc.updateACNote', {
      collectionArray: ['ac_note'],
      callback: (v, finalSetAttrs) => {
        finalSetAttrs.ac_note = v.ac_note.toLowerCase();
      },
    });
  }
  setup() {
    on('change:type', () => {
      this.updateType();
    });
    on('change:size', () => {
      this.updateSize();
    });
    on('change:ac_srd', () => {
      this.updateAC();
    });
    on('change:is_npc', () => {
      this.switchTo();
    });
    on('change:challenge', () => {
      this.updateChallenge();
    });
    on('change:hp_srd', () => {
      this.updateHPFromSRD();
    });
    on('change:hit_dice change:hit_die change:hp_extra change:constitution_mod', () => {
      this.updateHP();
    });
    on('change:hit_dice change:hit_die', () => {
      this.updateHD();
    });
    on('change:content_srd', () => {
      this.updateContent();
    });
    on('change:senses', () => {
      this.updateSenses();
    });
    on('change:languages', () => {
      this.updateLanguages();
    });
    on('change:npc_speed', () => {
      this.updateSpeed();
    });
    on('change:ac_note', () => {
      this.updateACNote();
    });
  }
}
<|endoftext|>


<commit_before>window.onload = function() {
    var s = document.getElementsByTagName('div'), ti;
    if (!s) return;
    big = { current: 0, forward: fwd, reverse: rev, go: go, length: s.length };
    function go(n) {
        big.current = n;
        var i = 1e3, e = s[n], t = parseInt(e.dataset.timeToNext || 0, 10);
        document.body.className = e.dataset.bodyclass || '';
        for (var k = 0; k < s.length; k++) s[k].style.display = 'none';
        e.style.display = 'inline';
        e.style.fontSize = i + 'px';
        if (e.firstChild && e.firstChild.nodeName === 'IMG') {
            document.body.style.backgroundImage = 'url("' + e.firstChild.src + '")';
            e.firstChild.style.display = 'none';
            if ('classList' in e) e.classList.add('imageText');
        } else {
            document.body.style.backgroundImage = '';
            document.body.style.backgroundColor = e.style.backgroundColor;
        }
        if (ti !== undefined) window.clearInterval(ti);
        if (t > 0) ti = window.setTimeout(fwd, (t * 1000));
        while ((e.offsetWidth > window.innerWidth ||
            e.offsetHeight > window.innerHeight) && i >= 0) {
            e.style.fontSize = (i -= 2) + 'px';
        }
        e.style.marginTop = ((window.innerHeight - e.offsetHeight) / 2) + 'px';
        if (window.location.hash !== n) window.location.hash = n;
        document.title = e.textContent || e.innerText;
    }
    document.onclick = function() { go(++big.current % (s.length)); };
    function fwd() { go(Math.min(s.length - 1, ++big.current)); }
    function rev() { go(Math.max(0, --big.current)); }
    document.onkeydown = function(e) {
        if (e.which === 39 || e.which === 34 || e.which === 40) fwd();
        if (e.which === 37 || e.which === 33 || e.which === 38) rev();
    };
    document.ontouchstart = function(e) {
        var x0 = e.changedTouches[0].pageX;
        document.ontouchend = function(e) {
            var x1 = e.changedTouches[0].pageX;
            if (x1 - x0 < 0) fwd();
            if (x1 - x0 > 0) rev();
        };
    };
    function parse_hash() {
        return Math.max(Math.min(s.length - 1,
            parseInt(window.location.hash.substring(1), 10)), 0);
    }
    if (window.location.hash) big.current = parse_hash() || big.current;
    window.onhashchange = function() {
        var c = parse_hash();
        if (c !== big.current) go(c);
    };
    go(big.current);
};
<commit_msg>Handle resize event<commit_after>window.onload = function() {
    var s = document.getElementsByTagName('div'), ti;
    if (!s) return;
    big = { current: 0, forward: fwd, reverse: rev, go: go, length: s.length };
    function resize() {
        var w = window.innerWidth, h = window.innerHeight, e = s[big.current];
        e.style.fontSize = h + 'px';
        for (var i = h-2; e.offsetWidth > w || e.offsetHeight > h; i-=2) {
            e.style.fontSize = i + 'px';
        }
        e.style.marginTop = ((h - e.offsetHeight) / 2) + 'px';
    }
    function go(n) {
        big.current = n;
        var e = s[n], t = parseInt(e.dataset.timeToNext || 0, 10);
        document.body.className = e.dataset.bodyclass || '';
        for (var k = 0; k < s.length; k++) s[k].style.display = 'none';
        e.style.display = 'inline';
        if (e.firstChild && e.firstChild.nodeName === 'IMG') {
            document.body.style.backgroundImage = 'url("' + e.firstChild.src + '")';
            e.firstChild.style.display = 'none';
            if ('classList' in e) e.classList.add('imageText');
        } else {
            document.body.style.backgroundImage = '';
            document.body.style.backgroundColor = e.style.backgroundColor;
        }
        if (ti !== undefined) window.clearInterval(ti);
        if (t > 0) ti = window.setTimeout(fwd, (t * 1000));
        resize();
        if (window.location.hash !== n) window.location.hash = n;
        document.title = e.textContent || e.innerText;
    }
    document.onclick = function() { go(++big.current % (s.length)); };
    function fwd() { go(Math.min(s.length - 1, ++big.current)); }
    function rev() { go(Math.max(0, --big.current)); }
    document.onkeydown = function(e) {
        if (e.which === 39 || e.which === 34 || e.which === 40) fwd();
        if (e.which === 37 || e.which === 33 || e.which === 38) rev();
    };
    document.ontouchstart = function(e) {
        var x0 = e.changedTouches[0].pageX;
        document.ontouchend = function(e) {
            var x1 = e.changedTouches[0].pageX;
            if (x1 - x0 < 0) fwd();
            if (x1 - x0 > 0) rev();
        };
    };
    function parse_hash() {
        return Math.max(Math.min(s.length - 1,
            parseInt(window.location.hash.substring(1), 10)), 0);
    }
    if (window.location.hash) big.current = parse_hash() || big.current;
    window.onhashchange = function() {
        var c = parse_hash();
        if (c !== big.current) go(c);
    };
    window.onresize = resize;
    go(big.current);
};
<|endoftext|>


<commit_before>#!/usr/bin/env node

var fs = require('fs')
var peervision = require('peervision')
var airswarm = require('airswarm')
var signalhub = require('signalhub')
var net = require('net')
var choppa = require('choppa')
var minimist = require('minimist')
var pump = require('pump')

var argv = minimist(process.argv)

var id = argv._[2]
var vision = peervision(id ? new Buffer(id, 'hex') : null)

airswarm('pv-' + vision.id.toString('hex'), function (p) {
  console.error('DEBUG: Got new peer!')
  pump(p, vision.createStream(), p)
})

vision.on('upload', function (index) {
  console.error('DEBUG: Uploading block #' + index)
})

vision.on('download', function (index) {
  console.error('DEBUG: Downloading block #' + index)
})

var blocks = 0

if (!id) {
  console.error('Stream id is', vision.id.toString('hex'))
  console.error('Enter the files you want to stream:')
  process.stdin.on('data', function (data) {
    fs.createReadStream(data.toString().trim()).pipe(choppa(16 * 1024)).on('data', function (data) {
      blocks++
      vision.append(data)
    }).on('end', function () {
      console.error('DEBUG: Appended', blocks, 'blocks')
    })
  })
} else {
  var offset = Number(argv.offset || 0)
  loop()

  function loop () {
    vision.get(offset, function (err, data) {
      if (err) throw err
      offset++
      process.stdout.write(data, loop)
    })
  }
}
<commit_msg>bump peervision and debug output<commit_after>#!/usr/bin/env node

var fs = require('fs')
var peervision = require('peervision')
var airswarm = require('airswarm')
var signalhub = require('signalhub')
var net = require('net')
var choppa = require('choppa')
var minimist = require('minimist')
var pump = require('pump')

var argv = minimist(process.argv)

var id = argv._[2]
var vision = peervision(id ? new Buffer(id, 'hex') : null)
var debug = argv.debug ? console.error.bind(console, 'DEBUG:') : function () {}

if (argv.webrtc) {
  var wrtc = require('wrtc')
  var swarm = require('webrtc-swarm')
  var sw = swarm(signalhub('pv-' + vision.id.toString('hex'), ['https://signalhub.mafintosh.com']), {wrtc: wrtc})
  sw.on('peer', function (p) {
    debug('WebRTC peer')
    pump(p, vision.createStream(), p)
  })
}

airswarm('pv-' + vision.id.toString('hex'), function (p) {
  debug('Airswarm peer')
  pump(p, vision.createStream(), p)
})

vision.on('upload', function (index) {
  debug('Uploading block #' + index)
})

vision.on('download', function (index) {
  debug('Downloading block #' + index)
})

var blocks = 0

if (!id) {
  console.error('Stream id is', vision.id.toString('hex'))

  if (argv.stdin) {
    process.stdin.on('data', function (data) {
      vision.append(data)
    })
    return
  }

  console.error('Enter the files you want to stream:')
  process.stdin.on('data', function (data) {
    fs.createReadStream(data.toString().trim()).pipe(choppa(16 * 1024)).on('data', function (data) {
      blocks++
      vision.append(data)
    }).on('end', function () {
      debug('Appended', blocks, 'blocks')
    })
  })
} else {
  var offset = Number(argv.offset || 0)
  loop()

  function loop () {
    vision.get(offset, function (err, data) {
      if (err) throw err
      offset++
      process.stdout.write(data, loop)
    })
  }
}
<|endoftext|>