Monday, November 08, 2010 at 12:05 AM.

system.verbs.builtins.radio.weblog.writeRssFile

on writeRssFile (f, title, link, description, language="none", flCategories=false, maxdays=infinity, adrcallback=nil, adrblog=radio.weblog.init (), adrGetUrlCallback=nil, ttl=60) {
	<<Changes
		<<1/20/03; 1:02:07 PM by DW
			<<Allow attributes added by callbacks, as described here:
				<<http://backend.userland.com/stories/storyReader$210#elementsWithAttributes
		<<1/19/03; 7:40:04 AM by DW
			<<Add callbacks, allowing applications to:
				<<1. Add namespaces to the <rss> element.
				<<2. Add elements at the <channel> level.
				<<3. Add elements at the <item> level.
			<<The callback scripts are in user.radio.callbacks.writeRssNamespace, writeRssChannelElement, writeRssItemElement. 
			<<Docs are here:
				<<http://backend.userland.com/stories/storyReader$210
		<<11/15/02; 4:48:52 PM by JES
			<<When generating the copyright sub-element of channel, encode entities in the author's name.
		<<11/9/02; 8:21:31 AM by DW
			<<Generate a skipHours element if there's enough data, and if the feature is enabled.
		<<10/30/02; 8:33:11 AM by DW
			<<Add <category> element pointing to the changes.xml file on weblogs.com, if public pinging is enabled.
		<<10/24/02; 6:44:07 AM by DW
			<<Gather info for Weblogs.Com ping for RSS feed. See also radio.thread.agents.pingWeblogsCom.
		<<10/21/02; 11:49:55 AM by JES
			<<Entity-encode enclosure URLs. Only include comment elements if the commentLink macro is present in the item template.
		<<10/17/02; 12:21:42 AM by JES
			<<Added the comments sub-element of item according to the RSS 2.0 spec.
		<<9/27/02; 7:36:14 AM by DW
			<<Drop the xmlns attribute on the rss element, its presence breaks some parsers. Instead, I'm letting Scripting News bear the brunt here, it'll retain the xmlns attribute, and shake out the broken parsers. They'll have a choice of fixing them or not. Note that with or without the xmlns attribute it's still a valid RSS 2.0 feed.
		<<9/10/02; 1:44:11 PM by DW
			<<isPermaLink attribute on guid defaults true, so we don't need to include the attribute. Keeps file size down.
			<<description doesn't have a type attribute, undo Jake's change from 9/2/02.
			<<Change version number to 2.0. 
			<<Change <docs> element to point to the 2.0 spec.
		<<Pre RSS 2.0 changes
			<<9/2/02; 5:21:12 PM by JES
				<<Added the isPermaLink attribute to the guid element. For feeds generated by Radio, isPermaLink is present and true for any feeds which also have HTML counterparts. Added the type="text/html" attribute to description.
					<<guid -> isPermaLink attribute -- http://backend.userland.com/rss#ltguidgtSubelementOfLtitemgt
					<<description -> type attribute -- http://backend.userland.com/rss#ltitemgtSubelementOfLtchannelgt
			<<9/1/02; 1:29:17 AM by JES
				<<Added an optional ttl parameter to specify the value for the ttl sub-element of channel. Default is 60 (minutes).
			<<9/1/02; 1:10:21 AM by JES
				<<Added guid and pubDate sub-elements of item, from the RSS 0.94 spec.
					<<guid -- http://backend.userland.com/rss#ltguidgtSubelementOfLtitemgt
					<<pubDate -- http://backend.userland.com/rss#ltpubdategtSubelementOfLtitemgt
			<<8/26/02; 9:02:52 PM by JES
				<<Encode item titles.
			<<5/8/02; 8:08:46 PM by DW
				<<Encode category names.
			<<5/8/02; 9:34:43 AM by DW
				<<Allow the user to completely replace writeRssFile. We check user.radio.callbacks.writeRssFile. If the table is non-empty, we loop over the table calling procedures, passing the parameter list we received. The first one that doesn't scripterror gets to speak for us. This allows people to experiment with different formats and encoding, and see how those changes interact with aggregators, incl the one that's built into Radio. We can't afford to experiment with our entire installed base. So this makes it possible for developers to experiment, learn, and perhaps will allow the format(s) we support to evolve. 
			<<4/22/02; 2:39:15 PM by DW
				<<Entity-encode the link sub-element of item.
			<<4/11/02; 9:19:00 AM by DW
				<<Add support for description filter callbacks.
			<<4/7/02; 9:55:21 AM by DW
				<<Do the macro processing or descriptions unconditionally, and in-line instead of calling radio.string.processMacros, which was only processing the descriptions if it contained "<%". This broke shortcuts. Prior art is the Manila-Blogger Bridge Tool, which unconditionally processes macros.
			<<3/12/02; 2:50:23 PM by JES
				<<If the pref is set, auto-generate <link> elements for each item. The url for the <link> is determined by calling a callback, whose address is specified in the new optional adrGetUrlCallback parameter.
					<<The callback itself takes two parameters: adrpost -- the address of the post, and adrurl -- the address of a string in which to save the url. If the callback returns true, then the <link> element is added.
			<<3/11/02; 2:55:28 PM by DW
				<<If Titles-And-Links are turned off, don't include them in the RSS output.
			<<3/11/02; 7:45:56 AM by DW
				<<For each post, if it has elements called link and title, add those to the RSS output.
			<<1/20/02; 9:47:31 AM by DW
				<<Factored from radio.weblog.publishRss. 
				<<A single bottleneck for generating RSS.
				<<We get the file as a key to managing the cache, so let's also write the file.
				<<The callback, if supplied, determines if a post is to be included in the RSS file. If not present, the posts in the loop are included.
			<<1/20/02; 9:07:41 AM by DW
				<<Process macros as RSS is being generated.
				<<Optimized using cache at system.temp.radio.rssCache. Average time to generate RSS for Dave's Handsome Radio Blog went from 40 ticks to 5 ticks. Moral of the story, encoding text takes a lot of cycles. 
			<<1/20/02; 8:16:28 AM by DW
				<<Respect the preference settings for managing editor and webmaster when generating the main RSS feed.
			<<12/20/01; 2:09:47 AM by JES
				<<Use file.writeTextFile instead of file.writeWholeFile to write the RSS.
			<<12/9/01; 12:42:35 PM by DW
				<<Get in synch with radio.weblog.publishCategoryRss, they have the same parts, similar values.
			<<10/30/01; 2:24:35 PM by JES
				<<Respect the pref for the max number of items to post, at adrblog^.prefs.maxOutputItemsPerChannel.
			<<10/6/01; 9:37:10 PM by JES
				<<Commented out code which transfers files to an FTP account. This is now accomplished through the upstreaming drivers.
			<<3/4/01; 12:24:48 PM by PBS
				<<Don't put a blank line at the top of RSS files.
			<<3/1/01; 11:39:48 PM by JES
				<<Write files out with Radu as the creator, and TEXT as the type.
			<<2/22/01; 9:31:01 PM by JES
				<<If this is a Mac, convert the text to Latin when building the XML.
				<<Soft-code the year in the copyright notice; it was hard-coded for 2001.
			<<2/17/01; 7:50:18 PM by JES
				<<Changed the URL in the <link> element so that it links to the HTML rendering, instead of to http://www.tbd.com/.
	
	bundle { //check with callbacks, allow wholesale override of writeRssFile functionality
		try {
			local (adrscript);
			for adrscript in @user.radio.callbacks.writeRssFile {
				try {
					while typeof (adrscript^) == addresstype {
						adrscript = adrscript^};
					return (adrscript^ (f, title, link, description, language, flCategories, maxdays, adrcallback, adrblog, adrGetUrlCallback))}}}};
	
	local (adrcache);
	bundle { //set adrcache
		local (adrtable = @system.temp.radio.rssCache);
		if not defined (adrtable^) {
			new (tabletype, adrtable)};
		adrcache = @adrtable^.[f];
		if not defined (adrcache^) {
			new (tabletype, adrcache)};
		if not defined (adrcache^.items) {
			new (tabletype, @adrcache^.items)}};
	
	local (flCloud = false);
	bundle { //set flCloud
		try {
			if not file.exists (f) { //radio.file.getFileAttributes doesn't work if the file doesn't exist
				file.writeTextFile (f, " ")};
			local (adrfile, adrspec);
			radio.file.getFileAttributes (f, @adrfile);
			radio.upstream.getUpstreamSpec (adrfile, @adrspec);
			if adrspec^.server == user.radio.prefs.rssCloud.server {
				if adrspec^.port == user.radio.prefs.rssCloud.port {
					if adrspec^.rpcPath == user.radio.prefs.rssCloud.path {
						flCloud = true}}}}};
	
	local (flComments = adrblog^.prefs.flCommentLinksEnabled);
	try { //set flComments
		local (f = user.radio.prefs.wwwFolder + "rss.xml");
		local (atts);
		radio.webserver.gatherAttributes (f, @atts);
		if defined (atts.itemTemplate) {
			local (s);
			if string.lower (atts.itemTemplate) endsWith ".opml" {
				local (lo);
				op.xmlToOutline (file.readWhileFile (atts.itemTemplate), @lo);
				s = string (lo)}
			else {
				s = file.readWholeFile (atts.itemTemplate)};
			flComments = (string.lower (s) contains "<%commentlink%>")}};
	
	local (xmltext);
	bundle { //build xmltext
		local (indentlevel = 0);
		on add (s) {
			xmltext = xmltext + string.filledstring ("\t", indentlevel) + s + "\r\n"};
		on encode (s) {
			if system.environment.isMac { //02/22/2001 JES: convert to Latin text
				return (xml.entityEncode (latinToMac.macToLatin (s), true))}
			else {
				return (xml.entityEncode (s, true))}};
		on popXmlDeclaration (s) {
			local (ixlinefeed = string.patternMatch ("\n", s));
			return (string.delete (s, 1, ixlinefeed))};
		on runCallbacks (adrtable, adritem=nil) {
			local (s = "");
			return (s)};
		add ("<?xml version=\"1.0\"?>");
		add ("<!-- RSS generated by Radio UserLand v" + frontier.version () + " on " + date.netstandardstring (clock.now ()) + " -->");
		<<bundle //9/27/02 by DW -- old <rss> element with xmlns attribute
			<<add ("<rss version=\"2.0\" xmlns=\"http://backend.userland.com/rss2\">"); indentlevel++
		<<bundle ///1/19/03 by DW -- old <rss> element, when life was simpler
			<<add ("<rss version=\"2.0\">"); indentlevel++
		bundle { //1/19/03 by DW -- <rss> element, now supports callbacks for adding namespace decls
			local (spacestable);
			new (tabletype, @spacestable);
			try {
				local (adrscript);
				for adrscript in @user.radio.callbacks.writeRssNamespace {
					try {
						while typeof (adrscript^) == addresstype {
							adrscript = adrscript^};
						adrscript^ (@spacestable)}}};
			local (adr, s = "");
			for adr in @spacestable {
				s = s + "xmlns:" + nameof (adr^) + "=\"" + string (adr^) + "\" "};
			s = string.trimwhitespace (s);
			if sizeof (s) == 0 { //no namespaces
				add ("<rss version=\"2.0\">"); indentlevel++}
			else {
				add ("<rss version=\"2.0\" " + s + ">"); indentlevel++}};
		add ("<channel>"); indentlevel++;
		add ("<title>" + encode (title) + "</title>");
		add ("<link>" + encode (link) + "</link>");
		add ("<description>" + encode (description) + "</description>");
		if language != "none" {
			add ("<language>" + language + "</language>")};
		add ("<copyright>" + "Copyright " + date.year () + " " + encode (adrblog^.prefs.authorName) + "</copyright>");
		add ("<lastBuildDate>" + date.netstandardstring (clock.now ()) + "</lastBuildDate>");
		add ("<docs>http://backend.userland.com/rss</docs>");
		add ("<generator>Radio UserLand v" + frontier.version () + "</generator>");
		add ("<managingEditor>" + encode (adrblog^.prefs.managingEditor) + "</managingEditor>");
		add ("<webMaster>" + encode (adrblog^.prefs.webmaster) + "</webMaster>");
		bundle { //add <category> element pointing to changes.xml; 10/30/02 by DW
			if adrblog^.prefs.flPublicBlog {
				add ("<category domain=\"http://www.weblogs.com/rssUpdates/changes.xml\">rssUpdates</category> ")}};
		bundle { //add skipHours if there's enough data and feature is enabled, 11/9/02 by DW
			if adrblog^.prefs.flGenerateSkipHours { //feature enabled
				local (skiplist = radio.weblog.getSkipHours (adrblog));
				if sizeof (skiplist) > 0 {
					add ("<skipHours>"); indentlevel++;
					for hour in skiplist {
						add ("<hour>" + hour + "</hour>")};
					add ("</skipHours>"); indentlevel--}}};
		bundle { //add cloud element
			if flCloud {
				with user.radio.prefs.rssCloud {
					add ("<cloud domain=\"" + server + "\" port=\"" + port + "\" path=\"" + path + "\" registerProcedure=\"" + registerProcedure + "\" protocol=\"" + protocol + "\"/>")}}};
		add ("<ttl>" + ttl + "</ttl>");
		on docallbacks (adrcallbacks, adrpost=nil) {
			local (t, adr, name);
			new (tabletype, @t);
			try { //call the callbacks
				local (adrscript);
				for adrscript in adrcallbacks {
					try {
						while typeof (adrscript^) == addresstype {
							adrscript = adrscript^};
						if adrpost != nil {
							adrscript^ (@t, adrpost)}
						else {
							adrscript^ (@t)}}}};
			for adr in @t {
				name = nameof (adr^);
				if name contains "\t" { //allowing more than one element with the same name
					name = string.nthfield (name, "\t", 2)};
				if typeof (adr^) == tabletype {
					local (adrsubitem, s = "<" + name);
					for adrsubitem in adr {
						if nameof (adrsubitem^) != "/value" {
							s = s + " " + nameof (adrsubitem^) + "=\"" + encode (string (adrsubitem^)) + "\""}};
					adrsubitem = @adr^.["/value"];
					if defined (adrsubitem^) {
						s = s + ">" + encode (string (adrsubitem^)) + "</" + name + ">"}
					else {
						s = s + " />"};
					add (s)}
				else {
					add ("<" + name + ">" + encode (string (adr^)) + "</" + name + ">")}}};
		bundle { //1/19/03 by DW -- add elements at the <channel> level
			docallbacks (@user.radio.callbacks.writeRssChannelElement)};
		bundle { //add the items
			local (i, adrpost, s, ctitems = 0);
			local (ctdays = 0, lastday = -1, itemstext = "", lastitemdate, flinclude);
			for i = sizeof (adrblog^.posts) downto 1 {
				adrpost = @adrblog^.posts [i];
				bundle { //check date rollover
					local (when = adrpost^.when, day, month, year, hour, minute, second);
					date.get (when, @day, @month, @year, @hour, @minute, @second);
					if lastday == -1 { //first time through loop
						lastday = day}
					else {
						if day != lastday {
							ctdays++;
							if ctdays == maxdays {
								break};
							lastday = day}}};
				bundle { //set flinclude
					flinclude = true;
					if adrcallback != nil {
						try {flinclude = adrcallback^ (adrpost)}}};
				if flinclude {
					add ("<item>"); indentlevel++;
					bundle { //add title and link present in the post, if enabled
						if adrblog^.prefs.flTitleAndLinkOnPostForm {
							if defined (adrpost^.title) {
								if sizeOf (adrpost^.title) > 0 {
									add ("<title>" + encode (adrpost^.title) + "</title>")}};
							if defined (adrpost^.link) {
								add ("<link>" + encode (adrpost^.link) + "</link>")}
							else { //if the auto-generate-links pref is set, use a generated link if possible
								if adrblog^.prefs.flAutoGenerateLinks {
									if adrGetUrlCallback != nil {
										local (url, flincludelink = false);
										try {flincludelink = adrGetUrlCallback^ (adrpost, @url)};
										if flincludelink {
											add ("<link>" + url + "</link>")}}}}}};
					bundle { //add description, either from the cache, or by recalcing
						local (flrecalc = true);
						local (adritemcache = @adrcache^.items.[nameof (adrpost^)]);
						if defined (adritemcache^) {
							if timeModified (@adrpost^.text) <= adritemcache^.when {
								flrecalc = false}};
						if flrecalc {
							local (itemtext = string (adrpost^.text));
							bundle { //call the description filter callbacks, 4/11/02 by DW
								try {
									local (adrscript);
									for adrscript in @adrblog^.callbacks.rssFilterDescription {
										try {
											while typeof (adrscript^) == addresstype {
												adrscript = adrscript^};
											itemtext = adrscript^ (itemtext, adrpost)}}}};
							bundle { //process macros in the description, into itemtext, 4/7/02 by DW
								local (pt);
								new (tabletype, @pt);
								pt.macroStartCharacters = "<%";
								pt.macroEndCharacters = "%>";
								pt.processMacrosInHtmlTags = true;
								pt.tools = @radio.data.website.["#tools"];
								try {itemtext = encode (html.processmacros (itemtext, adrpagetable:@pt))}};
								<<itemtext = encode (radio.string.processMacros (string (adrpost^.text)))
							new (tabletype, adritemcache);
							adritemcache^.text = itemtext;
							adritemcache^.when = clock.now ()};
						add ("<description>" + adritemcache^.text + "</description>")};
					bundle { //add guid and pubDate from the RSS 2.0 spec
						bundle { //add guid
							if adrGetUrlCallback != nil {
								local (uri, ispermalink = false);
								try {ispermalink = adrGetUrlCallback^ (adrpost, @uri)};
								if ispermalink {
									add ("<guid>" + uri + "</guid>")}
								else {
									local (folder = file.folderFromPath (f));
									local (urlfolder = radio.upstream.getFileUrl (folder));
									local (day, month, year, hour, minute, second);
									date.get (adrpost^.when, @day, @month, @year, @hour, @minute, @second);
									uri = urlfolder + year + "/" + string.padwithzeros (month, 2) + "/" + string.padwithzeros (day, 2) + ".html#a" + number (nameOf (adrpost^));
									add ("<guid isPermaLink=\"false\">" + uri + "</guid>")}}};
						bundle { //add pubDate
							try {add ("<pubDate>" + date.netStandardString (adrpost^.when) + "</pubDate>") }}};
					if defined (adrpost^.sourceName) and defined (adrpost^.sourceUrl) {
						add ("<source url=\"" + encode (adrpost^.sourceUrl) + "\">" + encode (adrpost^.sourceName) + "</source>")};
					if defined (adrpost^.enclosure) {
						with adrpost^.enclosure {
							if not defined (error) {
								add ("<enclosure url=\"" + encode (url) + "\" length=\"" + length + "\" type=\"" + type + "\"/>")}}};
					if flCategories {
						if defined (adrpost^.categories) {
							local (adr);
							for adr in @adrpost^.categories {
								add ("<category>" + encode (nameof (adr^)) + "</category>")}}};
					if flComments {
						local (commentPageUrl = radio.weblog.getCommentLink (number (nameOf (adrpost^)), adrblog) );
						if commentPageUrl != "" {
							add ("<comments>" + encode (commentPageUrl) + "</comments>")}};
					bundle { //1/19/03 by DW -- add elements at the <item> level
						docallbacks (@user.radio.callbacks.writeRssItemElement, adrpost)};
					add ("</item>"); indentlevel--;
					lastitemdate = adrpost^.when;
					if ++ctitems >= adrblog^.prefs.maxOutputItemsPerChannel {
						break}}}};
		add ("</channel>"); indentlevel--;
		add ("</rss>"); indentlevel--};
	file.sureFilePath (f);
	file.writeTextFile (f, xmltext);
	
	bundle { //prepare to notify weblogs.com, 10/24/02 by DW
		local (adrtable = @system.temp.radio.pingAfterUpstream.[f], adrfile);
		try {
			new (tabletype, adrtable);
			radio.file.getFileAttributes (f, @adrfile);
			adrtable^.url = adrfile^.upstream.url;
			adrtable^.flUpstreamed = false;
			adrtable^.name = title;
			adrtable^.pingCategory = "rss"}
		else {
			try {delete (adrtable)}}};
	
	return (xmltext)}
<<bundle //test code, 1/19/03; 8:20:44 AM by DW
	<<radio.weblog.publishRss ()
	<<webbrowser.openurl ("http://127.0.0.1:5335/rss.xml")
<<bundle //test code
	<<local (tc = clock.ticks ())
	<<writeRssFile (user.radio.prefs.wwwfolder + "aaa.xml", "Test RSS File", "http://www.example.com/", "Oh the buzzing of the bees")
	<<dialog.alert (clock.ticks () - tc)



This listing is for code that runs in the OPML Editor environment. I created these listings because I wanted the search engines to index it, so that when I want to look up something in my codebase I don't have to use the much slower search functionality in my object database. Dave Winer.