code wiki / _hdl_build / nx_library_shelf.nx

nx_library_shelf.nx source

↩ module page · 210 lines · 13570 B

1// nx_library_shelf.nx -- SOVEREIGN, ZERO-JAVASCRIPT library shelf (operator: "no js, nishi everywhere"). 2// Enumerates the rendered books under knowledge/staging/media/reader/<slug>/ (getdents64), reads each book.json 3// for title/author/cover, and emits a static HTML+CSS card grid (knowledge/staging/media/library_shelf.html) -- 4// covers shown on the cards (the artwork ingestion now visible), each card a plain <a> to reader_static_<slug>.html. 5// 0 <script>: grid via CSS grid, theme via radio/:checked, no client code. Logic lives in this Nishi organ. 6// expect_exit: 0 license_tier: ORIGINAL 7import "nx_syscalls.nx" 8const K_MAGIC_1024: i64 = 1024 9const K_MAGIC_65536: i64 = 65536 10 11func slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 12func w(fd: i64, s: *u8) -> i64 { sys_write(fd, s, slen(s)); return 0 } 13func wn(fd: i64, v0: i64) -> i64 { var v: i64=v0; if v<0 { sys_write(fd,"-" as *u8,1); v=0-v } let b: *u8=sys_mmap(24); var k: i64=0; if v==0 {b[0]=48 as u8;k=1} while v>0 {b[k]=(48+(v%10)) as u8; v=v/10; k=k+1} let o: *u8=sys_mmap(24); var j: i64=0; while j<k {o[j]=b[k-1-j];j=j+1} sys_write(fd,o,k); return 0 } 14func p(s: *u8) -> i64 { w(1, s); return 0 } 15func wesc(fd: i64, s: *u8) -> i64 { 16 var i: i64 = 0 17 while s[i] != (0 as u8) { 18 let c: i64 = s[i] as i64 19 if c == 0x26 { w(fd, "&amp;" as *u8) } else { if c == 0x3c { w(fd, "&lt;" as *u8) } else { if c == 0x3e { w(fd, "&gt;" as *u8) } else { sys_write(fd, (s as i64 + i) as *u8, 1) } } } 20 i = i + 1 21 } 22 return 0 23} 24func sh_find(hay: *u8, hl: i64, needle: *u8) -> i64 { 25 let nl: i64 = slen(needle); if nl == 0 { return 0-1 } 26 var i: i64 = 0 27 while i + nl <= hl { var k: i64=0; var hit: i64=1; while k<nl { if hay[i+k]!=needle[k]{hit=0;k=nl}else{k=k+1} } if hit==1 {return i} i=i+1 } 28 return 0-1 29} 30// extract "key":"value" (value follows the key's closing quote); unescape \" \\. ret len or -1. 31func sh_valstr(hay: *u8, hl: i64, key: *u8, out: *u8, cap: i64) -> i64 { 32 let at: i64 = sh_find(hay, hl, key) 33 if at < 0 { out[0]=0 as u8; return 0-1 } 34 var i: i64 = at + slen(key) // key ends with opening quote -> i at first value char 35 var o: i64 = 0 36 while i < hl { 37 let c: i64 = hay[i] as i64 38 if c == 0x22 { i = hl } else { if c == 0x5c { if i+1<hl { if o<cap-1 { out[o]=hay[i+1]; o=o+1 } i=i+2 } else { i=i+1 } } else { if o<cap-1 { out[o]=hay[i] as u8; o=o+1 } i=i+1 } } 39 } 40 out[o] = 0 as u8 41 return o 42} 43func ensure_dir(path: *u8) -> i64 { __syscall(258, 0-100, path, 0x1ed, 0, 0, 0); return 0 } 44 45// read knowledge/staging/media/reader/<slug>/book.json -> title/author/cover into the given buffers; ret 1 if ok. 46func load_book(slug: *u8, title: *u8, author: *u8, cover: *u8) -> i64 { 47 let path: *u8 = sys_mmap(K_MAGIC_1024) 48 var l: i64 = 0 49 let pfx: *u8 = "knowledge/staging/media/reader/" as *u8 50 var z: i64=0; while pfx[z]!=(0 as u8){ path[l]=pfx[z]; l=l+1; z=z+1 } 51 z=0; while slug[z]!=(0 as u8){ path[l]=slug[z]; l=l+1; z=z+1 } 52 let sj: *u8 = "/book.json" as *u8; z=0; while sj[z]!=(0 as u8){ path[l]=sj[z]; l=l+1; z=z+1 } path[l]=0 as u8 53 let lp: *i64 = sys_mmap(8) as *i64; lp[0]=0 54 let bj: *u8 = sys_read_file(path, lp) 55 if (bj as i64)==0 { return 0 } 56 let bn: i64 = lp[0] 57 title[0]=0 as u8; sh_valstr(bj, bn, "\"title\":\"" as *u8, title, K_MAGIC_1024) 58 author[0]=0 as u8; sh_valstr(bj, bn, "\"author\":\"" as *u8, author, K_MAGIC_1024) 59 cover[0]=0 as u8; sh_valstr(bj, bn, "\"cover\":\"" as *u8, cover, 512) 60 return 1 61} 62 63func emit_card(fd: i64, slug: *u8, title: *u8, author: *u8, cover: *u8) -> i64 { 64 w(fd, "<a class=\"card\" href=\"reader_static_" as *u8); w(fd, slug); w(fd, ".html\">" as *u8) 65 if cover[0] != (0 as u8) { 66 w(fd, "<img class=\"cv\" src=\"reader/" as *u8); w(fd, slug); w(fd, "/" as *u8); w(fd, cover); w(fd, "\" alt=\"\">" as *u8) 67 } else { 68 w(fd, "<span class=\"cv noart\">" as *u8); wesc(fd, title); w(fd, "</span>" as *u8) 69 } 70 w(fd, "<span class=\"ct\">" as *u8); wesc(fd, title); w(fd, "</span>" as *u8) 71 w(fd, "<span class=\"ca\">" as *u8); wesc(fd, author); w(fd, "</span></a>\n" as *u8) 72 return 0 73} 74 75// an audiobook dir has audio.json (not book.json) -> title/narrator into the buffers; ret 1 if ok. 76func load_audio(slug: *u8, title: *u8, narrator: *u8) -> i64 { 77 let path: *u8 = sys_mmap(K_MAGIC_1024) 78 var l: i64 = 0 79 let pfx: *u8 = "knowledge/staging/media/reader/" as *u8 80 var z: i64=0; while pfx[z]!=(0 as u8){ path[l]=pfx[z]; l=l+1; z=z+1 } 81 z=0; while slug[z]!=(0 as u8){ path[l]=slug[z]; l=l+1; z=z+1 } 82 let sj: *u8 = "/audio.json" as *u8; z=0; while sj[z]!=(0 as u8){ path[l]=sj[z]; l=l+1; z=z+1 } path[l]=0 as u8 83 let lp: *i64 = sys_mmap(8) as *i64; lp[0]=0 84 let bj: *u8 = sys_read_file(path, lp) 85 if (bj as i64)==0 { return 0 } 86 let bn: i64 = lp[0] 87 title[0]=0 as u8; sh_valstr(bj, bn, "\"title\":\"" as *u8, title, K_MAGIC_1024) 88 narrator[0]=0 as u8; sh_valstr(bj, bn, "\"narrator\":\"" as *u8, narrator, K_MAGIC_1024) 89 return 1 90} 91// audiobook card: a headphones glyph cover, links to the zero-JS player.html (nx_audio_render) for that slug. 92func emit_audio_card(fd: i64, slug: *u8, title: *u8, narrator: *u8) -> i64 { 93 w(fd, "<a class=\"card\" href=\"reader/" as *u8); w(fd, slug); w(fd, "/player.html\">" as *u8) 94 w(fd, "<span class=\"cv noart\" style=\"font-size:2.4rem;background:#2a2218;color:#c89b5a\">\xf0\x9f\x8e\xa7</span>" as *u8) 95 w(fd, "<span class=\"ct\">" as *u8); wesc(fd, title); w(fd, "</span>" as *u8) 96 w(fd, "<span class=\"ca\">\xf0\x9f\x8e\xa7 " as *u8); wesc(fd, narrator); w(fd, "</span></a>\n" as *u8) 97 return 0 98} 99 100func sh_streq(a: *u8, b: *u8) -> i64 { var i: i64=0; while a[i]!=(0 as u8){ if a[i]!=b[i]{return 0} i=i+1 } if b[i]!=(0 as u8){return 0} return 1 } 101// curated-shelf filter: is slug `nm` listed (one slug per line) in the curate-list buffer? 102func in_list(nm: *u8, list: *u8, listn: i64) -> i64 { 103 var nl: i64 = 0; while nm[nl]!=(0 as u8){nl=nl+1} 104 var i: i64 = 0 105 while i < listn { 106 var e: i64 = i; var go: i64 = 1 107 while go == 1 { if e >= listn { go = 0 } else { if list[e] == (0x0a as u8) { go = 0 } else { e = e + 1 } } } 108 let llen: i64 = e - i 109 if llen == nl { var k: i64=0; var hit: i64=1; while k<nl { if list[i+k]!=nm[k]{hit=0;k=nl}else{k=k+1} } if hit==1 { return 1 } } 110 i = e + 1 111 } 112 return 0 113} 114 115func main(argc: i64, argv: *i64) -> i64 { 116 ensure_dir("knowledge/staging/media" as *u8) 117 var curated: i64 = 0 118 if argc >= 2 { if sh_streq(argv[1] as *u8, "curate" as *u8) == 1 { curated = 1 } } // `curate` -> emit ONLY books_curated.txt slugs to a SEPARATE file (default + gates keep the full shelf) 119 var out: *u8 = "knowledge/staging/media/library_shelf.html" as *u8 120 if curated == 1 { out = "knowledge/staging/media/library_shelf_curated.html" as *u8 } 121 let fd: i64 = sys_openat_wr(out, 0x1a4) 122 if fd < 0 { p("LIBRARY-SHELF FAIL open out\n" as *u8); sys_exit(1); return 1 } 123 124 w(fd, "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">\n" as *u8) 125 w(fd, "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" as *u8) 126 w(fd, "<title>Nishi Library</title>\n<style>\n" as *u8) 127 w(fd, "*{box-sizing:border-box} html,body{margin:0}\n" as *u8) 128 w(fd, ".thr{position:absolute;width:0;height:0;opacity:0;pointer-events:none}\n" as *u8) 129 w(fd, ".app{--bg:#fbfbf9;--fg:#1d1d1b;--muted:#6b6b66;--rule:#e3e3dd;--sheet:#fff;background:var(--bg);color:var(--fg);min-height:100vh;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif}\n" as *u8) 130 w(fd, "#th-dark:checked~.app{--bg:#15161a;--fg:#d8d8d2;--muted:#8d8d88;--rule:#2a2c33;--sheet:#1e2027}\n" as *u8) 131 w(fd, "#th-sepia:checked~.app{--bg:#f4ecd8;--fg:#43391f;--muted:#8a7a55;--rule:#e0d4b6;--sheet:#fbf4e3}\n" as *u8) 132 w(fd, ".bar{position:sticky;top:0;display:flex;gap:.6rem;align-items:center;padding:.7rem 1rem;background:var(--sheet);border-bottom:1px solid var(--rule)}\n" as *u8) 133 w(fd, ".bar h1{font-size:1.1rem;margin:0;flex:1}\n" as *u8) 134 w(fd, ".tbar label{cursor:pointer;border:1px solid var(--rule);border-radius:8px;padding:.25rem .55rem;font-size:13px;color:var(--fg)}\n" as *u8) 135 w(fd, ".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:1.2rem;padding:1.4rem;max-width:1100px;margin:0 auto}\n" as *u8) 136 w(fd, ".card{display:flex;flex-direction:column;text-decoration:none;color:var(--fg)}\n" as *u8) 137 w(fd, ".cv{width:100%;aspect-ratio:2/3;object-fit:cover;border-radius:6px;box-shadow:0 6px 20px rgba(0,0,0,.22);background:var(--sheet)}\n" as *u8) 138 w(fd, ".noart{display:flex;align-items:center;justify-content:center;text-align:center;padding:1rem;font-size:.9rem;border:1px solid var(--rule)}\n" as *u8) 139 w(fd, ".ct{font-weight:600;font-size:.92rem;margin:.5rem 0 .1rem;line-height:1.25}\n" as *u8) 140 w(fd, ".ca{color:var(--muted);font-size:.82rem} .empty{padding:3rem;text-align:center;color:var(--muted)}\n" as *u8) 141 w(fd, ".skip{position:absolute;left:-999px;top:0;background:#7a5cff;color:#fff;padding:.5rem .8rem;z-index:99} .skip:focus{left:0}\n" as *u8) 142 w(fd, "@media(pointer:coarse){.tbar label{min-height:44px;display:flex;align-items:center;justify-content:center}}\n" as *u8) // S-class a11y: >=44px touch targets 143 w(fd, ":focus-visible{outline:3px solid #7a5cff;outline-offset:2px;border-radius:3px}\n" as *u8) // WCAG 2.4.7: visible keyboard focus 144 w(fd, "@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}\n" as *u8) // WCAG 2.3.3 145 w(fd, "</style></head>\n<body>\n" as *u8) 146 w(fd, "<a class=\"skip\" href=\"#main\">Skip to content</a>\n" as *u8) 147 w(fd, "<input class=\"thr\" type=\"radio\" name=\"nxtheme\" id=\"th-light\" checked>\n" as *u8) 148 w(fd, "<input class=\"thr\" type=\"radio\" name=\"nxtheme\" id=\"th-sepia\">\n" as *u8) 149 w(fd, "<input class=\"thr\" type=\"radio\" name=\"nxtheme\" id=\"th-dark\">\n" as *u8) 150 w(fd, "<div class=\"app\">\n<header class=\"bar\"><h1>\xf0\x9f\x93\x9a Nishi Library</h1>\n" as *u8) 151 w(fd, "<span class=\"tbar\" role=\"group\" aria-label=\"Theme\"><label for=\"th-light\">Light</label><label for=\"th-sepia\">Sepia</label><label for=\"th-dark\">Dark</label></span></header>\n" as *u8) 152 w(fd, "<main class=\"grid\" id=\"main\" aria-label=\"Library\">\n" as *u8) 153 154 // ===== optional CURATE list: knowledge/staging/media/books_curated.txt (one slug/line) -> emit ONLY those ===== 155 let curlp: *i64 = sys_mmap(8) as *i64; curlp[0]=0 156 var curbuf: *u8 = 0 as *u8; var curn: i64 = 0 157 if curated == 1 { curbuf = sys_read_file("knowledge/staging/media/books_curated.txt\x00" as *u8, curlp); curn = curlp[0] } 158 159 // ===== enumerate slug subdirs under reader/ via getdents64 ===== 160 let dirp: *u8 = "knowledge/staging/media/reader\x00" as *u8 161 let dfd: i64 = __syscall(257, 0-100, dirp, 0x10000, 0, 0, 0) // openat AT_FDCWD O_RDONLY|O_DIRECTORY 162 var nbooks: i64 = 0 163 var naudio: i64 = 0 164 let title: *u8 = sys_mmap(K_MAGIC_1024) 165 let author: *u8 = sys_mmap(K_MAGIC_1024) 166 let cover: *u8 = sys_mmap(512) 167 let nm: *u8 = sys_mmap(512) 168 if dfd >= 0 { 169 let buf: *u8 = sys_mmap(K_MAGIC_65536) 170 var go: i64 = 1 171 while go == 1 { 172 let nread: i64 = __syscall(217, dfd, buf, K_MAGIC_65536, 0, 0, 0) // getdents64 173 if nread <= 0 { go = 0 } else { 174 var pos: i64 = 0 175 while pos < nread { 176 let reclen: i64 = (buf[pos+16] as i64) | ((buf[pos+17] as i64) << 8) 177 let dtype: i64 = buf[pos+18] as i64 178 // name at pos+19 (NUL-term) 179 var nl: i64 = 0 180 while buf[pos+19+nl] != (0 as u8) { nm[nl]=buf[pos+19+nl]; nl=nl+1 } nm[nl]=0 as u8 181 var skip: i64 = 0 182 if nl == 0 { skip = 1 } 183 if nl == 1 { if nm[0]==(0x2e as u8) { skip = 1 } } // "." 184 if nl == 2 { if nm[0]==(0x2e as u8) { if nm[1]==(0x2e as u8) { skip=1 } } } // ".." 185 if dtype != 4 { skip = 1 } // DT_DIR only 186 if curated == 1 { if in_list(nm, curbuf, curn) == 0 { skip = 1 } } // curated mode: only listed slugs 187 if skip == 0 { 188 if load_book(nm, title, author, cover) == 1 { 189 emit_card(fd, nm, title, author, cover) 190 nbooks = nbooks + 1 191 } else { 192 if load_audio(nm, title, author) == 1 { // author buffer reused for narrator 193 emit_audio_card(fd, nm, title, author) 194 naudio = naudio + 1 195 } 196 } 197 } 198 if reclen <= 0 { pos = nread } else { pos = pos + reclen } 199 } 200 } 201 } 202 sys_close(dfd) 203 } 204 if nbooks + naudio == 0 { w(fd, "<p class=\"empty\">No books rendered yet. Run nx_reader_render / nx_audio_render for a slug.</p>\n" as *u8) } 205 206 w(fd, "</main>\n</div>\n</body></html>\n" as *u8) 207 sys_close(fd) 208 p("LIBRARY-SHELF ok books=" as *u8); wn(1, nbooks); p(" audiobooks=" as *u8); wn(1, naudio); p(" -> knowledge/staging/media/library_shelf.html [ZERO JS]\n" as *u8) 209 sys_exit(0); return 0 210}