Source: src/includes/menu.inc.js

/**
 * Execute the page callback associated with the current path and return its
 * content.
 * @return {Object}
 */
function menu_execute_active_handler() {
  try {

    // Determine the path and then grab the page id.
    var path = null;
    if (arguments[0]) { path = arguments[0]; }
    if (!path) { path = drupalgap_path_get(); }
    var page_id = drupalgap_get_page_id(path);

    // @todo - Make sure the user has access to this DrupalGap menu path!

    // Get the router path.
    var router_path = drupalgap_router_path_get();

    if (!router_path) { return; }

    // Call the page call back for this router path and send along any
    // arguments.
    var function_name = drupalgap.menu_links[router_path].page_callback;
    var page_arguments = [];
    if (function_exists(function_name)) {

      // Grab the page callback function and get ready to build the html.
      var fn = window[function_name];
      var content = '';

      // Are there any arguments to send to the page callback?
      if (drupalgap.menu_links[router_path].page_arguments) {
        // For each page argument, if the argument is an integer, grab the
        // corresponding arg(#), otherwise just push the arg onto the page
        // arguments. Then try to prepare any entity that may be present in
        // the url so the entity is sent via the page arguments to the page
        // callback, instead of just sending the integer.
        var args = arg(null, path);
        for (var index in drupalgap.menu_links[router_path].page_arguments) {
            if (!drupalgap.menu_links[router_path].page_arguments.hasOwnProperty(index)) { continue; }
            var object = drupalgap.menu_links[router_path].page_arguments[index];
            if (is_int(object) && args[object]) {
              page_arguments.push(args[object]);
            }
            else { page_arguments.push(object); }
        }

        // Call the page callback function with the page arguments.
        content = fn.apply(null, Array.prototype.slice.call(page_arguments));
      }
      else {
        // There are no arguments, just return the page callback result.
        content = fn();
      }

      // If the content came back as a string, convert it to a render object
      // so any jQM page events can be attached to the content if necessary.
      if (typeof content === 'string') {
        content = {
          content: {
            markup: content
          }
        };
      }

      // Clear out any previous jQM page events, then if there are any jQM
      // event callback functions attached to the menu link for this page, set
      // each event up to be fired with inline JS on the page. We pass along
      // any page arguments to the jQM event handler function.
      drupalgap.page.jqm_events = [];
      var jqm_page_events = drupalgap_jqm_page_events();
      var jqm_page_event_args = null;
      if (page_arguments.length > 0) {
        jqm_page_event_args = JSON.stringify(page_arguments);
      }
      for (var i = 0; i < jqm_page_events.length; i++) {
        if (drupalgap.menu_links[router_path][jqm_page_events[i]]) {
          var jqm_page_event = jqm_page_events[i];
          var jqm_page_event_callback =
            drupalgap.menu_links[router_path][jqm_page_event];
          if (function_exists(jqm_page_event_callback)) {
            var options = {
              'page_id': page_id,
              'jqm_page_event': jqm_page_event,
              'jqm_page_event_callback': jqm_page_event_callback,
              'jqm_page_event_args': jqm_page_event_args
            };
            content[jqm_page_event] = {
              markup: drupalgap_jqm_page_event_script_code(options)
            };
          }
          else {
            console.log(
              'menu_execute_active_handler (' + path + ') - the jQM ' +
              jqm_page_event + ' call back function ' +
              jqm_page_event_callback + ' does not exist!'
            );
          }
        }
      }

      // Add a pageshow handler for the page title.
      if (typeof content === 'object') {
        var options = {
          'page_id': page_id,
          'jqm_page_event': 'pageshow',
          'jqm_page_event_callback': '_drupalgap_page_title_pageshow',
          'jqm_page_event_args': jqm_page_event_args
        };
        content['drupalgap_page_title_pageshow'] = {
          markup: drupalgap_jqm_page_event_script_code(options)
        };
      }

      // And finally return the content.
      return content;
    }
    else {
      // No page call back specified.
      console.log(
        'menu_execute_active_handler - no page callback (' + router_path + ')'
      );
      console.log(JSON.stringify(drupalgap.menu_links[router_path]));
    }
  }
  catch (error) {
    console.log('menu_execute_active_handler(' + path + ') - ' + error);
  }
}

/**
 * Gets a router item.
 * @return {Object}
 */
function menu_get_item() {
  try {
    var path = null;
    var router_item = null;
    if (arguments[0]) { path = arguments[0]; }
    if (arguments[1]) { router_item = arguments[1]; }
    if (path && drupalgap.menu_links[path]) {
      return drupalgap.menu_links[path];
    }
    else { return null; }
  }
  catch (error) { console.log('menu_get_item - ' + error); }
}

