<commit_before>/* eslint-disable react/prop-types */
import 'moment-timezone'
import { Alert, Button } from 'react-bootstrap'
import { connect } from 'react-redux'
import { FormattedMessage, injectIntl } from 'react-intl'
import coreUtils from '@opentripplanner/core-utils'
import FromToLocationPicker from '@opentripplanner/from-to-location-picker'
import moment from 'moment'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import styled from 'styled-components'

import * as apiActions from '../../actions/api'
import * as mapActions from '../../actions/map'
import * as uiActions from '../../actions/ui'
import * as userActions from '../../actions/user'
import { getPersistenceMode } from '../../util/user'
import { getShowUserSettings, getStopViewerConfig } from '../../util/state'
import { navigateBack } from '../../util/ui'
import { stopIsFlex } from '../../util/viewer'
import Icon from '../util/icon'
import Strong from '../util/strong-text'

import LiveStopTimes from './live-stop-times'
import StopScheduleTable from './stop-schedule-table'

const { getUserTimezone, OTP_API_DATE_FORMAT } = coreUtils.time

const defaultState = {
  date: moment().format(OTP_API_DATE_FORMAT),
  isShowingSchedule: false
}

// A scrollable container for the contents of the stop viewer body.
const Scrollable = styled.div`
  margin-right: -12px;
  overflow-y: auto;
  padding-right: 12px;
`
// Alert with custom styles
const StyledAlert = styled(Alert)`
  /* 'clear: both' prevents the date selector from overlapping with the alert. */
  clear: both;
  margin: 10px 0;
  padding: 5px 10px;
  text-align: center;
`

class StopViewer extends Component {
  state = defaultState

  static propTypes = {
    hideBackButton: PropTypes.bool,
    stopData: PropTypes.object,
    viewedStop: PropTypes.object
  }

  _backClicked = () => navigateBack()

  _setLocationFromStop = (locationType) => {
    const { setLocation, stopData } = this.props
    const location = {
      lat: stopData.lat,
      lon: stopData.lon,
      name: stopData.name
    }
    setLocation({ location, locationType, reverseGeocode: false })
    this.setState({ popupPosition: null })
  }

  _onClickPlanTo = () => this._setLocationFromStop('to')

  _onClickPlanFrom = () => this._setLocationFromStop('from')

  componentDidMount() {
    // Load the viewed stop in the store when the Stop Viewer first mounts
    this.props.fetchStopInfo(this.props.viewedStop)
  }

  _toggleFavorite = () => {
    const { forgetStop, rememberStop, stopData } = this.props
    if (this._isFavorite()) forgetStop(stopData)
    else rememberStop(stopData)
  }

  _findStopTimesForDate = (date) => {
    const { findStopTimesForStop, viewedStop } = this.props
    findStopTimesForStop({ date, stopId: viewedStop.stopId })
  }

  _toggleScheduleView = () => {
    const { date, isShowingSchedule } = this.state

    // If not currently showing schedule view, fetch schedules for current date.
    // Otherwise fetch next arrivals.
    if (isShowingSchedule) this._findStopTimesForDate(date)

    this.setState({ isShowingSchedule: !isShowingSchedule })
  }

  _isFavorite = () =>
    this.props.stopData &&
    this.props.favoriteStops.findIndex(
      (s) => s.id === this.props.stopData.id
    ) !== -1

  componentDidUpdate(prevProps, prevState) {
    const { fetchStopInfo, viewedStop } = this.props
    const { date, isShowingSchedule } = this.state

    if (prevProps.viewedStop?.stopId !== viewedStop?.stopId) {
      // Reset state to default if stop changes (i.e., show next arrivals).
      this.setState(defaultState)
      fetchStopInfo(viewedStop)
    } else if (!!isShowingSchedule && !prevState.isShowingSchedule) {
      // If the viewing mode has changed to schedule view,
      // then fetch all departures for the current day
      // (otherwise only a few are shown).
      this._findStopTimesForDate(date)
    }
  }

  handleDateChange = (evt) => {
    const date = evt.target.value
    this._findStopTimesForDate(date)
    this.setState({ date })
  }

  _zoomToStop = () => {
    this.props.zoomToStop(this.props.stopData)
  }

  _renderHeader = () => {
    const { enableFavoriteStops, hideBackButton, showUserSettings, stopData } =
      this.props
    return (

        {/* Back button */}
        {!hideBackButton && (

            <Button bsSize="small" onClick={this._backClicked}>
              <Icon type="arrow-left" />
              <FormattedMessage id="common.forms.back" />
            </Button>

        )}

        {/* Header Text */}

          {stopData ? (
            {stopData.name}
          ) : (

              <FormattedMessage id="components.StopViewer.loadingText" />

          )}
          {showUserSettings && enableFavoriteStops ? (
            <Button
              bsSize="large"
              bsStyle="link"
              onClick={this._toggleFavorite}
              style={{
                color: this._isFavorite() ? 'yellow' : 'black',
                marginLeft: '5px',
                padding: 0
              }}
            >
              <Icon type={this._isFavorite() ? 'star' : 'star-o'} />
            </Button>
          ) : null}

    )
  }

  /**
   * Plan trip from/to here buttons, plus the schedule/next arrivals toggle.
   * TODO: Can this use SetFromToButtons?
   */
  _renderControls = () => {
    const { homeTimezone, intl, stopData } = this.props
    const { isShowingSchedule } = this.state
    const userTimeZone = getUserTimezone()
    const inHomeTimezone = homeTimezone && homeTimezone === userTimeZone

    // Rewrite stop ID to not include Agency prefix, if present
    // TODO: make this functionality configurable?
    let stopId
    if (stopData && stopData.id) {
      stopId = stopData.id.includes(':')
        ? stopData.id.split(':')[1]
        : stopData.id
    }

    const isFlex = stopIsFlex(stopData)

    let timezoneWarning
    if (!inHomeTimezone) {
      const timezoneCode = moment().tz(homeTimezone).format('z')

      // Display a banner about the departure timezone if user's timezone is not the configured 'homeTimezone'
      // (e.g. cases where a user in New York looks at a schedule in Los Angeles).
      timezoneWarning = (

      )
    }

    return (

          <FormattedMessage
            id="components.StopViewer.displayStopId"
            values={{ stopId: stopData.code || stopId, strong: Strong }}
          />
          <button
            className="link-button"
            onClick={this._zoomToStop}
            title={intl.formatMessage({
              id: 'components.StopViewer.zoomToStop'
            })}
          >
            <Icon type="search" />
          </button>
          {!isFlex && (
            <button
              className="link-button pull-right"
              onClick={this._toggleScheduleView}
              style={{ fontSize: 'small' }}
            >
              <Icon
                type={isShowingSchedule ? 'clock-o' : 'calendar'}
                withSpace
              />
              {isShowingSchedule ? (
                <FormattedMessage id="components.StopViewer.viewNextArrivals" />
              ) : (
                <FormattedMessage id="components.StopViewer.viewSchedule" />
              )}
            </button>
          )}
          {isShowingSchedule && (
            <input
              className="pull-right"
              onChange={this.handleDateChange}
              onKeyDown={this.props.onKeyDown}
              required
              type="date"
              value={this.state.date}
            />
          )}

        <FromToLocationPicker
          label
          onFromClick={this._onClickPlanFrom}
          onToClick={this._onClickPlanTo}
        />

        {timezoneWarning}

    )
  }

  render() {
    const {
      autoRefreshStopTimes,
      findStopTimesForStop,
      homeTimezone,
      nearbyStops,
      setHoveredStop,
      showNearbyStops,
      stopData,
      stopViewerArriving,
      stopViewerConfig,
      toggleAutoRefresh,
      transitOperators,
      viewedStop
    } = this.props
    const { date, isShowingSchedule } = this.state
    const { showBlockIds } = stopViewerConfig
    const hasStopTimesAndRoutes = !!(
      stopData &&
      stopData.stopTimes &&
      stopData.stopTimes.length > 0 &&
      stopData.routes
    )
    let contents
    if (!hasStopTimesAndRoutes) {
      contents = (

          <FormattedMessage id="components.StopViewer.noStopsFound" />

      )
      /* If geometries are available (and are not a point) and stop times
         are not, it is a strong indication that the stop is a flex stop.

         The extra checks stopData are needed to ensure that the message is
         not shown while stopData is loading
         */
      if (stopIsFlex(stopData)) {
        contents = (

            <FormattedMessage id="components.StopViewer.flexStop" />

        )
      }
    } else if (isShowingSchedule) {
      contents = (
        <StopScheduleTable
          date={date}
          homeTimezone={homeTimezone}
          showBlockIds={showBlockIds}
          stopData={stopData}
        />
      )
    } else {
      contents = (
        <LiveStopTimes
          autoRefreshStopTimes={autoRefreshStopTimes}
          findStopTimesForStop={findStopTimesForStop}
          homeTimezone={homeTimezone}
          nearbyStops={nearbyStops}
          setHoveredStop={setHoveredStop}
          showNearbyStops={showNearbyStops}
          stopData={stopData}
          stopViewerArriving={stopViewerArriving}
          stopViewerConfig={stopViewerConfig}
          toggleAutoRefresh={toggleAutoRefresh}
          transitOperators={transitOperators}
          viewedStop={viewedStop}
        />
      )
    }

    return (

        {/* Header Block */}
        {this._renderHeader()}

        {stopData && (

            {this._renderControls()}
            <Scrollable>{contents}</Scrollable>
            {/* Future: add stop details */}

        )}

    )
  }
}

// connect to redux store

