You are here

recipe.module in Recipe 5

Same filename and directory in other branches
  1. 8.2 recipe.module
  2. 6 recipe.module
  3. 7.2 recipe.module
  4. 7 recipe.module

recipe.module - share recipes for drupal 5.x

@todo

File

recipe.module
View source
<?php

/**
 * @file
 * recipe.module - share recipes
 * for drupal 5.x
 *
 * @todo
 */

/**
 * Implementation of hook_perm().
 */
function recipe_perm() {
  return array(
    t('create recipes'),
    t('edit own recipes'),
  );
}

/**
 * Implementation of hook_load().
 */
function recipe_load($node) {
  $recipe = db_fetch_object(db_query('SELECT * FROM {recipe} WHERE nid = %d', $node->nid));
  $recipe->ingredients = recipe_load_ingredients($node);
  return $recipe;
}

/**
 * Implementation of hook_link().
 */
function recipe_link($type, $node = NULL, $teaser = FALSE) {
  $links = array();
  if ($type == 'node' && $node->type == 'recipe') {
    if (!$teaser) {
      if (variable_get('recipe_export_recipeml_enable', 1) == 1) {
        $links['recipe_recipeml'] = array(
          'title' => t('Export to RecipeML'),
          'href' => "recipe/export/recipeml/{$node->nid}",
          'attributes' => array(
            'title' => t('Export this recipe to RecipeML.'),
          ),
        );
      }
      if (variable_get('recipe_export_html_enable', 1) == 1) {
        $links['recipe_html'] = array(
          'title' => t('Printer-friendly version'),
          'href' => "recipe/export/html/{$node->nid}",
          'attributes' => array(
            'title' => t('Show a printer-friendly version of this recipe.'),
          ),
        );
      }
    }
  }
  return $links;
}

/**
 * Implementation of hook_node_info(). This function replaces hook_node_name()
 * and hook_node_types() from 4.6.
 */
function recipe_node_info() {
  return array(
    'recipe' => array(
      'name' => t('Recipe'),
      'module' => 'recipe',
      'description' => t("Share your favorite recipes with your fellow cooks."),
    ),
  );
}

/**
 * Implementation of hook_help().
 */
function recipe_help($section) {
  switch ($section) {
    case 'node/add/recipe':
      return variable_get("recipe_help", "");
  }
}

/**
 * Implementation of hook_insert().
 *
 * As a new node is being inserted into the database, we need to do our own
 * database inserts.
 */
function recipe_insert($node) {
  db_query("INSERT INTO {recipe} (nid, source, yield, notes, instructions, preptime) VALUES (%d, '%s', '%s', '%s', '%s', '%d')", $node->nid, $node->source, $node->yield, $node->notes, $node->instructions, $node->preptime);
  recipe_save_ingredients($node);
}

/**
 * Implementation of hook_update().
 *
 * As an existing node is being updated in the database, we need to do our own
 * database updates.
 */
function recipe_update($node) {
  db_query("UPDATE {recipe} SET source = '%s', yield = '%s', notes = '%s', instructions = '%s', preptime = '%d' WHERE nid = %d", $node->source, $node->yield, $node->notes, $node->instructions, $node->preptime, $node->nid);
  recipe_save_ingredients($node);
}

/**
 * Implementation of hook_delete().
 *
 * When a node is deleted, we need to clean up related tables.
 */
function recipe_delete($node) {
  db_query("DELETE FROM {recipe} WHERE nid = %d", $node->nid);
  db_query("DELETE FROM {recipe_node_ingredient} WHERE nid = %d", $node->nid);
}

/**
 * Implementation of hook_form().
 */