/**
 * Returns default menu item options.
 * @return {Object}
 */
function menu_item_default_options() {
    return { attributes: menu_item_default_attributes() };
}

/**
 * Returns default menu item attributes.
 * @return {Object}
 */
function menu_item_default_attributes() {
    return { 'class': '' };
}

/**
 * Returns an array containing the names of system-defined (default) menus.
 * @return {Object}
 */
function menu_list_system_menus() {
  try {
    var system_menus = {
      'user_menu_anonymous': {
        'title': t('User menu authenticated')
      },
      'user_menu_authenticated': {
        'title': t('User menu authenticated')
      },
      'main_menu': {
        'title': t('Main menu')
      },
      'primary_local_tasks': {
        'title': t('Primary Local Tasks')
      }
    };
    // Add the menu_name to each menu as a property.
    for (var menu_name in system_menus) {
        if (!system_menus.hasOwnProperty(menu_name)) { continue; }
        var menu = system_menus[menu_name];
        menu.menu_name = menu_name;
    }
    return system_menus;
  }
  catch (error) { console.log('menu_list_system_menus - ' + error); }
}

/**
 * Collects and alters the menu definitions.
 */
function menu_router_build() {
  //  Calls all hook_menu implementations and builds a collection of menu links.
  try {
    // For each module that implements hook_menu, iterate over each of the
    // menu links defined by the hook, then add each menu item to
    // drupalgap.menu_links keyed by the path.
    var modules = module_implements('menu');
    var function_name;
    var fn;
    var menu_links;
    for (var index in modules) {
        if (!modules.hasOwnProperty(index)) { continue; }
        var module = modules[index];
        // Determine the hook function name, grab the function, and call it
        // to retrieve the hook's menu links.
        function_name = module + '_menu';
        fn = window[function_name];
        menu_links = fn();
        // Iterate over each item.
        for (var path in menu_links) {
            if (!menu_links.hasOwnProperty(path)) { continue; }
            var menu_item = menu_links[path];
            // Attach module name to item.
            menu_item.module = module;
            // Set a default type for the item if one isn't provided.
            if (typeof menu_item.type === 'undefined') {
              menu_item.type = 'MENU_NORMAL_ITEM';
            }
            // Set default options and attributes if none have been provided.
            // @TODO - Now that we are doing this here, there may be a few
            // places throughout the code that were checking for these
            // undefined objects that will no longer need to be checked.
            if (typeof menu_item.options === 'undefined') {
              menu_item.options = menu_item_default_options();
            }
            else if (typeof menu_item.options.attributes === 'undefined') {
              menu_item.options.attributes = menu_item_default_attributes();
            }
            // Make the path available as a property in the menu link.
            menu_item.path = path;
            // Determine any parent, sibling, and child paths for the item.
            drupalgap_menu_router_build_menu_item_relationships(
              path,
              menu_item
            );
            // Attach item to menu links.
            drupalgap.menu_links[path] = menu_item;
        }
    }
  }
  catch (error) { console.log('menu_router_build - ' + error); }
}

/**
 * Given a menu link path, this determines and returns the router path as a
 * string.
 * @param {String} path
 * @return {String}
 */