const mapStateToProps = (state) => {
  const showUserSettings = getShowUserSettings(state)
  const stopViewerConfig = getStopViewerConfig(state)
  const { config, transitIndex, ui } = state.otp
  const { homeTimezone, language, persistence, stopViewer, transitOperators } =
    config
  const { autoRefreshStopTimes, favoriteStops } = state.user.localUser
  const stopLookup = transitIndex.stops
  const stopData = stopLookup[ui.viewedStop.stopId]
  const nearbyStops = stopData?.nearbyStops?.map((stopId) => stopLookup[stopId])
  return {
    autoRefreshStopTimes,
    enableFavoriteStops: getPersistenceMode(persistence).isLocalStorage,
    favoriteStops,
    homeTimezone,
    nearbyStops,
    showNearbyStops: stopViewer.nearbyRadius > 0,
    showUserSettings,
    stopData,
    stopViewerArriving: language.stopViewerArriving,
    stopViewerConfig,
    transitOperators,
    viewedStop: ui.viewedStop
  }
}

const mapDispatchToProps = {
  fetchStopInfo: apiActions.fetchStopInfo,
  findStopTimesForStop: apiActions.findStopTimesForStop,
  forgetStop: userActions.forgetStop,
  rememberStop: userActions.rememberStop,
  setHoveredStop: uiActions.setHoveredStop,
  setLocation: mapActions.setLocation,
  setMainPanelContent: uiActions.setMainPanelContent,
  toggleAutoRefresh: uiActions.toggleAutoRefresh,
  zoomToStop: mapActions.zoomToStop
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(injectIntl(StopViewer))
<commit_msg>refactor(StopViewer): Remove moment reference<commit_after>/* eslint-disable react/prop-types */
import 'moment-timezone'
import { Alert, Button } from 'react-bootstrap'
import { connect } from 'react-redux'
import { format } from 'date-fns-tz'
import { FormattedMessage, injectIntl } from 'react-intl'
import coreUtils from '@opentripplanner/core-utils'
import dateFnsUSLocale from 'date-fns/locale/en-US'
import FromToLocationPicker from '@opentripplanner/from-to-location-picker'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import styled from 'styled-components'

import * as apiActions from '../../actions/api'
import * as mapActions from '../../actions/map'
import * as uiActions from '../../actions/ui'
import * as userActions from '../../actions/user'
import { getPersistenceMode } from '../../util/user'
import { getShowUserSettings, getStopViewerConfig } from '../../util/state'
import { navigateBack } from '../../util/ui'
import { stopIsFlex } from '../../util/viewer'
import Icon from '../util/icon'
import Strong from '../util/strong-text'

import LiveStopTimes from './live-stop-times'
import StopScheduleTable from './stop-schedule-table'

const { getCurrentDate, getUserTimezone } = coreUtils.time

function getDefaultState(timeZone) {
  return {
    // Compare dates/times in the stop viewer based on the agency's timezone.
    date: getCurrentDate(timeZone),
    isShowingSchedule: false
  }
}

// A scrollable container for the contents of the stop viewer body.
const Scrollable = styled.div`
  margin-right: -12px;
  overflow-y: auto;
  padding-right: 12px;
`
// Alert with custom styles
const StyledAlert = styled(Alert)`
  /* 'clear: both' prevents the date selector from overlapping with the alert. */
  clear: both;
  margin: 10px 0;
  padding: 5px 10px;
  text-align: center;
`

class StopViewer extends Component {
  constructor(props) {
    super(props)
    this.state = getDefaultState(props.homeTimezone)
  }

  static propTypes = {
    hideBackButton: PropTypes.bool,
    stopData: PropTypes.object,
    viewedStop: PropTypes.object
  }

  _backClicked = () => navigateBack()

  _setLocationFromStop = (locationType) => {
    const { setLocation, stopData } = this.props
    const location = {
      lat: stopData.lat,
      lon: stopData.lon,
      name: stopData.name
    }
    setLocation({ location, locationType, reverseGeocode: false })
    this.setState({ popupPosition: null })
  }

  _onClickPlanTo = () => this._setLocationFromStop('to')

  _onClickPlanFrom = () => this._setLocationFromStop('from')

  componentDidMount() {
    // Load the viewed stop in the store when the Stop Viewer first mounts
    this.props.fetchStopInfo(this.props.viewedStop)
  }

  _toggleFavorite = () => {
    const { forgetStop, rememberStop, stopData } = this.props
    if (this._isFavorite()) forgetStop(stopData)
    else rememberStop(stopData)
  }

  _findStopTimesForDate = (date) => {
    const { findStopTimesForStop, viewedStop } = this.props
    findStopTimesForStop({ date, stopId: viewedStop.stopId })
  }

  _toggleScheduleView = () => {
    const { date, isShowingSchedule } = this.state

    // If not currently showing schedule view, fetch schedules for current date.
    // Otherwise fetch next arrivals.
    if (isShowingSchedule) this._findStopTimesForDate(date)

    this.setState({ isShowingSchedule: !isShowingSchedule })
  }

  _isFavorite = () =>
    this.props.stopData &&
    this.props.favoriteStops.findIndex(
      (s) => s.id === this.props.stopData.id
    ) !== -1

  componentDidUpdate(prevProps, prevState) {
    const { fetchStopInfo, homeTimezone, viewedStop } = this.props
    const { date, isShowingSchedule } = this.state

    if (prevProps.viewedStop?.stopId !== viewedStop?.stopId) {
      // Reset state to default if stop changes (i.e., show next arrivals).
      this.setState(getDefaultState(homeTimezone))
      fetchStopInfo(viewedStop)
    } else if (!!isShowingSchedule && !prevState.isShowingSchedule) {
      // If the viewing mode has changed to schedule view,
      // then fetch all departures for the current day
      // (otherwise only a few are shown).
      this._findStopTimesForDate(date)
    }
  }

  handleDateChange = (evt) => {
    const date = evt.target.value
    this._findStopTimesForDate(date)
    this.setState({ date })
  }

  _zoomToStop = () => {
    this.props.zoomToStop(this.props.stopData)
  }

  _renderHeader = () => {
    const { enableFavoriteStops, hideBackButton, showUserSettings, stopData } =
      this.props
    return (

        {/* Back button */}
        {!hideBackButton && (

            <Button bsSize="small" onClick={this._backClicked}>
              <Icon type="arrow-left" />
              <FormattedMessage id="common.forms.back" />
            </Button>

        )}

        {/* Header Text */}

          {stopData ? (
            {stopData.name}
          ) : (

              <FormattedMessage id="components.StopViewer.loadingText" />

          )}
          {showUserSettings && enableFavoriteStops ? (
            <Button
              bsSize="large"
              bsStyle="link"
              onClick={this._toggleFavorite}
              style={{
                color: this._isFavorite() ? 'yellow' : 'black',
                marginLeft: '5px',
                padding: 0
              }}
            >
              <Icon type={this._isFavorite() ? 'star' : 'star-o'} />
            </Button>
          ) : null}

    )
  }

  /**
   * Plan trip from/to here buttons, plus the schedule/next arrivals toggle.
   * TODO: Can this use SetFromToButtons?
   */
  _renderControls = () => {
    const { homeTimezone, intl, stopData } = this.props
    const { isShowingSchedule } = this.state
    const inHomeTimezone = homeTimezone && homeTimezone === getUserTimezone()

    // Rewrite stop ID to not include Agency prefix, if present
    // TODO: make this functionality configurable?
    let stopId
    if (stopData && stopData.id) {
      stopId = stopData.id.includes(':')
        ? stopData.id.split(':')[1]
        : stopData.id
    }

    const isFlex = stopIsFlex(stopData)

    let timezoneWarning
    if (!inHomeTimezone) {
      const timezoneCode = format(Date.now(), 'z', {
        // To avoid ambiguities for now, use the English-US timezone abbreviations ("EST", "PDT", etc.)
        locale: dateFnsUSLocale,
        timeZone: homeTimezone
      })

      // Display a banner about the departure timezone if user's timezone is not the configured 'homeTimezone'
      // (e.g. cases where a user in New York looks at a schedule in Los Angeles).
      timezoneWarning = (

      )
    }

    return (

          <FormattedMessage
            id="components.StopViewer.displayStopId"
            values={{ stopId: stopData.code || stopId, strong: Strong }}
          />
          <button
            className="link-button"
            onClick={this._zoomToStop}
            title={intl.formatMessage({
              id: 'components.StopViewer.zoomToStop'
            })}
          >
            <Icon type="search" />
          </button>
          {!isFlex && (
            <button
              className="link-button pull-right"
              onClick={this._toggleScheduleView}
              style={{ fontSize: 'small' }}
            >
              <Icon
                type={isShowingSchedule ? 'clock-o' : 'calendar'}
                withSpace
              />
              {isShowingSchedule ? (
                <FormattedMessage id="components.StopViewer.viewNextArrivals" />
              ) : (
                <FormattedMessage id="components.StopViewer.viewSchedule" />
              )}
            </button>
          )}
          {isShowingSchedule && (
            <input
              className="pull-right"
              onChange={this.handleDateChange}
              onKeyDown={this.props.onKeyDown}
              required
              type="date"
              value={this.state.date}
            />
          )}

        <FromToLocationPicker
          label
          onFromClick={this._onClickPlanFrom}
          onToClick={this._onClickPlanTo}
        />

        {timezoneWarning}

    )
  }

  render() {
    const {
      autoRefreshStopTimes,
      findStopTimesForStop,
      homeTimezone,
      nearbyStops,
      setHoveredStop,
      showNearbyStops,
      stopData,
      stopViewerArriving,
      stopViewerConfig,
      toggleAutoRefresh,
      transitOperators,
      viewedStop
    } = this.props
    const { date, isShowingSchedule } = this.state
    const { showBlockIds } = stopViewerConfig
    const hasStopTimesAndRoutes = !!(
      stopData &&
      stopData.stopTimes &&
      stopData.stopTimes.length > 0 &&
      stopData.routes
    )
    let contents
    if (!hasStopTimesAndRoutes) {
      contents = (

          <FormattedMessage id="components.StopViewer.noStopsFound" />

      )
      /* If geometries are available (and are not a point) and stop times
         are not, it is a strong indication that the stop is a flex stop.

         The extra checks stopData are needed to ensure that the message is
         not shown while stopData is loading
         */
      if (stopIsFlex(stopData)) {
        contents = (

            <FormattedMessage id="components.StopViewer.flexStop" />

        )
      }
    } else if (isShowingSchedule) {
      contents = (
        <StopScheduleTable
          date={date}
          homeTimezone={homeTimezone}
          showBlockIds={showBlockIds}
          stopData={stopData}
        />
      )
    } else {
      contents = (
        <LiveStopTimes
          autoRefreshStopTimes={autoRefreshStopTimes}
          findStopTimesForStop={findStopTimesForStop}
          homeTimezone={homeTimezone}
          nearbyStops={nearbyStops}
          setHoveredStop={setHoveredStop}
          showNearbyStops={showNearbyStops}
          stopData={stopData}
          stopViewerArriving={stopViewerArriving}
          stopViewerConfig={stopViewerConfig}
          toggleAutoRefresh={toggleAutoRefresh}
          transitOperators={transitOperators}
          viewedStop={viewedStop}
        />
      )
    }

    return (

        {/* Header Block */}
        {this._renderHeader()}

        {stopData && (

            {this._renderControls()}
            <Scrollable>{contents}</Scrollable>
            {/* Future: add stop details */}

        )}

    )
  }
}

// connect to redux store

const mapStateToProps = (state) => {
  const showUserSettings = getShowUserSettings(state)
  const stopViewerConfig = getStopViewerConfig(state)
  const { config, transitIndex, ui } = state.otp
  const { homeTimezone, language, persistence, stopViewer, transitOperators } =
    config
  const { autoRefreshStopTimes, favoriteStops } = state.user.localUser
  const stopLookup = transitIndex.stops
  const stopData = stopLookup[ui.viewedStop.stopId]
  const nearbyStops = stopData?.nearbyStops?.map((stopId) => stopLookup[stopId])
  return {
    autoRefreshStopTimes,
    enableFavoriteStops: getPersistenceMode(persistence).isLocalStorage,
    favoriteStops,
    homeTimezone,
    nearbyStops,
    showNearbyStops: stopViewer.nearbyRadius > 0,
    showUserSettings,
    stopData,
    stopViewerArriving: language.stopViewerArriving,
    stopViewerConfig,
    transitOperators,
    viewedStop: ui.viewedStop
  }
}

const mapDispatchToProps = {
  fetchStopInfo: apiActions.fetchStopInfo,
  findStopTimesForStop: apiActions.findStopTimesForStop,
  forgetStop: userActions.forgetStop,
  rememberStop: userActions.rememberStop,
  setHoveredStop: uiActions.setHoveredStop,
  setLocation: mapActions.setLocation,
  setMainPanelContent: uiActions.setMainPanelContent,
  toggleAutoRefresh: uiActions.toggleAutoRefresh,
  zoomToStop: mapActions.zoomToStop
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(injectIntl(StopViewer))
<|endoftext|>


<commit_before>var _ = require('lodash');
var ss = require('simple-statistics');
var evalculist = require('evalculist');
var isItem = require('./item/isItem');
var keyToVarName = require('./keyToVarName');
var urlFinder = require('./urlFinder');

module.exports = function (graph) {
  graph.register('createComputationContextObject', ['isItem','getItemByGuid'], function (isItem, getItemByGuid) {
    var itemMethods = ['$item', '$$item', '$siblings'];

    var fnToString = _.constant('[Function]');

    function ContextObject (item) {
      if (item) {
        var _this = this;
        _.each(itemMethods, function(method) {
          _this[method] = function () {
            return item[method].apply(item, arguments);
          };
          _this[method].toString = fnToString;
        });

        this.$parent = item.parent;
        this.$index = item.parent ? item.parent.items.indexOf(item) : 0;
        this.$items = item.items;
        this.$name = item.key;
        this.$guid = item.guid;
      }
    }

    var proto = ContextObject.prototype;

    var valIfItem = function (item) { return isItem(item) ? item.valueOf() : item; };
    var itemsIfItem = function (item) { return isItem(item) ? item.items : item; };
    var itemsFirst = function (fn) {
      return function () {
        arguments[0] = itemsIfItem(arguments[0]);
        return fn.apply(this, arguments);
      };
    };
    var allItemsAndValues = function (fn) {
      return function () {
        var i = arguments.length;
        while (--i >= 0) {
          arguments[i] = itemsIfItem(arguments[i]);
          if (_.isArray(arguments[i])) arguments[i] = _.map(arguments[i], proto.valueOf);
        }
        return fn.apply(this, arguments);
      };
    };

    var mathKeys = ['E','LN2','LN10','LOG2E','LOG10E','PI','SQRT1_2','SQRT2','abs','acos','acosh','asin','asinh','atan','atan2','atanh','cbrt','ceil','clz32','cos','cosh','exp','expm1','floor','fround','hypot','imul','log','log1p','log2','log10','max','min','pow','random','round','sign','sin','sinh','sqrt','tan','tanh','trunc'];
    var ssKeys = ['median','mode','product','variance','sampleVariance','standardDeviation','sampleStandardDeviation','medianAbsoluteDeviation','interquartileRange','harmonicMean','geometricMean','rootMeanSquare','sampleSkewness','factorial'];
    var lodashKeys = ['difference','intersection','reverse','union','uniq','xor',
                      'filter','find','findLast','map','reduce','zip','unzip',
                      'ceil','floor','max','min','round','clamp','inRange','range',
                      'camelCase','capitalize','kebabCase','lowerCase','join','split',
                      'parseInt','repeat','replace','snakeCase','split','startCase','toLower',
                      'toUpper','truncate','upperCase','words','times','identity'];
    _.each(mathKeys, function(key) { proto[key] = Math[key]; });
    _.each(ssKeys, function(key) { proto[key] = ss[key]; });
    _.each(lodashKeys, function (key) { proto[key] = _[key]; });

    var iterators = ['average','mean','median','mode','standardDeviation','products',
                    'unzip','filter','find','findLast','map','reduce','min','max','join'];

    _.each(iterators, function (methodName) {
      proto[methodName] = itemsFirst(proto[methodName]);
    });

    var doubleIterators = ['difference','intersection','union','xor','zip'];

    _.each(doubleIterators, function (methodName) {
      proto[methodName] = allItemsAndValues(proto[methodName]);
    });

    var conditionals = ['isArray','isNumber','isFunction','isString'];

    _.each(conditionals, function (methodName) {
      proto[methodName] = _.flow(valIfItem, _[methodName]);
    });

    proto.PLACEHOLDER = { toString: _.constant('[PLACEHOLDER]') };

    proto.partial = _.rest(function (fn, partialArgs) {
      var partialFn = _.rest(function (args) {
        var finalArgs = partialArgs.map(function (pArg) {
          if (pArg === proto.PLACEHOLDER) return args.shift();
          return pArg;
        }).concat(args);
        return fn.apply(null, finalArgs);
      });
      partialFn.toString = fnToString;
      return partialFn;
    });

    proto.average = proto.mean = itemsFirst(function (items, defaultValue) {
      return proto.sum(items, defaultValue) / items.length;
    });

    proto.sum = itemsFirst(function(items, defaultValue) {
      if (defaultValue != null) {
        return items.reduce(function(sum, item) {
          var val = item && item.valueOf();
          if (!_.isNumber(val) || _.isNaN(val)) val = defaultValue;
          return sum + val;
        }, 0);
      }
      return items.reduce(function(sum, item) {
        var val = item.valueOf();
        return sum + (_.isNumber(val) ? val : NaN);
      }, 0);
    });

    proto.quantile = itemsFirst(function (sample, p) {
      if (_.isNumber(p)) return ss.quantile(sample, p);
    });

    proto.interquartileRange = itemsFirst(function (items) {
      var values = _.map(items, valIfItem);
      return ss.interquartileRange(values);
    });

    proto.binomialCoefficient = function binomial(n, k) {
      // if ((typeof n !== 'number') || (typeof k !== 'number')) return false;
      var coeff = 1;
      for (var x = n-k+1; x <= n; x++) coeff *= x;
      for (x = 1; x <= k; x++) coeff /= x;
      return coeff;
    };
    proto.isInteger = function (n) { return _.isNumber(n) && n % 1 === 0; }
    proto.gcd = function (a, b) {
      var _gcd = function (a, b) { return b !== 0 ? _gcd(b, a % b) : a; };
      if (proto.isInteger(a) && proto.isInteger(b)) return _gcd(a, b);
      return NaN;
    };
    proto.lcm = function (a, b) { return Math.abs(a) * (Math.abs(b) / proto.gcd(a, b)); };
    proto.fraction = function (numerator, denominator, returnFn) {
      if (_.isFunction(denominator) && !returnFn) {
        returnFn = denominator;
        denominator = 1;
      } else if (denominator == null) {
        denominator = 1;
      }
      if (!returnFn) {
        returnFn = function (n, d) { return '' + n + '/' +  d; };
      }
      var ndec = numerator.toString().split('.')[1] || '';
      var ddec = denominator.toString().split('.')[1] || '';
      if (ndec || ddec) {
        var adjustor = Math.pow(10, Math.max(ndec.length, ddec.length));
        numerator = Math.round(numerator * adjustor);
        denominator = Math.round(denominator * adjustor);
      }
      var gcd = proto.gcd(numerator, denominator);
      return returnFn(numerator / gcd, denominator / gcd);
    };
    proto.wordCount = function (item) {
      var count = 0;
      var items = item;
      if (isItem(item)) {
        if (_.isUndefined(item.key)) item.valueOf();
        count = _.words('' + item.key + ' ' + (item.val || '')).length;
        items = item.items;
        if (items.length === 0) return count;
      } else if (!_.isArray(items)) {
        return _.words('' + item).length;
      }
      return _.reduce(items, function (sum, item) {
        return sum + proto.wordCount(item);
      }, count);
    };
    proto.mod = proto.modulo = function(a, b) { return (+a % (b = +b) + b) % b; };
    proto.polarToCartesian = function (r, theta) {
      var x = r * Math.cos(theta),
          y = r * Math.sin(theta);
      return [x, y];
    };
    proto.cartesianToPolar = function (x, y) {
      var r = Math.sqrt(x * x + y * y),
          theta = Math.atan(y / x);
      return [r, theta];
    };
    proto.degreesToRadians = function (degrees) {
      return (degrees / 360) * (2 * Math.PI);
    };

    proto.radiansToDegrees = function (radians) {
      return 360 * radians / (2 * Math.PI);
    };

    proto.length = proto.count = itemsFirst(_.property('length'));
    proto.name = proto.key = _.property('key');
    proto.valueOf = _.method('valueOf');
    proto.toString = _.method('toString');
    proto.recursiveCount = proto.rcount = itemsFirst(function (items) {
      return items.length + _.reduce(items, function (m, item) {
        return m + proto.recursiveCount(item.items);
      }, 0);
    });

    proto.true = true;
    proto.false = false;
    proto.null = null;

    proto.itemsOf = _.property('items');
    proto.nameOf = _.property('key');
    proto.parentOf = _.property('parent');
    proto.guidOf = _.property('guid');
    proto.indexOf = itemsFirst(function (array, item) {
      return array.indexOf(item);
    });
    proto.pluckItems = itemsFirst(function (items, key) {
      var condition;
      if (_.isFunction(key)) {
        condition = key;
      } else {
        condition = { key: key };
      }
      return items.reduce(function (pluckedItems, item) {
        var pluckedItem = _.find(item.items, condition);
        if (pluckedItem) pluckedItems.push(pluckedItem);
        return pluckedItems;
      }, []);
    });

    proto.lambda = proto.function = proto.fn = _.rest(function (string, partialArgs) {
      var pieces = ('' + string).split('|');
      var argNames = pieces.length > 1 ? pieces.shift().split(',') : [];
      var fnBody = pieces.join('|')
      var evalFn = evalculist(fnBody);
      var fn = function () {
        var args = partialArgs.concat(_.toArray(arguments));
        var localVars = argNames.reduce(function (localVars, name, i) {
          localVars[_.trim(name)] = args[i];
          return localVars;
        }, {});
        return evalFn({
          variable: function (name) {
            if (localVars.hasOwnProperty(name)) return localVars[name];
            return proto[name];
          },
          accessor: proto.accessor,
          dotAccessor: proto.dotAccessor,
          assignment: function (name, val) {
            return localVars[name] = val;
          }
        });
      };
      var argString = argNames.slice(partialArgs.length).join(',');
      var fnString = argString ? (argString + '|'  + fnBody) : fnBody;
      fn.toString = _.constant(fnString);
      return fn;
    });

    proto.flowMap = itemsFirst(function () {
      var fns = _.flatten(arguments);
      var fmap = _.rest(function (args) {
        return _.map(fns, function (fn) {
          return fn.valueOf().apply(null, args);
        });
      });
      fmap.toString = fnToString;
      return fmap;
    });

    proto.flow = itemsFirst(function () {
      var fns = _.flatten(arguments);
      var flow = function () {
        return _.reduce(fns, function (r, fn) {
          return [fn.valueOf().apply(null, r)];
        }, arguments)[0];
      };
      flow.toString = fnToString;
      return flow;
    });

    proto.compose = itemsFirst(function () {
      var fns = _.flatten(arguments);
      fns.reverse();
      return proto.flow(fns);
    });

    var curry2 = function (fn) {
      var fn0, fn1;
      fn0 = function () {
        if (arguments.length === 0) return fn0;
        var arg0 = arguments[0];
        fn1 = function (arg1) { return fn(arg0, arg1); };
        if (arguments.length > 1) return fn1(arguments[1]);
        fn1.toString = fnToString;
        return fn1;
      };
      fn0.toString = fnToString;
      return fn0;
    };

    proto.isEqualTo = proto.eq = curry2(function (b, a) { return a == b; });
    proto.isGreaterThan = proto.gt = curry2(function (b, a) { return a > b; });
    proto.isGreaterThanOrEqualTo = proto.gte = curry2(function (b, a) { return a >= b; });
    proto.isLessThan = proto.lt = curry2(function (b, a) { return a < b; });
    proto.isLessThanOrEqualTo = proto.lte = curry2(function (b, a) { return a <= b; });

    // TODO should these logic functions check and adapt to non-function arguments?
    proto.and = function () {
      var fns = _.flatten(arguments),
          and;
      and = function () {
        var args = arguments;
        return _.every(fns, function (fn) {
          return fn.valueOf().apply(null, args);
        });
      };
      and.toString = fnToString;
      return and;
    };

    proto.or = function () {
      var fns = _.flatten(arguments),
          or;
      or = function () {
        var args = arguments;
        return _.some(fns, function (fn) {
          return fn.valueOf().apply(null, args);
        });
      };
      or.toString = fnToString;
      return or;
    };

    proto.not = function (fn) {
      return function () {
        return !fn.valueOf().apply(null, arguments);
      };
    };

    proto.item = curry2(function (key, list) {
      if (isItem(list)) return list.$item(key);
      if (_.isNumber(key)) return list[key];
      var condition;
      if (_.isFunction(key)) {
        condition = key;
      } else {
        condition = { key: key };
      }
      return _.find(list, condition);
    });

    proto.itemOf = curry2(function (list, key) {
      return proto.item(key, list);
    });

    proto.global_item = function (key) {
      return window.topItem.$item(key);
    };

    proto.itemByGuid = function (guid) {
      return getItemByGuid(guid);
    };

    // evalculist adds a special "accessor" function for things like a['b']
    // so it becomes accessor(a, 'b')
    proto.accessor = function (obj, key) {
      if (_.isNumber(key) && !isItem(obj[key])) return obj[key];
      return proto.pluckItems(obj, key);
    };

    proto.dotAccessor = itemsFirst(function (items, key) {
      var item = _.findLast(items, function (item) {
        if (!item.key) item.valueOf();
        return keyToVarName(item.key) === key;
      });
      if (item && item.hasVal) return item.valueOf();
      return item;
    });

    proto.flatten = itemsFirst(function (items) {
      if (isItem(items[0])) {
        return _.reduce(items, function (flatItems, item) {
          flatItems.push(item);
          if (item.items.length) return flatItems.concat(proto.flatten(item));
          return flatItems;
        }, []);
      } else {
        return _.flatten(items);
      }
    });

    var imageToString = _.constant('[Image]');
    proto.image = function (url, width, height) {
      if (!urlFinder.isUrl(url)) return NaN;
      var html = '<img src="' + url + '"';
      if (_.isNumber(+width)) html += ' width="' + (+width) + '"';
      if (_.isNumber(+height)) html += ' height="' + (+height) + '"';
      html += '/>';
      return {
        mediaType: 'image',
        toString: imageToString,
        toHTML: _.constant(html),
      };
    };

    proto.uniq = proto.unique = itemsFirst(function (items) {
      items = _.map(items, proto.valueOf);
      return _.uniq(items);
    });

    proto.slice = itemsFirst(function (items, start, end) {
      var result = _.slice(items, start, end);
      if (_.isString(items)) return result.join('');
      return result;
    });

    proto.reverse = itemsFirst(function (items) {
      var result = _.reverse(_.toArray(items));
      if (_.isString(items)) return result.join('');
      return result;
    });

    _.each(proto, function (fn, key) {
      proto[_.snakeCase(key)] = fn;
      if (_.isFunction(fn)) fn.toString = fnToString;
    });

    return function (item) {
      return new ContextObject(item);
    };

  });
};
<commit_msg>Add "ungroup" function<commit_after>var _ = require('lodash');
var ss = require('simple-statistics');
var evalculist = require('evalculist');
var isItem = require('./item/isItem');
var keyToVarName = require('./keyToVarName');
var urlFinder = require('./urlFinder');

module.exports = function (graph) {
  graph.register('createComputationContextObject', ['isItem','getItemByGuid'], function (isItem, getItemByGuid) {
    var itemMethods = ['$item', '$$item', '$siblings'];

    var fnToString = _.constant('[Function]');

    function ContextObject (item) {
      if (item) {
        var _this = this;
        _.each(itemMethods, function(method) {
          _this[method] = function () {
            return item[method].apply(item, arguments);
          };
          _this[method].toString = fnToString;
        });

        this.$parent = item.parent;
        this.$index = item.parent ? item.parent.items.indexOf(item) : 0;
        this.$items = item.items;
        this.$name = item.key;
        this.$guid = item.guid;
      }
    }

    var proto = ContextObject.prototype;

    var valIfItem = function (item) { return isItem(item) ? item.valueOf() : item; };
    var itemsIfItem = function (item) { return isItem(item) ? item.items : item; };
    var itemsFirst = function (fn) {
      return function () {
        arguments[0] = itemsIfItem(arguments[0]);
        return fn.apply(this, arguments);
      };
    };
    var allItemsAndValues = function (fn) {
      return function () {
        var i = arguments.length;
        while (--i >= 0) {
          arguments[i] = itemsIfItem(arguments[i]);
          if (_.isArray(arguments[i])) arguments[i] = _.map(arguments[i], proto.valueOf);
        }
        return fn.apply(this, arguments);
      };
    };

    var mathKeys = ['E','LN2','LN10','LOG2E','LOG10E','PI','SQRT1_2','SQRT2','abs','acos','acosh','asin','asinh','atan','atan2','atanh','cbrt','ceil','clz32','cos','cosh','exp','expm1','floor','fround','hypot','imul','log','log1p','log2','log10','max','min','pow','random','round','sign','sin','sinh','sqrt','tan','tanh','trunc'];
    var ssKeys = ['median','mode','product','variance','sampleVariance','standardDeviation','sampleStandardDeviation','medianAbsoluteDeviation','interquartileRange','harmonicMean','geometricMean','rootMeanSquare','sampleSkewness','factorial'];
    var lodashKeys = ['difference','intersection','reverse','union','uniq','xor',
                      'filter','find','findLast','map','reduce','zip','unzip',
                      'ceil','floor','max','min','round','clamp','inRange','range',
                      'camelCase','capitalize','kebabCase','lowerCase','join','split',
                      'parseInt','repeat','replace','snakeCase','split','startCase','toLower',
                      'toUpper','truncate','upperCase','words','times','identity'];
    _.each(mathKeys, function(key) { proto[key] = Math[key]; });
    _.each(ssKeys, function(key) { proto[key] = ss[key]; });
    _.each(lodashKeys, function (key) { proto[key] = _[key]; });

    var iterators = ['average','mean','median','mode','standardDeviation','products',
                    'unzip','filter','find','findLast','map','reduce','min','max','join'];

    _.each(iterators, function (methodName) {
      proto[methodName] = itemsFirst(proto[methodName]);
    });

    var doubleIterators = ['difference','intersection','union','xor','zip'];

    _.each(doubleIterators, function (methodName) {
      proto[methodName] = allItemsAndValues(proto[methodName]);
    });

    var conditionals = ['isArray','isNumber','isFunction','isString'];

    _.each(conditionals, function (methodName) {
      proto[methodName] = _.flow(valIfItem, _[methodName]);
    });

    proto.PLACEHOLDER = { toString: _.constant('[PLACEHOLDER]') };

    proto.partial = _.rest(function (fn, partialArgs) {
      var partialFn = _.rest(function (args) {
        var finalArgs = partialArgs.map(function (pArg) {
          if (pArg === proto.PLACEHOLDER) return args.shift();
          return pArg;
        }).concat(args);
        return fn.apply(null, finalArgs);
      });
      partialFn.toString = fnToString;
      return partialFn;
    });

    proto.average = proto.mean = itemsFirst(function (items, defaultValue) {
      return proto.sum(items, defaultValue) / items.length;
    });

    proto.sum = itemsFirst(function(items, defaultValue) {
      if (defaultValue != null) {
        return items.reduce(function(sum, item) {
          var val = item && item.valueOf();
          if (!_.isNumber(val) || _.isNaN(val)) val = defaultValue;
          return sum + val;
        }, 0);
      }
      return items.reduce(function(sum, item) {
        var val = item.valueOf();
        return sum + (_.isNumber(val) ? val : NaN);
      }, 0);
    });

    proto.quantile = itemsFirst(function (sample, p) {
      if (_.isNumber(p)) return ss.quantile(sample, p);
    });

    proto.interquartileRange = itemsFirst(function (items) {
      var values = _.map(items, valIfItem);
      return ss.interquartileRange(values);
    });

    proto.binomialCoefficient = function binomial(n, k) {
      // if ((typeof n !== 'number') || (typeof k !== 'number')) return false;
      var coeff = 1;
      for (var x = n-k+1; x <= n; x++) coeff *= x;
      for (x = 1; x <= k; x++) coeff /= x;
      return coeff;
    };
    proto.isInteger = function (n) { return _.isNumber(n) && n % 1 === 0; }
    proto.gcd = function (a, b) {
      var _gcd = function (a, b) { return b !== 0 ? _gcd(b, a % b) : a; };
      if (proto.isInteger(a) && proto.isInteger(b)) return _gcd(a, b);
      return NaN;
    };
    proto.lcm = function (a, b) { return Math.abs(a) * (Math.abs(b) / proto.gcd(a, b)); };
    proto.fraction = function (numerator, denominator, returnFn) {
      if (_.isFunction(denominator) && !returnFn) {
        returnFn = denominator;
        denominator = 1;
      } else if (denominator == null) {
        denominator = 1;
      }
      if (!returnFn) {
        returnFn = function (n, d) { return '' + n + '/' +  d; };
      }
      var ndec = numerator.toString().split('.')[1] || '';
      var ddec = denominator.toString().split('.')[1] || '';
      if (ndec || ddec) {
        var adjustor = Math.pow(10, Math.max(ndec.length, ddec.length));
        numerator = Math.round(numerator * adjustor);
        denominator = Math.round(denominator * adjustor);
      }
      var gcd = proto.gcd(numerator, denominator);
      return returnFn(numerator / gcd, denominator / gcd);
    };
    proto.wordCount = function (item) {
      var count = 0;
      var items = item;
      if (isItem(item)) {
        if (_.isUndefined(item.key)) item.valueOf();
        count = _.words('' + item.key + ' ' + (item.val || '')).length;
        items = item.items;
        if (items.length === 0) return count;
      } else if (!_.isArray(items)) {
        return _.words('' + item).length;
      }
      return _.reduce(items, function (sum, item) {
        return sum + proto.wordCount(item);
      }, count);
    };
    proto.mod = proto.modulo = function(a, b) { return (+a % (b = +b) + b) % b; };
    proto.polarToCartesian = function (r, theta) {
      var x = r * Math.cos(theta),
          y = r * Math.sin(theta);
      return [x, y];
    };
    proto.cartesianToPolar = function (x, y) {
      var r = Math.sqrt(x * x + y * y),
          theta = Math.atan(y / x);
      return [r, theta];
    };
    proto.degreesToRadians = function (degrees) {
      return (degrees / 360) * (2 * Math.PI);
    };

    proto.radiansToDegrees = function (radians) {
      return 360 * radians / (2 * Math.PI);
    };

    proto.length = proto.count = itemsFirst(_.property('length'));
    proto.name = proto.key = _.property('key');
    proto.valueOf = _.method('valueOf');
    proto.toString = _.method('toString');
    proto.recursiveCount = proto.rcount = itemsFirst(function (items) {
      return items.length + _.reduce(items, function (m, item) {
        return m + proto.recursiveCount(item.items);
      }, 0);
    });

    proto.true = true;
    proto.false = false;
    proto.null = null;

    proto.itemsOf = _.property('items');
    proto.nameOf = _.property('key');
    proto.parentOf = _.property('parent');
    proto.guidOf = _.property('guid');
    proto.indexOf = itemsFirst(function (array, item) {
      return array.indexOf(item);
    });
    proto.pluckItems = itemsFirst(function (items, key) {
      var condition;
      if (_.isFunction(key)) {
        condition = key;
      } else {
        condition = { key: key };
      }
      return items.reduce(function (pluckedItems, item) {
        var pluckedItem = _.find(item.items, condition);
        if (pluckedItem) pluckedItems.push(pluckedItem);
        return pluckedItems;
      }, []);
    });

    proto.lambda = proto.function = proto.fn = _.rest(function (string, partialArgs) {
      var pieces = ('' + string).split('|');
      var argNames = pieces.length > 1 ? pieces.shift().split(',') : [];
      var fnBody = pieces.join('|')
      var evalFn = evalculist(fnBody);
      var fn = function () {
        var args = partialArgs.concat(_.toArray(arguments));
        var localVars = argNames.reduce(function (localVars, name, i) {
          localVars[_.trim(name)] = args[i];
          return localVars;
        }, {});
        return evalFn({
          variable: function (name) {
            if (localVars.hasOwnProperty(name)) return localVars[name];
            return proto[name];
          },
          accessor: proto.accessor,
          dotAccessor: proto.dotAccessor,
          assignment: function (name, val) {
            return localVars[name] = val;
          }
        });
      };
      var argString = argNames.slice(partialArgs.length).join(',');
      var fnString = argString ? (argString + '|'  + fnBody) : fnBody;
      fn.toString = _.constant(fnString);
      return fn;
    });

    proto.flowMap = itemsFirst(function () {
      var fns = _.flatten(arguments);
      var fmap = _.rest(function (args) {
        return _.map(fns, function (fn) {
          return fn.valueOf().apply(null, args);
        });
      });
      fmap.toString = fnToString;
      return fmap;
    });

    proto.flow = itemsFirst(function () {
      var fns = _.flatten(arguments);
      var flow = function () {
        return _.reduce(fns, function (r, fn) {
          return [fn.valueOf().apply(null, r)];
        }, arguments)[0];
      };
      flow.toString = fnToString;
      return flow;
    });

    proto.compose = itemsFirst(function () {
      var fns = _.flatten(arguments);
      fns.reverse();
      return proto.flow(fns);
    });

    var curry2 = function (fn) {
      var fn0, fn1;
      fn0 = function () {
        if (arguments.length === 0) return fn0;
        var arg0 = arguments[0];
        fn1 = function (arg1) { return fn(arg0, arg1); };
        if (arguments.length > 1) return fn1(arguments[1]);
        fn1.toString = fnToString;
        return fn1;
      };
      fn0.toString = fnToString;
      return fn0;
    };

    proto.isEqualTo = proto.eq = curry2(function (b, a) { return a == b; });
    proto.isGreaterThan = proto.gt = curry2(function (b, a) { return a > b; });
    proto.isGreaterThanOrEqualTo = proto.gte = curry2(function (b, a) { return a >= b; });
    proto.isLessThan = proto.lt = curry2(function (b, a) { return a < b; });
    proto.isLessThanOrEqualTo = proto.lte = curry2(function (b, a) { return a <= b; });

    // TODO should these logic functions check and adapt to non-function arguments?
    proto.and = function () {
      var fns = _.flatten(arguments),
          and;
      and = function () {
        var args = arguments;
        return _.every(fns, function (fn) {
          return fn.valueOf().apply(null, args);
        });
      };
      and.toString = fnToString;
      return and;
    };

    proto.or = function () {
      var fns = _.flatten(arguments),
          or;
      or = function () {
        var args = arguments;
        return _.some(fns, function (fn) {
          return fn.valueOf().apply(null, args);
        });
      };
      or.toString = fnToString;
      return or;
    };

    proto.not = function (fn) {
      return function () {
        return !fn.valueOf().apply(null, arguments);
      };
    };

    proto.item = curry2(function (key, list) {
      if (isItem(list)) return list.$item(key);
      if (_.isNumber(key)) return list[key];
      var condition;
      if (_.isFunction(key)) {
        condition = key;
      } else {
        condition = { key: key };
      }
      return _.find(list, condition);
    });

    proto.itemOf = curry2(function (list, key) {
      return proto.item(key, list);
    });

    proto.global_item = function (key) {
      return window.topItem.$item(key);
    };

    proto.itemByGuid = function (guid) {
      return getItemByGuid(guid);
    };

    // evalculist adds a special "accessor" function for things like a['b']
    // so it becomes accessor(a, 'b')
    proto.accessor = function (obj, key) {
      if (_.isNumber(key) && !isItem(obj[key])) return obj[key];
      return proto.pluckItems(obj, key);
    };

    proto.dotAccessor = itemsFirst(function (items, key) {
      var item = _.findLast(items, function (item) {
        if (!item.key) item.valueOf();
        return keyToVarName(item.key) === key;
      });
      if (item && item.hasVal) return item.valueOf();
      return item;
    });

    proto.flatten = itemsFirst(function (items) {
      if (isItem(items[0])) {
        return _.reduce(items, function (flatItems, item) {
          flatItems.push(item);
          if (item.items.length) return flatItems.concat(proto.flatten(item));
          return flatItems;
        }, []);
      } else {
        return _.flatten(items);
      }
    });

    var imageToString = _.constant('[Image]');
    proto.image = function (url, width, height) {
      if (!urlFinder.isUrl(url)) return NaN;
      var html = '<img src="' + url + '"';
      if (_.isNumber(+width)) html += ' width="' + (+width) + '"';
      if (_.isNumber(+height)) html += ' height="' + (+height) + '"';
      html += '/>';
      return {
        mediaType: 'image',
        toString: imageToString,
        toHTML: _.constant(html),
      };
    };

    proto.uniq = proto.unique = itemsFirst(function (items) {
      items = _.map(items, proto.valueOf);
      return _.uniq(items);
    });

    proto.ungroup = itemsFirst(function (items, depth) {
      depth = +(depth == null ? 1 : depth);
      if (_.isNaN(depth)) return depth;
      while (--depth >= 0) {
        items = _.reduce(items, function (ungroupedItems, item) {
          return ungroupedItems.concat(item.items);
        }, []);
      }
      return items;
    });

    proto.slice = itemsFirst(function (items, start, end) {
      var result = _.slice(items, start, end);
      if (_.isString(items)) return result.join('');
      return result;
    });

    proto.reverse = itemsFirst(function (items) {
      var result = _.reverse(_.toArray(items));
      if (_.isString(items)) return result.join('');
      return result;
    });

    _.each(proto, function (fn, key) {
      proto[_.snakeCase(key)] = fn;
      if (_.isFunction(fn)) fn.toString = fnToString;
    });

    return function (item) {
      return new ContextObject(item);
    };

  });
};
<|endoftext|>


<commit_before>/*
 * Brackets Themes Copyright (c) 2014 Miguel Castillo.
 *
 * Licensed under MIT
 */

define(function () {
    "use strict";

    var FileSystem     = brackets.getModule("filesystem/FileSystem"),
        ExtensionUtils = brackets.getModule("utils/ExtensionUtils");

    var commentRegex = /\/\*([\s\S]*?)\*\//mg,
        scrollbarsRegex = /(?:[^}|,]*)::-webkit-scrollbar(?:[\s\S]*?){(?:[\s\S]*?)}/mg;

    /**
    *  Theme object to encasulate all the logic in one pretty bundle.
    *  The theme will self register when it is created.
    *
    *  * Required settings are fileName and path
    *
    * @constructor
    */
    function Theme(options) {
        var _self = this;
        var fileName = options.fileName;
        _self.options = options;

        // Create a display and a theme name from the file name
        _self.fileName    = fileName;
        _self.path        = options.path;
        _self.displayName = toDisplayName(fileName);
        _self.name        = fileName.substring(0, fileName.lastIndexOf('.'));
    }

    Theme.prototype.load = function(force) {
        var theme = this;

        if (theme.css && !force) {
            return theme;
        }

        var file = FileSystem.getFileForPath (this.path + "/" + this.fileName);
        return readFile(file)
            .then(function(content) {
                var result = extractScrollbars(content);
                theme.scrollbar = result.scrollbar;
                return result.content;
            })
            .then(function(content) {
                return lessify(content, theme);
            })
            .then(function(style) {
                return ExtensionUtils.addEmbeddedStyleSheet(style);
            })
            .then(function(styleNode) {
                theme.css = styleNode;
                return theme;
            })
            .promise();
    };

    /**
    *  Takes all dashes and converts them to white spaces...
    *  Then takes all first letters and capitalizes them.
    */
    function toDisplayName (name) {
        name = name.substring(0, name.lastIndexOf('.')).replace(/-/g, ' ');
        var parts = name.split(" ");

        $.each(parts.slice(0), function (index, part) {
            parts[index] = part[0].toUpperCase() + part.substring(1);
        });

        return parts.join(" ");
    }

    function readFile(file) {
        var deferred = $.Deferred();

        try {
            file.read(function( err, content /*, stat*/ ) {
                if ( err ) {
                    deferred.reject(err);
                    return;
                }

                deferred.resolve(content);
            });
        }
        catch(ex) {
            deferred.reject(false);
        }

        return deferred.promise();
    }

    function extractScrollbars(content) {
        var scrollbar = [];

        // Go through and extract out scrollbar customizations so that we can
        // enable/disable via settings.
        content = content
            .replace(commentRegex, "")
            .replace(scrollbarsRegex, function(match) {
                scrollbar.push(match);
                return "";
            });

        return {
            content: content,
            scrollbar: scrollbar
        };
    }

    function lessify(content, theme) {
        var deferred = $.Deferred(),
            parser = new less.Parser();

        parser.parse("." + theme.name + "{" + content + "}", function (err, tree) {
            if (err) {
                deferred.reject(err);
            }
            else {
                deferred.resolve(tree.toCSS());
            }
        });

        return deferred.promise();
    }

    return Theme;
});

<commit_msg>Added full file object to avoid mismatches of directory paths in different OS platforms<commit_after>/*
 * Brackets Themes Copyright (c) 2014 Miguel Castillo.
 *
 * Licensed under MIT
 */

define(function () {
    "use strict";

    var FileSystem     = brackets.getModule("filesystem/FileSystem"),
        ExtensionUtils = brackets.getModule("utils/ExtensionUtils");

    var commentRegex = /\/\*([\s\S]*?)\*\//mg,
        scrollbarsRegex = /(?:[^}|,]*)::-webkit-scrollbar(?:[\s\S]*?){(?:[\s\S]*?)}/mg;

    /**
    *  Theme object to encasulate all the logic in one pretty bundle.
    *  The theme will self register when it is created.
    *
    *  * Required settings are fileName and path
    *
    * @constructor
    */
    function Theme(options) {
        var _self     = this,
            file      = FileSystem.getFileForPath (options.path + "/" + options.fileName),
            fileName  = options.fileName;

        _self.options     = options;
        _self.file        = file;
        _self.path        = file.parentPath;
        _self.fileName    = fileName;
        _self.displayName = toDisplayName(fileName);
        _self.name        = fileName.substring(0, fileName.lastIndexOf('.'));
    }

    Theme.prototype.load = function(force) {
        var theme = this;

        if (theme.css && !force) {
            return theme;
        }

        return readFile(this.file)
            .then(function(content) {
                var result = extractScrollbars(content);
                theme.scrollbar = result.scrollbar;
                return result.content;
            })
            .then(function(content) {
                return lessify(content, theme);
            })
            .then(function(style) {
                return ExtensionUtils.addEmbeddedStyleSheet(style);
            })
            .then(function(styleNode) {
                theme.css = styleNode;
                return theme;
            })
            .promise();
    };

    /**
    *  Takes all dashes and converts them to white spaces...
    *  Then takes all first letters and capitalizes them.
    */
    function toDisplayName (name) {
        name = name.substring(0, name.lastIndexOf('.')).replace(/-/g, ' ');
        var parts = name.split(" ");

        $.each(parts.slice(0), function (index, part) {
            parts[index] = part[0].toUpperCase() + part.substring(1);
        });

        return parts.join(" ");
    }

    function readFile(file) {
        var deferred = $.Deferred();

        try {
            file.read(function( err, content /*, stat*/ ) {
                if ( err ) {
                    deferred.reject(err);
                    return;
                }

                deferred.resolve(content);
            });
        }
        catch(ex) {
            deferred.reject(false);
        }

        return deferred.promise();
    }

    function extractScrollbars(content) {
        var scrollbar = [];

        // Go through and extract out scrollbar customizations so that we can
        // enable/disable via settings.
        content = content
            .replace(commentRegex, "")
            .replace(scrollbarsRegex, function(match) {
                scrollbar.push(match);
                return "";
            });

        return {
            content: content,
            scrollbar: scrollbar
        };
    }

    function lessify(content, theme) {
        var deferred = $.Deferred(),
            parser = new less.Parser();

        parser.parse("." + theme.name + "{" + content + "}", function (err, tree) {
            if (err) {
                deferred.reject(err);
            }
            else {
                deferred.resolve(tree.toCSS());
            }
        });

        return deferred.promise();
    }

    return Theme;
});

