code wiki / (root) / nx_atom_v1.nx

nx_atom_v1.nx source

↩ module page · 189 lines · 6748 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 37// nx_safety_envelope: 38// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 39// sil_target: SIL1 40// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 41// verdict: NOT_YET_EVALUATED 42 43import "nx_syscalls.nx" 44import "nx_html_escape.nx" 45import "nx_iso8601.nx" 46const ATOM_MAGIC_4096: i64 = 4096 47const ATOM_MAGIC_1777000000: i64 = 1777000000 48 49const ATOM_ERR_SHORT: i64 = -1 50 51func atom_put(out: *u8, cap: i64, off: i64, 52 src: *u8, n: i64) -> i64 { 53 if off + n > cap { return ATOM_ERR_SHORT } 54 var i: i64 = 0 55 while i < n { 56 out[off + i] = src[i] 57 i = i + 1 58 } 59 return off + n 60} 61 62func atom_put_escaped(out: *u8, cap: i64, off: i64, 63 src: *u8, n: i64) -> i64 { 64 let w: i64 = html_escape(out + off, cap - off, src, n) 65 if w < 0 { return ATOM_ERR_SHORT } 66 return off + w 67} 68 69// 20-byte ISO 8601 timestamp. 70func atom_put_iso(out: *u8, cap: i64, off: i64, unix_sec: i64) -> i64 { 71 if off + 20 > cap { return ATOM_ERR_SHORT } 72 iso_format_unix(out + off, cap - off, unix_sec) 73 return off + 20 74} 75 76// Open feed + emit channel metadata. 77func atom_begin(out: *u8, cap: i64, off: i64, 78 title: *u8, title_len: i64, 79 link: *u8, link_len: i64, 80 id: *u8, id_len: i64, 81 author: *u8, author_len: i64, 82 updated_unix: i64) -> i64 { 83 var cur: i64 = off 84 cur = atom_put(out, cap, cur, 85 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n", 38) 86 if cur < 0 { return cur } 87 cur = atom_put(out, cap, cur, 88 "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n", 43) 89 if cur < 0 { return cur } 90 91 cur = atom_put(out, cap, cur, "<title>", 7) 92 if cur < 0 { return cur } 93 cur = atom_put_escaped(out, cap, cur, title, title_len) 94 if cur < 0 { return cur } 95 cur = atom_put(out, cap, cur, "</title>\n<link href=\"", 21) 96 if cur < 0 { return cur } 97 cur = atom_put_escaped(out, cap, cur, link, link_len) 98 if cur < 0 { return cur } 99 cur = atom_put(out, cap, cur, "\"/>\n<id>", 8) 100 if cur < 0 { return cur } 101 cur = atom_put_escaped(out, cap, cur, id, id_len) 102 if cur < 0 { return cur } 103 cur = atom_put(out, cap, cur, "</id>\n<updated>", 15) 104 if cur < 0 { return cur } 105 cur = atom_put_iso(out, cap, cur, updated_unix) 106 if cur < 0 { return cur } 107 cur = atom_put(out, cap, cur, 108 "</updated>\n<author><name>", 25) 109 if cur < 0 { return cur } 110 cur = atom_put_escaped(out, cap, cur, author, author_len) 111 if cur < 0 { return cur } 112 cur = atom_put(out, cap, cur, "</name></author>\n", 17) 113 if cur < 0 { return cur } 114 return cur 115} 116 117// Emit one <entry>. 118func atom_entry(out: *u8, cap: i64, off: i64, 119 title: *u8, title_len: i64, 120 link: *u8, link_len: i64, 121 id: *u8, id_len: i64, 122 summary: *u8, summary_len: i64, 123 content: *u8, content_len: i64, 124 updated_unix: i64) -> i64 { 125 var cur: i64 = off 126 cur = atom_put(out, cap, cur, "<entry>\n<title>", 15) 127 if cur < 0 { return cur } 128 cur = atom_put_escaped(out, cap, cur, title, title_len) 129 if cur < 0 { return cur } 130 cur = atom_put(out, cap, cur, "</title>\n<link href=\"", 21) 131 if cur < 0 { return cur } 132 cur = atom_put_escaped(out, cap, cur, link, link_len) 133 if cur < 0 { return cur } 134 cur = atom_put(out, cap, cur, "\"/>\n<id>", 8) 135 if cur < 0 { return cur } 136 cur = atom_put_escaped(out, cap, cur, id, id_len) 137 if cur < 0 { return cur } 138 cur = atom_put(out, cap, cur, "</id>\n<updated>", 15) 139 if cur < 0 { return cur } 140 cur = atom_put_iso(out, cap, cur, updated_unix) 141 if cur < 0 { return cur } 142 cur = atom_put(out, cap, cur, "</updated>\n<summary>", 20) 143 if cur < 0 { return cur } 144 cur = atom_put_escaped(out, cap, cur, summary, summary_len) 145 if cur < 0 { return cur } 146 cur = atom_put(out, cap, cur, 147 "</summary>\n<content type=\"html\"><![CDATA[", 41) 148 if cur < 0 { return cur } 149 cur = atom_put(out, cap, cur, content, content_len) 150 if cur < 0 { return cur } 151 cur = atom_put(out, cap, cur, "]]></content>\n</entry>\n", 23) 152 if cur < 0 { return cur } 153 return cur 154} 155 156func atom_end(out: *u8, cap: i64, off: i64) -> i64 { 157 return atom_put(out, cap, off, "</feed>\n", 8) 158} 159 160// Compile-only smoke. 161func main() -> i64 { 162 let out: *u8 = sys_mmap(ATOM_MAGIC_4096) 163 var off: i64 = 0 164 165 off = atom_begin(out, ATOM_MAGIC_4096, off, 166 "Nishi Notes", 11, 167 "https://nishifamily.com/notes", 29, 168 "tag:nishifamily.com,2026:notes", 30, 169 "Wes Elder", 9, 170 ATOM_MAGIC_1777000000) 171 if off < 0 { return 1 } 172 173 off = atom_entry(out, ATOM_MAGIC_4096, off, 174 "First entry", 11, 175 "https://nishifamily.com/notes/1", 31, 176 "tag:nishifamily.com,2026:note/1", 31, 177 "Short summary", 13, 178 "<p>HTML <em>body</em></p>", 25, 179 ATOM_MAGIC_1777000000) 180 if off < 0 { return 2 } 181 182 off = atom_end(out, ATOM_MAGIC_4096, off) 183 if off < 0 { return 3 } 184 185 // First bytes are \"<?xml\". 186 if out[0] != 0x3C { return 4 } 187 if out[1] != 0x3F { return 5 } 188 return 0 189}