function recipe_form(&$node) {

  // drupal 4.7 requires the title field to be defined by the custom node's module
  $form['title'] = array(
    '#type' => 'textfield',
    '#title' => t('Title'),
    '#size' => 60,
    '#maxlength' => 128,
    '#required' => TRUE,
    '#default_value' => $node->title,
  );

  // Now we define the form elements specific to our node type.
  $form['body'] = array(
    '#type' => 'textarea',
    '#title' => t('Description'),
    '#default_value' => $node->body,
    '#cols' => 60,
    '#rows' => 2,
    '#description' => t('A short description or "teaser" for the recipe.'),
    '#required' => TRUE,
  );
  $form['yield'] = array(
    '#type' => 'textfield',
    '#title' => t('Yield'),
    '#default_value' => $node->yield,
    '#size' => 10,
    '#maxlength' => 10,
    '#description' => t('The number of servings the recipe will make.'),
    '#attributes' => NULL,
    '#required' => TRUE,
  );
  $form['preptime'] = array(
    '#type' => 'select',
    '#title' => t("Preparation time"),
    '#default_value' => $node->preptime,
    '#options' => array(
      5 => t('5 minutes'),
      10 => t('10 minutes'),
      15 => t('15 minutes'),
      20 => t('20 minutes'),
      30 => t('30 minutes'),
      45 => t('45 minutes'),
      60 => t('1 hour'),
      90 => t('1 1/2 hours'),
      120 => t('2 hours'),
      150 => t('2 1/2 hours'),
      180 => t('3 hours'),
      210 => t('3 1/2 hours'),
      240 => t('4 hours'),
      300 => t('5 hours'),
      360 => t('6 hours'),
    ),
    '#description' => t("How long does this recipe take to prepare (i.e. elapsed time)"),
  );
  $form["source"] = array(
    '#type' => 'textfield',
    '#title' => t("Source"),
    '#default_value' => $node->source,
    '#size' => 60,
    '#maxlength' => 127,
    '#description' => t("Optional. Does anyone else deserve credit for this recipe?"),
  );

  // Table of existing ingredients
  $form['ingredients']['#tree'] = TRUE;
  $system = variable_get('recipe_ingredient_system', 'complex');
  if ($system == 'complex') {
    $form['ingredients']['headings'] = array(
      '#value' => '<div><table ><thead><tr><th>' . t('Quantity') . '</th><th>' . t('Units') . '</th><th>' . t('Ingredient Name') . '</th></tr></thead>' . "\n" . '<tbody>',
    );
  }
  else {
    $form['ingredients']['headings'] = array(
      '#value' => '<div><table ><thead><tr><th>' . t('Ingredients') . '</th></tr></thead>' . "\n" . '<tbody>',
    );
  }
  $rows = array();
  $callback = 'recipe/ingredient/autocomplete';
  $num_ingredients = 0;
  if ($node->ingredients) {
    foreach ($node->ingredients as $id => $ingredient) {
      $num_ingredients = $id + 1;
      if ($id == 0) {
        $j = '0';
      }
      else {
        $j = $id;
      }
      if ($ingredient->name && isset($ingredient->quantity)) {

        // When can the following statement be true?
        if (!$ingredient) {
          drupal_set_message(t('Recipe Module: An error has occured.  Please report this error to the system administrator.'), 'error');
          $ingredient->quantity = '';
          $ingredient->unit_id = 21;
          $ingredient->name = '';
        }
        if ($system == 'complex') {
          $form['ingredients'][$j]['open_tags'] = array(
            '#value' => '<tr><th>',
          );
          $form['ingredients'][$j]['quantity'] = array(
            '#type' => 'textfield',
            '#title' => '',
            '#default_value' => preg_replace('/\\&frasl;/', '/', recipe_ingredient_quantity_from_decimal($ingredient->quantity)),
            '#size' => 8,
            '#maxlength' => 8,
          );
          $form['ingredients'][$j]['mid1_tags'] = array(
            '#value' => '</th><th>',
          );
          $form['ingredients'][$j]['unit_id'] = array(
            '#type' => 'select',
            '#title' => '',
            '#default_value' => $ingredient->unit_id,
            '#options' => recipe_unit_options(),
          );
          $form['ingredients'][$j]['mid2_tags'] = array(
            '#value' => '</th><th>',
          );
          $form['ingredients'][$j]['name'] = array(
            '#type' => 'textfield',
            '#title' => '',
            '#default_value' => $ingredient->name,
            '#size' => 64,
            '#maxlength' => 128,
            '#autocomplete_path' => $callback,
          );
          $form['ingredients'][$j]['close_tags'] = array(
            '#value' => '</th></tr>',
          );
        }
        else {
          if ($ingredient->name) {
            if ($ingredient->quantity == 0) {
              $ingredient->quantity = '';
            }
            else {
              $ingredient->quantity .= ' ';
            }
            if ($ingredient->abbreviation != '') {
              $ingredient->abbreviation .= ' ';
            }
            $ingredient->name = $ingredient->quantity . $ingredient->abbreviation . $ingredient->name;
          }
          $form['ingredients'][$j]['open_tags'] = array(
            '#value' => '<tr><th>',
            '#tree' => TRUE,
          );
          $form['ingredients'][$j]['name'] = array(
            '#type' => 'textfield',
            '#title' => '',
            '#default_value' => $ingredient->name,
            '#size' => 64,
            '#maxlength' => 128,
            '#autocomplete_path' => $callback,
          );
          $form['ingredients'][$j]['close_tags'] = array(
            '#value' => '</th></tr>',
          );
        }

        // else
      }

      // if ($ingredient->name && isset($ingredient->quantity))
    }

    // foreach ($node->ingredients as $id => $ingredient)
  }

  // if ($node->ingredients)
  // Add ten more spots for ingredients than are already used
  for ($i = $num_ingredients; $i < $num_ingredients + 10; $i++) {
    if ($i == 0) {
      $j = '0';
    }
    else {
      $j = $i;
    }
    if ($system == 'complex') {
      $form['ingredients'][$j]['open_tags'] = array(
        '#value' => '<tr><th>',
      );
      $form['ingredients'][$j]['quantity'] = array(
        '#type' => 'textfield',
        '#title' => '',
        '#size' => 8,
        '#maxlength' => 8,
      );
      $form['ingredients'][$j]['mid1_tags'] = array(
        '#value' => '</th><th>',
      );
      $form['ingredients'][$j]['unit_id'] = array(
        '#type' => 'select',
        '#title' => '',
        '#options' => recipe_unit_options(),
        '#default_value' => 2,
      );
      $form['ingredients'][$j]['mid2_tags'] = array(
        '#value' => '</th><th>',
      );
      $form['ingredients'][$j]['name'] = array(
        '#type' => 'textfield',
        '#title' => '',
        '#size' => 64,
        '#maxlength' => 128,
        '#autocomplete_path' => $callback,
      );
      $form['ingredients'][$j]['close_tags'] = array(
        '#value' => '</th></tr>',
      );
    }
    else {
      $form['ingredients'][$j]['open_tags'] = array(
        '#value' => '<tr><th>',
      );
      $form['ingredients'][$j]['name'] = array(
        '#type' => 'textfield',
        '#title' => '',
        '#size' => 64,
        '#maxlength' => 128,
        '#autocomplete_path' => $callback,
      );
      $form['ingredients'][$j]['close_tags'] = array(
        '#value' => '</th></tr>',
      );
    }
  }
  $form['ingredients']['end'] = array(
    '#value' => '</tbody>' . "\n" . '</table></div>' . "\n",
  );
  $form['instructions'] = array(
    '#type' => 'textarea',
    '#title' => t('Instructions'),
    '#default_value' => $node->instructions,
    '#cols' => 60,
    '#rows' => 10,
    '#description' => t('Step by step instructions on how to prepare and cook the recipe.'),
  );
  $form['notes'] = array(
    '#type' => 'textarea',
    '#title' => t("Additional Notes"),
    '#default_value' => $node->notes,
    '#cols' => 60,
    '#rows' => 5,
    '#description' => t("Optional. Describe a great dining experience relating to this recipe, or note which wine or other dishes complement this recipe"),
  );
  $form['filter'] = filter_form($node->format);
  return $form;
}

/**
 * Settings form for menu callback
 */
function recipe_admin_settings() {
  $form['recipe_index_depth'] = array(
    '#type' => 'select',
    '#title' => t('Index Depth'),
    '#default_value' => variable_get('recipe_index_depth', 0),
    '#options' => array(
      0 => t('All Terms'),
      1 => "1",
      2 => "2",
      3 => "3",
      4 => "4",
      5 => "5",
      6 => "6",
    ),
    '#description' => t("Defines how many levels of terms should be displayed on any given recipe index page. For example, if you select 1 then only one level of the Recipe index tree will be displayed at a time."),
  );
  $form['recipe_recent_box_enable'] = array(
    '#type' => 'radios',
    '#title' => t('Recent Recipes Box'),
    '#default_value' => variable_get('recipe_recent_box_enable', 1),
    '#options' => array(
      t('Disabled'),
      t('Enabled'),
    ),
    '#description' => t('Enables or Disables the recent recipes box on the recipes index page.'),
    '#required' => false,
  );
  $form['recipe_recent_box_title'] = array(
    '#type' => 'textfield',
    '#title' => t('Box Title'),
    '#default_value' => variable_get('recipe_recent_box_title', t('Latest Recipes')),
    '#size' => 35,
    '#maxlength' => 255,
    '#description' => t('Title of the Recent Recipes Box on the Recipes index page.'),
  );
  $form['recipe_recent_display'] = array(
    '#type' => 'select',
    '#title' => t('Recipes to Display'),
    '#default_value' => variable_get('recipe_recent_display', '5'),
    '#options' => array(
      5 => "5",
      10 => "10",
      15 => "15",
    ),
    '#description' => t("Sets the number of recent recipes that will be displayed in the Recent Recipes box. (0 = not displayed)."),
  );
  $form['recipe_help'] = array(
    '#type' => 'textarea',
    '#title' => t('Explanation or submission guidelines'),
    '#default_value' => variable_get('recipe_help', ''),
    '#cols' => 55,
    '#rows' => 4,
    '#description' => t('This text will be displayed at the top of the recipe submission form.  Useful for helping or instructing your users.'),
  );
  $options = array(
    'simple' => t('Simple'),
    'complex' => t('Complex'),
  );
  $form['recipe_ingredient_system'] = array(
    '#type' => 'radios',
    '#title' => t('Ingredient entering system'),
    '#default_value' => variable_get('recipe_ingredient_system', 'complex'),
    '#options' => $options,
    '#description' => t('The simple ingredient system allows all ingredients to be entered on one line.  The complex system forces the user to seperate the quanity and units from the ingredient'),
  );
  $form['recipe_fraction_display'] = array(
    '#type' => 'textfield',
    '#title' => t('Fractions Display String'),
    '#default_value' => variable_get('recipe_fraction_display', t('{%d }%d&frasl;%d')),
    '#size' => 35,
    '#maxlength' => 255,
    '#description' => t('How fractions should be displayed.  Leave blank to display as decimals.  Each incidence of %d will be replaced by the whole number, the numerator, and the denominator in that order.  Anything between curly braces will not be displayed when the whole number is equal to 0.  Recommended settings are "{%d }%d&amp;frasl;%d" or "{%d }&lt;sup&gt;%d&lt;/sup&gt;/&lt;sub&gt;%d&lt;/sub&gt;"'),
  );
  $form['recipe_export_html_enable'] = array(
    '#type' => 'radios',
    '#title' => t('Export HTML'),
    '#default_value' => variable_get('recipe_export_html_enable', 1),
    '#options' => array(
      t('Disabled'),
      t('Enabled'),
    ),
    '#description' => t('Enables or Disables the Export as HTML link.'),
    '#required' => false,
  );
  $form['recipe_export_recipeml_enable'] = array(
    '#type' => 'radios',
    '#title' => t('Export RecipeML'),
    '#default_value' => variable_get('recipe_export_recipeml_enable', 1),
    '#options' => array(
      t('Disabled'),
      t('Enabled'),
    ),
    '#description' => t('Enables or Disables the Export as RecipeML link.'),
    '#required' => false,
  );
  return system_settings_form($form);
}