<|endoftext|>


<commit_before>/*
 * Copyright (c) 2018. Stephan Mahieu
 *
 * This file is subject to the terms and conditions defined in
 * file 'LICENSE', which is part of this source code package.
 */

class OptionsUtil {

    static getInterfaceTheme() {
        const defaultValue = "default";
        return new Promise((resolve, reject) => {
            browser.storage.local.get({prefInterfaceTheme: defaultValue}).then(
                result => {
                    resolve(result.prefInterfaceTheme);
                },
                () => {
                    resolve(defaultValue);
                }
            );
        });
    }

    static getMultilineThresholds() {
        const defaultValue = {age: 10, length: 500};
        return new Promise((resolve, reject) => {
            browser.storage.local.get({prefMultilineThresholds: defaultValue}).then(
                result => {
                    resolve(result.prefMultilineThresholds);
                },
                () => {
                    resolve(defaultValue);
                }
            );
        });
    }

    static getCleanupPrefs() {
        return new Promise((resolve, reject) => {
            browser.storage.local.get({
                prefAutomaticCleanup: CleanupConst.DEFAULT_DO_CLEANUP,
                prefKeepDaysHistory : CleanupConst.DEFAULT_DAYS_TO_KEEP
            }).then(
                result => {
                    resolve(result);
                },
                () => {
                    resolve();
                }
            );
        });
    }

