<commit_before>define(
  'tinymce.themes.mobile.ios.core.IosSetup',

  [
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Throttler',
    'ephox.sugar.api.dom.Focus',
    'ephox.sugar.api.events.DomEvent',
    'ephox.sugar.api.node.Body',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.properties.Css',
    'global!clearInterval',
    'global!clearTimeout',
    'global!console',
    'global!Math',
    'global!parseInt',
    'global!setInterval',
    'global!setTimeout',
    'tinymce.themes.mobile.ios.focus.FakeSelection',
    'tinymce.themes.mobile.ios.scroll.IosScrolling',
    'tinymce.themes.mobile.ios.smooth.BackgroundActivity',
    'tinymce.themes.mobile.ios.view.Greenzone',
    'tinymce.themes.mobile.ios.view.IosUpdates',
    'tinymce.themes.mobile.ios.view.IosViewport',
    'tinymce.themes.mobile.touch.view.Orientation',
    'tinymce.themes.mobile.util.CaptureBin',
    'tinymce.themes.mobile.util.Rectangles'
  ],

  function (
    Fun, Option, Throttler, Focus, DomEvent, Body, Element, Css, clearInterval, clearTimeout, console, Math, parseInt, setInterval, setTimeout, FakeSelection,
    IosScrolling, BackgroundActivity, Greenzone, IosUpdates, IosViewport, Orientation, CaptureBin, Rectangles
  ) {
    var VIEW_MARGIN = 5;

    var register = function (toolstrip, socket, container, outerWindow, structure, cWin) {
      var scroller = BackgroundActivity(function (y) {
        return IosScrolling.moveWindowScroll(toolstrip, socket, y);
      });

      // NOTE: This is a WebView specific way of scrolling when out of bounds. When we need to make
      // the webapp work again, we'll have to adjust this function. Essentially, it just jumps the scroll
      // back to show the current selection rectangle.
      var scrollBounds = function () {
        var rects = Rectangles.getRectangles(cWin);
        return Option.from(rects[0]).bind(function (rect) {
          var viewTop = rect.top() - socket.dom().scrollTop;
          var outside = viewTop > outerWindow.innerHeight + VIEW_MARGIN || viewTop < -VIEW_MARGIN;
          return outside ? Option.some({
            top: Fun.constant(viewTop),
            bottom: Fun.constant(viewTop + rect.height())
          }) : Option.none();
        });
      };

      var scrollThrottle = Throttler.last(function () {
        /*
         * As soon as the window is back to 0 (idle), scroll the toolbar and socket back into place on scroll.
         */
        scroller.idle(function () {
          console.log('** idle **');
          IosUpdates.updatePositions(container, outerWindow.pageYOffset).get(function (/* _ */) {
            var extraScroll = scrollBounds();
            extraScroll.each(function (extra) {
              // TODO: Smoothly animate this in a way that doesn't conflict with anything else.
              socket.dom().scrollTop = socket.dom().scrollTop + extra.top();
            });
            scroller.start(0);
            structure.refresh();
          });
        });
      }, 1000);

      var onScroll = DomEvent.bind(Element.fromDom(outerWindow), 'scroll', function () {
        if (outerWindow.pageYOffset < 0) {
          return;
        }

        /*
        We've experimented with trying to set the socket scroll (hidden vs. scroll) based on whether the outer body
        has scrolled. When the window starts scrolling, we would lock the socket scroll, and we would
        unlock it when the window stopped scrolling. This would give a nice rubber-band effect at the end
        of the content, but would break the code that tried to position the text in the viewable area
        (more details below). Also, as soon as you flicked to outer scroll, if you started scrolling up again,
        you would drag the whole window down, because you would still be in outerscroll mode. That's hardly
        much of a problem, but it is a minor issue. It also didn't play nicely with keeping the toolbar on the screen.

        The big problem was that this was incompatible with the toolbar and scrolling code. We need a padding inside
        the socket so that the bottom of the content can be scrolled into the viewable greenzone. If it doesn't
        have padding, then unless we move the socket top to some negative value as well, then we can't get
        a scrollTop high enough to get the selection into the viewable greenzone. This is the purpose of the
        padding at the bottom of the iframe. Without it, the scroll consistently jumps back to its
        max scroll value, and you can't keep the last line on screen when the keyboard is up.

        However, if the padding is too large, then the content can be 'effectively' scrolled off the screen
        (the iframe anyway), and the user can get lost about where they are. Our short-term fix is just to
        make the padding at the end the height - the greenzone height so that content should always be
        visible on the screen, even if they've scrolled to the end.
        */

        scrollThrottle.throttle();
      });

      IosUpdates.updatePositions(container, outerWindow.pageYOffset).get(Fun.identity);

      return {
        unbind: onScroll.unbind
      };
    };

    var setup = function (bag) {
      var cWin = bag.cWin();
      var ceBody = bag.ceBody();
      var socket = bag.socket();
      var toolstrip = bag.toolstrip();
      var toolbar = bag.toolbar();
      var contentElement = bag.contentElement();
      var keyboardType = bag.keyboardType();
      var outerWindow = bag.outerWindow();

      var structure = IosViewport.takeover(socket, ceBody, toolstrip);
      var keyboardModel = keyboardType(bag.outerBody(), cWin, Body.body(), contentElement, toolstrip, toolbar);

      var toEditing = function () {
        // Consider inlining, though it will make it harder to follow the API
        keyboardModel.toEditing();
        clearSelection();
      };

      var toReading = function () {
        keyboardModel.toReading();
      };

      var onToolbarTouch = function (event) {
        keyboardModel.onToolbarTouch(event);
      };

      var onOrientation = Orientation.onChange(outerWindow, {
        onChange: Fun.noop,
        onReady: structure.refresh
      });

      // NOTE: When the window is resizing (probably due to meta tags and viewport definitions), we are not receiving a window resize event.
      // However, it happens shortly after we start Ios mode, so here we just wait for the first window size event that we get. This code
      // is also the same code that is used for the Orientation ready event.
      onOrientation.onAdjustment(function () {
        structure.refresh();
      });

      var onResize = DomEvent.bind(Element.fromDom(outerWindow), 'resize', function () {
        if (structure.isExpanding()) {
          structure.refresh();
        }
      });

      var onScroll = register(toolstrip, socket, bag.outerBody(), outerWindow, structure, cWin);

      var unfocusedSelection = FakeSelection(cWin, contentElement);

      var refreshSelection = function () {
        if (unfocusedSelection.isActive()) {
          unfocusedSelection.update();
        }
      };

      var highlightSelection = function () {
        unfocusedSelection.update();
      };

      var clearSelection = function () {
        unfocusedSelection.clear();
      };

      var scrollIntoView = function (top, bottom) {
        Greenzone.scrollIntoView(cWin, socket, top, bottom);
      };

      var syncHeight = function () {
        Css.set(contentElement, 'height', contentElement.dom().contentWindow.document.body.scrollHeight + 'px');
      };

      var setViewportOffset = function (newYOffset) {
        structure.setViewportOffset(newYOffset);
        IosScrolling.moveOnlyTop(socket, newYOffset).get(Fun.identity);
      };

      var destroy = function () {
        structure.restore();
        onOrientation.destroy();
        onScroll.unbind();
        onResize.unbind();
        keyboardModel.destroy();

        unfocusedSelection.destroy();

        // Try and dismiss the keyboard on close, as they have no input focus.
        CaptureBin.input(Body.body(), Focus.blur);
      };

      return {
        toEditing: toEditing,
        toReading: toReading,
        onToolbarTouch: onToolbarTouch,
        refreshSelection: refreshSelection,
        clearSelection: clearSelection,
        highlightSelection: highlightSelection,
        scrollIntoView: scrollIntoView,
        updateToolbarPadding: Fun.noop,
        setViewportOffset: setViewportOffset,
        syncHeight: syncHeight,
        destroy: destroy
      };
    };

    return {
      setup: setup
    };
  }
);<commit_msg>TM-44: removing debugging stuff<commit_after>define(
  'tinymce.themes.mobile.ios.core.IosSetup',

  [
    'ephox.katamari.api.Fun',
    'ephox.katamari.api.Option',
    'ephox.katamari.api.Throttler',
    'ephox.sugar.api.dom.Focus',
    'ephox.sugar.api.events.DomEvent',
    'ephox.sugar.api.node.Body',
    'ephox.sugar.api.node.Element',
    'ephox.sugar.api.properties.Css',
    'global!clearInterval',
    'global!clearTimeout',
    'global!console',
    'global!Math',
    'global!parseInt',
    'global!setInterval',
    'global!setTimeout',
    'tinymce.themes.mobile.ios.focus.FakeSelection',
    'tinymce.themes.mobile.ios.scroll.IosScrolling',
    'tinymce.themes.mobile.ios.smooth.BackgroundActivity',
    'tinymce.themes.mobile.ios.view.Greenzone',
    'tinymce.themes.mobile.ios.view.IosUpdates',
    'tinymce.themes.mobile.ios.view.IosViewport',
    'tinymce.themes.mobile.touch.view.Orientation',
    'tinymce.themes.mobile.util.CaptureBin',
    'tinymce.themes.mobile.util.Rectangles'
  ],

  function (
    Fun, Option, Throttler, Focus, DomEvent, Body, Element, Css, clearInterval, clearTimeout, console, Math, parseInt, setInterval, setTimeout, FakeSelection,
    IosScrolling, BackgroundActivity, Greenzone, IosUpdates, IosViewport, Orientation, CaptureBin, Rectangles
  ) {
    var VIEW_MARGIN = 5;

    var register = function (toolstrip, socket, container, outerWindow, structure, cWin) {
      var scroller = BackgroundActivity(function (y) {
        return IosScrolling.moveWindowScroll(toolstrip, socket, y);
      });

      // NOTE: This is a WebView specific way of scrolling when out of bounds. When we need to make
      // the webapp work again, we'll have to adjust this function. Essentially, it just jumps the scroll
      // back to show the current selection rectangle.
      var scrollBounds = function () {
        var rects = Rectangles.getRectangles(cWin);
        return Option.from(rects[0]).bind(function (rect) {
          var viewTop = rect.top() - socket.dom().scrollTop;
          var outside = viewTop > outerWindow.innerHeight + VIEW_MARGIN || viewTop < -VIEW_MARGIN;
          return outside ? Option.some({
            top: Fun.constant(viewTop),
            bottom: Fun.constant(viewTop + rect.height())
          }) : Option.none();
        });
      };

      var scrollThrottle = Throttler.last(function () {
        /*
         * As soon as the window is back to 0 (idle), scroll the toolbar and socket back into place on scroll.
         */
        scroller.idle(function () {
          IosUpdates.updatePositions(container, outerWindow.pageYOffset).get(function (/* _ */) {
            var extraScroll = scrollBounds();
            extraScroll.each(function (extra) {
              // TODO: Smoothly animate this in a way that doesn't conflict with anything else.
              socket.dom().scrollTop = socket.dom().scrollTop + extra.top();
            });
            scroller.start(0);
            structure.refresh();
          });
        });
      }, 1000);

      var onScroll = DomEvent.bind(Element.fromDom(outerWindow), 'scroll', function () {
        if (outerWindow.pageYOffset < 0) {
          return;
        }

        /*
        We've experimented with trying to set the socket scroll (hidden vs. scroll) based on whether the outer body
        has scrolled. When the window starts scrolling, we would lock the socket scroll, and we would
        unlock it when the window stopped scrolling. This would give a nice rubber-band effect at the end
        of the content, but would break the code that tried to position the text in the viewable area
        (more details below). Also, as soon as you flicked to outer scroll, if you started scrolling up again,
        you would drag the whole window down, because you would still be in outerscroll mode. That's hardly
        much of a problem, but it is a minor issue. It also didn't play nicely with keeping the toolbar on the screen.

        The big problem was that this was incompatible with the toolbar and scrolling code. We need a padding inside
        the socket so that the bottom of the content can be scrolled into the viewable greenzone. If it doesn't
        have padding, then unless we move the socket top to some negative value as well, then we can't get
        a scrollTop high enough to get the selection into the viewable greenzone. This is the purpose of the
        padding at the bottom of the iframe. Without it, the scroll consistently jumps back to its
        max scroll value, and you can't keep the last line on screen when the keyboard is up.

        However, if the padding is too large, then the content can be 'effectively' scrolled off the screen
        (the iframe anyway), and the user can get lost about where they are. Our short-term fix is just to
        make the padding at the end the height - the greenzone height so that content should always be
        visible on the screen, even if they've scrolled to the end.
        */

        scrollThrottle.throttle();
      });

      IosUpdates.updatePositions(container, outerWindow.pageYOffset).get(Fun.identity);

      return {
        unbind: onScroll.unbind
      };
    };

    var setup = function (bag) {
      var cWin = bag.cWin();
      var ceBody = bag.ceBody();
      var socket = bag.socket();
      var toolstrip = bag.toolstrip();
      var toolbar = bag.toolbar();
      var contentElement = bag.contentElement();
      var keyboardType = bag.keyboardType();
      var outerWindow = bag.outerWindow();

      var structure = IosViewport.takeover(socket, ceBody, toolstrip);
      var keyboardModel = keyboardType(bag.outerBody(), cWin, Body.body(), contentElement, toolstrip, toolbar);

      var toEditing = function () {
        // Consider inlining, though it will make it harder to follow the API
        keyboardModel.toEditing();
        clearSelection();
      };

      var toReading = function () {
        keyboardModel.toReading();
      };

      var onToolbarTouch = function (event) {
        keyboardModel.onToolbarTouch(event);
      };

      var onOrientation = Orientation.onChange(outerWindow, {
        onChange: Fun.noop,
        onReady: structure.refresh
      });

      // NOTE: When the window is resizing (probably due to meta tags and viewport definitions), we are not receiving a window resize event.
      // However, it happens shortly after we start Ios mode, so here we just wait for the first window size event that we get. This code
      // is also the same code that is used for the Orientation ready event.
      onOrientation.onAdjustment(function () {
        structure.refresh();
      });

      var onResize = DomEvent.bind(Element.fromDom(outerWindow), 'resize', function () {
        if (structure.isExpanding()) {
          structure.refresh();
        }
      });

      var onScroll = register(toolstrip, socket, bag.outerBody(), outerWindow, structure, cWin);

      var unfocusedSelection = FakeSelection(cWin, contentElement);

      var refreshSelection = function () {
        if (unfocusedSelection.isActive()) {
          unfocusedSelection.update();
        }
      };

      var highlightSelection = function () {
        unfocusedSelection.update();
      };

      var clearSelection = function () {
        unfocusedSelection.clear();
      };

      var scrollIntoView = function (top, bottom) {
        Greenzone.scrollIntoView(cWin, socket, top, bottom);
      };

      var syncHeight = function () {
        Css.set(contentElement, 'height', contentElement.dom().contentWindow.document.body.scrollHeight + 'px');
      };

      var setViewportOffset = function (newYOffset) {
        structure.setViewportOffset(newYOffset);
        IosScrolling.moveOnlyTop(socket, newYOffset).get(Fun.identity);
      };

      var destroy = function () {
        structure.restore();
        onOrientation.destroy();
        onScroll.unbind();
        onResize.unbind();
        keyboardModel.destroy();

        unfocusedSelection.destroy();

        // Try and dismiss the keyboard on close, as they have no input focus.
        CaptureBin.input(Body.body(), Focus.blur);
      };

      return {
        toEditing: toEditing,
        toReading: toReading,
        onToolbarTouch: onToolbarTouch,
        refreshSelection: refreshSelection,
        clearSelection: clearSelection,
        highlightSelection: highlightSelection,
        scrollIntoView: scrollIntoView,
        updateToolbarPadding: Fun.noop,
        setViewportOffset: setViewportOffset,
        syncHeight: syncHeight,
        destroy: destroy
      };
    };

    return {
      setup: setup
    };
  }
);<|endoftext|>


