<commit_before>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @flow
 */

'use strict';

const invariant = require('invariant');
const warnOnce = require('warnOnce');

// Export React, plus some native additions.
module.exports = {
  // Components
  get AccessibilityInfo() {
    return require('AccessibilityInfo');
  },
  get ActivityIndicator() {
    return require('ActivityIndicator');
  },
  get ART() {
    return require('ReactNativeART');
  },
  get Button() {
    return require('Button');
  },
  get CheckBox() {
    return require('CheckBox');
  },
  get DatePickerIOS() {
    return require('DatePickerIOS');
  },
  get DrawerLayoutAndroid() {
    return require('DrawerLayoutAndroid');
  },
  get FlatList() {
    return require('FlatList');
  },
  get Image() {
    return require('Image');
  },
  get ImageBackground() {
    return require('ImageBackground');
  },
  get ImageEditor() {
    warnOnce(
      'image-editor-moved',
      'Image Editor has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/image-editor' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-image-editor',
    );
    return require('ImageEditor');
  },
  get ImageStore() {
    warnOnce(
      'imagestore-deprecation',
      'ImageStore is deprecated and will be removed in a future release. ' +
        'To get a base64-encoded string from a local image use either of the following third-party libraries:' +
        "* expo-file-system: `readAsStringAsync(filepath, 'base64')`" +
        "* react-native-fs: `readFile(filepath, 'base64')`",
    );
    return require('ImageStore');
  },
  get InputAccessoryView() {
    return require('InputAccessoryView');
  },
  get KeyboardAvoidingView() {
    return require('KeyboardAvoidingView');
  },
  get MaskedViewIOS() {
    warnOnce(
      'maskedviewios-moved',
      'MaskedViewIOS has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/masked-view' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-masked-view',
    );
    return require('MaskedViewIOS');
  },
  get Modal() {
    return require('Modal');
  },
  get Picker() {
    return require('Picker');
  },
  get PickerIOS() {
    return require('PickerIOS');
  },
  get ProgressBarAndroid() {
    return require('ProgressBarAndroid');
  },
  get ProgressViewIOS() {
    return require('ProgressViewIOS');
  },
  get SafeAreaView() {
    return require('SafeAreaView');
  },
  get ScrollView() {
    return require('ScrollView');
  },
  get SectionList() {
    return require('SectionList');
  },
  get SegmentedControlIOS() {
    return require('SegmentedControlIOS');
  },
  get Slider() {
    warnOnce(
      'slider-moved',
      'Slider has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/slider' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-slider',
    );
    return require('Slider');
  },
  get Switch() {
    return require('Switch');
  },
  get RefreshControl() {
    return require('RefreshControl');
  },
  get StatusBar() {
    warnOnce(
      'statusbar-moved',
      'StatusBar has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/status-bar' instead of 'react-native'. " +
        'https://github.com/react-native-community/react-native-statusbar',
    );
    return require('StatusBar');
  },
  get SwipeableFlatList() {
    return require('SwipeableFlatList');
  },
  get Text() {
    return require('Text');
  },
  get TextInput() {
    return require('TextInput');
  },
  get ToolbarAndroid() {
    return require('ToolbarAndroid');
  },
  get Touchable() {
    return require('Touchable');
  },
  get TouchableHighlight() {
    return require('TouchableHighlight');
  },
  get TouchableNativeFeedback() {
    return require('TouchableNativeFeedback');
  },
  get TouchableOpacity() {
    return require('TouchableOpacity');
  },
  get TouchableWithoutFeedback() {
    return require('TouchableWithoutFeedback');
  },
  get View() {
    return require('View');
  },
  get ViewPagerAndroid() {
    warnOnce(
      'viewpager-moved',
      'ViewPagerAndroid has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/viewpager' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-viewpager',
    );
    return require('ViewPagerAndroid');
  },
  get VirtualizedList() {
    return require('VirtualizedList');
  },
  get WebView() {
    warnOnce(
      'webview-moved',
      'WebView has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from 'react-native-webview' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-webview',
    );
    return require('WebView');
  },

  // APIs
  get ActionSheetIOS() {
    return require('ActionSheetIOS');
  },
  get Alert() {
    return require('Alert');
  },
  get AlertIOS() {
    warnOnce(
      'alert-ios',
      'AlertIOS is deprecated. Use the `Alert` module directly instead.',
    );
    return require('Alert');
  },
  get Animated() {
    return require('Animated');
  },
  get AppRegistry() {
    return require('AppRegistry');
  },
  get AppState() {
    return require('AppState');
  },
  get AsyncStorage() {
    warnOnce(
      'async-storage-moved',
      'Async Storage has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/async-storage' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-async-storage',
    );
    return require('AsyncStorage');
  },
  get BackHandler() {
    return require('BackHandler');
  },
  get CameraRoll() {
    warnOnce(
      'cameraroll-moved',
      'CameraRoll has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/cameraroll' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-cameraroll',
    );
    return require('CameraRoll');
  },
  get Clipboard() {
    return require('Clipboard');
  },
  get DatePickerAndroid() {
    return require('DatePickerAndroid');
  },
  get DeviceInfo() {
    return require('DeviceInfo');
  },
  get Dimensions() {
    return require('Dimensions');
  },
  get Easing() {
    return require('Easing');
  },
  get findNodeHandle() {
    return require('ReactNative').findNodeHandle;
  },
  get I18nManager() {
    return require('I18nManager');
  },
  get ImagePickerIOS() {
    return require('ImagePickerIOS');
  },
  get InteractionManager() {
    return require('InteractionManager');
  },
  get Keyboard() {
    return require('Keyboard');
  },
  get LayoutAnimation() {
    return require('LayoutAnimation');
  },
  get Linking() {
    return require('Linking');
  },
  get NativeEventEmitter() {
    return require('NativeEventEmitter');
  },
  get NetInfo() {
    warnOnce(
      'netinfo-moved',
      'NetInfo has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/netinfo' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-netinfo',
    );
    return require('NetInfo');
  },
  get PanResponder() {
    return require('PanResponder');
  },
  get PermissionsAndroid() {
    return require('PermissionsAndroid');
  },
  get PixelRatio() {
    return require('PixelRatio');
  },
  get PushNotificationIOS() {
    warnOnce(
      'pushNotificationIOS-moved',
      'PushNotificationIOS has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/push-notification-ios' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-push-notification-ios',
    );
    return require('PushNotificationIOS');
  },
  get Settings() {
    return require('Settings');
  },
  get Share() {
    return require('Share');
  },
  get StatusBarIOS() {
    return require('StatusBarIOS');
  },
  get StyleSheet() {
    return require('StyleSheet');
  },
  get Systrace() {
    return require('Systrace');
  },
  get TimePickerAndroid() {
    return require('TimePickerAndroid');
  },
  get ToastAndroid() {
    return require('ToastAndroid');
  },
  get TVEventHandler() {
    return require('TVEventHandler');
  },
  get UIManager() {
    return require('UIManager');
  },
  get unstable_batchedUpdates() {
    return require('ReactNative').unstable_batchedUpdates;
  },
  get UTFSequence() {
    return require('UTFSequence');
  },
  get Vibration() {
    return require('Vibration');
  },
  get YellowBox() {
    return require('YellowBox');
  },

  // Plugins
  get DeviceEventEmitter() {
    return require('RCTDeviceEventEmitter');
  },
  get NativeAppEventEmitter() {
    return require('RCTNativeAppEventEmitter');
  },
  get NativeModules() {
    return require('NativeModules');
  },
  get Platform() {
    return require('Platform');
  },
  get processColor() {
    return require('processColor');
  },
  get requireNativeComponent() {
    return require('requireNativeComponent');
  },

  // Prop Types
  get ColorPropType() {
    return require('DeprecatedColorPropType');
  },
  get EdgeInsetsPropType() {
    return require('DeprecatedEdgeInsetsPropType');
  },
  get PointPropType() {
    return require('DeprecatedPointPropType');
  },
  get ViewPropTypes() {
    return require('DeprecatedViewPropTypes');
  },
};

if (__DEV__) {
  // $FlowFixMe This is intentional: Flow will error when attempting to access ListView.
  Object.defineProperty(module.exports, 'ListView', {
    configurable: true,
    get() {
      invariant(
        false,
        'ListView has been removed from React Native. ' +
          'See https://fb.me/nolistview for more information or use ' +
          '`deprecated-react-native-listview`.',
      );
    },
  });

  // $FlowFixMe This is intentional: Flow will error when attempting to access SwipeableListView.
  Object.defineProperty(module.exports, 'SwipeableListView', {
    configurable: true,
    get() {
      invariant(
        false,
        'SwipeableListView has been removed from React Native. ' +
          'See https://fb.me/nolistview for more information or use ' +
          '`deprecated-react-native-swipeable-listview`.',
      );
    },
  });
}
<commit_msg>Remove AlertIOS<commit_after>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @flow
 */

'use strict';

const invariant = require('invariant');
const warnOnce = require('warnOnce');

