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}