/**
 * Implementation of hook_menu().
 *
 * Note: when editing this function you must visit 'admin/menu' to reset the cache
 */
function recipe_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'node/add/recipe',
      'title' => t('Recipe'),
      'access' => user_access('create recipes'),
    );
    $items[] = array(
      'path' => 'recipe',
      'title' => t('Recipes'),
      'callback' => 'recipe_page',
      'access' => user_access('access content'),
      'type' => MENU_SUGGESTED_ITEM,
    );
    $items[] = array(
      'path' => 'recipe/ingredient/autocomplete',
      'title' => t('Ingredient autocomplete'),
      'callback' => 'recipe_autocomplete_page',
      'type' => MENU_CALLBACK,
      'access' => user_access('access content'),
    );
    $items[] = array(
      'path' => 'recipe/export',
      'callback' => 'recipe_export',
      'type' => MENU_CALLBACK,
      'access' => user_access('access content'),
    );
    $items[] = array(
      'path' => 'recipe/importolddb',
      'title' => t('import old recipes'),
      'callback' => 'recipe_import_from_46',
      'type' => MENU_SUGGESTED_ITEM,
      'access' => user_access('create recipes'),
    );
    $items[] = array(
      'path' => 'admin/settings/recipe',
      'title' => t('Recipe module'),
      'description' => t('Settings that control how the recipe module functions.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => 'recipe_admin_settings',
      'access' => user_access('administer site configuration'),
      'type' => MENU_NORMAL_ITEM,
    );
  }
  return $items;
}

/**
 * Implementation of hook_access().
 */
function recipe_access($op, $node) {
  global $user;
  if ($op == 'create') {

    // Only users with permission to do so may create this node type.
    return user_access('create recipes');
  }

  // Users who create a node may edit or delete it later, assuming they have the
  // necessary permissions.
  if ($op == 'update' || $op == 'delete') {
    if (user_access('edit own recipes') && $user->uid == $node->uid) {
      return TRUE;
    }
  }
}

/**
 * Implementation of hook_block().
 */
function recipe_block($op = 'list', $delta = 0, $edit = array()) {

  // The $op parameter determines what piece of information is being requested.
  switch ($op) {
    case 'list':

      // If $op is "list", we just need to return a list of block descriptions.
      // This is used to provide a list of possible blocks to the administrator,
      // end users will not see these descriptions.
      $blocks[0]['info'] = t('Newest recipes');
      return $blocks;
    case 'view':

      // If $op is "view", then we need to generate the block for display
      // purposes. The $delta parameter tells us which block is being requested.
      switch ($delta) {
        case 0:

          // The subject is displayed at the top of the block. Note that it
          // should be passed through t() for translation.
          $block['subject'] = t('Newest Recipes');

          // The content of the block is typically generated by calling a custom
          // function.
          $result = db_query_range(db_rewrite_sql("SELECT n.nid, n.title, n.uid, u.name FROM {node} n INNER JOIN {node_revisions} r ON n.vid = r.vid INNER JOIN {users} u ON n.uid = u.uid WHERE n.type='recipe' AND n.status =1 ORDER BY n.created DESC"), 0, 5);
          $block["content"] = node_title_list($result);
          break;
      }
      return $block;
  }
}

/**
 * Implementation of hook_view().
 */
function recipe_view(&$node, $teaser = FALSE, $page = FALSE) {
  if ($page) {
    drupal_set_breadcrumb(array(
      l(t('Home'), ''),
      l(t('Recipes'), 'recipe'),
    ));
    drupal_add_css(drupal_get_path('module', 'recipe') . '/recipe.css');
  }
  $node = recipe_node_prepare($node, $teaser);
  $node->content['body'] = array(
    '#value' => $teaser ? $node->teaser : theme('node_recipe', $node, $page),
    '#weight' => 1,
  );
  return $node;
}

/**
 * Returns a cached array of recipe unit types
 */
function recipe_unit_options() {
  static $options;
  static $unit_rs;
  if (!isset($unit_rs)) {
    $unit_rs = db_query('SELECT id,type,name,abbreviation FROM {recipe_unit} ORDER BY type ASC, metric');
    $options = array();
    while ($r = db_fetch_object($unit_rs)) {
      if (isset($r->type)) {
        if (!isset($options[$r->type])) {
          $options[$r->type] = array();
        }
        $options[$r->type][$r->id] = $r->name . ' (' . $r->abbreviation . ')';
      }
      else {
        $options[$r->id] = $r->name . ' (' . $r->abbreviation . ')';
      }
    }
  }
  return $options;
}

/**
 * Converts a recipe ingredient name to and ID
 */
function recipe_ingredient_id_from_name($name) {
  static $cache;
  if (!$cache[$name]) {
    $ingredient_id = db_result(db_query("SELECT id FROM {recipe_ingredient} WHERE LOWER(name)='%s'", trim(strtolower($name))));
    if (!$ingredient_id) {
      global $active_db;
      $node_link = db_result(db_query("SELECT nid FROM {node} WHERE title = '%s'", $name));
      db_query("INSERT INTO {recipe_ingredient} (name, link) VALUES ('%s', '%s')", $name, $node_link);
      $ingredient_id = db_result(db_query("SELECT id FROM {recipe_ingredient} WHERE LOWER(name)='%s'", trim(strtolower($name))));
    }
    $cache[$name] = $ingredient_id;
  }
  return $cache[$name];
}

/**
 * Converts an ingredient's quantity from decimal to fraction
 */