// Export React, plus some native additions.
module.exports = {
  // Components
  get AccessibilityInfo() {
    return require('AccessibilityInfo');
  },
  get ActivityIndicator() {
    return require('ActivityIndicator');
  },
  get ART() {
    return require('ReactNativeART');
  },
  get Button() {
    return require('Button');
  },
  get CheckBox() {
    return require('CheckBox');
  },
  get DatePickerIOS() {
    return require('DatePickerIOS');
  },
  get DrawerLayoutAndroid() {
    return require('DrawerLayoutAndroid');
  },
  get FlatList() {
    return require('FlatList');
  },
  get Image() {
    return require('Image');
  },
  get ImageBackground() {
    return require('ImageBackground');
  },
  get ImageEditor() {
    warnOnce(
      'image-editor-moved',
      'Image Editor has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/image-editor' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-image-editor',
    );
    return require('ImageEditor');
  },
  get ImageStore() {
    warnOnce(
      'imagestore-deprecation',
      'ImageStore is deprecated and will be removed in a future release. ' +
        'To get a base64-encoded string from a local image use either of the following third-party libraries:' +
        "* expo-file-system: `readAsStringAsync(filepath, 'base64')`" +
        "* react-native-fs: `readFile(filepath, 'base64')`",
    );
    return require('ImageStore');
  },
  get InputAccessoryView() {
    return require('InputAccessoryView');
  },
  get KeyboardAvoidingView() {
    return require('KeyboardAvoidingView');
  },
  get MaskedViewIOS() {
    warnOnce(
      'maskedviewios-moved',
      'MaskedViewIOS has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/masked-view' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-masked-view',
    );
    return require('MaskedViewIOS');
  },
  get Modal() {
    return require('Modal');
  },
  get Picker() {
    return require('Picker');
  },
  get PickerIOS() {
    return require('PickerIOS');
  },
  get ProgressBarAndroid() {
    return require('ProgressBarAndroid');
  },
  get ProgressViewIOS() {
    return require('ProgressViewIOS');
  },
  get SafeAreaView() {
    return require('SafeAreaView');
  },
  get ScrollView() {
    return require('ScrollView');
  },
  get SectionList() {
    return require('SectionList');
  },
  get SegmentedControlIOS() {
    return require('SegmentedControlIOS');
  },
  get Slider() {
    warnOnce(
      'slider-moved',
      'Slider has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/slider' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-slider',
    );
    return require('Slider');
  },
  get Switch() {
    return require('Switch');
  },
  get RefreshControl() {
    return require('RefreshControl');
  },
  get StatusBar() {
    warnOnce(
      'statusbar-moved',
      'StatusBar has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/status-bar' instead of 'react-native'. " +
        'https://github.com/react-native-community/react-native-statusbar',
    );
    return require('StatusBar');
  },
  get SwipeableFlatList() {
    return require('SwipeableFlatList');
  },
  get Text() {
    return require('Text');
  },
  get TextInput() {
    return require('TextInput');
  },
  get ToolbarAndroid() {
    return require('ToolbarAndroid');
  },
  get Touchable() {
    return require('Touchable');
  },
  get TouchableHighlight() {
    return require('TouchableHighlight');
  },
  get TouchableNativeFeedback() {
    return require('TouchableNativeFeedback');
  },
  get TouchableOpacity() {
    return require('TouchableOpacity');
  },
  get TouchableWithoutFeedback() {
    return require('TouchableWithoutFeedback');
  },
  get View() {
    return require('View');
  },
  get ViewPagerAndroid() {
    warnOnce(
      'viewpager-moved',
      'ViewPagerAndroid has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/viewpager' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-viewpager',
    );
    return require('ViewPagerAndroid');
  },
  get VirtualizedList() {
    return require('VirtualizedList');
  },
  get WebView() {
    warnOnce(
      'webview-moved',
      'WebView has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from 'react-native-webview' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-webview',
    );
    return require('WebView');
  },

  // APIs
  get ActionSheetIOS() {
    return require('ActionSheetIOS');
  },
  get Alert() {
    return require('Alert');
  },
  get Animated() {
    return require('Animated');
  },
  get AppRegistry() {
    return require('AppRegistry');
  },
  get AppState() {
    return require('AppState');
  },
  get AsyncStorage() {
    warnOnce(
      'async-storage-moved',
      'Async Storage has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/async-storage' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-async-storage',
    );
    return require('AsyncStorage');
  },
  get BackHandler() {
    return require('BackHandler');
  },
  get CameraRoll() {
    warnOnce(
      'cameraroll-moved',
      'CameraRoll has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/cameraroll' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-cameraroll',
    );
    return require('CameraRoll');
  },
  get Clipboard() {
    return require('Clipboard');
  },
  get DatePickerAndroid() {
    return require('DatePickerAndroid');
  },
  get DeviceInfo() {
    return require('DeviceInfo');
  },
  get Dimensions() {
    return require('Dimensions');
  },
  get Easing() {
    return require('Easing');
  },
  get findNodeHandle() {
    return require('ReactNative').findNodeHandle;
  },
  get I18nManager() {
    return require('I18nManager');
  },
  get ImagePickerIOS() {
    return require('ImagePickerIOS');
  },
  get InteractionManager() {
    return require('InteractionManager');
  },
  get Keyboard() {
    return require('Keyboard');
  },
  get LayoutAnimation() {
    return require('LayoutAnimation');
  },
  get Linking() {
    return require('Linking');
  },
  get NativeEventEmitter() {
    return require('NativeEventEmitter');
  },
  get NetInfo() {
    warnOnce(
      'netinfo-moved',
      'NetInfo has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/netinfo' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-netinfo',
    );
    return require('NetInfo');
  },
  get PanResponder() {
    return require('PanResponder');
  },
  get PermissionsAndroid() {
    return require('PermissionsAndroid');
  },
  get PixelRatio() {
    return require('PixelRatio');
  },
  get PushNotificationIOS() {
    warnOnce(
      'pushNotificationIOS-moved',
      'PushNotificationIOS has been extracted from react-native core and will be removed in a future release. ' +
        "It can now be installed and imported from '@react-native-community/push-notification-ios' instead of 'react-native'. " +
        'See https://github.com/react-native-community/react-native-push-notification-ios',
    );
    return require('PushNotificationIOS');
  },
  get Settings() {
    return require('Settings');
  },
  get Share() {
    return require('Share');
  },
  get StatusBarIOS() {
    return require('StatusBarIOS');
  },
  get StyleSheet() {
    return require('StyleSheet');
  },
  get Systrace() {
    return require('Systrace');
  },
  get TimePickerAndroid() {
    return require('TimePickerAndroid');
  },
  get ToastAndroid() {
    return require('ToastAndroid');
  },
  get TVEventHandler() {
    return require('TVEventHandler');
  },
  get UIManager() {
    return require('UIManager');
  },
  get unstable_batchedUpdates() {
    return require('ReactNative').unstable_batchedUpdates;
  },
  get UTFSequence() {
    return require('UTFSequence');
  },
  get Vibration() {
    return require('Vibration');
  },
  get YellowBox() {
    return require('YellowBox');
  },

  // Plugins
  get DeviceEventEmitter() {
    return require('RCTDeviceEventEmitter');
  },
  get NativeAppEventEmitter() {
    return require('RCTNativeAppEventEmitter');
  },
  get NativeModules() {
    return require('NativeModules');
  },
  get Platform() {
    return require('Platform');
  },
  get processColor() {
    return require('processColor');
  },
  get requireNativeComponent() {
    return require('requireNativeComponent');
  },

  // Prop Types
  get ColorPropType() {
    return require('DeprecatedColorPropType');
  },
  get EdgeInsetsPropType() {
    return require('DeprecatedEdgeInsetsPropType');
  },
  get PointPropType() {
    return require('DeprecatedPointPropType');
  },
  get ViewPropTypes() {
    return require('DeprecatedViewPropTypes');
  },
};

if (__DEV__) {
  // $FlowFixMe This is intentional: Flow will error when attempting to access ListView.
  Object.defineProperty(module.exports, 'ListView', {
    configurable: true,
    get() {
      invariant(
        false,
        'ListView has been removed from React Native. ' +
          'See https://fb.me/nolistview for more information or use ' +
          '`deprecated-react-native-listview`.',
      );
    },
  });

  // $FlowFixMe This is intentional: Flow will error when attempting to access SwipeableListView.
  Object.defineProperty(module.exports, 'SwipeableListView', {
    configurable: true,
    get() {
      invariant(
        false,
        'SwipeableListView has been removed from React Native. ' +
          'See https://fb.me/nolistview for more information or use ' +
          '`deprecated-react-native-swipeable-listview`.',
      );
    },
  });
}
<|endoftext|>


<commit_before>
import OffsetKey from '../utils/offset-key'
import React from 'react'
import ReactDOM from 'react-dom'

/**
 * Leaf.
 */

class Leaf extends React.Component {

  /**
   * Properties.
   */

  static propTypes = {
    index: React.PropTypes.number.isRequired,
    marks: React.PropTypes.object.isRequired,
    node: React.PropTypes.object.isRequired,
    renderMark: React.PropTypes.func.isRequired,
    state: React.PropTypes.object.isRequired,
    text: React.PropTypes.string.isRequired
  };

  /**
   * Should component update?
   *
   * @param {Object} props
   * @return {Boolean} shouldUpdate
   */

  shouldComponentUpdate(props) {
    const { index, node, state } = props
    const { selection } = state

    if (
      props.index != this.props.index ||
      props.text != this.props.text ||
      props.marks != this.props.marks
    ) {
      return true
    }

    const { start, end } = OffsetKey.findBounds(node.key, index, state)
    return selection.hasEdgeBetween(node, start, end)
  }