<commit_before>/*

EXAMPLE USAGE:
const config = {
  hubHost: {
    production: 'http://hub.svc.prod',
    staging: 'http://hub.svc.staging',
    test: 'http://hub.svc.dev',
    development: 'http://hub.svc.dev',
  },
  appHost: {
    production: 'http://wma-email-sender.prod.flightstats.io:3000',
    staging: 'http://wma-email-sender.staging.flightstats.io:3000',
    test: 'http://localhost:3001',
    development: 'http://localhost:3000',
  },
  hubParallelCalls: 2,
};

const watcher = new HubWatcher(expressApp, config);
watcher.watchChannel('wma_email_outbox', sendEmail);

 */

import Datahub from './datahub';
import os from 'os';

let localIPAddress = null;
const SUCCESS_STATUS_CODE = 200;
const FAILURE_STATUS_CODE = 422;
const RESPONSE_HEADERS = {'Content-Type': 'text/json'};

export default class HubWatcher {
  constructor(expressApp, config) {
    if (!expressApp) {
      throw new Error(`HubWatcher: Missing Express app`);
    }

    if (!config) {
      throw new Error('HubWatcher: Missing config');
    }

    if (!config.hubHost || !config.appHost[env()]) {
      throw new Error(`HubWatcher config: Missing "hubHost.${env()}"`);
    }

    if (!config.appHost || !config.appHost[env()]) {
      throw new Error(`HubWatcher config: Missing "appHost.${env()}"`);
    }

    if (typeof(expressApp.post) !== 'function') {
      throw new Error('HubWatcher: Express app must implement .post()');
    }

    this.expressApp = expressApp;
    this.config = config;
    this.watchedChannels = [];
  }

