r21283 - Code Review

From MediaWiki.org

Jump to: navigation, search
Repository:MediaWiki
Revision:r21282 | r21283 (on ViewVC) | r21284 >
Date:13:39, 16 April 2007
Author:daniel
Status:new
Tags:
Comment:adding support for RSS/Atom feeds; experimental
Modified paths:

Diff [purge]

Index: trunk/extensions/News/News.php
===================================================================
--- trunk/extensions/News/News.php	(revision 21282)
+++ trunk/extensions/News/News.php	(revision 21283)
@@ -22,19 +22,172 @@
 	'description' => 'shows recent changes on a wiki page',
 );
 
+$wgNewsFeedURLPattern = false; // pattern for feed-URLs; useful when using rewrites for canonical feed URLs
+$wgNewsFeedUserPattern = false; // pattern to use for the author-field in feed items.
+
 $wgExtensionFunctions[] = "wfNewsExtension";
 
 $wgAutoloadClasses['NewsRenderer'] = dirname( __FILE__ ) . '/NewsRenderer.php';
+$wgHooks['ArticleViewHeader'][] = 'wfNewsArticleViewHeader';
+$wgHooks['ArticlePurge'][] = 'wfNewsArticlePurge';
+$wgHooks['SkinTemplateOutputPageBeforeExec'][] = 'wfNewsSkinTemplateOutputPageBeforeExec';
 
+//FIXME: find a way to override the feed URLs generated by OutputPage::getHeadLinks
+
 function wfNewsExtension() {
     global $wgParser;
-    $wgParser->setHook( "news", "newsxRenderNews" );
+    $wgParser->setHook( "news", "wfNewsTag" );
+    $wgParser->setHook( "newsfeed", "wfNewsFeedTag" );
+    $wgParser->setHook( "newsfeedlink", "wfNewsFeedLinkTag" );
 }
 