  componentDidMount() {
    this.updateSelection()
  }

  componentDidUpdate() {
    this.updateSelection()
  }

  updateSelection() {
    const { state } = this.props
    const { selection } = state

    // If the selection is not focused we have nothing to do.
    if (!selection.isFocused) return

    const { anchorOffset, focusOffset } = selection
    const { node, index } = this.props
    const { start, end } = OffsetKey.findBounds(node.key, index, state)

    // If neither matches, the selection doesn't start or end here, so exit.
    const hasAnchor = selection.hasAnchorBetween(node, start, end)
    const hasFocus = selection.hasFocusBetween(node, start, end)
    if (!hasAnchor && !hasFocus) return

    // We have a selection to render, so prepare a few things...
    const native = window.getSelection()
    const el = ReactDOM.findDOMNode(this).firstChild

    // If both the start and end are here, set the selection all at once.
    if (hasAnchor && hasFocus) {
      native.removeAllRanges()
      const range = window.document.createRange()
      range.setStart(el, anchorOffset - start)
      native.addRange(range)
      native.extend(el, focusOffset - start)
      return
    }

    // If the selection is forward, we can set things in sequence. In
    // the first leaf to render, reset the selection and set the new start. And
    // then in the second leaf to render, extend to the new end.
    if (selection.isForward) {
      if (hasAnchor) {
        native.removeAllRanges()
        const range = window.document.createRange()
        range.setStart(el, anchorOffset - start)
        native.addRange(range)
      } else if (hasFocus) {
        native.extend(el, focusOffset - start)
      }
    }

    // Otherwise, if the selection is backward, we need to hack the order a bit.
    // In the first leaf to render, set a phony start anchor to store the true
    // end position. And then in the second leaf to render, set the start and
    // extend the end to the stored value.
    else {
      if (hasFocus) {
        native.removeAllRanges()
        const range = window.document.createRange()
        range.setStart(el, focusOffset - start)
        native.addRange(range)
      } else if (hasAnchor) {
        const endNode = native.focusNode
        const endOffset = native.focusOffset
        native.removeAllRanges()
        const range = window.document.createRange()
        range.setStart(el, anchorOffset - start)
        native.addRange(range)
        native.extend(endNode, endOffset)
      }
    }
  }

  render() {
    const { node, index, text, marks, renderMark } = this.props
    const offsetKey = OffsetKey.stringify({
      key: node.key,
      index
    })

    const style = marks.reduce((memo, mark) => {
      return {
        ...memo,
        ...renderMark(mark, marks),
      }
    }, {})

    return (

        {text ||
}

    )
  }

}

/**
 * Export.
 */