  watchChannel(channelName, fnHandler) {
    this.expressApp.post(buildCallbackRoute(channelName), this.postHandler(channelName, fnHandler));

    if (this.watchedChannels.indexOf(channelName) === -1) {
      return this.initWebhook(channelName)
      .then(() => {
        this.watchedChannels.push(channelName);
      });
    }
    else {
      console.log('[node-datahub HubWatcher] webhook already initialized for', channelName);
    }

    return Promise.resolve();
  }

  postHandler(channelName, fnHandler) {
    return (req, res) => {
      if (typeof(fnHandler) !== 'function') {
        throw new Error(`Callback handler for ${channelName} is not a function. It's a ${typeof(fnHandler)}: ${fnHandler}`)
      }

      let responseStatusCode = FAILURE_STATUS_CODE;
      let requestBodyData = null;

      try {
        if (typeof(req.body) === 'string') {
          requestBodyData = JSON.parse(req.body);
        }
        else {
          requestBodyData = req.body;
        }

        const datahub = new Datahub({
          url: this.config.hubHost[env()],
          requestPromiseOptions: {
            resolveWithFullResponse: true
          },
        });

        return datahub.getGroupCallbackContent(requestBodyData)
        .then((hubDataItemResponse) => {
          if (requestBodyData.uris && requestBodyData.uris.length > 1) {
            throw new Error(`HubWatcher: Expected hub callback "uris" attribute to be length 1 but was ${JSON.stringify(requestBodyData.uris)}`);
          }

          const hubDataItemURI = (requestBodyData.uris ? requestBodyData.uris[0] : null);

          return fnHandler(hubDataItemResponse.body, hubDataItemURI)
          .then((result) => {
            responseStatusCode = SUCCESS_STATUS_CODE;
          })
          .catch((err) => {
            throw new Error(`Error running ${channelName} callback handler: ${err}`);
          })
        })
        .catch((err) => {
          console.error('[node-datahub HubWatcher] Error getting', channelName, 'callback content:', err.message);
        })
        .then(() => {
          res.status(responseStatusCode).end();
        });
      }
      catch(err) {
        console.error('[node-datahub HubWatcher] Caught error getting', channelName, 'callback content:', err);
        res.status(responseStatusCode).end();
      }
    }
  }

  initWebhook(channelName) {
    const callbackConfig = {
      name: buildCallbackName(channelName),
      channelName: channelName,
      callbackUrl: buildCallbackUrl(channelName, this.config.appHost[env()]),
      parallelCalls: this.config.hubParallelCalls,
    };

    const hubHost = this.config.hubHost[env()];

    const datahub = new Datahub({
      url: hubHost,
      requestPromiseOptions: { resolveWithFullResponse: true },
    });

    const callbackName = buildCallbackName(channelName);

    return datahub.getGroupCallback(callbackName)
    .then((result) => {
      // if dev env, and if host is different, recreate group for current host
      const localCallbackUrl = callbackConfig.callbackUrl;

      if (result && result.body && result.body.callbackUrl !== localCallbackUrl) {
        console.log('[node-datahub HubWatcher] Updating group callback URL from', result.body.callbackUrl, 'to', callbackConfig.callbackUrl);

        return datahub.deleteGroupCallback(callbackName)
        .then((result) => {
          console.log('[node-datahub HubWatcher] Deleted hub callback:', callbackName);
          return createHubCallback(datahub, callbackConfig);
        })
        .catch((error) => {
          console.error('[node-datahub HubWatcher] Error deleting hub callback:', error.stack);
          done(error);
        }
        );
      }
      else {
        // Existing callback configured properly
        return null;
      }
    })
    .catch((error) => {
      if (error.statusCode == 404) {
        console.log('[node-datahub HubWatcher] Creating nonexistent callback', callbackConfig);
        return createHubCallback(datahub, callbackConfig);
      }

      console.error('[node-datahub HubWatcher] Error retrieving group callback:', error);
      return null;
    });
  }

} // end of class

function createHubCallback(datahub, callbackConfig) {
  return datahub.createGroupCallback(callbackConfig)
  .then((result) => {
    console.log('[node-datahub HubWatcher] Created hub callback for', callbackConfig.name);
  })
  .catch((error) => {
    console.error('[node-datahub HubWatcher] Failed to create callback:', error);
  });
}

function getLocalIPAddress() {
  if (localIPAddress) {
    return localIPAddress;
  }

  if (process.env.IP) {
    localIPAddress = process.env.IP;
    console.log('[node-datahub HubWatcher] using IP environment variable for hub webhook:', localIPAddress);
  }
  else {
    const ifaces = os.networkInterfaces();
    let firstIPAddress = null;

    for (let ifname in ifaces) {
      let ifaceAddresses = ifaces[ifname];

      for (let j in ifaceAddresses) {
        const iface = ifaceAddresses[j];

        if (iface.family === 'IPv4' && !iface.internal) {
          // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses

          localIPAddress = iface.address;

          if (env() === 'development') {
            if (iface.address.search(/10\.*/) === 0) {
              localIPAddress = iface.address;
            }
          }
          else {
            // Use the first IP
            localIPAddress = localIPAddress || iface.address;
          }
        }
      }
    }

    console.log('[node-datahub HubWatcher] detected IP:', localIPAddress);
  }

  if (!localIPAddress) {
    throw new Error('Unable to get local IP address. Set the IP environment variable to your 10.x.x.x address.');
  }

  return localIPAddress;
}

function buildCallbackName(channelName) {
  return [channelName, env()].join('_');
}

function buildCallbackRoute(channelName) {
  return `/hub-callbacks/${channelName}`;
}

function buildCallbackUrl(channelName, appHost) {
  const callbackUrl = appHost + buildCallbackRoute(channelName);
  return callbackUrl.replace(/localhost/, getLocalIPAddress());
}

function env() {
  return process.env.NODE_ENV ? process.env.NODE_ENV : 'development';
}
<commit_msg>Hub callback name now includes username along with env in development and test environments.<commit_after>/*

EXAMPLE USAGE:
const config = {
  hubHost: {
    production: 'http://hub.svc.prod',
    staging: 'http://hub.svc.staging',
    test: 'http://hub.svc.dev',
    development: 'http://hub.svc.dev',
  },
  appHost: {
    production: 'http://wma-email-sender.prod.flightstats.io:3000',
    staging: 'http://wma-email-sender.staging.flightstats.io:3000',
    test: 'http://localhost:3001',
    development: 'http://localhost:3000',
  },
  hubParallelCalls: 2,
};

const watcher = new HubWatcher(expressApp, config);
watcher.watchChannel('wma_email_outbox', sendEmail);

 */

