<commit_before>/**
 * Creates a router middleware for a set of routes. Does a bunch of things:
 *
 * - Adds in location metadata and kicks off ROUTE_TO_NEXT actions.
 * - Calls any onEnter/onLeave hooks before the final ROUTE_TO action.
 * - Handles "exiting" of routes to the server.
 * - Tries its best to handle events that should open a tab or window instead of
 *   routing.
 * - Handles async routes.
 */

import createMiddleware from '../utils/createMiddleware';
import parseUrl from '../utils/parseUrl';
import ActionTypes from '../ActionTypes';
import routableUrl from '../utils/routableUrl';
import Actions from '../Actions';
import statesAreEqual from '../utils/statesAreEqual';
import isRightClick from '../utils/isRightClick';
import findRoutes from '../utils/findRoutes';
import matchRoutes from '../utils/matchRoutes';
import cloneRoutesForKey from '../utils/cloneRoutesForKey';
import isOnlyLocationHashChange from '../utils/isOnlyLocationHashChange';

const UNDEFINED_HREF = 'http://example.com/';

let _id = 0;
const uniqueId = () => {
  _id++;
  return _id;
};

const browserUrl = () => {
  if (typeof window === 'undefined') {
    return UNDEFINED_HREF;
  }
  if (!window || !window.location) {
    return UNDEFINED_HREF;
  }
  return window.location.href;
};

const baseUrl = browserUrl();

const cleanEvent = (
  { metaKey, ctrlKey, shiftKey, defaultPrevented, which, button } = {}
) => ({ metaKey, ctrlKey, shiftKey, defaultPrevented, which, button });

const openWindow = (...args) => {
  if (typeof window !== 'undefined') {
    window.open(...args);
  }
};

const isOnlyHashChange = (current, next) => {
  if (current && next) {
    return isOnlyLocationHashChange(current.location, next.location);
  }
  return false;
};

// Remove routes from the beginning of list a that are the same in route b.
const removeSameParentRoutes = (a, b) => {
  a = a || [];
  b = b || [];
  if (a.length > 0 && b.length > 0 && a[0].path === b[0].path) {
    return removeSameParentRoutes(a.slice(1), b.slice(1));
  }
  return a;
};

const routeMeta = match => {
  if (match && match.routes) {
    return match.routes.reduce(
      (currMeta, route) => {
        const extraStateFromFunction =
          typeof route.assign === 'function'
            ? route.assign({
              params: currMeta.params,
              query: currMeta.query
            })
            : undefined;
        const extraState =
          (typeof extraStateFromFunction === 'object' &&
            typeof extraStateFromFunction !== 'function' &&
            extraStateFromFunction) ||
          (typeof route.assign === 'object' && route.assign) ||
          {};
        return {
          name: route.name,
          params: currMeta.params,
          query: currMeta.query,
          ...extraState
        };
      },
      {
        params: match.params,
        query: match.query
      }
    );
  }

  return {};
};

const noOpBatchedUpdates = cb => {
  cb();
};

const createRouterMiddleware = (
  {
    routes,
    batchedUpdates = noOpBatchedUpdates,
    fetchRoute: fetchRouteFn = route => {
      console.error('No fetchRoute function provided for route:', route);
      return route;
    }
  } = {}
) => {
  if (!routes || !(typeof routes === 'object')) {
    throw new Error(
      'Must provide mapping from route strings to route objects.'
    );
  }

  const routeListeners = [];

  const emitRoutesChanged = () =>
    routeListeners.forEach(handler => handler(routes));

  const setRoutes = newRoutes => {
    routes = newRoutes;
    emitRoutesChanged();
  };

  const fetchRoute = route => {
    if (typeof route.fetch === 'function') {
      return Promise.resolve(route.fetch());
    }
    return Promise.resolve(fetchRouteFn(route));
  };

  const fetchAndUpdateRoutes = url => {
    const match = matchRoutes(routes, url);
    if (match && match.routes) {
      // Find the new route.
      let matchedRoutes = findRoutes(routes, match.key);
      if (matchedRoutes) {
        const lastRoute = matchedRoutes[matchedRoutes.length - 1];
        if (lastRoute.fetch) {
          return fetchRoute(lastRoute).then(resultRoute => {
            // Make sure we still have a place to put this.
            matchedRoutes = findRoutes(routes, match.key);
            if (!matchedRoutes) {
              return undefined;
            }
            const newRoutes = cloneRoutesForKey(routes, match.key);
            const newRoute = {
              ...resultRoute,
              fetch: undefined
            };
            const lastKey = match.key[match.key.length - 1];
            const parentRoutes =
              findRoutes(newRoutes, match.key.slice(0, match.key.length - 1)) ||
              [];
            const parentContainer =
              parentRoutes.length > 0
                ? parentRoutes[parentRoutes.length - 1].routes
                : newRoutes;
            parentContainer[lastKey] = newRoute;
            setRoutes(newRoutes);
            return fetchAndUpdateRoutes(url);
          });
        }
      }
    }
    return Promise.resolve();
  };

  let inFlightNext = null;

  const middleware = store => {
    const handlers = {
      // Normalize the action against current href and origin.
      // Dispatch action for next route.
      [ActionTypes.ROUTE_TO_INIT]({ router, dispatch, action, getState }) {
        const parsedUrl = parseUrl(action.payload.href, router.href || baseUrl);

        const url = routableUrl(
          parsedUrl.href,
          router.origin || parsedUrl.origin
        );

        const nextMatch = url != null && matchRoutes(routes, url);

        const assign = routeMeta(nextMatch);

        const routeId = uniqueId();

        const nextAction = {
          type: ActionTypes.ROUTE_TO_NEXT,
          payload: {
            ...action.payload,
            event: cleanEvent(action.payload.event || {}),
            isHistoryChange: action.payload.isHistoryChange
          },
          meta: {
            url,
            location: parsedUrl,
            assign,
            _routeId: routeId,
            routeKey: nextMatch && nextMatch.key
          }
        };

        if (nextMatch && nextMatch.routes) {
          const leafRoute = nextMatch.routes[nextMatch.routes.length - 1];
          if (leafRoute.fetch) {
            const fetchAction = { ...nextAction };
            fetchAction.type = ActionTypes.ROUTE_TO_FETCH;
            dispatch(fetchAction);
            return fetchAndUpdateRoutes(url).then(() => {
              const nextRouter = getState().router;
              if (nextRouter.fetch && nextRouter.fetch._routeId === routeId) {
                const newNextMatch = url != null && matchRoutes(routes, url);
                const newAssign = routeMeta(newNextMatch);
                const newNextAction = {
                  ...nextAction,
                  meta: {
                    ...nextAction.meta,
                    assign: newAssign,
                    routeKey: newNextMatch && newNextMatch.key
                  }
                };
                dispatch({
                  type: ActionTypes.ROUTE_TO_FETCH,
                  payload: null
                });
                return dispatch(newNextAction);
              }
              return undefined;
            });
          }
        }

        return dispatch(nextAction);
      },

      // Check for cancelling events.
      // Check for duplicate routes.
      // Otherwise, continue routing and check for cancellations/redirections.
      [ActionTypes.ROUTE_TO_NEXT]({
        router: currentRouter,
        getState,
        dispatch,
        action,
        next
      }) {
        const { meta } = action;
        const { event } = action.payload;

        if (event.defaultPrevented) {
          return Promise.resolve();
        }

        if (isRightClick(event)) {
          return Promise.resolve();
        }

        if (event.metaKey || event.ctrlKey || event.shiftKey) {
          return Promise.resolve(
            dispatch({
              ...action,
              type: ActionTypes.ROUTE_TO_WINDOW
            })
          );
        }

        // Don't allow routing to the exact same location as next.
        if (
          currentRouter.next &&
          currentRouter.next.location.href === meta.location.href &&
          statesAreEqual(currentRouter.next.state, action.payload.state)
        ) {
          // Unless we change `exit` flag, in which case we can modify
          // the next route.
          if (!!currentRouter.next.exit !== !!action.payload.exit) {
            dispatch({
              type: ActionTypes.MODIFY_ROUTE,
              payload: {
                exit: !!action.payload.exit
              }
            });
          }
          return inFlightNext || Promise.resolve();
        }

        // If there's a route in flight, auto cancel it.
        if (currentRouter.next) {
          dispatch(Actions.cancelRoute());
        }

        // Don't allow routing to the exact same location as current.
        if (
          currentRouter.current &&
          currentRouter.current.location.href === meta.location.href &&
          statesAreEqual(currentRouter.current.state, action.payload.state)
        ) {
          return Promise.resolve();
        }

        const result = next(action);
        const routeTo = (...args) => {
          dispatch(Actions.routeTo(...args));
        };
        const cancelRoute = (...args) => {
          dispatch(Actions.cancelRoute(...args));
        };
        const nextRoutes = findRoutes(routes, meta.routeKey);
        const currMatch =
          currentRouter.current &&
          matchRoutes(routes, currentRouter.current.url);

        inFlightNext = Promise.resolve(result)
          // Handle onLeave for the current route.
          .then(() => {
            const { router } = getState();
            if (router.next && router.next._routeId === meta._routeId) {
              if (router.current) {
                if (currMatch && currMatch.routes) {
                  const currLeavingRoutes = removeSameParentRoutes(
                    currMatch.routes,
                    nextRoutes
                  );

                  return currLeavingRoutes.reduceRight((promise, route) => {
                    if (route.onLeave && typeof route.onLeave === 'function') {
                      return (promise || Promise.resolve()).then(() =>
                        route.onLeave({
                          routeTo,
                          cancelRoute,
                          getState,
                          dispatch,
                          action,
                          router
                        })
                      );
                    }
                    return promise;
                  }, undefined);
                }
              }
            }
            return undefined;
          })
          // Handle onEnter for the next route.
          .then(() => {
            const { router } = getState();
            if (router.next && router.next._routeId === meta._routeId) {
              if (nextRoutes) {
                const nextEnteringRoutes = removeSameParentRoutes(
                  nextRoutes,
                  currMatch && currMatch.routes
                );

                return nextEnteringRoutes.reduce((promise, route) => {
                  if (route.onEnter && typeof route.onEnter === 'function') {
                    return (promise || Promise.resolve()).then(() =>
                      route.onEnter({
                        routeTo,
                        cancelRoute,
                        getState,
                        dispatch,
                        action,
                        router
                      })
                    );
                  }
                  return promise;
                }, undefined);
              }
            }
            return undefined;
          })
          // Finally, move to the next route.
          .then(() => {
            const { router } = getState();
            let dispatchResult;
            // Finish the route if we're still routing the same route.
            if (router.next && router.next._routeId === meta._routeId) {
              batchedUpdates(() => {
                dispatchResult = dispatch({
                  ...action,
                  type: router.next.exit
                    ? ActionTypes.ROUTE_TO_EXIT
                    : ActionTypes.ROUTE_TO
                });
              });
            }
            return dispatchResult;
          });

        return inFlightNext;
      },

      // Intercept actual route if unknown route or exit route.
      [ActionTypes.ROUTE_TO]({ router, dispatch, next, action }) {
        inFlightNext = null;
        const { meta } = action;

        const nextRoutes = findRoutes(routes, meta.routeKey);

        // Only allow exiting if we have already routed, to prevent loops.
        if (router.current) {
          // Probably should get route `exit` into state, but we need to
          // differenciate between route exits and legitimate redirects.
          if (!nextRoutes || nextRoutes.some(route => route.exit)) {
            return dispatch({
              ...action,
              type: ActionTypes.ROUTE_TO_EXIT
            });
          }
        }

        return next(action);
      },

      [ActionTypes.ROUTE_TO_WINDOW]({ action }) {
        inFlightNext = null;
        const { meta } = action;
        const { event } = action.payload;

        if (event.shiftKey) {
          openWindow(meta.location.href, '_blank');
          return;
        }

        openWindow(meta.location.href);
      },

      [ActionTypes.ROUTE_TO_EXIT]({ action, next, router }) {
        inFlightNext = null;
        const { meta } = action;

        // If we change back to ROUTE_TO here, we DO NOT RE-DISPATCH.
        // If we did, we could loop back around.
        // This logic could move to ROUTE_TO to avoid this, but it's nice to
        // only worry about window here. Alternatively, we could add a flag to
        // avoid looping, but that seems more confusing.

        if (typeof window !== 'undefined') {
          if (isOnlyHashChange(router.current, meta)) {
            // Browser wouldn't have reloaded this, so we won't either.
            // Probably should add a `reload` option later to force this.
            return next({
              ...action,
              type: ActionTypes.ROUTE_TO
            });
          } else if (isOnlyHashChange(window, meta)) {
            // Need for force a change, because window is out of sync.
            // Cancel may not have made it through yet to History.
            window.location.hash = meta.location.hash;
            window.location.reload();
          } else if (action.payload.replace && window.location.replace) {
            window.location.replace(meta.location.href);
          } else {
            window.location.href = meta.location.href;
          }
        } else if (meta.url) {
          // If we have a url and no window, then go ahead with ROUTE_TO.
          // (Because origin matched anyway.)
          return next({
            ...action,
            type: ActionTypes.ROUTE_TO
          });
        }
        return undefined;
      },

      [ActionTypes.CANCEL_ROUTE]({ action, next }) {
        inFlightNext = null;
        return next(action);
      },

      [ActionTypes.RESET_LOCATION]({ action, next }) {
        const newNextAction = {
          ...action,
          payload: {
            location: parseUrl(action.payload.href, baseUrl)
          }
        };

        return next(newNextAction);
      }
    };

    return createMiddleware(store, handlers);
  };

  middleware.setRoutes = setRoutes;

  middleware.onRoutesChanged = handler => {
    routeListeners.push(handler);

    const unsubscribe = () => {
      for (let i = 0; i < routeListeners.length; i++) {
        if (routeListeners[i] === handler) {
          routeListeners.splice(i, 1);
          break;
        }
      }
    };

    return unsubscribe;
  };

  return middleware;
};