+function wfNewsTag( $templatetext, $argv, &$parser ) {
+    global $wgTitle;
 
-function newsxRenderNews( $templatetext, $argv, &$parser ) {
-    $renderer = new NewsRenderer($templatetext, $argv, $parser);
+    $parser->disableCache(); //TODO: use smart cache & purge...?
+    $renderer = new NewsRenderer($wgTitle, $templatetext, $argv, $parser);
+
     return $renderer->renderNews();
 }
 
+function wfNewsFeedTag( $templatetext, $argv, &$parser ) {
+    global $wgTitle, $wgOut;
+
+    $parser->disableCache(); //TODO: use smart cache & purge...?
+    $wgOut->setSyndicated( true );
+
+    #$rss = $renderer->renderFeedMetaLink( 'rss' );
+    #$atom = $renderer->renderFeedMetaLink( 'atom' );
+    #$parser->mOutput->addHeadItem($rss . $atom);
+
+    $renderer = new NewsRenderer($wgTitle, $templatetext, $argv, $parser);
+    $html = $renderer->renderFeedPreview();
+    return $html;
+}
+
+function wfNewsFeedLinkTag( $linktext, $argv, &$parser ) {
+    return NewsRenderer::renderFeedLink($linktext, $argv, $parser);
+}
+
+function wfNewsCacheKey( $title, $format ) {
+    //global $wgLang;
+    //NOTE: per-language caching might be needed at some point.
+    //      right now, caching is done for anon users only 
+    //      (the content language might be set individually however, 
+    //      using an extension like LanguageSelector)
+
+    return "@newsfeed:" . urlencode($title->getPrefixedDBKey()) . '|' . urlencode($format);
+}
+
+function wfNewsArticleViewHeader( &$article ) {
+    global $wgRequest, $wgOut, $wgFeedClasses, $wgUser;
+
+    $format = $wgRequest->getVal( 'feed' );
+    if (!$format) return true; 
+
+    $wgOut->disable();
+    //XXX: returning false currently doesn't stop the rest of Article::view to execute :(
+
+    $title = $article->getTitle();
+    $format = strtolower( trim($format) );
+
+    if ( !isset($wgFeedClasses[$format] ) ) {
+        wfHttpError(400, "Bad Request", "unknown feed format: " . $format); //TODO: better code & text
+        return false;
+    }
+
+    if (!$article->exists()) {
+        wfHttpError(404, "Not Found", "feed page not found: " . $title->getPrefixedText()); //TODO: better text
+        return false;
+    }
+
+    $note = '';
+
+    //NOTE: do caching for anon users only, because of user-specific 
+    //      rendering of textual content
+    if ($wgUser->isAnon()) {
+        $cachekey = wfNewsCacheKey($title, $format);
+        $ocache = wfGetParserCacheStorage();
+        $e = $ocache ? $ocache->get( $cachekey ) : NULL;
+        $note .= ' anon;';
+    }
+    else {
+        $cachekey = NULL;
+        $ocache = NULL;
+        $e = NULL;
+        $note .= ' user;';
+    }
+
+    if ( $e ) {
+        $lastchange = wfTimestamp(TS_UNIX, NewsRenderer::getLastChangeTime());
+        if ($lastchange < $e['timestamp']) {
+            print $e['xml'] . "\n<!-- cached: $note -->\n";
+            return false; //done
+        }
+        else {
+            $note .= " stale: $lastchange >= {$e['timestamp']};";
+        }
+    }
+
+    global $wgParser; //evil global
+
+    if (!$wgParser->mOptions) { //XXX: ugly hack :(
+        $wgParser->mOptions = new ParserOptions; 
+        $wgParser->setOutputType( OT_HTML );
+        $wgParser->clearState();
+        $wgParser->mTitle = $title;
+    }
+
+    $renderer = NewsRenderer::newFromArticle( $article, $wgParser );
+    if (!$renderer) {
+        wfHttpError(404, "Bad Request", "no feed found on page: " . $title->getPrefixedText() ); //TODO: better code & text
+        return;
+    }
+
+    $description = ''; //TODO: grab from article content... but what? and how?
+    $ts = time();
+    $xml = $renderer->renderFeed( $format, $description );
+
+    $e = array( 'xml' => $xml, 'timestamp' => $ts );
+    if ($ocache) {
+        $ocache->set( $cachekey, $e, $ts + 24 * 60 * 60 ); //cache for max 24 hours; cached record is discarded when anything turns up in RC anyway.
+        $note .= ' updated;';
+    }
+
+    $wgOut->disable();
+    print $xml . "\n<!-- fresh: $note -->\n";
+    return false; //done
+}
+
+function wfNewsArticlePurge( &$article ) {
+    global $wgFeedClasses;
+
+    $ocache = wfGetParserCacheStorage();
+    if (!$ocache) return true;
+
+    $title = $article->getTitle();
+
+    foreach( $wgFeedClasses as $format => $class ) {
+        $cachekey = wfNewsCacheKey( $title, $format );
+        $ocache->delete( $cachekey );
+    }
+
+    return true;
+}
+
+function wfNewsSkinTemplateOutputPageBeforeExec( &$skin, &$tpl ) {
+    $feeds = $tpl->data['feeds'];
+    if (!$feeds) return true;
+
+    $title = $skin->mTitle; //hack...
+
+    foreach ($feeds as $format => $e) {
+        $e['href'] = NewsRenderer::getFeedURL( $title, $format );
+        $feeds[$format] = $e;
+    }
+
+    $tpl->setRef( 'feeds', $feeds );
+    true;
+}
+
 ?>
\ No newline at end of file
Index: trunk/extensions/News/NewsRenderer.php
===================================================================
--- trunk/extensions/News/NewsRenderer.php	(revision 21282)
+++ trunk/extensions/News/NewsRenderer.php	(revision 21283)
@@ -14,6 +14,9 @@
 	die( 1 );
 }
 
+define('NEWS_HEAD_LENGTH', 1024 * 2);
+define('NEWS_HEAD_SCAN', 256);
+
 #no need to include, rely on autoloader
 #global $IP;
 #require_once( "$IP/includes/RecentChange.php" );
@@ -23,6 +26,11 @@
 	var $parser;
 	var $skin;
 
+	var $title;
+
+	var $prefix;
+	var $postfix;
+
 	var $usetemplate;
 	var $templatetext;
 	var $templateparser;