export default Leaf
<commit_msg>fix performance issue in Leaf (#158)<commit_after>
import OffsetKey from '../utils/offset-key'
import React from 'react'
import ReactDOM from 'react-dom'

/**
 * Leaf.
 */

class Leaf extends React.Component {

  /**
   * Properties.
   */

  static propTypes = {
    index: React.PropTypes.number.isRequired,
    marks: React.PropTypes.object.isRequired,
    node: React.PropTypes.object.isRequired,
    renderMark: React.PropTypes.func.isRequired,
    state: React.PropTypes.object.isRequired,
    text: React.PropTypes.string.isRequired
  };

  /**
   * Should component update?
   *
   * @param {Object} props
   * @return {Boolean} shouldUpdate
   */

  shouldComponentUpdate(props) {
    const { index, node, state } = props
    const { selection } = state

    if (
      props.index != this.props.index ||
      props.text != this.props.text ||
      props.marks != this.props.marks
    ) {
      return true
    }

    const { start, end } = OffsetKey.findBounds(node.key, index, state)
    return selection.hasEdgeBetween(node, start, end)
  }

  componentDidMount() {
    this.updateSelection()
  }

  componentDidUpdate() {
    this.updateSelection()
  }

  updateSelection() {
    const { state } = this.props
    const { selection } = state

    // If the selection is not focused we have nothing to do.
    if (!selection.isFocused) return

    const { anchorOffset, focusOffset } = selection
    const { node, index } = this.props
    const { start, end } = OffsetKey.findBounds(node.key, index, state)

    // If neither matches, the selection doesn't start or end here, so exit.
    const hasAnchor = selection.hasAnchorBetween(node, start, end)
    const hasFocus = selection.hasFocusBetween(node, start, end)
    if (!hasAnchor && !hasFocus) return

    // We have a selection to render, so prepare a few things...
    const native = window.getSelection()
    const el = ReactDOM.findDOMNode(this).firstChild

    // If both the start and end are here, set the selection all at once.
    if (hasAnchor && hasFocus) {
      native.removeAllRanges()
      const range = window.document.createRange()
      range.setStart(el, anchorOffset - start)
      native.addRange(range)
      native.extend(el, focusOffset - start)
      return
    }

    // If the selection is forward, we can set things in sequence. In
    // the first leaf to render, reset the selection and set the new start. And
    // then in the second leaf to render, extend to the new end.
    if (selection.isForward) {
      if (hasAnchor) {
        native.removeAllRanges()
        const range = window.document.createRange()
        range.setStart(el, anchorOffset - start)
        native.addRange(range)
      } else if (hasFocus) {
        native.extend(el, focusOffset - start)
      }
    }

    // Otherwise, if the selection is backward, we need to hack the order a bit.
    // In the first leaf to render, set a phony start anchor to store the true
    // end position. And then in the second leaf to render, set the start and
    // extend the end to the stored value.
    else {
      if (hasFocus) {
        native.removeAllRanges()
        const range = window.document.createRange()
        range.setStart(el, focusOffset - start)
        native.addRange(range)
      } else if (hasAnchor) {
        const endNode = native.focusNode
        const endOffset = native.focusOffset
        native.removeAllRanges()
        const range = window.document.createRange()
        range.setStart(el, anchorOffset - start)
        native.addRange(range)
        native.extend(endNode, endOffset)
      }
    }
  }

  render() {
    const { node, index, text, marks, renderMark } = this.props
    const offsetKey = OffsetKey.stringify({
      key: node.key,
      index
    })

    const style = marks.reduce((memo, mark) => {
      const renderedMark = renderMark(mark, marks)
      Object.keys(renderedMark).forEach((k) => {
        memo[k] = renderedMark[k]
      })
      return memo
    }, {})

    return (

        {text ||
}

    )
  }

}

/**
 * Export.
 */

export default Leaf
<|endoftext|>


<commit_before>/*
 * jitsu.js: Top-level include for the jitsu module.
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

require.paths.unshift(__dirname);

var jitsu = exports,
    auth = false,
    started = false;

// Failure HTTP Response codes based
// off of `/lib/broodmother/slave/service.js`
var failCodes = jitsu.failCodes = {
  400: "Bad Request",
  403: "Not Authorized",
  404: "Item not found",
  500: "Internal Server Error"
};

// Success HTTP Response codes based
// off of `/lib/broodmother/slave/service.js`
var successCodes = jitsu.successCodes = {
  200: "OK",
  201: "Created"
};

jitsu.utils         = require('jitsu/utils');
jitsu.log           = require('jitsu/log');
jitsu.api           = {};
jitsu.api.Client    = require('jitsu/api/client').Client;
jitsu.api.Apps      = require('jitsu/api/apps').Apps;
jitsu.api.Snapshots = require('jitsu/api/snapshots').Snapshots;
jitsu.api.Users     = require('jitsu/api/users').Users;
jitsu.prompt        = require('jitsu/prompt');
jitsu.config        = require('jitsu/config');
jitsu.commands      = require('jitsu/commands');

var eyes = require('eyes'),
    winston = require('winston'),
    colors = require('colors');

//
// ### function start (command, callback)
// #### @command {string} Command to execute once started
// #### @callback {function} Continuation to pass control to when complete.
// Starts the jitsu CLI and runs the specified command.
//
jitsu.start = function (command, callback) {
  // Setup the initial prompt but don't leave it open
  jitsu.prompt.start().pause();

  winston.info('Welcome to ' + 'Nodejitsu'.grey);
  winston.info('It worked if it ends with ' + 'Nodejitsu'.grey + ' ok'.yellow.bold);

  jitsu.config.load(function (err) {
    if (!jitsu.config.username) {
      return jitsu.setupUser(function (err) {
        return err ? callback(err) : jitsu.exec(command, callback);
      });
    }

    return err ? callback(err) : jitsu.exec(command, callback);
  });
};

//
// ### function exec (command, callback)
// #### @command {string} Command to execute
// #### @callback {function} Continuation to pass control to when complete.
// Runs the specified command in the jitsu CLI.
//
jitsu.exec = function (command, callback) {
  if (!command) {
    winston.error('No command supplied');
    return jitsu.showError(command, {}, true, true);
  }

  function execCommand (err) {
    if (err) {
      return callback(err);
    }

    winston.info('Executing command ' + jitsu.commands.parse(command).splice(0, 2).join(' ').magenta);
    jitsu.commands.run(command, function (err, shallow) {
      if (err) {
        return jitsu.showError(command, err, shallow);
      }

      //
      // TODO (indexzero): Something here
      //
      callback();
    });
  }

  return !started ? jitsu.setup(execCommand) : execCommand();
};

//
// ### function setup (callback)
// #### @callback {function} Continuation to pass control to when complete.
// Sets up the instances of the Resource clients for jitsu and tests
// the username/password combo for the authenticated user.
//
jitsu.setup = function (callback) {
  if (started) {
    return;
  }

  jitsu.users = new jitsu.api.Users(jitsu.config.settings);
  jitsu.apps = new jitsu.api.Apps(jitsu.config.settings);
  jitsu.snapshots = new jitsu.api.Snapshots(jitsu.config.settings);
  started = true;
  callback();
};

//
// ### function auth (callback)
// #### @callback {function} Continuation to pass control to when complete.
// Attempts to authenicate the user identified in `jitsu.config.settings.auth`
//
jitsu.auth = function (callback) {
  if (auth) {
    return callback();
  }

  jitsu.users.auth(function (err, success) {
    if (err || !success) {
      winston.error('Unable to Autenticate as ' + jitsu.config.username.magenta);
      winston.error(err.message);
      return callback(err);
    }

    auth = true;
    winston.info('Authenticated as ' + jitsu.config.username.magenta);
    callback();
  });
};

//
// ### function setupUser (callback)
// #### @callback {function} Continuation to pass control to when complete.
// Prompts the user for their username / password combo, then sets up the jitsu CLI,
// and saves the resulting configuration to disk.
//
jitsu.setupUser = function (callback) {
  winston.info('No user has been setup on this machine');
  jitsu.prompt.get(['username', 'password'], function (result) {
    jitsu.config.settings.auth = [result.username, result.password].join(':');
    jitsu.setup();
    jitsu.auth(function () {
      jitsu.config.save(function (err) {
        return err ? callback(err) : callback();
      });
    });
  });
};

jitsu.showError = function (command, err, shallow, skip) {
  if (err.statusCode !== '403' && !skip) {
    winston.error('Error running command ' + command.magenta);

    if (err.message) {
      winston.error(err.message);
    }

    if (err.result && err.result.error) {
      winston.error(err.result.error);
    }
    else {
      if (err.stack && !shallow) {
        err.stack.split('\n').forEach(function (trace) {
          winston.error(trace);
        });
      }
    }
  }

  winston.info('Nodejitsu '.grey + 'not ok'.red.bold);
};<commit_msg>[fix] Small fix to `jitsu.setup()` to make it more succinct<commit_after>/*
 * jitsu.js: Top-level include for the jitsu module.
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

require.paths.unshift(__dirname);

var jitsu = exports,
    auth = false,
    started = false;

// Failure HTTP Response codes based
// off of `/lib/broodmother/slave/service.js`
var failCodes = jitsu.failCodes = {
  400: "Bad Request",
  403: "Not Authorized",
  404: "Item not found",
  500: "Internal Server Error"
};

// Success HTTP Response codes based
// off of `/lib/broodmother/slave/service.js`
var successCodes = jitsu.successCodes = {
  200: "OK",
  201: "Created"
};

jitsu.utils         = require('jitsu/utils');
jitsu.log           = require('jitsu/log');
jitsu.api           = {};
jitsu.api.Client    = require('jitsu/api/client').Client;
jitsu.api.Apps      = require('jitsu/api/apps').Apps;
jitsu.api.Snapshots = require('jitsu/api/snapshots').Snapshots;
jitsu.api.Users     = require('jitsu/api/users').Users;
jitsu.prompt        = require('jitsu/prompt');
jitsu.config        = require('jitsu/config');
jitsu.commands      = require('jitsu/commands');

var eyes = require('eyes'),
    winston = require('winston'),
    colors = require('colors');

//
// ### function start (command, callback)
// #### @command {string} Command to execute once started
// #### @callback {function} Continuation to pass control to when complete.
// Starts the jitsu CLI and runs the specified command.
//
jitsu.start = function (command, callback) {
  // Setup the initial prompt but don't leave it open
  jitsu.prompt.start().pause();

  winston.info('Welcome to ' + 'Nodejitsu'.grey);
  winston.info('It worked if it ends with ' + 'Nodejitsu'.grey + ' ok'.yellow.bold);

  jitsu.config.load(function (err) {
    if (!jitsu.config.username) {
      return jitsu.setupUser(function (err) {
        return err ? callback(err) : jitsu.exec(command, callback);
      });
    }

    return err ? callback(err) : jitsu.exec(command, callback);
  });
};

//
// ### function exec (command, callback)
// #### @command {string} Command to execute
// #### @callback {function} Continuation to pass control to when complete.
// Runs the specified command in the jitsu CLI.
//
jitsu.exec = function (command, callback) {
  if (!command) {
    winston.error('No command supplied');
    return jitsu.showError(command, {}, true, true);
  }

  function execCommand (err) {
    if (err) {
      return callback(err);
    }

    winston.info('Executing command ' + jitsu.commands.parse(command).splice(0, 2).join(' ').magenta);
    jitsu.commands.run(command, function (err, shallow) {
      if (err) {
        return jitsu.showError(command, err, shallow);
      }

      //
      // TODO (indexzero): Something here
      //
      callback();
    });
  }

  return !started ? jitsu.setup(execCommand) : execCommand();
};

//
// ### function setup (callback)
// #### @callback {function} Continuation to pass control to when complete.
// Sets up the instances of the Resource clients for jitsu and tests
// the username/password combo for the authenticated user.
//
jitsu.setup = function (callback) {
  if (!started) {
    jitsu.users = new jitsu.api.Users(jitsu.config.settings);
    jitsu.apps = new jitsu.api.Apps(jitsu.config.settings);
    jitsu.snapshots = new jitsu.api.Snapshots(jitsu.config.settings);
    started = true;
  }

  callback();
};

//
// ### function auth (callback)
// #### @callback {function} Continuation to pass control to when complete.
// Attempts to authenicate the user identified in `jitsu.config.settings.auth`
//
jitsu.auth = function (callback) {
  if (auth) {
    return callback();
  }

  jitsu.users.auth(function (err, success) {
    if (err || !success) {
      winston.error('Unable to Autenticate as ' + jitsu.config.username.magenta);
      winston.error(err.message);
      return callback(err);
    }

    auth = true;
    winston.info('Authenticated as ' + jitsu.config.username.magenta);
    callback();
  });
};

//
// ### function setupUser (callback)
// #### @callback {function} Continuation to pass control to when complete.
// Prompts the user for their username / password combo, then sets up the jitsu CLI,
// and saves the resulting configuration to disk.
//
jitsu.setupUser = function (callback) {
  winston.info('No user has been setup on this machine');
  jitsu.prompt.get(['username', 'password'], function (result) {
    jitsu.config.settings.auth = [result.username, result.password].join(':');
    jitsu.setup();
    jitsu.auth(function () {
      jitsu.config.save(function (err) {
        return err ? callback(err) : callback();
      });
    });
  });
};

jitsu.showError = function (command, err, shallow, skip) {
  if (err.statusCode !== '403' && !skip) {
    winston.error('Error running command ' + command.magenta);

    if (err.message) {
      winston.error(err.message);
    }

    if (err.result && err.result.error) {
      winston.error(err.result.error);
    }
    else {
      if (err.stack && !shallow) {
        err.stack.split('\n').forEach(function (trace) {
          winston.error(trace);
        });
      }
    }
  }

  winston.info('Nodejitsu '.grey + 'not ok'.red.bold);
};<|endoftext|>


<commit_before>(function(){
    /**
     * Render style for block
     */
    this.subscribe('block/style/update', function(data) {
        var block_info = this.getState().blocks[data.designer_id][data.block_id]

        var baseSelector = '[data-id='+data.block_id+'] .block-content[data-is="vd-block-'+block_info.type+'"]'
        $('head').find('style#'+data.designer_id+'-'+data.block_id).remove();

        var styleTag = '<style id="'+data.designer_id+'-'+data.block_id+'">';

        for(var key in data.styles) {
            styleTag += baseSelector+' '+key+'{';
            for(var name in data.styles[key]){
                if(data.styles[key][name]){
                    styleTag += name + ':'+data.styles[key][name]+';'
                }
            }
            styleTag += '}';
        }
        styleTag += '<style>'
        $('head').append(styleTag);
    })
    /**
     * Render style for block
     */
    this.subscribe('block/style/media/update', function(data) {
        var block_info = this.getState().blocks[data.designer_id][data.block_id]

        var baseSelector = '[data-id='+data.block_id+'] .block-content[data-is="vd-block-'+block_info.type+'"]'
        $('head').find('style#'+data.designer_id+'-'+data.block_id).remove();
        var medias = {
            'phone': '@media screen and (max-width: 767px)',
            'tablet': '@media screen and (min-width: 768px) and (max-width: 991px)',
            'desktop': '@media screen and (min-width: 992px)'
        }
        var styleTag = '<style id="'+data.designer_id+'-'+data.block_id+'">';

        for(var key in data.styles) {
            styleTag += medias[key]+'{';

            var styles = data.styles[key]
            for (var selector in styles) {
                styleTag += baseSelector+' '+selector+'{';
                for(var name in styles[selector]){
                    if(styles[selector][name]){
                        styleTag += name + ':'+styles[selector][name]+';'
                    }
                }
                styleTag += '}';
            }

            styleTag += '}';
        }
        styleTag += '<style>'

        $('head').append(styleTag);
    })
}.bind(d_visual_designer))()<commit_msg>Fix bugs<commit_after>(function(){
    /**
     * Render style for block
     */
    this.subscribe('block/style/update', function(data) {
        var block_info = this.getState().blocks[data.designer_id][data.block_id]

        var baseSelector = '[data-id='+data.block_id+'] .block-content[data-is="vd-block-'+block_info.type+'"]'
        $('head').find('style#'+data.designer_id+'-'+data.block_id).remove();

        var styleTag = '<style id="'+data.designer_id+'-'+data.block_id+'">';

        for(var key in data.styles) {
            styleTag += baseSelector+' '+key+'{';
            for(var name in data.styles[key]){
                if(data.styles[key][name]){
                    styleTag += name + ':'+data.styles[key][name]+';'
                }
            }
            styleTag += '}';
        }
        styleTag += '<style>'
        $('head').append(styleTag);
    })
    /**
     * Render style for block
     */
    this.subscribe('block/style/media/update', function(data) {
        var block_info = this.getState().blocks[data.designer_id][data.block_id]

        var baseSelector = '[data-id='+data.block_id+'] .block-content[data-is="vd-block-'+block_info.type+'"]'
        $('head').find('style#media-'+data.designer_id+'-'+data.block_id).remove();
        var medias = {
            'phone': '@media screen and (max-width: 767px)',
            'tablet': '@media screen and (min-width: 768px) and (max-width: 991px)',
            'desktop': '@media screen and (min-width: 992px)'
        }
        var styleTag = '<style id="media-'+data.designer_id+'-'+data.block_id+'">';

        for(var key in data.styles) {
            styleTag += medias[key]+'{';

            var styles = data.styles[key]
            for (var selector in styles) {
                styleTag += baseSelector+' '+selector+'{';
                for(var name in styles[selector]){
                    if(styles[selector][name]){
                        styleTag += name + ':'+styles[selector][name]+';'
                    }
                }
                styleTag += '}';
            }

            styleTag += '}';
        }
        styleTag += '<style>'

        $('head').append(styleTag);
    })
}.bind(d_visual_designer))()<|endoftext|>