function recipe_ingredient_quantity_from_decimal($ingredient_quantity) {
  if (strpos($ingredient_quantity, '.') && variable_get('recipe_fraction_display', t('{%d} %d&frasl;%d'))) {
    $decimal = $ingredient_quantity;
    if ($decimal == 0) {
      $whole = 0;
      $numerator = 0;
      $denominator = 1;
      $top_heavy = 0;
    }
    else {
      $sign = 1;
      if ($decimal < 0) {
        $sign = -1;
      }
    }
    if (floor(abs($decimal)) == 0) {
      $whole = 0;
      $conversion = abs($decimal);
    }
    else {
      $whole = floor(abs($decimal));
      $conversion = abs($decimal);
    }
    $power = 1;
    $flag = 0;
    while ($flag == 0) {
      $argument = $conversion * $power;
      if ($argument == floor($argument)) {
        $flag = 1;
      }
      else {
        $power = $power * 10;
      }
    }

    // workaround for thirds, sixths, ninths, twelfths
    $overrides = array(
      '3333' => array(
        1,
        3,
      ),
      '6666' => array(
        2,
        3,
      ),
      '9999' => array(
        3,
        3,
      ),
      // thirds
      '1666' => array(
        1,
        6,
      ),
      '8333' => array(
        5,
        6,
      ),
      // sixths
      '1111' => array(
        1,
        9,
      ),
      '2222' => array(
        2,
        9,
      ),
      '4444' => array(
        4,
        9,
      ),
      '5555' => array(
        5,
        9,
      ),
      '7777' => array(
        7,
        9,
      ),
      '8888' => array(
        8,
        9,
      ),
      // ninths
      '0833' => array(
        1,
        12,
      ),
      '4166' => array(
        5,
        12,
      ),
      '5833' => array(
        7,
        12,
      ),
      '9166' => array(
        11,
        12,
      ),
    );
    $conversionstr = substr((string) ($conversion - floor($conversion)), 2, 4);
    if (array_key_exists($conversionstr, $overrides)) {
      if ($overrides[$conversionstr][0] == $overrides[$conversionstr][1]) {
        return ($whole + 1) * $sign;
      }
      $denominator = $overrides[$conversionstr][1];
      $numerator = floor($conversion) * $denominator + $overrides[$conversionstr][0];
    }
    else {
      $numerator = $conversion * $power;
      $denominator = $power;
    }
    $hcf = recipe_euclid($numerator, $denominator);
    $numerator = $numerator / $hcf;
    $denominator = $denominator / $hcf;
    $whole = $sign * $whole;
    $top_heavy = $sign * $numerator;
    $numerator = abs($top_heavy) - abs($whole) * $denominator;
    if ($whole == 0 && $sign == -1) {
      $numerator = $numerator * $sign;
    }
    $ingredient_quantity = sprintf(variable_get('recipe_fraction_display', t('{%d} %d&frasl;%d')), $whole, $numerator, $denominator);
    if ($whole == 0 && strpos($ingredient_quantity, '{') >= 0) {

      /* remove anything in curly braces */
      $ingredient_quantity = preg_replace('/{.*}/', '', $ingredient_quantity);
    }
    else {

      /* remove just the curly braces, but keep everything between them */
      $ingredient_quantity = preg_replace('/{|}/', '', $ingredient_quantity);
    }
  }
  return filter_xss_admin($ingredient_quantity);
}

/**
 * Converts an ingredient's quantity from fractions to decimal
 */
function recipe_ingredient_quantity_from_fraction($ingredient_quantity) {
  if ($pos_slash = strpos($ingredient_quantity, '/')) {
    $pos_space = strpos($ingredient_quantity, ' ');

    // can't trust $pos_space to be a zero value if there is no space
    // so set it explicitly
    if ($pos_space === false) {
      $pos_space = 0;
    }
    $whole = substr($ingredient_quantity, 0, $pos_space);
    $numerator = substr($ingredient_quantity, $pos_space, $pos_slash);
    $denominator = substr($ingredient_quantity, $pos_slash + 1);
    $ingredient_quantity = $whole + $numerator / $denominator;
  }
  return $ingredient_quantity;
}

/**
 * Saves the changed ingredients of a recipe node to the database
 * (by comparing the old and new ingredients first)
 */
function recipe_save_ingredients($node) {
  if (!$node->ingredients) {
    $node->ingredients = array();
  }
  $changes = recipe_ingredients_diff($node->ingredients, recipe_load_ingredients($node));
  if (count($changes->remove) > 0) {
    $ids = implode(',', $changes->remove);
    db_query("DELETE FROM {recipe_node_ingredient} WHERE id IN (%s)", $ids);
  }
  foreach ($changes->add as $ingredient) {
    $ingredient->id = recipe_ingredient_id_from_name($ingredient->name);
    $ingredient->quantity = recipe_ingredient_quantity_from_fraction($ingredient->quantity);
    db_query("INSERT INTO {recipe_node_ingredient} (nid,ingredient_id,quantity,unit_id) VALUES (%d,%d,%f,%d)", $node->nid, $ingredient->id, $ingredient->quantity, $ingredient->unit_id);
  }
  foreach ($changes->update as $ingredient) {
    $ingredient->id = recipe_ingredient_id_from_name($ingredient->name);
    $ingredient->quantity = recipe_ingredient_quantity_from_fraction($ingredient->quantity);
    db_query("UPDATE {recipe_node_ingredient} SET quantity='%f', unit_id='%d' WHERE nid='%d' AND  ingredient_id='%d'", $ingredient->quantity, $ingredient->unit_id, $node->nid, $ingredient->id);
  }
}

/**
 * Compares two arrays of ingredients and returns the differences
 */
function recipe_ingredients_diff($a1, $a2) {
  $return->add = array();
  $return->remove = array();
  $return->update = array();
  foreach ($a1 as $pl) {
    $pl = (object) $pl;
    $pl->name = trim($pl->name);
    if ($pl->name) {
      if (!_in_array($pl, $return->add)) {

        // Duplicate entries for the same ingredient are ignored.
        if (!_in_array($pl, $a2)) {
          $return->add[] = $pl;
        }
        else {
          if (!_in_array($pl, $return->update)) {
            $return->update[] = $pl;
          }
        }
      }
    }
  }
  foreach ($a2 as $k => $pl) {
    if (!_in_array($pl, $a1)) {
      $return->remove[] = $pl->id;
    }
  }
  return $return;
}

/**
 * Custom in_array() function because PHP 4 in_aray() doesnt seem to
 * handle the first arguement being an object
 */
function _in_array($a, $b) {
  $a->name = trim(strtolower($a->name));
  foreach ($b as $row) {
    $compareto = "";
    if (is_array($row)) {
      $compareto = trim(strtolower($row["name"]));
    }
    else {
      $compareto = trim(strtolower($row->name));
    }
    if ($a->name === $compareto) {
      return true;
    }
  }
  return false;
}

/**
 * Loads the ingredients for a recipe
 */
