code wiki / (root) / nx_sitemap.nx

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}