    static getFilterPrefs() {
        return new Promise((resolve, reject) => {
            browser.storage.local.get({
                prefDomainFilter: 'all',
                prefDomainList  : [],
                prefFieldList   : [],
                prefRetainType  : 'all',
                prefSaveInIncognitoMode: false
            }).then(
                result => {
                    resolve(result);
                },
                () => {
                    resolve();
                }
            );
        });
    }

    static getShortcutKeysPrefs() {
        return new Promise((resolve, reject) => {
            browser.storage.local.get({
                prefShortcutKeys: {
                    _execute_browser_action: OptionsUtil.getDefaultShortcutKey('_execute_browser_action'),
                    open_fhc               : OptionsUtil.getDefaultShortcutKey('open_fhc'),
                    toggle_display_fields  : OptionsUtil.getDefaultShortcutKey('toggle_display_fields'),
                    fill_recent            : OptionsUtil.getDefaultShortcutKey('fill_recent'),
                    fill_often             : OptionsUtil.getDefaultShortcutKey('fill_often'),
                    clear_filled           : OptionsUtil.getDefaultShortcutKey('clear_filled')
                }
            }).then(
                result => {
                    resolve(result);
                },
                () => {
                    resolve();
                }
            );
        });
    }

    static getShortcutKeysEnablePrefs() {
        return new Promise((resolve, reject) => {
            browser.storage.local.get({
                prefShortcutKeys: {
                    open_fhc_enable               : true,
                    toggle_display_fields_enable  : true,
                    fill_recent_enable            : true,
                    fill_often_enable             : true,
                    clear_filled_enable           : true
                }
            }).then(
                result => {
                    resolve(result);
                },
                () => {
                    resolve();
                }
            );
        });
    }