<commit_before>// Dependencies that makes this all work! Yay packages!
var gulp = require('gulp'),
    cleanCSS = require('gulp-clean-css'),
    del = require('del'),
    inlineCSS = require('gulp-inline-css'),
    preprocess = require('gulp-preprocess'),
    rename = require('gulp-rename'),
    strip = require('gulp-strip-comments');

// Load in environment variables from .boilerplate.defaults first and then .boilerplate.custom if it exists
var boilerplateConfig = require('dotenv-extended').load(
    {
        encoding: 'utf8',
        silent: true,
        path: '.boilerplate.custom',
        defaults: '.boilerplate.defaults',
        errorOnMissing: false,
        errorOnExtra: false,
        assignToProcessEnv: true,
        overrideProcessEnv: true
    }
);

// We need to check if DOCTYPE_VERSION is using an XHTML value for inline-css purposes later
var currentSetDoctype = process.env.DOCTYPE_VERSION;
if (/XHTML/i.test(currentSetDoctype)) {
    xmlMode = true;
} else {
    xmlMode = false;
}

// Clean out the build directories
gulp.task('clean', function () {
    return del(
        [
            'tmp/css/**',
            'tmp/html-samples/**',
            'dist/boilerplate/**'
        ]
    ).then(paths => {
        console.log('Cleaning directories :\n', paths.join('\n'));
    });
});

// Pre-process CSS files with config values
gulp.task('preprocess-css', function() {
    var stream = gulp.src('./app/css/*.css')
    .pipe(preprocess())
    .pipe(gulp.dest('./tmp/css/'))
    return stream;
});

// Minify processed CSS for inclusion in the email boilerplate
gulp.task('minify-css', ['preprocess-css'], function() {
    var stream = gulp.src('./tmp/css/*.css')
    .pipe(cleanCSS(
        {
            // We don't want to go too crazy when minifying, this is email after all!
            advanced: false,
            aggressiveMerging: false,
            debug: true, // Needed for the size output on before and after min files
            keepBreaks: true,
            keepSpecialComments: 0,
            shorthandCompacting: false,
            // Defensive for less friendly email clients
            compatibility: '*,' +
            '-properties.colors,' +
            '-properties.zeroUnits'
        },
        // Log original size and minified size in gulp output
        function(details) {
            console.log(details.name + ': ' + details.stats.originalSize);
            console.log(details.name + '.min' + ': ' + details.stats.minifiedSize);
        })
    )
    .pipe(rename({ extname: '.css.min' }))
    .pipe(gulp.dest('./tmp/css/'));
    return stream;
});

// Build all HTML samples, inline CSS and strip comments to be included in boilerplate
gulp.task('build-html-samples', function() {
    gulp.src('./app/html-samples/*.html')
    .pipe(preprocess())
    .pipe(inlineCSS(
        {
            applyStyleTags: true,
            removeStyleTags: true,
            removeHtmlSelectors: true,
            applyTableAttributes: true,
            xmlMode: xmlMode
        }
    ))
    .pipe(strip(
        {
            ignore: /<!--\[if(?!\s*(?:\[[^\]]+]|<!|>))(?:(?!-->).)*-->/g,
            safe: true,
            trim: true
        }
    ))
    .pipe(gulp.dest('./tmp/html-samples/'));
});

// Build email boilerplate HTML and put everything together!
gulp.task('preprocess-boilerplate', ['minify-css'], function() {
    // We only want to target this file for HTML processing, the guidelines version is static and legacy
    var stream = gulp.src('app/email-boilerplate.html.preprocess')
    .pipe(preprocess({ extension: 'html' }))
    .pipe(rename('email-boilerplate.html'))
    .pipe(gulp.dest('./dist/boilerplate/'))
    return stream;
});

// Inline CSS to main layout elements after boilerplate is generated and update it
gulp.task('inline-css', ['preprocess-boilerplate'], function() {

    var stream = gulp.src('./dist/boilerplate/email-boilerplate.html')
    .pipe(inlineCSS(
        {
            applyTableAttributes: true,
            applyWidthAttributes: true,
            applyStyleTags: false,
            applyLinkTags: true,
            removeStyleTags: false,
            removeLinkTags: true,
            xmlMode: xmlMode
        }
    ))
    .pipe(gulp.dest('./dist/boilerplate/'));
    return stream;
});

// Detect certain configurations and warn when they are less optimal
gulp.task('check-config', ['inline-css'], function() {

    var isMsoNamespacesEnabled = process.env.ENABLE_VML_NAMESPACES;
    var isXuaCompatMetaTagEnabled = process.env.ENABLE_XUA_COMPATIBLE_META_TAG;
    var xuaCompatValue = process.env.XUA_COMPATIBLE_VALUE;
    var isViewportMetaTagEnabled = process.env.ENABLE_VIEWPORT_META_TAG;
    var viewportValue = process.env.VIEWPORT_META_VALUE;
    var isFormatDetectionEnabled = process.env.ENABLE_FORMAT_DETECTION_META_TAG;
    var formatDetectionValue = process.env.FORMAT_DETECTION_META_VALUE;
    var isYahooShortcutsCSSEnabled = process.env.ENABLE_YAHOO_SHORTCUTS_CSS;
    var isMsoDpiScalingFixEnabled = process.env.ENABLE_MSO_DPI_SCALING_FIX;
    var isWebkitMediaQueryEnabled = process.env.ENABLE_WEBKIT_DETECTION_QUERY;
    var isGeckoMediaQueryEnabled = process.env.ENABLE_GECKO_MEDIA_QUERY;
    var isIEMediaQueryEnabled = process.env.ENABLE_IE_10_11_MEDIA_QUERY;
    var isGmailAndroidFixEnabled = process.env.ENABLE_GMAIL_ANDROID_RESIZE_FIX;
    var isGmailiOSFontFixEnabled = process.env.ENABLE_GMAIL_IOS_FONT_FIX;
    var isTableContainerFixedWidth = process.env.TABLE_CONTAINER_FIXED_WIDTH;

    // Small function to re-use for warnings, because lazy
    function configWarn(configMessage) {
        var label = 'CONFIGURATION:'
        console.warn(label, configMessage)
    }

    // While they act as booleans, they aren't actually REAL booleans (quacks like a duck etc..)
    if((isMsoNamespacesEnabled === 'false') && (isMsoDpiScalingFixEnabled === 'true')) {
        configWarn('ENABLE_VML_NAMESPACES should be set to true when ENABLE_MSO_DPI_SCALING_FIX is set to true');
    }

    if(isXuaCompatMetaTagEnabled === 'false') {
        configWarn('You should strongly consider setting ENABLE_XUA_COMPATIBLE_META_TAG to true for better Windows Phone/IE compatibility');
    }

    if((isXuaCompatMetaTagEnabled === 'true') && (!/edge/i.test(xuaCompatValue))) {
        configWarn('Using any other value than IE=edge for XAU_COMPATIBLE_VALUE is not recommended');
    }

    if(isViewportMetaTagEnabled === 'false') {
        configWarn('You should strongly consider setting ENABLE_VIEWPORT_META_TAG to true for correct scaling on mobile devices');
    }

    if((isViewportMetaTagEnabled === 'true') && (/user-scalable=no/i.test(viewportValue))) {
        configWarn('You should not disable pinch zooming/resizing gestures, it is bad UX');
    }

    if((isFormatDetectionEnabled === 'true') && (/no/i.test(formatDetectionValue))) {
        configWarn('Disabling auto formatting data also disables any user interaction events i.e. tap to call');
    }

    if(isYahooShortcutsCSSEnabled === 'true') {
        configWarn('Yahoo Shortcuts is no longer a feature of the Yahoo! Mail webmail client and does not need additional CSS to override');
    }

    if(isWebkitMediaQueryEnabled === 'true') {
        configWarn('The Webkit vendor prefixes return true on some non-webkit clients, do not rely on it for accurate detection');
    }

    if(isGeckoMediaQueryEnabled === 'true') {
        configWarn('The Gecko/Mozilla media query is often mangled in Yahoo! Mail and can cause unexpected CSS issues');
    }

    if(isIEMediaQueryEnabled === 'true') {
        configWarn('The IE 10/11 media query will also return true on IEMobile (Windows Phone 8.0/8.1)');
    }

    if((isGmailAndroidFixEnabled === 'true') && (isTableContainerFixedWidth === 'false')) {
        configWarn('You should set ENABLE_GMAIL_ANDROID_RESIZE_FIX to false when not using a fixed width container table');
    }

    if((isGmailiOSFontFixEnabled === 'true') && (isTableContainerFixedWidth === 'false')) {
        configWarn('You should set ENABLE_GMAIL_IOS_FONT_FIX to false when not using a fixed width container table');
    }

    if((isGmailiOSFontFixEnabled === 'true') || (isGmailAndroidFixEnabled === 'true')) {
        configWarn('Gmail app font/image spacer hacks for iOS and Android should be deprecated due to CSS3 support, see USAGE.md for more info');
    }

    // Check for older .env file
    var fs = require('fs');
    fs.stat('.env', function(err, stat) {
        if(err == null) {
            configWarn('.env is deprecated, please rename your custom settings file to .boilerplate.custom');
        }
    });

});