export default createRouterMiddleware;
<commit_msg>Use `browserUrl` as default if no `payload.href` exists<commit_after>/**
 * Creates a router middleware for a set of routes. Does a bunch of things:
 *
 * - Adds in location metadata and kicks off ROUTE_TO_NEXT actions.
 * - Calls any onEnter/onLeave hooks before the final ROUTE_TO action.
 * - Handles "exiting" of routes to the server.
 * - Tries its best to handle events that should open a tab or window instead of
 *   routing.
 * - Handles async routes.
 */

import createMiddleware from '../utils/createMiddleware';
import parseUrl from '../utils/parseUrl';
import ActionTypes from '../ActionTypes';
import routableUrl from '../utils/routableUrl';
import Actions from '../Actions';
import statesAreEqual from '../utils/statesAreEqual';
import isRightClick from '../utils/isRightClick';
import findRoutes from '../utils/findRoutes';
import matchRoutes from '../utils/matchRoutes';
import cloneRoutesForKey from '../utils/cloneRoutesForKey';
import isOnlyLocationHashChange from '../utils/isOnlyLocationHashChange';
import browserUrl from '../utils/browserUrl';

const UNDEFINED_HREF = 'http://example.com/';

let _id = 0;
const uniqueId = () => {
  _id++;
  return _id;
};

const browserUrl = () => {
  if (typeof window === 'undefined') {
    return UNDEFINED_HREF;
  }
  if (!window || !window.location) {
    return UNDEFINED_HREF;
  }
  return window.location.href;
};

const baseUrl = browserUrl();

const cleanEvent = (
  { metaKey, ctrlKey, shiftKey, defaultPrevented, which, button } = {}
) => ({ metaKey, ctrlKey, shiftKey, defaultPrevented, which, button });

const openWindow = (...args) => {
  if (typeof window !== 'undefined') {
    window.open(...args);
  }
};

const isOnlyHashChange = (current, next) => {
  if (current && next) {
    return isOnlyLocationHashChange(current.location, next.location);
  }
  return false;
};

// Remove routes from the beginning of list a that are the same in route b.
const removeSameParentRoutes = (a, b) => {
  a = a || [];
  b = b || [];
  if (a.length > 0 && b.length > 0 && a[0].path === b[0].path) {
    return removeSameParentRoutes(a.slice(1), b.slice(1));
  }
  return a;
};

const routeMeta = match => {
  if (match && match.routes) {
    return match.routes.reduce(
      (currMeta, route) => {
        const extraStateFromFunction =
          typeof route.assign === 'function'
            ? route.assign({
              params: currMeta.params,
              query: currMeta.query
            })
            : undefined;
        const extraState =
          (typeof extraStateFromFunction === 'object' &&
            typeof extraStateFromFunction !== 'function' &&
            extraStateFromFunction) ||
          (typeof route.assign === 'object' && route.assign) ||
          {};
        return {
          name: route.name,
          params: currMeta.params,
          query: currMeta.query,
          ...extraState
        };
      },
      {
        params: match.params,
        query: match.query
      }
    );
  }

  return {};
};

const noOpBatchedUpdates = cb => {
  cb();
};

