Index: includes/common.inc
===================================================================
--- includes/common.inc	(revision 108)
+++ includes/common.inc	(revision 110)
@@ -810,7 +810,59 @@
  * Functions to format numbers, strings, dates, etc.
  */
 
+function format_xml($string) {
+  return drupal_specialchars(strip_tags($string));
+}
+
 /**
+ * Generates a string of XML from an associative array structure. The array
+ * keys are tag names. The values are either the contents of the tag or, if
+ * there are multiple tags with the same name, a integer indexed array of tag
+ * contents. 
+ *
+ * In the case of a simple xml element, the contents is a string. For more
+ * complex elements, it is an array. The '.ATTRIBUTES' key is as associative
+ * array of attribute names as keys and associated values. Integer keys are
+ * simple character data and named keys are tag names with values being the
+ * contents of the tag.
+ *
+ * @param $xml
+ *   An associtive array of tag names to contents.
+ * @param $indent
+ *   The number of spaces to put in front of each line of xml.
+ * @return
+ *   The string of XML generated using the $xml array.
+ */
+function drupal_xml($xml, $indent = 0) {
+  $output = '';
+  foreach ($xml as $name => $value) {
+    if (is_int($name)) {
+      $output .= str_repeat(' ', $indent) . $value ."\n";
+    }
+    else if (is_array($value) && is_int(reset(array_keys($value)))) { // this is actually an array of tags with the same name
+      foreach ($value as $tag) {
+        $output .= drupal_xml(array($name => $tag), $indent);
+      }
+    }
+    else {
+      $output .= str_repeat(' ', $indent) .'<'. $name;
+      if (is_string($value)) { // simple element containing only a string
+        $output .= '>'. $value .'</'. $name;
+      }
+      else {
+        if ($value['.ATTRIBUTES']) { // the tag has attributes
+          $output .= drupal_attributes($value['.ATTRIBUTES']);
+          unset($value['.ATTRIBUTES']); // avoid further processing of attributes
+        }
+        $output .= count($value) == 0 ? ' /' : ">\n". drupal_xml($value, $indent + 1) . str_repeat(' ', $indent) .'</'. $name;
+      }
+      $output .= ">\n";
+    }
+  }
+  return $output;
+}
+
+/**
  * Formats an RSS channel.
  *
  * Arbitrary elements may be added using the $args associative array.
Index: modules/taxonomy.module
===================================================================
--- modules/taxonomy.module	(revision 108)
+++ modules/taxonomy.module	(revision 110)
@@ -1053,4 +1053,12 @@
   return $term->tid;
 }
 
+function taxonomy_rss($op, &$xml, $node = NULL) {
+  if ($op == 'item' && $node != $NULL) {
+    foreach (taxonomy_node_get_terms($node->nid) as $term) {
+      $xml['dc:subject'][] = format_xml($term->name);
+    }
+  }
+}
+
 ?>
Index: modules/node.module
===================================================================
--- modules/node.module	(revision 108)
+++ modules/node.module	(revision 110)
@@ -989,46 +989,96 @@
 function node_feed($nodes = 0, $channel = array()) {
   global $base_url, $locale;
 
+  $channel = array_merge(array(
+        'title' => format_xml(variable_get('site_name', 'drupal') .' - '. variable_get('site_slogan', '')),
+        'link' => format_xml(url('', NULL, NULL, TRUE)),
+        'description' => format_xml(variable_get('site_mission', '')),
+        'dc:language' => $locale
+        ), $channel);
+  node_feed_invoke_all('channel', $channel);
+
+  $channel['.ATTRIBUTES'] = array('rdf:about' => $channel['link']);
+  $rss['channel'] = $channel;
+
   if (!$nodes) {
     $nodes = db_query_range('SELECT n.nid FROM {node} n '. node_access_join_sql() .' WHERE '. node_access_where_sql() .' AND n.promote = 1 AND n.status = 1 ORDER BY n.created DESC', 0, 15);
   }
 
-  while ($node = db_fetch_object($nodes)) {
-    // Load the specified node:
-    $item = node_load(array('nid' => $node->nid));
-    $link = url("node/$node->nid", NULL, NULL, 1);
+  while ($node_result = db_fetch_object($nodes)) {
+    $node = node_load(array('nid' => $node_result->nid));
 
     // Filter and prepare node teaser
-    if (node_hook($item, 'view')) {
-      node_invoke($item, 'view', TRUE, FALSE);
+    if (node_hook($node, 'view')) {
+      node_invoke($node, 'view', TRUE, FALSE);
     }
     else {
-      $item = node_prepare($item, TRUE);
+      $node = node_prepare($node, TRUE);
     }
 
-    $items .= format_rss_item($item->title, $link, $item->teaser, array('pubDate' => date('r', $item->changed)));
+    $item = array(
+        'title' => format_xml($node->title),
+        'link' => format_xml(url('node/'. $node->nid, NULL, NULL, TRUE)),
+        'description' => drupal_specialchars(check_output($node->teaser ? $node->teaser : $node->body)),
+        'dc:date' => gmdate('Y-m-d\Th:i:s\Z', $node->changed)
+        );
+    node_feed_invoke_all('item', $item, $node);
+
+    $item['.ATTRIBUTES'] = array('rdf:about' => $item['link']);
+    $rss['item'][] = $item;
+    $rss['channel']['items']['rdf:Seq']['rdf:li'][]['.ATTRIBUTES'] = array('resource' => $item['link']);
   }
 
-  $channel_defaults = array(
-    'version'     => '0.92',
-    'title'       => variable_get('site_name', 'drupal') .' - '. variable_get('site_slogan', ''),
-    'link'        => $base_url,
-    'description' => variable_get('site_mission', ''),
-    'language'    => $locale
-  );
-  $channel = array_merge($channel_defaults, $channel);
+  $rss = array_merge($rss, node_feed_get_xml());
+  $rss['.ATTRIBUTES'] = array_merge(array(
+        'xml:base' => url('', NULL, NULL, TRUE),
+        'xmlns:rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 
+        'xmlns' => 'http://purl.org/rss/1.0/',
+        'xmlns:dc' => 'http://purl.org/dc/elements/1.1/'
+        ), node_feed_get_namespaces());
 
-  $output = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
-  $output .= "<!DOCTYPE rss [<!ENTITY % HTMLlat1 PUBLIC \"-//W3C//ENTITIES Latin 1 for XHTML//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent\">]>\n";
-  $output .= "<rss version=\"". $channel["version"] . "\" xml:base=\"". $base_url ."\">\n";
-  $output .= format_rss_channel($channel['title'], $channel['link'], $channel['description'], $items, $channel['language']);
-  $output .= "</rss>\n";
-
   drupal_set_header('Content-Type: text/xml; charset=utf-8');
-  print $output;
+  print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
+  print "<!DOCTYPE rss [<!ENTITY % HTMLlat1 PUBLIC \"-//W3C//ENTITIES Latin 1 for XHTML//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent\">]>\n";
+  print drupal_xml(array('rdf:RDF' => $rss));
 }
 
+function node_feed_add_namespace($prefix = NULL, $iri = NULL) {
+  static $namespaces = array();
+  if (isset($prefix)) {
+    $namespaces['xmlns:'. $prefix] = $iri;
+  }
+  return $namespaces;
+}
+
+function node_feed_get_namespaces() {
+  return node_feed_add_namespace();
+}
+
+function node_feed_add_xml($xml = NULL) {
+  static $rss = array();
+  if (isset($xml)) {
+    $rss = array_merge($rss, $xml);
+  }
+  return $rss;
+}
+
+function node_feed_get_xml() {
+  return node_feed_add_xml();
+}
+
 /**
+ * Invokes hook_feed on all the modules.
+ */
+function node_feed_invoke_all($op, &$xml, $node = NULL) {
+  foreach (module_list() as $module) {
+    $function = $module .'_rss';
+    if (function_exists($function)) {
+      $function($op, $xml, $node);
+    }
+  }
+}
+
+/**
  * Perform validation checks on the given node.
  */
 function node_validate($node) {