function recipe_load_ingredients($node) {
  $rs = db_query('
  SELECT
    ri.id,
    i.name,
    i.link,
    ri.quantity,
    ri.unit_id,
    u.abbreviation,
    ri.ingredient_id
  FROM
    {recipe_node_ingredient} ri,
    {recipe_ingredient} i,
    {recipe_unit} u
  WHERE
    ri.ingredient_id = i.id
    AND ri.unit_id = u.id
    AND ri.nid=%d
  ORDER BY
	ri.id', $node->nid);
  $ingredients = array();
  while ($ingredient = db_fetch_object($rs)) {
    $ingredients[] = $ingredient;
  }
  return $ingredients;
}

/**
 * Converts a recipe unit ID to it's abbreviation
 */
function recipe_unit_abbreviation($unit_id) {
  static $abbreviations;
  if (!$abbreviations) {
    $rs = db_query('SELECT id,abbreviation FROM {recipe_unit}');
    while ($unit = db_fetch_object($rs)) {
      $abbreviations[$unit->id] = $unit->abbreviation;
    }
  }
  return $abbreviations[$unit_id];
}

/**
 * Converts a recipe unit ID to it's name */
function recipe_unit_name($unit_id) {
  static $unit_names;
  if (!$unit_names) {
    $rs = db_query('SELECT id,name FROM {recipe_unit}');
    while ($unit = db_fetch_object($rs)) {
      $unit_names[$unit->id] = $unit->name;
    }
  }
  return $unit_names[$unit_id];
}

/**
 * Menu callback; Generates various representation of a recipe page with
 * all descendants and prints the requested representation to output.
 *
 * The function delegates the generation of output to helper functions.
 * The function name is derived by prepending 'recipe_export_' to the
 * given output type.  So, e.g., a type of 'html' results in a call to
 * the function recipe_export_html().
 *
 * @param type
 *   - a string encoding the type of output requested.
 *       The following types are currently supported in recipe module
 *          html: HTML (printer friendly output)
 *          recipeml: XML (RecipeML formatted output)
 *       Other types can be supported with contributed modules.
 * @param nid
 *   - an integer representing the node id (nid) of the node to export
 *
 */
function recipe_export($type = 'html', $nid = 0) {
  $type = drupal_strtolower($type);
  $export_function = 'recipe_export_' . $type;
  if (function_exists($export_function)) {
    echo call_user_func($export_function, $nid);
  }
  else {
    drupal_set_message(t('Unknown export format.'));
    drupal_not_found();
  }
}

/**
 * This function is called by recipe_export() to generate HTML for export.
 *
 * @param nid
 *   - an integer representing the node id (nid) of the node to export
 * @return
 * - string containing HTML representing the recipe
 */
function recipe_export_html($nid) {
  if ($nid == 0) {
    drupal_goto('recipe');
  }
  $node = node_load(array(
    'nid' => $nid,
    'type' => 'recipe',
  ));
  $node = recipe_node_prepare($node, FALSE);
  $output = theme('node_recipe', $node, FALSE);
  $html = theme('recipe_export_html', check_plain($node->title), $output);
  return $html;
}

/**
 * This function is called by recipe_export() to generate RecipeML for export.
 *
 * @param nid
 *   - an integer representing the node id (nid) of the node to export
 * @return
 * - string containing the recipe in RecipeML
*/
function recipe_export_recipeml($nid) {
  if ($nid == 0) {
    drupal_goto('recipe');
  }
  $node = node_load(array(
    'nid' => $nid,
    'type' => 'recipe',
  ));
  drupal_set_header('Content-type: text/xml');
  $output = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . '<!DOCTYPE recipeml PUBLIC "-//FormatData//DTD RecipeML 0.5//EN" "http://www.formatdata.com/recipeml/recipeml.dtd">' . "\n" . '<recipeml version="0.5">' . "\n" . '  <recipe>' . "\n" . '    <head>' . "\n" . '      <title>' . $node->title . '</title>' . "\n" . '    </head>' . "\n" . '    <yield><qty>' . $node->yield . '</qty></yield>' . "\n" . '    <ingredients>';
  foreach ($node->ingredients as $ingredient) {
    $output .= "\n" . '<ing><amt><qty>' . $ingredient->quantity . '</qty><unit>' . $ingredient->abbreviation . '</unit></amt><item>' . $ingredient->name . '</item></ing>';
  }
  $output .= "\n" . '    </ingredients>' . "\n" . '    <directions>' . $node->instructions . '</directions>' . "\n" . '  </recipe>' . "\n" . '</recipeml>';
  return $output;
}

/**
 * Callback function for ingredient autocomplete
 */
function recipe_autocomplete_page($string = "", $limit = 10) {
  $matches = array();
  $rs = db_query("SELECT name FROM {recipe_ingredient} WHERE LOWER(name) LIKE '%s%%' ORDER BY name LIMIT %d", strtolower($string), $limit);
  while ($r = db_fetch_object($rs)) {
    $matches[$r->name] = check_plain($r->name);
  }
  print drupal_to_js($matches);
  exit;
}

/**
 * Implementation of hook_validate().
 *
 * Errors should be signaled with form_set_error().
 */
function recipe_validate(&$node) {
  if (!$node->ingredients) {
    return;
  }
  $ingredients = array();
  foreach ($node->ingredients as $key => $ingredient) {
    $ingredient = (object) $ingredient;
    if (!isset($ingredient->quantity)) {
      $ingredient = recipe_parse_ingredient_string($ingredient->name);
    }
    if ($ingredient->name && _in_array($ingredient, $ingredients)) {
      form_set_error("recipe", t('Duplicate ingredients are not allowed.'));
    }
    else {
      $ingredients[] = $ingredient;
    }
    $node->ingredients[$key] = $ingredient;
  }
}

/**
 * Converts an ingredients name string to an ingredient object
 */
function recipe_parse_ingredient_string($ingredient_string) {
  if (preg_match('#([0-9.]+(?:\\s?\\d*/\\d*)?\\s?)?(?:([a-zA-Z.]*)\\s)?(.*)#', trim($ingredient_string), $matches)) {
    $ingredient->name = $matches[3];
    $ingredient->quantity = trim($matches[1]);
    if ($ingredient->quantity == 0) {
      $ingredient->quantity = 0;
    }
    $t_unit = $matches[2];
    $unit = recipe_unit_from_name($t_unit);
    if ($unit) {
      $ingredient->unit_id = $unit->id;
      $ingredient->abbreviation = $unit->abbreviation;
    }
    else {
      $ingredient->unit_id = 29;
      $ingredient->abbreviation = '';
      $ingredient->name = $t_unit . ' ' . $ingredient->name;
    }
    $ingredient->name = trim($ingredient->name);
    return $ingredient;
  }
  else {
    return false;
  }
}

/**
 * Returns information about a unit based on a unit abbreviation or name
 */
function recipe_unit_from_name($name) {
  if (strlen($name) > 1) {
    $string = strtolower($name);
  }
  else {
    $string = $name;
  }
  $ending = substr($string, -1, 1);
  if ($ending == 's' && $string != 'ds' || $ending == '.') {
    $string = substr($string, 0, strlen($string) - 1);
  }
  $ending = substr($string, -1, 1);
  if ($ending == 's' && $string != 'ds' || $ending == '.') {
    $string = substr($string, 0, strlen($string) - 1);
  }
  static $units_array;
  if (!$units_array) {
    $rs = db_query('SELECT id,name,abbreviation FROM {recipe_unit}');
    while ($unit = db_fetch_object($rs)) {
      $units_array[strtolower($unit->name)] = $unit;
      $units_array[$unit->abbreviation] = $unit;
    }
  }
  return $units_array[$string];
}

/**
 Menu Callback - created output for the main recipe page.

 @return $body
**/
function recipe_page() {
  $body = "";
  if (arg(1) == "feed") {
    module_invoke('node', 'feed', module_invoke('taxonomy', 'select_nodes', recipe_get_recipe_terms(), 'or', 0, FALSE));
  }
  else {
    if (arg(1) != NULL) {
      $breadcrumb = drupal_get_breadcrumb();
      $term = recipe_build_breadcrumbs($breadcrumb);
      drupal_set_breadcrumb($breadcrumb);
      if ($term != NULL) {
        $content = recipe_index($term->tid);
        if ($content != '') {
          $body = theme('box', $term->name . '- ' . t('Sub Categories'), $content);
        }
        $terms = array_merge(array(
          $term->tid,
        ), array_map('_recipe_get_tid_from_term', module_invoke('taxonomy', 'get_children', $term->tid)));
        $body .= module_invoke('taxonomy', 'render_nodes', module_invoke('taxonomy', 'select_nodes', $terms));
      }
    }
    else {
      $body = '';
      if (variable_get('recipe_recent_box_enable', 1)) {
        $body = theme('box', variable_get('recipe_recent_box_title', t('Latest Recipes')), module_invoke('node', 'title_list', recipe_get_latest(variable_get('recipe_recent_display', '5')), '') . theme('recipe_more_info', theme('feed_icon', url("recipe/feed"))));
      }
      $content = recipe_index();
      if ($content != '') {
        $body .= theme('box', t('Recipe Categories'), $content);
      }
    }
  }
  return $body;
}

/**
 Builds a breadcrumb list.

 @param breadcrumb a reference to the breadcrumb array. New items will be appending to this array.

 @return returns a term object if the last item in the url is a term, otherwise returns NULL.
**/
function recipe_build_breadcrumbs(&$breadcrumb) {
  if (arg(1) != NULL) {
    $i = 1;
    $url = 'recipe';
    $breadcrumb[] = l(ucwords(t('Recipes')), $url);
    while (arg($i) != NULL) {
      $last_term = urldecode(arg($i));
      $url = $url . '/' . urlencode($last_term);
      $breadcrumb[] = l(ucwords($last_term), $url);
      $i++;
    }
    $term = current(module_invoke('taxonomy', 'get_term_by_name', $last_term));
    return $term;
  }
  return NULL;
}

/**
 Recursively traverses the term tree to construct the index.

 @return string the output for this tree.
**/
function recipe_build_index(&$tree, $parent_url) {
  $output = '';
  if ($tree == array()) {
    return '';
  }
  do {
    $cur = current($tree);
    $nex = next($tree);
    if ($nex === false) {
      $next_depth = -1;
    }
    else {
      $next_depth = $nex->depth;
    }
    $cur->link = $parent_url . '/' . urlencode(strtolower(trim($cur->name)));
    $cur->children = '';
    if ($next_depth > $cur->depth) {
      $cur->children = recipe_build_index($tree, $cur->link);

      // sync $next_depth, because 'next item' may be shoved forward
      // Thanks for the patch Roderik.
      $nex = current($tree);
      if ($nex === false) {
        $next_depth = -1;
      }
      else {
        $next_depth = $nex->depth;
      }
    }
    $cur->count = module_invoke('taxonomy', 'term_count_nodes', $cur->tid);
    $output .= theme('recipe_index_item', $cur);
  } while ($cur->depth == $next_depth);
  return theme('recipe_list', $output);
}

/**
  Constructs a url from the current url arguments list.

  @return a string containing a formated URL.
**/
function recipe_get_current_url() {
  $arg_index = 1;
  $url = arg(0);
  while ($argument = arg($arg_index)) {
    $url .= '/' . urlencode($argument);
    $arg_index++;
  }
  return $url;
}

/**
 Get the latest recipes

 @return a database query result.
**/
function recipe_get_latest($count = 0) {
  $tids = recipe_get_recipe_terms();
  return recipe_select_nodes($tids, 'or', 0, FALSE, $count);
}

/**
 Get all the terms associated with Recipes.

 @return an array of unique term ids.
**/
function recipe_get_recipe_terms() {
  $vocabs = recipe_get_vocabularies();
  $tids = array();
  foreach ($vocabs as $vocab) {
    $tids = array_merge($tids, recipe_tax_get_terms($vocab->vid));
  }
  return array_unique($tids);
}

/**
 Gets all the vocabularies that are associated with the recipe module.

 @return array the vocabularies.
**/
function recipe_get_vocabularies() {
  $vocabularies = module_invoke('taxonomy', 'get_vocabularies', 'recipe');
  return $vocabularies;
}

/**
 Constructs the recipe index page, using theme functions.

 @return a string containing the output ready for display.
**/
function recipe_index($tid = 0) {
  $output = "";
  $vocabularies = recipe_get_vocabularies();
  foreach ($vocabularies as $vocab) {
    $max_depth = variable_get('recipe_index_depth', 0);
    $vocab_tree = module_invoke('taxonomy', 'get_tree', $vocab->vid, $tid, -1, $max_depth <= 0 ? NULL : $max_depth);
    $content = '';
    while (current($vocab_tree) != NULL) {
      $content .= recipe_build_index($vocab_tree, recipe_get_current_url());
    }
    if ($content != '') {
      $output .= theme('recipe_index', $vocab->name, $content);
    }
  }
  return $output;
}

/**
 * Custom version of node_prepare().
 *
 */
function recipe_node_prepare($node, $teaser = FALSE) {
  $node->readmore = TRUE;
  if ($teaser == FALSE) {
    $node->body = check_markup($node->body, $node->format, FALSE);
    $node->instructions = check_markup($node->instructions, $node->format, FALSE);
    if ($node->notes) {
      $node->notes = check_markup($node->notes, $node->format, FALSE);
    }
    if ($node->source) {
      $node->source = check_markup($node->source, $node->format, FALSE);
    }
    if ($node->ingredients) {
      $tmp = $node->ingredients;
      $node->ingredients = array();
      foreach ($tmp as $ingredient) {

        // For preview, node->ingredients is an array, for actual display, it's an object
        if (is_array($ingredient)) {
          if (isset($ingredient["name"])) {
            $ingredient["name"] = check_plain($ingredient["name"]);
          }
        }
        else {
          if (is_object($ingredient)) {
            if (isset($ingredient->name)) {
              $ingredient->name = check_plain($ingredient->name);
            }
          }
        }
        $node->ingredients[] = $ingredient;
      }
    }
  }
  else {
    $node->teaser = check_markup($node->body, $node->format, FALSE);
  }
  return $node;
}

/**
 * Finds all nodes that match selected taxonomy conditions.
 * This is just a copy of taxonomy_select_nodes() but
 * includes node title field in the selection.
 * Is this useful or try to find a taxonomy function
 * to achieve this?
 *
 * @param $tids
 *   An array of term IDs to match.
 * @param $operator
 *   How to interpret multiple IDs in the array. Can be "or" or "and".
 * @param $depth
 *   How many levels deep to traverse the taxonomy tree. Can be a nonnegative
 *   integer or "all".
 * @param $pager
 *   Whether the nodes are to be used with a pager (the case on most Drupal
 *   pages) or not (in an XML feed, for example).
 * @return
 *   A resource identifier pointing to the query results.
 */
function recipe_select_nodes($tids = array(), $operator = 'or', $depth = 0, $pager = TRUE, $count = 0) {
  if (count($tids) > 0) {

    // For each term ID, generate an array of descendant term IDs to the right depth.
    $descendant_tids = array();
    if ($depth === 'all') {
      $depth = NULL;
    }
    foreach ($tids as $index => $tid) {
      $term = module_invoke('taxonomy', 'get_term', $tid);
      $tree = module_invoke('taxonomy', 'get_tree', $term->vid, $tid, -1, $depth);
      $descendant_tids[] = array_merge(array(
        $tid,
      ), array_map('_recipe_get_tid_from_term', $tree));
    }
    if ($operator == 'or') {
      $str_tids = implode(',', call_user_func_array('array_merge', $descendant_tids));
      if (module_exists('category')) {
        $sql = 'SELECT DISTINCT n.nid, n.title, n.sticky, n.created FROM {node} n INNER JOIN {category_node} cn ON n.nid = cn.nid WHERE cn.cid IN (' . $str_tids . ') AND n.status = 1 ORDER BY n.sticky DESC, n.created DESC';
        $sql_count = 'SELECT COUNT(n.nid) FROM {node} n INNER JOIN {category_node} cn ON n.nid = cn.nid WHERE cn.tid IN (' . $str_tids . ') AND n.status = 1';
      }
      else {
        $sql = 'SELECT DISTINCT n.nid, n.title, n.sticky, n.created FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid WHERE tn.tid IN (' . $str_tids . ') AND n.status = 1 ORDER BY n.sticky DESC, n.created DESC';
        $sql_count = 'SELECT COUNT(n.nid) FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid WHERE tn.tid IN (' . $str_tids . ') AND n.status = 1';
      }
    }
    else {
      $joins = '';
      $wheres = '';
      if (module_exists('category')) {
        foreach ($descendant_tids as $index => $tids) {
          $joins .= ' INNER JOIN {category_node} cn' . $index . ' ON n.nid = cn' . $index . '.nid';
          $wheres .= ' AND cn' . $index . '.cid IN (' . implode(',', $tids) . ')';
        }
      }
      else {
        foreach ($descendant_tids as $index => $tids) {
          $joins .= ' INNER JOIN {term_node} tn' . $index . ' ON n.nid = tn' . $index . '.nid';
          $wheres .= ' AND tn' . $index . '.tid IN (' . implode(',', $tids) . ')';
        }
      }
      $sql = 'n.nid, n.title, n.sticky, n.created FROM {node} n ' . $joins . ' WHERE n.status = 1 AND ' . $wheres . ' ORDER BY n.sticky DESC, n.created DESC';
      $sql_count = 'SELECT COUNT(n.nid) FROM {node} n ' . $joins . ' WHERE n.status = 1 AND ' . $wheres;
    }
  }
  else {

    // no taxonomy used
    $sql = "SELECT nid, title, sticky, created FROM {node} WHERE status = 1 AND type = 'recipe' ORDER BY sticky DESC, created DESC";
    $sql_count = "SELECT COUNT(nid) FROM {node} WHERE status = 1 AND type = 'recipe'";
  }
  if ($pager && $count > 0) {
    $result = pager_query(db_rewrite_sql($sql), variable_get('default_nodes_main', 10), 0, db_rewrite_sql($sql_count));
  }
  else {
    $count = $count > 0 ? $count : 15;
    $result = db_query_range(db_rewrite_sql($sql), 0, $count);
  }
  return $result;
}

/**
 Get all the terms in a given vocabulary.

 @return an array of unique term ids.
**/
function recipe_tax_get_terms($vid) {
  $result = db_query("SELECT tid FROM {term_data} WHERE vid = %d", $vid);
  $tids = array();
  while ($term = db_fetch_array($result)) {
    $tids[] = $term['tid'];
  }
  return array_unique($tids);
}

/**
 Helper function for array map purposes.
 @param term the term object from which the tid will be extracted.

 @return the tid member of $term.
**/
function _recipe_get_tid_from_term($term) {
  return $term->tid;
}

/**
 * Theme functions group
 */

/**
 * A custom theme function.
 *
 * By using this function to format our node-specific information, themes
 * can override this presentation if they wish. We also wrap the default
 * presentation in a CSS class that is prefixed by the module name. This
 * way, style sheets can modify the output without requiring theme code.
 */
function theme_node_recipe($node, $yield_form = TRUE) {

  // Get custom yield or default to a factor of 1
  if ($yield_form && intval($node->yield) == $node->yield) {
    if ($_POST['op'] == t('Change')) {
      $yield = $_POST['custom_yield'];
    }
    else {
      if ($_POST['op'] == t('Halve')) {
        $yield = $_POST['custom_yield'] / 2;
      }
      else {
        if ($_POST['op'] == t('Double')) {
          $yield = $_POST['custom_yield'] * 2;
        }
      }
    }
    if ($yield && $yield != $node->yield && $node->yield != 0) {
      $factor = $yield / $node->yield;
      $node->yield = $yield;
    }
    else {
      $factor = 1;
    }
    $_POST = array();
    $yield = drupal_get_form('recipe_custom_yield_form', $node);
  }
  else {
    $yield = $node->yield;
    $factor = 1;
  }

  // Construct the $ingredients[] array
  if ($node->ingredients) {
    foreach ($node->ingredients as $ingredient) {
      if (isset($ingredient->quantity) && $ingredient->name) {
        if (!$ingredient->abbreviation) {
          $ingredient->abbreviation = recipe_unit_abbreviation($ingredient->unit_id);
        }
        if ($ingredient->quantity > 0) {
          $ingredient->quantity *= $factor;
        }
        else {
          $ingredient->quantity = '';
        }
        if (variable_get('recipe_fraction_display', t('{%d} %d&frasl;%d'))) {
          $ingredient->quantity = recipe_ingredient_quantity_from_decimal($ingredient->quantity);
        }
        if (!empty($ingredient->link)) {
          $ingredient->name = l($ingredient->name, 'node/' . $ingredient->link);
        }
        $ingredients[] = $ingredient->quantity . ' <acronym title="' . recipe_unit_name($ingredient->unit_id) . '">' . $ingredient->abbreviation . '</acronym> ' . $ingredient->name;
      }
    }
  }

  // Construct the summary
  $summary = '<table>';
  $summary .= '<tr><th>' . t('Yield') . '</th><td>' . $yield . '</td></tr>';
  if ($node->source) {
    $summary .= '<tr><th>' . t('Source') . '</th><td>' . $node->source . '</td></tr>';
  }
  if ($node->preptime) {
    if ($node->preptime < 60) {
      $preptime = format_plural($node->preptime, '1 minute', '@count minutes');
    }
    elseif ($node->preptime % 60 == 0) {
      $preptime = format_plural($node->preptime / 60, '1 hour', '@count hours');
    }
    else {
      $preptime = t('!time hours', array(
        '!time' => recipe_ingredient_quantity_from_decimal($node->preptime / 60),
      ));
    }
    $summary .= '<tr><th>' . t('Prep Time') . '</th><td>' . $preptime . '</td></tr>';
  }
  $vocabs = taxonomy_get_vocabularies('recipe');
  if (count($vocabs) > 0) {
    foreach ($vocabs as $vocab) {
      $terms = taxonomy_node_get_terms_by_vocabulary($node->nid, $vocab->vid);
      if (count($terms) > 0) {
        $summary .= '<tr><th>' . $vocab->name . '</th><td>';
        foreach ($terms as $term) {
          $summary .= l($term->name, 'taxonomy/term/' . $term->tid) . ' ';
        }
        $summary .= '</td></tr>';
      }
    }
  }
  $summary .= '</table>';

  // Create the output
  $output = '';
  $output .= '<div class="recipe-summary">' . theme('box', t('Summary'), $summary) . '</div>';
  $output .= '<div class="recipe-description">' . theme('box', t('Description'), $node->body) . '</div>';
  $output .= '<div class="recipe-ingredients">' . theme('box', t('Ingredients'), theme('item_list', $ingredients)) . '</div>';
  $output .= '<div class="recipe-instructions">' . theme('box', t('Instructions'), $node->instructions) . '</div>';
  if ($node->notes !== '') {
    $output .= '<div class="recipe-notes">' . theme('box', t('Notes'), $node->notes) . '</div>';
  }
  return $output;
}
function recipe_custom_yield_form($node) {
  $form['custom_yield_container'] = array(
    '#type' => 'fieldset',
    '#collapsible' => FALSE,
    '#collapsed' => FALSE,
  );
  $form['custom_yield_container']['custom_yield'] = array(
    '#type' => 'textfield',
    '#default_value' => $node->yield,
    '#size' => 2,
    '#maxlength' => 4,
  );
  $form['custom_yield_container']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Change'),
  );
  $form['custom_yield_container']['halve'] = array(
    '#type' => 'submit',
    '#value' => t('Halve'),
  );
  $form['custom_yield_container']['double'] = array(
    '#type' => 'submit',
    '#value' => t('Double'),
  );
  return $form;
}

