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}