nx_sitemap.nx source
↩ module page · 201 lines · 7098 B
1// sitemap.nx -- XML Sitemap 0.9 builder.
2//
3// Specification: sitemaps.org (Google/Yahoo/MSN 2005 joint draft).
4// Every search crawler uses it to discover pages it might not
5// reach via link-following. Referenced from robots.txt and served
6// at /sitemap.xml or /sitemap-index.xml.
7//
8// Skeleton:
9// <?xml version=\"1.0\" encoding=\"utf-8\"?>
10// <urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">
11// <url>
12// <loc>https://example.com/foo</loc>
13// <lastmod>2026-04-22</lastmod>
14// <changefreq>weekly</changefreq>
15// <priority>0.8</priority>
16// </url>
17// ...
18// </urlset>
19//
20// Soft caps (per spec): 50,000 URLs per sitemap, 50 MiB uncompressed.
21// For larger sites use a sitemap index (not implemented here yet;
22// a future sitemap_index.nx will compose this).
23//
24// Invariants:
25// S1 loc is required; lastmod/changefreq/priority optional.
26// S2 lastmod is date-only format YYYY-MM-DD (from unix_sec
27// via civil_date.nx). Sitemaps accept ISO 8601 but
28// date-only is canonical and smaller.
29// S3 changefreq values: always, hourly, daily, weekly, monthly,
30// yearly, never. We surface them as constants; caller picks.
31// S4 Stream builder: sitemap_begin -> sitemap_url* -> sitemap_end.
32
33// nx_safety_envelope:
34// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
35// sil_target: SIL1
36// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
37// verdict: NOT_YET_EVALUATED
38
39import "nx_syscalls.nx"
40import "nx_html_escape.nx"
41import "nx_civil_date.nx"
42const SM_MAGIC_4096: i64 = 4096
43const SM_MAGIC_1777000000: i64 = 1777000000
44
45const SM_ERR_SHORT: i64 = -1
46
47const SM_FREQ_ALWAYS: i64 = 1
48const SM_FREQ_HOURLY: i64 = 2
49const SM_FREQ_DAILY: i64 = 3
50const SM_FREQ_WEEKLY: i64 = 4
51const SM_FREQ_MONTHLY: i64 = 5
52const SM_FREQ_YEARLY: i64 = 6
53const SM_FREQ_NEVER: i64 = 7
54const SM_FREQ_NONE: i64 = 0
55
56func sm_put(out: *u8, cap: i64, off: i64, src: *u8, n: i64) -> i64 {
57 if off + n > cap { return SM_ERR_SHORT }
58 var i: i64 = 0
59 while i < n {
60 out[off + i] = src[i]
61 i = i + 1
62 }
63 return off + n
64}
65
66func sm_put_escaped(out: *u8, cap: i64, off: i64,
67 src: *u8, n: i64) -> i64 {
68 let w: i64 = html_escape(out + off, cap - off, src, n)
69 if w < 0 { return SM_ERR_SHORT }
70 return off + w
71}
72
73// Write 4-digit year, 2-digit month, 2-digit day: YYYY-MM-DD.
74func sm_put_date(out: *u8, cap: i64, off: i64, unix_sec: i64) -> i64 {
75 if off + 10 > cap { return SM_ERR_SHORT }
76 let cd_raw: *u8 = sys_mmap(128)
77 let cd: *CivilDate = cd_raw as *CivilDate
78 civil_from_unix(unix_sec, cd)
79 out[off] = 0x30 + ((cd.year / 1000) % 10)
80 out[off + 1] = 0x30 + ((cd.year / 100) % 10)
81 out[off + 2] = 0x30 + ((cd.year / 10) % 10)
82 out[off + 3] = 0x30 + (cd.year % 10)
83 out[off + 4] = 0x2D
84 out[off + 5] = 0x30 + ((cd.month / 10) % 10)
85 out[off + 6] = 0x30 + (cd.month % 10)
86 out[off + 7] = 0x2D
87 out[off + 8] = 0x30 + ((cd.day / 10) % 10)
88 out[off + 9] = 0x30 + (cd.day % 10)
89 return off + 10
90}
91
92// Write a changefreq value based on SM_FREQ_*. Returns new
93// offset or unchanged if freq == SM_FREQ_NONE.
94func sm_put_freq(out: *u8, cap: i64, off: i64, freq: i64) -> i64 {
95 if freq == SM_FREQ_NONE { return off }
96 var cur: i64 = off
97 cur = sm_put(out, cap, cur, "<changefreq>", 12)
98 if cur < 0 { return cur }
99 if freq == SM_FREQ_ALWAYS { cur = sm_put(out, cap, cur, "always", 6) }
100 if freq == SM_FREQ_HOURLY { cur = sm_put(out, cap, cur, "hourly", 6) }
101 if freq == SM_FREQ_DAILY { cur = sm_put(out, cap, cur, "daily", 5) }
102 if freq == SM_FREQ_WEEKLY { cur = sm_put(out, cap, cur, "weekly", 6) }
103 if freq == SM_FREQ_MONTHLY { cur = sm_put(out, cap, cur, "monthly", 7) }
104 if freq == SM_FREQ_YEARLY { cur = sm_put(out, cap, cur, "yearly", 6) }
105 if freq == SM_FREQ_NEVER { cur = sm_put(out, cap, cur, "never", 5) }
106 if cur < 0 { return cur }
107 cur = sm_put(out, cap, cur, "</changefreq>\n", 14)
108 return cur
109}
110
111// Emit opening + urlset.
112func sitemap_begin(out: *u8, cap: i64, off: i64) -> i64 {
113 var cur: i64 = off
114 cur = sm_put(out, cap, cur,
115 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n", 38)
116 if cur < 0 { return cur }
117 cur = sm_put(out, cap, cur,
118 "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n", 62)
119 return cur
120}
121
122// Emit one <url>. Pass -1 for lastmod_unix to omit it.
123// Pass SM_FREQ_NONE for freq to omit changefreq.
124// Pass -1 for priority_tenths to omit. Priority is an integer
125// 0..10 (tenths of a unit); written as 0.N.
126func sitemap_url(out: *u8, cap: i64, off: i64,
127 loc: *u8, loc_len: i64,
128 lastmod_unix: i64,
129 freq: i64,
130 priority_tenths: i64) -> i64 {
131 var cur: i64 = off
132 cur = sm_put(out, cap, cur, "<url>\n<loc>", 11)
133 if cur < 0 { return cur }
134 cur = sm_put_escaped(out, cap, cur, loc, loc_len)
135 if cur < 0 { return cur }
136 cur = sm_put(out, cap, cur, "</loc>\n", 7)
137 if cur < 0 { return cur }
138
139 if lastmod_unix >= 0 {
140 cur = sm_put(out, cap, cur, "<lastmod>", 9)
141 if cur < 0 { return cur }
142 cur = sm_put_date(out, cap, cur, lastmod_unix)
143 if cur < 0 { return cur }
144 cur = sm_put(out, cap, cur, "</lastmod>\n", 11)
145 if cur < 0 { return cur }
146 }
147
148 cur = sm_put_freq(out, cap, cur, freq)
149 if cur < 0 { return cur }
150
151 if priority_tenths >= 0 {
152 if priority_tenths <= 10 {
153 cur = sm_put(out, cap, cur, "<priority>", 10)
154 if cur < 0 { return cur }
155 if priority_tenths == 10 {
156 cur = sm_put(out, cap, cur, "1.0", 3)
157 } else {
158 if cur + 3 > cap { return SM_ERR_SHORT }
159 out[cur] = 0x30
160 out[cur + 1] = 0x2E
161 out[cur + 2] = 0x30 + priority_tenths
162 cur = cur + 3
163 }
164 if cur < 0 { return cur }
165 cur = sm_put(out, cap, cur, "</priority>\n", 12)
166 if cur < 0 { return cur }
167 }
168 }
169
170 cur = sm_put(out, cap, cur, "</url>\n", 7)
171 return cur
172}
173
174func sitemap_end(out: *u8, cap: i64, off: i64) -> i64 {
175 return sm_put(out, cap, off, "</urlset>\n", 10)
176}
177
178// Compile-only smoke.
179func main() -> i64 {
180 let out: *u8 = sys_mmap(SM_MAGIC_4096)
181 var off: i64 = 0
182 off = sitemap_begin(out, SM_MAGIC_4096, off)
183 if off < 0 { return 1 }
184
185 off = sitemap_url(out, SM_MAGIC_4096, off,
186 "https://nishifamily.com/", 24,
187 SM_MAGIC_1777000000, SM_FREQ_WEEKLY, 10)
188 if off < 0 { return 2 }
189
190 off = sitemap_url(out, SM_MAGIC_4096, off,
191 "https://nishifamily.com/blog/1", 30,
192 SM_MAGIC_1777000000, SM_FREQ_MONTHLY, 8)
193 if off < 0 { return 3 }
194
195 off = sitemap_end(out, SM_MAGIC_4096, off)
196 if off < 0 { return 4 }
197
198 if out[0] != 0x3C { return 5 }
199 if out[1] != 0x3F { return 6 }
200 return 0
201}