Index: token.tokens.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/token/token.tokens.inc,v
retrieving revision 1.33
diff -u -p -r1.33 token.tokens.inc
--- token.tokens.inc	16 Nov 2010 03:12:10 -0000	1.33
+++ token.tokens.inc	16 Nov 2010 04:14:59 -0000
@@ -64,6 +64,13 @@ function token_token_info() {
     'description' => t('The content type of the node.'),
     'type' => 'content-type',
   );
+  if (_token_node_get_first_term()) {
+    $info['tokens']['node']['term'] = array(
+      'name' => t('First taxonomy term'),
+      'description' => t('The lowest-weighted taxonomy term associated with the node from the lowest-weighted vocabulary.'),
+      'type' => 'term',
+    );
+  }
 
   // Content type tokens.
   $info['types']['content-type'] = array(
@@ -232,6 +239,11 @@ function token_tokens($type, $tokens, ar
           $type_name = node_type_get_name($node);
           $replacements[$original] = $sanitize ? check_plain($type_name) : $type_name;
           break;
+        case 'term':
+          if ($term = _token_node_get_first_term($node)) {
+            $replacements[$original] = $sanitize ? check_plain($term->name) : $term->name;
+          }
+          break;
       }
     }
 
@@ -242,6 +254,9 @@ function token_tokens($type, $tokens, ar
     if (($node_type_tokens = token_find_with_prefix($tokens, 'content-type')) && $node_type = node_type_load($node->type)) {
       $replacements += token_generate('content-type', $node_type_tokens, array('node_type' => $node_type), $options);
     }
+    if (($term_tokens = token_find_with_prefix($tokens, 'term')) && $term = _token_node_get_first_term($node)) {
+      $replacements += token_generate('term', $term_tokens, array('term' => $term), $options);
+    }
   }
 
   // Content type tokens.
@@ -729,3 +744,43 @@ function _token_profile_fields() {
 
   return $fields;
 }
+
+/**
+ * Find the first taxonomy term associated with a node.
+ *
+ * By 'first' the function returns the lowest-weight term, of the lowest-weight
+ * vocabulary that is associated with the node.
+ *
+ * @param $node
+ *   A node object.
+ *
+ * @return
+ *   The first term associated with the node.
+ */
+function _token_node_get_first_term($node = NULL) {
+  static $can_load_node_terms;
+  $terms = &drupal_static(__FUNCTION__, array());
+
+  if (!isset($can_load_node_terms)) {
+    $can_load_node_terms = module_exists('taxonomy') && variable_get('taxonomy_maintain_index_table', TRUE);
+  }
+
+  if (!isset($node)) {
+    return $can_load_node_terms;
+  }
+  elseif ($can_load_node_terms && !isset($terms[$node->nid])) {
+    $query = db_select('taxonomy_index', 'ti');
+    $query->innerJoin('taxonomy_term_data', 'td', 'ti.tid = td.tid');
+    $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid');
+    $query->addField('ti', 'tid');
+    $query->condition('ti.nid', $node->nid);
+    $query->orderBy('tv.weight');
+    $query->orderBy('td.weight');
+    $query->orderBy('td.name');
+    $query->range(0, 1);
+    $tid = $query->execute()->fetchField();
+    $terms[$node->nid] = $tid ? taxonomy_term_load($tid) : FALSE;
+  }
+
+  return isset($terms[$node->nid]) ? $terms[$node->nid] : FALSE;
+}
Index: token.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/token/token.test,v
retrieving revision 1.27
diff -u -p -r1.27 token.test
--- token.test	16 Nov 2010 03:12:10 -0000	1.27
+++ token.test	16 Nov 2010 04:14:59 -0000
@@ -150,7 +150,10 @@ class TokenNodeTestCase extends TokenTes
   }
 
   function testNodeTokens() {
-    $source_node = $this->drupalCreateNode(array('log' => $this->randomName(), 'path' => array('alias' => 'content/source-node')));
+    $source_node = $this->drupalCreateNode(array(
+      'log' => $this->randomName(),
+      'path' => array('alias' => 'content/source-node'),
+    ));
     $tokens = array(
       'source' => NULL,
       'source:nid' => NULL,
@@ -169,7 +172,10 @@ class TokenNodeTestCase extends TokenTes
     );
     $this->assertTokens('node', $source_node, $tokens);
 
-    $translated_node = $this->drupalCreateNode(array('tnid' => $source_node->nid, 'type' => 'article'));
+    $translated_node = $this->drupalCreateNode(array(
+      'tnid' => $source_node->nid,
+      'type' => 'article',
+    ));
     $tokens = array(
       'source' => $source_node->title,
       'source:nid' => $source_node->nid,
@@ -185,6 +191,8 @@ class TokenNodeTestCase extends TokenTes
       'type' => 'article',
       'type-name' => 'Article',
       'tnid' => $source_node->nid,
+      'term' => NULL,
+      'term:tid' => NULL,
     );
     $this->assertTokens('node', $translated_node, $tokens);
   }
@@ -325,6 +333,27 @@ class TokenTaxonomyTestCase extends Toke
     $this->assertTokens('vocabulary', $vocabulary, $tokens);
   }
 
+  /**
+   * Test the node taxonomy tokens.
+   */
+  function testNodeTermTokens() {
+    $term1 = $this->addTerm($this->vocab, array('name' => 'Cats', 'weight' => 5));
+    $term2 = $this->addTerm($this->vocab, array('name' => 'Dogs', 'weight' => 0));
+
+    $tags[LANGUAGE_NONE][0]['tid'] = $term1->tid;
+    $tags[LANGUAGE_NONE][1]['tid'] = $term2->tid;
+    $node = $this->drupalCreateNode(array(
+      'type' => 'article',
+      'field_tags' => $tags,
+    ));
+
+    $tokens = array(
+      'term' => 'Dogs',
+      'term:tid' => $term2->tid,
+    );
+    $this->assertTokens('node', $node, $tokens);
+  }
+
   function addVocabulary(array $vocabulary = array()) {
     $vocabulary += array(
       'name' => drupal_strtolower($this->randomName(5)),