const createRouterMiddleware = (
  {
    routes,
    batchedUpdates = noOpBatchedUpdates,
    fetchRoute: fetchRouteFn = route => {
      console.error('No fetchRoute function provided for route:', route);
      return route;
    }
  } = {}
) => {
  if (!routes || !(typeof routes === 'object')) {
    throw new Error(
      'Must provide mapping from route strings to route objects.'
    );
  }

  const routeListeners = [];

  const emitRoutesChanged = () =>
    routeListeners.forEach(handler => handler(routes));

  const setRoutes = newRoutes => {
    routes = newRoutes;
    emitRoutesChanged();
  };

  const fetchRoute = route => {
    if (typeof route.fetch === 'function') {
      return Promise.resolve(route.fetch());
    }
    return Promise.resolve(fetchRouteFn(route));
  };

  const fetchAndUpdateRoutes = url => {
    const match = matchRoutes(routes, url);
    if (match && match.routes) {
      // Find the new route.
      let matchedRoutes = findRoutes(routes, match.key);
      if (matchedRoutes) {
        const lastRoute = matchedRoutes[matchedRoutes.length - 1];
        if (lastRoute.fetch) {
          return fetchRoute(lastRoute).then(resultRoute => {
            // Make sure we still have a place to put this.
            matchedRoutes = findRoutes(routes, match.key);
            if (!matchedRoutes) {
              return undefined;
            }
            const newRoutes = cloneRoutesForKey(routes, match.key);
            const newRoute = {
              ...resultRoute,
              fetch: undefined
            };
            const lastKey = match.key[match.key.length - 1];
            const parentRoutes =
              findRoutes(newRoutes, match.key.slice(0, match.key.length - 1)) ||
              [];
            const parentContainer =
              parentRoutes.length > 0
                ? parentRoutes[parentRoutes.length - 1].routes
                : newRoutes;
            parentContainer[lastKey] = newRoute;
            setRoutes(newRoutes);
            return fetchAndUpdateRoutes(url);
          });
        }
      }
    }
    return Promise.resolve();
  };

  let inFlightNext = null;

  const middleware = store => {
    const handlers = {
      // Normalize the action against current href and origin.
      // Dispatch action for next route.
      [ActionTypes.ROUTE_TO_INIT]({ router, dispatch, action, getState }) {
        const parsedUrl = parseUrl(action.payload.href, router.href || baseUrl);

        const url = routableUrl(
          parsedUrl.href,
          router.origin || parsedUrl.origin
        );

        const nextMatch = url != null && matchRoutes(routes, url);

        const assign = routeMeta(nextMatch);

        const routeId = uniqueId();

        const nextAction = {
          type: ActionTypes.ROUTE_TO_NEXT,
          payload: {
            ...action.payload,
            event: cleanEvent(action.payload.event || {}),
            isHistoryChange: action.payload.isHistoryChange
          },
          meta: {
            url,
            location: parsedUrl,
            assign,
            _routeId: routeId,
            routeKey: nextMatch && nextMatch.key
          }
        };

        if (nextMatch && nextMatch.routes) {
          const leafRoute = nextMatch.routes[nextMatch.routes.length - 1];
          if (leafRoute.fetch) {
            const fetchAction = { ...nextAction };
            fetchAction.type = ActionTypes.ROUTE_TO_FETCH;
            dispatch(fetchAction);
            return fetchAndUpdateRoutes(url).then(() => {
              const nextRouter = getState().router;
              if (nextRouter.fetch && nextRouter.fetch._routeId === routeId) {
                const newNextMatch = url != null && matchRoutes(routes, url);
                const newAssign = routeMeta(newNextMatch);
                const newNextAction = {
                  ...nextAction,
                  meta: {
                    ...nextAction.meta,
                    assign: newAssign,
                    routeKey: newNextMatch && newNextMatch.key
                  }
                };
                dispatch({
                  type: ActionTypes.ROUTE_TO_FETCH,
                  payload: null
                });
                return dispatch(newNextAction);
              }
              return undefined;
            });
          }
        }

        return dispatch(nextAction);
      },

      // Check for cancelling events.
      // Check for duplicate routes.
      // Otherwise, continue routing and check for cancellations/redirections.
      [ActionTypes.ROUTE_TO_NEXT]({
        router: currentRouter,
        getState,
        dispatch,
        action,
        next
      }) {
        const { meta } = action;
        const { event } = action.payload;

        if (event.defaultPrevented) {
          return Promise.resolve();
        }

        if (isRightClick(event)) {
          return Promise.resolve();
        }

        if (event.metaKey || event.ctrlKey || event.shiftKey) {
          return Promise.resolve(
            dispatch({
              ...action,
              type: ActionTypes.ROUTE_TO_WINDOW
            })
          );
        }

        // Don't allow routing to the exact same location as next.
        if (
          currentRouter.next &&
          currentRouter.next.location.href === meta.location.href &&
          statesAreEqual(currentRouter.next.state, action.payload.state)
        ) {
          // Unless we change `exit` flag, in which case we can modify
          // the next route.
          if (!!currentRouter.next.exit !== !!action.payload.exit) {
            dispatch({
              type: ActionTypes.MODIFY_ROUTE,
              payload: {
                exit: !!action.payload.exit
              }
            });
          }
          return inFlightNext || Promise.resolve();
        }

        // If there's a route in flight, auto cancel it.
        if (currentRouter.next) {
          dispatch(Actions.cancelRoute());
        }

        // Don't allow routing to the exact same location as current.
        if (
          currentRouter.current &&
          currentRouter.current.location.href === meta.location.href &&
          statesAreEqual(currentRouter.current.state, action.payload.state)
        ) {
          return Promise.resolve();
        }

        const result = next(action);
        const routeTo = (...args) => {
          dispatch(Actions.routeTo(...args));
        };
        const cancelRoute = (...args) => {
          dispatch(Actions.cancelRoute(...args));
        };
        const nextRoutes = findRoutes(routes, meta.routeKey);
        const currMatch =
          currentRouter.current &&
          matchRoutes(routes, currentRouter.current.url);

        inFlightNext = Promise.resolve(result)
          // Handle onLeave for the current route.
          .then(() => {
            const { router } = getState();
            if (router.next && router.next._routeId === meta._routeId) {
              if (router.current) {
                if (currMatch && currMatch.routes) {
                  const currLeavingRoutes = removeSameParentRoutes(
                    currMatch.routes,
                    nextRoutes
                  );

                  return currLeavingRoutes.reduceRight((promise, route) => {
                    if (route.onLeave && typeof route.onLeave === 'function') {
                      return (promise || Promise.resolve()).then(() =>
                        route.onLeave({
                          routeTo,
                          cancelRoute,
                          getState,
                          dispatch,
                          action,
                          router
                        })
                      );
                    }
                    return promise;
                  }, undefined);
                }
              }
            }
            return undefined;
          })
          // Handle onEnter for the next route.
          .then(() => {
            const { router } = getState();
            if (router.next && router.next._routeId === meta._routeId) {
              if (nextRoutes) {
                const nextEnteringRoutes = removeSameParentRoutes(
                  nextRoutes,
                  currMatch && currMatch.routes
                );

                return nextEnteringRoutes.reduce((promise, route) => {
                  if (route.onEnter && typeof route.onEnter === 'function') {
                    return (promise || Promise.resolve()).then(() =>
                      route.onEnter({
                        routeTo,
                        cancelRoute,
                        getState,
                        dispatch,
                        action,
                        router
                      })
                    );
                  }
                  return promise;
                }, undefined);
              }
            }
            return undefined;
          })
          // Finally, move to the next route.
          .then(() => {
            const { router } = getState();
            let dispatchResult;
            // Finish the route if we're still routing the same route.
            if (router.next && router.next._routeId === meta._routeId) {
              batchedUpdates(() => {
                dispatchResult = dispatch({
                  ...action,
                  type: router.next.exit
                    ? ActionTypes.ROUTE_TO_EXIT
                    : ActionTypes.ROUTE_TO
                });
              });
            }
            return dispatchResult;
          });

        return inFlightNext;
      },

      // Intercept actual route if unknown route or exit route.
      [ActionTypes.ROUTE_TO]({ router, dispatch, next, action }) {
        inFlightNext = null;
        const { meta } = action;

        const nextRoutes = findRoutes(routes, meta.routeKey);

        // Only allow exiting if we have already routed, to prevent loops.
        if (router.current) {
          // Probably should get route `exit` into state, but we need to
          // differenciate between route exits and legitimate redirects.
          if (!nextRoutes || nextRoutes.some(route => route.exit)) {
            return dispatch({
              ...action,
              type: ActionTypes.ROUTE_TO_EXIT
            });
          }
        }

        return next(action);
      },

      [ActionTypes.ROUTE_TO_WINDOW]({ action }) {
        inFlightNext = null;
        const { meta } = action;
        const { event } = action.payload;

        if (event.shiftKey) {
          openWindow(meta.location.href, '_blank');
          return;
        }

        openWindow(meta.location.href);
      },

      [ActionTypes.ROUTE_TO_EXIT]({ action, next, router }) {
        inFlightNext = null;
        const { meta } = action;

        // If we change back to ROUTE_TO here, we DO NOT RE-DISPATCH.
        // If we did, we could loop back around.
        // This logic could move to ROUTE_TO to avoid this, but it's nice to
        // only worry about window here. Alternatively, we could add a flag to
        // avoid looping, but that seems more confusing.

        if (typeof window !== 'undefined') {
          if (isOnlyHashChange(router.current, meta)) {
            // Browser wouldn't have reloaded this, so we won't either.
            // Probably should add a `reload` option later to force this.
            return next({
              ...action,
              type: ActionTypes.ROUTE_TO
            });
          } else if (isOnlyHashChange(window, meta)) {
            // Need for force a change, because window is out of sync.
            // Cancel may not have made it through yet to History.
            window.location.hash = meta.location.hash;
            window.location.reload();
          } else if (action.payload.replace && window.location.replace) {
            window.location.replace(meta.location.href);
          } else {
            window.location.href = meta.location.href;
          }
        } else if (meta.url) {
          // If we have a url and no window, then go ahead with ROUTE_TO.
          // (Because origin matched anyway.)
          return next({
            ...action,
            type: ActionTypes.ROUTE_TO
          });
        }
        return undefined;
      },

      [ActionTypes.CANCEL_ROUTE]({ action, next }) {
        inFlightNext = null;
        return next(action);
      },

      [ActionTypes.RESET_LOCATION]({ action, next }) {
        const newNextAction = {
          ...action,
          payload: {
            location: parseUrl(action.payload.href || browserUrl(), baseUrl)
          }
        };

        return next(newNextAction);
      }
    };

    return createMiddleware(store, handlers);
  };

  middleware.setRoutes = setRoutes;

  middleware.onRoutesChanged = handler => {
    routeListeners.push(handler);

    const unsubscribe = () => {
      for (let i = 0; i < routeListeners.length; i++) {
        if (routeListeners[i] === handler) {
          routeListeners.splice(i, 1);
          break;
        }
      }
    };

    return unsubscribe;
  };

  return middleware;
};

