code wiki / (root) / nx_rss.nx

nx_rss.nx source

↩ module page · 189 lines · 6754 B

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