    static applyShortcutKeysPrefs() {
        OptionsUtil.getShortcutKeysPrefs().then((prefs) => {

            // get all shortcut commands (max 4 for chrome)
            browser.commands.getAll().then( (commands) => (

                // for each command change the shortcut where preference differs from default
                commands.forEach(function(command) {
                    const prefShortcut = prefs.prefShortcutKeys[command.name];
                    if (prefShortcut !== command.shortcut) {
                        browser.commands.update({
                            name: command.name,
                            shortcut: prefShortcut
                        });
                    }
                })
            ));
        });
    }

    static getDefaultShortcutKey(commandName) {
        // defaults must be equal to the defaults in manifest.json
        switch(commandName) {
            case '_execute_browser_action': return 'Alt+Shift+P';
            case 'open_fhc':                return 'Alt+Shift+F';
            case 'toggle_display_fields':   return 'Alt+Shift+D';
            case 'fill_recent':             return 'Alt+Shift+R';
            case 'fill_often':              return 'Alt+Shift+O';
            case 'clear_filled':            return 'Alt+Shift+C';
        }
    }

    static isDomainfilterActive(filterPrefs) {
        return 'all' !== filterPrefs.prefDomainFilter;
    }

    static isDomainBlocked(domain, filterPrefs) {
        if (!domain) {
            // internal browser windows like about: have no domain, add-ons can not access these
            return true;
        }

        switch (filterPrefs.prefDomainFilter) {
            case 'all':
                return false;

            case 'blacklist':
                return OptionsUtil._wildcardListMatch(domain, filterPrefs.prefDomainList);

            case 'whitelist':
                return !OptionsUtil._wildcardListMatch(domain, filterPrefs.prefDomainList);

            default:
                console.warn('Unimplemented domainfilter ' + filterPrefs.prefDomainFilter);
                return false;
        }
    }