export default createRouterMiddleware;
<|endoftext|>


<commit_before>
var EventDefDateMutation = Class.extend({

	clearEnd: false,
	forceTimed: false,
	forceAllDay: false,

	// Durations. if 0-ms duration, will be null instead.
	// Callers should not set this directly.
	dateDelta: null,
	startDelta: null,
	endDelta: null,

	/*
	returns an undo function.
	*/
	buildNewDateProfile: function(eventDateProfile, calendar) {
		var start = eventDateProfile.start.clone();
		var end = null;
		var shouldRezone = false;

		if (!this.clearEnd && eventDateProfile.end) {
			end = eventDateProfile.end.clone();
		}

		if (this.forceTimed) {
			shouldRezone = true;

			if (!start.hasTime()) {
				start.time(0);
			}

			if (end && !end.hasTime()) {
				end.time(0);
			}
		}
		else if (this.forceAllDay) {

			if (start.hasTime()) {
				start.stripTime();
			}

			if (end && end.hasTime()) {
				end.stripTime();
			}
		}

		if (this.dateDelta) {
			shouldRezone = true;

			start.add(this.dateDelta);

			if (end) {
				end.add(this.dateDelta);
			}
		}

		// do this before adding startDelta to start, so we can work off of start
		if (this.endDelta) {
			shouldRezone = true;

			if (!end) {
				end = calendar.getDefaultEventEnd(eventDateProfile.isAllDay(), start);
			}

			end.add(this.endDelta);
		}

		if (this.startDelta) {
			shouldRezone = true;

			start.add(this.startDelta);
		}

		if (shouldRezone) {
			start = calendar.applyTimezone(start);

			if (end) {
				end = calendar.applyTimezone(end);
			}
		}

		// TODO: okay to access calendar option?
		if (!end && calendar.opt('forceEventDuration')) {
			end = calendar.getDefaultEventEnd(eventDateProfile.isAllDay(), start);
		}

		return new EventDateProfile(start, end, calendar);
	},

	setDateDelta: function(dateDelta) {
		if (dateDelta && dateDelta.valueOf()) {
			this.dateDelta = dateDelta;
		}
		else {
			this.dateDelta = null;
		}
	},

	setStartDelta: function(startDelta) {
		if (startDelta && startDelta.valueOf()) {
			this.startDelta = startDelta;
		}
		else {
			this.startDelta = null;
		}
	},

	setEndDelta: function(endDelta) {
		if (endDelta && endDelta.valueOf()) {
			this.endDelta = endDelta;
		}
		else {
			this.endDelta = null;
		}
	},

	isEmpty: function() {
		return !this.clearEnd && !this.forceTimed && !this.forceAllDay &&
			!this.dateDelta && !this.startDelta && !this.endDelta;
	}

});

EventDefDateMutation.createFromDiff = function(dateProfile0, dateProfile1, largeUnit) {
	var clearEnd = dateProfile0.end && !dateProfile1.end;
	var forceTimed = dateProfile0.isAllDay() && !dateProfile1.isAllDay();
	var forceAllDay = !dateProfile0.isAllDay() && dateProfile1.isAllDay();
	var dateDelta;
	var endDiff;
	var endDelta;
	var mutation;

	// subtracts the dates in the appropriate way, returning a duration
	function subtractDates(date1, date0) { // date1 - date0
		if (largeUnit) {
			return diffByUnit(date1, date0, largeUnit); // poorly named
		}
		else if (dateProfile1.isAllDay()) {
			return diffDay(date1, date0); // poorly named
		}
		else {
			return diffDayTime(date1, date0); // poorly named
		}
	}

	dateDelta = subtractDates(dateProfile1.start, dateProfile0.start);

	if (dateProfile1.end) {
		endDiff = subtractDates(dateProfile1.end, dateProfile0.getEnd());
		endDelta = endDiff.subtract(dateDelta);
	}

	mutation = new EventDefDateMutation();
	mutation.clearEnd = clearEnd;
	mutation.forceTimed = forceTimed;
	mutation.forceAllDay = forceAllDay;
	mutation.setDateDelta(dateDelta);
	mutation.setEndDelta(endDelta);

	return mutation;
};
<commit_msg>fix for bad getEnd<commit_after>
var EventDefDateMutation = Class.extend({

	clearEnd: false,
	forceTimed: false,
	forceAllDay: false,

	// Durations. if 0-ms duration, will be null instead.
	// Callers should not set this directly.
	dateDelta: null,
	startDelta: null,
	endDelta: null,

	/*
	returns an undo function.
	*/
	buildNewDateProfile: function(eventDateProfile, calendar) {
		var start = eventDateProfile.start.clone();
		var end = null;
		var shouldRezone = false;

		if (!this.clearEnd && eventDateProfile.end) {
			end = eventDateProfile.end.clone();
		}

		if (this.forceTimed) {
			shouldRezone = true;

			if (!start.hasTime()) {
				start.time(0);
			}

			if (end && !end.hasTime()) {
				end.time(0);
			}
		}
		else if (this.forceAllDay) {

			if (start.hasTime()) {
				start.stripTime();
			}

			if (end && end.hasTime()) {
				end.stripTime();
			}
		}

		if (this.dateDelta) {
			shouldRezone = true;

			start.add(this.dateDelta);

			if (end) {
				end.add(this.dateDelta);
			}
		}

		// do this before adding startDelta to start, so we can work off of start
		if (this.endDelta) {
			shouldRezone = true;

			if (!end) {
				end = calendar.getDefaultEventEnd(eventDateProfile.isAllDay(), start);
			}

			end.add(this.endDelta);
		}

		if (this.startDelta) {
			shouldRezone = true;

			start.add(this.startDelta);
		}

		if (shouldRezone) {
			start = calendar.applyTimezone(start);

			if (end) {
				end = calendar.applyTimezone(end);
			}
		}

		// TODO: okay to access calendar option?
		if (!end && calendar.opt('forceEventDuration')) {
			end = calendar.getDefaultEventEnd(eventDateProfile.isAllDay(), start);
		}

		return new EventDateProfile(start, end, calendar);
	},

	setDateDelta: function(dateDelta) {
		if (dateDelta && dateDelta.valueOf()) {
			this.dateDelta = dateDelta;
		}
		else {
			this.dateDelta = null;
		}
	},

	setStartDelta: function(startDelta) {
		if (startDelta && startDelta.valueOf()) {
			this.startDelta = startDelta;
		}
		else {
			this.startDelta = null;
		}
	},

	setEndDelta: function(endDelta) {
		if (endDelta && endDelta.valueOf()) {
			this.endDelta = endDelta;
		}
		else {
			this.endDelta = null;
		}
	},

	isEmpty: function() {
		return !this.clearEnd && !this.forceTimed && !this.forceAllDay &&
			!this.dateDelta && !this.startDelta && !this.endDelta;
	}

});

EventDefDateMutation.createFromDiff = function(dateProfile0, dateProfile1, largeUnit) {
	var clearEnd = dateProfile0.end && !dateProfile1.end;
	var forceTimed = dateProfile0.isAllDay() && !dateProfile1.isAllDay();
	var forceAllDay = !dateProfile0.isAllDay() && dateProfile1.isAllDay();
	var dateDelta;
	var endDiff;
	var endDelta;
	var mutation;

	// subtracts the dates in the appropriate way, returning a duration
	function subtractDates(date1, date0) { // date1 - date0
		if (largeUnit) {
			return diffByUnit(date1, date0, largeUnit); // poorly named
		}
		else if (dateProfile1.isAllDay()) {
			return diffDay(date1, date0); // poorly named
		}
		else {
			return diffDayTime(date1, date0); // poorly named
		}
	}

	dateDelta = subtractDates(dateProfile1.start, dateProfile0.start);

	if (dateProfile1.end) {
		// use unzonedRanges because dateProfile0.end might be null
		endDiff = subtractDates(
			dateProfile1.unzonedRange.getEnd(),
			dateProfile0.unzonedRange.getEnd()
		);
		endDelta = endDiff.subtract(dateDelta);
	}

	mutation = new EventDefDateMutation();
	mutation.clearEnd = clearEnd;
	mutation.forceTimed = forceTimed;
	mutation.forceAllDay = forceAllDay;
	mutation.setDateDelta(dateDelta);
	mutation.setEndDelta(endDelta);

	return mutation;
};
<|endoftext|>


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

let path = require('path');

let minimist = require('minimist');
let fsExtra = require('fs-extra');

let args = minimist(process.argv.slice(2));

let from = args._[0] || path.join(__dirname, '..', 'react-native-website');
let to = args._[1] || path.join(__dirname, '..');
let version = args._[2] || 'unversioned';
let toVersion = path.join(to, 'versions', version);

let docsFrom = path.join(from, 'docs/');
let sidebarsJson = path.join(from, 'website', 'sidebars.json');

let reactNative = path.join(toVersion, 'react-native');