import Datahub from './datahub';
import os from 'os';

let localIPAddress = null;
const SUCCESS_STATUS_CODE = 200;
const FAILURE_STATUS_CODE = 422;
const RESPONSE_HEADERS = {'Content-Type': 'text/json'};

export default class HubWatcher {
  constructor(expressApp, config) {
    if (!expressApp) {
      throw new Error(`HubWatcher: Missing Express app`);
    }

    if (!config) {
      throw new Error('HubWatcher: Missing config');
    }

    if (!config.hubHost || !config.appHost[env()]) {
      throw new Error(`HubWatcher config: Missing "hubHost.${env()}"`);
    }

    if (!config.appHost || !config.appHost[env()]) {
      throw new Error(`HubWatcher config: Missing "appHost.${env()}"`);
    }

    if (typeof(expressApp.post) !== 'function') {
      throw new Error('HubWatcher: Express app must implement .post()');
    }

    this.expressApp = expressApp;
    this.config = config;
    this.watchedChannels = [];
  }

  watchChannel(channelName, fnHandler) {
    this.expressApp.post(buildCallbackRoute(channelName), this.postHandler(channelName, fnHandler));

    if (this.watchedChannels.indexOf(channelName) === -1) {
      return this.initWebhook(channelName)
      .then(() => {
        this.watchedChannels.push(channelName);
      });
    }
    else {
      console.log('[node-datahub HubWatcher] webhook already initialized for', channelName);
    }

    return Promise.resolve();
  }

  postHandler(channelName, fnHandler) {
    return (req, res) => {
      if (typeof(fnHandler) !== 'function') {
        throw new Error(`Callback handler for ${channelName} is not a function. It's a ${typeof(fnHandler)}: ${fnHandler}`)
      }

      let responseStatusCode = FAILURE_STATUS_CODE;
      let requestBodyData = null;

      try {
        if (typeof(req.body) === 'string') {
          requestBodyData = JSON.parse(req.body);
        }
        else {
          requestBodyData = req.body;
        }

        const datahub = new Datahub({
          url: this.config.hubHost[env()],
          requestPromiseOptions: {
            resolveWithFullResponse: true
          },
        });

        return datahub.getGroupCallbackContent(requestBodyData)
        .then((hubDataItemResponse) => {
          if (requestBodyData.uris && requestBodyData.uris.length > 1) {
            throw new Error(`HubWatcher: Expected hub callback "uris" attribute to be length 1 but was ${JSON.stringify(requestBodyData.uris)}`);
          }

          const hubDataItemURI = (requestBodyData.uris ? requestBodyData.uris[0] : null);

          return fnHandler(hubDataItemResponse.body, hubDataItemURI)
          .then((result) => {
            responseStatusCode = SUCCESS_STATUS_CODE;
          })
          .catch((err) => {
            throw new Error(`Error running ${channelName} callback handler: ${err}`);
          })
        })
        .catch((err) => {
          console.error('[node-datahub HubWatcher] Error getting', channelName, 'callback content:', err.message);
        })
        .then(() => {
          res.status(responseStatusCode).end();
        });
      }
      catch(err) {
        console.error('[node-datahub HubWatcher] Caught error getting', channelName, 'callback content:', err);
        res.status(responseStatusCode).end();
      }
    }
  }

  initWebhook(channelName) {
    const callbackName = buildCallbackName(channelName);

    const callbackConfig = {
      name: callbackName,
      channelName: channelName,
      callbackUrl: buildCallbackUrl(channelName, this.config.appHost[env()]),
      parallelCalls: this.config.hubParallelCalls,
    };

    const hubHost = this.config.hubHost[env()];

    const datahub = new Datahub({
      url: hubHost,
      requestPromiseOptions: { resolveWithFullResponse: true },
    });

    return datahub.getGroupCallback(callbackName)
    .then((result) => {
      // if dev env, and if host is different, recreate group for current host
      const localCallbackUrl = callbackConfig.callbackUrl;

      if (result && result.body && result.body.callbackUrl !== localCallbackUrl) {
        console.log('[node-datahub HubWatcher] Updating group callback URL from', result.body.callbackUrl, 'to', callbackConfig.callbackUrl);

        return datahub.deleteGroupCallback(callbackName)
        .then((result) => {
          console.log('[node-datahub HubWatcher] Deleted hub callback:', callbackName);
          return createHubCallback(datahub, callbackConfig);
        })
        .catch((error) => {
          console.error('[node-datahub HubWatcher] Error deleting hub callback:', error.stack);
          done(error);
        }
        );
      }
      else {
        // Existing callback configured properly
        return null;
      }
    })
    .catch((error) => {
      if (error.statusCode == 404) {
        console.log('[node-datahub HubWatcher] Creating nonexistent callback', callbackConfig);
        return createHubCallback(datahub, callbackConfig);
      }

      console.error('[node-datahub HubWatcher] Error retrieving group callback:', error);
      return null;
    });
  }

} // end of class

function createHubCallback(datahub, callbackConfig) {
  return datahub.createGroupCallback(callbackConfig)
  .then((result) => {
    console.log('[node-datahub HubWatcher] Created hub callback for', callbackConfig.name);
  })
  .catch((error) => {
    console.error('[node-datahub HubWatcher] Failed to create callback:', error);
  });
}

function getLocalIPAddress() {
  if (localIPAddress) {
    return localIPAddress;
  }

  if (process.env.IP) {
    localIPAddress = process.env.IP;
    console.log('[node-datahub HubWatcher] using IP environment variable for hub webhook:', localIPAddress);
  }
  else {
    const ifaces = os.networkInterfaces();
    let firstIPAddress = null;

    for (let ifname in ifaces) {
      let ifaceAddresses = ifaces[ifname];

      for (let j in ifaceAddresses) {
        const iface = ifaceAddresses[j];

        if (iface.family === 'IPv4' && !iface.internal) {
          // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses

          localIPAddress = iface.address;

          if (env() === 'development') {
            if (iface.address.search(/10\.*/) === 0) {
              localIPAddress = iface.address;
            }
          }
          else {
            // Use the first IP
            localIPAddress = localIPAddress || iface.address;
          }
        }
      }
    }

    console.log('[node-datahub HubWatcher] detected IP:', localIPAddress);
  }

  if (!localIPAddress) {
    throw new Error('Unable to get local IP address. Set the IP environment variable to your 10.x.x.x address.');
  }

  return localIPAddress;
}

function buildCallbackName(channelName) {
  let suffix = env();

  if (['development', 'test'].indexOf(env()) !== -1) {
    suffix = `${process.env.USER || getLocalIPAddress()}_${suffix}`;
  }

  return [channelName, suffix].join('_');
}

function buildCallbackRoute(channelName) {
  return `/hub-callbacks/${channelName}`;
}

function buildCallbackUrl(channelName, appHost) {
  const callbackUrl = appHost + buildCallbackRoute(channelName);
  return callbackUrl.replace(/localhost/, getLocalIPAddress());
}

function env() {
  return process.env.NODE_ENV ? process.env.NODE_ENV : 'development';
}
<|endoftext|>


<commit_before>var _ = require('./utils.js');

// Breadth-first walk the tree in reverse order
var inverseBreadth = function(node, cb) {
  if(!node.children) return;
  for(var i = node.children.length - 1; i >= 0; i--) {
    cb(node.children[i]);
  };
  _.each(node.children, function(child) {
    inverseBreadth(child, cb);
  });
};

// Clean tree from alternatives
var cleanTree = function(node) {
  if(!node.children) return;
  for(var i = node.children.length - 1; i >= 0; i--) {
    var child = node.children[i];
    if(child.type == 'Alternative') {
      node.children.splice(i, 1);
    } else {
      cleanTree(child);
    }
  };
  return node;
};

