code wiki / (root) / nx_rss_lib.nx

nx_rss_lib.nx source

↩ module page · 164 lines · 6146 B

1// nx_rss_lib.nx -- THE RSS 2.0 BUILDER AS A LIBRARY (extracted 2026-09-16 from nx_rss.nx, whose compile-only smoke main() made it 2// un-importable -- the first consumer that needed to COMPOSE it, the /compare daily feed, could not). Every builder function 3// below is byte-identical to the one it was moved from; nx_rss.nx now imports this file and keeps only its smoke main. 4// rss.nx -- RSS 2.0 feed builder. 5// 6// Emits valid RSS 2.0 XML (specification: Harvard Berkman Center, 7// 2009). Every feed reader, podcast app, news aggregator, and 8// many static site generators consume RSS. nishi-pages without 9// RSS is half a blog engine. 10// 11// Feed skeleton: 12// <?xml version=\"1.0\" encoding=\"utf-8\"?> 13// <rss version=\"2.0\"> 14// <channel> 15// <title>...</title> 16// <link>...</link> 17// <description>...</description> 18// <language>en</language> 19// <pubDate>Wed, 22 Apr 2026 12:34:56 GMT</pubDate> 20// <item> 21// <title>...</title> 22// <link>...</link> 23// <description>...</description> 24// <pubDate>...</pubDate> 25// <guid>...</guid> 26// </item> 27// ... 28// </channel> 29// </rss> 30// 31// All dates use IMF-fixdate (http_date.nx). Text content is 32// HTML-escaped (html_escape.nx) so the feed can contain '<', 33// '>', '&' safely. 34// 35// This is a stream-builder: caller walks their posts and calls 36// rss_item for each; we don't own the post list. All output 37// appends to a caller-owned buffer with bounds checking. 38// 39// Invariants: 40// R1 Order matters: rss_begin -> rss_item* -> rss_end. 41// R2 Caller supplies HTML-unsafe text; we escape internally. 42// R3 All dates are unix seconds converted to GMT fixdate. 43// R4 Description can contain HTML; we wrap it in CDATA rather 44// than escaping, matching WordPress / Ghost convention. 45// (strict RSS says escape, but CDATA is universally 46// accepted and preserves readability.) 47 48// nx_safety_envelope: 49// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 50// sil_target: SIL1 51// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 52// verdict: NOT_YET_EVALUATED 53 54import "nx_syscalls.nx" 55import "nx_html_escape.nx" 56import "nx_http_date.nx" 57const RSS_MAGIC_4096: i64 = 4096 58 59const RSS_ERR_SHORT: i64 = -1 60 61// Write literal bytes from src into out at off. Returns new off 62// or RSS_ERR_SHORT. 63func rss_put(out: *u8, cap: i64, off: i64, 64 src: *u8, n: i64) -> i64 { 65 if off + n > cap { return RSS_ERR_SHORT } 66 var i: i64 = 0 67 while i < n { 68 out[off + i] = src[i] 69 i = i + 1 70 } 71 return off + n 72} 73 74// Write an HTML-escaped text range. 75func rss_put_escaped(out: *u8, cap: i64, off: i64, 76 src: *u8, n: i64) -> i64 { 77 let written: i64 = html_escape(out + off, cap - off, src, n) 78 if written < 0 { return RSS_ERR_SHORT } 79 return off + written 80} 81 82// Emit the channel opening + channel-level metadata. Caller 83// supplies title/link/description + channel pubDate (unix seconds). 84func rss_begin(out: *u8, cap: i64, off: i64, 85 title: *u8, title_len: i64, 86 link: *u8, link_len: i64, 87 desc: *u8, desc_len: i64, 88 channel_pub_unix: i64) -> i64 { 89 var cur: i64 = off 90 cur = rss_put(out, cap, cur, 91 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n", 38) 92 if cur < 0 { return cur } 93 cur = rss_put(out, cap, cur, "<rss version=\"2.0\">\n", 20) 94 if cur < 0 { return cur } 95 cur = rss_put(out, cap, cur, "<channel>\n", 10) 96 if cur < 0 { return cur } 97 98 cur = rss_put(out, cap, cur, "<title>", 7) 99 if cur < 0 { return cur } 100 cur = rss_put_escaped(out, cap, cur, title, title_len) 101 if cur < 0 { return cur } 102 cur = rss_put(out, cap, cur, "</title>\n<link>", 15) 103 if cur < 0 { return cur } 104 cur = rss_put_escaped(out, cap, cur, link, link_len) 105 if cur < 0 { return cur } 106 cur = rss_put(out, cap, cur, "</link>\n<description>", 21) 107 if cur < 0 { return cur } 108 cur = rss_put_escaped(out, cap, cur, desc, desc_len) 109 if cur < 0 { return cur } 110 cur = rss_put(out, cap, cur, "</description>\n<pubDate>", 24) 111 if cur < 0 { return cur } 112 // http-date is 29 bytes. 113 if cur + 29 > cap { return RSS_ERR_SHORT } 114 http_date_format_unix(out + cur, cap - cur, channel_pub_unix) 115 cur = cur + 29 116 cur = rss_put(out, cap, cur, "</pubDate>\n", 11) 117 if cur < 0 { return cur } 118 return cur 119} 120 121// Emit one <item>. guid is usually the post URL or a stable ID. 122func rss_item(out: *u8, cap: i64, off: i64, 123 title: *u8, title_len: i64, 124 link: *u8, link_len: i64, 125 desc: *u8, desc_len: i64, 126 guid: *u8, guid_len: i64, 127 pub_unix: i64) -> i64 { 128 var cur: i64 = off 129 cur = rss_put(out, cap, cur, "<item>\n<title>", 14) 130 if cur < 0 { return cur } 131 cur = rss_put_escaped(out, cap, cur, title, title_len) 132 if cur < 0 { return cur } 133 134 cur = rss_put(out, cap, cur, "</title>\n<link>", 15) 135 if cur < 0 { return cur } 136 cur = rss_put_escaped(out, cap, cur, link, link_len) 137 if cur < 0 { return cur } 138 139 // description in CDATA -- preserves HTML markup in the body. 140 cur = rss_put(out, cap, cur, 141 "</link>\n<description><![CDATA[", 30) 142 if cur < 0 { return cur } 143 cur = rss_put(out, cap, cur, desc, desc_len) 144 if cur < 0 { return cur } 145 cur = rss_put(out, cap, cur, "]]></description>\n<guid>", 24) 146 if cur < 0 { return cur } 147 cur = rss_put_escaped(out, cap, cur, guid, guid_len) 148 if cur < 0 { return cur } 149 cur = rss_put(out, cap, cur, "</guid>\n<pubDate>", 17) 150 if cur < 0 { return cur } 151 if cur + 29 > cap { return RSS_ERR_SHORT } 152 http_date_format_unix(out + cur, cap - cur, pub_unix) 153 cur = cur + 29 154 cur = rss_put(out, cap, cur, "</pubDate>\n</item>\n", 19) 155 if cur < 0 { return cur } 156 return cur 157} 158 159// Close channel + rss. 160func rss_end(out: *u8, cap: i64, off: i64) -> i64 { 161 return rss_put(out, cap, off, "</channel>\n</rss>\n", 18) 162} 163 164// Compile-only smoke: 1-item feed.