    static isTextFieldBlocked(domain, fieldname, filterPrefs) {
        return filterPrefs.prefFieldList.length > 0 && OptionsUtil._wildcardFieldMatch(domain, fieldname, filterPrefs.prefFieldList);
    }

    static doRetainSinglelineField(filterPrefs) {
        // not multi-line only
        return 'multi' !== filterPrefs.prefRetainType;
    }

    static doRetainMultilineField(filterPrefs) {
        // not single-line only
        return 'single' !== filterPrefs.prefRetainType;
    }

    static doSaveInIncognitoMode(filterPrefs) {
        return filterPrefs.prefSaveInIncognitoMode;
    }

    static initColPrefs(columnVisPrefs) {
        let newPrefs = [];
        if (Array.isArray(columnVisPrefs)) {
            newPrefs = Array.from(columnVisPrefs);
        }
        for (let i=newPrefs.length; i<=9; i++) {
            newPrefs.push(true);
        }
        return newPrefs;
    }

    static initColSmallPrefs(columnVisPrefs) {
        const newPrefs = OptionsUtil.initColPrefs(columnVisPrefs);
        if (Array.isArray(columnVisPrefs) && columnVisPrefs.length < 9) {
            // visibility not set previously, set default hidden columns
            newPrefs[4] = false;
            newPrefs[8] = false;
            newPrefs[9] = false;
        }
        return newPrefs;
    }