/**
 Controls the output of the rendered index list.

 @return string the output for the index list.
**/
function theme_recipe_index(&$name, &$index_list) {
  if ($index_list != "") {
    return "<div class=\"item-list\">\n{$index_list}\n</div>\n";
  }
  return "";
}

/**
 Displays a single index item.

 @return string the output for this item.
**/
function theme_recipe_index_item(&$term) {
  $description = $term->description != '' ? "<p class=\"recipe-desc\">" . $term->description . "</p>" : '';
  if ($term->count > 0) {
    return "<li ><div class=\"recipe-title\">" . l($term->name . " ({$term->count})", $term->link) . "</div>" . $description . $term->children . "</li>";
  }
  else {
    return "<li><div class=\"recipe-title\">" . $term->name . " ({$term->count})</div>" . $description . $term->children . "</li>";
  }
}

/**
 Displays a single one level list. Called for each group of items at the same depth.

 @return string the output for this list.
**/
function theme_recipe_list(&$output) {
  if ($output != '') {
    return "<ul>" . $output . "</ul>\n";
  }
  return '';
}

/**
 Displays more information content, suck as "more" links, and
 feed images.

 @return formatted string containint the output.
**/
function theme_recipe_more_info($content) {
  return "<div class=\"more-link\">" . $content . "</div>";
}