@@ -42,9 +50,35 @@
 	var $onlynew;
 	var $onlypatrolled;
 
-	function __construct( $templatetext, $argv, &$parser ) {
+	static function newFromArticle( &$article, &$parser ) {
+		$title = $article->getTitle();
+		$article->getContent(); 
+		$text = $article->mContent;
+		if (!$text) return NULL;
+
+		$uniq_prefix = "\x07NR-UNIQ";
+		$elements = array( 'nowiki', 'gallery', 'newsfeed');
+		$matches = array();
+		$text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+
+		foreach( $matches as $marker => $data ) {
+			list( $element, $content, $params, $tag ) = $data;
+			$tagName = strtolower( $element );
+
+			if ($tagName != 'newsfeed') continue;
+			#if (!is_null($id) && (!isset($params['id']) || $params['id'] != $id)) continue;
+			
+			return new NewsRenderer( $title, $content, $params, $parser );
+		}
+
+		return NULL;
+	}
+
+	function __construct( $title, $templatetext, $argv, &$parser ) {
 		global $wgContLang, $wgUser;
 
+		$this->title = $title;
+
 		$this->skin = $wgUser->getSkin();
 		$this->parser = $parser;
 	
@@ -62,24 +96,38 @@
 	
 		#$template = @$argv['template'];
 	
-		if ( $this->usetemplate ) {
+		#if ( $this->usetemplate ) {
 			#print "<pre>$templatetitle</pre>";
 	
-			$this->templateparser = clone $parser;
-			$this->templateparser->setOutputType( OT_HTML );
-	
+			$this->templateparser = $parser;
+			#$this->templateparser = clone $parser;
+			#$this->templateparser->setOutputType( OT_HTML );
+			$this->templateoptions = new ParserOptions;
+			$this->templateoptions->setEditSection( false );
+			$this->templateoptions->setNumberHeadings( false );
+			$this->templateoptions->setRemoveComments( true );
+			$this->templateoptions->setUseTeX( false );
+			$this->templateoptions->setUseDynamicDates( false );
+			$this->templateoptions->setInterwikiMagic( true ); //strip interlanguage-links
+			$this->templateoptions->setAllowSpecialInclusion( false );
+
 			#$this->templatetitle = Title::newFromText( $template, NS_TEMPLATE );
 			#$templatetext = $templateparser->fetchTemplate( $templatetitle );
 			#print "<pre>$templatetext</pre>";
 		
-			$this->templateoptions = new ParserOptions;
 			#$templateoptions->setRemoveComments( true );
 			#$templateoptions->setMaxIncludeSize( self::MAX_INCLUDE_SIZE );
-		}
-		else {
+		#}
+
+		if ( !$this->usetemplate ) {
 			$this->changelist = new OldChangesList( $this->skin );
 		}
 	
+		#$this->feedId = @$argv['id'];
+
+		$this->prefix = @$argv['prefix'];
+		$this->postfix = @$argv['postfix'];
+
 		$this->limit = @$argv['limit'];
 		if ( !$this->limit ) $this->limit = 10;
 		else if ( $this->limit > 100 ) $this->limit = 100;
@@ -150,9 +198,30 @@
 			$group[] = 'rc_namespace AND rc_title';
 		}
 		*/
-	
 	}
 
+	/*
+	function getFeedId() {
+		return $this->feedId;
+	}
+	*/
+
+	/*
+	function getCacheKey() {
+		return '@' . get_class($this) . ':' . 
+			($this->templatetext ? md5($this->templatetext) : $this->templatetext ). '|' .
+			$this->namespaces . '|' .
+			$this->categories . '|' .
+			$this->types . '|' .
+			$this->nominor . ',' .
+			$this->noanon . ',' .
+			$this->nobot . ',' .
+			$this->notalk . ',' .
+			$this->onlynew . ',' .
+			$this->onlypatrolled ;
+	}
+	*/
+
 	function query( $dbr, $limit, $offset = 0 ) {
 		list( $trecentchanges, $tpage, $tcategorylinks ) = $dbr->tableNamesN( 'recentchanges', 'page', 'categorylinks' );
 	
@@ -191,24 +260,15 @@
 	
 		$sql = $dbr->limitResult( $sql, $limit, $offset );
 	
-		$res = $dbr->query( $sql, 'newsxFetchRows' );
+		$res = $dbr->query( $sql, 'NewsRenderer::query' );
 	
 		return $res;
 	}
 	
-	# The callback function for converting the input text to HTML output
-	function renderNews( ) {
-		global $wgTitle;
-
-		$this->parser->disableCache();
-	
+	function fetchNews( ) {
 		$dbr = wfGetDB( DB_SLAVE );
+		$news = array();
 	
-		$text = '';
-	
-		if ( !$this->usetemplate )
-			$text .= $this->changelist->beginRecentChangesList();
-
 		$remaining = $this->limit;
 		$offset = 0;
 		$ignore = array(); #collect stuff we already have, when in unique mode
@@ -228,8 +288,7 @@
 					$ignore[$k] = true;
 				}
 		
-				$t = $this->renderRow( $row );
-				$text .= trim($t) . "\n"; #FIXME: handle blank lines at the end sanely. Paragraphs may be desired, but not when using lists.
+				$news[] = $row;
 				$remaining -= 1;
 			}
 	
@@ -238,9 +297,32 @@
 			if ( !$has ) break; #empty result set, stop trying 
 		}
 		