function drupalgap_get_menu_link_router_path(path) {
  try {

    // @TODO - Why is this function called twice sometimes? E.G. via an MVC item
    // view item/local_users/user/0, this function gets called twice in one page
    // load, that can't be good.

    // @TODO - this function has a limitation in the types of menu paths it can
    // handle, for example a menu path of 'collection/%/%/list' with a path of
    // 'collection/local_users/user/list' can't find eachother. So we had to
    // change the mvc_menu() item path to be collection/list/%/%.

    // @TODO - each time this function is called, we should create a static
    // record of the result router path, keyed by the incoming path, that way
    // this heavy function can be called more often with less resource.

    // Is this path defined in drupalgap.menu_links? If it is, use it's router
    // path if it is defined, otherwise just set its router path to its own
    // path.
    if (drupalgap.menu_links[path]) {
      if (typeof drupalgap.menu_links[path].router_path === 'undefined') {
        return path;
      }
      else {
        return drupalgap.menu_links[path].router_path;
      }
    }

    // Let's figure out where to route this menu item, and attach the
    // router to the item.
    var router_path = null;
    var args = arg(null, path);

    // If there is an integer in the path, replace it with the wildcard
    // defined via hook_menu implementation.
    if (args) {
      var args_size = args.length;
      switch (args[0]) {
        case 'comment':
        case 'user':
        case 'node':
          if (args_size > 1 && is_int(parseInt(args[1]))) {
            args[1] = '%';
            router_path = args.join('/');
          }
          break;
        case 'taxonomy':
          if (args_size > 2 && (args[1] == 'vocabulary' || args[1] == 'term') &&
              is_int(parseInt(args[2]))
          ) {
            args[2] = '%';
            router_path = args.join('/');
          }
          break;
        default:
          if (args_size > 1 && is_int(parseInt(args[1]))) {
            args[1] = '%';
            router_path = args.join('/');
          }
          break;
      }
    }

    // If we haven't found a router path yet, try some other techniques to find
    // it. If all else fails, just set the router path to the path itself.
    if (!router_path) {
      // Are there any paths in drupalgap.menu_links that would be good
      // candidates as the router for this path? Let's start at the back of the
      // argument list and start replacing each with a wildcard (%) and then
      // see if there is a path in drupalgap.menu_links that can handle it.
      if (args && args.length > 1) {
        var temp_router_path;
        for (var i = args.length - 1; i != -1; i--) {
          temp_router_path = '';
          for (var j = 0; j < args.length; j++) {
            if (j < i) {
              temp_router_path += args[j];
            }
            else {
              temp_router_path += '%';
            }
            if (j != args.length - 1) {
              temp_router_path += '/';
            }
          }
          // If we found a router path, let's use it.
          if (drupalgap.menu_links[temp_router_path]) {
            router_path = temp_router_path;
            break;
          }
        }
      }
    }

    // If the router path is a default menu local task, inherit its parent
    // router path.
    if (drupalgap.menu_links[router_path] &&
        drupalgap.menu_links[router_path].type == 'MENU_DEFAULT_LOCAL_TASK' &&
        drupalgap.menu_links[router_path].parent) {
      router_path = drupalgap.menu_links[router_path].parent;
    }

    // If there isn't a router and we couldn't find one, we'll just route
    // to the path itself.
    if (!router_path) { router_path = path; }

    // Finally, return the router path.
    return router_path;
  }
  catch (error) {
    console.log('drupalgap_get_menu_link_router_path - ' + error);
  }
}


/**
 * Loads all of the menus specified in drupalgap.settings.menus into
 * drupalgap.menus. This is called after menu_router_build(), so any system
 * defined menus will already be present and should be overwritten with any
 * customizations present in the settings. It then iterates over the menu links
 * specified in drupalgap.menu_links and attaches any of them that have a
 * menu_name to their corresponding menu in drupalgap.menus. Any menu link items
 * that have a 'region' property specified will be added to
 * drupalgap.theme.regions[region].
 */
function drupalgap_menus_load() {
  try {
    if (drupalgap.settings.menus) {
      // Process each menu defined in the settings.
      for (var menu_name in drupalgap.settings.menus) {
          if (!drupalgap.settings.menus.hasOwnProperty(menu_name)) { continue; }
          var menu = drupalgap.settings.menus[menu_name];
          // If the menu does not already exist, it is a custom menu, so create
          // the menu and its corresponding block.
          if (!drupalgap.menus[menu_name]) {
            // If the custom menu doesn't have its machine name set, set it.
            if (!menu.menu_name) { menu.menu_name = menu_name; }
            // Save the custom menu, as long is its name isn't 'regions',
            // because that is a special "system" menu that allows menu links to
            // be placed directly in regions via settings.js. Keep in mind the
            // 'regions' menu is in fact NOT a system menu.
            if (menu_name != 'regions') {
              menu_save(menu);
              // Make a block for this custom menu.
              var block_delta = menu.menu_name;
              drupalgap.blocks[0][block_delta] = {
                name: block_delta,
                delta: block_delta,
                module: 'menu'
              };
            }
          }
          else {
            // The menu is a system defined menu, merge it together with any
            // custom settings.
            $.extend(true, drupalgap.menus[menu_name], menu);
          }
      }
      // Now that we have all of the menus loaded up, and the menu router is
      // built, let's iterate over all the menu links and perform various
      // operations on them.
      for (var path in drupalgap.menu_links) {
          if (!drupalgap.menu_links.hasOwnProperty(path)) { continue; }
          var menu_link = drupalgap.menu_links[path];
          // Let's grab any links from the router that have a menu specified,
          // and add the link to the router.
          if (menu_link.menu_name) {
            if (drupalgap.menus[menu_link.menu_name]) {
              // Create a links array for the menu if one doesn't exist already.
              if (!drupalgap.menus[menu_link.menu_name].links) {
                drupalgap.menus[menu_link.menu_name].links = [];
              }
              // Add the path to the menu link inside the menu.
              menu_link.path = path;
              // Now push the link onto the menu. We only care about the title,
              // path and options, as this is just a link. The rest of the
              // menu link data can be retrieved from drupalgap.menu_links.
              var link =
                drupalgap_menus_load_convert_menu_link_to_link_json(menu_link);
              drupalgap.menus[menu_link.menu_name].links.push(link);
            }
            else {
              console.log(
                'drupalgap_menus_load - menu does not exist (' +
                menu_link.menu_name + '), cannot attach link to it (' +
                path + ')'
              );
            }
          }
          // If the menu link is set to a specific region, create a links array
          // for the region if one doesn't exist already, then add the menu item
          // to the links array as a link.
          if (menu_link.region) {
            if (!drupalgap.theme.regions[menu_link.region.name].links) {
              drupalgap.theme.regions[menu_link.region.name].links = [];
            }
            drupalgap.theme.regions[menu_link.region.name].links.push(
              menu_link
            );
          }
      }
      // If there are any region menu links defined in settings.js, create a
      // links array for the region if one doesn't exist already, then add the
      // menu item to the links array as a link.
      if (typeof drupalgap.settings.menus.regions !== 'undefined') {
        for (var region in drupalgap.settings.menus.regions) {
            if (!drupalgap.settings.menus.regions.hasOwnProperty(region)) { continue; }
            var menu = drupalgap.settings.menus.regions[region];
            if (
              typeof menu.links !== 'undefined' &&
              $.isArray(menu.links) &&
              menu.links.length > 0
            ) {
              if (!drupalgap.theme.regions[region].links) {
                drupalgap.theme.regions[region].links = [];
              }
              for (var index in menu.links) {
                  if (!menu.links.hasOwnProperty(index)) { continue; }
                  var link = menu.links[index];
                  drupalgap.theme.regions[region].links.push(link);
              }
            }
        }
      }
    }
  }
  catch (error) { console.log('drupalgap_menus_load - ' + error); }
}