/**
 * How the recipe's HTML export should be themed
 *
 * @ingroup themeable
 */
function theme_recipe_export_html($title, $content) {
  global $base_url;
  $html = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n";
  $html .= '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">';
  $html .= "<head>\n<title>" . $title . "</title>\n";
  $html .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
  $html .= '<base href="' . $base_url . '/" />' . "\n";
  $html .= "<style type=\"text/css\">\n@import url(" . drupal_get_path('module', 'recipe') . "/recipe.css);\n</style>\n";
  $html .= "</head>\n<body>\n<h1 class=title>" . $title . "</h1>\n" . $content . "\n</body>\n</html>\n";
  return $html;
}

/**
 *
 *
 */
function recipe_euclid($number_one, $number_two) {
  if ($number_one == 0 or $number_two == 0) {
    $hcf = 1;
    return $hcf;
  }
  else {
    if ($number_one < $number_two) {
      $buffer = $number_one;
      $number_one = $number_two;
      $number_two = $buffer;
    }
    $dividend = $number_one;
    $divisor = $number_two;
    $remainder = $dividend;
    while ($remainder > 0) {
      if (floor($dividend / $divisor) == $dividend / $divisor) {
        $quotient = $dividend / $divisor;
        $remainder = 0;
      }
      else {
        $quotient = floor($dividend / $divisor);
        $remainder = $dividend - $quotient * $divisor;
      }
      $hcf = $divisor;
      $dividend = $divisor;
      $divisor = $remainder;
    }
  }
  return $hcf;
}
function recipe_import_from_46() {
  $rcount = 0;
  $icount = 0;
  $out = "<p>";
  $old = db_query("SELECT * FROM {recipe_old}");
  while ($o = db_fetch_object($old)) {
    db_query("INSERT INTO {recipe} (nid, source, yield, preptime, notes, instructions) VALUES('%d', '%s', '%d', '%d', '%s', '%s')", $o->nid, $o->source, intval($o->yield), $o->preptime, $o->notes, $o->instructions);
    $out .= t('Imported recipe @recipeid', array(
      '@recipeid' => $o->nid,
    )) . '<br />';
    $rcount++;
  }
  $out .= "</p>";
  $out .= "<p><pre>";
  $ings = db_query("SELECT * FROM {recipe_ingredients}");
  while ($i = db_fetch_object($ings)) {
    $unitid = 29;

    // "unknown"
    $quantity = recipe_ingredient_quantity_from_fraction($i->ingredient);

    // best guess
    $out .= "\n\n" . t('Parsing @ingredient', array(
      '@ingredient' => $i->ingredient,
    )) . "\n";
    $nmatches = preg_match("/([^a-zA-Z]*)([^ ]*) (.*)/", $i->ingredient, $matches);

    //print_r($matches);
    if ($nmatches > 0) {
      $quantity = recipe_ingredient_quantity_from_fraction($matches[1]);
      $unit = $matches[2];
      $name = $matches[3];
      $out .= t('looking up unit for "@unit"', array(
        '@unit' => strtolower($unit),
      )) . "\n";
      $unit_rs = db_query("SELECT id FROM {recipe_unit} WHERE '%s' LIKE CONCAT(LOWER(name),'%%') OR abbreviation='%s'", strtolower($unit), $unit);

      // allow pints to match pint etc
      if ($unito = db_fetch_object($unit_rs)) {
        $out .= t('Got unit id @unitid', array(
          '@unitid' => $unito->id,
        )) . "\n";
        $unitid = $unito->id;
      }
      else {
        $name = $unit . " " . $name;
      }
    }
    $iid = recipe_ingredient_id_from_name($name);
    $out .= t('Got id for "@name": @iid', array(
      '@name' => $name,
      '@iid' => $iid,
    ));
    db_query("INSERT INTO {recipe_node_ingredient} (nid, unit_id, quantity, ingredient_id) VALUES('%d', '%d', '%f', '%d')", $i->nid, $unitid, $quantity, $iid);
    $icount++;
  }
  $out .= "</pre></p>";
  $out .= '<p>' . t('Imported @rcount recipes and @icount ingredients.', array(
    '@rcount' => $rcount,
    '@icount' => $icount,
  )) . '</p>';
  return $out;
}