// The order is important! The CSS pre-process stuff must happen first otherwise minify will break everything!
gulp.task('default',
    [
        'clean',
        'preprocess-css',
        'minify-css',
        'build-html-samples',
        'preprocess-boilerplate',
        'inline-css',
        'check-config'
    ]
);
<commit_msg>Add warning for disabled MSO DPI fix<commit_after>// Dependencies that makes this all work! Yay packages!
var gulp = require('gulp'),
    cleanCSS = require('gulp-clean-css'),
    del = require('del'),
    inlineCSS = require('gulp-inline-css'),
    preprocess = require('gulp-preprocess'),
    rename = require('gulp-rename'),
    strip = require('gulp-strip-comments');

// Load in environment variables from .boilerplate.defaults first and then .boilerplate.custom if it exists
var boilerplateConfig = require('dotenv-extended').load(
    {
        encoding: 'utf8',
        silent: true,
        path: '.boilerplate.custom',
        defaults: '.boilerplate.defaults',
        errorOnMissing: false,
        errorOnExtra: false,
        assignToProcessEnv: true,
        overrideProcessEnv: true
    }
);

// We need to check if DOCTYPE_VERSION is using an XHTML value for inline-css purposes later
var currentSetDoctype = process.env.DOCTYPE_VERSION;
if (/XHTML/i.test(currentSetDoctype)) {
    xmlMode = true;
} else {
    xmlMode = false;
}

// Clean out the build directories
gulp.task('clean', function () {
    return del(
        [
            'tmp/css/**',
            'tmp/html-samples/**',
            'dist/boilerplate/**'
        ]
    ).then(paths => {
        console.log('Cleaning directories :\n', paths.join('\n'));
    });
});

// Pre-process CSS files with config values
gulp.task('preprocess-css', function() {
    var stream = gulp.src('./app/css/*.css')
    .pipe(preprocess())
    .pipe(gulp.dest('./tmp/css/'))
    return stream;
});

// Minify processed CSS for inclusion in the email boilerplate
gulp.task('minify-css', ['preprocess-css'], function() {
    var stream = gulp.src('./tmp/css/*.css')
    .pipe(cleanCSS(
        {
            // We don't want to go too crazy when minifying, this is email after all!
            advanced: false,
            aggressiveMerging: false,
            debug: true, // Needed for the size output on before and after min files
            keepBreaks: true,
            keepSpecialComments: 0,
            shorthandCompacting: false,
            // Defensive for less friendly email clients
            compatibility: '*,' +
            '-properties.colors,' +
            '-properties.zeroUnits'
        },
        // Log original size and minified size in gulp output
        function(details) {
            console.log(details.name + ': ' + details.stats.originalSize);
            console.log(details.name + '.min' + ': ' + details.stats.minifiedSize);
        })
    )
    .pipe(rename({ extname: '.css.min' }))
    .pipe(gulp.dest('./tmp/css/'));
    return stream;
});

// Build all HTML samples, inline CSS and strip comments to be included in boilerplate
gulp.task('build-html-samples', function() {
    gulp.src('./app/html-samples/*.html')
    .pipe(preprocess())
    .pipe(inlineCSS(
        {
            applyStyleTags: true,
            removeStyleTags: true,
            removeHtmlSelectors: true,
            applyTableAttributes: true,
            xmlMode: xmlMode
        }
    ))
    .pipe(strip(
        {
            ignore: /<!--\[if(?!\s*(?:\[[^\]]+]|<!|>))(?:(?!-->).)*-->/g,
            safe: true,
            trim: true
        }
    ))
    .pipe(gulp.dest('./tmp/html-samples/'));
});

// Build email boilerplate HTML and put everything together!
gulp.task('preprocess-boilerplate', ['minify-css'], function() {
    // We only want to target this file for HTML processing, the guidelines version is static and legacy
    var stream = gulp.src('app/email-boilerplate.html.preprocess')
    .pipe(preprocess({ extension: 'html' }))
    .pipe(rename('email-boilerplate.html'))
    .pipe(gulp.dest('./dist/boilerplate/'))
    return stream;
});

// Inline CSS to main layout elements after boilerplate is generated and update it
gulp.task('inline-css', ['preprocess-boilerplate'], function() {

    var stream = gulp.src('./dist/boilerplate/email-boilerplate.html')
    .pipe(inlineCSS(
        {
            applyTableAttributes: true,
            applyWidthAttributes: true,
            applyStyleTags: false,
            applyLinkTags: true,
            removeStyleTags: false,
            removeLinkTags: true,
            xmlMode: xmlMode
        }
    ))
    .pipe(gulp.dest('./dist/boilerplate/'));
    return stream;
});