let mainAsync = async () => {
  // Go through each file and fix them
  let files = await fsExtra.readdir(to);

  let sidebarInfo = await fsExtra.readJson(sidebarsJson);
  let guides = sidebarInfo.docs.Guides;
  let components = sidebarInfo.docs.Components;
  let apis = sidebarInfo.docs.APIs;
  let basics = sidebarInfo.docs['The Basics'];

  await fsExtra.ensureDir(reactNative);

  let transformFileAysnc = async (src, dest) => {
    let basename = path.basename(src);
    switch (basename) {
      case 'getting-started.md':
      case 'getting-started.md':
      case 'more-resources.md':
      case 'appregistry.md':
      case 'geolocation.md':
      case 'building-for-apple-tv.md':
      case 'components-and-apis.md':
      case 'debugging.md':
      case 'integration-with-existing-apps.md':
      case 'running-on-device.md':
      case 'troubleshooting.md':
      case 'upgrading.md':
      case 'improvingux.md':

      // Things that maybe should just have a note telling people about the better Expo version?
      // TODO: Maybe do that?
      case 'imageeditor.md':
      case 'imagepickerios.md':
      case 'cameraroll.md':
      case 'linking.md':
      case 'permissionsandroid.md':
      case 'pushnotificationios.md':
        console.log('Skipping ' + basename);
        return;
    }

    let s = '';
    let contents = await fsExtra.readFile(src, 'utf8');
    let lines = contents.split('\n');
    let inCodeBlock = false;

    let inAlertSpecialSection = false;

    for (let l of lines) {
      l = l.replace('```ReactNativeWebPlayer', '```javascript');
      // TODO: Make this work with Snack :/
      l = l.replace(/```SnackPlayer.*$/, '```javascript');
      l = l.replace(
        '(more-resources.md)',
        '(https://facebook.github.io/react-native/docs/more-resources.html)'
      );

      l.replace(
       /\[`Toolbar` widget\]\([^\)]+\)/,
       '[`Toolbar` widget](https://developer.android.com/reference/android/support/v7/widget/Toolbar.html)'
      );
      l.replace(
        /\[navigator\.geolocation\]\([^\)]+\)/g,
        '[navigator.geolocation](../../sdk/location/)'
      );

      l.replace(
        /\[CameraRoll\]([^\)]+\)/g,
        '[CameraRoll](https://facebook.github.io/react-native/docs/cameraroll.html)'
      );

      // `](./foo` -> `](foo`
      l = l.replace(
        /\]\(\.\/([^\):]+)/g,
        (_match, path) => `](${path}`
      );
      // `](foo.md)` -> `](../foo/)`
      // `](foo.md#bar)` -> `](../foo/#bar]`
      l = l.replace(
        /\]\(([^\):]+).md(#[^\)]+)?\)/g,
        (_match, path, anchor) => `](../${path}/${anchor ? anchor : ''})`
      );

      // TODO: Make these global replaces instead of just first instance
      l = l.replace(
        '<img src="/react-native/docs/assets/',
        ' '![' + $1 + '](https://facebook.github.io/react-native/docs/'
      );

      let r = new RegExp('!\\[(.*)\\]\\(\\/react\\-native\\/docs\\/', 'g');
      l = l.replace(
        r,
        (match, $1) => '![' + $1 + '](https://facebook.github.io/react-native/docs/'
      );

      switch (basename) {
        default:
          if (/^```/.test(l)) {
            if (!inCodeBlock) {
              if (l === '```') {
                l = l.replace(/```\s*$/, '```javascript');
              }
            }
            l = '\n' + l + '\n';
            inCodeBlock = !inCodeBlock;
          }
          break;
      }

      if (!inCodeBlock) {
        let nl = '';
        let inInlineCodeBlock = false;
        for (let c of l) {
          let nc = c;
          if (c === '`') {
            inInlineCodeBlock = !inInlineCodeBlock;
          }
          if (!inInlineCodeBlock) {
            switch (c) {
              case '<':
                if (l.indexOf('|') < 0) {
                  break;
                }
              case '{':
              case '}':
                // case '>':
                nc = '${"' + c + '"}';
                break;
              default:
                nc = c;
                break;
            }
          }
          nl += nc;
        }
        l = nl;
      }

      if (basename === 'alert.md') {
        if (l === '
') {
          inAlertSpecialSection = false;

          l =
            '#### iOS Alert Example\n\n![iOS Alert Example](https://facebook.github.io/react-native/docs/assets/Alert/exampleios.gif)' +
            '\n';
          l +=
            '#### Android Alert Example\n\n![Android Alert Example](https://facebook.github.io/react-native/docs/assets/Alert/exampleandroid.gif)';
        }
        if (l === '
') {
          inAlertSpecialSection = true;
        }
        if (inAlertSpecialSection) {
          l = '';
        }
      }

      if (basename === 'image.md') {
        l = l.replace(/^> /, ' ');
      }
      if (basename === 'flatlist.md') {
        if (/Minimal /.test(l)) {
          l = l + '\n\n```javascript';
          inCodeBlock = true;
        }
        if (/More complex, multi-select example/.test(l)) {
          l = '```\n\n' + l;
          inCodeBlock = false;
        }

        if (/class MyListItem extends/.test(l)) {
          l = '```javascript\n' + l;
          inCodeBlock = true;
        }

        if (/This is a convenience wrapper/.test(l)) {
          l = '```\n\n' + l;
          inCodeBlock = false;
        }
      }

      if (basename === 'picker.md') {
        if (/    <Picker$/.test(l)) {
          l = '\n```javascript\n\n' + l;
          inCodeBlock = true;
        }
        if (/    <\/Picker>/.test(l)) {
          l = l + '\n\n```\n';
          inCodeBlock = false;
        }
      }

      if (basename === 'segmentedcontrolios.md') {
        if (/<SegmentedControlIOS/.test(l)) {
          l = '\n' + l;
        }
        if (l.startsWith('/>')) {
          l = l + '\n';
        }
      }

      if (basename === 'layoutanimation.md') {
        if (/UIManager.setLayoutAnimationEnabledExperimental/.test(l)) {
          l = '\n```java\n' + l + '\n```\n';
        }
      }

      if (basename === 'javascript-environment.md') {
        l = l.replace(
          "`` var who = 'world'; var str = `Hello ${who}`; ``",
          "``` var who = 'world'; var str = `Hello ${who}`; ```"
        );
      }

      switch (basename) {
        case 'touchablewithoutfeedback.md':
          if (l.startsWith('#')) {
            l = l.replace(/`/g, '');
          }
          break;
        default:
          break;
      }

      if (l.startsWith('|')) {
      }

      s += l + '\n';
    }

    await fsExtra.writeFile(dest, s, 'utf8');
    // console.log(src, "->", dest);
    console.log('Wrote ' + basename);
  };

  let copyFilesAsync = async (x, destDir) => {
    for (let i = 0; i < x.length; i++) {
      let src = path.join(docsFrom, x[i]) + '.md';
      let dest = path.join(destDir, x[i]) + '.md';
      await transformFileAysnc(src, dest);
    }
  };

  for (let [x, dest] of [
    [guides, reactNative],
    [components, reactNative],
    [basics, reactNative],
    [apis, reactNative],
  ]) {
    await copyFilesAsync(x, dest);
  }

  console.log(
    '#guides',
    guides.length,
    '#components',
    components.length,
    '#apis',
    apis.length,
    '#basics',
    basics.length
  );
};

if (require.main === module) {
  (() => {
    mainAsync()
      .then(() => {
        // done
      })
      .catch(err => {
        console.error('Error: ' + err);
      });
  })();
}
<commit_msg>[docs] import-react-native-docs escapes brackets with \<commit_after>#!/usr/bin/env node

let path = require('path');

let minimist = require('minimist');
let fsExtra = require('fs-extra');

let args = minimist(process.argv.slice(2));

let from = args._[0] || path.join(__dirname, '..', 'react-native-website');
let to = args._[1] || path.join(__dirname, '..');
let version = args._[2] || 'unversioned';
let toVersion = path.join(to, 'versions', version);

let docsFrom = path.join(from, 'docs/');
let sidebarsJson = path.join(from, 'website', 'sidebars.json');

let reactNative = path.join(toVersion, 'react-native');

let mainAsync = async () => {
  // Go through each file and fix them
  let files = await fsExtra.readdir(to);

  let sidebarInfo = await fsExtra.readJson(sidebarsJson);
  let guides = sidebarInfo.docs.Guides;
  let components = sidebarInfo.docs.Components;
  let apis = sidebarInfo.docs.APIs;
  let basics = sidebarInfo.docs['The Basics'];

  await fsExtra.ensureDir(reactNative);

  let transformFileAysnc = async (src, dest) => {
    let basename = path.basename(src);
    switch (basename) {
      case 'getting-started.md':
      case 'getting-started.md':
      case 'more-resources.md':
      case 'appregistry.md':
      case 'geolocation.md':
      case 'building-for-apple-tv.md':
      case 'components-and-apis.md':
      case 'debugging.md':
      case 'integration-with-existing-apps.md':
      case 'running-on-device.md':
      case 'troubleshooting.md':
      case 'upgrading.md':
      case 'improvingux.md':

      // Things that maybe should just have a note telling people about the better Expo version?
      // TODO: Maybe do that?
      case 'imageeditor.md':
      case 'imagepickerios.md':
      case 'cameraroll.md':
      case 'linking.md':
      case 'permissionsandroid.md':
      case 'pushnotificationios.md':
        console.log('Skipping ' + basename);
        return;
    }

    let s = '';
    let contents = await fsExtra.readFile(src, 'utf8');
    let lines = contents.split('\n');
    let inCodeBlock = false;

    let inAlertSpecialSection = false;

    for (let l of lines) {
      l = l.replace('```ReactNativeWebPlayer', '```javascript');
      // TODO: Make this work with Snack :/
      l = l.replace(/```SnackPlayer.*$/, '```javascript');
      l = l.replace(
        '(more-resources.md)',
        '(https://facebook.github.io/react-native/docs/more-resources.html)'
      );

      l.replace(
       /\[`Toolbar` widget\]\([^\)]+\)/,
       '[`Toolbar` widget](https://developer.android.com/reference/android/support/v7/widget/Toolbar.html)'
      );
      l.replace(
        /\[navigator\.geolocation\]\([^\)]+\)/g,
        '[navigator.geolocation](../../sdk/location/)'
      );

      l.replace(
        /\[CameraRoll\]([^\)]+\)/g,
        '[CameraRoll](https://facebook.github.io/react-native/docs/cameraroll.html)'
      );

      // `](./foo` -> `](foo`
      l = l.replace(
        /\]\(\.\/([^\):]+)/g,
        (_match, path) => `](${path}`
      );
      // `](foo.md)` -> `](../foo/)`
      // `](foo.md#bar)` -> `](../foo/#bar]`
      l = l.replace(
        /\]\(([^\):]+).md(#[^\)]+)?\)/g,
        (_match, path, anchor) => `](../${path}/${anchor ? anchor : ''})`
      );

      // TODO: Make these global replaces instead of just first instance
      l = l.replace(
        '<img src="/react-native/docs/assets/',
        ' '![' + $1 + '](https://facebook.github.io/react-native/docs/'
      );

      let r = new RegExp('!\\[(.*)\\]\\(\\/react\\-native\\/docs\\/', 'g');
      l = l.replace(
        r,
        (match, $1) => '![' + $1 + '](https://facebook.github.io/react-native/docs/'
      );

      switch (basename) {
        default:
          if (/^```/.test(l)) {
            if (!inCodeBlock) {
              if (l === '```') {
                l = l.replace(/```\s*$/, '```javascript');
              }
            }
            l = '\n' + l + '\n';
            inCodeBlock = !inCodeBlock;
          }
          break;
      }

      if (!inCodeBlock) {
        let nl = '';
        let inInlineCodeBlock = false;
        for (let c of l) {
          let nc = c;
          if (c === '`') {
            inInlineCodeBlock = !inInlineCodeBlock;
          }
          if (!inInlineCodeBlock && ['<', '>'].includes(c)) {
            nc = '\\' + c;
          }
          nl += nc;
        }
        l = nl;
      }

      if (basename === 'alert.md') {
        if (l === '
') {
          inAlertSpecialSection = false;

          l =
            '#### iOS Alert Example\n\n![iOS Alert Example](https://facebook.github.io/react-native/docs/assets/Alert/exampleios.gif)' +
            '\n';
          l +=
            '#### Android Alert Example\n\n![Android Alert Example](https://facebook.github.io/react-native/docs/assets/Alert/exampleandroid.gif)';
        }
        if (l === '
') {
          inAlertSpecialSection = true;
        }
        if (inAlertSpecialSection) {
          l = '';
        }
      }

      if (basename === 'image.md') {
        l = l.replace(/^> /, ' ');
      }
      if (basename === 'flatlist.md') {
        if (/Minimal /.test(l)) {
          l = l + '\n\n```javascript';
          inCodeBlock = true;
        }
        if (/More complex, multi-select example/.test(l)) {
          l = '```\n\n' + l;
          inCodeBlock = false;
        }

        if (/class MyListItem extends/.test(l)) {
          l = '```javascript\n' + l;
          inCodeBlock = true;
        }

        if (/This is a convenience wrapper/.test(l)) {
          l = '```\n\n' + l;
          inCodeBlock = false;
        }
      }

      if (basename === 'picker.md') {
        if (/    <Picker$/.test(l)) {
          l = '\n```javascript\n\n' + l;
          inCodeBlock = true;
        }
        if (/    <\/Picker>/.test(l)) {
          l = l + '\n\n```\n';
          inCodeBlock = false;
        }
      }

      if (basename === 'segmentedcontrolios.md') {
        if (/<SegmentedControlIOS/.test(l)) {
          l = '\n' + l;
        }
        if (l.startsWith('/>')) {
          l = l + '\n';
        }
      }

      if (basename === 'layoutanimation.md') {
        if (/UIManager.setLayoutAnimationEnabledExperimental/.test(l)) {
          l = '\n```java\n' + l + '\n```\n';
        }
      }

      if (basename === 'javascript-environment.md') {
        l = l.replace(
          "`` var who = 'world'; var str = `Hello ${who}`; ``",
          "``` var who = 'world'; var str = `Hello ${who}`; ```"
        );
      }

      switch (basename) {
        case 'touchablewithoutfeedback.md':
          if (l.startsWith('#')) {
            l = l.replace(/`/g, '');
          }
          break;
        default:
          break;
      }

      if (l.startsWith('|')) {
      }

      s += l + '\n';
    }

    await fsExtra.writeFile(dest, s, 'utf8');
    // console.log(src, "->", dest);
    console.log('Wrote ' + basename);
  };

  let copyFilesAsync = async (x, destDir) => {
    for (let i = 0; i < x.length; i++) {
      let src = path.join(docsFrom, x[i]) + '.md';
      let dest = path.join(destDir, x[i]) + '.md';
      await transformFileAysnc(src, dest);
    }
  };

  for (let [x, dest] of [
    [guides, reactNative],
    [components, reactNative],
    [basics, reactNative],
    [apis, reactNative],
  ]) {
    await copyFilesAsync(x, dest);
  }

  console.log(
    '#guides',
    guides.length,
    '#components',
    components.length,
    '#apis',
    apis.length,
    '#basics',
    basics.length
  );
};

if (require.main === module) {
  (() => {
    mainAsync()
      .then(() => {
        // done
      })
      .catch(err => {
        console.error('Error: ' + err);
      });
  })();
}
<|endoftext|>


<commit_before>/**
 * NeoTabs - jQuery plugin
 *
 * source: http://github.com/PascalPrecht/jquery.neotabs.js/
 * site: http://pascalprecht.github.com/jquery.neotabs.js/
 *
 * @author: Pascal Precht <pascal.precht@gmail.com>
 * Released under the MIT and GPL Licenses.
 */
;(function ($, window, undefined) {

  var pluginName = 'neoTabs',
      document = window.document,

      defaults = {
        wrapperClass: 'content',
        activeClass: 'active',
        tabHeadClass: 'tabhead',
        tabBodyClass: 'tabbody',
        firstTabClass: 'first',
        lastTabClass: 'last',
        clearfixClass: 'group',
        tabsListClass: 'tabs-list',
        tabHeadElement: 'h4',
        tabsPosition: 'top',
        cssClassAvailable: true,
        fx: 'show',
        fxSpeed: 0,
        autoAnchor: true,
        wrapInnerTabs: '',
        dropdownTabLabel: '▾',
        dropdownTabClass: 'dropdown',
        dropdownTabsListClass: 'tabs-list',
        dropdownTabsClearfixClass: 'group'
      },
      tabsCount = 0,
      keyCodes = {
        37: -1,
        38: -1,
        39: +1,
        40: +1
      },
      positions = {
        top: 'prepend',
        bottom: 'append'
      };

  // Helper function to check exclusively for an HTML5 data-attribute. Thanks to
  // @cburgdorf for typing down this gist for me: (https://gist.github.com/3979912)
  $.fn.hasDataAttr = function (attr) {
    var value = this.attr('data-' + attr);
    return typeof value !== 'undefined' && value !== false;
  };

  if ($('body').data('accessibleTabsCount') !== undefined) {
    tabsCount = $('body').data('accessibleTabsCount');
  }

  $('body').data('accessibleTabsCount', tabsCount);

  function NeoTabs(element, options) {
    var _this = this;

    $.extend(_this, {
      $el: element,
      opts: $.extend({}, defaults, options),
      keyCodes: keyCodes,
      hasDropdown: false,
      hasPreActiveTab: false,
      tabsList: null,
      dropdownTabsList: null,
      currentTabsCount: tabsCount
    });

    _this.tabsList = new TabsList({
      clearfixClass: _this.opts.clearfixClass,
      tabsListClass: _this.opts.tabsListClass
    });

    _this.$el.wrapInner('

');

    _this.$el.find(_this.opts.tabHeadElement).each(function (i) {

      var $tabHeadElement = $(this);

      if (!_this.hasDropdown && $tabHeadElement.hasDataAttr('neotabs-dropdown')) {
        _this.dropdownTabsList = new TabsList({
          clearfixClass: _this.opts.dropdownTabsClearfixClass,
          tabsListClass: _this.opts.dropdownTabsListClass
        });
        _this.hasDropdown = true;
      }

      if (!_this.hasPreActiveTab && $tabHeadElement.hasDataAttr('neotabs-active')) {
        $tabHeadElement.addClass(_this.opts.activeClass);
        _this.hasPreActiveTab = true;
      }

      var tab = new Tab({
        label: $tabHeadElement.html(),
        id: _this.generateTabId(tabsCount, i),
        tabsList: null,
        cssClass: (_this.opts.cssClassAvailable) ?
          (($tabHeadElement.attr('class') || '') + ' ' + _this.opts.tabHeadClass) :
          _this.opts.tabHeadClass
      });

      if (_this.hasDropdown) {
        _this.dropdownTabsList.addTab(tab);
      } else {
        _this.tabsList.addTab(tab);
      }

      $tabHeadElement.attr({
        'id': tab.id,
        'class': _this.opts.tabHeadClass,
        'tab-index': '-1'
      });
    });

    // Generate dropdown tab if hasDropdown flag is true
    if (_this.hasDropdown) {
      _this.tabsList.addTab(new Tab({
        label: _this.opts.dropdownTabLabel,
        id: '',
        tabsList: _this.dropdownTabsList,
        cssClass: _this.opts.tabHeadClass + ' ' + _this.opts.dropdownTabClass
      }));
    }

    if (!_this.$el.find('.' + _this.opts.tabsListClass).length) {
      _this.$el[positions[_this.opts.tabsPosition]](_this.tabsList.toHtml());
    }

    var $content = _this.$el.find('.' + _this.opts.tabBodyClass),
        $tabsList = _this.$el.find('.' + _this.opts.tabsListClass);

    // Show the first tab content by default
    if ($content.length > 0) {
      $content.hide();
      $($content[0]).show();
    }

    // Which tab should be active?
    $tabsList.find(' > li:first')
      .addClass(_this.opts.firstTabClass + ((!_this.hasPreActiveTab) ? ' ' + _this.opts.activeClass : ''))
      .closest('ul').find('> li:last')
      .addClass(_this.opts.lastTabClass);

    if (_this.opts.wrapInnerTabs) {
      $tabsList.find('> li > a').wrapInner(_this.opts.wrapInnerTabs);
    }

    $tabsList.find('> li a').each(function (i) {

      $(this).on('click', function (e) {
        e.preventDefault();

        $tabsList
          .find('> li.' + _this.opts.activeClass)
          .removeClass(_this.opts.activeClass);

        $(this).parent().addClass(_this.opts.activeClass);
        $(this).blur();

        var j = i;

        if (!$(this).parent().hasClass(_this.opts.dropdownTabClass)) {
          _this.$el.find('.' + _this.opts.tabBodyClass + ':visible').hide();

          // little hack to because we have more tabHead then tabBodys
          if ($(this).closest('.' + _this.opts.dropdownTabClass).length) {
            j = i-1;
          }

          $(this).focus().keyup(function (e) {
            if (_this.keyCodes[e.keyCode]) {
              if (_this.activateTab(_this.currentTabsCount, (j + _this.keyCodes[e.keyCode]))) {
                $(this).unbind('keyup');
              }
            }
          });

          _this.$el.find('.' + _this.opts.tabBodyClass).eq(j)[_this.opts.fx](_this.opts.fxSpeed);
        }
      });

      $(this).focus(function () {
        $(document).keyup(function (e) {
          if (_this.keyCodes[e.keyCode]) {
            _this.activateTab(_this.currentTabsCount, (i + _this.keyCodes[e.keyCode]));
          }
        });
      });

      $(this).blur(function () {
        $(document).unbind('keyup');
      });
    });

    // If we have an anchor in our url, trigger click event on the right tab
    if (_this.opts.autoAnchor && window.location.hash) {
      var $anchorTab = $('.' + _this.opts.tabsListClass).find(window.location.hash);
      if ($anchorTab.size()) {
        $anchorTab.click();
      }
    }

    if (_this.hasPreActiveTab) {
      $('.' + _this.opts.tabsListClass + ' .' + _this.opts.activeClass + ' a').click();
    }

    tabsCount++;
  }

  NeoTabs.prototype.activateTab = function (tabsCount, tabCount) {
    $tab = $('#'+this.generateTabId(tabsCount, tabCount));
    if ($tab.length) {
      $tab.click();
      return true;
    }
    return false;
  };

  NeoTabs.prototype.generateTabId = function (tabsCount, tabCount) {
    return 'accessibletabscontent' + tabsCount + '-' + tabCount;
  };

  function TabsList(options) {
    this.tabs = [];
    this.clearfixClass = options.clearfixClass;
    this.tabsListClass = options.tabsListClass;
  }

  TabsList.prototype.addTab = function (tab) {
    this.tabs.push(tab);
  };

  TabsList.prototype.toHtml = function () {
    var len = this.tabs.length,
        i = 0,
        html = '
';

    for (; i < len; i++) {
      html += this.tabs[i].toHtml();
    }
    return html;
  };

  function Tab(options) {
    this.label = options.label;
    this.id = options.id;
    this.tabsList = options.tabsList;
    this.cssClass = options.cssClass;
  }

  Tab.prototype.toHtml = function () {
    var html = '
' + this.label + '';

    if (this.tabsList) {
      html += this.tabsList.toHtml();
    }
    html += '
';
    return html;
  };

  $.fn[pluginName] = function (options) {
    return this.each(function () {
      if (!$.data(this, 'plugin_' + pluginName)) {
        $.data(this, 'plugin_' + pluginName, new NeoTabs($(this), options ));
      }
    });
  };
}(jQuery, window));
<commit_msg>Fixed lint bug<commit_after>/**
 * NeoTabs - jQuery plugin
 *
 * source: http://github.com/PascalPrecht/jquery.neotabs.js/
 * site: http://pascalprecht.github.com/jquery.neotabs.js/
 *
 * @author: Pascal Precht <pascal.precht@gmail.com>
 * Released under the MIT and GPL Licenses.
 */
;(function ($, window, undefined) {

  var pluginName = 'neoTabs',
      document = window.document,

      defaults = {
        wrapperClass: 'content',
        activeClass: 'active',
        tabHeadClass: 'tabhead',
        tabBodyClass: 'tabbody',
        firstTabClass: 'first',
        lastTabClass: 'last',
        clearfixClass: 'group',
        tabsListClass: 'tabs-list',
        tabHeadElement: 'h4',
        tabsPosition: 'top',
        cssClassAvailable: true,
        fx: 'show',
        fxSpeed: 0,
        autoAnchor: true,
        wrapInnerTabs: '',
        dropdownTabLabel: '▾',
        dropdownTabClass: 'dropdown',
        dropdownTabsListClass: 'tabs-list',
        dropdownTabsClearfixClass: 'group'
      },
      tabsCount = 0,
      keyCodes = {
        37: -1,
        38: -1,
        39: +1,
        40: +1
      },
      positions = {
        top: 'prepend',
        bottom: 'append'
      };

  // Helper function to check exclusively for an HTML5 data-attribute. Thanks to
  // @cburgdorf for typing down this gist for me: (https://gist.github.com/3979912)
  $.fn.hasDataAttr = function (attr) {
    var value = this.attr('data-' + attr);
    return typeof value !== 'undefined' && value !== false;
  };

  if ($('body').data('accessibleTabsCount') !== undefined) {
    tabsCount = $('body').data('accessibleTabsCount');
  }

  $('body').data('accessibleTabsCount', tabsCount);

  function NeoTabs(element, options) {
    var _this = this;

    $.extend(_this, {
      $el: element,
      opts: $.extend({}, defaults, options),
      keyCodes: keyCodes,
      hasDropdown: false,
      hasPreActiveTab: false,
      tabsList: null,
      dropdownTabsList: null,
      currentTabsCount: tabsCount
    });

    _this.tabsList = new TabsList({
      clearfixClass: _this.opts.clearfixClass,
      tabsListClass: _this.opts.tabsListClass
    });

    _this.$el.wrapInner('

');

    _this.$el.find(_this.opts.tabHeadElement).each(function (i) {

      var $tabHeadElement = $(this);

      if (!_this.hasDropdown && $tabHeadElement.hasDataAttr('neotabs-dropdown')) {
        _this.dropdownTabsList = new TabsList({
          clearfixClass: _this.opts.dropdownTabsClearfixClass,
          tabsListClass: _this.opts.dropdownTabsListClass
        });
        _this.hasDropdown = true;
      }

      if (!_this.hasPreActiveTab && $tabHeadElement.hasDataAttr('neotabs-active')) {
        $tabHeadElement.addClass(_this.opts.activeClass);
        _this.hasPreActiveTab = true;
      }

      var tab = new Tab({
        label: $tabHeadElement.html(),
        id: _this.generateTabId(tabsCount, i),
        tabsList: null,
        cssClass: (_this.opts.cssClassAvailable) ?
          (($tabHeadElement.attr('class') || '') + ' ' + _this.opts.tabHeadClass) :
          _this.opts.tabHeadClass
      });

      if (_this.hasDropdown) {
        _this.dropdownTabsList.addTab(tab);
      } else {
        _this.tabsList.addTab(tab);
      }

      $tabHeadElement.attr({
        'id': tab.id,
        'class': _this.opts.tabHeadClass,
        'tab-index': '-1'
      });
    });

    // Generate dropdown tab if hasDropdown flag is true
    if (_this.hasDropdown) {
      _this.tabsList.addTab(new Tab({
        label: _this.opts.dropdownTabLabel,
        id: '',
        tabsList: _this.dropdownTabsList,
        cssClass: _this.opts.tabHeadClass + ' ' + _this.opts.dropdownTabClass
      }));
    }

    if (!_this.$el.find('.' + _this.opts.tabsListClass).length) {
      _this.$el[positions[_this.opts.tabsPosition]](_this.tabsList.toHtml());
    }

    var $content = _this.$el.find('.' + _this.opts.tabBodyClass),
        $tabsList = _this.$el.find('.' + _this.opts.tabsListClass);

    // Show the first tab content by default
    if ($content.length > 0) {
      $content.hide();
      $($content[0]).show();
    }

    // Which tab should be active?
    $tabsList.find(' > li:first')
      .addClass(_this.opts.firstTabClass + ((!_this.hasPreActiveTab) ? ' ' + _this.opts.activeClass : ''))
      .closest('ul').find('> li:last')
      .addClass(_this.opts.lastTabClass);

    if (_this.opts.wrapInnerTabs) {
      $tabsList.find('> li > a').wrapInner(_this.opts.wrapInnerTabs);
    }

    $tabsList.find('> li a').each(function (i) {

      $(this).on('click', function (e) {
        e.preventDefault();

        $tabsList
          .find('> li.' + _this.opts.activeClass)
          .removeClass(_this.opts.activeClass);

        $(this).parent().addClass(_this.opts.activeClass);
        $(this).blur();

        var j = i;

        if (!$(this).parent().hasClass(_this.opts.dropdownTabClass)) {
          _this.$el.find('.' + _this.opts.tabBodyClass + ':visible').hide();

          // little hack to because we have more tabHead then tabBodys
          if ($(this).closest('.' + _this.opts.dropdownTabClass).length) {
            j = i-1;
          }

          $(this).focus().keyup(function (e) {
            if (_this.keyCodes[e.keyCode]) {
              if (_this.activateTab(_this.currentTabsCount, (j + _this.keyCodes[e.keyCode]))) {
                $(this).unbind('keyup');
              }
            }
          });

          _this.$el.find('.' + _this.opts.tabBodyClass).eq(j)[_this.opts.fx](_this.opts.fxSpeed);
        }
      });

      $(this).focus(function () {
        $(document).keyup(function (e) {
          if (_this.keyCodes[e.keyCode]) {
            _this.activateTab(_this.currentTabsCount, (i + _this.keyCodes[e.keyCode]));
          }
        });
      });

      $(this).blur(function () {
        $(document).unbind('keyup');
      });
    });

    // If we have an anchor in our url, trigger click event on the right tab
    if (_this.opts.autoAnchor && window.location.hash) {
      var $anchorTab = $('.' + _this.opts.tabsListClass).find(window.location.hash);
      if ($anchorTab.size()) {
        $anchorTab.click();
      }
    }

    if (_this.hasPreActiveTab) {
      $('.' + _this.opts.tabsListClass + ' .' + _this.opts.activeClass + ' a').click();
    }

    tabsCount++;
  }

  NeoTabs.prototype.activateTab = function (tabsCount, tabCount) {
    var $tab = $('#'+this.generateTabId(tabsCount, tabCount));
    if ($tab.length) {
      $tab.click();
      return true;
    }
    return false;
  };

  NeoTabs.prototype.generateTabId = function (tabsCount, tabCount) {
    return 'accessibletabscontent' + tabsCount + '-' + tabCount;
  };

  function TabsList(options) {
    this.tabs = [];
    this.clearfixClass = options.clearfixClass;
    this.tabsListClass = options.tabsListClass;
  }

  TabsList.prototype.addTab = function (tab) {
    this.tabs.push(tab);
  };

  TabsList.prototype.toHtml = function () {
    var len = this.tabs.length,
        i = 0,
        html = '
';

    for (; i < len; i++) {
      html += this.tabs[i].toHtml();
    }
    return html;
  };

  function Tab(options) {
    this.label = options.label;
    this.id = options.id;
    this.tabsList = options.tabsList;
    this.cssClass = options.cssClass;
  }

  Tab.prototype.toHtml = function () {
    var html = '
' + this.label + '';

    if (this.tabsList) {
      html += this.tabsList.toHtml();
    }
    html += '
';
    return html;
  };

  $.fn[pluginName] = function (options) {
    return this.each(function () {
      if (!$.data(this, 'plugin_' + pluginName)) {
        $.data(this, 'plugin_' + pluginName, new NeoTabs($(this), options ));
      }
    });
  };
}(jQuery, window));
<|endoftext|>


<commit_before>Ext.define('App.view.Window',{
    extend: 'Ext.window.Window',
    wm: null,//task bar / window manager button
    wmImg: 'css/ok.png',
    wmTooltip: 'wm tooltip',
    constrainHeader: true,// doesn't work properly
    autoShow: true,
    maximizable: true,
    tools:[{
        type:'refresh',
        tooltip: 'view developent: load new version',
        callback: function reload_devel_view(panel, tool, event){
            var to = { renderTo: Ext.getCmp('desk').getEl() }
               ,wmId = panel.wmId

            panel.destroy()
            Ext.Loader.loadScript({
                url: (App.cfg.backend.url || '') + '/view/' + wmId + '.js'
               ,onLoad: function view_loaded(){
                    Ext.Loader.loadScript({
                        url: (App.cfg.backend.url || '') + '/controller/' + wmId + '.js'
                       ,onLoad: function ctl_loaded(){
                            App.create('controller.' + wmId, null, to)
                        }
                       ,onError: function ctl_not_loaded(){
                            App.create('view.' + wmId, null, to)
                        }
                    })
                }
            })
        }
    },
    {
        type:'help',
        tooltip: 'Get Help',
        callback: function(panel, tool, event){
        }
    }],
    initComponent:
    function dynamic_init_window(){
    var me = this

        me.callParent()

        me.wm = Ext.getCmp('wm').add({
            text: ''
           ,itemId: me.wmId || ''
           ,enableToggle: true
           ,pressed: true
           ,tooltip: me.wmTooltip
           ,toggleHandler: me.onWMToggle
           ,scope: me
        })
        me.on({
            destroy: function onCloseWindow(){
                Ext.getCmp('wm').remove(me.wm, /*autoDestroy:*/ true)
                me.wm = null
            },
            activate: function onActivateWindow(){
                me.wm.toggle(true, true)
            },
            deactivate: function onDeactivateWindow(){
                me.wm.toggle(false, true)
            }
        })
    }
   ,onWMToggle:
    function onWMToggle(_, next_state){
    var me = this// window
        if(next_state){
            Ext.WindowManager.front.setActive(false)
            me.isHidden() && me.show()
            me.setActive(true)
        } else {
            me.hide()
            me.wm.toggle(false, true)
            Ext.WindowManager.front.setActive(true)
        }
    }
})
<commit_msg>view.Window: add `removeScriptElement()` after script load<commit_after>Ext.define('App.view.Window',{
    extend: 'Ext.window.Window',
    wm: null,//task bar / window manager button
    wmImg: 'css/ok.png',
    wmTooltip: 'wm tooltip',
    constrainHeader: true,// doesn't work properly
    autoShow: true,
    maximizable: true,
    tools:[{
        type:'refresh',
        tooltip: 'view developent: load new version',
        callback: function reload_devel_view(panel, tool, event){
            var to = { renderTo: Ext.getCmp('desk').getEl() }
               ,wmId = panel.wmId
               ,url

            panel.destroy()
            Ext.Loader.loadScript({
                url: url = (App.cfg.backend.url || '') + '/view/' + wmId + '.js'
               ,onLoad: function view_loaded(){
                    Ext.Loader.removeScriptElement(url)
                    Ext.Loader.loadScript({
                        url: url = (App.cfg.backend.url || '') + '/controller/' + wmId + '.js'
                       ,onLoad: function ctl_loaded(){
                            Ext.Loader.removeScriptElement(url)
                            App.create('controller.' + wmId, null, to)
                        }
                       ,onError: function ctl_not_loaded(){
                            Ext.Loader.removeScriptElement(url)
                            App.create('view.' + wmId, null, to)
                        }
                    })
                }
            })
        }
    },
    {
        type:'help',
        tooltip: 'Get Help',
        callback: function(panel, tool, event){
        }
    }],
    initComponent:
    function dynamic_init_window(){
    var me = this

        me.callParent()

        me.wm = Ext.getCmp('wm').add({
            text: ''
           ,itemId: me.wmId || ''
           ,enableToggle: true
           ,pressed: true
           ,tooltip: me.wmTooltip
           ,toggleHandler: me.onWMToggle
           ,scope: me
        })
        me.on({
            destroy: function onCloseWindow(){
                Ext.getCmp('wm').remove(me.wm, /*autoDestroy:*/ true)
                me.wm = null
            },
            activate: function onActivateWindow(){
                me.wm.toggle(true, true)
            },
            deactivate: function onDeactivateWindow(){
                me.wm.toggle(false, true)
            }
        })
    }
   ,onWMToggle:
    function onWMToggle(_, next_state){
    var me = this// window
        if(next_state){
            Ext.WindowManager.front.setActive(false)
            me.isHidden() && me.show()
            me.setActive(true)
        } else {
            me.hide()
            me.wm.toggle(false, true)
            Ext.WindowManager.front.setActive(true)
        }
    }
})
<|endoftext|>