Functions

Namesort descending Description
recipe_access Implementation of hook_access().
recipe_admin_settings Settings form for menu callback
recipe_autocomplete_page Callback function for ingredient autocomplete
recipe_block Implementation of hook_block().
recipe_build_breadcrumbs Builds a breadcrumb list.
recipe_build_index Recursively traverses the term tree to construct the index.
recipe_custom_yield_form
recipe_delete Implementation of hook_delete().
recipe_euclid
recipe_export Menu callback; Generates various representation of a recipe page with all descendants and prints the requested representation to output.
recipe_export_html This function is called by recipe_export() to generate HTML for export.
recipe_export_recipeml This function is called by recipe_export() to generate RecipeML for export.
recipe_form Implementation of hook_form().
recipe_get_current_url Constructs a url from the current url arguments list.
recipe_get_latest Get the latest recipes
recipe_get_recipe_terms Get all the terms associated with Recipes.
recipe_get_vocabularies Gets all the vocabularies that are associated with the recipe module.
recipe_help Implementation of hook_help().
recipe_import_from_46
recipe_index Constructs the recipe index page, using theme functions.
recipe_ingredients_diff Compares two arrays of ingredients and returns the differences
recipe_ingredient_id_from_name Converts a recipe ingredient name to and ID
recipe_ingredient_quantity_from_decimal Converts an ingredient's quantity from decimal to fraction
recipe_ingredient_quantity_from_fraction Converts an ingredient's quantity from fractions to decimal
recipe_insert Implementation of hook_insert().
recipe_link Implementation of hook_link().
recipe_load Implementation of hook_load().
recipe_load_ingredients Loads the ingredients for a recipe
recipe_menu Implementation of hook_menu().
recipe_node_info Implementation of hook_node_info(). This function replaces hook_node_name() and hook_node_types() from 4.6.
recipe_node_prepare Custom version of node_prepare().
recipe_page Menu Callback - created output for the main recipe page.
recipe_parse_ingredient_string Converts an ingredients name string to an ingredient object
recipe_perm Implementation of hook_perm().
recipe_save_ingredients Saves the changed ingredients of a recipe node to the database (by comparing the old and new ingredients first)
recipe_select_nodes Finds all nodes that match selected taxonomy conditions. This is just a copy of taxonomy_select_nodes() but includes node title field in the selection. Is this useful or try to find a taxonomy function to achieve this?
recipe_tax_get_terms Get all the terms in a given vocabulary.
recipe_unit_abbreviation Converts a recipe unit ID to it's abbreviation
recipe_unit_from_name Returns information about a unit based on a unit abbreviation or name
recipe_unit_name Converts a recipe unit ID to it's name
recipe_unit_options Returns a cached array of recipe unit types
recipe_update Implementation of hook_update().
recipe_validate Implementation of hook_validate().
recipe_view Implementation of hook_view().
theme_node_recipe A custom theme function.
theme_recipe_export_html How the recipe's HTML export should be themed
theme_recipe_index Controls the output of the rendered index list.
theme_recipe_index_item Displays a single index item.
theme_recipe_list Displays a single one level list. Called for each group of items at the same depth.
theme_recipe_more_info Displays more information content, suck as "more" links, and feed images.
_in_array Custom in_array() function because PHP 4 in_aray() doesnt seem to handle the first arguement being an object
_recipe_get_tid_from_term Helper function for array map purposes.