// Detect certain configurations and warn when they are less optimal
gulp.task('check-config', ['inline-css'], function() {

    var isMsoNamespacesEnabled = process.env.ENABLE_VML_NAMESPACES;
    var isXuaCompatMetaTagEnabled = process.env.ENABLE_XUA_COMPATIBLE_META_TAG;
    var xuaCompatValue = process.env.XUA_COMPATIBLE_VALUE;
    var isViewportMetaTagEnabled = process.env.ENABLE_VIEWPORT_META_TAG;
    var viewportValue = process.env.VIEWPORT_META_VALUE;
    var isFormatDetectionEnabled = process.env.ENABLE_FORMAT_DETECTION_META_TAG;
    var formatDetectionValue = process.env.FORMAT_DETECTION_META_VALUE;
    var isYahooShortcutsCSSEnabled = process.env.ENABLE_YAHOO_SHORTCUTS_CSS;
    var isMsoDpiScalingFixEnabled = process.env.ENABLE_MSO_DPI_SCALING_FIX;
    var isWebkitMediaQueryEnabled = process.env.ENABLE_WEBKIT_DETECTION_QUERY;
    var isGeckoMediaQueryEnabled = process.env.ENABLE_GECKO_MEDIA_QUERY;
    var isIEMediaQueryEnabled = process.env.ENABLE_IE_10_11_MEDIA_QUERY;
    var isGmailAndroidFixEnabled = process.env.ENABLE_GMAIL_ANDROID_RESIZE_FIX;
    var isGmailiOSFontFixEnabled = process.env.ENABLE_GMAIL_IOS_FONT_FIX;
    var isTableContainerFixedWidth = process.env.TABLE_CONTAINER_FIXED_WIDTH;

    // Small function to re-use for warnings, because lazy
    function configWarn(configMessage) {
        var label = 'CONFIGURATION:'
        console.warn(label, configMessage)
    }

    // While they act as booleans, they aren't actually REAL booleans (quacks like a duck etc..)
    if((isMsoNamespacesEnabled === 'false') && (isMsoDpiScalingFixEnabled === 'true')) {
        configWarn('ENABLE_VML_NAMESPACES should be set to true when ENABLE_MSO_DPI_SCALING_FIX is set to true');
    }

    if(isXuaCompatMetaTagEnabled === 'false') {
        configWarn('You should strongly consider setting ENABLE_XUA_COMPATIBLE_META_TAG to true for better Windows Phone/IE compatibility');
    }

    if((isXuaCompatMetaTagEnabled === 'true') && (!/edge/i.test(xuaCompatValue))) {
        configWarn('Using any other value than IE=edge for XAU_COMPATIBLE_VALUE is not recommended');
    }

    if(isViewportMetaTagEnabled === 'false') {
        configWarn('You should strongly consider setting ENABLE_VIEWPORT_META_TAG to true for correct scaling on mobile devices');
    }

    if((isViewportMetaTagEnabled === 'true') && (/user-scalable=no/i.test(viewportValue))) {
        configWarn('You should not disable pinch zooming/resizing gestures, it is bad UX');
    }

    if((isFormatDetectionEnabled === 'true') && (/no/i.test(formatDetectionValue))) {
        configWarn('Disabling auto formatting data also disables any user interaction events i.e. tap to call');
    }

    if(isMsoDpiScalingFixEnabled === 'false') {
        configWarn('You should strongly consider setting ENABLE_MSO_DPI_SCALING_FIX to true for fixing image scaling with DPI values higher than 96');
    }

    if(isYahooShortcutsCSSEnabled === 'true') {
        configWarn('Yahoo Shortcuts is no longer a feature of the Yahoo! Mail webmail client and does not need additional CSS to override');
    }

    if(isWebkitMediaQueryEnabled === 'true') {
        configWarn('The Webkit vendor prefixes return true on some non-webkit clients, do not rely on it for accurate detection');
    }

    if(isGeckoMediaQueryEnabled === 'true') {
        configWarn('The Gecko/Mozilla media query is often mangled in Yahoo! Mail and can cause unexpected CSS issues');
    }

    if(isIEMediaQueryEnabled === 'true') {
        configWarn('The IE 10/11 media query will also return true on IEMobile (Windows Phone 8.0/8.1)');
    }

    if((isGmailAndroidFixEnabled === 'true') && (isTableContainerFixedWidth === 'false')) {
        configWarn('You should set ENABLE_GMAIL_ANDROID_RESIZE_FIX to false when not using a fixed width container table');
    }

    if((isGmailiOSFontFixEnabled === 'true') && (isTableContainerFixedWidth === 'false')) {
        configWarn('You should set ENABLE_GMAIL_IOS_FONT_FIX to false when not using a fixed width container table');
    }

    if((isGmailiOSFontFixEnabled === 'true') || (isGmailAndroidFixEnabled === 'true')) {
        configWarn('Gmail app font/image spacer hacks for iOS and Android should be deprecated due to CSS3 support, see USAGE.md for more info');
    }

    // Check for older .env file
    var fs = require('fs');
    fs.stat('.env', function(err, stat) {
        if(err == null) {
            configWarn('.env is deprecated, please rename your custom settings file to .boilerplate.custom');
        }
    });

});

// The order is important! The CSS pre-process stuff must happen first otherwise minify will break everything!
gulp.task('default',
    [
        'clean',
        'preprocess-css',
        'minify-css',
        'build-html-samples',
        'preprocess-boilerplate',
        'inline-css',
        'check-config'
    ]
);
<|endoftext|>


<commit_before>var _ = require('underscore');
var Backbone = require('backbone');
var CartoError = require('./error-handling/carto-error');
var Engine = require('../../engine');
var Events = require('./events');
var LayerBase = require('./layer/base');
var Layers = require('./layers');
var LeafletLayer = require('./leaflet-layer');
var VERSION = require('../../../package.json').version;
var CartoValidationError = require('error-handling/carto-validation-error');

function getValidationError (code) {
  return new CartoValidationError('client', code);
}

/**
 * This is the entry point for a Carto.js application.
 *
 * A CARTO client allows managing layers and dataviews. It also takes care
 * of the communication between a Carto.js application and the services in CARTO.
 * To create a new client you need a CARTO account, where you will be able to get
 * your API key and username.
 *
 * @param {object} settings
 * @param {string} settings.apiKey - API key used to authenticate against CARTO
 * @param {string} settings.username - Name of the user
 * @param {string} [settings.serverUrl] - URL of the windshaft server
 *
 * @example
 * var client = new carto.Client({
 *   apiKey: 'YOUR_API_KEY_HERE',
 *   username: 'YOUR_USERNAME_HERE'
 * });
 *
 * @constructor
 * @memberof carto
 * @api
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 */
function Client (settings) {
  _checkSettings(settings);
  this._layers = new Layers();
  this._dataviews = [];
  this._engine = new Engine({
    apiKey: settings.apiKey,
    username: settings.username,
    serverUrl: settings.serverUrl || 'https://{user}.carto.com'.replace(/{user}/, settings.username),
    statTag: 'carto.js-v' + VERSION
  });
  this._bindEngine(this._engine);
}

_.extend(Client.prototype, Backbone.Events);

/**
 * Add a layer to the client.
 *
 * @param {carto.layer.Base} - The layer to be added
 * @param {object} opts
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} - A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.addLayer = function (layer, opts) {
  return this.addLayers([layer], opts);
};

/**
 * Add multiple layers to the client.
 *
 * @param {carto.layer.Base[]} - An array with the layers to be added
 * @param {object} opts
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.addLayers = function (layers, opts) {
  opts = opts || {};
  layers.forEach(this._addLayer, this);
  if (opts.reload === false) {
    return Promise.resolve();
  }
  return this._reload();
};

/**
 * Remove a layer from the client
 *
 * @param {carto.layer.Base} - The layer to be removed
 * @param {object} opts
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.removeLayer = function (layer, opts) {
  return this.removeLayers([layer], opts);
};

/**
 * Remove multiple layer from the client
 *
 * @param {carto.layer.Base[]} - An array with the layers to be removed
 * @param {object} opts
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.removeLayers = function (layers, opts) {
  opts = opts || {};
  layers.forEach(this._removeLayer, this);
  if (opts.reload === false) {
    return Promise.resolve();
  }
  return this._reload();
};

/**
 * Get all the layers from the client
 *
 * @returns {carto.layer.Base[]} An array with all the Layers from the client
 * @api
 */
Client.prototype.getLayers = function () {
  return this._layers.toArray();
};

/**
 * Add a dataview to the client.
 *
 * @param {carto.dataview.Base} - The dataview to be added
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} - A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.addDataview = function (dataview, opts) {
  return this.addDataviews([dataview], opts);
};

/**
 * Add multipe dataviews to the client.
 *
 * @param {carto.dataview.Base[]} - An array with the dataviews to be added
 * @param {object} opts
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.addDataviews = function (dataviews, opts) {
  opts = opts || {};
  dataviews.forEach(this._addDataview, this);
  if (opts.reload === false) {
    return Promise.resolve();
  }
  return this._reload();
};

/**
 * Remove a dataview from the client.
 *
 * @param {carto.dataview.Base} - The dataview array to be removed
 * @param {object} opts
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.removeDataview = function (dataview, opts) {
  opts = opts || {};
  this._dataviews.splice(this._dataviews.indexOf(dataview));
  this._engine.removeDataview(dataview.$getInternalModel());
  if (opts.reload === false) {
    return Promise.resolve();
  }
  return this._reload();
};

/**
 * Get all the dataviews from the client
 *
 * @returns {carto.dataview.Base[]} An array with all the dataviews from the client
 * @api
 */
Client.prototype.getDataviews = function () {
  return this._dataviews;
};

/**
 * Return a Leaflet layer that groups all the layers that have been
 * added to this client.
 *
 * @returns {L.TileLayer} A Leaflet layer that groups all the layers:
 * {@link http://leafletjs.com/reference-1.2.0.html#tilelayer|L.TileLayer}
 * @api
 */
Client.prototype.getLeafletLayer = function () {
  this._leafletLayer = this._leafletLayer || new LeafletLayer(this._layers, this._engine);
  return this._leafletLayer;
};

/**
 * Call engine.reload wrapping the native cartojs errors
 * into public CartoErrors.
 */
Client.prototype._reload = function () {
  return this._engine.reload()
    .then(function () {
      return Promise.resolve();
    })
    .catch(function (error) {
      return Promise.reject(new CartoError(error));
    });
};

/**
 * Helper used to link a layer and an engine.
 * @private
 */
Client.prototype._addLayer = function (layer, engine) {
  _checkLayer(layer);
  this._layers.add(layer);
  layer.$setEngine(this._engine);
  this._engine.addLayer(layer.$getInternalModel());
};

/**
 * Helper used to remove a layer from the client.
 */
Client.prototype._removeLayer = function (layer) {
  _checkLayer(layer);
  this._layers.remove(layer);
  this._engine.removeLayer(layer.$getInternalModel());
};

/**
 * Helper used to link a dataview and an engine
 * @private
 */
Client.prototype._addDataview = function (dataview, engine) {
  this._dataviews.push(dataview);
  dataview.$setEngine(this._engine);
  this._engine.addDataview(dataview.$getInternalModel());
};

/**
 * Client exposes Event.SUCCESS and RELOAD_ERROR to the api users,
 * those events are wrappers using _engine internaly.
 */
Client.prototype._bindEngine = function (engine) {
  engine.on(Engine.Events.RELOAD_SUCCESS, function () {
    this.trigger(Events.SUCCESS);
  }.bind(this));

  engine.on(Engine.Events.RELOAD_ERROR, function (err) {
    this.trigger(Events.ERROR, new CartoError(err, { layers: this._layers }));
  }.bind(this));

  engine.on('error', function (err) {
    this.trigger(Events.ERROR, new CartoError(err));
  }.bind(this));
};