+		return $news;
+	}
+
+	function renderNews( ) {
+		global $wgTitle;
+
+		$news = $this->fetchNews();
+
+		$text = '';
+	
+		if ( $this->usetemplate ) {
+			$text .= $this->prefix;
+		}
+		else {
+			$text .= $this->changelist->beginRecentChangesList();
+		}
+	
+		foreach ($news as $row) { 
+			$t = $this->renderRow( $row );
+			$text .= trim($t) . "\n"; #TODO: handle blank lines at the end sanely. Paragraphs may be desired, but not when using lists.
+		}
+		
 		if ( $this->usetemplate ) { #it's wikitext, parse
-			$output = $this->templateparser->parse( $text, $wgTitle, $this->templateoptions, true );
-			$html =  $output->getText();
+			#$output = $this->templateparser->parse( $text, $wgTitle, $this->templateoptions, true );
+			$text .= $this->postfix;
+			$html = $this->templateparser->recursiveTagParse( $text );
 		}
 		else { #it's already html
 			$text .= $this->changelist->endRecentChangesList();
@@ -249,14 +331,130 @@
 	
 		return $html;
 	}
+
+	function renderFeed( $format, $description = '' ) {
+		global $wgSitename, $wgFeedClasses;
+		$date = wfTimestamp(); //XXX: use MAX(rc_timestamp) ?
+
+		$cls = $wgFeedClasses[$format];
+		if (!class_exists($cls)) return false;
+
+		$url = $this->title->getFullUrl();
+		$feed = new $cls( $this->title->getText() . ' - ' . $wgSitename , $description, $url, $date );
+
+		$news = $this->fetchNews();
+
+		ob_start();
+		$feed->outHeader();	
+		foreach ($news as $row) { 
+			$t = $this->renderRow( $row, true );
+			$item = $this->makeFeedItem( $row, $t, true );
+
+			$feed->outItem( $item );
+		}
+		
+		$feed->outFooter();
+		$xml = ob_get_contents();
+		ob_end_clean();
+
+		return $xml;
+	}
 	
-	function renderRow( $row ) {
+	function renderFeedPreview( ) {
+		$news = $this->fetchNews();
+
+		$html = '';
+
+		foreach ($news as $row) { 
+			$t = $this->renderRow( $row, true );
+			$item = $this->makeFeedItem( $row, $t, false );
+			$t = $this->renderFeedItem( $item );
+			$html .= $t;
+		}
+
+		return $html;
+	}
+	
+	function renderFeedItem( $item ) {
+		global $wgContLang, $wgUser;
+		$sk = $wgUser->getSkin();
+
+		$html = '';
+		$html .= '<div class="newsfeed-item">';
+		$html .= '<div class="newsfeed-item-head">';
+
+		$html .= '<h1>' . $sk->makeKnownLinkObj( $item->title_object ) . '</h1>';
+
+		$html .= '<p><small>';
+		$html .= $item->getAuthor();
+		$html .= ', ';
+		$html .= $wgContLang->timeanddate( $item->getDate() );
+		if ( $item->getComments() ) {
+			$html .= ' - <i>';
+			$html .= htmlspecialchars( $item->raw_comment );
+			$html .= '</i>';
+		}
+		$html .= '</small></p>';
+
+		$html .= '</div>';
+
+		$html .= '<div class="newsfeed-item-content">';
+		$html .= $item->raw_text;
+		$html .= '</div>';
+		$html .= '</div>';
+		return $html;
+	}
+
+	function makeFeedItem( $row, $text, $standalone ) {
+		global $wgNewsFeedUserPattern;
+
+		$text = $text . ' __NOTOC__'; #XXX ugly hack!
+
+		if ($standalone) {
+			$output = $this->templateparser->parse( $text, $GLOBALS['wgTitle'], $this->templateoptions, true );
+			$text = $output->mText;
+		}
+		else {  //FIXME: mask interwikis, categories, etc!!!!!!!!
+			$text = $this->templateparser->recursiveTagParse( $text );
+		}
+
+		if ( $wgNewsFeedUserPattern ) {
+			$user = str_replace('$1', $row->rc_user_text, $wgNewsFeedUserPattern);
+		}
+		else {
+			$user = $row->rc_user_text;
+		}
+
+		$title = Title::makeTitle( $row->rc_namespace, $row->rc_title ); //XXX: this is redundant, we already have a title object in renderRow. But no good way to pass it :(
+		$item = new FeedItem( $title->getPrefixedText(), 
+					$text, 
+					$title->getFullURL(), 
+					$row->rc_timestamp,
+					$user,
+					$row->rc_comment );
+
+		//XXX: ugly hack - things used by preview
+		$item->raw_text = $text; //needed because FeedItem holds text html-encoded internally. wtf
+		$item->raw_comment = $row->rc_comment; //needed because FeedItem holds text html-encoded internally. wtf
+		$item->title_object = $title; //title object
+		return $item;
+	}
+
+	function renderRow( $row, $forFeed = false ) {
 		global $wgUser, $wgLang;
-	
+
 		$change = RecentChange::newFromRow( $row );
 		$change->counter = 0; //hack
-	
-		if ( !$this->usetemplate ) {
+
+		$usetemplate = $this->usetemplate;
+		$templatetext = $this->templatetext;
+
+		if (!$templatetext && $forFeed) {
+			$templatetext = '{{{head}}}';
+			$usetemplate = true;
+		}
+
+		if ( !$usetemplate ) {
 			#$pagelink = $this->skin->makeKnownLinkObj( $title );
 		
 			$this->changelist->insertDateHeader($dummy, $row->rc_timestamp); #dummy call to suppress date headers
@@ -276,7 +474,7 @@
 			$params['minor'] = $row->rc_minor ? 'true' : '';
 			$params['bot'] = $row->rc_bot ? 'true' : '';
 			$params['patrolled'] = $row->rc_patrolled ? 'true' : '';
-			$params['anon'] = ( $row->rc_user <= 0 ) ? 'true' : ''; #TODO: perhaps use (rc_user == rc_ip) instead? That would take care of entries from importing.
+			$params['anon'] = ( $row->rc_user <= 0 ) ? 'true' : ''; #XXX: perhaps use (rc_user == rc_ip) instead? That would take care of entries from importing.
 			$params['new'] = ( $row->rc_type == RC_NEW ) ? 'true' : '';
 
 			$params['type'] = $row->rc_type;
@@ -300,11 +498,184 @@
 			$params['permalink'] = $permaq ? $title->getFullURL( $permaq ) : '';
 
 			$params['comment'] = str_replace( array( '{{', '}}', '|', '\'' ), array( '&#123;&#123;', '&#125;&#125;', '&#124;', '$#39;' ), wfEscapeWikiText( $row->rc_comment ) );
-	
-			$text = $this->templateparser->replaceVariables( $this->templatetext, $params );
+			
+			if ( stripos($templatetext, '{{{content}}}')!==false || stripos($templatetext, '{{{head}}}')!==false ) {
+				$article = new Article( $title, $row->rc_this_oldid );
+				$t = $article->getContent(); 
+
+				$params['content'] = NewsRenderer::sanitizeWikiText( $t );
+				//TODO: expand variables & templates first, so cut-off applies to effective content, 
+				//      and extension tags from templates are stripped properly
+				//TODO: avoid magic categories, interwiki-links, etc
+
+				if ( stripos($templatetext, '{{{head}}}')!==false ) {
+					$params['head'] = NewsRenderer::extractHead( $params['content'], $title );
+				}
+			}
+
+			$text = $this->templateparser->replaceVariables( $templatetext, $params );
 			return $text;
 		}
 	}
+
+	/*
+	function renderFeedMetaLink( $format ) {
+		$format = strtolower(trim($format));
+
+		$name = $format;
+		if ($name == 'rss') $name = 'RSS 2.0';
+		else if ($name == 'atom') $name = 'Atom 1.0';
+
+		$mime = "application/$format+xml"; //hack
+		$url = NewsRenderer::getFeedURL($this->title, $format);
+		#$id = $this->feedId ? htmlspecialchars($this->feedId) : NULL;
+
+		$html = '<link rel="alternate" type="'.$mime.'" title="'.($id?"$id - ":'').$name.'" href="'.htmlspecialchars($url).'">';
+		return $html;
+	}
+	*/
+
+	static function getFeedURL( $title, $format ) {
+		global $wgNewsFeedURLPattern;
+
+		if ( $wgNewsFeedURLPattern ) {
+			$params = array(
+				'$1' => urlencode( $title->getPrefixedDBKey() ),
+				'$2' => urlencode( $format ),
+				#'$3' => urlencode( $feedId ),
+			);
+
+			$url = str_replace(array_keys($params), array_values($params), $wgNewsFeedURLPattern);
+		}
+		else {
+			$q = 'feed=' . urlencode( $format );
+			#if ($feedId) $q .= '&feed=' . urlencode( $feedId );
+	
+			$url = $title->getFullUrl($q);
+		}
+
+		return $url;
+	}
+
+	static function renderFeedLink( $text, $argv, &$parser ) {
+		$t = @$argv['feed'];
+		if ($t) $t = $parser->replaceVariables($t, array());
+
+		$title = $t === NULL ? NULL : Title::newFromText($t);
+		if (!$title) $title = $GLOBALS['wgTitle'];
+
+		#$id = @$argv['id'];
+		$format = @$argv['format'];
+		if (!$format) $format = 'rss';
+		else $format = strtolower(trim($format));
+
+		$icon = @$argv['icon'];
+		$iconright = false;
+		if (preg_match('/^(.+)\|(\w+)$/', $icon, $m)) {
+			$icon = $m[1];
+			$iconright = ( strtolower(trim($m[2])) === 'right' );
+		}
+		
+		$ticon = $icon ? Title::newFromText($icon, NS_IMAGE) : NULL;
+		$image = $ticon ? new Image( $ticon ) : NULL;
+		$thumb = $image ? $image->getThumbnail(80, 16) : NULL;
+		if ($image && !$thumb) $thumb = $image;
+		$iconurl = $thumb ? $thumb->getUrl() : NULL;
+
+		$url = NewsRenderer::getFeedURL($title, $format); 
+
+		$ttl = @$argv['title'];
+		if ($ttl) $ttl = $parser->replaceVariables($ttl, array());
+
+		$s = '';
+		if ($text) {
+			$s .= $parser->recursiveTagParse($text);
+			if (!$ttl) $ttl = $text . ' (' . $format . ')';
+		}
+		else {
+			if (!$ttl) $ttl = $format;
+		}
+
+		if ($iconurl) {
+			$ic = '<img border="0" src="'.htmlspecialchars($iconurl).'" alt="'.htmlspecialchars($ttl).'" title="'.htmlspecialchars($ttl).'"/>';
+			if ($s === '') $s = $ic;
+			else if ($iconright) $s = "$s&nbsp;$ic";
+			else $s = "$ic&nbsp;$s";
+		}
+
+		$html = '<a href="'.htmlspecialchars($url).'" title="'.htmlspecialchars($ttl).'">'.$s.'</a>';
+		return $html;
+	}
+
+	static function getLastChangeTime( ) {
+		$dbr = wfGetDB( DB_SLAVE );
+		list( $trecentchanges ) = $dbr->tableNamesN( 'recentchanges' );
+
+		$sql = 'select max(rc_timestamp) from ' . $trecentchanges;
+		$res = $dbr->query( $sql, 'NewsRenderer::getLastChangeTime' );
+		if (!$res) return false;
+
+		$row = $dbr->fetchRow($res);
+		if (!$row) return false;
+
+		return $row[0];
+	}
+
+	static function sanitizeWikiText( $text ) {
+		global $wgParser;
+
+		$elements = array_keys( $wgParser->mTagHooks );
+		$uniq_prefix = "\x07NR-UNIQ";
+
+		$matches = array();
+		$text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+
+		foreach( $matches as $marker => $data ) {
+			list( $element, $content, $params, $tag ) = $data;
+			$tagName = strtolower( $element );
+
+			$output = '';
+			if ($tagName == '!--') $output = $tag; //keep comments for now, may be stripped later
+
+			#print "* $marker => " . htmlspecialchars($output) . "<br/>\n";
+			$text = str_replace($marker, $output, $text);
+		}
+
+		return $text;
+	}
+
+	private static function cutHead( $text, $separators, $suffix ) {
+		$i = NEWS_HEAD_LENGTH - 1;
+		while ($i > NEWS_HEAD_LENGTH - NEWS_HEAD_SCAN) {
+			$ch = substr($text, $i, 1);
+			if (in_array($ch, $separators)) {
+				$text = substr($text, 0, $i);
+				return trim($text) . $suffix;
+			}
+
+			$i -= 1;
+		}
+
+		return false;
+	}
+
+	static function extractHead( $text, $title = NULL ) {
+		$text = trim($text);
+
+		if ( strlen($text) < NEWS_HEAD_LENGTH ) return $text;
+
+		$suffix = "\n" . '... ([[' . $title->getPrefixedText() . ']]...)';
+
+		$t = preg_replace('/^(.*?)<!--\s*summary\s+end\s*-->.*$/si', '\1', $text);
+		if ($t != $text) return trim($t);
+
+		if ( $t = NewsRenderer::cutHead($text, array("\r", "\n"), $suffix) ) return $t;
+		if ( $t = NewsRenderer::cutHead($text, array("."), $suffix) ) return $t;
+		if ( $t = NewsRenderer::cutHead($text, array(" ", "\t"), $suffix) ) return $t;
+		
+		$text = substr($text, 0, 512) . $suffix;
+		return $text;
+	}
 }
 
 ?>
\ No newline at end of file
Index: trunk/extensions/News/README
===================================================================
--- trunk/extensions/News/README	(revision 21282)
+++ trunk/extensions/News/README	(revision 21283)
@@ -5,8 +5,8 @@
           GNU Free Documentation License (GFDL)
 --------------------------------------------------------------------------
 
-The News extension provides a custom tag, <news>, that allows the inclusion of
-an excerpt from the Special:Recentchanges page to be shown on any wiki page.
+The News extension allows a custom excerpt from Special:Recentchanges to be 
+included on a wiki page, or to be published as an RSS or Atom feed. 
 It supports several types of filtering as well as full custom formating of
 entries, using template syntax.
 
@@ -18,7 +18,7 @@
 Note that the functionality of this extension overlaps with the DynamicPageList
 and DynamicPageList2 extensions - however, this extension has a different focus.
 
-== INSTALLING ==
+== Installing ==
 
 Copy the News directory into the extensions folder of your 
 MediaWiki installation. Then add the following line to your
@@ -26,8 +26,20 @@
 
   require_once( "$IP/extensions/News/News.php" );
 
-== USAGE ==
+== Usage ==
 
+The News extension prvodes provides three custom tags:
+* <news>: this includes a list of recent changes on the wiki page
+* <newsfeed>: this defines a news feed of recent changes; on the wiki page,
+  a preview is rendered, similar to the output of the <news> tag; the wiki
+  page then also supports the newsfeed action, which returns the feed in
+  RSS or Atom format.
+* <newsfeedlink>: this creates a link to a news feed defined using a
+  <newsfeed> tag. This is convenient for creating prominent links to the 
+  news feeds.
+
+=== Filtering and formatting ===
+
 To get the last 10 changes to your wiki on any wiki page, use the
 following:
 
@@ -47,9 +59,13 @@
 
 For a full list of options and template parameters, see below.
 
-=== OPTIONS ===
+The <newsfeed> tag supports the same optiosn for filtering and
+formatting as the <news> tag. For information on how to access a feed defined
+using <newsfeed>, see the section "Accessing Feeds" below.
+
+=== Options ===
 The following options (tag attributes) can be used to controll the output of the
-<news> tags:
+<news> and <newsfeed> tags:
 
 * unique        show only the most recent change to each page
 
@@ -83,7 +99,13 @@
                 "false"). If given, the edit shown may not refer to the current
                 revision. 
 
-=== PARAMETERS ===
+* prefix        wikitext to be inserted before the wikitext generated from the
+                template text is parsed. Can be used to make tables from news.
+
+* postfix       wikitext to be inserted after the wikitext generated from the
+                template text is parsed. Can be used to make tables from news.
+
+=== Parameters ===
 When giving a template text between the <news> tags, the following
 template-parameters are available (use them as {{{xxx}}}):
 
@@ -130,3 +152,55 @@
 
 * new_len      page length after the edit
 
+* content      the full content of the page
+
+* head         the page's content up to about 2KB of text, with smart cut-off.
+
+=== Accessing Feeds ===
+
+If the page Foo defines a feed using a <newsfeed> tag, that feed can be
+referenced by using feed=rss or feed=atom in the url respectively.
+
+So, if the URL path for page Foo is /wiki/Foo, you can use
+  /wiki/Foo?feed=rss 
+to get an RSS feed for that page. If the URL is /w/index.php?title=Foo,
+you would use 
+  /w/index.php?title=Foo&feed=rss
+
+You can conveniently create links to feeds using the <newsfeedlink> tag:
+For example, 
+  <newsfeedlink feed="Foo" format="rss">My Foo Feed</newsfeedlink>
+would generate a link to the news feed defined on page Foo using a
+<newsfeed> tag.
+
+You can also specify an icon to use in the link:
+  <newsfeedlink feed="Foo" format="rss" icon="rss.png">My Foo Feed</newsfeedlink>
+would generate a link that has the image "rss.png" before the link text
+(the icon option refers to the name of an image uploaded to the wiki). 
+  <newsfeedlink feed="Foo" format="rss" icon="rss.png|right">My Foo Feed</newsfeedlink>
+would generate a link that has the image to the right of the link text; and
+  <newsfeedlink feed="Foo" format="rss" icon="rss.png" title="RSS feed"/>
+generates a link that only shows the given icon. The title attribute specifies
+the tooltip to show when the mouse hovers over the link.
+
+Note that the link text may contain full wiki text, and the title-attribute may
+contain variables like {{PAGENAME}}.
+
+== Configuration ==
+Configuration settings to define in LocalSettings.php
+
+* $wgNewsFeedURLPattern: this defines the pattern used by the <newsfeedlink>
+tag to generate feed URLs. In the pattern, $1 will be replaced by the page title,
+and $2 will be replaced by the requested feed format.
+If you are using pretty URLs with $wgArticlePath set to $wgScript/$1 or /wiki/$1,
+etc, you can use the following for nicer feed URLs: 
+  $wgNewsFeedURLPattern = $wgArticlePath . '?feed=$2';
+(note that $wgArticlePath already contains $1 withe the meaning "page title")
+If you want to use rewrite rules for canonical feed URLs, like /feed/Foo.rss,
+set
+  $wgNewsFeedURLPattern = '/feed/$1.$2';
+
+* $wgNewsFeedUserPattern: this defines the pattern used to generate author
+names in feed items. In the pattern, $1 is replaced by the user name. To 
+e.g. generate email-addresses at your site as author names, use
+  $wgNewsFeedUserPattern = '$1@' . $wgServerName;
Views
Toolbox