    /**
     * Check if field matches any field in the fieldList,
     * if field in fieldlist contains an optional domain also check against given fieldDomain.
     */
    static _wildcardFieldMatch(fieldDomain, fieldname, fieldList) {
        return fieldList.some( (fieldItem) => {
            let filterField = fieldItem;
            let domainMatch = true;
            if (fieldItem.includes(':')) {
                // check for domain match
                domainMatch = false;
                const parts = fieldItem.split(':');
                const filterDomain = parts[0];
                filterField = parts[1];
                domainMatch = OptionsUtil._wildcardMatch(fieldDomain, filterDomain);
            }
            if (domainMatch) {
                return OptionsUtil._wildcardMatch(fieldname, filterField);
            }
        });
    }

    /** check if value matches any item in the matchList, item may contain wildcards. */
    static _wildcardListMatch(value, matchList) {
        return matchList.some((matchItem) => OptionsUtil._wildcardMatch(value, matchItem));
    }

    /**
     * check if value matches the matchValue, matchValue may contain wildcards.
     *
     * Valid wildcards are:
     * - matchValue starting and/or ending with '*'
     * - matchValue is '<empty>' which only matches an empty value
     *
     */
    static _wildcardMatch(value, matchValue) {
        console.log('Checking value [' + value + '] against matchValue [' + matchValue + ']');
        if (matchValue === '<empty>' && value === '') {
            return true;
        }
        const matchProp = OptionsUtil._getWildcardProps(matchValue);

        if (!matchProp.startsWithWildcard && !matchProp.endsWithWildcard) {
            // match exactly?
            return matchValue === value;
        }
        if (matchProp.startsWithWildcard && matchProp.endsWithWildcard) {
            // substring match?
            return value.includes(matchProp.valueClean);
        }
        if (matchProp.startsWithWildcard) {
            // matches end?
            return value.endsWith(matchProp.valueClean);
        }
        // matches begin?
        return value.startsWith(matchProp.valueClean);
    }