/**
 * Given a menu link item from drupalgap_menus_load(), this will return a JSON
 * object representing a link object compatible with theme_link(). It contains
 * the link title, path and options.
 * @param {Object} menu_link
 * @return {Object}
 */
function drupalgap_menus_load_convert_menu_link_to_link_json(menu_link) {
  try {
    var link = {};
    if (menu_link.title) {
      // TODO - this is strange, we have to fill the 'text' value so theme_link
      // will play nice. These two properties, and their usage, need a thorough
      // review, only one should probably be used.
      // UPDATE - the link.text property is probably no longer used, it should
      // be safe to get rid of. In fact, this whole function is dumb and should
      // go away.
      link.title = menu_link.title;
      link.text = menu_link.title;
    }
    if (menu_link.path) { link.path = menu_link.path; }
    if (menu_link.options) { link.options = menu_link.options; }
    // If it is a menu link on a region, and it has options, set the link
    // options to the ones provided in the menu link region settings.
    if (menu_link.region && menu_link.region.options) {
      link.options = menu_link.options = menu_link.region.options;
    }
    return link;
  }
  catch (error) {
    console.log(
      'drupalgap_menus_load_convert_menu_link_to_link_json - ' + error
    );
  }
}


/**
 * Given a path, and its corresponding menu item, this will determine any
 * parent, sibling, and/or child menu item paths and set the references on each
 * so they are all aware of eachother's paths.
 * @param {String} path
 * @param {Object} menu_item
 */
function drupalgap_menu_router_build_menu_item_relationships(path, menu_item) {
  try {
    // Split up the path arguments.
    var args = arg(null, path);
    // Any parent?
    if (args.length > 1) {
      // Set the parent path.
      var parent = args.splice(0, args.length - 1).join('/');
      menu_item.parent = parent;
      // Make sure the parent exists.
      if (drupalgap.menu_links[parent]) {
        // Now tell the parent about this child. If the parent doesn't yet have
        // any children, setup the children array on the parent.
        if (typeof drupalgap.menu_links[parent].children === 'undefined') {
          drupalgap.menu_links[parent].children = [];
        }
        drupalgap.menu_links[parent].children.push(path);
        // Now tell any siblings about this item, and tell this item about any
        // siblings.
        if (typeof menu_item.siblings === 'undefined') {
          menu_item.siblings = [];
        }
        for (var index in drupalgap.menu_links[parent].children) {
            if (!drupalgap.menu_links[parent].children.hasOwnProperty(index)) { continue; }
            var sibling = drupalgap.menu_links[parent].children[index];
            if (sibling != path && drupalgap.menu_links[sibling]) {
              if (
                typeof drupalgap.menu_links[sibling].siblings === 'undefined'
              ) {
                drupalgap.menu_links[sibling].siblings = [];
              }
              drupalgap.menu_links[sibling].siblings.push(path);
              menu_item.siblings.push(sibling);
            }
        }
      }
    }
  }
  catch (error) {
    console.log('drupalgap_menu_router_build_relationships - ' + error);
  }
}