/**
 * Utility function to reduce duplicated code.
 * Check if an object inherits from LayerBase.
 */
function _checkLayer (object) {
  if (!(object instanceof LayerBase)) {
    throw getValidationError('badLayerType');
  }
}

function _checkSettings (settings) {
  _checkApiKey(settings.apiKey);
  _checkUsername(settings.username);
  if (settings.serverUrl) {
    _checkServerUrl(settings.serverUrl, settings.username);
  }
}

function _checkApiKey (apiKey) {
  if (!apiKey) {
    throw getValidationError('apiKeyRequired');
  }
  if (!_.isString(apiKey)) {
    throw getValidationError('apiKeyString');
  }
}

function _checkUsername (username) {
  if (!username) {
    throw getValidationError('usernameRequired');
  }
  if (!_.isString(username)) {
    throw getValidationError('usernameString');
  }
}

function _checkServerUrl (serverUrl, username) {
  var urlregex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/;
  if (!serverUrl.match(urlregex)) {
    throw getValidationError('nonValidServerURL');
  }
  if (serverUrl.indexOf(username) < 0) {
    throw getValidationError('serverURLDoesntMatchUsername');
  }
}

module.exports = Client;
<commit_msg>Fix bundling error<commit_after>var _ = require('underscore');
var Backbone = require('backbone');
var CartoError = require('./error-handling/carto-error');
var Engine = require('../../engine');
var Events = require('./events');
var LayerBase = require('./layer/base');
var Layers = require('./layers');
var LeafletLayer = require('./leaflet-layer');
var VERSION = require('../../../package.json').version;
var CartoValidationError = require('./error-handling/carto-validation-error');

function getValidationError (code) {
  return new CartoValidationError('client', code);
}

/**
 * This is the entry point for a Carto.js application.
 *
 * A CARTO client allows managing layers and dataviews. It also takes care
 * of the communication between a Carto.js application and the services in CARTO.
 * To create a new client you need a CARTO account, where you will be able to get
 * your API key and username.
 *
 * @param {object} settings
 * @param {string} settings.apiKey - API key used to authenticate against CARTO
 * @param {string} settings.username - Name of the user
 * @param {string} [settings.serverUrl] - URL of the windshaft server
 *
 * @example
 * var client = new carto.Client({
 *   apiKey: 'YOUR_API_KEY_HERE',
 *   username: 'YOUR_USERNAME_HERE'
 * });
 *
 * @constructor
 * @memberof carto
 * @api
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 */
function Client (settings) {
  _checkSettings(settings);
  this._layers = new Layers();
  this._dataviews = [];
  this._engine = new Engine({
    apiKey: settings.apiKey,
    username: settings.username,
    serverUrl: settings.serverUrl || 'https://{user}.carto.com'.replace(/{user}/, settings.username),
    statTag: 'carto.js-v' + VERSION
  });
  this._bindEngine(this._engine);
}

_.extend(Client.prototype, Backbone.Events);

/**
 * Add a layer to the client.
 *
 * @param {carto.layer.Base} - The layer to be added
 * @param {object} opts
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} - A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.addLayer = function (layer, opts) {
  return this.addLayers([layer], opts);
};

/**
 * Add multiple layers to the client.
 *
 * @param {carto.layer.Base[]} - An array with the layers to be added
 * @param {object} opts
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.addLayers = function (layers, opts) {
  opts = opts || {};
  layers.forEach(this._addLayer, this);
  if (opts.reload === false) {
    return Promise.resolve();
  }
  return this._reload();
};

/**
 * Remove a layer from the client
 *
 * @param {carto.layer.Base} - The layer to be removed
 * @param {object} opts
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.removeLayer = function (layer, opts) {
  return this.removeLayers([layer], opts);
};

/**
 * Remove multiple layer from the client
 *
 * @param {carto.layer.Base[]} - An array with the layers to be removed
 * @param {object} opts
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.removeLayers = function (layers, opts) {
  opts = opts || {};
  layers.forEach(this._removeLayer, this);
  if (opts.reload === false) {
    return Promise.resolve();
  }
  return this._reload();
};

/**
 * Get all the layers from the client
 *
 * @returns {carto.layer.Base[]} An array with all the Layers from the client
 * @api
 */
Client.prototype.getLayers = function () {
  return this._layers.toArray();
};

/**
 * Add a dataview to the client.
 *
 * @param {carto.dataview.Base} - The dataview to be added
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} - A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.addDataview = function (dataview, opts) {
  return this.addDataviews([dataview], opts);
};

/**
 * Add multipe dataviews to the client.
 *
 * @param {carto.dataview.Base[]} - An array with the dataviews to be added
 * @param {object} opts
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.addDataviews = function (dataviews, opts) {
  opts = opts || {};
  dataviews.forEach(this._addDataview, this);
  if (opts.reload === false) {
    return Promise.resolve();
  }
  return this._reload();
};

/**
 * Remove a dataview from the client.
 *
 * @param {carto.dataview.Base} - The dataview array to be removed
 * @param {object} opts
 * @param {boolean} opts.reload - Default: true. A boolean flag controlling if the client should be reloaded
 *
 * @fires CartoError
 * @fires carto.events.SUCCESS
 *
 * @returns {Promise} A promise that will be fulfilled when the reload cycle is completed
 * @api
 */
Client.prototype.removeDataview = function (dataview, opts) {
  opts = opts || {};
  this._dataviews.splice(this._dataviews.indexOf(dataview));
  this._engine.removeDataview(dataview.$getInternalModel());
  if (opts.reload === false) {
    return Promise.resolve();
  }
  return this._reload();
};

/**
 * Get all the dataviews from the client
 *
 * @returns {carto.dataview.Base[]} An array with all the dataviews from the client
 * @api
 */
Client.prototype.getDataviews = function () {
  return this._dataviews;
};

/**
 * Return a Leaflet layer that groups all the layers that have been
 * added to this client.
 *
 * @returns {L.TileLayer} A Leaflet layer that groups all the layers:
 * {@link http://leafletjs.com/reference-1.2.0.html#tilelayer|L.TileLayer}
 * @api
 */
Client.prototype.getLeafletLayer = function () {
  this._leafletLayer = this._leafletLayer || new LeafletLayer(this._layers, this._engine);
  return this._leafletLayer;
};

/**
 * Call engine.reload wrapping the native cartojs errors
 * into public CartoErrors.
 */
Client.prototype._reload = function () {
  return this._engine.reload()
    .then(function () {
      return Promise.resolve();
    })
    .catch(function (error) {
      return Promise.reject(new CartoError(error));
    });
};

/**
 * Helper used to link a layer and an engine.
 * @private
 */
Client.prototype._addLayer = function (layer, engine) {
  _checkLayer(layer);
  this._layers.add(layer);
  layer.$setEngine(this._engine);
  this._engine.addLayer(layer.$getInternalModel());
};

/**
 * Helper used to remove a layer from the client.
 */
Client.prototype._removeLayer = function (layer) {
  _checkLayer(layer);
  this._layers.remove(layer);
  this._engine.removeLayer(layer.$getInternalModel());
};

/**
 * Helper used to link a dataview and an engine
 * @private
 */
Client.prototype._addDataview = function (dataview, engine) {
  this._dataviews.push(dataview);
  dataview.$setEngine(this._engine);
  this._engine.addDataview(dataview.$getInternalModel());
};

/**
 * Client exposes Event.SUCCESS and RELOAD_ERROR to the api users,
 * those events are wrappers using _engine internaly.
 */
Client.prototype._bindEngine = function (engine) {
  engine.on(Engine.Events.RELOAD_SUCCESS, function () {
    this.trigger(Events.SUCCESS);
  }.bind(this));

  engine.on(Engine.Events.RELOAD_ERROR, function (err) {
    this.trigger(Events.ERROR, new CartoError(err, { layers: this._layers }));
  }.bind(this));

  engine.on('error', function (err) {
    this.trigger(Events.ERROR, new CartoError(err));
  }.bind(this));
};

/**
 * Utility function to reduce duplicated code.
 * Check if an object inherits from LayerBase.
 */
function _checkLayer (object) {
  if (!(object instanceof LayerBase)) {
    throw getValidationError('badLayerType');
  }
}

function _checkSettings (settings) {
  _checkApiKey(settings.apiKey);
  _checkUsername(settings.username);
  if (settings.serverUrl) {
    _checkServerUrl(settings.serverUrl, settings.username);
  }
}

function _checkApiKey (apiKey) {
  if (!apiKey) {
    throw getValidationError('apiKeyRequired');
  }
  if (!_.isString(apiKey)) {
    throw getValidationError('apiKeyString');
  }
}

function _checkUsername (username) {
  if (!username) {
    throw getValidationError('usernameRequired');
  }
  if (!_.isString(username)) {
    throw getValidationError('usernameString');
  }
}

function _checkServerUrl (serverUrl, username) {
  var urlregex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/;
  if (!serverUrl.match(urlregex)) {
    throw getValidationError('nonValidServerURL');
  }
  if (serverUrl.indexOf(username) < 0) {
    throw getValidationError('serverURLDoesntMatchUsername');
  }
}

module.exports = Client;
<|endoftext|>