    static _getWildcardProps(matchValue) {
        const result = {
            startsWithWildcard: false,
            endsWithWildcard: false,
            valueClean: ''
        };

        let startIdx = 0;
        let endIdx = matchValue.length;

        if (matchValue.startsWith('*')) {
            result.startsWithWildcard = true;
            startIdx = 1;
        }
        if (matchValue.endsWith('*')) {
            result.endsWithWildcard = true;
            --endIdx;
        }
        result.valueClean = matchValue.substring(startIdx, endIdx);

        return result;
    }
}
<commit_msg>Prevent exception if browser.commands.update() unavailable<commit_after>/*
 * Copyright (c) 2018. Stephan Mahieu
 *
 * This file is subject to the terms and conditions defined in
 * file 'LICENSE', which is part of this source code package.
 */

class OptionsUtil {

    static getInterfaceTheme() {
        const defaultValue = "default";
        return new Promise((resolve, reject) => {
            browser.storage.local.get({prefInterfaceTheme: defaultValue}).then(
                result => {
                    resolve(result.prefInterfaceTheme);
                },
                () => {
                    resolve(defaultValue);
                }
            );
        });
    }

    static getMultilineThresholds() {
        const defaultValue = {age: 10, length: 500};
        return new Promise((resolve, reject) => {
            browser.storage.local.get({prefMultilineThresholds: defaultValue}).then(
                result => {
                    resolve(result.prefMultilineThresholds);
                },
                () => {
                    resolve(defaultValue);
                }
            );
        });
    }

    static getCleanupPrefs() {
        return new Promise((resolve, reject) => {
            browser.storage.local.get({
                prefAutomaticCleanup: CleanupConst.DEFAULT_DO_CLEANUP,
                prefKeepDaysHistory : CleanupConst.DEFAULT_DAYS_TO_KEEP
            }).then(
                result => {
                    resolve(result);
                },
                () => {
                    resolve();
                }
            );
        });
    }

    static getFilterPrefs() {
        return new Promise((resolve, reject) => {
            browser.storage.local.get({
                prefDomainFilter: 'all',
                prefDomainList  : [],
                prefFieldList   : [],
                prefRetainType  : 'all',
                prefSaveInIncognitoMode: false
            }).then(
                result => {
                    resolve(result);
                },
                () => {
                    resolve();
                }
            );
        });
    }

    static getShortcutKeysPrefs() {
        return new Promise((resolve, reject) => {
            browser.storage.local.get({
                prefShortcutKeys: {
                    _execute_browser_action: OptionsUtil.getDefaultShortcutKey('_execute_browser_action'),
                    open_fhc               : OptionsUtil.getDefaultShortcutKey('open_fhc'),
                    toggle_display_fields  : OptionsUtil.getDefaultShortcutKey('toggle_display_fields'),
                    fill_recent            : OptionsUtil.getDefaultShortcutKey('fill_recent'),
                    fill_often             : OptionsUtil.getDefaultShortcutKey('fill_often'),
                    clear_filled           : OptionsUtil.getDefaultShortcutKey('clear_filled')
                }
            }).then(
                result => {
                    resolve(result);
                },
                () => {
                    resolve();
                }
            );
        });
    }

    static getShortcutKeysEnablePrefs() {
        return new Promise((resolve, reject) => {
            browser.storage.local.get({
                prefShortcutKeys: {
                    open_fhc_enable               : true,
                    toggle_display_fields_enable  : true,
                    fill_recent_enable            : true,
                    fill_often_enable             : true,
                    clear_filled_enable           : true
                }
            }).then(
                result => {
                    resolve(result);
                },
                () => {
                    resolve();
                }
            );
        });
    }

    static applyShortcutKeysPrefs() {
        if (!browser.commands.update) {
            return;
        }
        OptionsUtil.getShortcutKeysPrefs().then((prefs) => {

            // get all shortcut commands (max 4 for chrome)
            browser.commands.getAll().then( (commands) => (

                // for each command change the shortcut where preference differs from default
                commands.forEach(function(command) {
                    const prefShortcut = prefs.prefShortcutKeys[command.name];
                    if (prefShortcut !== command.shortcut) {
                        browser.commands.update({
                            name: command.name,
                            shortcut: prefShortcut
                        });
                    }
                })
            ));
        });
    }

    static getDefaultShortcutKey(commandName) {
        // defaults must be equal to the defaults in manifest.json
        switch(commandName) {
            case '_execute_browser_action': return 'Alt+Shift+P';
            case 'open_fhc':                return 'Alt+Shift+F';
            case 'toggle_display_fields':   return 'Alt+Shift+D';
            case 'fill_recent':             return 'Alt+Shift+R';
            case 'fill_often':              return 'Alt+Shift+O';
            case 'clear_filled':            return 'Alt+Shift+C';
        }
    }

    static isDomainfilterActive(filterPrefs) {
        return 'all' !== filterPrefs.prefDomainFilter;
    }

    static isDomainBlocked(domain, filterPrefs) {
        if (!domain) {
            // internal browser windows like about: have no domain, add-ons can not access these
            return true;
        }

        switch (filterPrefs.prefDomainFilter) {
            case 'all':
                return false;

            case 'blacklist':
                return OptionsUtil._wildcardListMatch(domain, filterPrefs.prefDomainList);

            case 'whitelist':
                return !OptionsUtil._wildcardListMatch(domain, filterPrefs.prefDomainList);

            default:
                console.warn('Unimplemented domainfilter ' + filterPrefs.prefDomainFilter);
                return false;
        }
    }

    static isTextFieldBlocked(domain, fieldname, filterPrefs) {
        return filterPrefs.prefFieldList.length > 0 && OptionsUtil._wildcardFieldMatch(domain, fieldname, filterPrefs.prefFieldList);
    }

    static doRetainSinglelineField(filterPrefs) {
        // not multi-line only
        return 'multi' !== filterPrefs.prefRetainType;
    }

    static doRetainMultilineField(filterPrefs) {
        // not single-line only
        return 'single' !== filterPrefs.prefRetainType;
    }

    static doSaveInIncognitoMode(filterPrefs) {
        return filterPrefs.prefSaveInIncognitoMode;
    }

    static initColPrefs(columnVisPrefs) {
        let newPrefs = [];
        if (Array.isArray(columnVisPrefs)) {
            newPrefs = Array.from(columnVisPrefs);
        }
        for (let i=newPrefs.length; i<=9; i++) {
            newPrefs.push(true);
        }
        return newPrefs;
    }

    static initColSmallPrefs(columnVisPrefs) {
        const newPrefs = OptionsUtil.initColPrefs(columnVisPrefs);
        if (Array.isArray(columnVisPrefs) && columnVisPrefs.length < 9) {
            // visibility not set previously, set default hidden columns
            newPrefs[4] = false;
            newPrefs[8] = false;
            newPrefs[9] = false;
        }
        return newPrefs;
    }

    /**
     * Check if field matches any field in the fieldList,
     * if field in fieldlist contains an optional domain also check against given fieldDomain.
     */
    static _wildcardFieldMatch(fieldDomain, fieldname, fieldList) {
        return fieldList.some( (fieldItem) => {
            let filterField = fieldItem;
            let domainMatch = true;
            if (fieldItem.includes(':')) {
                // check for domain match
                domainMatch = false;
                const parts = fieldItem.split(':');
                const filterDomain = parts[0];
                filterField = parts[1];
                domainMatch = OptionsUtil._wildcardMatch(fieldDomain, filterDomain);
            }
            if (domainMatch) {
                return OptionsUtil._wildcardMatch(fieldname, filterField);
            }
        });
    }

    /** check if value matches any item in the matchList, item may contain wildcards. */
    static _wildcardListMatch(value, matchList) {
        return matchList.some((matchItem) => OptionsUtil._wildcardMatch(value, matchItem));
    }

    /**
     * check if value matches the matchValue, matchValue may contain wildcards.
     *
     * Valid wildcards are:
     * - matchValue starting and/or ending with '*'
     * - matchValue is '<empty>' which only matches an empty value
     *
     */
    static _wildcardMatch(value, matchValue) {
        console.log('Checking value [' + value + '] against matchValue [' + matchValue + ']');
        if (matchValue === '<empty>' && value === '') {
            return true;
        }
        const matchProp = OptionsUtil._getWildcardProps(matchValue);

        if (!matchProp.startsWithWildcard && !matchProp.endsWithWildcard) {
            // match exactly?
            return matchValue === value;
        }
        if (matchProp.startsWithWildcard && matchProp.endsWithWildcard) {
            // substring match?
            return value.includes(matchProp.valueClean);
        }
        if (matchProp.startsWithWildcard) {
            // matches end?
            return value.endsWith(matchProp.valueClean);
        }
        // matches begin?
        return value.startsWith(matchProp.valueClean);
    }

    static _getWildcardProps(matchValue) {
        const result = {
            startsWithWildcard: false,
            endsWithWildcard: false,
            valueClean: ''
        };

        let startIdx = 0;
        let endIdx = matchValue.length;

        if (matchValue.startsWith('*')) {
            result.startsWithWildcard = true;
            startIdx = 1;
        }
        if (matchValue.endsWith('*')) {
            result.endsWithWildcard = true;
            --endIdx;
        }
        result.valueClean = matchValue.substring(startIdx, endIdx);

        return result;
    }
}
<|endoftext|>