code wiki / (root) / atom.nx

atom.nx source

↩ module page · 181 lines · 6523 B

1// atom.nx -- Atom 1.0 feed builder (RFC 4287). 2// 3// Modern alternative to RSS 2.0. Atom's strictness (required 4// id, updated, author; XML namespaces; RFC 3339 dates) makes it 5// cleaner for programmatic parsing. Google, NetNewsWire, and 6// many news APIs prefer Atom. Nishi-pages should emit both -- 7// RSS for universal compatibility, Atom for correctness. 8// 9// Feed skeleton: 10// <?xml version=\"1.0\" encoding=\"utf-8\"?> 11// <feed xmlns=\"http://www.w3.org/2005/Atom\"> 12// <title>...</title> 13// <link href=\"...\"/> 14// <id>...</id> 15// <updated>2026-04-22T12:34:56Z</updated> 16// <author><name>...</name></author> 17// <entry> 18// <title>...</title> 19// <link href=\"...\"/> 20// <id>...</id> 21// <updated>...</updated> 22// <summary>...</summary> 23// <content type=\"html\"><![CDATA[...]]></content> 24// </entry> 25// </feed> 26// 27// Dates: ISO 8601 (iso8601.nx) -- RFC 3339 is the Atom-specified 28// subset. HTML content: CDATA wrap, same convention as rss.nx. 29// 30// Invariants: 31// A1 id must be a stable tag: URI or URL. Caller supplies; 32// we emit as-is (escaped). 33// A2 updated is required at both feed + entry level; we 34// require it rather than defaulting. 35// A3 Stream builder: atom_begin -> atom_entry* -> atom_end. 36 37import "syscalls.nx" 38import "html_escape.nx" 39import "iso8601.nx" 40 41const ATOM_ERR_SHORT: i64 = -1 42 43func atom_put(out: *u8, cap: i64, off: i64, 44 src: *u8, n: i64) -> i64 { 45 if off + n > cap { return ATOM_ERR_SHORT } 46 var i: i64 = 0 47 while i < n { 48 out[off + i] = src[i] 49 i = i + 1 50 } 51 return off + n 52} 53 54func atom_put_escaped(out: *u8, cap: i64, off: i64, 55 src: *u8, n: i64) -> i64 { 56 let w: i64 = html_escape(out + off, cap - off, src, n) 57 if w < 0 { return ATOM_ERR_SHORT } 58 return off + w 59} 60 61// 20-byte ISO 8601 timestamp. 62func atom_put_iso(out: *u8, cap: i64, off: i64, unix_sec: i64) -> i64 { 63 if off + 20 > cap { return ATOM_ERR_SHORT } 64 iso_format_unix(out + off, cap - off, unix_sec) 65 return off + 20 66} 67 68// Open feed + emit channel metadata. 69func atom_begin(out: *u8, cap: i64, off: i64, 70 title: *u8, title_len: i64, 71 link: *u8, link_len: i64, 72 id: *u8, id_len: i64, 73 author: *u8, author_len: i64, 74 updated_unix: i64) -> i64 { 75 var cur: i64 = off 76 cur = atom_put(out, cap, cur, 77 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n", 38) 78 if cur < 0 { return cur } 79 cur = atom_put(out, cap, cur, 80 "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n", 43) 81 if cur < 0 { return cur } 82 83 cur = atom_put(out, cap, cur, "<title>", 7) 84 if cur < 0 { return cur } 85 cur = atom_put_escaped(out, cap, cur, title, title_len) 86 if cur < 0 { return cur } 87 cur = atom_put(out, cap, cur, "</title>\n<link href=\"", 21) 88 if cur < 0 { return cur } 89 cur = atom_put_escaped(out, cap, cur, link, link_len) 90 if cur < 0 { return cur } 91 cur = atom_put(out, cap, cur, "\"/>\n<id>", 8) 92 if cur < 0 { return cur } 93 cur = atom_put_escaped(out, cap, cur, id, id_len) 94 if cur < 0 { return cur } 95 cur = atom_put(out, cap, cur, "</id>\n<updated>", 15) 96 if cur < 0 { return cur } 97 cur = atom_put_iso(out, cap, cur, updated_unix) 98 if cur < 0 { return cur } 99 cur = atom_put(out, cap, cur, 100 "</updated>\n<author><name>", 25) 101 if cur < 0 { return cur } 102 cur = atom_put_escaped(out, cap, cur, author, author_len) 103 if cur < 0 { return cur } 104 cur = atom_put(out, cap, cur, "</name></author>\n", 17) 105 if cur < 0 { return cur } 106 return cur 107} 108 109// Emit one <entry>. 110func atom_entry(out: *u8, cap: i64, off: i64, 111 title: *u8, title_len: i64, 112 link: *u8, link_len: i64, 113 id: *u8, id_len: i64, 114 summary: *u8, summary_len: i64, 115 content: *u8, content_len: i64, 116 updated_unix: i64) -> i64 { 117 var cur: i64 = off 118 cur = atom_put(out, cap, cur, "<entry>\n<title>", 15) 119 if cur < 0 { return cur } 120 cur = atom_put_escaped(out, cap, cur, title, title_len) 121 if cur < 0 { return cur } 122 cur = atom_put(out, cap, cur, "</title>\n<link href=\"", 21) 123 if cur < 0 { return cur } 124 cur = atom_put_escaped(out, cap, cur, link, link_len) 125 if cur < 0 { return cur } 126 cur = atom_put(out, cap, cur, "\"/>\n<id>", 8) 127 if cur < 0 { return cur } 128 cur = atom_put_escaped(out, cap, cur, id, id_len) 129 if cur < 0 { return cur } 130 cur = atom_put(out, cap, cur, "</id>\n<updated>", 15) 131 if cur < 0 { return cur } 132 cur = atom_put_iso(out, cap, cur, updated_unix) 133 if cur < 0 { return cur } 134 cur = atom_put(out, cap, cur, "</updated>\n<summary>", 20) 135 if cur < 0 { return cur } 136 cur = atom_put_escaped(out, cap, cur, summary, summary_len) 137 if cur < 0 { return cur } 138 cur = atom_put(out, cap, cur, 139 "</summary>\n<content type=\"html\"><![CDATA[", 41) 140 if cur < 0 { return cur } 141 cur = atom_put(out, cap, cur, content, content_len) 142 if cur < 0 { return cur } 143 cur = atom_put(out, cap, cur, "]]></content>\n</entry>\n", 23) 144 if cur < 0 { return cur } 145 return cur 146} 147 148func atom_end(out: *u8, cap: i64, off: i64) -> i64 { 149 return atom_put(out, cap, off, "</feed>\n", 8) 150} 151 152// Compile-only smoke. 153func main() -> i64 { 154 let out: *u8 = sys_mmap(4096) 155 var off: i64 = 0 156 157 off = atom_begin(out, 4096, off, 158 "Nishi Notes", 11, 159 "https://nishifamily.com/notes", 29, 160 "tag:nishifamily.com,2026:notes", 30, 161 "Wes Elder", 9, 162 1777000000) 163 if off < 0 { return 1 } 164 165 off = atom_entry(out, 4096, off, 166 "First entry", 11, 167 "https://nishifamily.com/notes/1", 31, 168 "tag:nishifamily.com,2026:note/1", 31, 169 "Short summary", 13, 170 "<p>HTML <em>body</em></p>", 25, 171 1777000000) 172 if off < 0 { return 2 } 173 174 off = atom_end(out, 4096, off) 175 if off < 0 { return 3 } 176 177 // First bytes are \"<?xml\". 178 if out[0] != 0x3C { return 4 } 179 if out[1] != 0x3F { return 5 } 180 return 0 181}