var Parser = {
  indentSpaces: 2,

  // Parse the given template source and spit out a traversable tree
  parseTemplate: function(tmpl) {
    var self = this;
    var lines = tmpl.split('\n');
    var lastIndent = 0;
    var lineNum = 0;
    var top = {
      type: 'TOP',
      children: []
    };
    var lastNode = top;
    var stack = [top];
    var slurpyMode = false;
    _.each(lines, function(line) {
      lineNum++;
      var indent = Math.max(0, line.search(/\S/)) / self.indentSpaces;
      // Regard anything below a slurpy tag as text
      if(slurpyMode && (indent > lastIndent || !line.replace(/\s/g, '').length)) {
        var parent = lastNode;
        parent.children.push({
          type: 'Text',
          content: line.slice(lastIndent * self.indentSpaces + self.indentSpaces) + '\n'
        });
        return;
      }
      slurpyMode = false;
      // Remove indentation
      line = line.slice(indent * self.indentSpaces);
      // Ignore comments
      var ci = line.indexOf('//');
      if(ci == 0) return;
      // Ignore empty lines
      if(!line.replace(/\s/g, '').length) return;
      // Push and pop the current parent from the stack
      // as dictated by indentation
      if(indent > lastIndent) {
        if(indent - lastIndent > 1) {
          throw({message: 'Too much indentation', lineNum: lineNum});
        }
        if(lastNode.content) throw({message: 'Elements cannot have both text and children elements', lineNum: lineNum});
        stack.push(lastNode);
      } else if(indent < lastIndent) {
        _.times(lastIndent - indent, function() {
          stack.pop();
        });
      }
      var node = self.parseLine(line);
      // Add the generated node to its parent
      var parent = _.last(stack);
      parent.children.push(node);
      // If we have a multiTags hierarchy -> use rightmost child as lastNode
      var child = node;
      while(child.children && child.children[0]) {
        child = child.children[0];
      };
      lastIndent = indent;
      lastNode = child;
      if(node.slurpy) {
        slurpyMode = true;
      }
    });
    // Squash alternatives into their respective statements
    var previous;
    var alternativeStack = [];
    inverseBreadth(top, function(node) {
      // Collect alternatives from bottom to top
      if(node.type == 'Alternative') {
        alternativeStack.push(node);
      // Attach to next statement we encounter
      } else {
        if(alternativeStack.length) {
          node.alternatives = _.map(alternativeStack.reverse(), function(alt) {
            return alt.children;
          });
          alternativeStack = [];
        }
      }
      previous = node;
    });
    // Remove alternatives
    return cleanTree(top);
  },

  // Convert line into tree node
  parseLine: function(line) {
    // Statement
    if(line.indexOf('{{') == 0) {
      return this.parseStatement(line);
    // Text
    } else if(line.indexOf('|') == 0) {
      return {
        type: 'Text',
        content: line.slice(2) + '\n'
      };
    // Tag definition
    } else {
      return this.parseTag(line);
    }
  },

  // Parse all major components from a tag, including inline content
  parseTag: function(line) {
    var self = this;
    var m = line.match(/(([\w-#\.]+>)*)([\w-]+)?(#([\w-]+))?((\.[\w-]+)*)(\((.*)\))?(\.)?( (.*))?/);
    var multiTags = m[1];
    var tag = m[3] || 'div';
    var id = m[5];
    var classes = m[6] ? m[6].slice(1).split('.') : [];
    var inParens = m[9];
    var dot = m[10];
    var content = m[12];
    var attributes = {};
    var statements = [];
    // Parse attributes
    if(inParens) {
      var attrDefinitions;
      var i = inParens.indexOf('{{');
      if(i > -1) {
        attrDefinitions = inParens.slice(0, i);
        var statementDefinitions = inParens.slice(i, inParens.length);
        statements = self.parseMicroStatements(statementDefinitions);
      } else {
        attrDefinitions = inParens;
      }
      _.each(_.scan(attrDefinitions, /([\w-]+?)=(['"])(.+?)\2\s?/g), function(m) { // '
        var key = m[1];
        var value = m[3];
        attributes[key] = value;
      });
      // _.each(_.scan(statementDefinitions, /{{(\w+)\s+(\w+)\s+(\w+)}}/g), function(m) {
      //   statements[m[2]] = m[3];
      // });
    }
    // Make AST node
    var tag = {
      type: 'HTMLTag',
      tag: tag,
      id: id,
      classes: classes,
      attributes: attributes,
      content: content,
      statements: statements,
      slurpy: !!dot,
      children: []
    };
    // Build a hierarchy from multi tags
    if(multiTags) {
      multiTags = multiTags.slice(0, -1);
      var tags = _.map(multiTags.split('>'), function(t) {
        return self.parseTag(t);
      });
      var top = tags.shift();
      var prev = top;
      _.each(tags, function(t) {
        prev.children.push(t);
        prev = t;
      });
      prev.children.push(tag);
      return top;
    } else {
      return tag;
    }
  },

  parseMicroStatements: function(string) {
    var statements = [];
    _.each(_.scan(string, /{{(\w+)\s+(.*)}}/g), function(m) {
      var statement = m[1];
      var rest = m[2];
      if(statement == 'as') {
        statements.push({statement: 'as', varName: rest});
      } else if(statement == 'action') {
        var m = rest.match(/(\w+)\s+(\w+)/);
        statements.push({statement: 'action', event: m[1], method: m[2]});
      } else {
        throw('Unknown statement: ' + statement);
      }
    });
    return statements;
  },

  // Takes an statement line and creates the approriate node
  parseStatement: function(line) {
    var m;
    var out;
    // if-greater
    if(m = line.match(/{{if\s+(.+)\s+>\s+(.+)\s*}}/)) {
      out = {
        type: 'Statement',
        keyword: 'if-greater',
        path1: m[1],
        path2: m[2]
      };
    // if-equal
    } else if(m = line.match(/{{if\s+(.+)\s+==\s+(.+)\s*}}/)) {
      out = {
        type: 'Statement',
        keyword: 'if-equal',
        path1: m[1],
        path2: m[2]
      };
    // if-not-equal
    } else if(m = line.match(/{{if\s+(.+)\s+!=\s+(.+)\s*}}/)) {
      out = {
        type: 'Statement',
        keyword: 'if-not-equal',
        path1: m[1],
        path2: m[2]
      };
    // if
    } else if(m = line.match(/{{if\s+(.+)}}/)) {
      out = {
        type: 'Statement',
        keyword: 'if',
        path: m[1]
      };
    // for
    } else if(m = line.match(/{{for\s+((\w+)\s+in\s+)?(.+)}}/)) {
      out = {
        type: 'Statement',
        keyword: 'for',
        itemPath: m[2],
        itemsPath: m[3]
      };
    // view
    } else if(m = line.match(/{{view\s+(\w+)?(\(.*\))?}}/)) {
      var parens = m[2];
      var args = parens ? _.map(parens.slice(1, -1).split(','), function(argument) {
        return argument.replace(/\s/g, '');
      }) : [];
      out = {
        type: 'Statement',
        keyword: 'view',
        viewModel: m[1],
        arguments: args
      };
    // import
    } else if(m = line.match(/{{import\s+(\w+)(\(.*\))?}}/)) {
      var parens = m[2];
      var args = {};
      if(parens) {
        _.each(parens.slice(1, -1).split(','), function(argument) {
          var pair = argument.replace(/\s/g, '').split(':');
          var vari = pair[0];
          var expr = pair[1];
          args[vari] = expr;
        });
      }
      out = {
        type: 'Statement',
        keyword: 'import',
        templateName: m[1] + '.tmpl',
        arguments: args
      };
    // content
    } else if(m = line.match(/{{content}}/)) {
      out = {
        type: 'Statement',
        keyword: 'content'
      };
    // client
    } else if(m = line.match(/{{client}}/)) {
      out = {
        type: 'Statement',
        keyword: 'client'
      };
    // route
    } else if(m = line.match(/{{route (.*)}}/)) {
      out = {
        type: 'Statement',
        keyword: 'route',
        path: m[1]
      };
    // =>
    } else if(m = line.match(/{{=>}}/)) {
      out = {
        type: 'Alternative'
      };
    } else {
      throw('Unknown statement: ' + line);
    }
    out.children = [];
    return out;
  }
};

module.exports = Parser;
<commit_msg>Allow whitespace surrounding multi tag arrow<commit_after>var _ = require('./utils.js');

// Breadth-first walk the tree in reverse order
var inverseBreadth = function(node, cb) {
  if(!node.children) return;
  for(var i = node.children.length - 1; i >= 0; i--) {
    cb(node.children[i]);
  };
  _.each(node.children, function(child) {
    inverseBreadth(child, cb);
  });
};

// Clean tree from alternatives
var cleanTree = function(node) {
  if(!node.children) return;
  for(var i = node.children.length - 1; i >= 0; i--) {
    var child = node.children[i];
    if(child.type == 'Alternative') {
      node.children.splice(i, 1);
    } else {
      cleanTree(child);
    }
  };
  return node;
};

var Parser = {
  indentSpaces: 2,

  // Parse the given template source and spit out a traversable tree
  parseTemplate: function(tmpl) {
    var self = this;
    var lines = tmpl.split('\n');
    var lastIndent = 0;
    var lineNum = 0;
    var top = {
      type: 'TOP',
      children: []
    };
    var lastNode = top;
    var stack = [top];
    var slurpyMode = false;
    _.each(lines, function(line) {
      lineNum++;
      var indent = Math.max(0, line.search(/\S/)) / self.indentSpaces;
      // Regard anything below a slurpy tag as text
      if(slurpyMode && (indent > lastIndent || !line.replace(/\s/g, '').length)) {
        var parent = lastNode;
        parent.children.push({
          type: 'Text',
          content: line.slice(lastIndent * self.indentSpaces + self.indentSpaces) + '\n'
        });
        return;
      }
      slurpyMode = false;
      // Remove indentation
      line = line.slice(indent * self.indentSpaces);
      // Ignore comments
      var ci = line.indexOf('//');
      if(ci == 0) return;
      // Ignore empty lines
      if(!line.replace(/\s/g, '').length) return;
      // Push and pop the current parent from the stack
      // as dictated by indentation
      if(indent > lastIndent) {
        if(indent - lastIndent > 1) {
          throw({message: 'Too much indentation', lineNum: lineNum});
        }
        if(lastNode.content) throw({message: 'Elements cannot have both text and children elements', lineNum: lineNum});
        stack.push(lastNode);
      } else if(indent < lastIndent) {
        _.times(lastIndent - indent, function() {
          stack.pop();
        });
      }
      var node = self.parseLine(line);
      // Add the generated node to its parent
      var parent = _.last(stack);
      parent.children.push(node);
      // If we have a multiTags hierarchy -> use rightmost child as lastNode
      var child = node;
      while(child.children && child.children[0]) {
        child = child.children[0];
      };
      lastIndent = indent;
      lastNode = child;
      if(node.slurpy) {
        slurpyMode = true;
      }
    });
    // Squash alternatives into their respective statements
    var previous;
    var alternativeStack = [];
    inverseBreadth(top, function(node) {
      // Collect alternatives from bottom to top
      if(node.type == 'Alternative') {
        alternativeStack.push(node);
      // Attach to next statement we encounter
      } else {
        if(alternativeStack.length) {
          node.alternatives = _.map(alternativeStack.reverse(), function(alt) {
            return alt.children;
          });
          alternativeStack = [];
        }
      }
      previous = node;
    });
    // Remove alternatives
    return cleanTree(top);
  },

  // Convert line into tree node
  parseLine: function(line) {
    // Statement
    if(line.indexOf('{{') == 0) {
      return this.parseStatement(line);
    // Text
    } else if(line.indexOf('|') == 0) {
      return {
        type: 'Text',
        content: line.slice(2) + '\n'
      };
    // Tag definition
    } else {
      return this.parseTag(line);
    }
  },

  // Parse all major components from a tag, including inline content
  parseTag: function(line) {
    var self = this;
    var m = line.match(/(([\w-#\.]+\s*>\s*)*)([\w-]+)?(#([\w-]+))?((\.[\w-]+)*)(\((.*)\))?(\.)?( (.*))?/);
    var multiTags = m[1];
    var tag = m[3] || 'div';
    var id = m[5];
    var classes = m[6] ? m[6].slice(1).split('.') : [];
    var inParens = m[9];
    var dot = m[10];
    var content = m[12];
    var attributes = {};
    var statements = [];
    // Parse attributes
    if(inParens) {
      var attrDefinitions;
      var i = inParens.indexOf('{{');
      if(i > -1) {
        attrDefinitions = inParens.slice(0, i);
        var statementDefinitions = inParens.slice(i, inParens.length);
        statements = self.parseMicroStatements(statementDefinitions);
      } else {
        attrDefinitions = inParens;
      }
      _.each(_.scan(attrDefinitions, /([\w-]+?)=(['"])(.+?)\2\s?/g), function(m) { // '
        var key = m[1];
        var value = m[3];
        attributes[key] = value;
      });
      // _.each(_.scan(statementDefinitions, /{{(\w+)\s+(\w+)\s+(\w+)}}/g), function(m) {
      //   statements[m[2]] = m[3];
      // });
    }
    // Make AST node
    var tag = {
      type: 'HTMLTag',
      tag: tag,
      id: id,
      classes: classes,
      attributes: attributes,
      content: content,
      statements: statements,
      slurpy: !!dot,
      children: []
    };
    // Build a hierarchy from multi tags
    if(multiTags) {
      // Remove trailing '>' before splitting
      multiTags = multiTags.trim().slice(0, -1);
      var tags = _.map(multiTags.split('>'), function(t) {
        return self.parseTag(t.trim());
      });
      var top = tags.shift();
      var prev = top;
      _.each(tags, function(t) {
        prev.children.push(t);
        prev = t;
      });
      prev.children.push(tag);
      return top;
    } else {
      return tag;
    }
  },

  parseMicroStatements: function(string) {
    var statements = [];
    _.each(_.scan(string, /{{(\w+)\s+(.*)}}/g), function(m) {
      var statement = m[1];
      var rest = m[2];
      if(statement == 'as') {
        statements.push({statement: 'as', varName: rest});
      } else if(statement == 'action') {
        var m = rest.match(/(\w+)\s+(\w+)/);
        statements.push({statement: 'action', event: m[1], method: m[2]});
      } else {
        throw('Unknown statement: ' + statement);
      }
    });
    return statements;
  },

  // Takes an statement line and creates the approriate node
  parseStatement: function(line) {
    var m;
    var out;
    // if-greater
    if(m = line.match(/{{if\s+(.+)\s+>\s+(.+)\s*}}/)) {
      out = {
        type: 'Statement',
        keyword: 'if-greater',
        path1: m[1],
        path2: m[2]
      };
    // if-equal
    } else if(m = line.match(/{{if\s+(.+)\s+==\s+(.+)\s*}}/)) {
      out = {
        type: 'Statement',
        keyword: 'if-equal',
        path1: m[1],
        path2: m[2]
      };
    // if-not-equal
    } else if(m = line.match(/{{if\s+(.+)\s+!=\s+(.+)\s*}}/)) {
      out = {
        type: 'Statement',
        keyword: 'if-not-equal',
        path1: m[1],
        path2: m[2]
      };
    // if
    } else if(m = line.match(/{{if\s+(.+)}}/)) {
      out = {
        type: 'Statement',
        keyword: 'if',
        path: m[1]
      };
    // for
    } else if(m = line.match(/{{for\s+((\w+)\s+in\s+)?(.+)}}/)) {
      out = {
        type: 'Statement',
        keyword: 'for',
        itemPath: m[2],
        itemsPath: m[3]
      };
    // view
    } else if(m = line.match(/{{view\s+(\w+)?(\(.*\))?}}/)) {
      var parens = m[2];
      var args = parens ? _.map(parens.slice(1, -1).split(','), function(argument) {
        return argument.replace(/\s/g, '');
      }) : [];
      out = {
        type: 'Statement',
        keyword: 'view',
        viewModel: m[1],
        arguments: args
      };
    // import
    } else if(m = line.match(/{{import\s+(\w+)(\(.*\))?}}/)) {
      var parens = m[2];
      var args = {};
      if(parens) {
        _.each(parens.slice(1, -1).split(','), function(argument) {
          var pair = argument.replace(/\s/g, '').split(':');
          var vari = pair[0];
          var expr = pair[1];
          args[vari] = expr;
        });
      }
      out = {
        type: 'Statement',
        keyword: 'import',
        templateName: m[1] + '.tmpl',
        arguments: args
      };
    // content
    } else if(m = line.match(/{{content}}/)) {
      out = {
        type: 'Statement',
        keyword: 'content'
      };
    // client
    } else if(m = line.match(/{{client}}/)) {
      out = {
        type: 'Statement',
        keyword: 'client'
      };
    // route
    } else if(m = line.match(/{{route (.*)}}/)) {
      out = {
        type: 'Statement',
        keyword: 'route',
        path: m[1]
      };
    // =>
    } else if(m = line.match(/{{=>}}/)) {
      out = {
        type: 'Alternative'
      };
    } else {
      throw('Unknown statement: ' + line);
    }
    out.children = [];
    return out;
  }
};

module.exports = Parser;
<|endoftext|>


<commit_before>
/**
 * WYMeditor.DocumentStructureManager
 * ==================================
 *
 * This manager controls the rules for the subset of HTML that WYMeditor will
 * allow the user to create. For technical users, there are justifiable reasons
 * to use any in-spec HTML, but for users of WYMeditor (not implementors), the
 * goal is make it intuitive for them to create the structured markup that will
 * fit their needs.
 *
 * For example, while it's valid HTML to have a mix
 * of DIV and P tags at the top level of a document, in practice, this is
 * confusing for non-technical users. The DocumentStructureManager allows the
 * WYMeditor implementor to standardize on P tags in the root of the document,
 * which will automatically convert DIV tags in the root to P tags.
 *
 */
WYMeditor.DocumentStructureManager = function(wym, defaultRootContainer) {
    this._wym = wym;
    this.structureRules = WYMeditor.DocumentStructureManager.DEFAULTS;
    this.setDefaultRootContainer(defaultRootContainer);
};

jQuery.extend(WYMeditor.DocumentStructureManager, {
    // Only these containers are allowed as valid `defaultRootContainer`
    // options.
    VALID_DEFAULT_ROOT_CONTAINERS : [
        "p",
        "div"
    ],

    // Cooresponding titles for use in the containers panel for the valid
    // default root containers
    DEFAULT_ROOT_CONTAINER_TITLES : {
        p: "Paragraph",
        div: "Division"
    },

    // These containers prevent the user from using up/down/enter/backspace
    // to move above or below them, thus effectively blocking the creation
    // of new blocks. We must use temporary spacer elements to correct this
    // while the document is being edited.
    CONTAINERS_BLOCKING_NAVIGATION : ["table", "blockquote", "pre"],

    DEFAULTS : {
        // By default, this container will be used for all root contents. This
        // defines the container used when "enter" is pressed from the root and
        // also which container wraps or replaces containers found at the root
        // that aren't allowed. Only
        // `DocumentStructureManager.VALID_DEFAULT_ROOT_CONTAINERS` are allowed
        // here. Whichever you choose, the VALID_DEFAULT_ROOT_CONTAINERS will
        // be automatically converted when found at the top level of your
        // document.
        defaultRootContainer: 'p',

        // These containers cannot be used as root containers. This includes
        // any default root containers that are not the chosen default root
        // container. By default, this is set to the list of valid root
        // containers that are not the defaultRootContainer.
        notValidRootContainers: ['div'],

        // Only these containers are allowed as a direct child of the body tag.
        // All other containers located there will be wrapped in the
        // `defaultRootContainer`, unless they're one of the tags in
        // `convertIfRoot`.
        validRootContainers: [
            'p',
            'div',
            'h1',
            'h2',
            'h3',
            'h4',
            'h5',
            'h6',
            'pre',
            'blockquote',
            'table',
            'ol',
            'ul'
        ],

        // If these tags are found in the root, they'll be converted to the
        // `defaultRootContainer` container instead of the default of being
        // wrapped.
        convertIfRootContainers: [
            'div'
        ],

        // The elements that are allowed to be turned in to lists.
        validListConversionTargetContainers: [
            "p",
            "div",
            "h1",
            "h2",
            "h3",
            "h4",
            "h5",
            "h6",
            "pre",
            "blockquote",
            "td",
            "th"
        ],

        // For most block elements, the default behavior when a user attempts
        // to convert it to a list is to convert that block to a ol/ul and wrap
        // the contents in an li. For containers in `wrapContentsInList`,
        // instead of converting the container, we should just wrap the
        // contents of the container in a ul/ol + li.
        wrapContentsInList: [
            'td',
            'th'
        ]
    }
});

/**
 * Set the default container created/used in the root.
 *
 * @param defaultRootContainer String A string representation of the tag to
 * use.
 */
WYMeditor.DocumentStructureManager.prototype.setDefaultRootContainer = function(
    defaultRootContainer
) {
    var validContainers,
        index;

    if (this.structureRules.defaultRootContainer === defaultRootContainer) {
        // This is already our current configuration. No need to do the
        // work again.
        return;
    }

    // Make sure the new container is one of the valid options
    validContainers = WYMeditor.DocumentStructureManager.VALID_DEFAULT_ROOT_CONTAINERS;
    index = jQuery.inArray(defaultRootContainer, validContainers);
    if (index === -1) {
        throw new Error(
            "a defaultRootContainer of '" +
            defaultRootContainer +
            "' is not supported"
        );
    }

    this.structureRules.defaultRootContainer = defaultRootContainer;

    // No other possible option for default root containers is valid expect for
    // the one choosen default root container
    this.structureRules.notValidRootContainers =
        WYMeditor.DocumentStructureManager.VALID_DEFAULT_ROOT_CONTAINERS;
    this.structureRules.notValidRootContainers.splice(index, 1);

    this.adjustDefaultRootContainerUI();

    // TODO: Actually do all of the switching required to move from p to div or
    // from div to p for the topLevelContainer
};

/**
    adjustDefaultRootContainerUI
    ============================

    Adds a new link for the default root container to the containers panel in
    the editor if needed, and removes any other links for valid default root
    containers form the containers panel besides the link for the chosen
    default root container.
*/
WYMeditor.DocumentStructureManager.prototype.adjustDefaultRootContainerUI = function() {
    var wym = this._wym,
        defaultRootContainer = this.structureRules.defaultRootContainer,
        $containerItems,
        $containerLink,
        $newContainerItem,
        containerName,
        newContainerLinkNeeded,
        newContainerLinkHtml,
        i;

    $containerItems = jQuery(wym._box).find(wym._options.containersSelector)
                                      .find('li');
    newContainerLinkNeeded = true;

    // Remove container links for any other valid default root container from
    // the containers panel besides the link for the chosen default root
    // container
    for (i = 0; i < $containerItems.length; ++i) {
        $containerLink = $containerItems.eq(i).find('a');
        containerName = $containerLink.attr('name').toLowerCase();
        if (jQuery.inArray(containerName,
                           this.structureRules.notValidRootContainers) > -1) {
            $containerItems.eq(i).remove();
        }
        if (containerName === defaultRootContainer) {
            newContainerLinkNeeded = false;
        }
    }

    // Add new link for the default root container to the containers panel if
    // needed
    if (newContainerLinkNeeded) {
        newContainerLinkHtml = wym._options.containersItemHtml;
        newContainerLinkHtml = WYMeditor.Helper.replaceAll(
            newContainerLinkHtml,
            WYMeditor.CONTAINER_NAME,
            defaultRootContainer.toUpperCase()
        );
        newContainerLinkHtml = WYMeditor.Helper.replaceAll(
            newContainerLinkHtml,
            WYMeditor.CONTAINER_TITLE,
            WYMeditor.DocumentStructureManager.DEFAULT_ROOT_CONTAINER_TITLES[defaultRootContainer]
        );
        newContainerLinkHtml = WYMeditor.Helper.replaceAll(
            newContainerLinkHtml,
            WYMeditor.CONTAINER_CLASS,
            "wym_containers_" + defaultRootContainer
        );
        $newContainerItem = jQuery(newContainerLinkHtml);
        $containerItems = jQuery(wym._box).find(wym._options.containersSelector)
                                          .find('li');
        $containerItems.eq(0).before($newContainerItem);

        // Bind click event for the new link
        $newContainerItem.find('a').click(function () {
            wym.container(jQuery(this).attr(WYMeditor.NAME));
            return false;
        });
    }
}

<commit_msg>De-linted editor/document-structure-manager.js<commit_after>/*jslint maxlen: 90 */
/* global -$ */
"use strict";

/**
 * WYMeditor.DocumentStructureManager
 * ==================================
 *
 * This manager controls the rules for the subset of HTML that WYMeditor will
 * allow the user to create. For technical users, there are justifiable reasons
 * to use any in-spec HTML, but for users of WYMeditor (not implementors), the
 * goal is make it intuitive for them to create the structured markup that will
 * fit their needs.
 *
 * For example, while it's valid HTML to have a mix
 * of DIV and P tags at the top level of a document, in practice, this is
 * confusing for non-technical users. The DocumentStructureManager allows the
 * WYMeditor implementor to standardize on P tags in the root of the document,
 * which will automatically convert DIV tags in the root to P tags.
 *
 */
WYMeditor.DocumentStructureManager = function (wym, defaultRootContainer) {
    this._wym = wym;
    this.structureRules = WYMeditor.DocumentStructureManager.DEFAULTS;
    this.setDefaultRootContainer(defaultRootContainer);
};

jQuery.extend(WYMeditor.DocumentStructureManager, {
    // Only these containers are allowed as valid `defaultRootContainer`
    // options.
    VALID_DEFAULT_ROOT_CONTAINERS : [
        "p",
        "div"
    ],

    // Cooresponding titles for use in the containers panel for the valid
    // default root containers
    DEFAULT_ROOT_CONTAINER_TITLES : {
        p: "Paragraph",
        div: "Division"
    },

    // These containers prevent the user from using up/down/enter/backspace
    // to move above or below them, thus effectively blocking the creation
    // of new blocks. We must use temporary spacer elements to correct this
    // while the document is being edited.
    CONTAINERS_BLOCKING_NAVIGATION : ["table", "blockquote", "pre"],

    DEFAULTS : {
        // By default, this container will be used for all root contents. This
        // defines the container used when "enter" is pressed from the root and
        // also which container wraps or replaces containers found at the root
        // that aren't allowed. Only
        // `DocumentStructureManager.VALID_DEFAULT_ROOT_CONTAINERS` are allowed
        // here. Whichever you choose, the VALID_DEFAULT_ROOT_CONTAINERS will
        // be automatically converted when found at the top level of your
        // document.
        defaultRootContainer: 'p',

        // These containers cannot be used as root containers. This includes
        // any default root containers that are not the chosen default root
        // container. By default, this is set to the list of valid root
        // containers that are not the defaultRootContainer.
        notValidRootContainers: ['div'],

        // Only these containers are allowed as a direct child of the body tag.
        // All other containers located there will be wrapped in the
        // `defaultRootContainer`, unless they're one of the tags in
        // `convertIfRoot`.
        validRootContainers: [
            'p',
            'div',
            'h1',
            'h2',
            'h3',
            'h4',
            'h5',
            'h6',
            'pre',
            'blockquote',
            'table',
            'ol',
            'ul'
        ],

        // If these tags are found in the root, they'll be converted to the
        // `defaultRootContainer` container instead of the default of being
        // wrapped.
        convertIfRootContainers: [
            'div'
        ],

        // The elements that are allowed to be turned in to lists.
        validListConversionTargetContainers: [
            "p",
            "div",
            "h1",
            "h2",
            "h3",
            "h4",
            "h5",
            "h6",
            "pre",
            "blockquote",
            "td",
            "th"
        ],

        // For most block elements, the default behavior when a user attempts
        // to convert it to a list is to convert that block to a ol/ul and wrap
        // the contents in an li. For containers in `wrapContentsInList`,
        // instead of converting the container, we should just wrap the
        // contents of the container in a ul/ol + li.
        wrapContentsInList: [
            'td',
            'th'
        ]
    }
});

/**
 * Set the default container created/used in the root.
 *
 * @param defaultRootContainer String A string representation of the tag to
 * use.
 */
WYMeditor.DocumentStructureManager.prototype.setDefaultRootContainer = function
(
    defaultRootContainer
) {
    var validContainers,
        index,
        DSManager;

    if (this.structureRules.defaultRootContainer === defaultRootContainer) {
        // This is already our current configuration. No need to do the
        // work again.
        return;
    }

    // Make sure the new container is one of the valid options
    DSManager = WYMeditor.DocumentStructureManager;
    validContainers = DSManager.VALID_DEFAULT_ROOT_CONTAINERS;
    index = jQuery.inArray(defaultRootContainer, validContainers);
    if (index === -1) {
        throw new Error(
            "a defaultRootContainer of '" +
            defaultRootContainer +
            "' is not supported"
        );
    }

    this.structureRules.defaultRootContainer = defaultRootContainer;

    // No other possible option for default root containers is valid expect for
    // the one choosen default root container
    this.structureRules.notValidRootContainers =
        WYMeditor.DocumentStructureManager.VALID_DEFAULT_ROOT_CONTAINERS;
    this.structureRules.notValidRootContainers.splice(index, 1);

    this.adjustDefaultRootContainerUI();

    // TODO: Actually do all of the switching required to move from p to div or
    // from div to p for the topLevelContainer
};

/**
    adjustDefaultRootContainerUI
    ============================

    Adds a new link for the default root container to the containers panel in
    the editor if needed, and removes any other links for valid default root
    containers form the containers panel besides the link for the chosen
    default root container.
*/
WYMeditor.DocumentStructureManager.prototype.adjustDefaultRootContainerUI = function () {
    var wym = this._wym,
        defaultRootContainer = this.structureRules.defaultRootContainer,
        $containerItems,
        $containerLink,
        $newContainerItem,
        containerName,
        newContainerLinkNeeded,
        newContainerLinkHtml,
        i,
        DSManager;

    $containerItems = jQuery(wym._box).find(wym._options.containersSelector)
                                      .find('li');
    newContainerLinkNeeded = true;

    // Remove container links for any other valid default root container from
    // the containers panel besides the link for the chosen default root
    // container
    for (i = 0; i < $containerItems.length; ++i) {
        $containerLink = $containerItems.eq(i).find('a');
        containerName = $containerLink.attr('name').toLowerCase();
        if (jQuery.inArray(containerName,
                           this.structureRules.notValidRootContainers) > -1) {
            $containerItems.eq(i).remove();
        }
        if (containerName === defaultRootContainer) {
            newContainerLinkNeeded = false;
        }
    }

    // Add new link for the default root container to the containers panel if
    // needed
    if (newContainerLinkNeeded) {
        newContainerLinkHtml = wym._options.containersItemHtml;
        newContainerLinkHtml = WYMeditor.Helper.replaceAll(
            newContainerLinkHtml,
            WYMeditor.CONTAINER_NAME,
            defaultRootContainer.toUpperCase()
        );
        DSManager = WYMeditor.DocumentStructureManager;
        newContainerLinkHtml = WYMeditor.Helper.replaceAll(
            newContainerLinkHtml,
            WYMeditor.CONTAINER_TITLE,
            DSManager.DEFAULT_ROOT_CONTAINER_TITLES[defaultRootContainer]
        );
        newContainerLinkHtml = WYMeditor.Helper.replaceAll(
            newContainerLinkHtml,
            WYMeditor.CONTAINER_CLASS,
            "wym_containers_" + defaultRootContainer
        );
        $newContainerItem = jQuery(newContainerLinkHtml);
        $containerItems = jQuery(wym._box).find(wym._options.containersSelector)
                                          .find('li');
        $containerItems.eq(0).before($newContainerItem);

        // Bind click event for the new link
        $newContainerItem.find('a').click(function () {
            wym.container(jQuery(this).attr(WYMeditor.NAME));
            return false;
        });
    }
};

<|endoftext|>