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, "&" as *u8) } else { if c == 0x3c { w(fd, "<" as *u8) } else { if c == 0x3e { w(fd, ">" 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}